em-http-request 0.2.0

Sign up to get free protection for your applications and to get access to all the features.

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