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