em-http-request 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of em-http-request might be problematic. Click here for more details.

@@ -0,0 +1,48 @@
1
+ /**
2
+ * Copyright (c) 2005 Zed A. Shaw
3
+ * You can redistribute it and/or modify it under the same terms as Ruby.
4
+ */
5
+
6
+ #ifndef http11_parser_h
7
+ #define http11_parser_h
8
+
9
+ #include <sys/types.h>
10
+
11
+ #if defined(_WIN32)
12
+ #include <stddef.h>
13
+ #endif
14
+
15
+ typedef void (*element_cb)(void *data, const char *at, size_t length);
16
+ typedef void (*field_cb)(void *data, const char *field, size_t flen, const char *value, size_t vlen);
17
+
18
+ typedef struct httpclient_parser {
19
+ int cs;
20
+ size_t body_start;
21
+ int content_len;
22
+ size_t nread;
23
+ size_t mark;
24
+ size_t field_start;
25
+ size_t field_len;
26
+
27
+ void *data;
28
+
29
+ field_cb http_field;
30
+ element_cb reason_phrase;
31
+ element_cb status_code;
32
+ element_cb chunk_size;
33
+ element_cb http_version;
34
+ element_cb header_done;
35
+ element_cb last_chunk;
36
+
37
+
38
+ } httpclient_parser;
39
+
40
+ int httpclient_parser_init(httpclient_parser *parser);
41
+ int httpclient_parser_finish(httpclient_parser *parser);
42
+ size_t httpclient_parser_execute(httpclient_parser *parser, const char *data, size_t len, size_t off);
43
+ int httpclient_parser_has_error(httpclient_parser *parser);
44
+ int httpclient_parser_is_finished(httpclient_parser *parser);
45
+
46
+ #define httpclient_parser_nread(parser) (parser)->nread
47
+
48
+ #endif
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Copyright (c) 2005 Zed A. Shaw
3
+ * You can redistribute it and/or modify it under the same terms as Ruby.
4
+ */
5
+
6
+ #include "http11_parser.h"
7
+ #include <stdio.h>
8
+ #include <assert.h>
9
+ #include <stdlib.h>
10
+ #include <ctype.h>
11
+ #include <string.h>
12
+
13
+ #define LEN(AT, FPC) (FPC - buffer - parser->AT)
14
+ #define MARK(M,FPC) (parser->M = (FPC) - buffer)
15
+ #define PTR_TO(F) (buffer + parser->F)
16
+ #define L(M) fprintf(stderr, "" # M "\n");
17
+
18
+
19
+ /** machine **/
20
+ %%{
21
+ machine httpclient_parser;
22
+
23
+ action mark {MARK(mark, fpc); }
24
+
25
+ action start_field { MARK(field_start, fpc); }
26
+
27
+ action write_field {
28
+ parser->field_len = LEN(field_start, fpc);
29
+ }
30
+
31
+ action start_value { MARK(mark, fpc); }
32
+
33
+ action write_value {
34
+ parser->http_field(parser->data, PTR_TO(field_start), parser->field_len, PTR_TO(mark), LEN(mark, fpc));
35
+ }
36
+
37
+ action reason_phrase {
38
+ parser->reason_phrase(parser->data, PTR_TO(mark), LEN(mark, fpc));
39
+ }
40
+
41
+ action status_code {
42
+ parser->status_code(parser->data, PTR_TO(mark), LEN(mark, fpc));
43
+ }
44
+
45
+ action http_version {
46
+ parser->http_version(parser->data, PTR_TO(mark), LEN(mark, fpc));
47
+ }
48
+
49
+ action chunk_size {
50
+ parser->chunk_size(parser->data, PTR_TO(mark), LEN(mark, fpc));
51
+ }
52
+
53
+ action last_chunk {
54
+ parser->last_chunk(parser->data, NULL, 0);
55
+ }
56
+
57
+ action done {
58
+ parser->body_start = fpc - buffer + 1;
59
+ if(parser->header_done != NULL)
60
+ parser->header_done(parser->data, fpc + 1, pe - fpc - 1);
61
+ fbreak;
62
+ }
63
+
64
+ # line endings
65
+ CRLF = ("\r\n" | "\n");
66
+
67
+ # character types
68
+ CTL = (cntrl | 127);
69
+ tspecials = ("(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\\" | "\"" | "/" | "[" | "]" | "?" | "=" | "{" | "}" | " " | "\t");
70
+
71
+ # elements
72
+ token = (ascii -- (CTL | tspecials));
73
+
74
+ Reason_Phrase = (any -- CRLF)* >mark %reason_phrase;
75
+ Status_Code = digit{3} >mark %status_code;
76
+ http_number = (digit+ "." digit+) ;
77
+ HTTP_Version = ("HTTP/" http_number) >mark %http_version ;
78
+ Status_Line = HTTP_Version " " Status_Code " "? Reason_Phrase :> CRLF;
79
+
80
+ field_name = token+ >start_field %write_field;
81
+ field_value = any* >start_value %write_value;
82
+ message_header = field_name ":" " "* field_value :> CRLF;
83
+
84
+ Response = Status_Line (message_header)* (CRLF @done);
85
+
86
+ chunk_ext_val = token+;
87
+ chunk_ext_name = token+;
88
+ chunk_extension = (";" chunk_ext_name >start_field %write_field %start_value ("=" chunk_ext_val >start_value)? %write_value )*;
89
+ last_chunk = "0"? chunk_extension :> (CRLF @last_chunk @done);
90
+ chunk_size = xdigit+;
91
+ chunk = chunk_size >mark %chunk_size chunk_extension space* :> (CRLF @done);
92
+ Chunked_Header = (chunk | last_chunk);
93
+
94
+ main := Response | Chunked_Header;
95
+ }%%
96
+
97
+ /** Data **/
98
+ %% write data;
99
+
100
+ int httpclient_parser_init(httpclient_parser *parser) {
101
+ int cs = 0;
102
+ %% write init;
103
+ parser->cs = cs;
104
+ parser->body_start = 0;
105
+ parser->content_len = 0;
106
+ parser->mark = 0;
107
+ parser->nread = 0;
108
+ parser->field_len = 0;
109
+ parser->field_start = 0;
110
+
111
+ return(1);
112
+ }
113
+
114
+
115
+ /** exec **/
116
+ size_t httpclient_parser_execute(httpclient_parser *parser, const char *buffer, size_t len, size_t off) {
117
+ const char *p, *pe;
118
+ int cs = parser->cs;
119
+
120
+ assert(off <= len && "offset past end of buffer");
121
+
122
+ p = buffer+off;
123
+ pe = buffer+len;
124
+
125
+ assert(*pe == '\0' && "pointer does not end on NUL");
126
+ assert(pe - p == len - off && "pointers aren't same distance");
127
+
128
+
129
+ %% write exec;
130
+
131
+ parser->cs = cs;
132
+ parser->nread += p - (buffer + off);
133
+
134
+ assert(p <= pe && "buffer overflow after parsing execute");
135
+ assert(parser->nread <= len && "nread longer than length");
136
+ assert(parser->body_start <= len && "body starts after buffer end");
137
+ assert(parser->mark < len && "mark is after buffer end");
138
+ assert(parser->field_len <= len && "field has length longer than whole buffer");
139
+ assert(parser->field_start < len && "field starts after buffer end");
140
+
141
+ if(parser->body_start) {
142
+ /* final \r\n combo encountered so stop right here */
143
+ parser->nread = parser->body_start;
144
+ }
145
+
146
+ return(parser->nread);
147
+ }
148
+
149
+ int httpclient_parser_finish(httpclient_parser *parser)
150
+ {
151
+ int cs = parser->cs;
152
+
153
+ parser->cs = cs;
154
+
155
+ if (httpclient_parser_has_error(parser) ) {
156
+ return -1;
157
+ } else if (httpclient_parser_is_finished(parser) ) {
158
+ return 1;
159
+ } else {
160
+ return 0;
161
+ }
162
+ }
163
+
164
+ int httpclient_parser_has_error(httpclient_parser *parser) {
165
+ return parser->cs == httpclient_parser_error;
166
+ }
167
+
168
+ int httpclient_parser_is_finished(httpclient_parser *parser) {
169
+ return parser->cs == httpclient_parser_first_final;
170
+ }
@@ -0,0 +1,18 @@
1
+ #--
2
+ # Copyright (C)2008 Ilya Grigorik
3
+ # You can redistribute this under the terms of the Ruby license
4
+ # See file LICENSE for details
5
+ #++
6
+
7
+ require 'rubygems'
8
+ require 'eventmachine'
9
+
10
+
11
+ require File.dirname(__FILE__) + '/http11_client'
12
+ require File.dirname(__FILE__) + '/em_buffer'
13
+
14
+ require File.dirname(__FILE__) + '/em-http/core_ext/hash'
15
+ require File.dirname(__FILE__) + '/em-http/client'
16
+ require File.dirname(__FILE__) + '/em-http/multi'
17
+ require File.dirname(__FILE__) + '/em-http/request'
18
+ require File.dirname(__FILE__) + '/em-http/decoders'
@@ -0,0 +1,504 @@
1
+ # #--
2
+ # Copyright (C)2008 Ilya Grigorik
3
+ #
4
+ # Includes portion originally Copyright (C)2007 Tony Arcieri
5
+ # Includes portion originally Copyright (C)2005 Zed Shaw
6
+ # You can redistribute this under the terms of the Ruby
7
+ # license See file LICENSE for details
8
+ # #--
9
+
10
+ module EventMachine
11
+
12
+ # A simple hash is returned for each request made by HttpClient with the
13
+ # headers that were given by the server for that request.
14
+ class HttpResponseHeader < Hash
15
+ # The reason returned in the http response ("OK","File not found",etc.)
16
+ attr_accessor :http_reason
17
+
18
+ # The HTTP version returned.
19
+ attr_accessor :http_version
20
+
21
+ # The status code (as a string!)
22
+ attr_accessor :http_status
23
+
24
+ # E-Tag
25
+ def etag
26
+ self["ETag"]
27
+ end
28
+
29
+ def last_modified
30
+ time = self["Last-Modified"]
31
+ Time.parse(time) if time
32
+ end
33
+
34
+ # HTTP response status as an integer
35
+ def status
36
+ Integer(http_status) rescue nil
37
+ end
38
+
39
+ # Length of content as an integer, or nil if chunked/unspecified
40
+ def content_length
41
+ @content_length ||= ((s = self[HttpClient::CONTENT_LENGTH]) &&
42
+ (s =~ /^(\d+)$/)) ? $1.to_i : nil
43
+ end
44
+
45
+ # Cookie header from the server
46
+ def cookie
47
+ self[HttpClient::SET_COOKIE]
48
+ end
49
+
50
+ # Is the transfer encoding chunked?
51
+ def chunked_encoding?
52
+ /chunked/i === self[HttpClient::TRANSFER_ENCODING]
53
+ end
54
+
55
+ def keep_alive?
56
+ /keep-alive/i === self[HttpClient::KEEP_ALIVE]
57
+ end
58
+
59
+ def compressed?
60
+ /gzip|compressed|deflate/i === self[HttpClient::CONTENT_ENCODING]
61
+ end
62
+
63
+ def location
64
+ self[HttpClient::LOCATION]
65
+ end
66
+ end
67
+
68
+ class HttpChunkHeader < Hash
69
+ # When parsing chunked encodings this is set
70
+ attr_accessor :http_chunk_size
71
+
72
+ # Size of the chunk as an integer
73
+ def chunk_size
74
+ return @chunk_size unless @chunk_size.nil?
75
+ @chunk_size = @http_chunk_size ? @http_chunk_size.to_i(base=16) : 0
76
+ end
77
+ end
78
+
79
+ # Methods for building HTTP requests
80
+ module HttpEncoding
81
+ HTTP_REQUEST_HEADER="%s %s HTTP/1.1\r\n"
82
+ FIELD_ENCODING = "%s: %s\r\n"
83
+ BASIC_AUTH_ENCODING = "%s: Basic %s\r\n"
84
+
85
+ # Escapes a URI.
86
+ def escape(s)
87
+ s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
88
+ '%'+$1.unpack('H2'*$1.size).join('%').upcase
89
+ }.tr(' ', '+')
90
+ end
91
+
92
+ # Unescapes a URI escaped string.
93
+ def unescape(s)
94
+ s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
95
+ [$1.delete('%')].pack('H*')
96
+ }
97
+ end
98
+
99
+ # Map all header keys to a downcased string version
100
+ def munge_header_keys(head)
101
+ head.inject({}) { |h, (k, v)| h[k.to_s.downcase] = v; h }
102
+ end
103
+
104
+ # HTTP is kind of retarded that you have to specify a Host header, but if
105
+ # you include port 80 then further redirects will tack on the :80 which is
106
+ # annoying.
107
+ def encode_host
108
+ @uri.host + (@uri.port != 80 ? ":#{@uri.port}" : "")
109
+ end
110
+
111
+ def encode_request(method, path, query)
112
+ HTTP_REQUEST_HEADER % [method.to_s.upcase, encode_query(path, query)]
113
+ end
114
+
115
+ def encode_query(path, query)
116
+ encoded_query = if query.kind_of?(Hash)
117
+ query.map { |k, v| encode_param(k, v) }.join('&')
118
+ else
119
+ query.to_s
120
+ end
121
+ if !@uri.query.to_s.empty?
122
+ encoded_query = [encoded_query, @uri.query].reject {|part| part.empty?}.join("&")
123
+ end
124
+ return path if encoded_query.to_s.empty?
125
+ "#{path}?#{encoded_query}"
126
+ end
127
+
128
+ # URL encodes query parameters:
129
+ # single k=v, or a URL encoded array, if v is an array of values
130
+ def encode_param(k, v)
131
+ if v.is_a?(Array)
132
+ v.map { |e| escape(k) + "[]=" + escape(e) }.join("&")
133
+ else
134
+ escape(k) + "=" + escape(v)
135
+ end
136
+ end
137
+
138
+ # Encode a field in an HTTP header
139
+ def encode_field(k, v)
140
+ FIELD_ENCODING % [k, v]
141
+ end
142
+
143
+ # Encode basic auth in an HTTP header
144
+ def encode_basic_auth(k,v)
145
+ BASIC_AUTH_ENCODING % [k, Base64.encode64(v.join(":")).chomp]
146
+ end
147
+
148
+ def encode_headers(head)
149
+ head.inject('') do |result, (key, value)|
150
+ # Munge keys from foo-bar-baz to Foo-Bar-Baz
151
+ key = key.split('-').map { |k| k.capitalize }.join('-')
152
+ unless key == "Authorization"
153
+ result << encode_field(key, value)
154
+ else
155
+ result << encode_basic_auth(key, value)
156
+ end
157
+ end
158
+ end
159
+
160
+ def encode_cookie(cookie)
161
+ if cookie.is_a? Hash
162
+ cookie.inject('') { |result, (k, v)| result << encode_param(k, v) + ";" }
163
+ else
164
+ cookie
165
+ end
166
+ end
167
+ end
168
+
169
+ class HttpClient < Connection
170
+ include EventMachine::Deferrable
171
+ include HttpEncoding
172
+
173
+ TRANSFER_ENCODING="TRANSFER_ENCODING"
174
+ CONTENT_ENCODING="CONTENT_ENCODING"
175
+ CONTENT_LENGTH="CONTENT_LENGTH"
176
+ KEEP_ALIVE="CONNECTION"
177
+ SET_COOKIE="SET_COOKIE"
178
+ LOCATION="LOCATION"
179
+ HOST="HOST"
180
+ CRLF="\r\n"
181
+
182
+ attr_accessor :method, :options, :uri
183
+ attr_reader :response, :response_header, :errors
184
+
185
+ def post_init
186
+ @parser = HttpClientParser.new
187
+ @data = EventMachine::Buffer.new
188
+ @response_header = HttpResponseHeader.new
189
+ @chunk_header = HttpChunkHeader.new
190
+
191
+ @state = :response_header
192
+ @parser_nbytes = 0
193
+ @response = ''
194
+ @errors = ''
195
+ @content_decoder = nil
196
+ @stream = nil
197
+ end
198
+
199
+ # start HTTP request once we establish connection to host
200
+ def connection_completed
201
+ ssl = @options[:tls] || @options[:ssl] || {}
202
+ start_tls(ssl) if @uri.scheme == "https" or @uri.port == 443
203
+
204
+ send_request_header
205
+ send_request_body
206
+ end
207
+
208
+ # request is done, invoke the callback
209
+ def on_request_complete
210
+ begin
211
+ @content_decoder.finalize! if @content_decoder
212
+ rescue HttpDecoders::DecoderError
213
+ on_error "Content-decoder error"
214
+ end
215
+ unbind
216
+ end
217
+
218
+ # request failed, invoke errback
219
+ def on_error(msg, dns_error = false)
220
+ @errors = msg
221
+
222
+ # no connection signature on DNS failures
223
+ # fail the connection directly
224
+ dns_error == true ? fail(self) : unbind
225
+ end
226
+
227
+ # assign a stream processing block
228
+ def stream(&blk)
229
+ @stream = blk
230
+ end
231
+
232
+ def normalize_body
233
+ if @options[:body].is_a? Hash
234
+ @options[:body].to_params
235
+ else
236
+ @options[:body]
237
+ end
238
+ end
239
+
240
+ def send_request_header
241
+ query = @options[:query]
242
+ head = @options[:head] ? munge_header_keys(@options[:head]) : {}
243
+ body = normalize_body
244
+
245
+ # Set the Host header if it hasn't been specified already
246
+ head['host'] ||= encode_host
247
+
248
+ # Set the Content-Length if body is given
249
+ head['content-length'] = body.length if body
250
+
251
+ # Set the User-Agent if it hasn't been specified
252
+ head['user-agent'] ||= "EventMachine HttpClient"
253
+
254
+ # Set the cookie header if provided
255
+ if cookie = head.delete('cookie')
256
+ head['cookie'] = encode_cookie(cookie)
257
+ end
258
+
259
+ # Build the request
260
+ request_header = encode_request(@method, @uri.path, query)
261
+ request_header << encode_headers(head)
262
+ request_header << CRLF
263
+
264
+ send_data request_header
265
+ end
266
+
267
+ def send_request_body
268
+ return unless @options[:body]
269
+ body = normalize_body
270
+ send_data body
271
+ end
272
+
273
+ def receive_data(data)
274
+ @data << data
275
+ dispatch
276
+ end
277
+
278
+ # Called when part of the body has been read
279
+ def on_body_data(data)
280
+ if @content_decoder
281
+ begin
282
+ @content_decoder << data
283
+ rescue HttpDecoders::DecoderError
284
+ on_error "Content-decoder error"
285
+ end
286
+ else
287
+ on_decoded_body_data(data)
288
+ end
289
+ end
290
+
291
+ def on_decoded_body_data(data)
292
+ if @stream
293
+ @stream.call(data)
294
+ else
295
+ @response << data
296
+ end
297
+ end
298
+
299
+ def unbind
300
+ if @state == :finished || (@state == :body && @bytes_remaining.nil?)
301
+ succeed(self)
302
+ else
303
+ fail(self)
304
+ end
305
+ close_connection
306
+ end
307
+
308
+ #
309
+ # Response processing
310
+ #
311
+
312
+ def dispatch
313
+ while case @state
314
+ when :response_header
315
+ parse_response_header
316
+ when :chunk_header
317
+ parse_chunk_header
318
+ when :chunk_body
319
+ process_chunk_body
320
+ when :chunk_footer
321
+ process_chunk_footer
322
+ when :response_footer
323
+ process_response_footer
324
+ when :body
325
+ process_body
326
+ when :finished, :invalid
327
+ break
328
+ else raise RuntimeError, "invalid state: #{@state}"
329
+ end
330
+ end
331
+ end
332
+
333
+ def parse_header(header)
334
+ return false if @data.empty?
335
+
336
+ begin
337
+ @parser_nbytes = @parser.execute(header, @data.to_str, @parser_nbytes)
338
+ rescue EventMachine::HttpClientParserError
339
+ @state = :invalid
340
+ on_error "invalid HTTP format, parsing fails"
341
+ end
342
+
343
+ return false unless @parser.finished?
344
+
345
+ # Clear parsed data from the buffer
346
+ @data.read(@parser_nbytes)
347
+ @parser.reset
348
+ @parser_nbytes = 0
349
+
350
+ true
351
+ end
352
+
353
+ def parse_response_header
354
+ return false unless parse_header(@response_header)
355
+
356
+ unless @response_header.http_status and @response_header.http_reason
357
+ @state = :invalid
358
+ on_error "no HTTP response"
359
+ return false
360
+ end
361
+
362
+ # correct location header - some servers will incorrectly give a relative URI
363
+ if @response_header.location
364
+ begin
365
+ location = URI.parse @response_header.location
366
+ if location.relative?
367
+ location = (@uri.merge location).to_s
368
+ @response_header[LOCATION] = location
369
+ end
370
+ rescue
371
+ on_error "Location header format error"
372
+ return false
373
+ end
374
+ end
375
+
376
+ # shortcircuit on HEAD requests
377
+ if @method == "HEAD"
378
+ @state = :finished
379
+ on_request_complete
380
+ end
381
+
382
+ if @response_header.chunked_encoding?
383
+ @state = :chunk_header
384
+ elsif @response_header.content_length
385
+ @state = :body
386
+ @bytes_remaining = @response_header.content_length
387
+ else
388
+ @state = :body
389
+ @bytes_remaining = nil
390
+ end
391
+
392
+ if decoder_class = HttpDecoders.decoder_for_encoding(response_header[CONTENT_ENCODING])
393
+ begin
394
+ @content_decoder = decoder_class.new do |s| on_decoded_body_data(s) end
395
+ rescue HttpDecoders::DecoderError
396
+ on_error "Content-decoder error"
397
+ end
398
+ end
399
+
400
+ true
401
+ end
402
+
403
+ def parse_chunk_header
404
+ return false unless parse_header(@chunk_header)
405
+
406
+ @bytes_remaining = @chunk_header.chunk_size
407
+ @chunk_header = HttpChunkHeader.new
408
+
409
+ @state = @bytes_remaining > 0 ? :chunk_body : :response_footer
410
+ true
411
+ end
412
+
413
+ def process_chunk_body
414
+ if @data.size < @bytes_remaining
415
+ @bytes_remaining -= @data.size
416
+ on_body_data @data.read
417
+ return false
418
+ end
419
+
420
+ on_body_data @data.read(@bytes_remaining)
421
+ @bytes_remaining = 0
422
+
423
+ @state = :chunk_footer
424
+ true
425
+ end
426
+
427
+ def process_chunk_footer
428
+ return false if @data.size < 2
429
+
430
+ if @data.read(2) == CRLF
431
+ @state = :chunk_header
432
+ else
433
+ @state = :invalid
434
+ on_error "non-CRLF chunk footer"
435
+ end
436
+
437
+ true
438
+ end
439
+
440
+ def process_response_footer
441
+ return false if @data.size < 2
442
+
443
+ if @data.read(2) == CRLF
444
+ if @data.empty?
445
+ @state = :finished
446
+ on_request_complete
447
+ else
448
+ @state = :invalid
449
+ on_error "garbage at end of chunked response"
450
+ end
451
+ else
452
+ @state = :invalid
453
+ on_error "non-CRLF response footer"
454
+ end
455
+
456
+ false
457
+ end
458
+
459
+ def process_body
460
+ if @bytes_remaining.nil?
461
+ on_body_data @data.read
462
+ return false
463
+ end
464
+
465
+ if @bytes_remaining.zero?
466
+ @state = :finished
467
+ on_request_complete
468
+ return false
469
+ end
470
+
471
+ if @data.size < @bytes_remaining
472
+ @bytes_remaining -= @data.size
473
+ on_body_data @data.read
474
+ return false
475
+ end
476
+
477
+ on_body_data @data.read(@bytes_remaining)
478
+ @bytes_remaining = 0
479
+
480
+ # If Keep-Alive is enabled, the server may be pushing more data to us
481
+ # after the first request is complete. Hence, finish first request, and
482
+ # reset state.
483
+ if @response_header.keep_alive?
484
+ @data.clear # hard reset, TODO: add support for keep-alive connections!
485
+ @state = :finished
486
+ on_request_complete
487
+
488
+ else
489
+ if @data.empty?
490
+ @state = :finished
491
+ on_request_complete
492
+ else
493
+ @state = :invalid
494
+ on_error "garbage at end of body"
495
+ end
496
+ end
497
+
498
+ false
499
+ end
500
+
501
+
502
+ end
503
+
504
+ end