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