omf_common 6.0.0 → 6.0.2.pre.1

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.
Files changed (50) hide show
  1. data/Gemfile +4 -0
  2. data/bin/file_broadcaster.rb +56 -0
  3. data/bin/file_receiver.rb +62 -0
  4. data/bin/omf_keygen +21 -0
  5. data/bin/{monitor_topic.rb → omf_monitor_topic} +21 -8
  6. data/bin/omf_send_create +118 -0
  7. data/bin/{send_request.rb → omf_send_request} +12 -7
  8. data/example/engine_alt.rb +23 -24
  9. data/example/ls_app.yaml +21 -0
  10. data/lib/omf_common.rb +73 -12
  11. data/lib/omf_common/auth.rb +15 -0
  12. data/lib/omf_common/auth/certificate.rb +174 -0
  13. data/lib/omf_common/auth/certificate_store.rb +72 -0
  14. data/lib/omf_common/auth/ssh_pub_key_convert.rb +80 -0
  15. data/lib/omf_common/comm.rb +66 -9
  16. data/lib/omf_common/comm/amqp/amqp_communicator.rb +40 -13
  17. data/lib/omf_common/comm/amqp/amqp_file_transfer.rb +259 -0
  18. data/lib/omf_common/comm/amqp/amqp_topic.rb +14 -21
  19. data/lib/omf_common/comm/local/local_communicator.rb +31 -2
  20. data/lib/omf_common/comm/local/local_topic.rb +19 -3
  21. data/lib/omf_common/comm/topic.rb +48 -34
  22. data/lib/omf_common/comm/xmpp/communicator.rb +19 -10
  23. data/lib/omf_common/comm/xmpp/topic.rb +22 -81
  24. data/lib/omf_common/default_logging.rb +11 -0
  25. data/lib/omf_common/eventloop.rb +14 -0
  26. data/lib/omf_common/eventloop/em.rb +39 -6
  27. data/lib/omf_common/eventloop/local_evl.rb +15 -0
  28. data/lib/omf_common/exec_app.rb +29 -15
  29. data/lib/omf_common/message.rb +53 -5
  30. data/lib/omf_common/message/json/json_message.rb +149 -39
  31. data/lib/omf_common/message/xml/message.rb +112 -39
  32. data/lib/omf_common/protocol/6.0.rnc +5 -1
  33. data/lib/omf_common/protocol/6.0.rng +12 -0
  34. data/lib/omf_common/version.rb +1 -1
  35. data/omf_common.gemspec +7 -2
  36. data/test/fixture/omf_test.cert.pem +15 -0
  37. data/test/fixture/omf_test.pem +15 -0
  38. data/test/fixture/omf_test.pub +1 -0
  39. data/test/fixture/omf_test.pub.pem +6 -0
  40. data/test/omf_common/auth/certificate_spec.rb +113 -0
  41. data/test/omf_common/auth/ssh_pub_key_convert_spec.rb +13 -0
  42. data/test/omf_common/comm/topic_spec.rb +175 -0
  43. data/test/omf_common/comm/xmpp/communicator_spec.rb +15 -16
  44. data/test/omf_common/comm/xmpp/topic_spec.rb +63 -10
  45. data/test/omf_common/comm_spec.rb +66 -9
  46. data/test/omf_common/message/xml/message_spec.rb +43 -13
  47. data/test/omf_common/message_spec.rb +14 -0
  48. data/test/test_helper.rb +25 -0
  49. metadata +78 -15
  50. data/bin/send_create.rb +0 -94
@@ -37,5 +37,16 @@ module OmfCommon
37
37
  def warn(*args, &block)
38
38
  logger.warn(*args, &block)
39
39
  end
40
+
41
+ # Log a warning message for deprecated methods
42
+ def warn_deprecation(deprecated_name, *suggest_names)
43
+ logger.warn "[DEPRECATION] '#{deprecated_name}' is deprecated. Please use '#{suggest_names.join(', ')}' instead."
44
+ end
45
+
46
+ def warn_removed(deprecated_name)
47
+ define_method(deprecated_name) do |*args, &block|
48
+ logger.warn "[DEPRECATION] '#{deprecated_name}' is deprecated and not supported. Please do not use it."
49
+ end
50
+ end
40
51
  end
41
52
  end
