chamber 2.10.2 → 2.11.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/LICENSE.txt +1 -1
- data/lib/chamber.rb +3 -6
- data/lib/chamber/binary/heroku.rb +1 -0
- data/lib/chamber/binary/runner.rb +29 -13
- data/lib/chamber/binary/travis.rb +1 -0
- data/lib/chamber/commands/base.rb +10 -9
- data/lib/chamber/commands/comparable.rb +1 -0
- data/lib/chamber/commands/compare.rb +8 -7
- data/lib/chamber/commands/files.rb +1 -0
- data/lib/chamber/commands/heroku.rb +1 -0
- data/lib/chamber/commands/heroku/clear.rb +2 -1
- data/lib/chamber/commands/heroku/compare.rb +1 -0
- data/lib/chamber/commands/heroku/pull.rb +3 -4
- data/lib/chamber/commands/heroku/push.rb +1 -0
- data/lib/chamber/commands/initialize.rb +88 -76
- data/lib/chamber/commands/securable.rb +3 -2
- data/lib/chamber/commands/secure.rb +3 -1
- data/lib/chamber/commands/show.rb +7 -6
- data/lib/chamber/commands/travis.rb +1 -0
- data/lib/chamber/commands/travis/secure.rb +1 -0
- data/lib/chamber/configuration.rb +14 -13
- data/lib/chamber/context_resolver.rb +52 -55
- data/lib/chamber/encryption_methods/none.rb +4 -2
- data/lib/chamber/encryption_methods/public_key.rb +4 -2
- data/lib/chamber/encryption_methods/ssl.rb +11 -9
- data/lib/chamber/errors/decryption_failure.rb +1 -0
- data/lib/chamber/file.rb +27 -18
- data/lib/chamber/file_set.rb +14 -13
- data/lib/chamber/filters/decryption_filter.rb +48 -18
- data/lib/chamber/filters/encryption_filter.rb +32 -22
- data/lib/chamber/filters/environment_filter.rb +109 -16
- data/lib/chamber/filters/failed_decryption_filter.rb +10 -8
- data/lib/chamber/filters/insecure_filter.rb +1 -0
- data/lib/chamber/filters/namespace_filter.rb +8 -7
- data/lib/chamber/filters/secure_filter.rb +10 -9
- data/lib/chamber/filters/translate_secure_keys_filter.rb +10 -9
- data/lib/chamber/instance.rb +5 -4
- data/lib/chamber/key_pair.rb +82 -0
- data/lib/chamber/keys/base.rb +64 -0
- data/lib/chamber/keys/decryption.rb +41 -0
- data/lib/chamber/keys/encryption.rb +41 -0
- data/lib/chamber/namespace_set.rb +10 -9
- data/lib/chamber/rails.rb +1 -0
- data/lib/chamber/rails/railtie.rb +1 -0
- data/lib/chamber/rubinius_fix.rb +1 -0
- data/lib/chamber/settings.rb +45 -41
- data/lib/chamber/types/secured.rb +14 -12
- data/lib/chamber/version.rb +2 -1
- metadata +28 -27
- metadata.gz.sig +0 -0
- data/lib/chamber/decryption_key.rb +0 -52
- data/lib/chamber/environmentable.rb +0 -27
- data/lib/chamber/filters/boolean_conversion_filter.rb +0 -41
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 212f658524053ccae210207899aaadd11fca8dc3
|
4
|
+
data.tar.gz: 2ab2300b8a8a0107f7c094b79e344209f24a385a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6d90c504c34d974a5d915aa1eb63f5c5c1c6dff4cf7fda43a072b2464e04d6d11ecacf196d921a5ecb9c2521a685a6199421abcfb6dad90c62f149cba277a273
|
7
|
+
data.tar.gz: d9168743dc703563e8e5ef86f4fb5fce13d00a3d6a7c590a7147d0902d2f8e73df7957fb070f8ce7d5541b483a7c5f8d94bf7b350ec56ab63a8d5ef58f27f357
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data.tar.gz.sig
CHANGED
Binary file
|
data/LICENSE.txt
CHANGED
data/lib/chamber.rb
CHANGED
@@ -1,9 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'chamber/rubinius_fix'
|
3
4
|
require 'chamber/instance'
|
4
5
|
require 'chamber/rails'
|
5
6
|
|
6
7
|
module Chamber
|
8
|
+
attr_writer :instance
|
9
|
+
|
7
10
|
def load(options = {})
|
8
11
|
self.instance = Instance.new(options)
|
9
12
|
end
|
@@ -16,16 +19,10 @@ module Chamber
|
|
16
19
|
instance.settings
|
17
20
|
end
|
18
21
|
|
19
|
-
protected
|
20
|
-
|
21
|
-
attr_accessor :instance
|
22
|
-
|
23
22
|
def instance
|
24
23
|
@instance ||= Instance.new({})
|
25
24
|
end
|
26
25
|
|
27
|
-
public
|
28
|
-
|
29
26
|
def method_missing(name, *args)
|
30
27
|
return instance.public_send(name, *args) if instance.respond_to?(name)
|
31
28
|
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'thor'
|
3
4
|
require 'chamber/rubinius_fix'
|
4
5
|
require 'chamber/binary/travis'
|
@@ -46,22 +47,28 @@ class Runner < Thor
|
|
46
47
|
desc: 'Used to quickly assign a given scenario to the chamber ' \
|
47
48
|
'command (eg Rails apps)'
|
48
49
|
|
49
|
-
class_option :
|
50
|
-
type: :
|
51
|
-
desc: 'The path to or contents of the private key associated
|
52
|
-
'the project (typically .chamber.pem)'
|
50
|
+
class_option :decryption_keys,
|
51
|
+
type: :array,
|
52
|
+
desc: 'The path to or contents of the private key (or keys) associated ' \
|
53
|
+
'with the project (typically .chamber.pem)'
|
54
|
+
|
55
|
+
class_option :encryption_keys,
|
56
|
+
type: :array,
|
57
|
+
desc: 'The path to or contents of the public key (or keys) associated ' \
|
58
|
+
'with the project (typically .chamber.pub.pem)'
|
53
59
|
|
54
|
-
|
55
|
-
type: :string,
|
56
|
-
desc: 'The path to or contents of the public key associated with ' \
|
57
|
-
'the project (typically .chamber.pub.pem)'
|
60
|
+
################################################################################
|
58
61
|
|
59
62
|
desc 'travis SUBCOMMAND ...ARGS', 'For manipulating Travis CI environment variables'
|
60
63
|
subcommand 'travis', Chamber::Binary::Travis
|
61
64
|
|
65
|
+
################################################################################
|
66
|
+
|
62
67
|
desc 'heroku SUBCOMMAND ...ARGS', 'For manipulating Heroku environment variables'
|
63
68
|
subcommand 'heroku', Chamber::Binary::Heroku
|
64
69
|
|
70
|
+
################################################################################
|
71
|
+
|
65
72
|
desc 'show', 'Displays the list of settings and their values'
|
66
73
|
|
67
74
|
method_option :as_env,
|
@@ -70,8 +77,6 @@ class Runner < Thor
|
|
70
77
|
desc: 'Whether the displayed settings should be environment ' \
|
71
78
|
'variable compatible'
|
72
79
|
|
73
|
-
desc 'only_sensitive', 'Only show secured/securable settings'
|
74
|
-
|
75
80
|
method_option :only_sensitive,
|
76
81
|
type: :boolean,
|
77
82
|
aliases: '-s',
|
@@ -82,13 +87,20 @@ class Runner < Thor
|
|
82
87
|
puts Commands::Show.call(options.merge(shell: self))
|
83
88
|
end
|
84
89
|
|
90
|
+
################################################################################
|
91
|
+
|
85
92
|
desc 'files', 'Lists the settings files which are parsed with the given options'
|
93
|
+
|
86
94
|
def files
|
87
95
|
puts Commands::Files.call(options.merge(shell: self))
|
88
96
|
end
|
89
97
|
|
90
|
-
|
91
|
-
|
98
|
+
################################################################################
|
99
|
+
|
100
|
+
desc 'compare', 'Displays the difference between the settings in the first set ' \
|
101
|
+
'of namespaces and the settings in the second set. Useful for ' \
|
102
|
+
'tracking down why there may be issues in development versus test ' \
|
103
|
+
'or differences between staging and production.'
|
92
104
|
|
93
105
|
method_option :keys_only,
|
94
106
|
type: :boolean,
|
@@ -110,6 +122,8 @@ class Runner < Thor
|
|
110
122
|
Commands::Compare.call(options.merge(shell: self))
|
111
123
|
end
|
112
124
|
|
125
|
+
################################################################################
|
126
|
+
|
113
127
|
desc 'secure', 'Secures any values which appear to need to be encrypted in any of ' \
|
114
128
|
'the settings files which match irrespective of namespaces'
|
115
129
|
|
@@ -127,7 +141,9 @@ class Runner < Thor
|
|
127
141
|
Commands::Secure.call(options.merge(shell: self))
|
128
142
|
end
|
129
143
|
|
130
|
-
|
144
|
+
################################################################################
|
145
|
+
|
146
|
+
desc 'init', 'Sets Chamber up using best practices for secure configuration ' \
|
131
147
|
'management'
|
132
148
|
|
133
149
|
def init
|
@@ -1,28 +1,29 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'pathname'
|
3
4
|
require 'chamber/instance'
|
4
5
|
|
5
6
|
module Chamber
|
6
7
|
module Commands
|
7
8
|
class Base
|
8
|
-
def initialize(options = {})
|
9
|
-
self.chamber = Chamber::Instance.new options
|
10
|
-
self.shell = options[:shell]
|
11
|
-
self.rootpath = options[:rootpath]
|
12
|
-
self.dry_run = options[:dry_run]
|
13
|
-
end
|
14
|
-
|
15
9
|
def self.call(options = {})
|
16
10
|
new(options).call
|
17
11
|
end
|
18
12
|
|
19
|
-
protected
|
20
|
-
|
21
13
|
attr_accessor :chamber,
|
22
14
|
:shell,
|
23
15
|
:dry_run
|
24
16
|
attr_reader :rootpath
|
25
17
|
|
18
|
+
def initialize(options = {})
|
19
|
+
self.chamber = Chamber::Instance.new options
|
20
|
+
self.shell = options[:shell]
|
21
|
+
self.rootpath = options[:rootpath]
|
22
|
+
self.dry_run = options[:dry_run]
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
26
27
|
def rootpath=(other)
|
27
28
|
@rootpath ||= Pathname.new(other)
|
28
29
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'chamber/instance'
|
3
4
|
require 'chamber/commands/base'
|
4
5
|
require 'chamber/commands/comparable'
|
@@ -8,6 +9,13 @@ module Commands
|
|
8
9
|
class Compare < Chamber::Commands::Base
|
9
10
|
include Chamber::Commands::Comparable
|
10
11
|
|
12
|
+
attr_accessor :first_settings_instance,
|
13
|
+
:second_settings_instance
|
14
|
+
|
15
|
+
def self.call(options = {})
|
16
|
+
new(options).call
|
17
|
+
end
|
18
|
+
|
11
19
|
def initialize(options = {})
|
12
20
|
super
|
13
21
|
|
@@ -18,15 +26,8 @@ class Compare < Chamber::Commands::Base
|
|
18
26
|
self.second_settings_instance = Chamber::Instance.new(second_settings_options)
|
19
27
|
end
|
20
28
|
|
21
|
-
def self.call(options = {})
|
22
|
-
new(options).call
|
23
|
-
end
|
24
|
-
|
25
29
|
protected
|
26
30
|
|
27
|
-
attr_accessor :first_settings_instance,
|
28
|
-
:second_settings_instance
|
29
|
-
|
30
31
|
def first_settings_data
|
31
32
|
settings_data(first_settings_instance)
|
32
33
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'chamber/commands/base'
|
3
4
|
require 'chamber/commands/heroku'
|
4
5
|
|
@@ -9,7 +10,7 @@ class Clear < Chamber::Commands::Base
|
|
9
10
|
include Chamber::Commands::Heroku
|
10
11
|
|
11
12
|
def call
|
12
|
-
chamber.to_environment.
|
13
|
+
chamber.to_environment.each_key do |key|
|
13
14
|
next unless configuration.match(key)
|
14
15
|
|
15
16
|
if dry_run
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'chamber/commands/base'
|
3
4
|
require 'chamber/commands/heroku'
|
4
5
|
|
@@ -8,6 +9,8 @@ module Heroku
|
|
8
9
|
class Pull < Chamber::Commands::Base
|
9
10
|
include Chamber::Commands::Heroku
|
10
11
|
|
12
|
+
attr_accessor :target_file
|
13
|
+
|
11
14
|
def initialize(options = {})
|
12
15
|
super
|
13
16
|
|
@@ -21,10 +24,6 @@ class Pull < Chamber::Commands::Base
|
|
21
24
|
configuration
|
22
25
|
end
|
23
26
|
end
|
24
|
-
|
25
|
-
protected
|
26
|
-
|
27
|
-
attr_accessor :target_file
|
28
27
|
end
|
29
28
|
end
|
30
29
|
end
|
@@ -1,71 +1,128 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'pathname'
|
3
4
|
require 'fileutils'
|
4
5
|
require 'openssl'
|
6
|
+
require 'securerandom'
|
5
7
|
require 'chamber/configuration'
|
8
|
+
require 'chamber/key_pair'
|
6
9
|
require 'chamber/commands/base'
|
7
10
|
|
8
11
|
module Chamber
|
9
12
|
module Commands
|
10
13
|
class Initialize < Chamber::Commands::Base
|
14
|
+
def self.call(options = {})
|
15
|
+
new(options).call
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_accessor :basepath,
|
19
|
+
:namespaces
|
20
|
+
|
11
21
|
def initialize(options = {})
|
12
22
|
super
|
13
23
|
|
14
|
-
self.basepath
|
24
|
+
self.basepath = Chamber.configuration.basepath
|
25
|
+
self.namespaces = options.fetch(:namespaces, [])
|
15
26
|
end
|
16
27
|
|
17
28
|
# rubocop:disable Metrics/LineLength, Metrics/MethodLength, Metrics/AbcSize
|
18
29
|
def call
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
`chmod 644 #{public_key_filepath}`
|
30
|
+
key_pairs = namespaces.map do |namespace|
|
31
|
+
Chamber::KeyPair.new(namespace: namespace,
|
32
|
+
key_file_path: rootpath)
|
33
|
+
end
|
34
|
+
key_pairs << Chamber::KeyPair.new(namespace: nil,
|
35
|
+
key_file_path: rootpath)
|
26
36
|
|
27
|
-
|
37
|
+
key_pairs.each { |key_pair| generate_key_pair(key_pair) }
|
28
38
|
|
29
|
-
|
30
|
-
shell.append_to_file gitignore_filepath, "\n# Private and protected key files for Chamber\n"
|
31
|
-
shell.append_to_file gitignore_filepath, "#{private_key_filename}\n"
|
32
|
-
shell.append_to_file gitignore_filepath, "#{protected_key_filename}\n"
|
33
|
-
end
|
39
|
+
append_to_gitignore
|
34
40
|
|
35
41
|
shell.copy_file settings_template_filepath, settings_filepath
|
36
42
|
|
37
43
|
shell.say ''
|
38
|
-
shell.say '
|
44
|
+
shell.say '********************************************************************************', :green
|
45
|
+
shell.say ' Success!', :green
|
46
|
+
shell.say '********************************************************************************', :green
|
47
|
+
shell.say ''
|
48
|
+
|
49
|
+
shell.say '.chamber.pem is a DEFAULT Chamber key.', :red
|
50
|
+
shell.say ''
|
51
|
+
shell.say 'If you would like a key which is used only for things such as a certain'
|
52
|
+
shell.say 'environment (such as production), or your local machine, you can rerun'
|
53
|
+
shell.say 'the command like so:'
|
54
|
+
shell.say ''
|
55
|
+
shell.say '$ chamber init --namespaces="production my_machines_hostname"', :yellow
|
39
56
|
shell.say ''
|
40
|
-
|
57
|
+
|
58
|
+
shell.say 'The passphrase for your encrypted private key(s) are:'
|
41
59
|
shell.say ''
|
42
|
-
|
60
|
+
|
61
|
+
key_pairs.each do |key_pair|
|
62
|
+
shell.say "* #{key_pair.encrypted_private_key_filename}: "
|
63
|
+
shell.say key_pair.passphrase, :yellow
|
64
|
+
end
|
65
|
+
|
43
66
|
shell.say ''
|
44
|
-
shell.say '
|
67
|
+
shell.say 'Store these securely somewhere.'
|
45
68
|
shell.say ''
|
46
|
-
shell.say
|
69
|
+
shell.say 'You can send your team members any of the file(s) located at:'
|
70
|
+
shell.say ''
|
71
|
+
|
72
|
+
key_pairs.each do |key_pair|
|
73
|
+
shell.say '* '
|
74
|
+
shell.say key_pair.encrypted_private_key_filepath, :yellow
|
75
|
+
end
|
76
|
+
|
47
77
|
shell.say ''
|
48
|
-
shell.say 'and not have to worry about sending it via a secure medium (such as'
|
49
|
-
shell.say 'email), however do not send the passphrase along with it. Give it to'
|
50
|
-
shell.say 'your team members in person.'
|
78
|
+
shell.say 'and not have to worry about sending it via a secure medium (such as'
|
79
|
+
shell.say 'email), however do not send the passphrase along with it. Give it to'
|
80
|
+
shell.say 'your team members in person.'
|
51
81
|
shell.say ''
|
52
|
-
shell.say 'In order for them to decrypt it (for use with Chamber), they can
|
82
|
+
shell.say 'In order for them to decrypt it (for use with Chamber), they can use something'
|
83
|
+
shell.say 'like the following (swapping out the actual key filenames if necessary):'
|
53
84
|
shell.say ''
|
54
|
-
shell.say "$ cp
|
55
|
-
shell.say "$ ssh-keygen -p -f
|
85
|
+
shell.say "$ cp #{key_pairs[0].encrypted_private_key_filepath} #{key_pairs[0].unencrypted_private_key_filepath}", :yellow
|
86
|
+
shell.say "$ ssh-keygen -p -f #{key_pairs[0].unencrypted_private_key_filepath}", :yellow
|
56
87
|
shell.say ''
|
57
|
-
shell.say 'Enter the passphrase when prompted and leave the new passphrase blank.'
|
88
|
+
shell.say 'Enter the passphrase when prompted and leave the new passphrase blank.'
|
58
89
|
shell.say ''
|
59
90
|
end
|
60
91
|
# rubocop:enable Metrics/LineLength, Metrics/MethodLength, Metrics/AbcSize
|
61
92
|
|
62
|
-
|
63
|
-
|
93
|
+
protected
|
94
|
+
|
95
|
+
def generate_key_pair(key_pair)
|
96
|
+
shell.create_file key_pair.unencrypted_private_key_filepath,
|
97
|
+
key_pair.unencrypted_private_key_pem,
|
98
|
+
skip: true
|
99
|
+
shell.create_file key_pair.encrypted_private_key_filepath,
|
100
|
+
key_pair.encrypted_private_key_pem,
|
101
|
+
skip: true
|
102
|
+
shell.create_file key_pair.public_key_filepath,
|
103
|
+
key_pair.public_key_pem,
|
104
|
+
skip: true
|
105
|
+
|
106
|
+
`chmod 600 #{key_pair.unencrypted_private_key_filepath}`
|
107
|
+
`chmod 600 #{key_pair.encrypted_private_key_filepath}`
|
108
|
+
`chmod 644 #{key_pair.public_key_filepath}`
|
64
109
|
end
|
65
110
|
|
66
|
-
|
111
|
+
# rubocop:disable Style/GuardClause
|
112
|
+
def append_to_gitignore
|
113
|
+
::FileUtils.touch gitignore_filepath
|
114
|
+
|
115
|
+
gitignore_contents = ::File.read(gitignore_filepath)
|
67
116
|
|
68
|
-
|
117
|
+
unless gitignore_contents =~ /^\.chamber\*\.enc$/
|
118
|
+
shell.append_to_file gitignore_filepath, ".chamber*.enc\n"
|
119
|
+
end
|
120
|
+
|
121
|
+
unless gitignore_contents =~ /^\.chamber\*\.pem$/
|
122
|
+
shell.append_to_file gitignore_filepath, ".chamber*.pem\n"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
# rubocop:enable Style/GuardClause
|
69
126
|
|
70
127
|
def settings_template_filepath
|
71
128
|
@settings_template_filepath ||= templates_path + 'settings.yml'
|
@@ -78,7 +135,7 @@ class Initialize < Chamber::Commands::Base
|
|
78
135
|
def gem_path
|
79
136
|
@gem_path ||= Pathname.new(
|
80
137
|
::File.expand_path('../../../..', __FILE__),
|
81
|
-
|
138
|
+
)
|
82
139
|
end
|
83
140
|
|
84
141
|
def settings_filepath
|
@@ -88,51 +145,6 @@ class Initialize < Chamber::Commands::Base
|
|
88
145
|
def gitignore_filepath
|
89
146
|
@gitignore_filepath ||= rootpath + '.gitignore'
|
90
147
|
end
|
91
|
-
|
92
|
-
def protected_key_filepath
|
93
|
-
@protected_key_filepath ||= rootpath + protected_key_filename
|
94
|
-
end
|
95
|
-
|
96
|
-
def private_key_filepath
|
97
|
-
@private_key_filepath ||= rootpath + private_key_filename
|
98
|
-
end
|
99
|
-
|
100
|
-
def public_key_filepath
|
101
|
-
@public_key_filepath ||= rootpath + public_key_filename
|
102
|
-
end
|
103
|
-
|
104
|
-
def protected_key_filename
|
105
|
-
'.chamber.pem.enc'
|
106
|
-
end
|
107
|
-
|
108
|
-
def private_key_filename
|
109
|
-
'.chamber.pem'
|
110
|
-
end
|
111
|
-
|
112
|
-
def public_key_filename
|
113
|
-
'.chamber.pub.pem'
|
114
|
-
end
|
115
|
-
|
116
|
-
def rsa_protected_key
|
117
|
-
@rsa_protected_key ||= begin
|
118
|
-
cipher = OpenSSL::Cipher.new 'AES-128-CBC'
|
119
|
-
key = OpenSSL::PKey::RSA.new(2048)
|
120
|
-
|
121
|
-
key.export cipher, rsa_key_passphrase
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
|
-
def rsa_private_key
|
126
|
-
@rsa_private_key ||= OpenSSL::PKey::RSA.new(2048)
|
127
|
-
end
|
128
|
-
|
129
|
-
def rsa_public_key
|
130
|
-
rsa_private_key.public_key
|
131
|
-
end
|
132
|
-
|
133
|
-
def rsa_key_passphrase
|
134
|
-
@rsa_key_passphrase ||= SecureRandom.uuid
|
135
|
-
end
|
136
148
|
end
|
137
149
|
end
|
138
150
|
end
|