httpd_configmap_generator 0.1.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 +7 -0
- data/.gitignore +11 -0
- data/.rspec +1 -0
- data/.travis.yml +14 -0
- data/Dockerfile +16 -0
- data/Gemfile +4 -0
- data/LICENSE +201 -0
- data/README-active-directory.md +38 -0
- data/README-ipa.md +41 -0
- data/README-saml.md +70 -0
- data/README.md +386 -0
- data/Rakefile +8 -0
- data/bin/httpd_configmap_generator +101 -0
- data/httpd_configmap_generator.gemspec +34 -0
- data/lib/httpd_configmap_generator.rb +29 -0
- data/lib/httpd_configmap_generator/active_directory.rb +114 -0
- data/lib/httpd_configmap_generator/base.rb +83 -0
- data/lib/httpd_configmap_generator/base/command.rb +29 -0
- data/lib/httpd_configmap_generator/base/config.rb +13 -0
- data/lib/httpd_configmap_generator/base/config_map.rb +183 -0
- data/lib/httpd_configmap_generator/base/file.rb +66 -0
- data/lib/httpd_configmap_generator/base/kerberos.rb +13 -0
- data/lib/httpd_configmap_generator/base/network.rb +37 -0
- data/lib/httpd_configmap_generator/base/pam.rb +9 -0
- data/lib/httpd_configmap_generator/base/principal.rb +33 -0
- data/lib/httpd_configmap_generator/base/sssd.rb +51 -0
- data/lib/httpd_configmap_generator/export.rb +31 -0
- data/lib/httpd_configmap_generator/ipa.rb +122 -0
- data/lib/httpd_configmap_generator/options.rb +13 -0
- data/lib/httpd_configmap_generator/saml.rb +104 -0
- data/lib/httpd_configmap_generator/update.rb +39 -0
- data/lib/httpd_configmap_generator/version.rb +3 -0
- data/templates/etc/pam.d/httpd-auth +2 -0
- data/templates/httpd-configmap-generator-template.yaml +113 -0
- metadata +203 -0
@@ -0,0 +1,66 @@
|
|
1
|
+
require "pathname"
|
2
|
+
|
3
|
+
module HttpdConfigmapGenerator
|
4
|
+
class Base
|
5
|
+
def template_directory
|
6
|
+
@template_directory ||= begin
|
7
|
+
Pathname.new(Bundler.locked_gems.specs.select { |g| g.name == "httpd_configmap_generator" }.first.gem_dir).join("templates")
|
8
|
+
end
|
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
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module HttpdConfigmapGenerator
|
2
|
+
class Base
|
3
|
+
def enable_kerberos_dns_lookups
|
4
|
+
info_msg("Configuring Kerberos DNS Lookups")
|
5
|
+
config_file_backup(KERBEROS_CONFIG_FILE)
|
6
|
+
krb5config = File.read(KERBEROS_CONFIG_FILE)
|
7
|
+
krb5config[/(\s*)dns_lookup_kdc(\s*)=(\s*)(.*)/, 4] = 'true' if krb5config[/(\s*)dns_lookup_kdc(\s*)=/]
|
8
|
+
krb5config[/(\s*)dns_lookup_realm(\s*)=(\s*)(.*)/, 4] = 'true' if krb5config[/(\s*)dns_lookup_realm(\s*)=/]
|
9
|
+
debug_msg("- Updating #{KERBEROS_CONFIG_FILE}")
|
10
|
+
File.write(KERBEROS_CONFIG_FILE, krb5config)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module HttpdConfigmapGenerator
|
2
|
+
class Base
|
3
|
+
HOSTNAME_COMMAND = "/usr/bin/hostname".freeze
|
4
|
+
|
5
|
+
def realm
|
6
|
+
domain.upcase
|
7
|
+
end
|
8
|
+
|
9
|
+
def domain
|
10
|
+
domain_from_host(opts[:host])
|
11
|
+
end
|
12
|
+
|
13
|
+
def domain_from_host(host)
|
14
|
+
host.gsub(/^([^.]+\.)/, '') if host.present? && host.include?('.')
|
15
|
+
end
|
16
|
+
|
17
|
+
def host_reachable?(host)
|
18
|
+
require "net/ping"
|
19
|
+
Net::Ping::External.new(host).ping
|
20
|
+
end
|
21
|
+
|
22
|
+
def update_hostname(host)
|
23
|
+
command_run!(HOSTNAME_COMMAND, :params => [host]) if command_run(HOSTNAME_COMMAND).output.strip != host
|
24
|
+
end
|
25
|
+
|
26
|
+
def fetch_network_file(source_file, target_file)
|
27
|
+
require "net/http"
|
28
|
+
|
29
|
+
delete_target_file(target_file)
|
30
|
+
create_target_directory(target_file)
|
31
|
+
info_msg("Downloading #{source_file} ...")
|
32
|
+
result = Net::HTTP.get_response(URI(source_file))
|
33
|
+
raise "Failed to fetch URL file source #{source_file}" unless result.kind_of?(Net::HTTPSuccess)
|
34
|
+
File.write(target_file, result.body)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "awesome_spawn"
|
2
|
+
|
3
|
+
module HttpdConfigmapGenerator
|
4
|
+
class Principal < Base
|
5
|
+
attr_accessor :hostname
|
6
|
+
attr_accessor :realm # EXAMPLE.COM
|
7
|
+
attr_accessor :service # HTTP
|
8
|
+
|
9
|
+
attr_accessor :name # Kerberos principal name generated
|
10
|
+
|
11
|
+
def initialize(options = {})
|
12
|
+
options.each { |n, v| public_send("#{n}=", v) }
|
13
|
+
@realm = @realm.upcase if @realm
|
14
|
+
@name ||= "#{service}/#{hostname}@#{realm}"
|
15
|
+
@name
|
16
|
+
end
|
17
|
+
|
18
|
+
def register
|
19
|
+
request unless exist?
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def exist?
|
25
|
+
command_run(IPA_COMMAND, :params => ["-e", "skip_version_check=1", "service-find", "--principal", name]).success?
|
26
|
+
end
|
27
|
+
|
28
|
+
def request
|
29
|
+
# Using --force because these services tend not to be in dns. This is like VERIFY_NONE.
|
30
|
+
command_run!(IPA_COMMAND, :params => ["-e", "skip_version_check=1", "service-add", "--force", name])
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'iniparse'
|
2
|
+
|
3
|
+
module HttpdConfigmapGenerator
|
4
|
+
class Sssd < Base
|
5
|
+
attr_accessor :sssd
|
6
|
+
attr_accessor :opts
|
7
|
+
|
8
|
+
def initialize(opts = {})
|
9
|
+
@opts = opts
|
10
|
+
@sssd = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def load(file_path)
|
14
|
+
@sssd = IniParse.open(file_path)
|
15
|
+
end
|
16
|
+
|
17
|
+
def save(file_path)
|
18
|
+
return unless sssd
|
19
|
+
config_file_backup(file_path)
|
20
|
+
info_msg("Saving SSSD to #{file_path}")
|
21
|
+
sssd.save(file_path)
|
22
|
+
end
|
23
|
+
|
24
|
+
def configure_domain(domain)
|
25
|
+
domain = section("domain/#{domain}")
|
26
|
+
domain["ldap_user_extra_attrs"] = LDAP_ATTRS.keys.join(", ")
|
27
|
+
domain["entry_cache_timeout"] = 600
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_service(service)
|
31
|
+
services = section("sssd")["services"]
|
32
|
+
services = (services.split(",").map(&:strip) | [service]).join(", ")
|
33
|
+
sssd.section("sssd")["services"] = services
|
34
|
+
sssd.section(service)
|
35
|
+
end
|
36
|
+
|
37
|
+
def configure_ifp
|
38
|
+
add_service("ifp")
|
39
|
+
ifp = section("ifp")
|
40
|
+
ifp["allowed_uids"] = "#{APACHE_USER}, root"
|
41
|
+
ifp["user_attributes"] = LDAP_ATTRS.keys.collect { |k| "+#{k}" }.join(", ")
|
42
|
+
end
|
43
|
+
|
44
|
+
def section(key)
|
45
|
+
if key =~ /^domain\/.*$/
|
46
|
+
key = sssd.entries.collect(&:key).select { |k| k.downcase == key.downcase }.first
|
47
|
+
end
|
48
|
+
sssd.section(key)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module HttpdConfigmapGenerator
|
2
|
+
class Export < Base
|
3
|
+
def required_options
|
4
|
+
{
|
5
|
+
:input => { :description => "Input config map file",
|
6
|
+
:short => "-i" },
|
7
|
+
:file => { :description => "Config map file to export",
|
8
|
+
:short => "-l" },
|
9
|
+
:output => { :description => "The output file being exported",
|
10
|
+
:short => "-o" }
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
def export(opts)
|
15
|
+
validate_options(opts)
|
16
|
+
@opts = opts
|
17
|
+
config_map = ConfigMap.new(opts)
|
18
|
+
config_map.load(opts[:input])
|
19
|
+
config_map.export_file(opts[:file], opts[:output])
|
20
|
+
rescue => err
|
21
|
+
log_command_error(err)
|
22
|
+
raise err
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def validate_options(options)
|
28
|
+
raise "Input configuration map #{options[:input]} does not exist" unless File.exist?(options[:input])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module HttpdConfigmapGenerator
|
2
|
+
class Ipa < Base
|
3
|
+
IPA_INSTALL_COMMAND = "/usr/sbin/ipa-client-install".freeze
|
4
|
+
IPA_GETKEYTAB = "/usr/sbin/ipa-getkeytab".freeze
|
5
|
+
AUTH = {
|
6
|
+
:type => "external",
|
7
|
+
:subtype => "ipa"
|
8
|
+
}.freeze
|
9
|
+
|
10
|
+
def required_options
|
11
|
+
super.merge(
|
12
|
+
:ipa_server => { :description => "IPA Server Fqdn" },
|
13
|
+
:ipa_password => { :description => "IPA Server Password" }
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
def optional_options
|
18
|
+
super.merge(
|
19
|
+
:ipa_principal => { :description => "IPA Server Principal", :default => "admin" },
|
20
|
+
:ipa_domain => { :description => "Domain of IPA Server" },
|
21
|
+
:ipa_realm => { :description => "Realm of IPA Server" }
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
def persistent_files
|
26
|
+
%w(
|
27
|
+
/etc/http.keytab
|
28
|
+
/etc/ipa/ca.crt
|
29
|
+
/etc/ipa/default.conf
|
30
|
+
/etc/ipa/nssdb/cert8.db
|
31
|
+
/etc/ipa/nssdb/key3.db
|
32
|
+
/etc/ipa/nssdb/pwdfile.txt
|
33
|
+
/etc/ipa/nssdb/secmod.db
|
34
|
+
/etc/krb5.conf
|
35
|
+
/etc/krb5.keytab
|
36
|
+
/etc/nsswitch.conf
|
37
|
+
/etc/openldap/ldap.conf
|
38
|
+
/etc/pam.d/fingerprint-auth-ac
|
39
|
+
/etc/pam.d/httpd-auth
|
40
|
+
/etc/pam.d/password-auth-ac
|
41
|
+
/etc/pam.d/postlogin-ac
|
42
|
+
/etc/pam.d/smartcard-auth-ac
|
43
|
+
/etc/pam.d/system-auth-ac
|
44
|
+
/etc/pki/ca-trust/source/ipa.p11-kit
|
45
|
+
/etc/sssd/sssd.conf
|
46
|
+
/etc/sysconfig/authconfig
|
47
|
+
/etc/sysconfig/network
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
def configure(opts)
|
52
|
+
update_hostname(opts[:host])
|
53
|
+
command_run!(IPA_INSTALL_COMMAND,
|
54
|
+
:params => [
|
55
|
+
"-N", :force_join, :fixed_primary, :unattended, {
|
56
|
+
:realm= => realm,
|
57
|
+
:domain= => domain,
|
58
|
+
:server= => opts[:ipa_server],
|
59
|
+
:principal= => opts[:ipa_principal],
|
60
|
+
:password= => opts[:ipa_password]
|
61
|
+
}
|
62
|
+
])
|
63
|
+
configure_ipa_http_service
|
64
|
+
configure_pam
|
65
|
+
configure_sssd
|
66
|
+
enable_kerberos_dns_lookups
|
67
|
+
config_map = ConfigMap.new(opts)
|
68
|
+
config_map.generate(AUTH[:type], realm, persistent_files)
|
69
|
+
config_map.save(opts[:output])
|
70
|
+
rescue => err
|
71
|
+
log_command_error(err)
|
72
|
+
raise err
|
73
|
+
end
|
74
|
+
|
75
|
+
def configured?
|
76
|
+
File.exist?(SSSD_CONFIG)
|
77
|
+
end
|
78
|
+
|
79
|
+
def unconfigure
|
80
|
+
return unless configured?
|
81
|
+
command_run(IPA_INSTALL_COMMAND, :params => [:uninstall, :unattended])
|
82
|
+
end
|
83
|
+
|
84
|
+
def realm
|
85
|
+
@realm ||= opts[:ipa_realm] if opts[:ipa_realm].present?
|
86
|
+
@realm ||= domain
|
87
|
+
@realm ||= super
|
88
|
+
@realm = @realm.upcase
|
89
|
+
end
|
90
|
+
|
91
|
+
def domain
|
92
|
+
@domain ||= opts[:ipa_domain] if opts[:ipa_domain].present?
|
93
|
+
@domain ||= domain_from_host(opts[:ipa_server]) if opts[:ipa_server].present?
|
94
|
+
@domain ||= super
|
95
|
+
@domain
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def configure_sssd
|
101
|
+
info_msg("Configuring SSSD Service")
|
102
|
+
sssd = Sssd.new(opts)
|
103
|
+
sssd.load(SSSD_CONFIG)
|
104
|
+
sssd.configure_domain(domain)
|
105
|
+
sssd.add_service("pam")
|
106
|
+
sssd.configure_ifp
|
107
|
+
debug_msg("- Creating #{SSSD_CONFIG}")
|
108
|
+
sssd.save(SSSD_CONFIG)
|
109
|
+
end
|
110
|
+
|
111
|
+
def configure_ipa_http_service
|
112
|
+
info_msg("Configuring IPA HTTP Service")
|
113
|
+
command_run!("/usr/bin/kinit", :params => [opts[:ipa_principal]], :stdin_data => opts[:ipa_password])
|
114
|
+
service = Principal.new(:hostname => opts[:host], :realm => realm, :service => "HTTP")
|
115
|
+
service.register
|
116
|
+
debug_msg("- Fetching #{HTTP_KEYTAB}")
|
117
|
+
command_run!(IPA_GETKEYTAB, :params => {"-s" => opts[:ipa_server], "-k" => HTTP_KEYTAB, "-p" => service.name})
|
118
|
+
FileUtils.chown(APACHE_USER, nil, HTTP_KEYTAB)
|
119
|
+
FileUtils.chmod(0o600, HTTP_KEYTAB)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module HttpdConfigmapGenerator
|
2
|
+
def self.required_options
|
3
|
+
{
|
4
|
+
:host => { :description => "Application Domain" }
|
5
|
+
}
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.optional_options
|
9
|
+
{
|
10
|
+
:force => { :description => "Force configuration if configured already", :default => false }
|
11
|
+
}
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module HttpdConfigmapGenerator
|
2
|
+
class Saml < Base
|
3
|
+
MELLON_CREATE_METADATA_COMMAND = "/usr/libexec/mod_auth_mellon/mellon_create_metadata.sh".freeze
|
4
|
+
SAML2_CONFIG_DIRECTORY = "/etc/httpd/saml2".freeze
|
5
|
+
MIQSP_METADATA_FILE = "#{SAML2_CONFIG_DIRECTORY}/miqsp-metadata.xml".freeze
|
6
|
+
IDP_METADATA_FILE = "#{SAML2_CONFIG_DIRECTORY}/idp-metadata.xml".freeze
|
7
|
+
AUTH = {
|
8
|
+
:type => "saml",
|
9
|
+
:subtype => "saml"
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
def required_options
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
def optional_options
|
17
|
+
super.merge(
|
18
|
+
:keycloak_add_metadata => { :description => "Download and add the Keycloak metadata file",
|
19
|
+
:default => false },
|
20
|
+
:keycloak_server => { :description => "Keycloak Server Fqdn or IP" },
|
21
|
+
:keycloak_realm => { :description => "Keycloak Realm for this client"}
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
def persistent_files
|
26
|
+
file_list = %w(
|
27
|
+
/etc/httpd/saml2/miqsp-key.key
|
28
|
+
/etc/httpd/saml2/miqsp-cert.cert
|
29
|
+
/etc/httpd/saml2/miqsp-metadata.xml
|
30
|
+
)
|
31
|
+
file_list += [IDP_METADATA_FILE] if opts[:keycloak_add_metadata]
|
32
|
+
file_list
|
33
|
+
end
|
34
|
+
|
35
|
+
def configure(opts)
|
36
|
+
update_hostname(opts[:host])
|
37
|
+
Dir.mkdir(SAML2_CONFIG_DIRECTORY)
|
38
|
+
Dir.chdir(SAML2_CONFIG_DIRECTORY) do
|
39
|
+
command_run!(MELLON_CREATE_METADATA_COMMAND,
|
40
|
+
:params => [
|
41
|
+
"https://#{opts[:host]}",
|
42
|
+
"https://#{opts[:host]}/saml2"
|
43
|
+
])
|
44
|
+
rename_mellon_configfiles
|
45
|
+
fetch_idp_metadata
|
46
|
+
end
|
47
|
+
config_map = ConfigMap.new(opts)
|
48
|
+
config_map.generate(AUTH[:type], realm, persistent_files)
|
49
|
+
config_map.save(opts[:output])
|
50
|
+
rescue => err
|
51
|
+
log_command_error(err)
|
52
|
+
raise err
|
53
|
+
end
|
54
|
+
|
55
|
+
def configured?
|
56
|
+
File.exist?(MIQSP_METADATA_FILE)
|
57
|
+
end
|
58
|
+
|
59
|
+
def unconfigure
|
60
|
+
return unless configured?
|
61
|
+
FileUtils.rm_rf(SAML2_CONFIG_DIRECTORY) if Dir.exist?(SAML2_CONFIG_DIRECTORY)
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def validate_options(options)
|
67
|
+
super(options)
|
68
|
+
if options[:keycloak_add_metadata]
|
69
|
+
if options[:keycloak_server] == "" || options[:keycloak_realm] == ""
|
70
|
+
raise "Must specify both keycloak-server and keycloak-realm for fetching the IdP metadata file"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def rename_mellon_configfiles
|
76
|
+
info_msg("Renaming mellon config files")
|
77
|
+
Dir.chdir(SAML2_CONFIG_DIRECTORY) do
|
78
|
+
Dir.glob("https_*.*") do |mellon_file|
|
79
|
+
miq_saml2_file = nil
|
80
|
+
case mellon_file
|
81
|
+
when /^https_.*\.key$/
|
82
|
+
miq_saml2_file = "miqsp-key.key"
|
83
|
+
when /^https_.*\.cert$/
|
84
|
+
miq_saml2_file = "miqsp-cert.cert"
|
85
|
+
when /^https_.*\.xml$/
|
86
|
+
miq_saml2_file = "miqsp-metadata.xml"
|
87
|
+
end
|
88
|
+
if miq_saml2_file
|
89
|
+
debug_msg("- renaming #{mellon_file} to #{miq_saml2_file}")
|
90
|
+
File.rename(mellon_file, miq_saml2_file)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def fetch_idp_metadata
|
97
|
+
if opts[:keycloak_add_metadata]
|
98
|
+
source_file = "http://#{opts[:keycloak_server]}:8080"
|
99
|
+
source_file += "/auth/realms/#{opts[:keycloak_realm]}/protocol/saml/descriptor"
|
100
|
+
fetch_network_file(source_file, IDP_METADATA_FILE)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|