igrigorik-em-http-request 0.1.1

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
+ 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