@@ -84,7 +84,21 @@ module OmfCommon
84
84
  def on_stop(&block)
85
85
  warn "Missing implementation 'on_stop'"
86
86
  end
87
+
88
+ # Calling 'block' when having trapped an INT signal
89
+ #
90
+ def on_int_signal(&block)
91
+ # trap(:INT)
92
+ warn "Missing implementation 'on_int_signal'"
93
+ end
87
94
 
95
+ # Calling 'block' when having trapped a TERM signal
96
+ #
97
+ def on_term_signal(&block)
98
+ # trap(:TERM) {}
99
+ warn "Missing implementation 'on_term_signal'"
100
+ end
101
+
88
102
  private
89
103
  def initialize(opts = {}, &block)
90
104
  #run(&block) if block
@@ -25,15 +25,43 @@ module OmfCommon
25
25
  end
26
26
  end
27
27
 
28
+ # Call 'block' in the context of a separate thread.
29
+ #
30
+ def defer(&block)
31
+ raise "Can't handle 'defer' registration before the EM is up" unless EM.reactor_running?
32
+ EM.defer do
33
+ begin
34
+ block.call()
35
+ rescue => ex
36
+ error "Exception '#{ex}'"
37
+ debug "#{ex}\n\t#{ex.backtrace.join("\n\t")}"
38
+ end
39
+ end
40
+ end
41
+
28
42
  # Periodically call block every interval_sec
29
43
  #
30
44
  # @param [Float] interval in sec
31
45
  def every(interval_sec, &block)
32
- if EM.reactor_running?
33
- EM.add_periodic_timer(interval_sec, &block)
34
- else
35
- @deferred << lambda do
36
- EM.add_periodic_timer(interval_sec, &block)
46
+ # to allow canceling the periodic timer we need to
47
+ # hand back a reference to it which responds to 'cancel'
48
+ # As this is getting rather complex when allowing for
49
+ # registration before the EM is up and running, we simply throw
50
+ # and exception at this time.
51
+ raise "Can't handle 'every' registration before the EM is up" unless EM.reactor_running?
52
+ # if EM.reactor_running?
53
+ # EM.add_periodic_timer(interval_sec, &block)
54
+ # else
55
+ # @deferred << lambda do
56
+ # EM.add_periodic_timer(interval_sec, &block)
57
+ # end
58
+ # end
59
+ EM.add_periodic_timer(interval_sec) do
60
+ begin
61
+ block.call()
62
+ rescue => ex
63
+ error "Exception '#{ex}'"
64
+ debug "#{ex}\n\t#{ex.backtrace.join("\n\t")}"
37
65
  end
38
66
  end
39
67
  end
@@ -43,7 +71,12 @@ module OmfCommon
43
71
  @deferred.each { |proc| proc.call }
44
72
  @deferred = nil
45
73
  if block
46
- block.arity == 0 ? block.call : block.call(self)
74
+ begin
75
+ block.arity == 0 ? block.call : block.call(self)
76
+ rescue => ex
77
+ error "While executing run block - #{ex}"
78
+ error ex.backtrace.join("\n\t")
79
+ end
47
80
  end
48
81
  end
49
82
  end
@@ -31,6 +31,21 @@ module OmfCommon
31
31
  @tasks << [Time.now + interval_sec, block, :periodic => interval_sec]
32
32
  end
33
33
 
34
+ # Call 'block' in the context of a separate thread.
35
+ #
36
+ def defer(&block)
37
+ @logger.note("DEFER")
38
+ Thread.new do
39
+ begin
40
+ block.call()
41
+ rescue => ex
42
+ @logger.error "Exception '#{ex}'"
43
+ @logger.debug ex.backtract.join("\n\t")
44
+ end
45
+ end
46
+ end
47
+
48
+
34
49
  def stop
35
50
  @running = false
36
51
  end
@@ -32,7 +32,7 @@ require 'fcntl'
32
32
  # Borrows from Open3
33
33
  #
34
34
  class ExecApp
35
-
35
+
36
36
  # Holds the pids for all active apps
37
37
  @@all_apps = Hash.new
38
38
 
@@ -69,25 +69,27 @@ class ExecApp
69
69
  #
70
70
  # Run an application 'cmd' in a separate thread and monitor
71
71
  # its stdout. Also send status reports to the 'observer' by
