scout 5.7.1 → 5.7.2.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (251) hide show
  1. data/CHANGELOG.markdown +5 -0
  2. data/lib/scout/command/run.rb +1 -1
  3. data/lib/scout/server.rb +2 -1
  4. data/lib/scout/streamer.rb +3 -2
  5. data/lib/scout/streamer_daemon.rb +4 -4
  6. data/lib/scout/version.rb +1 -1
  7. data/test/scout_test.rb +7 -8
  8. data/vendor/httpclient/README.txt +759 -0
  9. data/vendor/httpclient/bin/httpclient +65 -0
  10. data/vendor/httpclient/lib/hexdump.rb +50 -0
  11. data/vendor/httpclient/lib/http-access2.rb +55 -0
  12. data/vendor/httpclient/lib/http-access2/cookie.rb +1 -0
  13. data/vendor/httpclient/lib/http-access2/http.rb +1 -0
  14. data/vendor/httpclient/lib/httpclient.rb +1156 -0
  15. data/vendor/httpclient/lib/httpclient/auth.rb +899 -0
  16. data/vendor/httpclient/lib/httpclient/cacert.p7s +1912 -0
  17. data/vendor/httpclient/lib/httpclient/connection.rb +88 -0
  18. data/vendor/httpclient/lib/httpclient/cookie.rb +438 -0
  19. data/vendor/httpclient/lib/httpclient/http.rb +1046 -0
  20. data/vendor/httpclient/lib/httpclient/include_client.rb +83 -0
  21. data/vendor/httpclient/lib/httpclient/session.rb +1025 -0
  22. data/vendor/httpclient/lib/httpclient/ssl_config.rb +403 -0
  23. data/vendor/httpclient/lib/httpclient/timeout.rb +140 -0
  24. data/vendor/httpclient/lib/httpclient/util.rb +178 -0
  25. data/vendor/httpclient/lib/httpclient/version.rb +3 -0
  26. data/vendor/httpclient/lib/oauthclient.rb +110 -0
  27. data/vendor/httpclient/sample/async.rb +8 -0
  28. data/vendor/httpclient/sample/auth.rb +11 -0
  29. data/vendor/httpclient/sample/cookie.rb +18 -0
  30. data/vendor/httpclient/sample/dav.rb +103 -0
  31. data/vendor/httpclient/sample/howto.rb +49 -0
  32. data/vendor/httpclient/sample/oauth_buzz.rb +57 -0
  33. data/vendor/httpclient/sample/oauth_friendfeed.rb +59 -0
  34. data/vendor/httpclient/sample/oauth_salesforce_10.rb +63 -0
  35. data/vendor/httpclient/sample/oauth_twitter.rb +61 -0
  36. data/vendor/httpclient/sample/ssl/0cert.pem +22 -0
  37. data/vendor/httpclient/sample/ssl/0key.pem +30 -0
  38. data/vendor/httpclient/sample/ssl/1000cert.pem +19 -0
  39. data/vendor/httpclient/sample/ssl/1000key.pem +18 -0
  40. data/vendor/httpclient/sample/ssl/htdocs/index.html +10 -0
  41. data/vendor/httpclient/sample/ssl/ssl_client.rb +22 -0
  42. data/vendor/httpclient/sample/ssl/webrick_httpsd.rb +29 -0
  43. data/vendor/httpclient/sample/stream.rb +21 -0
  44. data/vendor/httpclient/sample/thread.rb +27 -0
  45. data/vendor/httpclient/sample/wcat.rb +21 -0
  46. data/vendor/httpclient/test/ca-chain.cert +44 -0
  47. data/vendor/httpclient/test/ca.cert +23 -0
  48. data/vendor/httpclient/test/client.cert +19 -0
  49. data/vendor/httpclient/test/client.key +15 -0
  50. data/vendor/httpclient/test/helper.rb +129 -0
  51. data/vendor/httpclient/test/htdigest +1 -0
  52. data/vendor/httpclient/test/htpasswd +2 -0
  53. data/vendor/httpclient/test/runner.rb +2 -0
  54. data/vendor/httpclient/test/server.cert +19 -0
  55. data/vendor/httpclient/test/server.key +15 -0
  56. data/vendor/httpclient/test/sslsvr.rb +65 -0
  57. data/vendor/httpclient/test/subca.cert +21 -0
  58. data/vendor/httpclient/test/test_auth.rb +321 -0
  59. data/vendor/httpclient/test/test_cookie.rb +391 -0
  60. data/vendor/httpclient/test/test_hexdump.rb +14 -0
  61. data/vendor/httpclient/test/test_http-access2.rb +507 -0
  62. data/vendor/httpclient/test/test_httpclient.rb +1783 -0
  63. data/vendor/httpclient/test/test_include_client.rb +52 -0
  64. data/vendor/httpclient/test/test_ssl.rb +235 -0
  65. data/vendor/multi_json/.document +5 -0
  66. data/vendor/multi_json/.rspec +3 -0
  67. data/vendor/multi_json/.travis.yml +11 -0
  68. data/vendor/multi_json/.yardopts +6 -0
  69. data/vendor/multi_json/CHANGELOG.md +169 -0
  70. data/vendor/multi_json/CONTRIBUTING.md +46 -0
  71. data/vendor/multi_json/Gemfile +31 -0
  72. data/vendor/multi_json/LICENSE.md +20 -0
  73. data/vendor/multi_json/README.md +109 -0
  74. data/vendor/multi_json/Rakefile +12 -0
  75. data/vendor/multi_json/lib/multi_json.rb +157 -0
  76. data/vendor/multi_json/lib/multi_json/adapter.rb +48 -0
  77. data/vendor/multi_json/lib/multi_json/adapters/gson.rb +19 -0
  78. data/vendor/multi_json/lib/multi_json/adapters/jr_jackson.rb +19 -0
  79. data/vendor/multi_json/lib/multi_json/adapters/json_common.rb +25 -0
  80. data/vendor/multi_json/lib/multi_json/adapters/json_gem.rb +11 -0
  81. data/vendor/multi_json/lib/multi_json/adapters/json_pure.rb +11 -0
  82. data/vendor/multi_json/lib/multi_json/adapters/nsjsonserialization.rb +34 -0
  83. data/vendor/multi_json/lib/multi_json/adapters/oj.rb +24 -0
  84. data/vendor/multi_json/lib/multi_json/adapters/ok_json.rb +22 -0
  85. data/vendor/multi_json/lib/multi_json/adapters/yajl.rb +19 -0
  86. data/vendor/multi_json/lib/multi_json/convertible_hash_keys.rb +43 -0
  87. data/vendor/multi_json/lib/multi_json/load_error.rb +11 -0
  88. data/vendor/multi_json/lib/multi_json/options.rb +48 -0
  89. data/vendor/multi_json/lib/multi_json/vendor/okjson.rb +606 -0
  90. data/vendor/multi_json/lib/multi_json/version.rb +20 -0
  91. data/vendor/multi_json/multi_json.gemspec +22 -0
  92. data/vendor/multi_json/spec/adapter_shared_example.rb +235 -0
  93. data/vendor/multi_json/spec/has_options.rb +119 -0
  94. data/vendor/multi_json/spec/helper.rb +35 -0
  95. data/vendor/multi_json/spec/json_common_shared_example.rb +30 -0
  96. data/vendor/multi_json/spec/multi_json_spec.rb +226 -0
  97. data/vendor/{signature → pusher}/.document +0 -0
  98. data/vendor/{json_pure/diagrams/.keep → pusher/.gemtest} +0 -0
  99. data/vendor/pusher/.gitignore +23 -0
  100. data/vendor/pusher/.travis.yml +15 -0
  101. data/vendor/pusher/Gemfile +2 -0
  102. data/vendor/{pusher-gem → pusher}/LICENSE +1 -1
  103. data/vendor/pusher/README.md +186 -0
  104. data/vendor/{pusher-gem → pusher}/Rakefile +0 -0
  105. data/vendor/{pusher-gem → pusher}/examples/async_message.rb +0 -0
  106. data/vendor/pusher/lib/pusher.rb +60 -0
  107. data/vendor/{pusher-gem → pusher}/lib/pusher/channel.rb +47 -54
  108. data/vendor/pusher/lib/pusher/client.rb +306 -0
  109. data/vendor/pusher/lib/pusher/request.rb +107 -0
  110. data/vendor/pusher/lib/pusher/resource.rb +36 -0
  111. data/vendor/pusher/lib/pusher/webhook.rb +110 -0
  112. data/vendor/{pusher-gem → pusher}/pusher.gemspec +6 -5
  113. data/vendor/pusher/spec/channel_spec.rb +127 -0
  114. data/vendor/pusher/spec/client_spec.rb +464 -0
  115. data/vendor/{pusher-gem → pusher}/spec/spec_helper.rb +12 -0
  116. data/vendor/pusher/spec/web_hook_spec.rb +117 -0
  117. data/vendor/signature/.travis.yml +15 -0
  118. data/vendor/signature/Gemfile +1 -1
  119. data/vendor/signature/README.md +38 -28
  120. data/vendor/signature/lib/signature.rb +97 -15
  121. data/vendor/signature/lib/signature/query_encoder.rb +47 -0
  122. data/vendor/signature/lib/signature/version.rb +1 -1
  123. data/vendor/signature/signature.gemspec +3 -2
  124. data/vendor/signature/spec/signature_spec.rb +164 -55
  125. data/vendor/signature/spec/spec_helper.rb +2 -3
  126. metadata +120 -145
  127. data/vendor/json_pure/.gitignore +0 -12
  128. data/vendor/json_pure/.travis.yml +0 -20
  129. data/vendor/json_pure/CHANGES +0 -282
  130. data/vendor/json_pure/COPYING +0 -58
  131. data/vendor/json_pure/COPYING-json-jruby +0 -57
  132. data/vendor/json_pure/GPL +0 -340
  133. data/vendor/json_pure/Gemfile +0 -11
  134. data/vendor/json_pure/README-json-jruby.markdown +0 -33
  135. data/vendor/json_pure/README.rdoc +0 -358
  136. data/vendor/json_pure/Rakefile +0 -412
  137. data/vendor/json_pure/TODO +0 -1
  138. data/vendor/json_pure/VERSION +0 -1
  139. data/vendor/json_pure/data/example.json +0 -1
  140. data/vendor/json_pure/data/index.html +0 -38
  141. data/vendor/json_pure/data/prototype.js +0 -4184
  142. data/vendor/json_pure/ext/json/ext/fbuffer/fbuffer.h +0 -181
  143. data/vendor/json_pure/ext/json/ext/generator/depend +0 -1
  144. data/vendor/json_pure/ext/json/ext/generator/extconf.rb +0 -14
  145. data/vendor/json_pure/ext/json/ext/generator/generator.c +0 -1435
  146. data/vendor/json_pure/ext/json/ext/generator/generator.h +0 -148
  147. data/vendor/json_pure/ext/json/ext/parser/depend +0 -1
  148. data/vendor/json_pure/ext/json/ext/parser/extconf.rb +0 -13
  149. data/vendor/json_pure/ext/json/ext/parser/parser.c +0 -2204
  150. data/vendor/json_pure/ext/json/ext/parser/parser.h +0 -77
  151. data/vendor/json_pure/ext/json/ext/parser/parser.rl +0 -927
  152. data/vendor/json_pure/install.rb +0 -23
  153. data/vendor/json_pure/java/src/json/ext/ByteListTranscoder.java +0 -167
  154. data/vendor/json_pure/java/src/json/ext/Generator.java +0 -444
  155. data/vendor/json_pure/java/src/json/ext/GeneratorMethods.java +0 -232
  156. data/vendor/json_pure/java/src/json/ext/GeneratorService.java +0 -43
  157. data/vendor/json_pure/java/src/json/ext/GeneratorState.java +0 -543
  158. data/vendor/json_pure/java/src/json/ext/OptionsReader.java +0 -114
  159. data/vendor/json_pure/java/src/json/ext/Parser.java +0 -2644
  160. data/vendor/json_pure/java/src/json/ext/Parser.rl +0 -968
  161. data/vendor/json_pure/java/src/json/ext/ParserService.java +0 -35
  162. data/vendor/json_pure/java/src/json/ext/RuntimeInfo.java +0 -121
  163. data/vendor/json_pure/java/src/json/ext/StringDecoder.java +0 -167
  164. data/vendor/json_pure/java/src/json/ext/StringEncoder.java +0 -106
  165. data/vendor/json_pure/java/src/json/ext/Utils.java +0 -89
  166. data/vendor/json_pure/json-java.gemspec +0 -23
  167. data/vendor/json_pure/json.gemspec +0 -37
  168. data/vendor/json_pure/json_pure.gemspec +0 -39
  169. data/vendor/json_pure/lib/json.rb +0 -62
  170. data/vendor/json_pure/lib/json/add/bigdecimal.rb +0 -28
  171. data/vendor/json_pure/lib/json/add/complex.rb +0 -22
  172. data/vendor/json_pure/lib/json/add/core.rb +0 -11
  173. data/vendor/json_pure/lib/json/add/date.rb +0 -34
  174. data/vendor/json_pure/lib/json/add/date_time.rb +0 -50
  175. data/vendor/json_pure/lib/json/add/exception.rb +0 -31
  176. data/vendor/json_pure/lib/json/add/ostruct.rb +0 -31
  177. data/vendor/json_pure/lib/json/add/range.rb +0 -29
  178. data/vendor/json_pure/lib/json/add/rational.rb +0 -22
  179. data/vendor/json_pure/lib/json/add/regexp.rb +0 -30
  180. data/vendor/json_pure/lib/json/add/struct.rb +0 -30
  181. data/vendor/json_pure/lib/json/add/symbol.rb +0 -25
  182. data/vendor/json_pure/lib/json/add/time.rb +0 -38
  183. data/vendor/json_pure/lib/json/common.rb +0 -487
  184. data/vendor/json_pure/lib/json/ext.rb +0 -21
  185. data/vendor/json_pure/lib/json/ext/.keep +0 -0
  186. data/vendor/json_pure/lib/json/generic_object.rb +0 -70
  187. data/vendor/json_pure/lib/json/pure.rb +0 -21
  188. data/vendor/json_pure/lib/json/pure/generator.rb +0 -522
  189. data/vendor/json_pure/lib/json/pure/parser.rb +0 -359
  190. data/vendor/json_pure/lib/json/version.rb +0 -8
  191. data/vendor/json_pure/tests/fixtures/fail1.json +0 -1
  192. data/vendor/json_pure/tests/fixtures/fail10.json +0 -1
  193. data/vendor/json_pure/tests/fixtures/fail11.json +0 -1
  194. data/vendor/json_pure/tests/fixtures/fail12.json +0 -1
  195. data/vendor/json_pure/tests/fixtures/fail13.json +0 -1
  196. data/vendor/json_pure/tests/fixtures/fail14.json +0 -1
  197. data/vendor/json_pure/tests/fixtures/fail18.json +0 -1
  198. data/vendor/json_pure/tests/fixtures/fail19.json +0 -1
  199. data/vendor/json_pure/tests/fixtures/fail2.json +0 -1
  200. data/vendor/json_pure/tests/fixtures/fail20.json +0 -1
  201. data/vendor/json_pure/tests/fixtures/fail21.json +0 -1
  202. data/vendor/json_pure/tests/fixtures/fail22.json +0 -1
  203. data/vendor/json_pure/tests/fixtures/fail23.json +0 -1
  204. data/vendor/json_pure/tests/fixtures/fail24.json +0 -1
  205. data/vendor/json_pure/tests/fixtures/fail25.json +0 -1
  206. data/vendor/json_pure/tests/fixtures/fail27.json +0 -2
  207. data/vendor/json_pure/tests/fixtures/fail28.json +0 -2
  208. data/vendor/json_pure/tests/fixtures/fail3.json +0 -1
  209. data/vendor/json_pure/tests/fixtures/fail4.json +0 -1
  210. data/vendor/json_pure/tests/fixtures/fail5.json +0 -1
  211. data/vendor/json_pure/tests/fixtures/fail6.json +0 -1
  212. data/vendor/json_pure/tests/fixtures/fail7.json +0 -1
  213. data/vendor/json_pure/tests/fixtures/fail8.json +0 -1
  214. data/vendor/json_pure/tests/fixtures/fail9.json +0 -1
  215. data/vendor/json_pure/tests/fixtures/pass1.json +0 -56
  216. data/vendor/json_pure/tests/fixtures/pass15.json +0 -1
  217. data/vendor/json_pure/tests/fixtures/pass16.json +0 -1
  218. data/vendor/json_pure/tests/fixtures/pass17.json +0 -1
  219. data/vendor/json_pure/tests/fixtures/pass2.json +0 -1
  220. data/vendor/json_pure/tests/fixtures/pass26.json +0 -1
  221. data/vendor/json_pure/tests/fixtures/pass3.json +0 -6
  222. data/vendor/json_pure/tests/setup_variant.rb +0 -11
  223. data/vendor/json_pure/tests/test_json.rb +0 -545
  224. data/vendor/json_pure/tests/test_json_addition.rb +0 -196
  225. data/vendor/json_pure/tests/test_json_encoding.rb +0 -65
  226. data/vendor/json_pure/tests/test_json_fixtures.rb +0 -35
  227. data/vendor/json_pure/tests/test_json_generate.rb +0 -322
  228. data/vendor/json_pure/tests/test_json_generic_object.rb +0 -75
  229. data/vendor/json_pure/tests/test_json_string_matching.rb +0 -39
  230. data/vendor/json_pure/tests/test_json_unicode.rb +0 -72
  231. data/vendor/json_pure/tools/fuzz.rb +0 -139
  232. data/vendor/json_pure/tools/server.rb +0 -62
  233. data/vendor/pusher-gem/Gemfile +0 -2
  234. data/vendor/pusher-gem/README.md +0 -80
  235. data/vendor/pusher-gem/lib/pusher.rb +0 -107
  236. data/vendor/pusher-gem/lib/pusher/request.rb +0 -107
  237. data/vendor/pusher-gem/spec/channel_spec.rb +0 -274
  238. data/vendor/pusher-gem/spec/pusher_spec.rb +0 -87
  239. data/vendor/ruby-hmac/History.txt +0 -15
  240. data/vendor/ruby-hmac/Manifest.txt +0 -11
  241. data/vendor/ruby-hmac/README.md +0 -41
  242. data/vendor/ruby-hmac/Rakefile +0 -23
  243. data/vendor/ruby-hmac/lib/hmac-md5.rb +0 -11
  244. data/vendor/ruby-hmac/lib/hmac-rmd160.rb +0 -11
  245. data/vendor/ruby-hmac/lib/hmac-sha1.rb +0 -11
  246. data/vendor/ruby-hmac/lib/hmac-sha2.rb +0 -25
  247. data/vendor/ruby-hmac/lib/hmac.rb +0 -118
  248. data/vendor/ruby-hmac/lib/ruby_hmac.rb +0 -2
  249. data/vendor/ruby-hmac/ruby-hmac.gemspec +0 -33
  250. data/vendor/ruby-hmac/test/test_hmac.rb +0 -89
  251. data/vendor/signature/VERSION +0 -1
