igrigorik-em-http-request 0.1.1

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
+ require 'zlib'
10
+
11
+
12
+ require File.dirname(__FILE__) + '/http11_client'
13
+ require File.dirname(__FILE__) + '/em_buffer'
14
+
15
+ require File.dirname(__FILE__) + '/em-http/client'
16
+ require File.dirname(__FILE__) + '/em-http/multi'
17
+ require File.dirname(__FILE__) + '/em-http/request'
@@ -0,0 +1,408 @@
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
+ self.comm_inactivity_timeout = 5
159
+
160
+ @parser = HttpClientParser.new
161
+ @data = EventMachine::Buffer.new
162
+ @response_header = HttpResponseHeader.new
163
+ @chunk_header = HttpChunkHeader.new
164
+
165
+ @state = :response_header
166
+ @parser_nbytes = 0
167
+ @inflate = []
168
+ @response = ''
169
+ @errors = ''
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
+
181
+ if @response_header.compressed? and @inflate.include?(response_header[CONTENT_ENCODING])
182
+ case response_header[CONTENT_ENCODING]
183
+ when 'deflate' then
184
+ @response = Zlib::Inflate.inflate(@response)
185
+ when 'gzip', 'compressed' then
186
+ @response = Zlib::GzipReader.new(StringIO.new(@response)).read
187
+ end
188
+ end
189
+
190
+ unbind
191
+ end
192
+
193
+ # request failed, invoke errback
194
+ def on_error(msg)
195
+ @errors = msg
196
+ unbind
197
+ end
198
+
199
+ def send_request_header
200
+ query = @options[:query]
201
+ head = @options[:head] ? munge_header_keys(@options[:head]) : {}
202
+ body = @options[:body]
203
+
204
+ # Set the Host header if it hasn't been specified already
205
+ head['host'] ||= encode_host
206
+
207
+ # Set the Content-Length if body is given
208
+ head['content-length'] = body.length if body
209
+
210
+ # Set the User-Agent if it hasn't been specified
211
+ head['user-agent'] ||= "EventMachine HttpClient"
212
+
213
+ # Set auto-inflate flags
214
+ if head['accept-encoding']
215
+ @inflate = head['accept-encoding'].split(',').map {|t| t.strip}
216
+ end
217
+
218
+ # Build the request
219
+ request_header = encode_request(@method, @uri.path, query)
220
+ request_header << encode_headers(head)
221
+ request_header << CRLF
222
+
223
+ send_data request_header
224
+ end
225
+
226
+ def send_request_body
227
+ send_data @options[:body] if @options[:body]
228
+ end
229
+
230
+ def receive_data(data)
231
+ @data << data
232
+ dispatch
233
+ end
234
+
235
+ # Called when part of the body has been read
236
+ def on_body_data(data)
237
+ @response << data
238
+ end
239
+
240
+ def unbind
241
+ (@state == :finished) ? succeed : fail
242
+ close_connection
243
+ end
244
+
245
+ #
246
+ # Response processing
247
+ #
248
+
249
+ def dispatch
250
+ while case @state
251
+ when :response_header
252
+ parse_response_header
253
+ when :chunk_header
254
+ parse_chunk_header
255
+ when :chunk_body
256
+ process_chunk_body
257
+ when :chunk_footer
258
+ process_chunk_footer
259
+ when :response_footer
260
+ process_response_footer
261
+ when :body
262
+ process_body
263
+ when :finished, :invalid
264
+ break
265
+ else raise RuntimeError, "invalid state: #{@state}"
266
+ end
267
+ end
268
+ end
269
+
270
+ def parse_header(header)
271
+ return false if @data.empty?
272
+
273
+ begin
274
+ @parser_nbytes = @parser.execute(header, @data.to_str, @parser_nbytes)
275
+ rescue EventMachine::HttpClientParserError
276
+ @state = :invalid
277
+ on_error "invalid HTTP format, parsing fails"
278
+ end
279
+
280
+ return false unless @parser.finished?
281
+
282
+ # Clear parsed data from the buffer
283
+ @data.read(@parser_nbytes)
284
+ @parser.reset
285
+ @parser_nbytes = 0
286
+
287
+ true
288
+ end
289
+
290
+ def parse_response_header
291
+ return false unless parse_header(@response_header)
292
+
293
+ unless @response_header.http_status and @response_header.http_reason
294
+ @state = :invalid
295
+ on_error "no HTTP response"
296
+ return false
297
+ end
298
+
299
+ if @response_header.chunked_encoding?
300
+ @state = :chunk_header
301
+ else
302
+ @state = :body
303
+ @bytes_remaining = @response_header.content_length
304
+ end
305
+
306
+ true
307
+ end
308
+
309
+ def parse_chunk_header
310
+ return false unless parse_header(@chunk_header)
311
+
312
+ @bytes_remaining = @chunk_header.chunk_size
313
+ @chunk_header = HttpChunkHeader.new
314
+
315
+ @state = @bytes_remaining > 0 ? :chunk_body : :response_footer
316
+ true
317
+ end
318
+
319
+ def process_chunk_body
320
+ if @data.size < @bytes_remaining
321
+ @bytes_remaining -= @data.size
322
+ on_body_data @data.read
323
+ return false
324
+ end
325
+
326
+ on_body_data @data.read(@bytes_remaining)
327
+ @bytes_remaining = 0
328
+
329
+ @state = :chunk_footer
330
+ true
331
+ end
332
+
333
+ def process_chunk_footer
334
+ return false if @data.size < 2
335
+
336
+ if @data.read(2) == CRLF
337
+ @state = :chunk_header
338
+ else
339
+ @state = :invalid
340
+ on_error "non-CRLF chunk footer"
341
+ end
342
+
343
+ true
344
+ end
345
+
346
+ def process_response_footer
347
+ return false if @data.size < 2
348
+
349
+ if @data.read(2) == CRLF
350
+ if @data.empty?
351
+ @state = :finished
352
+ on_request_complete
353
+ else
354
+ @state = :invalid
355
+ on_error "garbage at end of chunked response"
356
+ end
357
+ else
358
+ @state = :invalid
359
+ on_error "non-CRLF response footer"
360
+ end
361
+
362
+ false
363
+ end
364
+
365
+ def process_body
366
+ if @bytes_remaining.nil?
367
+ on_body_data @data.read
368
+ return false
369
+ end
370
+
371
+ if @bytes_remaining.zero?
372
+ @state = :finished
373
+ on_request_complete
374
+ return false
375
+ end
376
+
377
+ if @data.size < @bytes_remaining
378
+ @bytes_remaining -= @data.size
379
+ on_body_data @data.read
380
+ return false
381
+ end
382
+
383
+ on_body_data @data.read(@bytes_remaining)
384
+ @bytes_remaining = 0
385
+
386
+ # If Keep-Alive is enabled, the server may be pushing more data to us
387
+ # after the first request is complete. Hence, finish first request, and
388
+ # reset state.
389
+ if @response_header.keep_alive?
390
+ @data.clear # hard reset, TODO: add support for keep-alive connections!
391
+ @state = :finished
392
+ on_request_complete
393
+
394
+ else
395
+ if @data.empty?
396
+ @state = :finished
397
+ on_request_complete
398
+ else
399
+ @state = :invalid
400
+ on_error "garbage at end of body"
401
+ end
402
+ end
403
+
404
+ false
405
+ end
406
+ end
407
+
408
+ end