72
- # calling its "on_app_event(eventType, appId, message")"
72
+ # calling its "call(eventType, appId, message")"
73
73
  #
74
74
  # @param id ID of application (used for reporting)
75
75
  # @param observer Observer of application's progress
76
76
  # @param cmd Command path and args
77
77
  # @param map_std_err_to_out If true report stderr as stdin [false]
78
78
  #
79
- def initialize(id, observer, cmd, map_std_err_to_out = false)
79
+ def initialize(id, cmd, map_std_err_to_out = false, working_directory = nil, &observer)
80
80
 
81
81
  @id = id
82
82
  @observer = observer
83
83
  @@all_apps[id] = self
84
+ @exit_status = nil
85
+ @threads = []
84
86
 
85
87
  pw = IO::pipe # pipe[0] for read, pipe[1] for write
86
88
  pr = IO::pipe
87
89
  pe = IO::pipe
88
90
 
89
91
  logger.debug "Starting application '#{id}' - cmd: '#{cmd}'"
90
- @observer.on_app_event(:STARTED, id, cmd)
92
+ @observer.call(:STARTED, id, cmd)
91
93
  @pid = fork {
92
94
  # child will remap pipes to std and exec cmd
93
95
  pw[1].close
@@ -103,6 +105,7 @@ class ExecApp
103
105
  pe[1].close
104
106
 
105
107
  begin
108
+ Dir.chdir working_directory if working_directory
106
109
  exec(cmd)
107
110
  rescue => ex
108
111
  cmd = cmd.join(' ') if cmd.kind_of?(Array)
@@ -118,23 +121,31 @@ class ExecApp
118
121
  monitor_pipe(:stdout, pr[0])
119
122
  monitor_pipe(map_std_err_to_out ? :stdout : :stderr, pe[0])
120
123
  # Create thread which waits for application to exit
121
- Thread.new(id, @pid) do |id, pid|
124
+ @threads << Thread.new(id, @pid) do |id, pid|
122
125
  ret = Process.waitpid(pid)
123
- status = $?
126
+ @exit_status = $?.exitstatus
124
127
  @@all_apps.delete(@id)
125
128
  # app finished
126
- if (status == 0) || @clean_exit
127
- s = "OK"
129
+ if (@exit_status == 0) || @clean_exit
128
130
  logger.debug "Application '#{id}' finished"
129
131
  else
130
- s = "ERROR"
131
- logger.debug "Application '#{id}' failed (code=#{status})"
132
+ logger.debug "Application '#{id}' failed (code=#{@exit_status})"
132
133
  end
133
- @observer.on_app_event("DONE.#{s}", @id, "status: #{status}")
134
134
  end
135
135
  @stdin = pw[1]
136
+
137
+ # wait for done in yet another thread
138
+ Thread.new do
139
+ @threads.each {|t| t.join }
140
+ if (@exit_status == 0) || @clean_exit
141
+ s = "OK"
142
+ else
143
+ s = "ERROR"
144
+ end
145
+ @observer.call("DONE.#{s}", @id, "status: #{@exit_status}")
146
+ end
136
147
  end
137
-
148
+
138
149
  private
139
150
 
140
151
  #
@@ -145,16 +156,19 @@ class ExecApp
145
156
  # @param pipe Pipe to read from
146
157
  #
147
158
  def monitor_pipe(name, pipe)
148
- Thread.new() do
159
+ @threads << Thread.new() do
149
160
  begin
150
161
  while true do
151
162
  s = pipe.readline.chomp
152
- @observer.on_app_event(name.to_s.upcase, @id, s)
163
+ #puts "#{name}: #{s}"
164
+ @observer.call(name.to_s.upcase, @id, s)
153
165
  end
154
166
  rescue EOFError
155
167
  # do nothing
156
- rescue Exception => err
168
+ #puts "++++ STOP MONITORING #{name}"
169
+ rescue => err
157
170
  logger.error "monitorApp(#{@id}): #{err}"
171
+ logger.debug "#{err}\n\t#{err.backtrace.join("\n\t")}"
158
172
  ensure
159
173
  pipe.close
160
174
  end
@@ -26,9 +26,10 @@ module OmfCommon
26
26
  }
27
27
  }
28
28
  @@message_class = nil
29
+ @@authenticate_messages = false
29
30
 
