tmm1-em-http-request 0.1.0

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
+ }
@@ -0,0 +1,404 @@
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
+ path + "?" + query.map { |k, v| encode_param(k, v) }.join('&')
98
+ end
99
+
100
+ # URL encodes query parameters:
101
+ # single k=v, or a URL encoded array, if v is an array of values
102
+ def encode_param(k, v)
103
+ if v.is_a?(Array)
104
+ v.map { |e| escape(k) + "[]=" + escape(e) }.join("&")
105
+ else
106
+ escape(k) + "=" + escape(v)
107
+ end
108
+ end
109
+
110
+ # Encode a field in an HTTP header
111
+ def encode_field(k, v)
112
+ FIELD_ENCODING % [k, v]
113
+ end
114
+
115
+ # Encode basic auth in an HTTP header
116
+ def encode_basic_auth(k,v)
117
+ BASIC_AUTH_ENCODING % [k, Base64.encode64(v.join(":")).chomp]
118
+ end
119
+
120
+ def encode_headers(head)
121
+ head.inject('') do |result, (key, value)|
122
+ # Munge keys from foo-bar-baz to Foo-Bar-Baz
123
+ key = key.split('-').map { |k| k.capitalize }.join('-')
124
+ unless key == "Authorization"
125
+ result << encode_field(key, value)
126
+ else
127
+ result << encode_basic_auth(key, value)
128
+ end
129
+ end
130
+ end
131
+
132
+ def encode_cookies(cookies)
133
+ cookies.inject('') { |result, (k, v)| result << encode_field('Cookie', encode_param(k, v)) }
134
+ end
135
+ end
136
+
137
+ class HttpClient < Connection
138
+ include EventMachine::Deferrable
139
+ include HttpEncoding
140
+
141
+ TRANSFER_ENCODING="TRANSFER_ENCODING"
142
+ CONTENT_ENCODING="CONTENT_ENCODING"
143
+ CONTENT_LENGTH="CONTENT_LENGTH"
144
+ KEEP_ALIVE="CONNECTION"
145
+ SET_COOKIE="SET_COOKIE"
146
+ LOCATION="LOCATION"
147
+ HOST="HOST"
148
+ CRLF="\r\n"
149
+
150
+ attr_accessor :method, :options, :uri
151
+ attr_reader :response, :response_header, :errors
152
+
153
+ def post_init
154
+ self.comm_inactivity_timeout = 5
155
+
156
+ @parser = HttpClientParser.new
157
+ @data = EventMachine::Buffer.new
158
+ @response_header = HttpResponseHeader.new
159
+ @chunk_header = HttpChunkHeader.new
160
+
161
+ @state = :response_header
162
+ @parser_nbytes = 0
163
+ @inflate = []
164
+ @response = ''
165
+ @errors = ''
166
+ end
167
+
168
+ # start HTTP request once we establish connection to host
169
+ def connection_completed
170
+ send_request_header
171
+ send_request_body
172
+ end
173
+
174
+ # request is done, invoke the callback
175
+ def on_request_complete
176
+
177
+ if @response_header.compressed? and @inflate.include?(response_header[CONTENT_ENCODING])
178
+ case response_header[CONTENT_ENCODING]
179
+ when 'deflate' then
180
+ @response = Zlib::Inflate.inflate(@response)
181
+ when 'gzip', 'compressed' then
182
+ @response = Zlib::GzipReader.new(StringIO.new(@response)).read
183
+ end
184
+ end
185
+
186
+ unbind
187
+ end
188
+
189
+ # request failed, invoke errback
190
+ def on_error(msg)
191
+ @errors = msg
192
+ unbind
193
+ end
194
+
195
+ def send_request_header
196
+ query = @options[:query]
197
+ head = @options[:head] ? munge_header_keys(@options[:head]) : {}
198
+ body = @options[:body]
199
+
200
+ # Set the Host header if it hasn't been specified already
201
+ head['host'] ||= encode_host
202
+
203
+ # Set the Content-Length if body is given
204
+ head['content-length'] = body.length if body
205
+
206
+ # Set the User-Agent if it hasn't been specified
207
+ head['user-agent'] ||= "EventMachine HttpClient"
208
+
209
+ # Set auto-inflate flags
210
+ if head['accept-encoding']
211
+ @inflate = head['accept-encoding'].split(',').map {|t| t.strip}
212
+ end
213
+
214
+ # Build the request
215
+ request_header = encode_request(@method, @uri.path, query)
216
+ request_header << encode_headers(head)
217
+ request_header << CRLF
218
+
219
+ send_data request_header
220
+ end
221
+
222
+ def send_request_body
223
+ send_data @options[:body] if @options[:body]
224
+ end
225
+
226
+ def receive_data(data)
227
+ @data << data
228
+ dispatch
229
+ end
230
+
231
+ # Called when part of the body has been read
232
+ def on_body_data(data)
233
+ @response << data
234
+ end
235
+
236
+ def unbind
237
+ (@state == :finished) ? succeed : fail
238
+ close_connection
239
+ end
240
+
241
+ #
242
+ # Response processing
243
+ #
244
+
245
+ def dispatch
246
+ while case @state
247
+ when :response_header
248
+ parse_response_header
249
+ when :chunk_header
250
+ parse_chunk_header
251
+ when :chunk_body
252
+ process_chunk_body
253
+ when :chunk_footer
254
+ process_chunk_footer
255
+ when :response_footer
256
+ process_response_footer
257
+ when :body
258
+ process_body
259
+ when :finished, :invalid
260
+ break
261
+ else raise RuntimeError, "invalid state: #{@state}"
262
+ end
263
+ end
264
+ end
265
+
266
+ def parse_header(header)
267
+ return false if @data.empty?
268
+
269
+ begin
270
+ @parser_nbytes = @parser.execute(header, @data.to_str, @parser_nbytes)
271
+ rescue EventMachine::HttpClientParserError
272
+ @state = :invalid
273
+ on_error "invalid HTTP format, parsing fails"
274
+ end
275
+
276
+ return false unless @parser.finished?
277
+
278
+ # Clear parsed data from the buffer
279
+ @data.read(@parser_nbytes)
280
+ @parser.reset
281
+ @parser_nbytes = 0
282
+
283
+ true
284
+ end
285
+
286
+ def parse_response_header
287
+ return false unless parse_header(@response_header)
288
+
289
+ unless @response_header.http_status and @response_header.http_reason
290
+ @state = :invalid
291
+ on_error "no HTTP response"
292
+ return false
293
+ end
294
+
295
+ if @response_header.chunked_encoding?
296
+ @state = :chunk_header
297
+ else
298
+ @state = :body
299
+ @bytes_remaining = @response_header.content_length
300
+ end
301
+
302
+ true
303
+ end
304
+
305
+ def parse_chunk_header
306
+ return false unless parse_header(@chunk_header)
307
+
308
+ @bytes_remaining = @chunk_header.chunk_size
309
+ @chunk_header = HttpChunkHeader.new
310
+
311
+ @state = @bytes_remaining > 0 ? :chunk_body : :response_footer
312
+ true
313
+ end
314
+
315
+ def process_chunk_body
316
+ if @data.size < @bytes_remaining
317
+ @bytes_remaining -= @data.size
318
+ on_body_data @data.read
319
+ return false
320
+ end
321
+
322
+ on_body_data @data.read(@bytes_remaining)
323
+ @bytes_remaining = 0
324
+
325
+ @state = :chunk_footer
326
+ true
327
+ end
328
+
329
+ def process_chunk_footer
330
+ return false if @data.size < 2
331
+
332
+ if @data.read(2) == CRLF
333
+ @state = :chunk_header
334
+ else
335
+ @state = :invalid
336
+ on_error "non-CRLF chunk footer"
337
+ end
338
+
339
+ true
340
+ end
341
+
342
+ def process_response_footer
343
+ return false if @data.size < 2
344
+
345
+ if @data.read(2) == CRLF
346
+ if @data.empty?
347
+ @state = :finished
348
+ on_request_complete
349
+ else
350
+ @state = :invalid
351
+ on_error "garbage at end of chunked response"
352
+ end
353
+ else
354
+ @state = :invalid
355
+ on_error "non-CRLF response footer"
356
+ end
357
+
358
+ false
359
+ end
360
+
361
+ def process_body
362
+ if @bytes_remaining.nil?
363
+ on_body_data @data.read
364
+ return false
365
+ end
366
+
367
+ if @bytes_remaining.zero?
368
+ @state = :finished
369
+ on_request_complete
370
+ return false
371
+ end
372
+
373
+ if @data.size < @bytes_remaining
374
+ @bytes_remaining -= @data.size
375
+ on_body_data @data.read
376
+ return false
377
+ end
378
+
379
+ on_body_data @data.read(@bytes_remaining)
380
+ @bytes_remaining = 0
381
+
382
+ # If Keep-Alive is enabled, the server may be pushing more data to us
383
+ # after the first request is complete. Hence, finish first request, and
384
+ # reset state.
385
+ if @response_header.keep_alive?
386
+ @data.clear # hard reset, TODO: add support for keep-alive connections!
387
+ @state = :finished
388
+ on_request_complete
389
+
390
+ else
391
+ if @data.empty?
392
+ @state = :finished
393
+ on_request_complete
394
+ else
395
+ @state = :invalid
396
+ on_error "garbage at end of body"
397
+ end
398
+ end
399
+
400
+ false
401
+ end
402
+ end
403
+
404
+ end
@@ -0,0 +1,51 @@
1
+ module EventMachine
2
+
3
+ # EventMachine based Multi request client, based on a streaming HTTPRequest class,
4
+ # which allows you to open multiple parallel connections and return only when all
5
+ # of them finish. (i.e. ideal for parallelizing workloads)
6
+ #
7
+ # == Example
8
+ #
9
+ # EventMachine.run {
10
+ #
11
+ # multi = EventMachine::MultiRequest.new
12
+ #
13
+ # # add multiple requests to the multi-handler
14
+ # multi.add(EventMachine::HttpRequest.new('http://www.google.com/').get)
15
+ # multi.add(EventMachine::HttpRequest.new('http://www.yahoo.com/').get)
16
+ #
17
+ # multi.callback {
18
+ # p multi.responses[:succeeded]
19
+ # p multi.responses[:failed]
20
+ #
21
+ # EventMachine.stop
22
+ # }
23
+ # }
24
+ #
25
+
26
+ class MultiRequest
27
+ include EventMachine::Deferrable
28
+
29
+ attr_reader :requests, :responses
30
+
31
+ def initialize
32
+ @requests = []
33
+ @responses = {:succeeded => [], :failed => []}
34
+ end
35
+
36
+ def add(conn)
37
+ conn.callback { @responses[:succeeded].push(conn); check_progress }
38
+ conn.errback { @responses[:failed].push(conn); check_progress }
39
+
40
+ @requests.push(conn)
41
+ end
42
+
43
+ protected
44
+
45
+ # invoke callback if all requests have completed
46
+ def check_progress
47
+ succeed if (@responses[:succeeded].size + @responses[:failed].size) == @requests.size
48
+ end
49
+
50
+ end
51
+ end