@@ -0,0 +1,107 @@
1
+ require 'signature'
2
+ require 'digest/md5'
3
+ require 'multi_json'
4
+
5
+ module Pusher
6
+ class Request
7
+ attr_reader :body, :params
8
+
9
+ def initialize(client, verb, uri, params, body = nil)
10
+ @client, @verb, @uri = client, verb, uri
11
+ @head = {}
12
+
13
+ @body = body
14
+ if body
15
+ params[:body_md5] = Digest::MD5.hexdigest(body)
16
+ @head['Content-Type'] = 'application/json'
17
+ end
18
+
19
+ request = Signature::Request.new(verb.to_s.upcase, uri.path, params)
20
+ request.sign(client.authentication_token)
21
+ @params = request.signed_params
22
+ end
23
+
24
+ def send_sync
25
+ http = @client.sync_http_client
26
+
27
+ begin
28
+ response = http.request(@verb, @uri, @params, @body, @head)
29
+ rescue HTTPClient::BadResponseError, HTTPClient::TimeoutError,
30
+ SocketError, Errno::ECONNREFUSED => e
31
+ error = Pusher::HTTPError.new("#{e.message} (#{e.class})")
32
+ error.original_error = e
33
+ raise error
34
+ end
35
+
36
+ body = response.body ? response.body.chomp : nil
37
+
38
+ return handle_response(response.code.to_i, body)
39
+ end
40
+
41
+ def send_async
42
+ if defined?(EventMachine) && EventMachine.reactor_running?
43
+ http_client = @client.em_http_client(@uri)
44
+ df = EM::DefaultDeferrable.new
45
+
46
+ http = case @verb
47
+ when :post
48
+ http_client.post({
49
+ :query => @params, :body => @body, :head => @head
50
+ })
51
+ when :get
52
+ http_client.get({
53
+ :query => @params, :head => @head
54
+ })
55
+ else
56
+ raise "Unsuported verb"
57
+ end
58
+ http.callback {
59
+ begin
60
+ df.succeed(handle_response(http.response_header.status, http.response.chomp))
61
+ rescue => e
62
+ df.fail(e)
63
+ end
64
+ }
65
+ http.errback { |e|
66
+ message = "Network error connecting to pusher (#{http.error})"
67
+ Pusher.logger.debug(message)
68
+ df.fail(Error.new(message))
69
+ }
70
+
71
+ return df
72
+ else
73
+ http = @client.sync_http_client
74
+
75
+ return http.request_async(@verb, @uri, @params, @body, @head)
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def handle_response(status_code, body)
82
+ case status_code
83
+ when 200
84
+ return symbolize_first_level(MultiJson.decode(body))
85
+ when 202
86
+ return true
87
+ when 400
88
+ raise Error, "Bad request: #{body}"
89
+ when 401
90
+ raise AuthenticationError, body
91
+ when 404
92
+ raise Error, "404 Not found (#{@uri.path})"
93
+ when 407
94
+ raise Error, "Proxy Authentication Required"
95
+ else
96
+ raise Error, "Unknown error (status code #{status_code}): #{body}"
97
+ end
98
+ end
99
+
100
+ def symbolize_first_level(hash)
101
+ hash.inject({}) do |result, (key, value)|
102
+ result[key.to_sym] = value
103
+ result
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,36 @@
1
+ module Pusher
2
+ class Resource
3
+ def initialize(client, path)
4
+ @client = client
5
+ @path = path
6
+ end
7
+
8
+ def get(params)
9
+ create_request(:get, params).send_sync
10
+ end
11
+
12
+ def get_async(params)
13
+ create_request(:get, params).send_async
14
+ end
15
+
16
+ def post(params)
17
+ body = MultiJson.encode(params)
18
+ create_request(:post, {}, body).send_sync
19
+ end
20
+
21
+ def post_async(params)
22
+ body = MultiJson.encode(params)
23
+ create_request(:post, {}, body).send_async
24
+ end
25
+
26
+ private
27
+
28
+ def create_request(verb, params, body = nil)
29
+ Request.new(@client, verb, url, params, body)
30
+ end
31
+
32
+ def url
33
+ @_url ||= @client.url(@path)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,110 @@
1
+ require 'multi_json'
2
+ require 'openssl'
3
+
4
+ module Pusher
5
+ # Used to parse and authenticate WebHooks
6
+ #
7
+ # @example Sinatra
8
+ # post '/webhooks' do
9
+ # webhook = Pusher::WebHook.new(request)
10
+ # if webhook.valid?
11
+ # webhook.events.each do |event|
12
+ # case event["name"]
13
+ # when 'channel_occupied'
14
+ # puts "Channel occupied: #{event["channel"]}"
15
+ # when 'channel_vacated'
16
+ # puts "Channel vacated: #{event["channel"]}"
17
+ # end
18
+ # end
19
+ # else
20
+ # status 401
21
+ # end
22
+ # return
23
+ # end
24
+ #
25
+ class WebHook
26
+ attr_reader :key, :signature
27
+
28
+ # Provide either a Rack::Request or a Hash containing :key, :signature,
29
+ # :body, and :content_type (optional)
30
+ #
31
+ def initialize(request, client = Pusher)
32
+ @client = client
33
+ # Should work without Rack
34
+ if defined?(Rack::Request) && request.kind_of?(Rack::Request)
35
+ @key = request.env['HTTP_X_PUSHER_KEY']
36
+ @signature = request.env["HTTP_X_PUSHER_SIGNATURE"]
37
+ @content_type = request.content_type
38
+
39
+ request.body.rewind
40
+ @body = request.body.read
41
+ request.body.rewind
42
+ else
43
+ @key, @signature, @body = request.values_at(:key, :signature, :body)
44
+ @content_type = request[:content_type] || 'application/json'
45
+ end
46
+ end
47
+
48
+ # Returns whether the WebHook is valid by checking that the signature
49
+ # matches the configured key & secret. In the case that the webhook is
50
+ # invalid, the reason is logged
51
+ #
52
+ # @param extra_tokens [Hash] If you have extra tokens for your Pusher app, you can specify them so that they're used to attempt validation.
53
+ #
54
+ def valid?(extra_tokens = nil)
55
+ extra_tokens = [extra_tokens] if extra_tokens.kind_of?(Hash)
56
+ if @key == @client.key
57
+ return check_signature(@client.secret)
58
+ elsif extra_tokens
59
+ extra_tokens.each do |token|
60
+ return check_signature(token[:secret]) if @key == token[:key]
61
+ end
62
+ end
63
+ Pusher.logger.warn "Received webhook with unknown key: #{key}"
64
+ return false
65
+ end
66
+
67
+ # Array of events (as Hashes) contained inside the webhook
68
+ #
69
+ def events
70
+ data["events"]
71
+ end
72
+
73
+ # The time at which the WebHook was initially triggered by Pusher, i.e.
74
+ # when the event occurred
75
+ #
76
+ # @return [Time]
77
+ #
78
+ def time
79
+ Time.at(data["time_ms"].to_f/1000)
80
+ end
81
+
82
+ # Access the parsed WebHook body
83
+ #
84
+ def data
85
+ @data ||= begin
86
+ case @content_type
87
+ when 'application/json'
88
+ MultiJson.decode(@body)
89
+ else
90
+ raise "Unknown Content-Type (#{@content_type})"
91
+ end
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ # Checks signature against secret and returns boolean
98
+ #
99
+ def check_signature(secret)
100
+ digest = OpenSSL::Digest::SHA256.new
101
+ expected = OpenSSL::HMAC.hexdigest(digest, secret, @body)
102
+ if @signature == expected
103
+ return true
104
+ else
105
+ Pusher.logger.warn "Received WebHook with invalid signature: got #{@signature}, expected #{expected}"
106
+ return false
107
+ end
108
+ end
109
+ end
110
+ end
@@ -3,7 +3,7 @@ $:.push File.expand_path("../lib", __FILE__)
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "pusher"
6
- s.version = "0.8.4"
6
+ s.version = "0.12.0"
7
7
  s.platform = Gem::Platform::RUBY
