faye-authentication 0.4.0 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9a617e82b1d7184dc0ce7016b8736c6e412500f3
4
- data.tar.gz: 640960f50c1a742c704bd4ded7930817caf22ee4
3
+ metadata.gz: 9ced587e82c17d594caf427c14ea7d844ab085af
4
+ data.tar.gz: 55b71c17bb0bbafb9240b42fba0662cccb6f0064
5
5
  SHA512:
6
- metadata.gz: c130a9cef8388dcc2666842339aea83206c3500e155c45b67ab5751ba29a339a8786e95c54cf754aeb8362d78cca94d120f8d2a3151bece77ee44c47e4121609
7
- data.tar.gz: 09ef066a4f2cbaddb41ac92175a0dbe58e9c3d6e8e78cb21c775bf9300a0f8026eea4cf0d89dc0b5a5a09bbaba1aa7f0c7b1d91549ad5db5a3209d8f220dff25
6
+ metadata.gz: fb749a65cbfcf3afd65901272950028c3dee2605d28ec3d2ac6bacd9b6e53eaaf2c547f96d71670a62ad0147db8d00b021a7c38c1cec36922c5d347d7b713530
7
+ data.tar.gz: 9d708c3c18d56b1e42cba5fc83e300f18d0e7e8d49dad06cdebd531fd1c75b3f12e9f130974f665bd7aeda8c1347d559d19c6253a3cfffacde0356ab7b06762c
data/.drone.yml ADDED
@@ -0,0 +1,10 @@
1
+ image: ruby2.0.0
2
+ cache:
3
+ - /tmp/bundler
4
+ env:
5
+ - RAILS_ENV=test
6
+ script:
7
+ - gem install bundler
8
+ - sudo chown ubuntu:ubuntu /tmp/bundler
9
+ - bundle install --path /tmp/bundler
10
+ - bundle exec rake
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## 0.5.0
2
+ - Add support for faye 1.1 (unreleased for now)
3
+ - Drop support for faye < 1.1
4
+ - More extensibility regarding public channels, extensions now take an options
5
+ hash with a whitelist lambda / function that will be called with the channel
6
+ name so developers can implement their own logic
7
+
1
8
  ## 0.4.0
2
9
  - Channels beginning by ``/public/`` do not require authentication anymore,
3
10
  However, globbing with public channels still require authentication.
data/Gemfile CHANGED
@@ -1,7 +1,4 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in faye-authentication.gemspec
4
- group :development do
5
- gem 'rspec-eventmachine', '~> 0.1', github: 'jcoglan/rspec-eventmachine'
6
- end
7
4
  gemspec
data/README.md CHANGED
@@ -40,15 +40,8 @@ Or install it yourself as:
40
40
 
41
41
  ### Channels requiring authentication
42
42
 