30
31
  def self.create(type, properties, body = {})
31
- @@message_class.create(type, properties, body)
32
+ @@message_class.create(type, properties || {}, body)
32
33
  end
33
34
 
34
35
  def self.create_inform_message(itype = nil, properties = {}, body = {})
@@ -36,10 +37,19 @@ module OmfCommon
36
37
  create(:inform, properties, body)
37
38
  end
38
39
 
39
- # Create and return a message by parsing 'str'
40
+ # Return true if all messages will be authenticated, return false otherwise
40
41
  #
41
- def self.parse(str)
42
- @@message_class.parse(str)
42
+ def self.authenticate?
43
+ @@authenticate_messages
44
+ end
45
+
46
+ # Parse message from 'str' and pass it to 'block'.
47
+ # If authnetication is on, the message will only be handed
48
+ # to 'block' if the source of the message can be authenticated.
49
+ #
50
+ def self.parse(str, content_type = nil, &block)
51
+ raise ArgumentError, 'Need message handling block' unless block
52
+ @@message_class.parse(str, content_type, &block)
43
53
  end
44
54
 
45
55
  def self.init(opts = {})
@@ -60,6 +70,7 @@ module OmfCommon
60
70
  else
61
71
  raise "Missing provider class info - :constructor"
62
72
  end
73
+ @@authenticate_messages = opts[:authenticate] if opts[:authenticate]
63
74
  end
64
75
 
65
76
  OMF_CORE_READ.each do |pname|
@@ -116,7 +127,15 @@ module OmfCommon
116
127
  raise NotImplementedError
117
128
  end
118
129
 
130
+ def properties
131
+ raise NotImplementedError
132
+ end
133
+
119
134
  def has_properties?
135
+ not properties.empty?
136
+ end
137
+
138
+ def guard?
120
139
  raise NotImplementedError
121
140
  end
122
141
 
@@ -138,11 +157,32 @@ module OmfCommon
138
157
  self.class.create_inform_message(itype, properties, body)
139
158
  end
140
159
 
160
+ # Fetch inform type
161
+ #
162
+ # When no format provided, return the value as it is.
163
+ #
164
+ # @param [Symbol] format to render itype, valid formats: :ruby, :frcp
165
+ #
166
+ def itype(format = nil)
167
+ if format && !_get_core(:itype).nil?
168
+ case format.to_sym
169
+ when :ruby
170
+ _get_core(:itype).to_s.downcase.gsub(/\./, '_')
171
+ when :frcp
172
+ _get_core(:itype).to_s.upcase.gsub(/_/, '.')
173
+ else
174
+ raise ArgumentError, "Unknown format '#{format}'. Please use ':ruby, :frcp' instead."
175
+ end
176
+ else
177
+ _get_core(:itype)
178
+ end
179
+ end
180
+
141
181
  def to_s
142
182
  raise NotImplementedError
143
183
  end
144
184
 
145
- def marshall
185
+ def marshall(include_cert = false)
146
186
  raise NotImplementedError
147
187
  end
148
188
 
@@ -171,6 +211,14 @@ module OmfCommon
171
211
  def _set_property(name, value, ns = nil)
172
212
  raise NotImplementedError
173
213
  end
214
+
215
+ def _set_core(key, value)
216
+ raise NotImplementedError
217
+ end
218
+
219
+ def _get_core(key)
220
+ raise NotImplementedError
221
+ end
174
222
  end
175
223
 
176
224
  end
@@ -1,42 +1,107 @@
1
1
 
2
2
  require 'json'
3
+ require 'omf_common/auth'
4
+ require 'json/jwt'
3
5
 
4
6
  module OmfCommon
5
7
  class Message
6
8
  class Json
7
9
  class Message < OmfCommon::Message
8
-
10
+
11
+ # This maps properties in the internal representation of
12
+ # a message to names used for the JSON message
13
+ #
14
+ @@key2json_key = {
15
+ operation: :op,
16
+ res_id: :rid
17
+ }
9
18
 
10
19
  def self.create(type, properties, body = {})
11
- if type == :request
20
+ if type == :request
12
21
  unless properties.kind_of?(Array)
13
22
  raise "Expected array, but got #{properties.class} for request message"
14
23
  end
15
24
  properties = {select: properties}
16
25
  elsif not properties.kind_of?(Hash)
