omf_common 6.0.0 → 6.0.2.pre.1

Sign up to get free protection for your applications and to get access to all the features.
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