43
- All channels require authentication, except channels beginning by ``/public/``
44
-
45
- However, globbing, even on ``/public/`` channels will require authentication.
46
-
47
- Example :
48
-
49
- - ``/public/foo`` does not require authentication
50
- - ``/public/bar/*`` requires authentication
51
-
43
+ All channels require authentication by default, however, it is possible to provide
44
+ a lambda to the faye extensions to let them know which channels are public.
52
45
 
53
46
  ### Authentication endpoint requirements
54
47
 
@@ -56,15 +49,14 @@ The endpoint will receive a POST request, and shall return a JSON hash with a ``
56
49
 
57
50
  The parameters sent to the endpoint are the following :
58
51
 
59
- ````
52
+ ```json
60
53
  {
61
- 'message' =>
62
- {
63
- 'channel' => '/foo/bar',
64
- 'clientId' => '123abc'
54
+ "message" : {
55
+ "channel": "/foo/bar",
56
+ "clientId": "123abc"
65
57
  }
66
58
  }
67
- ````
59
+ ```
68
60
 
69
61
  If the endpoint returns an error, the message won't be signed and the server will reject it.
70
62
 
@@ -72,7 +64,7 @@ You can use ``Faye::Authentication.sign`` to generate the signature from the mes
72
64
 
73
65
  Example (For a Rails application)
74
66
 
75
- ````ruby
67
+ ```ruby
76
68
  def auth
77
69
  if current_user.can?(:read, params[:message][:channel])
78
70
  render json: {signature: Faye::Authentication.sign(params[:message].slice(:channel,:clientId), 'your shared secret key')}
@@ -81,38 +73,53 @@ def auth
81
73
  end
82
74
  end
83
75
 
84
- ````
76
+ ```
85
77
 
86
78
  A Ruby HTTP Client is also available for publishing messages to your faye server
87
79
  without the hassle of using EventMachine :
88
80
 
89
- ````ruby
81
+ ```ruby
90
82
  Faye::Authentication::HTTPClient.publish('http://localhost:9290/faye', '/channel', 'data', 'your private key')
91
- ````
83
+ ```
92
84
  ### Javascript client extension
93
85
 
94
86
  Add the extension to your faye client :
95
87
 
96
- ````javascript
88
+ ```javascript
97
89
  var client = new Faye.Client('http://my.server/faye');
98
90
  client.addExtension(new FayeAuthentication(client));
99
- ````
91
+ ```
100
92
 
101
93
  By default, when sending a subscribe request or publishing a message, the extension
102
94
  will issue an AJAX request to ``/faye/auth``
103
95
 
104
96
  If you wish to change the endpoint, you can supply it as the second argument of the extension constructor, the first one being the client :
97
+ ````javascript
98
+ client.addExtension(new FayeAuthentication(client, '/my_custom_auth_endpoint'));
99
+ ````
100
+
101
+ If you want to specify some channels for which you don't want the extension to
102
+ call your endpoint, you can pass an options object with a ``whitelist`` key mapping
103
+ to a function :
104
+
105
+ ````javascript
106
+ function channelWhitelist(channel) {
107
+ // Allow channels beginning with /public but disallow globbing
108
+ return (channel.lastIndexOf('/public/', 0) === 0 && channel.indexOf('*') == -1);
109
+ }
110
+
111
+ client.addExtension(new FayeAuthentication(client, '/faye/auth', {whitelist: channelWhitelist}));
112
+ ````
105
113
 
106
- client.addExtension(new FayeAuthentication(client, '/my_custom_auth_endpoint'));
107
114
 
108
115
  ### Ruby Faye server extension
109
116
 
110
117
  Instanciate the extension with your secret key and add it to the server :
111
118
 
112
- ````ruby
119
+ ```ruby
113
120
  server = Faye::RackAdapter.new(:mount => '/faye', :timeout => 15)
114
121
  server.add_extension Faye::Authentication::ServerExtension.new('your shared secret key')
115
- ````
122
+ ```
116
123
 
117
124
  Faye::Authentication::ServerExtension expect that :
118
125
  - a ``signature`` is present in the message for publish/subscribe request
@@ -121,14 +128,28 @@ Faye::Authentication::ServerExtension expect that :
121
128
 
122
129
  Otherwise Faye Server will refuse the message.
123
130
 
131
+ If you want to specify some channels for which you don't want the extension require
132
+ authentication, you can pass an options hash with a ``whitelist`` key mapping
133
+ to a lambda :
134
+
135
+ ````ruby
136
+ channel_whitelist = lambda do |channel|
137
+ # Allow channels beginning with /public but disallow globbing
138
+ channel.start_with?('/public/') and not channel.include?('*')
139
+ end
140
+
141
+ server = Faye::RackAdapter.new(:mount => '/faye', :timeout => 15)
142
+ server.add_extension Faye::Authentication::ServerExtension.new('your shared secret key', {whitelist: channel_whitelist})
143
+ ````
144
+
124
145
  ### Ruby Faye client extension
125
146
 
126
147
  This extension allows the ruby ``Faye::Client`` to auto-sign its messages before sending them to the server.
127
148
 
128
- ````ruby
149
+ ```ruby
129
150
  client = Faye::Client.new('http://localhost:9292/faye')
130
151
  client.add_extension Faye::Authentication::ClientExtension.new('your shared secret key')
131
- ````
152
+ ```
132
153
 
133
154
  ## Contributing
134
155
 
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.6.0
@@ -1,8 +1,9 @@
1
- function FayeAuthentication(client, endpoint) {
1
+ function FayeAuthentication(client, endpoint, options) {
2
2
  this._client = client;
3
3
  this._endpoint = endpoint || '/faye/auth';
4
4
  this._signatures = {};
5
5
  this._outbox = {};
6
+ this._options = options || {};
6
7
  }
7
8
 
8
9
  FayeAuthentication.prototype.endpoint = function() {
@@ -49,16 +50,18 @@ FayeAuthentication.prototype.outgoing = function(message, callback) {
49
50
  };
50
51
 
51
52
  FayeAuthentication.prototype.authentication_required = function(message) {
52
- var subscription_or_channel = message.subscription || message.channel
53
- return (!this.public_channel(subscription_or_channel) && (message.channel == '/meta/subscribe' || message.channel.lastIndexOf('/meta/', 0) !== 0))
54
- };
55
-
56
- FayeAuthentication.prototype.public_channel = function(channel) {
57
- if (channel.lastIndexOf('/public/', 0) === 0) {
58
- return (channel.indexOf('*') == -1);
59
- } else {
53
+ var subscription_or_channel = message.subscription || message.channel;
54
+ if (message.channel == '/meta/subscribe' || message.channel.lastIndexOf('/meta/', 0) !== 0)
55
+ if(this._options.whitelist) {
56
+ try {
57
+ return (!this._options.whitelist(subscription_or_channel));
58
+ } catch (e) {
59
+ this.error("Error caught when evaluating whitelist function : " + e.message);
60
+ }
61
+ } else
62
+ return (true);
63
+ else
60
64
  return (false);
61
- }
62
65
  };
63
66
 
64
67
  FayeAuthentication.prototype.incoming = function(message, callback) {
@@ -69,8 +72,12 @@ FayeAuthentication.prototype.incoming = function(message, callback) {
69
72
  outbox_message.message.retried = true;
70
73
  delete outbox_message.message.id;
71
74
  delete this._outbox[message.id];
72
- this._client._send(outbox_message.message, callback);
75
+ this._client._sendMessage(outbox_message.message, {}, callback);
73
76
  }
74
77
  else
75
78
  callback(message);
76
79
  };
80
+
81
+ $(function() {
82
+ Faye.extend(FayeAuthentication.prototype, Faye.Logging);
83
+ });
@@ -18,13 +18,14 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_runtime_dependency 'jwt', '~> 1'
21
+ spec.add_runtime_dependency 'jwt', '~> 1.2'
22
+ spec.add_runtime_dependency 'faye', '~> 1.0'
22
23
 
23
24
  spec.add_development_dependency "bundler", "~> 1.5"
24
25
  spec.add_development_dependency "rake", '~> 10.3'
25
26
  spec.add_development_dependency 'rspec', '~> 3.0'
27
+ spec.add_development_dependency 'rspec-eventmachine', '~> 0.2'
26
28
  spec.add_development_dependency 'jasmine', '~> 2.0'
27
- spec.add_development_dependency 'faye', '~> 1.0'
28
29
  spec.add_development_dependency 'rack', '~> 1.5'
29
30
  spec.add_development_dependency 'thin', '~> 1.6'
30
31
  spec.add_development_dependency 'webmock', '~> 1.18'
@@ -8,7 +8,7 @@ module Faye
8
8
  end
9
9
 
10
10
  def outgoing(message, callback)
11
- if Faye::Authentication.authentication_required?(message)
11
+ if Faye::Authentication.authentication_required?(message, @options)
12
12
  message['signature'] = Faye::Authentication.sign({channel: message['subscription'] || message['channel'], clientId: message['clientId']}, @secret, @options)
13
13
  end
14
14
  callback.call(message)
@@ -5,12 +5,13 @@ module Faye
5
5
  class ServerExtension
6
6
  include Faye::Logging
7
7
 
8
- def initialize(secret)
8
+ def initialize(secret, options = {})
9
+ @options = options
9
10
  @secret = secret.to_s
10
11
  end
11
12
 
12
13
  def incoming(message, callback)
13
- if Faye::Authentication.authentication_required?(message)
14
+ if Faye::Authentication.authentication_required?(message, @options)
14
15
  begin
15
16
  Faye::Authentication.validate(message['signature'],
16
17
  message['subscription'] || message['channel'],
@@ -1,5 +1,5 @@
1
1
  module Faye
2
2
  module Authentication
3
- VERSION = "0.4.0"
3
+ VERSION = File.read(File.join(File.dirname(__FILE__),'..', '..', '..', 'VERSION') ).strip
4
4
  end
5
5
  end
@@ -1,4 +1,5 @@
1
1
  require 'jwt'
2
+ require 'faye/mixins/logging'
2
3
  require 'faye/authentication/version'
3
4
  require 'faye/authentication/server_extension'
4
5
  require 'faye/authentication/client_extension'
@@ -7,6 +8,9 @@ require 'faye/authentication/engine'
7
8
 
8
9
  module Faye
9
10
  module Authentication
11
+
12
+ extend Faye::Logging
13
+
10
14
  class AuthError < StandardError; end
11
15
  class ExpiredError < AuthError; end
12
16
  class PayloadError < AuthError; end
@@ -19,9 +23,12 @@ module Faye
19
23
 
20
24
  # Return signed payload or raise
21
25
  def self.decode(signature, secret)
22
- payload, _ = JWT.decode(signature, secret) rescue raise(AuthError)
23
- raise ExpiredError if Time.at(payload['exp'].to_i) < Time.now
26
+ payload, _ = JWT.decode(signature, secret)
24
27
  payload
28
+ rescue JWT::ExpiredSignature
29
+ raise ExpiredError
30
+ rescue
31
+ raise AuthError
25
32
  end
26
33
 
27
34
  # Return true if signature is valid and correspond to channel and clientId or raise
@@ -32,13 +39,18 @@ module Faye
32
39
  true
33
40
  end
34
41
 
35
- def self.authentication_required?(message)
42
+ def self.authentication_required?(message, options = {})
36
43
  subscription_or_channel = message['subscription'] || message['channel']
37
- !public_channel?(subscription_or_channel) && (message['channel'] == '/meta/subscribe' || (!(message['channel'].start_with?('/meta/'))))
38
- end
39
-
40
- def self.public_channel?(channel)
41
- channel.start_with?('/public/') and not channel.include?('*')
44
+ return false unless (message['channel'] == '/meta/subscribe' || (!(message['channel'].start_with?('/meta/'))))
45
+ whitelist_proc = options[:whitelist]
46
+ if whitelist_proc
47
+ begin
48
+ return !whitelist_proc.call(subscription_or_channel)
49
+ rescue => e
50
+ error("Error caught when evaluating whitelist lambda : #{e.message}")
51
+ end
52
+ end
53
+ true
42
54
  end
43
55
 
44
56
  end
@@ -14,6 +14,103 @@ describe('faye-authentication', function() {
14
14
 
15
15
  });
16
16
 
17
+ describe('authentication_required', function() {
18
+
19
+ beforeEach(function() {
20
+ this.auth = new FayeAuthentication(new Faye.Client('http://example.com'));
21
+ Faye.logger = null;
22
+ });
23
+
24
+ function sharedExamplesForSubscribeAndPublish() {
25
+ it('returns true if no options is passed', function() {
26
+ expect(this.auth.authentication_required(this.message)).toBe(true);
27
+ });
28
+
29
+ it('calls function with subscription or channel', function() {
30
+ this.auth._options.whitelist = function(message) { return(true); }
31
+ spyOn(this.auth._options, 'whitelist');
32
+ this.auth.authentication_required(this.message);
33
+ expect(this.auth._options.whitelist).toHaveBeenCalledWith(this.message.subscription || this.message.channel);
34
+ });
35
+
36
+ it('logs error if the function throws', function() {
37
+ this.auth._options.whitelist = function(message) { throw new Error("boom"); }
38
+ Faye.logger = {error: function() {}};
39
+ spyOn(Faye.logger, 'error');
40
+ this.auth.authentication_required(this.message);
41
+ expect(Faye.logger.error).toHaveBeenCalledWith('[Faye] Error caught when evaluating whitelist function : boom');
42
+ });
43
+
44
+ it ('returns false if function returns true', function() {
45
+ this.auth._options.whitelist = function(message) { return(true); }
46
+ expect(this.auth.authentication_required(this.message)).toBe(false);
47
+ });
48
+
49
+ it ('returns true if function returns false', function() {
50
+ this.auth._options.whitelist = function(message) { return(false); }
51
+ expect(this.auth.authentication_required(this.message)).toBe(true);
52
+ });
53
+ }
54
+
55
+ function sharedExamplesForMetaExceptPublish() {
56
+ it('returns false if no options is passed', function() {
57
+ expect(this.auth.authentication_required(this.message)).toBe(false);
58
+ });
59
+
60
+ it ('returns false if function returns true', function() {
61
+ this.auth._options.whitelist = function(message) { return(true); }
62
+ expect(this.auth.authentication_required(this.message)).toBe(false);
63
+ });
64
+
65
+ it ('returns false if function returns false', function() {
66
+ this.auth._options.whitelist = function(message) { return(false); }
67
+ expect(this.auth.authentication_required(this.message)).toBe(false);
68
+ });
69
+ }
70
+
71
+ describe('publish', function() {
72
+
73
+ beforeEach(function() {
74
+ this.message = {'channel': '/foobar'};
75
+ });
76
+
77
+ sharedExamplesForSubscribeAndPublish();
78
+ });
79
+
80
+ describe('subscribe', function() {
81
+ beforeEach(function() {
82
+ this.message = {'channel': '/meta/subscribe', 'subscription': '/foobar'};
83
+ });
84
+
85
+ sharedExamplesForSubscribeAndPublish();
86
+ });
87
+
88
+ describe('handshake', function() {
89
+ beforeEach(function() {
90
+ this.message = {'channel': '/meta/handshake'};
91
+ });
92
+
93
+ sharedExamplesForMetaExceptPublish();
94
+ });
95
+
96
+ describe('connect', function() {
97
+ beforeEach(function() {
98
+ this.message = {'channel': '/meta/connect'};
99
+ });
100
+
101
+ sharedExamplesForMetaExceptPublish();
102
+ });
103
+
104
+ describe('unsubscribe', function() {
105
+ beforeEach(function() {
106
+ this.message = {'channel': '/meta/unsubscribe', 'handshake': '/foobar'};
107
+ });
108
+
109
+ sharedExamplesForMetaExceptPublish();
110
+ });
111
+
112
+ });
113
+
17
114
  describe('extension', function() {
18
115
  beforeEach(function() {
19
116
  jasmine.Ajax.install();
@@ -54,87 +151,93 @@ describe('faye-authentication', function() {
54
151
  'responseText': '{"signature": "foobarsignature"}'
55
152
  });
56
153
 
57
- this.fake_transport = {connectionType: "fake", endpoint: {}, send: function() {}};
58
- spyOn(this.fake_transport, 'send');
154
+ this.dispatcher = {connectionType: "fake", sendMessage: function() {}, selectTransport: function() { }};
155
+ spyOn(this.dispatcher, 'sendMessage');
156
+ spyOn(this.dispatcher, 'selectTransport');
157
+ Faye.extend(this.dispatcher, Faye.Publisher)
59
158
  });
60
159
 
61
160
  it('should add the signature to subscribe message', function(done) {
62
161
  var self = this;
63
162
 
64
163
  this.client.handshake(function() {
65
- self.client._transport = self.fake_transport
164
+ self.client._dispatcher = self.dispatcher;
66
165
  self.client.subscribe('/foobar');
67
- }, this.client);
68
166
 
69
- setTimeout(function() {
70
- var calls = self.fake_transport.send.calls.all();
71
- var last_call = calls[calls.length - 1];
72
- var message = last_call.args[0].message;
73
- expect(message.channel).toBe('/meta/subscribe');
74
- expect(message.signature).toBe('foobarsignature');
75
- done();
76
- }, 500);
167
+ setTimeout(function() {
168
+ var calls = self.dispatcher.sendMessage.calls.all();
169
+ var last_call = calls[calls.length - 1];
170
+ var message = last_call.args[0];
171
+ expect(message.channel).toBe('/meta/subscribe');
172
+ expect(message.signature).toBe('foobarsignature');
173
+ done();
174
+ }, 300);
77
175
 
176
+ }, this.client);
78
177
  });
79
178
 
80
179
  it('should add the signature to publish message', function(done) {
81
180
  var self = this;
82
181
 
83
182
  this.client.handshake(function() {
84
- self.client._transport = self.fake_transport
183
+ self.client._dispatcher = self.dispatcher;
85
184
  self.client.publish('/foobar', {text: 'hallo'});
86
- }, this.client);
87
185
 
88
- setTimeout(function() {
89
- var calls = self.fake_transport.send.calls.all();
90
- var last_call = calls[calls.length - 1];
91
- var message = last_call.args[0].message;
92
- expect(message.channel).toBe('/foobar');
93
- expect(message.signature).toBe('foobarsignature');
94
- done();
95
- }, 500);
186
+ setTimeout(function() {
187
+ var calls = self.dispatcher.sendMessage.calls.all();
188
+ var last_call = calls[calls.length - 1];
189
+ var message = last_call.args[0];
190
+ expect(message.channel).toBe('/foobar');
191
+ expect(message.signature).toBe('foobarsignature');
192
+ done();
193
+ }, 300);
194
+
195
+ }, this.client);
96
196
  });
97
197
 
98
198
  it('preserves messages integrity', function(done) {
99
199
  var self = this;
100
200
 
101
201
  this.client.handshake(function() {
102
- self.client._transport = self.fake_transport
202
+ self.client._dispatcher = self.dispatcher;
103
203
  self.client.publish('/foo', {text: 'hallo'});
104
204
  self.client.subscribe('/foo');
105
- }, this.client);
106
205
 
107
- setTimeout(function() {
108
- var calls = self.fake_transport.send.calls.all();
109
- var subscribe_call = calls[calls.length - 1];
110
- var publish_call = calls[calls.length - 2];
111
- var subscribe_message = subscribe_call.args[0].message;
112
- var publish_message = publish_call.args[0].message;
113
- expect(publish_message.channel).toBe('/foo');
114
- expect(subscribe_message.channel).toBe('/meta/subscribe');
115
- expect(publish_message.signature).toBe('foobarsignature');
116
- expect(subscribe_message.signature).toBe('foobarsignature');
117
- done();
118
- }, 500);
119
- });
206
+ setTimeout(function() {
207
+ var calls = self.dispatcher.sendMessage.calls.all();
208
+ var subscribe_call = calls[calls.length - 1];
209
+ var publish_call = calls[calls.length - 2];
210
+ var subscribe_message = subscribe_call.args[0];
211
+ var publish_message = publish_call.args[0];
212
+ expect(publish_message.channel).toBe('/foo');
213
+ expect(subscribe_message.channel).toBe('/meta/subscribe');
214
+ expect(publish_message.signature).toBe('foobarsignature');
215
+ expect(subscribe_message.signature).toBe('foobarsignature');
216
+ done();
217
+ }, 300);
120
218
 
219
+ }, this.client);
220
+ });
121
221
 
122
- it('does not add the signature to a public message', function(done) {
222
+ it('does not add the signature if authentication is not required', function(done) {
123
223
  var self = this;
124
224
 
225
+ spyOn(this.auth, 'authentication_required').and.returnValue(false);
226
+
125
227
  this.client.handshake(function() {
126
- self.client._transport = self.fake_transport
127
- self.client.publish('/public/foo', {text: 'hallo'});
228
+ self.client._dispatcher = self.dispatcher;
229
+ self.client.publish('/foobar', {text: 'hallo'});
230
+
231
+ setTimeout(function() {
232
+ var calls = self.dispatcher.sendMessage.calls.all();
233
+ var last_call = calls[calls.length - 1];
234
+ var message = last_call.args[0];
235
+ expect(message.channel).toBe('/foobar');
236
+ expect(message.signature).toBe(undefined);
237
+ done();
238
+ }, 300);
128
239
  }, this.client);
129
240
 
130
- setTimeout(function() {
131
- var calls = self.fake_transport.send.calls.all();
132
- var last_call = calls[calls.length - 1];
133
- var message = last_call.args[0].message;
134
- expect(message.channel).toBe('/public/foo');
135
- expect(message.signature).toBe(undefined);
136
- done();
137
- }, 500);
138
241
  });
139
242
 
140
243
  });