httpd_configmap_generator 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/.travis.yml +3 -2
- data/.yamllint +11 -0
- data/Dockerfile +3 -2
- data/Gemfile +4 -0
- data/README-active-directory.md +11 -15
- data/README-ipa.md +7 -12
- data/README-ldap.md +62 -0
- data/README-oidc.md +39 -0
- data/README-saml.md +9 -14
- data/README.md +37 -49
- data/bin/httpd_configmap_generator +36 -50
- data/httpd_configmap_generator.gemspec +5 -3
- data/lib/httpd_configmap_generator.rb +2 -0
- data/lib/httpd_configmap_generator/active_directory.rb +2 -2
- data/lib/httpd_configmap_generator/base.rb +10 -6
- data/lib/httpd_configmap_generator/base/command.rb +19 -17
- data/lib/httpd_configmap_generator/base/config_helper.rb +15 -0
- data/lib/httpd_configmap_generator/base/config_map.rb +43 -26
- data/lib/httpd_configmap_generator/base/file_helper.rb +67 -0
- data/lib/httpd_configmap_generator/base/kerberos.rb +10 -8
- data/lib/httpd_configmap_generator/base/network.rb +27 -25
- data/lib/httpd_configmap_generator/base/pam.rb +6 -4
- data/lib/httpd_configmap_generator/base/sssd.rb +1 -1
- data/lib/httpd_configmap_generator/ipa.rb +12 -1
- data/lib/httpd_configmap_generator/ldap.rb +186 -0
- data/lib/httpd_configmap_generator/oidc.rb +48 -0
- data/lib/httpd_configmap_generator/saml.rb +16 -14
- data/lib/httpd_configmap_generator/version.rb +1 -1
- data/templates/httpd-scc-sysadmin.yaml +38 -0
- metadata +18 -14
- data/lib/httpd_configmap_generator/base/config.rb +0 -13
- data/lib/httpd_configmap_generator/base/file.rb +0 -65
- data/lib/httpd_configmap_generator/options.rb +0 -13
@@ -7,8 +7,8 @@
|
|
7
7
|
# -o filename: for the generated auth-config map.
|
8
8
|
#
|
9
9
|
|
10
|
-
require "bundler/setup"
|
11
|
-
require "
|
10
|
+
Dir.chdir(__dir__) { require "bundler/setup" }
|
11
|
+
require "optimist"
|
12
12
|
require "httpd_configmap_generator"
|
13
13
|
|
14
14
|
CMD = File.basename($PROGRAM_NAME)
|
@@ -20,62 +20,63 @@ end
|
|
20
20
|
|
21
21
|
module HttpdConfigmapGenerator
|
22
22
|
class Cli
|
23
|
-
|
24
|
-
|
23
|
+
SUB_COMMANDS = [HttpdConfigmapGenerator.supported_auth_types] | %w(update export)
|
24
|
+
|
25
|
+
def run
|
26
|
+
Optimist.options do
|
25
27
|
version("#{CMD} #{HttpdConfigmapGenerator::VERSION} - External Authentication Configuration script")
|
26
28
|
banner <<-EOS
|
27
29
|
#{version}
|
28
30
|
|
29
31
|
Usage: #{CMD} auth_type | update | export [--help | options]
|
30
32
|
|
33
|
+
supported auth_type: #{HttpdConfigmapGenerator.supported_auth_types.sort.join(', ')}
|
34
|
+
|
31
35
|
#{CMD} options are:
|
32
36
|
EOS
|
33
37
|
opt :version, "Version of the #{CMD} command",
|
34
38
|
:default => false, :short => "-V"
|
35
|
-
|
36
|
-
opt key, key_options[:description], HttpdConfigmapGenerator::Cli.options_for(key_options, true)
|
37
|
-
end
|
38
|
-
auth_config.optional_options.each do |key, key_options|
|
39
|
-
opt key, key_options[:description], HttpdConfigmapGenerator::Cli.options_for(key_options)
|
40
|
-
end
|
41
|
-
end
|
42
|
-
opts
|
43
|
-
end
|
44
|
-
|
45
|
-
def run_configure(auth_type)
|
46
|
-
begin
|
47
|
-
auth_config = HttpdConfigmapGenerator.new_config(auth_type)
|
48
|
-
rescue => err
|
49
|
-
error_msg(err.to_s)
|
39
|
+
stop_on(SUB_COMMANDS)
|
50
40
|
end
|
51
41
|
|
52
|
-
|
53
|
-
|
42
|
+
auth_type = ARGV.shift
|
43
|
+
Optimist.die "Must specify an authentication type" if auth_type.nil?
|
54
44
|
|
55
|
-
def run_update
|
56
45
|
begin
|
57
|
-
auth_config =
|
46
|
+
auth_config =
|
47
|
+
case auth_type
|
48
|
+
when "update" then HttpdConfigmapGenerator::Update.new
|
49
|
+
when "export" then HttpdConfigmapGenerator::Export.new
|
50
|
+
else HttpdConfigmapGenerator.new_config(auth_type)
|
51
|
+
end
|
58
52
|
rescue => err
|
59
53
|
error_msg(err.to_s)
|
60
54
|
end
|
61
55
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
error_msg(err.to_s)
|
56
|
+
params = Optimist.options do
|
57
|
+
auth_config.required_options.each do |key, key_options|
|
58
|
+
opt key, key_options[:description], HttpdConfigmapGenerator::Cli.options_for(key_options, true)
|
59
|
+
end
|
60
|
+
auth_config.optional_options.each do |key, key_options|
|
61
|
+
opt key, key_options[:description], HttpdConfigmapGenerator::Cli.options_for(key_options)
|
62
|
+
end
|
70
63
|
end
|
71
64
|
|
72
|
-
|
65
|
+
case auth_type
|
66
|
+
when "update" then auth_config.update(params)
|
67
|
+
when "export" then auth_config.export(params)
|
68
|
+
else auth_config.run_configure(params)
|
69
|
+
end
|
73
70
|
end
|
74
71
|
|
75
72
|
def self.options_for(key_options, required = false)
|
76
73
|
options = {}
|
77
|
-
|
78
|
-
|
74
|
+
if key_options[:default].nil?
|
75
|
+
options[:type] = key_options[:type] || :string
|
76
|
+
else
|
77
|
+
options[:default] = key_options[:default]
|
78
|
+
end
|
79
|
+
options[:required] = required
|
79
80
|
options[:short] = key_options[:short] if key_options[:short]
|
80
81
|
options[:multi] = key_options[:multi] if key_options[:multi]
|
81
82
|
options
|
@@ -83,19 +84,4 @@ Usage: #{CMD} auth_type | update | export [--help | options]
|
|
83
84
|
end
|
84
85
|
end
|
85
86
|
|
86
|
-
|
87
|
-
error_msg("
|
88
|
-
Usage: #{CMD} auth_type | update | export [--help | options]
|
89
|
-
Supported auth_type: #{HttpdConfigmapGenerator.supported_auth_types.join(', ')}
|
90
|
-
")
|
91
|
-
else
|
92
|
-
auth_type = ARGV.shift
|
93
|
-
case auth_type
|
94
|
-
when "update"
|
95
|
-
HttpdConfigmapGenerator::Cli.new.run_update
|
96
|
-
when "export"
|
97
|
-
HttpdConfigmapGenerator::Cli.new.run_export
|
98
|
-
else
|
99
|
-
HttpdConfigmapGenerator::Cli.new.run_configure(auth_type)
|
100
|
-
end
|
101
|
-
end
|
87
|
+
HttpdConfigmapGenerator::Cli.new.run
|
@@ -14,8 +14,10 @@ Gem::Specification.new do |s|
|
|
14
14
|
s.description = "The Httpd Configmap Generator"
|
15
15
|
s.licenses = ["Apache-2.0"]
|
16
16
|
|
17
|
-
|
18
|
-
|
17
|
+
if Dir.exist?(File.join(__dir__, ".git"))
|
18
|
+
s.files = `git ls-files -z`.split("\x0").reject do |f|
|
19
|
+
f.match(%r{^(test|spec|features)/})
|
20
|
+
end
|
19
21
|
end
|
20
22
|
s.bindir = "bin"
|
21
23
|
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) } - %w(console setup)
|
@@ -30,5 +32,5 @@ Gem::Specification.new do |s|
|
|
30
32
|
s.add_dependency "awesome_spawn", "~> 1.4"
|
31
33
|
s.add_dependency "iniparse", "~> 1.4"
|
32
34
|
s.add_dependency "more_core_extensions", "~> 3.4"
|
33
|
-
s.add_dependency "
|
35
|
+
s.add_dependency "optimist", "~> 3.0"
|
34
36
|
end
|
@@ -2,7 +2,9 @@ require "httpd_configmap_generator/version"
|
|
2
2
|
require "httpd_configmap_generator/base"
|
3
3
|
require "httpd_configmap_generator/active_directory"
|
4
4
|
require "httpd_configmap_generator/ipa"
|
5
|
+
require "httpd_configmap_generator/ldap"
|
5
6
|
require "httpd_configmap_generator/saml"
|
7
|
+
require "httpd_configmap_generator/oidc"
|
6
8
|
require "httpd_configmap_generator/update"
|
7
9
|
require "httpd_configmap_generator/export"
|
8
10
|
require "more_core_extensions/core_ext/hash"
|
@@ -9,6 +9,8 @@ module HttpdConfigmapGenerator
|
|
9
9
|
|
10
10
|
def required_options
|
11
11
|
super.merge(
|
12
|
+
:host => { :description => "Application Domain",
|
13
|
+
:short => "-h" },
|
12
14
|
:ad_domain => { :description => "Active Directory Domain" },
|
13
15
|
:ad_user => { :description => "Active Directory User" },
|
14
16
|
:ad_password => { :description => "Active Directory Password" }
|
@@ -34,10 +36,8 @@ module HttpdConfigmapGenerator
|
|
34
36
|
/etc/pam.d/postlogin-ac
|
35
37
|
/etc/pam.d/smartcard-auth-ac
|
36
38
|
/etc/pam.d/system-auth-ac
|
37
|
-
/etc/resolv.conf
|
38
39
|
/etc/sssd/sssd.conf
|
39
40
|
/etc/sysconfig/authconfig
|
40
|
-
/etc/sysconfig/network
|
41
41
|
)
|
42
42
|
end
|
43
43
|
|
@@ -1,8 +1,8 @@
|
|
1
1
|
require "pathname"
|
2
2
|
require "httpd_configmap_generator/base/command"
|
3
|
-
require "httpd_configmap_generator/base/
|
3
|
+
require "httpd_configmap_generator/base/config_helper"
|
4
4
|
require "httpd_configmap_generator/base/config_map"
|
5
|
-
require "httpd_configmap_generator/base/
|
5
|
+
require "httpd_configmap_generator/base/file_helper"
|
6
6
|
require "httpd_configmap_generator/base/kerberos"
|
7
7
|
require "httpd_configmap_generator/base/network"
|
8
8
|
require "httpd_configmap_generator/base/pam"
|
@@ -11,6 +11,13 @@ require "httpd_configmap_generator/base/sssd"
|
|
11
11
|
|
12
12
|
module HttpdConfigmapGenerator
|
13
13
|
class Base
|
14
|
+
include Command
|
15
|
+
include ConfigHelper
|
16
|
+
include FileHelper
|
17
|
+
include Kerberos
|
18
|
+
include Network
|
19
|
+
include Pam
|
20
|
+
|
14
21
|
APACHE_USER = "apache".freeze
|
15
22
|
HTTP_KEYTAB = "/etc/http.keytab".freeze
|
16
23
|
IPA_COMMAND = "/usr/bin/ipa".freeze
|
@@ -47,10 +54,7 @@ module HttpdConfigmapGenerator
|
|
47
54
|
|
48
55
|
def required_options
|
49
56
|
{
|
50
|
-
:
|
51
|
-
:short => "-h" },
|
52
|
-
:output => { :description => "Configuration map file to create",
|
53
|
-
:short => "-o" }
|
57
|
+
:output => { :description => "Configuration map file to create", :short => "-o" }
|
54
58
|
}
|
55
59
|
end
|
56
60
|
|
@@ -2,27 +2,29 @@ require "awesome_spawn"
|
|
2
2
|
|
3
3
|
module HttpdConfigmapGenerator
|
4
4
|
class Base
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
module Command
|
6
|
+
def command_run(executable, options = {})
|
7
|
+
if opts && opts[:debug]
|
8
|
+
debug_msg("Running Command: #{AwesomeSpawn.build_command_line(executable, options)}")
|
9
|
+
end
|
10
|
+
AwesomeSpawn.run(executable, options)
|
8
11
|
end
|
9
|
-
AwesomeSpawn.run(executable, options)
|
10
|
-
end
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
13
|
+
def command_run!(executable, options = {})
|
14
|
+
if opts && opts[:debug]
|
15
|
+
debug_msg("Running Command: #{AwesomeSpawn.build_command_line(executable, options)}")
|
16
|
+
end
|
17
|
+
AwesomeSpawn.run!(executable, options)
|
15
18
|
end
|
16
|
-
AwesomeSpawn.run!(executable, options)
|
17
|
-
end
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
20
|
+
def log_command_error(err)
|
21
|
+
err_msg("Command Error: #{err}")
|
22
|
+
if err.kind_of?(AwesomeSpawn::CommandResultError)
|
23
|
+
err_msg("stdout: #{err.result.output}")
|
24
|
+
err_msg("stderr: #{err.result.error}")
|
25
|
+
else
|
26
|
+
err_msg(err.backtrace)
|
27
|
+
end
|
26
28
|
end
|
27
29
|
end
|
28
30
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require "active_support"
|
2
|
+
require "active_support/core_ext" # for Time.current
|
3
|
+
|
4
|
+
module HttpdConfigmapGenerator
|
5
|
+
class Base
|
6
|
+
module ConfigHelper
|
7
|
+
def config_file_backup(path)
|
8
|
+
if File.exist?(path)
|
9
|
+
timestamp = Time.current.strftime(TIMESTAMP_FORMAT)
|
10
|
+
FileUtils.copy(path, "#{path}.#{timestamp}")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -16,11 +16,11 @@ module HttpdConfigmapGenerator
|
|
16
16
|
@config_map = template
|
17
17
|
end
|
18
18
|
|
19
|
-
def generate(auth_type, realm, file_list)
|
19
|
+
def generate(auth_type, realm = "undefined", file_list = nil, metadata = {})
|
20
20
|
info_msg("Generating Auth Config-Map for #{auth_type}")
|
21
21
|
@config_map = template(auth_type, realm)
|
22
22
|
file_specs = gen_filespecs(file_list)
|
23
|
-
define_configuration(file_specs)
|
23
|
+
define_configuration(file_specs, metadata)
|
24
24
|
include_files(file_specs)
|
25
25
|
end
|
26
26
|
|
@@ -71,7 +71,7 @@ module HttpdConfigmapGenerator
|
|
71
71
|
file_specs = []
|
72
72
|
file_list.each do |file|
|
73
73
|
file_specs << file_entry_spec(file.strip)
|
74
|
-
end
|
74
|
+
end unless file_list.nil?
|
75
75
|
file_specs.sort_by { |file_spec| file_spec[:basename] }
|
76
76
|
end
|
77
77
|
|
@@ -84,30 +84,42 @@ module HttpdConfigmapGenerator
|
|
84
84
|
file_specs = []
|
85
85
|
file_list.each do |file_to_add|
|
86
86
|
file_spec = file_to_add.split(",").map(&:strip)
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
if source_file =~ URI.regexp(%w(http https))
|
98
|
-
fetch_network_file(source_file, target_file)
|
99
|
-
file_entry = file_entry_spec(target_file, target_file, mode)
|
87
|
+
file_entry =
|
88
|
+
case file_spec.length
|
89
|
+
when 1
|
90
|
+
file_entry_spec(file_spec.first)
|
91
|
+
when 2
|
92
|
+
source_file, target_file = file_spec
|
93
|
+
file_entry_for_source_target(source_file, target_file)
|
94
|
+
when 3
|
95
|
+
source_file, target_file, mode = file_spec
|
96
|
+
file_entry_for_source_target_mode(source_file, target_file, mode)
|
100
97
|
else
|
101
|
-
|
98
|
+
raise "Invalid file specification #{file_to_add}"
|
102
99
|
end
|
103
|
-
else
|
104
|
-
raise "Invalid file specification #{file_to_add}"
|
105
|
-
end
|
106
100
|
file_specs << file_entry
|
107
101
|
end
|
108
102
|
file_specs.sort_by { |file_spec| file_spec[:basename] }
|
109
103
|
end
|
110
104
|
|
105
|
+
def file_entry_for_source_target(source_file, target_file)
|
106
|
+
raise "Must specify a mode for URL file sources" if source_file =~ URI.regexp(%w(http https))
|
107
|
+
file_entry = file_entry_spec(source_file, target_file)
|
108
|
+
file_entry[:source_file] = source_file
|
109
|
+
file_entry
|
110
|
+
end
|
111
|
+
|
112
|
+
def file_entry_for_source_target_mode(source_file, target_file, mode)
|
113
|
+
if source_file =~ URI.regexp(%w(http https))
|
114
|
+
fetch_network_file(source_file, target_file)
|
115
|
+
file_entry = file_entry_spec(target_file, target_file, mode)
|
116
|
+
else
|
117
|
+
file_entry = file_entry_spec(source_file, target_file, mode)
|
118
|
+
file_entry[:source_file] = source_file
|
119
|
+
end
|
120
|
+
file_entry
|
121
|
+
end
|
122
|
+
|
111
123
|
def file_entry_spec(source_file, target_file = nil, mode = nil)
|
112
124
|
target_file = source_file.dup unless target_file
|
113
125
|
unless mode
|
@@ -123,7 +135,7 @@ module HttpdConfigmapGenerator
|
|
123
135
|
}
|
124
136
|
end
|
125
137
|
|
126
|
-
def update_configuration(file_specs)
|
138
|
+
def update_configuration(file_specs, metadata={})
|
127
139
|
auth_configuration = fetch_auth_configuration
|
128
140
|
return define_configuration(file_specs) unless auth_configuration
|
129
141
|
# first, remove any file_specs references in the file list, we don't want duplication here.
|
@@ -134,7 +146,7 @@ module HttpdConfigmapGenerator
|
|
134
146
|
end
|
135
147
|
auth_configuration = auth_configuration.join("\n") + "\n"
|
136
148
|
# now, append any of the new file_specs at the end of the list.
|
137
|
-
append_configuration(auth_configuration, file_specs)
|
149
|
+
append_configuration(auth_configuration, file_specs, metadata)
|
138
150
|
end
|
139
151
|
|
140
152
|
def search_file_entry(target_file)
|
@@ -145,14 +157,14 @@ module HttpdConfigmapGenerator
|
|
145
157
|
entry ? entry.first.split('=')[1].strip.split(' ') : nil
|
146
158
|
end
|
147
159
|
|
148
|
-
def define_configuration(file_specs)
|
160
|
+
def define_configuration(file_specs, metadata={})
|
149
161
|
auth_configuration = "# External Authentication Configuration File\n#\n"
|
150
|
-
append_configuration(auth_configuration, file_specs)
|
162
|
+
append_configuration(auth_configuration, file_specs, metadata)
|
151
163
|
end
|
152
164
|
|
153
165
|
def include_files(file_specs)
|
154
166
|
file_specs.each do |file_spec|
|
155
|
-
content = File.read(file_spec[:target])
|
167
|
+
content = File.read(file_spec[:source_file] || file_spec[:target])
|
156
168
|
content = Base64.encode64(content) if file_spec[:binary]
|
157
169
|
# encode(:universal_newline => true) will convert \r\n to \n, necessary for to_yaml to render properly.
|
158
170
|
config_map[DATA_SECTION].merge!(file_basename(file_spec) => content.encode(:universal_newline => true))
|
@@ -163,12 +175,17 @@ module HttpdConfigmapGenerator
|
|
163
175
|
file_spec[:binary] ? "#{file_spec[:basename]}.base64" : file_spec[:basename]
|
164
176
|
end
|
165
177
|
|
166
|
-
def append_configuration(auth_configuration, file_specs)
|
178
|
+
def append_configuration(auth_configuration, file_specs, metadata)
|
167
179
|
file_specs.each do |file_spec|
|
168
180
|
debug_msg("Adding file #{file_spec[:target]} ...")
|
169
181
|
auth_configuration += "file = #{file_basename(file_spec)} #{file_spec[:target]} #{file_spec[:mode]}\n"
|
170
182
|
end
|
171
183
|
config_map[DATA_SECTION] ||= {}
|
184
|
+
|
185
|
+
metadata.each do |key, value|
|
186
|
+
config_map[DATA_SECTION].merge!(key => value)
|
187
|
+
end
|
188
|
+
|
172
189
|
config_map[DATA_SECTION].merge!(AUTH_CONFIGURATION => auth_configuration)
|
173
190
|
end
|
174
191
|
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require "pathname"
|
2
|
+
|
3
|
+
module HttpdConfigmapGenerator
|
4
|
+
class Base
|
5
|
+
module FileHelper
|
6
|
+
def template_directory
|
7
|
+
@template_directory ||=
|
8
|
+
Pathname.new(Gem::Specification.find_by_name("httpd_configmap_generator").full_gem_path).join("templates")
|
9
|
+
end
|
10
|
+
|
11
|
+
def cp_template(file, src_dir, dest_dir = "/")
|
12
|
+
src_path = path_join(src_dir, file)
|
13
|
+
dest_path = path_join(dest_dir, file.gsub(".erb", ""))
|
14
|
+
if src_path.to_s.include?(".erb")
|
15
|
+
File.write(dest_path, ERB.new(File.read(src_path), nil, '-').result(binding))
|
16
|
+
else
|
17
|
+
FileUtils.cp(src_path, dest_path)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def delete_target_file(file_path)
|
22
|
+
if File.exist?(file_path)
|
23
|
+
if opts[:force]
|
24
|
+
info_msg("File #{file_path} exists, forcing a delete")
|
25
|
+
File.delete(file_path)
|
26
|
+
else
|
27
|
+
raise "File #{file_path} already exist"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def create_target_directory(file_path)
|
33
|
+
dirname = File.dirname(file_path)
|
34
|
+
return if File.exist?(dirname)
|
35
|
+
debug_msg("Creating directory #{dirname} ...")
|
36
|
+
FileUtils.mkdir_p(dirname)
|
37
|
+
end
|
38
|
+
|
39
|
+
def rm_file(file, dir = "/")
|
40
|
+
path = path_join(dir, file)
|
41
|
+
File.delete(path) if File.exist?(path)
|
42
|
+
end
|
43
|
+
|
44
|
+
def path_join(*args)
|
45
|
+
path = Pathname.new(args.shift)
|
46
|
+
args.each { |path_seg| path = path.join("./#{path_seg}") }
|
47
|
+
path
|
48
|
+
end
|
49
|
+
|
50
|
+
def file_binary?(file)
|
51
|
+
data = File.read(file)
|
52
|
+
ascii = control = binary = total = 0
|
53
|
+
data[0..512].each_byte do |c|
|
54
|
+
total += 1
|
55
|
+
if c < 32
|
56
|
+
control += 1
|
57
|
+
elsif c >= 32 && c <= 128
|
58
|
+
ascii += 1
|
59
|
+
else
|
60
|
+
binary += 1
|
61
|
+
end
|
62
|
+
end
|
63
|
+
control.to_f / ascii > 0.1 || binary.to_f / ascii > 0.05
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|