astro-em-http-request 0.1.3.20090419

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.
@@ -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