0ab684
From f46bac1f3e8634e24c747d06b28e11b874f1e488 Mon Sep 17 00:00:00 2001
0ab684
From: Kazuki Yamaguchi <k@rhe.jp>
0ab684
Date: Thu, 16 Aug 2018 19:40:48 +0900
0ab684
Subject: [PATCH] config: support .include directive
0ab684
0ab684
OpenSSL 1.1.1 introduces a new '.include' directive. Update our config
0ab684
parser to support that.
0ab684
0ab684
As mentioned in the referenced GitHub issue, we should use the OpenSSL
0ab684
API instead of implementing the parsing logic ourselves, but it will
0ab684
need backwards-incompatible changes which we can't backport to stable
0ab684
versions. So continue to use the Ruby implementation for now.
0ab684
0ab684
Reference: https://github.com/ruby/openssl/issues/208
0ab684
---
0ab684
 ext/openssl/lib/openssl/config.rb | 54 ++++++++++++++++++++++++++++---------------
0ab684
 test/openssl/test_config.rb   | 54 +++++++++++++++++++++++++++++++++++++++++++
0ab684
 2 files changed, 90 insertions(+), 18 deletions(-)
0ab684
0ab684
diff --git a/ext/openssl/lib/openssl/config.rb b/ext/openssl/lib/openssl/config.rb
0ab684
index 88225451..ba3a54c8 100644
0ab684
--- a/ext/openssl/lib/openssl/config.rb
0ab684
+++ b/ext/openssl/lib/openssl/config.rb
0ab684
@@ -77,29 +77,44 @@ def get_key_string(data, section, key) # :nodoc:
0ab684
       def parse_config_lines(io)
0ab684
         section = 'default'
0ab684
         data = {section => {}}
0ab684
-        while definition = get_definition(io)
0ab684
+        io_stack = [io]
0ab684
+        while definition = get_definition(io_stack)
0ab684
           definition = clear_comments(definition)
0ab684
           next if definition.empty?