8
8
  s.authors = ["Pusher"]
9
9
  s.email = ["support@pusher.com"]
@@ -12,14 +12,15 @@ Gem::Specification.new do |s|
12
12
  s.description = %q{Wrapper for pusher.com REST api}
13
13
 
14
14
  s.add_dependency "multi_json", "~> 1.0"
15
- s.add_dependency "crack", "~> 0.1.0"
16
- s.add_dependency "ruby-hmac", "~> 0.4.0"
17
- s.add_dependency 'signature', "~> 0.1.2"
15
+ s.add_dependency 'signature', "~> 0.1.6"
16
+ s.add_dependency "httpclient", "~> 2.3.0"
17
+ s.add_dependency "jruby-openssl" if defined?(JRUBY_VERSION)
18
18
 
19
19
  s.add_development_dependency "rspec", "~> 2.0"
20
20
  s.add_development_dependency "webmock"
21
- s.add_development_dependency "em-http-request", "~> 1.0.0"
21
+ s.add_development_dependency "em-http-request", "~> 1.1.0"
22
22
  s.add_development_dependency "rake"
23
+ s.add_development_dependency "rack"
23
24
 
24
25
  s.files = `git ls-files`.split("\n")
25
26
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
@@ -0,0 +1,127 @@
1
+ require 'spec_helper'
2
+
3
+ describe Pusher::Channel do
4
+ before do
5
+ @client = Pusher::Client.new({
6
+ :app_id => '20',
7
+ :key => '12345678900000001',
8
+ :secret => '12345678900000001',
9
+ :host => 'api.pusherapp.com',
10
+ :port => 80,
11
+ })
12
+ @channel = @client['test_channel']
13
+ end
14
+
15
+ let(:pusher_url_regexp) { %r{/apps/20/events} }
16
+
17
+ def stub_post(status, body = nil)
18
+ options = {:status => status}
19
+ options.merge!({:body => body}) if body
20
+
21
+ stub_request(:post, pusher_url_regexp).to_return(options)
22
+ end
23
+
24
+ def stub_post_to_raise(e)
25
+ stub_request(:post, pusher_url_regexp).to_raise(e)
26
+ end
27
+
28
+ describe '#trigger!' do
29
+ it "should use @client.trigger internally" do
30
+ @client.should_receive(:trigger)
31
+ @channel.trigger('new_event', 'Some data')
32
+ end
33
+ end
34
+
35
+ describe '#trigger' do
36
+ it "should log failure if error raised in http call" do
37
+ stub_post_to_raise(HTTPClient::BadResponseError)
38
+
39
+ Pusher.logger.should_receive(:error).with("Exception from WebMock (HTTPClient::BadResponseError) (Pusher::HTTPError)")
40
+ Pusher.logger.should_receive(:debug) #backtrace
41
+ channel = Pusher::Channel.new(@client.url, 'test_channel', @client)
42
+ channel.trigger('new_event', 'Some data')
43
+ end
44
+
45
+ it "should log failure if Pusher returns an error response" do
46
+ stub_post 401, "some signature info"
47
+ Pusher.logger.should_receive(:error).with("some signature info (Pusher::AuthenticationError)")
48
+ Pusher.logger.should_receive(:debug) #backtrace
49
+ channel = Pusher::Channel.new(@client.url, 'test_channel', @client)
50
+ channel.trigger('new_event', 'Some data')
51
+ end
52
+ end
53
+
54
+ describe "#trigger_async" do
55
+ it "should use @client.trigger_async internally" do
56
+ @client.should_receive(:trigger_async)
57
+ @channel.trigger_async('new_event', 'Some data')
58
+ end
59
+ end
60
+
61
+ describe '#info' do
62
+ it "should call the Client#channel_info" do
63
+ @client.should_receive(:get).with("/channels/mychannel", anything)
64
+ @channel = @client['mychannel']
65
+ @channel.info
66
+ end
67
+
68
+ it "should assemble the requested attribes into the info option" do
69
+ @client.should_receive(:get).with(anything, {
70
+ :info => "user_count,connection_count"
71
+ })
72
+ @channel = @client['presence-foo']
73
+ @channel.info(%w{user_count connection_count})
74
+ end
75
+ end
76
+
77
+ describe "#authentication_string" do
78
+ def authentication_string(*data)
79
+ lambda { @channel.authentication_string(*data) }
80
+ end
81
+
82
+ it "should return an authentication string given a socket id" do
83
+ auth = @channel.authentication_string('socketid')
84
+
85
+ auth.should == '12345678900000001:827076f551e22451357939e4c7bb1200de29f921d5bf80b40d71668f9cd61c40'
86
+ end
87
+
88
+ it "should raise error if authentication is invalid" do
89
+ [nil, ''].each do |invalid|
90
+ authentication_string(invalid).should raise_error Pusher::Error
91
+ end
92
+ end
93
+
94
+ describe 'with extra string argument' do
95
+ it 'should be a string or nil' do
96
+ authentication_string('socketid', 123).should raise_error Pusher::Error
97
+ authentication_string('socketid', {}).should raise_error Pusher::Error
98
+
99
+ authentication_string('socketid', 'boom').should_not raise_error
100
+ authentication_string('socketid', nil).should_not raise_error
101
+ end
102
+
103
+ it "should return an authentication string given a socket id and custom args" do
104
+ auth = @channel.authentication_string('socketid', 'foobar')
105
+
106
+ auth.should == "12345678900000001:#{hmac(@client.secret, "socketid:test_channel:foobar")}"
107
+ end
108
+ end
109
+ end
110
+
111
+ describe '#authenticate' do
112
+ before :each do
113
+ @custom_data = {:uid => 123, :info => {:name => 'Foo'}}
114
+ end
115
+
116
+ it 'should return a hash with signature including custom data and data as json string' do
117
+ MultiJson.stub(:encode).with(@custom_data).and_return 'a json string'
118
+
119
+ response = @channel.authenticate('socketid', @custom_data)
120
+
121
+ response.should == {
122
+ :auth => "12345678900000001:#{hmac(@client.secret, "socketid:test_channel:a json string")}",
123
+ :channel_data => 'a json string'
124
+ }
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,464 @@
1
+ require 'spec_helper'
2
+
3
+ require 'em-http'
4
+
5
+ describe Pusher do
6
+ describe 'using multiple Client objects' do
7
+ before :each do
8
+ @client1 = Pusher::Client.new
9
+ @client2 = Pusher::Client.new
10
+
11
+ @client1.scheme = 'ws'
12
+ @client2.scheme = 'wss'
13
+ @client1.host = 'one'
14
+ @client2.host = 'two'
15
+ @client1.port = 81
16
+ @client2.port = 82
17
+ @client1.app_id = '1111'
18
+ @client2.app_id = '2222'
19
+ @client1.key = 'AAAA'
20
+ @client2.key = 'BBBB'
21
+ @client1.secret = 'aaaaaaaa'
22
+ @client2.secret = 'bbbbbbbb'
23
+ end
24
+
25
+ it "should send scheme messages to different objects" do
26
+ @client1.scheme.should_not == @client2.scheme
27
+ end
28
+
29
+ it "should send host messages to different objects" do
30
+ @client1.host.should_not == @client2.host
31
+ end
32
+
33
+ it "should send port messages to different objects" do
34
+ @client1.port.should_not == @client2.port
35
+ end
36
+
37
+ it "should send app_id messages to different objects" do
38
+ @client1.app_id.should_not == @client2.app_id
39
+ end
40
+
41
+ it "should send app_id messages to different objects" do
42
+ @client1.key.should_not == @client2.key
43
+ end
44
+
45
+ it "should send app_id messages to different objects" do
46
+ @client1.secret.should_not == @client2.secret
47
+ end
48
+
49
+ it "should send app_id messages to different objects" do
50
+ @client1.authentication_token.key.should_not == @client2.authentication_token.key
51
+ @client1.authentication_token.secret.should_not == @client2.authentication_token.secret
52
+ end
53
+
54
+ it "should send url messages to different objects" do
55
+ @client1.url.to_s.should_not == @client2.url.to_s
56
+ @client1.url = 'ws://one/apps/111'
57
+ @client2.url = 'wss://two/apps/222'
58
+ @client1.scheme.should_not == @client2.scheme
59
+ @client1.host.should_not == @client2.host
60
+ @client1.app_id.should_not == @client2.app_id
61
+ end
62
+
63
+ it "should send encrypted messages to different objects" do
64
+ @client1.encrypted = false
65
+ @client2.encrypted = true
66
+ @client1.scheme.should_not == @client2.scheme
67
+ @client1.port.should_not == @client2.port
68
+ end
69
+
70
+ it "should send [] messages to different objects" do
71
+ @client1['test'].should_not == @client2['test']
72
+ end
73
+
74
+ it "should send http_proxy messages to different objects" do
75
+ @client1.http_proxy = 'http://oneuser:onepassword@onehost:8080'
76
+ @client2.http_proxy = 'http://twouser:twopassword@twohost:8880'
77
+ @client1.http_proxy.should_not == @client2.http_proxy
78
+ end
79
+ end
80
+
81
+ # The behaviour should be the same when using the Client object, or the
82
+ # 'global' client delegated through the Pusher class
83
+ [lambda { Pusher }, lambda { Pusher::Client.new }].each do |client_gen|
84
+ before :each do
85
+ @client = client_gen.call
86
+ end
87
+
88
+ describe 'default configuration' do
89
+ it 'should be preconfigured for api host' do
90
+ @client.host.should == 'api.pusherapp.com'
91
+ end
92
+
93
+ it 'should be preconfigured for port 80' do
94
+ @client.port.should == 80
95
+ end
96
+
97
+ it 'should use standard logger if no other logger if defined' do
98
+ Pusher.logger.debug('foo')
99
+ Pusher.logger.should be_kind_of(Logger)
100
+ end
101
+ end
102
+
103
+ describe 'logging configuration' do
104
+ it "can be configured to use any logger" do
105
+ logger = double("ALogger")
106
+ logger.should_receive(:debug).with('foo')
107
+ Pusher.logger = logger
108
+ Pusher.logger.debug('foo')
109
+ Pusher.logger = nil
110
+ end
111
+ end
112
+
113
+ describe "configuration using url" do
114
+ it "should be possible to configure everything by setting the url" do
115
+ @client.url = "test://somekey:somesecret@api.staging.pusherapp.com:8080/apps/87"
116
+
117
+ @client.scheme.should == 'test'
118
+ @client.host.should == 'api.staging.pusherapp.com'
119
+ @client.port.should == 8080
120
+ @client.key.should == 'somekey'
121
+ @client.secret.should == 'somesecret'
122
+ @client.app_id.should == '87'
123
+ end
124
+
125
+ it "should override scheme and port when setting encrypted=true after url" do
126
+ @client.url = "http://somekey:somesecret@api.staging.pusherapp.com:8080/apps/87"
127
+ @client.encrypted = true
128
+
129
+ @client.scheme.should == 'https'
130
+ @client.port.should == 443
131
+ end
132
+ end
133
+
134
+ describe 'configuring a http proxy' do
135
+ it "should be possible to configure everything by setting the http_proxy" do
136
+ @client.http_proxy = 'http://someuser:somepassword@proxy.host.com:8080'
137
+
138
+ @client.proxy.should == {:scheme => 'http', :host => 'proxy.host.com', :port => 8080, :user => 'someuser', :password => 'somepassword'}
139
+ end
140
+ end
141
+
142
+ describe 'when configured' do
143
+ before :each do
144
+ @client.app_id = '20'
145
+ @client.key = '12345678900000001'
146
+ @client.secret = '12345678900000001'
147
+ end
148
+
149
+ describe '#[]' do
150
+ before do
151
+ @channel = @client['test_channel']
152
+ end
153
+
154
+ it 'should return a channel' do
155
+ @channel.should be_kind_of(Pusher::Channel)
156
+ end
157
+
158
+ %w{app_id key secret}.each do |config|
159
+ it "should raise exception if #{config} not configured" do
160
+ @client.send("#{config}=", nil)
161
+ lambda {
162
+ @client['test_channel']
163
+ }.should raise_error(Pusher::ConfigurationError)
164
+ end
165
+ end
166
+ end
167
+
168
+ describe '#channels' do
169
+ it "should call the correct URL and symbolise response correctly" do
170
+ api_path = %r{/apps/20/channels}
171
+ stub_request(:get, api_path).to_return({
172
+ :status => 200,
173
+ :body => MultiJson.encode('channels' => {
174
+ "channel1" => {},
175
+ "channel2" => {}
176
+ })
177
+ })
178
+ @client.channels.should == {
179
+ :channels => {
180
+ "channel1" => {},
181
+ "channel2" => {}
182
+ }
183
+ }
184
+ end
185
+ end
186
+
187
+ describe '#channel_info' do
188
+ it "should call correct URL and symbolise response" do
189
+ api_path = %r{/apps/20/channels/mychannel}
190
+ stub_request(:get, api_path).to_return({
191
+ :status => 200,
192
+ :body => MultiJson.encode({
193
+ 'occupied' => false,
194
+ })
195
+ })
196
+ @client.channel_info('mychannel').should == {
197
+ :occupied => false,
198
+ }
199
+ end
200
+ end
201
+
202
+ describe '#trigger' do
203
+ before :each do
204
+ @api_path = %r{/apps/20/events}
205
+ stub_request(:post, @api_path).to_return({
206
+ :status => 200,
207
+ :body => MultiJson.encode({})
208
+ })
209
+ end
210
+
211
+ it "should call correct URL" do
212
+ @client.trigger(['mychannel'], 'event', {'some' => 'data'}).
213
+ should == {}
214
+ end
215
+
216
+ it "should pass any parameters in the body of the request" do
217
+ @client.trigger(['mychannel', 'c2'], 'event', {'some' => 'data'}, {
218
+ :socket_id => "1234"
219
+ })
220
+ WebMock.should have_requested(:post, @api_path).with { |req|
221
+ parsed = MultiJson.decode(req.body)
222
+ parsed["name"].should == 'event'
223
+ parsed["channels"].should == ["mychannel", "c2"]
224
+ parsed["socket_id"].should == '1234'
225
+ }
226
+ end
227
+
228
+ it "should convert non string data to JSON before posting" do
229
+ @client.trigger(['mychannel'], 'event', {'some' => 'data'})
230
+ WebMock.should have_requested(:post, @api_path).with { |req|
231
+ MultiJson.decode(req.body)["data"].should == '{"some":"data"}'
232
+ }
233
+ end
234
+
235
+ it "should accept a single channel as well as an array" do
236
+ @client.trigger('mychannel', 'event', {'some' => 'data'})
237
+ WebMock.should have_requested(:post, @api_path).with { |req|
238
+ MultiJson.decode(req.body)["channels"].should == ['mychannel']
239
+ }
240
+ end
241
+ end
242
+
243
+ describe '#trigger_async' do
244
+ before :each do
245
+ @api_path = %r{/apps/20/events}
246
+ stub_request(:post, @api_path).to_return({
247
+ :status => 200,
248
+ :body => MultiJson.encode({})
249
+ })
250
+ end
251
+
252
+ it "should call correct URL" do
253
+ EM.run {
254
+ @client.trigger_async('mychannel', 'event', {'some' => 'data'}).callback { |r|
255
+ r.should == {}
256
+ EM.stop
257
+ }
258
+ }
259
+ end
260
+
261
+ it "should pass any parameters in the body of the request" do
262
+ EM.run {
263
+ @client.trigger_async('mychannel', 'event', {'some' => 'data'}, {
264
+ :socket_id => "1234"
265
+ }).callback {
266
+ WebMock.should have_requested(:post, @api_path).with { |req|
267
+ MultiJson.decode(req.body)["socket_id"].should == '1234'
268
+ }
269
+ EM.stop
270
+ }
271
+ }
272
+ end
273
+
274
+ it "should convert non string data to JSON before posting" do
275
+ EM.run {
276
+ @client.trigger_async('mychannel', 'event', {'some' => 'data'}).callback {
277
+ WebMock.should have_requested(:post, @api_path).with { |req|
278
+ MultiJson.decode(req.body)["data"].should == '{"some":"data"}'
279
+ }
280
+ EM.stop
281
+ }
282
+ }
283
+ end
284
+ end
285
+
286
+ [:get, :post].each do |verb|
287
+ describe "##{verb}" do
288
+ before :each do
289
+ @url_regexp = %r{api.pusherapp.com}
290
+ stub_request(verb, @url_regexp).
291
+ to_return(:status => 200, :body => "{}")
292
+ end
293
+
294
+ let(:call_api) { @client.send(verb, '/path') }
295
+
296
+ it "should use http by default" do
297
+ call_api
298
+ WebMock.should have_requested(verb, %r{http://api.pusherapp.com/apps/20/path})
299
+ end
300
+
301
+ it "should use https if configured" do
302
+ @client.encrypted = true
303
+ call_api
304
+ WebMock.should have_requested(verb, %r{https://api.pusherapp.com})
305
+ end
306
+
307
+ it "should format the respose hash with symbols at first level" do
308
+ stub_request(verb, @url_regexp).to_return({
309
+ :status => 200,
310
+ :body => MultiJson.encode({'something' => {'a' => 'hash'}})
311
+ })
312
+ call_api.should == {
313
+ :something => {'a' => 'hash'}
314
+ }
315
+ end
316
+
317
+ it "should catch all http exceptions and raise a Pusher::HTTPError wrapping the original error" do
318
+ stub_request(verb, @url_regexp).to_raise(HTTPClient::TimeoutError)
319
+
320
+ error = nil
321
+ begin
322
+ call_api
323
+ rescue => e
324
+ error = e
325
+ end
326
+
327
+ error.class.should == Pusher::HTTPError
328
+ error.should be_kind_of(Pusher::Error)
329
+ error.message.should == 'Exception from WebMock (HTTPClient::TimeoutError)'
330
+ error.original_error.class.should == HTTPClient::TimeoutError
331
+ end
332
+
333
+ it "should raise Pusher::Error if call returns 400" do
334
+ stub_request(verb, @url_regexp).to_return({:status => 400})
335
+ lambda { call_api }.should raise_error(Pusher::Error)
336
+ end
337
+
338
+ it "should raise AuthenticationError if pusher returns 401" do
339
+ stub_request(verb, @url_regexp).to_return({:status => 401})
340
+ lambda { call_api }.should raise_error(Pusher::AuthenticationError)
341
+ end
342
+
343
+ it "should raise Pusher::Error if pusher returns 404" do
344
+ stub_request(verb, @url_regexp).to_return({:status => 404})
345
+ lambda { call_api }.should raise_error(Pusher::Error, '404 Not found (/apps/20/path)')
346
+ end
347
+
348
+ it "should raise Pusher::Error if pusher returns 407" do
349
+ stub_request(verb, @url_regexp).to_return({:status => 407})
350
+ lambda { call_api }.should raise_error(Pusher::Error, 'Proxy Authentication Required')
351
+ end
352
+
353
+ it "should raise Pusher::Error if pusher returns 500" do
354
+ stub_request(verb, @url_regexp).to_return({:status => 500, :body => "some error"})
355
+ lambda { call_api }.should raise_error(Pusher::Error, 'Unknown error (status code 500): some error')
356
+ end
357
+ end
358
+ end
359
+
360
+ describe "async calling without eventmachine" do
361
+ [[:get, :get_async], [:post, :post_async]].each do |verb, method|
362
+ describe "##{method}" do
363
+ before :each do
364
+ @url_regexp = %r{api.pusherapp.com}
365
+ stub_request(verb, @url_regexp).
366
+ to_return(:status => 200, :body => "{}")
367
+ end
368
+
369
+ let(:call_api) {
370
+ @client.send(method, '/path').tap { |c|
371
+ # Allow the async thread (inside httpclient) to run
372
+ while !c.finished?
373
+ sleep 0.01
374
+ end
375
+ }
376
+ }
377
+
378
+ it "should use http by default" do
379
+ call_api
380
+ WebMock.should have_requested(verb, %r{http://api.pusherapp.com/apps/20/path})
381
+ end
382
+
383
+ it "should use https if configured" do
384
+ @client.encrypted = true
385
+ call_api
386
+ WebMock.should have_requested(verb, %r{https://api.pusherapp.com})
387
+ end
388
+
389
+ # Note that the raw httpclient connection object is returned and
390
+ # the response isn't handled (by handle_response) in the normal way.
391
+ it "should return a httpclient connection object" do
392
+ connection = call_api
393
+ connection.finished?.should be_true
394
+ response = connection.pop
395
+ response.status.should == 200
396
+ response.body.read.should == "{}"
397
+ end
398
+ end
399
+ end
400
+ end
401
+
402
+ describe "async calling with eventmachine" do
403
+ [[:get, :get_async], [:post, :post_async]].each do |verb, method|
404
+ describe "##{method}" do
405
+ before :each do
406
+ @url_regexp = %r{api.pusherapp.com}
407
+ stub_request(verb, @url_regexp).
408
+ to_return(:status => 200, :body => "{}")
409
+ end
410
+
411
+ let(:call_api) { @client.send(method, '/path') }
412
+
413
+ it "should use http by default" do
414
+ EM.run {
415
+ call_api.callback {
416
+ WebMock.should have_requested(verb, %r{http://api.pusherapp.com/apps/20/path})
417
+ EM.stop
418
+ }
419
+ }
420
+ end
421
+
422
+ it "should use https if configured" do
423
+ EM.run {
424
+ @client.encrypted = true
425
+ call_api.callback {
426
+ WebMock.should have_requested(verb, %r{https://api.pusherapp.com})
427
+ EM.stop
428
+ }
429
+ }
430
+ end
431
+
432
+ it "should format the respose hash with symbols at first level" do
433
+ EM.run {
434
+ stub_request(verb, @url_regexp).to_return({
435
+ :status => 200,
436
+ :body => MultiJson.encode({'something' => {'a' => 'hash'}})
437
+ })
438
+ call_api.callback { |response|
439
+ response.should == {
440
+ :something => {'a' => 'hash'}
441
+ }
442
+ EM.stop
443
+ }
444
+ }
445
+ end
446
+
447
+ it "should errback with Pusher::Error on unsuccessful response" do
448
+ EM.run {
449
+ stub_request(verb, @url_regexp).to_return({:status => 400})
450
+
451
+ call_api.errback { |e|
452
+ e.class.should == Pusher::Error
453
+ EM.stop
454
+ }.callback {
455
+ fail
456
+ }
457
+ }
458
+ end
459
+ end
460
+ end
461
+ end
462
+ end
463
+ end
464
+ end