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