astro-em-http-request 0.1.3.20090419

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,173 @@
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";
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
+ %%write eof;
144
+ parser->nread++;
145
+ }
146
+
147
+ return(parser->nread);
148
+ }
149
+
150
+ int httpclient_parser_finish(httpclient_parser *parser)
151
+ {
152
+ int cs = parser->cs;
153
+
154
+ %%write eof;
155
+
156
+ parser->cs = cs;
157
+
158
+ if (httpclient_parser_has_error(parser) ) {
159
+ return -1;
160
+ } else if (httpclient_parser_is_finished(parser) ) {
161
+ return 1;
162
+ } else {
163
+ return 0;
164
+ }
165
+ }
166
+
167
+ int httpclient_parser_has_error(httpclient_parser *parser) {
168
+ return parser->cs == httpclient_parser_error;
169
+ }
170
+
171
+ int httpclient_parser_is_finished(httpclient_parser *parser) {
172
+ return parser->cs == httpclient_parser_first_final;
173
+ }
data/lib/em-http.rb ADDED
@@ -0,0 +1,17 @@
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/client'
15
+ require File.dirname(__FILE__) + '/em-http/multi'
16
+ require File.dirname(__FILE__) + '/em-http/request'
17
+ require File.dirname(__FILE__) + '/em-http/decoders'
@@ -0,0 +1,428 @@
1
+ # -*- coding: undecided -*-
2
+ # #--
3
+ # Copyright (C)2008 Ilya Grigorik
4
+ #
5
+ # Includes portion originally Copyright (C)2007 Tony Arcieri
6
+ # Includes portion originally Copyright (C)2005 Zed Shaw
7
+ # You can redistribute this under the terms of the Ruby
8
+ # license See file LICENSE for details
9
+ # #--
10
+
11
+ module EventMachine
12
+
13
+ # A simple hash is returned for each request made by HttpClient with the
14
+ # headers that were given by the server for that request.
15
+ class HttpResponseHeader < Hash
16
+ # The reason returned in the http response ("OK","File not found",etc.)
17
+ attr_accessor :http_reason
18
+
19
+ # The HTTP version returned.
20
+ attr_accessor :http_version
21
+
22
+ # The status code (as a string!)
23
+ attr_accessor :http_status
24
+
25
+ # HTTP response status as an integer
26
+ def status
27
+ Integer(http_status) rescue nil
28
+ end
29
+
30
+ # Length of content as an integer, or nil if chunked/unspecified
31
+ def content_length
32
+ Integer(self[HttpClient::CONTENT_LENGTH]) rescue nil
33
+ end
34
+
35
+ # Is the transfer encoding chunked?
36
+ def chunked_encoding?
37
+ /chunked/i === self[HttpClient::TRANSFER_ENCODING]
38
+ end
39
+
40
+ def keep_alive?
41
+ /keep-alive/i === self[HttpClient::KEEP_ALIVE]
42
+ end
43
+
44
+ def compressed?
45
+ /gzip|compressed|deflate/i === self[HttpClient::CONTENT_ENCODING]
46
+ end
47
+ end
48
+
49
+ class HttpChunkHeader < Hash
50
+ # When parsing chunked encodings this is set
51
+ attr_accessor :http_chunk_size
52
+
53
+ # Size of the chunk as an integer
54
+ def chunk_size
55
+ return @chunk_size unless @chunk_size.nil?
56
+ @chunk_size = @http_chunk_size ? @http_chunk_size.to_i(base=16) : 0
57
+ end
58
+ end
59
+
60
+ # Methods for building HTTP requests
61
+ module HttpEncoding
62
+ HTTP_REQUEST_HEADER="%s %s HTTP/1.1\r\n"
63
+ FIELD_ENCODING = "%s: %s\r\n"
64
+ BASIC_AUTH_ENCODING = "%s: Basic %s\r\n"
65
+
66
+ # Escapes a URI.
67
+ def escape(s)
68
+ s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
69
+ '%'+$1.unpack('H2'*$1.size).join('%').upcase
70
+ }.tr(' ', '+')
71
+ end
72
+
73
+ # Unescapes a URI escaped string.
74
+ def unescape(s)
75
+ s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
76
+ [$1.delete('%')].pack('H*')
77
+ }
78
+ end
79
+
80
+ # Map all header keys to a downcased string version
81
+ def munge_header_keys(head)
82
+ head.inject({}) { |h, (k, v)| h[k.to_s.downcase] = v; h }
83
+ end
84
+
85
+ # HTTP is kind of retarded that you have to specify a Host header, but if
86
+ # you include port 80 then further redirects will tack on the :80 which is
87
+ # annoying.
88
+ def encode_host
89
+ @uri.host + (@uri.port.to_i != 80 ? ":#{@uri.port}" : "")
90
+ end
91
+
92
+ def encode_request(method, path, query)
93
+ HTTP_REQUEST_HEADER % [method.to_s.upcase, encode_query(path, query)]
94
+ end
95
+
96
+ def encode_query(path, query)
97
+ return path unless query
98
+ if query.kind_of? String
99
+ return "#{path}?#{query}"
100
+ else
101
+ return path + "?" + query.map { |k, v| encode_param(k, v) }.join('&')
102
+ end
103
+ end
104
+
105
+ # URL encodes query parameters:
106
+ # single k=v, or a URL encoded array, if v is an array of values
107
+ def encode_param(k, v)
108
+ if v.is_a?(Array)
109
+ v.map { |e| escape(k) + "[]=" + escape(e) }.join("&")
110
+ else
111
+ escape(k) + "=" + escape(v)
112
+ end
113
+ end
114
+
115
+ # Encode a field in an HTTP header
116
+ def encode_field(k, v)
117
+ FIELD_ENCODING % [k, v]
118
+ end
119
+
120
+ # Encode basic auth in an HTTP header
121
+ def encode_basic_auth(k,v)
122
+ BASIC_AUTH_ENCODING % [k, Base64.encode64(v.join(":")).chomp]
123
+ end
124
+
125
+ def encode_headers(head)
126
+ head.inject('') do |result, (key, value)|
127
+ # Munge keys from foo-bar-baz to Foo-Bar-Baz
128
+ key = key.split('-').map { |k| k.capitalize }.join('-')
129
+ unless key == "Authorization"
130
+ result << encode_field(key, value)
131
+ else
132
+ result << encode_basic_auth(key, value)
133
+ end
134
+ end
135
+ end
136
+
137
+ def encode_cookies(cookies)
138
+ cookies.inject('') { |result, (k, v)| result << encode_field('Cookie', encode_param(k, v)) }
139
+ end
140
+ end
141
+
142
+ class HttpClient < Connection
143
+ include EventMachine::Deferrable
144
+ include HttpEncoding
145
+
146
+ TRANSFER_ENCODING="TRANSFER_ENCODING"
147
+ CONTENT_ENCODING="CONTENT_ENCODING"
148
+ CONTENT_LENGTH="CONTENT_LENGTH"
149
+ KEEP_ALIVE="CONNECTION"
150
+ SET_COOKIE="SET_COOKIE"
151
+ LOCATION="LOCATION"
152
+ HOST="HOST"
153
+ CRLF="\r\n"
154
+
155
+ attr_accessor :method, :options, :uri
156
+ attr_reader :response, :response_header, :errors
157
+
158
+ def post_init
159
+ @parser = HttpClientParser.new
160
+ @data = EventMachine::Buffer.new
161
+ @response_header = HttpResponseHeader.new
162
+ @chunk_header = HttpChunkHeader.new
163
+
164
+ @state = :response_header
165
+ @parser_nbytes = 0
166
+ @response = ''
167
+ @inflate = []
168
+ @errors = ''
169
+ @content_decoder = nil
170
+ end
171
+
172
+ # start HTTP request once we establish connection to host
173
+ def connection_completed
174
+ send_request_header
175
+ send_request_body
176
+ end
177
+
178
+ # request is done, invoke the callback
179
+ def on_request_complete
180
+ begin
181
+ @content_decoder.finalize! if @content_decoder
182
+ rescue HttpDecoders::DecoderError
183
+ on_error "Content-decoder error"
184
+ end
185
+ unbind
186
+ end
187
+
188
+ # request failed, invoke errback
189
+ def on_error(msg)
190
+ @errors = msg
191
+ unbind
192
+ end
193
+
194
+ def send_request_header
195
+ query = @options[:query]
196
+ head = @options[:head] ? munge_header_keys(@options[:head]) : {}
197
+ body = @options[:body]
198
+
199
+ # Set the Host header if it hasn't been specified already
200
+ head['host'] ||= encode_host
201
+
202
+ # Set the Content-Length if body is given
203
+ head['content-length'] = body.length if body
204
+
205
+ # Set the User-Agent if it hasn't been specified
206
+ head['user-agent'] ||= "EventMachine HttpClient"
207
+
208
+ # Set auto-inflate flags
209
+ if head['accept-encoding']
210
+ @inflate = head['accept-encoding'].split(',').map {|t| t.strip}
211
+ end
212
+
213
+ # Build the request
214
+ request_header = encode_request(@method, @uri.path, query)
215
+ request_header << encode_headers(head)
216
+ request_header << CRLF
217
+
218
+ send_data request_header
219
+ end
220
+
221
+ def send_request_body
222
+ send_data @options[:body] if @options[:body]
223
+ end
224
+
225
+ def receive_data(data)
226
+ @data << data
227
+ dispatch
228
+ end
229
+
230
+ # Called when part of the body has been read
231
+ def on_body_data(data)
232
+ if @content_decoder
233
+ begin
234
+ @content_decoder << data
235
+ rescue HttpDecoders::DecoderError
236
+ on_error "Content-decoder error"
237
+ end
238
+ else
239
+ on_decoded_body_data(data)
240
+ end
241
+ end
242
+
243
+ def on_decoded_body_data(data)
244
+ if (on_response = @options[:on_response])
245
+ on_response.call(data)
246
+ else
247
+ @response << data
248
+ end
249
+ end
250
+
251
+ def unbind
252
+ (@state == :finished) ? succeed : fail
253
+ close_connection
254
+ end
255
+
256
+ #
257
+ # Response processing
258
+ #
259
+
260
+ def dispatch
261
+ while case @state
262
+ when :response_header
263
+ parse_response_header
264
+ when :chunk_header
265
+ parse_chunk_header
266
+ when :chunk_body
267
+ process_chunk_body
268
+ when :chunk_footer
269
+ process_chunk_footer
270
+ when :response_footer
271
+ process_response_footer
272
+ when :body
273
+ process_body
274
+ when :finished, :invalid
275
+ break
276
+ else raise RuntimeError, "invalid state: #{@state}"
277
+ end
278
+ end
279
+ end
280
+
281
+ def parse_header(header)
282
+ return false if @data.empty?
283
+
284
+ begin
285
+ @parser_nbytes = @parser.execute(header, @data.to_str, @parser_nbytes)
286
+ rescue EventMachine::HttpClientParserError
287
+ @state = :invalid
288
+ on_error "invalid HTTP format, parsing fails"
289
+ end
290
+
291
+ return false unless @parser.finished?
292
+
293
+ # Clear parsed data from the buffer
294
+ @data.read(@parser_nbytes)
295
+ @parser.reset
296
+ @parser_nbytes = 0
297
+
298
+ true
299
+ end
300
+
301
+ def parse_response_header
302
+ return false unless parse_header(@response_header)
303
+
304
+ unless @response_header.http_status and @response_header.http_reason
305
+ @state = :invalid
306
+ on_error "no HTTP response"
307
+ return false
308
+ end
309
+
310
+ if @response_header.chunked_encoding?
311
+ @state = :chunk_header
312
+ else
313
+ @state = :body
314
+ @bytes_remaining = @response_header.content_length
315
+ end
316
+
317
+ if @inflate.include?(response_header[CONTENT_ENCODING]) &&
318
+ decoder_class = HttpDecoders.decoder_for_encoding(response_header[CONTENT_ENCODING])
319
+ begin
320
+ @content_decoder = decoder_class.new do |s| on_decoded_body_data(s) end
321
+ rescue HttpDecoders::DecoderError
322
+ on_error "Content-decoder error"
323
+ end
324
+ end
325
+
326
+ true
327
+ end
328
+
329
+ def parse_chunk_header
330
+ return false unless parse_header(@chunk_header)
331
+
332
+ @bytes_remaining = @chunk_header.chunk_size
333
+ @chunk_header = HttpChunkHeader.new
334
+
335
+ @state = @bytes_remaining > 0 ? :chunk_body : :response_footer
336
+ true
337
+ end
338
+
339
+ def process_chunk_body
340
+ if @data.size < @bytes_remaining
341
+ @bytes_remaining -= @data.size
342
+ on_body_data @data.read
343
+ return false
344
+ end
345
+
346
+ on_body_data @data.read(@bytes_remaining)
347
+ @bytes_remaining = 0
348
+
349
+ @state = :chunk_footer
350
+ true
351
+ end
352
+
353
+ def process_chunk_footer
354
+ return false if @data.size < 2
355
+
356
+ if @data.read(2) == CRLF
357
+ @state = :chunk_header
358
+ else
359
+ @state = :invalid
360
+ on_error "non-CRLF chunk footer"
361
+ end
362
+
363
+ true
364
+ end
365
+
366
+ def process_response_footer
367
+ return false if @data.size < 2
368
+
369
+ if @data.read(2) == CRLF
370
+ if @data.empty?
371
+ @state = :finished
372
+ on_request_complete
373
+ else
374
+ @state = :invalid
375
+ on_error "garbage at end of chunked response"
376
+ end
377
+ else
378
+ @state = :invalid
379
+ on_error "non-CRLF response footer"
380
+ end
381
+
382
+ false
383
+ end
384
+
385
+ def process_body
386
+ if @bytes_remaining.nil?
387
+ on_body_data @data.read
388
+ return false
389
+ end
390
+
391
+ if @bytes_remaining.zero?
392
+ @state = :finished
393
+ on_request_complete
394
+ return false
395
+ end
396
+
397
+ if @data.size < @bytes_remaining
398
+ @bytes_remaining -= @data.size
399
+ on_body_data @data.read
400
+ return false
401
+ end
402
+
403
+ on_body_data @data.read(@bytes_remaining)
404
+ @bytes_remaining = 0
405
+
406
+ # If Keep-Alive is enabled, the server may be pushing more data to us
407
+ # after the first request is complete. Hence, finish first request, and
408
+ # reset state.
409
+ if @response_header.keep_alive?
410
+ @data.clear # hard reset, TODO: add support for keep-alive connections!
411
+ @state = :finished
412
+ on_request_complete
413
+
414
+ else
415
+ if @data.empty?
416
+ @state = :finished
417
+ on_request_complete
418
+ else
419
+ @state = :invalid
420
+ on_error "garbage at end of body"
421
+ end
422
+ end
423
+
424
+ false
425
+ end
426
+ end
427
+
428
+ end