17
26
  raise "Expected hash, but got #{properties.class}"
18
- end
27
+ end
19
28
  content = body.merge({
20
- operation: type,
29
+ op: type,
21
30
  mid: SecureRandom.uuid,
22
- properties: properties
31
+ props: properties
23
32
  })
24
33
  self.new(content)
25
34
  end
26
-
35
+
27
36
  def self.create_inform_message(itype = nil, properties = {}, body = {})
28
37
  body[:itype] = itype if itype
29
38
  create(:inform, properties, body)
30
39
  end
31
-
40
+
32
41
  # Create and return a message by parsing 'str'
33
42
  #
34
- def self.parse(str)
35
- content = JSON.parse(str, :symbolize_names => true)
36
- #puts content
37
- new(content)
43
+ def self.parse(str, content_type, &block)
44
+ #puts "CT>> #{content_type}"
45
+ case content_type.to_s
46
+ when 'jwt'
47
+ content = parse_jwt(str, &block)
48
+ when 'text/json'
49
+ content = JSON.parse(str, :symbolize_names => true)
50
+ else
51
+ warn "Received message with unknown content type '#{content_type}'"
52
+ end
53
+ #puts "CTTT>> #{content}::#{content.class}"
54
+ msg = content ? new(content) : nil
55
+ block.call(msg) if msg
56
+ msg
57
+ end
58
+
59
+ def self.parse_jwt(jwt_string)
60
+ key_or_secret = :skip_verification
61
+ # Code lifted from 'json-jwt-0.4.3/lib/json/jwt.rb'
62
+ case jwt_string.count('.')
63
+ when 2 # JWT / JWS
64
+ header, claims, signature = jwt_string.split('.', 3).collect do |segment|
65
+ UrlSafeBase64.decode64 segment.to_s
66
+ end
67
+ header, claims = [header, claims].collect do |json|
68
+ #MultiJson.load(json).with_indifferent_access
69
+ JSON.parse(json, :symbolize_names => true)
70
+ end
71
+ signature_base_string = jwt_string.split('.')[0, 2].join('.')
72
+ jwt = JSON::JWT.new claims
73
+ jwt.header = header
74
+ jwt.signature = signature
75
+
76
+ # NOTE:
77
+ # Some JSON libraries generates wrong format of JSON (spaces between keys and values etc.)
78
+ # So we need to use raw base64 strings for signature verification.
79
+ unless src = claims[:iss]
80
+ warn "JWT: Message is missing :iss element"
81
+ return nil
82
+ end
83
+ if cert_pem = claims[:crt]
84
+ # let's the credential store take care of it
85
+ OmfCommon::Auth::CertificateStore.instance.register_x509(cert_pem, src)
86
+ end
87
+ unless cert = OmfCommon::Auth::CertificateStore.instance.cert_for(src)
88
+ warn "JWT: Can't find cert for issuer '#{src}'"
89
+ return nil
90
+ end
91
+
92
+ unless cert.verify_cert
93
+ warn "JWT: Invalid certificate '#{cert.to_s}', NOT signed by root certificate."
94
+ end
95
+
96
+ #puts ">>> #{cert.to_x509.public_key}::#{signature_base_string}"
97
+ jwt.verify signature_base_string, cert.to_x509.public_key #unless key_or_secret == :skip_verification
98
+ JSON.parse(claims[:cnt], :symbolize_names => true)
99
+ else
100
+ warn('JWT: Invalid Format. JWT should include 2 or 3 dots.')
101
+ return nil
102
+ end
38
103
  end
39
-
104
+
40
105
  def each_property(&block)
41
106
  @properties.each do |k, v|
42
107
  #unless INTERNAL_PROPS.include?(k.to_sym)
@@ -44,16 +109,21 @@ module OmfCommon
44
109
  #end
45
110
  end
46
111
  end
47
-
112
+
113
+ def properties
114
+ @properties
115
+ end
116
+
117
+
48
118
  def has_properties?
49
119
  not @properties.empty?
50
120
  end
51
-
121
+
52
122
  def valid?
53
123
  true # don't do schema verification , yet
54
124
  end
55
-
56
- # Loop over all the unbound (sent without a value) properties
125
+
126
+ # Loop over all the unbound (sent without a value) properties
57
127
  # of a request message.