0ab684
-          if definition[0] == ?[
0ab684
+          case definition
0ab684
+          when /\A\[/
0ab684
             if /\[([^\]]*)\]/ =~ definition
0ab684
               section = $1.strip
0ab684
               data[section] ||= {}
0ab684
             else
0ab684
               raise ConfigError, "missing close square bracket"
0ab684
             end
0ab684
-          else
0ab684
-            if /\A([^:\s]*)(?:::([^:\s]*))?\s*=(.*)\z/ =~ definition
0ab684
-              if $2
0ab684
-                section = $1
0ab684
-                key = $2
0ab684
-              else
0ab684
-                key = $1
0ab684
+          when /\A\.include (.+)\z/
0ab684
+            path = $1
0ab684
+            if File.directory?(path)
0ab684
+              files = Dir.glob(File.join(path, "*.{cnf,conf}"), File::FNM_EXTGLOB)
0ab684
+            else
0ab684
+              files = [path]
0ab684
+            end
0ab684
+
0ab684
+            files.each do |filename|
0ab684
+              begin
0ab684
+                io_stack << StringIO.new(File.read(filename))
0ab684
+              rescue
0ab684
+                raise ConfigError, "could not include file '%s'" % filename
0ab684
               end
0ab684
-              value = unescape_value(data, section, $3)
0ab684
-              (data[section] ||= {})[key] = value.strip
0ab684
+            end
0ab684
+          when /\A([^:\s]*)(?:::([^:\s]*))?\s*=(.*)\z/
0ab684
+            if $2
0ab684
+              section = $1
0ab684
+              key = $2
0ab684
             else
0ab684
-              raise ConfigError, "missing equal sign"
0ab684
+              key = $1
0ab684
             end
0ab684
+            value = unescape_value(data, section, $3)
0ab684
+            (data[section] ||= {})[key] = value.strip
0ab684
+          else
0ab684
+            raise ConfigError, "missing equal sign"
0ab684
           end
0ab684
         end
0ab684
         data
0ab684
@@ -212,10 +227,10 @@ def clear_comments(line)
0ab684
         scanned.join
0ab684
       end
0ab684
 
0ab684
-      def get_definition(io)
0ab684
-        if line = get_line(io)
0ab684
+      def get_definition(io_stack)
0ab684
+        if line = get_line(io_stack)
0ab684
           while /[^\\]\\\z/ =~ line
0ab684
-            if extra = get_line(io)
0ab684
+            if extra = get_line(io_stack)
0ab684
               line += extra
0ab684
             else
0ab684
               break
0ab684
@@ -225,9 +240,12 @@ def get_definition(io)
0ab684
         end
0ab684
       end
0ab684
 
0ab684
-      def get_line(io)
0ab684
-        if line = io.gets
0ab684
-          line.gsub(/[\r\n]*/, '')
0ab684
+      def get_line(io_stack)
0ab684
+        while io = io_stack.last
0ab684
+          if line = io.gets
0ab684
+            return line.gsub(/[\r\n]*/, '')
0ab684
+          end
0ab684
+          io_stack.pop
0ab684
         end
0ab684
       end
0ab684
     end
0ab684
diff --git a/test/openssl/test_config.rb b/test/openssl/test_config.rb
0ab684
index 99dcc497..5653b5d0 100644
0ab684
--- a/test/openssl/test_config.rb
0ab684
+++ b/test/openssl/test_config.rb
0ab684
@@ -120,6 +120,49 @@ def test_s_parse_format
0ab684
     assert_equal("error in line 7: missing close square bracket", excn.message)
0ab684
   end
0ab684
 
0ab684
+  def test_s_parse_include
0ab684
+    in_tmpdir("ossl-config-include-test") do |dir|
0ab684
+      Dir.mkdir("child")
0ab684
+      File.write("child/a.conf", <<~__EOC__)
0ab684
+        [default]
0ab684
+        file-a = a.conf
0ab684
+        [sec-a]
0ab684
+        a = 123
0ab684
+      __EOC__
0ab684
+      File.write("child/b.cnf", <<~__EOC__)
0ab684
+        [default]
0ab684
+        file-b = b.cnf
0ab684
+        [sec-b]
0ab684
+        b = 123
0ab684
+      __EOC__
0ab684
+      File.write("include-child.conf", <<~__EOC__)
0ab684
+        key_outside_section = value_a
0ab684
+        .include child
0ab684
+      __EOC__
0ab684
+
0ab684
+      include_file = <<~__EOC__
0ab684
+        [default]
0ab684
+        file-main = unnamed
0ab684
+        [sec-main]
0ab684
+        main = 123
0ab684
+        .include include-child.conf
0ab684
+      __EOC__
0ab684
+
0ab684
+      # Include a file by relative path
0ab684
+      c1 = OpenSSL::Config.parse(include_file)
0ab684
+      assert_equal(["default", "sec-a", "sec-b", "sec-main"], c1.sections.sort)
0ab684
+      assert_equal(["file-main", "file-a", "file-b"], c1["default"].keys)
0ab684
+      assert_equal({"a" => "123"}, c1["sec-a"])
0ab684
+      assert_equal({"b" => "123"}, c1["sec-b"])
0ab684
+      assert_equal({"main" => "123", "key_outside_section" => "value_a"}, c1["sec-main"])
0ab684
+
0ab684
+      # Relative paths are from the working directory
0ab684
+      assert_raise(OpenSSL::ConfigError) do
0ab684
+        Dir.chdir("child") { OpenSSL::Config.parse(include_file) }
0ab684
+      end
0ab684
+    end
0ab684
+  end
0ab684
+
0ab684
   def test_s_load
0ab684
     # alias of new
0ab684
     c = OpenSSL::Config.load
0ab684
@@ -299,6 +342,17 @@ def test_clone
0ab684
     @it['newsection'] = {'a' => 'b'}
0ab684
     assert_not_equal(@it.sections.sort, c.sections.sort)
0ab684
   end
0ab684
+
0ab684
+  private
0ab684
+
0ab684
+  def in_tmpdir(*args)
0ab684
+    Dir.mktmpdir(*args) do |dir|
0ab684
+      dir = File.realpath(dir)
0ab684
+      Dir.chdir(dir) do
0ab684
+        yield dir
0ab684
+      end
0ab684
+    end
0ab684
+  end
0ab684
 end
0ab684
 
0ab684
 end