Skip to content

Commit f46bac1

Browse files
committed
config: support .include directive
OpenSSL 1.1.1 introduces a new '.include' directive. Update our config parser to support that. As mentioned in the referenced GitHub issue, we should use the OpenSSL API instead of implementing the parsing logic ourselves, but it will need backwards-incompatible changes which we can't backport to stable versions. So continue to use the Ruby implementation for now. Reference: #208
1 parent 307db49 commit f46bac1

File tree

2 files changed

+90
-18
lines changed

2 files changed

+90
-18
lines changed

lib/openssl/config.rb

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -76,29 +76,44 @@ def get_key_string(data, section, key) # :nodoc:
7676
def parse_config_lines(io)
7777
section = 'default'
7878
data = {section => {}}
79-
while definition = get_definition(io)
79+
io_stack = [io]
80+
while definition = get_definition(io_stack)
8081
definition = clear_comments(definition)
8182
next if definition.empty?
82-
if definition[0] == ?[
83+
case definition
84+
when /\A\[/
8385
if /\[([^\]]*)\]/ =~ definition
8486
section = $1.strip
8587
data[section] ||= {}
8688
else
8789
raise ConfigError, "missing close square bracket"
8890
end
89-
else
90-
if /\A([^:\s]*)(?:::([^:\s]*))?\s*=(.*)\z/ =~ definition
91-
if $2
92-
section = $1
93-
key = $2
94-
else
95-
key = $1
91+
when /\A\.include (.+)\z/
92+
path = $1
93+
if File.directory?(path)
94+
files = Dir.glob(File.join(path, "*.{cnf,conf}"), File::FNM_EXTGLOB)
95+
else
96+
files = [path]
97+
end
98+
99+
files.each do |filename|
100+
begin
101+
io_stack << StringIO.new(File.read(filename))
102+
rescue
103+
raise ConfigError, "could not include file '%s'" % filename
96104
end
97-
value = unescape_value(data, section, $3)
98-
(data[section] ||= {})[key] = value.strip
105+
end
106+
when /\A([^:\s]*)(?:::([^:\s]*))?\s*=(.*)\z/
107+
if $2
108+
section = $1
109+
key = $2
99110
else
100-
raise ConfigError, "missing equal sign"
111+
key = $1
101112
end
113+
value = unescape_value(data, section, $3)
114+
(data[section] ||= {})[key] = value.strip
115+
else
116+
raise ConfigError, "missing equal sign"
102117
end
103118
end
104119
data
@@ -211,10 +226,10 @@ def clear_comments(line)
211226
scanned.join
212227
end
213228

214-
def get_definition(io)
215-
if line = get_line(io)
229+
def get_definition(io_stack)
230+
if line = get_line(io_stack)
216231
while /[^\\]\\\z/ =~ line
217-
if extra = get_line(io)
232+
if extra = get_line(io_stack)
218233
line += extra
219234
else
220235
break
@@ -224,9 +239,12 @@ def get_definition(io)
224239
end
225240
end
226241

227-
def get_line(io)
228-
if line = io.gets
229-
line.gsub(/[\r\n]*/, '')
242+
def get_line(io_stack)
243+
while io = io_stack.last
244+
if line = io.gets
245+
return line.gsub(/[\r\n]*/, '')
246+
end
247+
io_stack.pop
230248
end
231249
end
232250
end

test/test_config.rb

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,49 @@ def test_s_parse_format
120120
assert_equal("error in line 7: missing close square bracket", excn.message)
121121
end
122122

123+
def test_s_parse_include
124+
in_tmpdir("ossl-config-include-test") do |dir|
125+
Dir.mkdir("child")
126+
File.write("child/a.conf", <<~__EOC__)
127+
[default]
128+
file-a = a.conf
129+
[sec-a]
130+
a = 123
131+
__EOC__
132+
File.write("child/b.cnf", <<~__EOC__)
133+
[default]
134+
file-b = b.cnf
135+
[sec-b]
136+
b = 123
137+
__EOC__
138+
File.write("include-child.conf", <<~__EOC__)
139+
key_outside_section = value_a
140+
.include child
141+
__EOC__
142+
143+
include_file = <<~__EOC__
144+
[default]
145+
file-main = unnamed
146+
[sec-main]
147+
main = 123
148+
.include include-child.conf
149+
__EOC__
150+
151+
# Include a file by relative path
152+
c1 = OpenSSL::Config.parse(include_file)
153+
assert_equal(["default", "sec-a", "sec-b", "sec-main"], c1.sections.sort)
154+
assert_equal(["file-main", "file-a", "file-b"], c1["default"].keys)
155+
assert_equal({"a" => "123"}, c1["sec-a"])
156+
assert_equal({"b" => "123"}, c1["sec-b"])
157+
assert_equal({"main" => "123", "key_outside_section" => "value_a"}, c1["sec-main"])
158+
159+
# Relative paths are from the working directory
160+
assert_raise(OpenSSL::ConfigError) do
161+
Dir.chdir("child") { OpenSSL::Config.parse(include_file) }
162+
end
163+
end
164+
end
165+
123166
def test_s_load
124167
# alias of new
125168
c = OpenSSL::Config.load
@@ -299,6 +342,17 @@ def test_clone
299342
@it['newsection'] = {'a' => 'b'}
300343
assert_not_equal(@it.sections.sort, c.sections.sort)
301344
end
345+
346+
private
347+
348+
def in_tmpdir(*args)
349+
Dir.mktmpdir(*args) do |dir|
350+
dir = File.realpath(dir)
351+
Dir.chdir(dir) do
352+
yield dir
353+
end
354+
end
355+
end
302356
end
303357

304358
end

0 commit comments

Comments
 (0)