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