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