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