58
128
  #
59
129
  def each_unbound_request_property(&block)
@@ -66,9 +136,9 @@ module OmfCommon
66
136
  block.call(el)
67
137
  end
68
138
  end
69
- end
70
-
71
- # Loop over all the bound (sent with a value) properties
139
+ end
140
+
141
+ # Loop over all the bound (sent with a value) properties
72
142
  # of a request message.
73
143
  #
74
144
  def each_bound_request_property(&block)
@@ -83,47 +153,87 @@ module OmfCommon
83
153
  end
84
154
  end
85
155
  end
86
- end
87
-
88
-
156
+ end
157
+
158
+
89
159
  def to_s
90
160
  "JsonMessage: #{@content.inspect}"
91
161
  end
92
-
93
- def marshall
94
- @content.to_json
162
+
163
+ # Marshall message into a string to be shipped across the network.
164
+ # Depending on authentication setting, the message will be signed as
165
+ # well, or maybe even dropped.
166
+ #
167
+ # @param [Topic] topic for which to marshall
168
+ #
169
+ def marshall(topic)
170
+ #puts "MARSHALL: #{@content.inspect} - #{@properties.to_hash.inspect}"
171
+ raise "Missing SRC declaration in #{@content}" unless @content[:src]
172
+ if @content[:src].is_a? OmfCommon::Comm::Topic
173
+ @content[:src] = @content[:src].id
174
+ end
175
+ #raise 'local/local' if @content[:src].id.match 'local:/local'
176
+ #puts @content.inspect
177
+ payload = @content.to_json
178
+ if self.class.authenticate?
179
+ src = @content[:src]
180
+ cert = OmfCommon::Auth::CertificateStore.instance.cert_for(src)
181
+ if cert && cert.can_sign?
182
+ debug "Found cert for '#{src} - #{cert}"
183
+ msg = {cnt: payload, iss: src}
184
+ unless @certOnTopic[k = [topic, src]]
185
+ # first time for this src on this topic, so let's send the cert along
186
+ msg[:crt] = cert.to_pem_compact
187
+ #ALWAYS ADD CERT @certOnTopic[k] = Time.now
188
+ end
189
+ #:RS256, :RS384, :RS512
190
+ p = JSON::JWT.new(msg).sign(cert.key , :RS256).to_s
191
+ #puts "SIGNED>> #{msg}"
192
+ return ['jwt', p]
193
+ end
194
+ end
195
+ ['text/json', payload]
95
196
  end
96
-
97
- private
197
+
198
+ private
98
199
  def initialize(content)
99
200
  debug "Create message: #{content.inspect}"
100
- @content = content
101
- unless op = content[:operation]
201
+ unless op = content[:op]
102
202
  raise "Missing message type (:operation)"
103
203
  end
104
- content[:operation] = op.to_sym # needs to be symbol
105
- @properties = content[:properties] || []
204
+ @content = {}
205
+ content[:op] = op.to_sym # needs to be symbol
206
+ if src = content[:src]
207
+ content[:src] = OmfCommon.comm.create_topic(src)
208
+ end
209
+ content.each {|k,v| _set_core(k, v)}
210
+ @properties = content[:props] || []
106
211
  #@properties = Hashie::Mash.new(content[:properties])
212
+ @authenticate = self.class.authenticate?
213
+ # keep track if we sent local certs on a topic. Should do this the first time
214
+ @certOnTopic = {}
107
215
  end
108
-
216
+
109
217
  def _set_core(key, value)
110
- @content[key] = value
218
+ @content[(@@key2json_key[key] || key).to_sym] = value
111
219
  end
112
220
 
113
221
  def _get_core(key)
114
- @content[key]
222
+ @content[@@key2json_key[key] || key]
115
223
  end
116
-
117
- def _set_property(key, value)
224
+
225
+ def _set_property(key, value, ns = nil)
226
+ warn "Can't handle namespaces yet" if ns
118
227
  @properties[key] = value
119
228
  end
120
229
 
121
- def _get_property(key)
230
+ def _get_property(key, ns = nil)
231
+ warn "Can't handle namespaces yet" if ns
122
232
  #puts key
123
233
  @properties[key]
124
234
  end
125
-
235
+
126
236
  end # class
127
237
  end
128
238
  end
129
- end
239
+ end