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