camerontaylor-em-http-request 0.1.7

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