em-http-request 0.2.10 → 0.2.14
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.
- data/Changelog.md +36 -0
- data/LICENSE +58 -58
- data/README.md +168 -0
- data/Rakefile +9 -5
- data/VERSION +1 -1
- data/em-http-request.gemspec +12 -9
- data/examples/fetch.rb +30 -30
- data/examples/fibered-http.rb +38 -38
- data/examples/oauth-tweet.rb +49 -49
- data/examples/socks5.rb +26 -0
- data/examples/websocket-handler.rb +28 -28
- data/examples/websocket-server.rb +8 -8
- data/ext/buffer/extconf.rb +53 -53
- data/ext/http11_client/ext_help.h +14 -14
- data/ext/http11_client/extconf.rb +6 -6
- data/ext/http11_client/http11_client.c +328 -328
- data/ext/http11_client/http11_parser.c +418 -418
- data/ext/http11_client/http11_parser.h +48 -48
- data/ext/http11_client/http11_parser.rl +170 -170
- data/lib/em-http/client.rb +268 -42
- data/lib/em-http/http_options.rb +4 -2
- data/lib/em-http/mock.rb +90 -50
- data/lib/em-http/multi.rb +21 -17
- data/lib/em-http/request.rb +1 -0
- data/lib/em-http.rb +20 -19
- data/spec/encoding_spec.rb +34 -0
- data/spec/fixtures/google.ca +20 -21
- data/spec/helper.rb +3 -2
- data/spec/mock_spec.rb +79 -34
- data/spec/multi_spec.rb +27 -10
- data/spec/request_spec.rb +301 -68
- data/spec/spec.opts +7 -0
- data/spec/stallion.rb +64 -3
- data/spec/stub_server.rb +22 -22
- metadata +16 -9
- data/README.rdoc +0 -138
- data/lib/em-http/core_ext/hash.rb +0 -53
- data/spec/hash_spec.rb +0 -24
data/lib/em-http/client.rb
CHANGED
|
@@ -23,17 +23,16 @@ module EventMachine
|
|
|
23
23
|
|
|
24
24
|
# E-Tag
|
|
25
25
|
def etag
|
|
26
|
-
self[
|
|
26
|
+
self[HttpClient::ETAG]
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def last_modified
|
|
30
|
-
|
|
31
|
-
Time.parse(time) if time
|
|
30
|
+
self[HttpClient::LAST_MODIFIED]
|
|
32
31
|
end
|
|
33
32
|
|
|
34
33
|
# HTTP response status as an integer
|
|
35
34
|
def status
|
|
36
|
-
Integer(http_status) rescue
|
|
35
|
+
Integer(http_status) rescue 0
|
|
37
36
|
end
|
|
38
37
|
|
|
39
38
|
# Length of content as an integer, or nil if chunked/unspecified
|
|
@@ -83,9 +82,9 @@ module EventMachine
|
|
|
83
82
|
|
|
84
83
|
# Escapes a URI.
|
|
85
84
|
def escape(s)
|
|
86
|
-
s.to_s.gsub(/([^
|
|
87
|
-
'%'+$1.unpack('H2'
|
|
88
|
-
}
|
|
85
|
+
s.to_s.gsub(/([^a-zA-Z0-9_.-]+)/n) {
|
|
86
|
+
'%'+$1.unpack('H2'*bytesize($1)).join('%').upcase
|
|
87
|
+
}
|
|
89
88
|
end
|
|
90
89
|
|
|
91
90
|
# Unescapes a URI escaped string.
|
|
@@ -95,6 +94,16 @@ module EventMachine
|
|
|
95
94
|
}
|
|
96
95
|
end
|
|
97
96
|
|
|
97
|
+
if ''.respond_to?(:bytesize)
|
|
98
|
+
def bytesize(string)
|
|
99
|
+
string.bytesize
|
|
100
|
+
end
|
|
101
|
+
else
|
|
102
|
+
def bytesize(string)
|
|
103
|
+
string.size
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
98
107
|
# Map all header keys to a downcased string version
|
|
99
108
|
def munge_header_keys(head)
|
|
100
109
|
head.inject({}) { |h, (k, v)| h[k.to_s.downcase] = v; h }
|
|
@@ -111,21 +120,27 @@ module EventMachine
|
|
|
111
120
|
end
|
|
112
121
|
end
|
|
113
122
|
|
|
114
|
-
def encode_request(method,
|
|
115
|
-
|
|
123
|
+
def encode_request(method, uri, query, proxy)
|
|
124
|
+
query = encode_query(uri, query)
|
|
125
|
+
|
|
126
|
+
# Non CONNECT proxies require that you provide the full request
|
|
127
|
+
# uri in request header, as opposed to a relative path.
|
|
128
|
+
query = uri.join(query) if proxy && proxy[:type] != :socks && !proxy[:use_connect]
|
|
129
|
+
|
|
130
|
+
HTTP_REQUEST_HEADER % [method.to_s.upcase, query]
|
|
116
131
|
end
|
|
117
132
|
|
|
118
|
-
def encode_query(
|
|
133
|
+
def encode_query(uri, query)
|
|
119
134
|
encoded_query = if query.kind_of?(Hash)
|
|
120
135
|
query.map { |k, v| encode_param(k, v) }.join('&')
|
|
121
136
|
else
|
|
122
137
|
query.to_s
|
|
123
138
|
end
|
|
124
|
-
|
|
125
|
-
|
|
139
|
+
|
|
140
|
+
if !uri.query.to_s.empty?
|
|
141
|
+
encoded_query = [encoded_query, uri.query].reject {|part| part.empty?}.join("&")
|
|
126
142
|
end
|
|
127
|
-
|
|
128
|
-
"#{path}?#{encoded_query}"
|
|
143
|
+
encoded_query.to_s.empty? ? uri.path : "#{uri.path}?#{encoded_query}"
|
|
129
144
|
end
|
|
130
145
|
|
|
131
146
|
# URL encodes query parameters:
|
|
@@ -138,6 +153,29 @@ module EventMachine
|
|
|
138
153
|
end
|
|
139
154
|
end
|
|
140
155
|
|
|
156
|
+
def form_encode_body(obj)
|
|
157
|
+
pairs = []
|
|
158
|
+
recursive = Proc.new do |h, prefix|
|
|
159
|
+
h.each do |k,v|
|
|
160
|
+
key = prefix == '' ? escape(k) : "#{prefix}[#{escape(k)}]"
|
|
161
|
+
|
|
162
|
+
if v.is_a? Array
|
|
163
|
+
nh = Hash.new
|
|
164
|
+
v.size.times { |t| nh[t] = v[t] }
|
|
165
|
+
recursive.call(nh, key)
|
|
166
|
+
|
|
167
|
+
elsif v.is_a? Hash
|
|
168
|
+
recursive.call(v, key)
|
|
169
|
+
else
|
|
170
|
+
pairs << "#{key}=#{escape(v)}"
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
recursive.call(obj, '')
|
|
176
|
+
return pairs.join('&')
|
|
177
|
+
end
|
|
178
|
+
|
|
141
179
|
# Encode a field in an HTTP header
|
|
142
180
|
def encode_field(k, v)
|
|
143
181
|
FIELD_ENCODING % [k, v]
|
|
@@ -183,10 +221,13 @@ module EventMachine
|
|
|
183
221
|
TRANSFER_ENCODING="TRANSFER_ENCODING"
|
|
184
222
|
CONTENT_ENCODING="CONTENT_ENCODING"
|
|
185
223
|
CONTENT_LENGTH="CONTENT_LENGTH"
|
|
224
|
+
LAST_MODIFIED="LAST_MODIFIED"
|
|
186
225
|
KEEP_ALIVE="CONNECTION"
|
|
187
226
|
SET_COOKIE="SET_COOKIE"
|
|
188
227
|
LOCATION="LOCATION"
|
|
189
228
|
HOST="HOST"
|
|
229
|
+
ETAG="ETAG"
|
|
230
|
+
|
|
190
231
|
CRLF="\r\n"
|
|
191
232
|
|
|
192
233
|
attr_accessor :method, :options, :uri
|
|
@@ -206,19 +247,28 @@ module EventMachine
|
|
|
206
247
|
@stream = nil
|
|
207
248
|
@disconnect = nil
|
|
208
249
|
@state = :response_header
|
|
250
|
+
@socks_state = nil
|
|
209
251
|
end
|
|
210
252
|
|
|
211
253
|
# start HTTP request once we establish connection to host
|
|
212
254
|
def connection_completed
|
|
213
|
-
# if
|
|
214
|
-
# to
|
|
215
|
-
|
|
216
|
-
|
|
255
|
+
# if a socks proxy is specified, then a connection request
|
|
256
|
+
# has to be made to the socks server and we need to wait
|
|
257
|
+
# for a response code
|
|
258
|
+
if socks_proxy? and @state == :response_header
|
|
259
|
+
@state = :connect_socks_proxy
|
|
260
|
+
send_socks_handshake
|
|
261
|
+
|
|
262
|
+
# if we need to negotiate the proxy connection first, then
|
|
263
|
+
# issue a CONNECT query and wait for 200 response
|
|
264
|
+
elsif connect_proxy? and @state == :response_header
|
|
265
|
+
@state = :connect_http_proxy
|
|
217
266
|
send_request_header
|
|
218
267
|
|
|
219
268
|
# if connecting via proxy, then state will be :proxy_connected,
|
|
220
269
|
# indicating successful tunnel. from here, initiate normal http
|
|
221
270
|
# exchange
|
|
271
|
+
|
|
222
272
|
else
|
|
223
273
|
@state = :response_header
|
|
224
274
|
ssl = @options[:tls] || @options[:ssl] || {}
|
|
@@ -247,6 +297,7 @@ module EventMachine
|
|
|
247
297
|
# fail the connection directly
|
|
248
298
|
dns_error == true ? fail(self) : unbind
|
|
249
299
|
end
|
|
300
|
+
alias :close :on_error
|
|
250
301
|
|
|
251
302
|
# assign a stream processing block
|
|
252
303
|
def stream(&blk)
|
|
@@ -258,6 +309,11 @@ module EventMachine
|
|
|
258
309
|
@disconnect = blk
|
|
259
310
|
end
|
|
260
311
|
|
|
312
|
+
# assign a headers parse callback
|
|
313
|
+
def headers(&blk)
|
|
314
|
+
@headers = blk
|
|
315
|
+
end
|
|
316
|
+
|
|
261
317
|
# raw data push from the client (WebSocket) should
|
|
262
318
|
# only be invoked after handshake, otherwise it will
|
|
263
319
|
# inject data into the header exchange
|
|
@@ -275,31 +331,75 @@ module EventMachine
|
|
|
275
331
|
def normalize_body
|
|
276
332
|
@normalized_body ||= begin
|
|
277
333
|
if @options[:body].is_a? Hash
|
|
278
|
-
@options[:body]
|
|
334
|
+
form_encode_body(@options[:body])
|
|
279
335
|
else
|
|
280
336
|
@options[:body]
|
|
281
337
|
end
|
|
282
338
|
end
|
|
283
339
|
end
|
|
284
340
|
|
|
341
|
+
# determines if there is enough data in the buffer
|
|
342
|
+
def has_bytes?(num)
|
|
343
|
+
@data.size >= num
|
|
344
|
+
end
|
|
345
|
+
|
|
285
346
|
def websocket?; @uri.scheme == 'ws'; end
|
|
347
|
+
def proxy?; !@options[:proxy].nil?; end
|
|
348
|
+
|
|
349
|
+
# determines if a proxy should be used that uses
|
|
350
|
+
# http-headers as proxy-mechanism
|
|
351
|
+
#
|
|
352
|
+
# this is the default proxy type if none is specified
|
|
353
|
+
def http_proxy?; proxy? && [nil, :http].include?(@options[:proxy][:type]); end
|
|
354
|
+
|
|
355
|
+
# determines if a http-proxy should be used with
|
|
356
|
+
# the CONNECT verb
|
|
357
|
+
def connect_proxy?; http_proxy? && (@options[:proxy][:use_connect] == true); end
|
|
358
|
+
|
|
359
|
+
# determines if a SOCKS5 proxy should be used
|
|
360
|
+
def socks_proxy?; proxy? && (@options[:proxy][:type] == :socks); end
|
|
361
|
+
|
|
362
|
+
def socks_methods
|
|
363
|
+
methods = []
|
|
364
|
+
methods << 2 if !options[:proxy][:authorization].nil? # 2 => Username/Password Authentication
|
|
365
|
+
methods << 0 # 0 => No Authentication Required
|
|
366
|
+
|
|
367
|
+
methods
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def send_socks_handshake
|
|
371
|
+
# Method Negotiation as described on
|
|
372
|
+
# http://www.faqs.org/rfcs/rfc1928.html Section 3
|
|
373
|
+
|
|
374
|
+
@socks_state = :method_negotiation
|
|
375
|
+
|
|
376
|
+
methods = socks_methods
|
|
377
|
+
send_data [5, methods.size].pack('CC') + methods.pack('C*')
|
|
378
|
+
end
|
|
286
379
|
|
|
287
380
|
def send_request_header
|
|
288
381
|
query = @options[:query]
|
|
289
382
|
head = @options[:head] ? munge_header_keys(@options[:head]) : {}
|
|
290
383
|
file = @options[:file]
|
|
384
|
+
proxy = @options[:proxy]
|
|
291
385
|
body = normalize_body
|
|
292
|
-
request_header = nil
|
|
293
386
|
|
|
294
|
-
|
|
295
|
-
proxy = @options[:proxy]
|
|
387
|
+
request_header = nil
|
|
296
388
|
|
|
297
|
-
|
|
389
|
+
if http_proxy?
|
|
390
|
+
# initialize headers for the http proxy
|
|
298
391
|
head = proxy[:head] ? munge_header_keys(proxy[:head]) : {}
|
|
299
392
|
head['proxy-authorization'] = proxy[:authorization] if proxy[:authorization]
|
|
300
|
-
request_header = HTTP_REQUEST_HEADER % ['CONNECT', "#{@uri.host}:#{@uri.port}"]
|
|
301
393
|
|
|
302
|
-
|
|
394
|
+
# if we need to negotiate the tunnel connection first, then
|
|
395
|
+
# issue a CONNECT query to the proxy first. This is an optional
|
|
396
|
+
# flag, by default we will provide full URIs to the proxy
|
|
397
|
+
if @state == :connect_http_proxy
|
|
398
|
+
request_header = HTTP_REQUEST_HEADER % ['CONNECT', "#{@uri.host}:#{@uri.port}"]
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
if websocket?
|
|
303
403
|
head['upgrade'] = 'WebSocket'
|
|
304
404
|
head['connection'] = 'Upgrade'
|
|
305
405
|
head['origin'] = @options[:origin] || @uri.host
|
|
@@ -332,7 +432,7 @@ module EventMachine
|
|
|
332
432
|
@last_effective_url = @uri
|
|
333
433
|
|
|
334
434
|
# Build the request headers
|
|
335
|
-
request_header ||= encode_request(@method, @uri
|
|
435
|
+
request_header ||= encode_request(@method, @uri, query, proxy)
|
|
336
436
|
request_header << encode_headers(head)
|
|
337
437
|
request_header << CRLF
|
|
338
438
|
send_data request_header
|
|
@@ -376,19 +476,24 @@ module EventMachine
|
|
|
376
476
|
|
|
377
477
|
def unbind
|
|
378
478
|
if (@state == :finished) && (@last_effective_url != @uri) && (@redirects < @options[:redirects])
|
|
379
|
-
|
|
380
|
-
|
|
479
|
+
begin
|
|
480
|
+
# update uri to redirect location if we're allowed to traverse deeper
|
|
481
|
+
@uri = @last_effective_url
|
|
381
482
|
|
|
382
|
-
|
|
383
|
-
|
|
483
|
+
# keep track of the depth of requests we made in this session
|
|
484
|
+
@redirects += 1
|
|
384
485
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
486
|
+
# swap current connection and reassign current handler
|
|
487
|
+
req = HttpOptions.new(@method, @uri, @options)
|
|
488
|
+
reconnect(req.host, req.port)
|
|
388
489
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
490
|
+
@response_header = HttpResponseHeader.new
|
|
491
|
+
@state = :response_header
|
|
492
|
+
@response = ''
|
|
493
|
+
@data.clear
|
|
494
|
+
rescue EventMachine::ConnectionError => e
|
|
495
|
+
on_error(e.message, true)
|
|
496
|
+
end
|
|
392
497
|
else
|
|
393
498
|
if @state == :finished || (@state == :body && @bytes_remaining.nil?)
|
|
394
499
|
succeed(self)
|
|
@@ -405,7 +510,9 @@ module EventMachine
|
|
|
405
510
|
|
|
406
511
|
def dispatch
|
|
407
512
|
while case @state
|
|
408
|
-
when :
|
|
513
|
+
when :connect_socks_proxy
|
|
514
|
+
parse_socks_response
|
|
515
|
+
when :connect_http_proxy
|
|
409
516
|
parse_response_header
|
|
410
517
|
when :response_header
|
|
411
518
|
parse_response_header
|
|
@@ -451,13 +558,17 @@ module EventMachine
|
|
|
451
558
|
def parse_response_header
|
|
452
559
|
return false unless parse_header(@response_header)
|
|
453
560
|
|
|
561
|
+
# invoke headers callback after full parse if one
|
|
562
|
+
# is specified by the user
|
|
563
|
+
@headers.call(@response_header) if @headers
|
|
564
|
+
|
|
454
565
|
unless @response_header.http_status and @response_header.http_reason
|
|
455
566
|
@state = :invalid
|
|
456
567
|
on_error "no HTTP response"
|
|
457
568
|
return false
|
|
458
569
|
end
|
|
459
570
|
|
|
460
|
-
if @state == :
|
|
571
|
+
if @state == :connect_http_proxy
|
|
461
572
|
# when a successfull tunnel is established, the proxy responds with a
|
|
462
573
|
# 200 response code. from here, the tunnel is transparent.
|
|
463
574
|
if @response_header.http_status.to_i == 200
|
|
@@ -475,9 +586,13 @@ module EventMachine
|
|
|
475
586
|
if @response_header.location
|
|
476
587
|
begin
|
|
477
588
|
location = Addressable::URI.parse(@response_header.location)
|
|
589
|
+
|
|
478
590
|
if location.relative?
|
|
479
591
|
location = @uri.join(location)
|
|
480
592
|
@response_header[LOCATION] = location.to_s
|
|
593
|
+
else
|
|
594
|
+
# if redirect is to an absolute url, check for correct URI structure
|
|
595
|
+
raise if location.host.nil?
|
|
481
596
|
end
|
|
482
597
|
|
|
483
598
|
# store last url on any sign of redirect
|
|
@@ -489,10 +604,13 @@ module EventMachine
|
|
|
489
604
|
end
|
|
490
605
|
end
|
|
491
606
|
|
|
492
|
-
#
|
|
607
|
+
# Fire callbacks immediately after recieving header requests
|
|
608
|
+
# if the request method is HEAD. In case of a redirect, terminate
|
|
609
|
+
# current connection and reinitialize the process.
|
|
493
610
|
if @method == "HEAD"
|
|
494
611
|
@state = :finished
|
|
495
|
-
|
|
612
|
+
close_connection
|
|
613
|
+
return false
|
|
496
614
|
end
|
|
497
615
|
|
|
498
616
|
if websocket?
|
|
@@ -524,6 +642,114 @@ module EventMachine
|
|
|
524
642
|
true
|
|
525
643
|
end
|
|
526
644
|
|
|
645
|
+
def send_socks_connect_request
|
|
646
|
+
# TO-DO: Implement address types for IPv6 and Domain
|
|
647
|
+
begin
|
|
648
|
+
ip_address = Socket.gethostbyname(@uri.host).last
|
|
649
|
+
send_data [5, 1, 0, 1, ip_address, @uri.port].flatten.pack('CCCCA4n')
|
|
650
|
+
|
|
651
|
+
rescue
|
|
652
|
+
@state = :invalid
|
|
653
|
+
on_error "could not resolve host", true
|
|
654
|
+
return false
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
true
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
# parses socks 5 server responses as specified
|
|
661
|
+
# on http://www.faqs.org/rfcs/rfc1928.html
|
|
662
|
+
def parse_socks_response
|
|
663
|
+
if @socks_state == :method_negotiation
|
|
664
|
+
return false unless has_bytes? 2
|
|
665
|
+
|
|
666
|
+
_, method = @data.read(2).unpack('CC')
|
|
667
|
+
|
|
668
|
+
if socks_methods.include?(method)
|
|
669
|
+
if method == 0
|
|
670
|
+
@socks_state = :connecting
|
|
671
|
+
|
|
672
|
+
return send_socks_connect_request
|
|
673
|
+
|
|
674
|
+
elsif method == 2
|
|
675
|
+
@socks_state = :authenticating
|
|
676
|
+
|
|
677
|
+
credentials = @options[:proxy][:authorization]
|
|
678
|
+
if credentials.size < 2
|
|
679
|
+
@state = :invalid
|
|
680
|
+
on_error "username and password are not supplied"
|
|
681
|
+
return false
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
username, password = credentials
|
|
685
|
+
|
|
686
|
+
send_data [5, username.length, username, password.length, password].pack('CCA*CA*')
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
else
|
|
690
|
+
@state = :invalid
|
|
691
|
+
on_error "proxy did not accept method"
|
|
692
|
+
return false
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
elsif @socks_state == :authenticating
|
|
696
|
+
return false unless has_bytes? 2
|
|
697
|
+
|
|
698
|
+
_, status_code = @data.read(2).unpack('CC')
|
|
699
|
+
|
|
700
|
+
if status_code == 0
|
|
701
|
+
# success
|
|
702
|
+
@socks_state = :connecting
|
|
703
|
+
|
|
704
|
+
return send_socks_connect_request
|
|
705
|
+
|
|
706
|
+
else
|
|
707
|
+
# error
|
|
708
|
+
@state = :invalid
|
|
709
|
+
on_error "access denied by proxy"
|
|
710
|
+
return false
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
elsif @socks_state == :connecting
|
|
714
|
+
return false unless has_bytes? 10
|
|
715
|
+
|
|
716
|
+
_, response_code, _, address_type, _, _ = @data.read(10).unpack('CCCCNn')
|
|
717
|
+
|
|
718
|
+
if response_code == 0
|
|
719
|
+
# success
|
|
720
|
+
@socks_state = :connected
|
|
721
|
+
@state = :proxy_connected
|
|
722
|
+
|
|
723
|
+
@response_header = HttpResponseHeader.new
|
|
724
|
+
|
|
725
|
+
# connection_completed will invoke actions to
|
|
726
|
+
# start sending all http data transparently
|
|
727
|
+
# over the socks connection
|
|
728
|
+
connection_completed
|
|
729
|
+
|
|
730
|
+
else
|
|
731
|
+
# error
|
|
732
|
+
@state = :invalid
|
|
733
|
+
|
|
734
|
+
error_messages = {
|
|
735
|
+
1 => "general socks server failure",
|
|
736
|
+
2 => "connection not allowed by ruleset",
|
|
737
|
+
3 => "network unreachable",
|
|
738
|
+
4 => "host unreachable",
|
|
739
|
+
5 => "connection refused",
|
|
740
|
+
6 => "TTL expired",
|
|
741
|
+
7 => "command not supported",
|
|
742
|
+
8 => "address type not supported"
|
|
743
|
+
}
|
|
744
|
+
error_message = error_messages[response_code] || "unknown error (code: #{response_code})"
|
|
745
|
+
on_error "socks5 connect error: #{error_message}"
|
|
746
|
+
return false
|
|
747
|
+
end
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
true
|
|
751
|
+
end
|
|
752
|
+
|
|
527
753
|
def parse_chunk_header
|
|
528
754
|
return false unless parse_header(@chunk_header)
|
|
529
755
|
|
|
@@ -628,8 +854,8 @@ module EventMachine
|
|
|
628
854
|
# slice the message out of the buffer and pass in
|
|
629
855
|
# for processing, and buffer data otherwise
|
|
630
856
|
buffer = @data.read
|
|
631
|
-
while msg = buffer.slice!(/\000([^\377]*)\377/)
|
|
632
|
-
msg.gsub!(
|
|
857
|
+
while msg = buffer.slice!(/\000([^\377]*)\377/n)
|
|
858
|
+
msg.gsub!(/\A\x00|\xff\z/n, '')
|
|
633
859
|
@stream.call(msg)
|
|
634
860
|
end
|
|
635
861
|
|
data/lib/em-http/http_options.rb
CHANGED
|
@@ -2,7 +2,7 @@ class HttpOptions
|
|
|
2
2
|
attr_reader :uri, :method, :host, :port, :options
|
|
3
3
|
|
|
4
4
|
def initialize(method, uri, options)
|
|
5
|
-
|
|
5
|
+
uri.normalize!
|
|
6
6
|
|
|
7
7
|
@options = options
|
|
8
8
|
@method = method.to_s.upcase
|
|
@@ -12,7 +12,9 @@ class HttpOptions
|
|
|
12
12
|
@host = proxy[:host]
|
|
13
13
|
@port = proxy[:port]
|
|
14
14
|
else
|
|
15
|
-
|
|
15
|
+
# optional host for cases where you may have
|
|
16
|
+
# pre-resolved the host, or you need an override
|
|
17
|
+
@host = options.delete(:host) || uri.host
|
|
16
18
|
@port = uri.port
|
|
17
19
|
end
|
|
18
20
|
|
data/lib/em-http/mock.rb
CHANGED
|
@@ -1,91 +1,131 @@
|
|
|
1
1
|
module EventMachine
|
|
2
|
+
OriginalHttpRequest = HttpRequest unless const_defined?(:OriginalHttpRequest)
|
|
3
|
+
|
|
2
4
|
class MockHttpRequest < EventMachine::HttpRequest
|
|
3
|
-
|
|
5
|
+
|
|
4
6
|
include HttpEncoding
|
|
5
|
-
|
|
6
|
-
class FakeHttpClient < EventMachine::HttpClient
|
|
7
7
|
|
|
8
|
+
class RegisteredRequest < Struct.new(:uri, :method, :headers)
|
|
9
|
+
def self.build(uri, method, headers)
|
|
10
|
+
new(uri, method.to_s.upcase, headers || {})
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class FakeHttpClient < EventMachine::HttpClient
|
|
15
|
+
attr_writer :response
|
|
16
|
+
attr_reader :data
|
|
8
17
|
def setup(response, uri)
|
|
9
18
|
@uri = uri
|
|
10
19
|
if response == :fail
|
|
11
20
|
fail(self)
|
|
12
21
|
else
|
|
13
|
-
|
|
14
|
-
|
|
22
|
+
if response.respond_to?(:call)
|
|
23
|
+
response.call(self)
|
|
24
|
+
@state = :body
|
|
25
|
+
else
|
|
26
|
+
receive_data(response)
|
|
27
|
+
end
|
|
28
|
+
@state == :body ? succeed(self) : fail(self)
|
|
15
29
|
end
|
|
16
30
|
end
|
|
17
|
-
|
|
31
|
+
|
|
18
32
|
def unbind
|
|
19
33
|
end
|
|
20
|
-
|
|
21
34
|
end
|
|
22
|
-
|
|
23
|
-
@@registry =
|
|
24
|
-
@@registry_count =
|
|
25
|
-
|
|
35
|
+
|
|
36
|
+
@@registry = Hash.new
|
|
37
|
+
@@registry_count = Hash.new{|h,k| h[k] = 0}
|
|
38
|
+
|
|
39
|
+
def self.use
|
|
40
|
+
activate!
|
|
41
|
+
yield
|
|
42
|
+
ensure
|
|
43
|
+
deactivate!
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.activate!
|
|
47
|
+
EventMachine.send(:remove_const, :HttpRequest)
|
|
48
|
+
EventMachine.send(:const_set, :HttpRequest, MockHttpRequest)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.deactivate!
|
|
52
|
+
EventMachine.send(:remove_const, :HttpRequest)
|
|
53
|
+
EventMachine.send(:const_set, :HttpRequest, OriginalHttpRequest)
|
|
54
|
+
end
|
|
55
|
+
|
|
26
56
|
def self.reset_counts!
|
|
27
|
-
@@registry_count
|
|
28
|
-
registry[query] = Hash.new{|h,k| h[k] = Hash.new(0)}
|
|
29
|
-
end
|
|
57
|
+
@@registry_count.clear
|
|
30
58
|
end
|
|
31
|
-
|
|
59
|
+
|
|
32
60
|
def self.reset_registry!
|
|
33
|
-
@@registry
|
|
34
|
-
registry[query] = Hash.new{|h,k| h[k] = {}}
|
|
35
|
-
end
|
|
61
|
+
@@registry.clear
|
|
36
62
|
end
|
|
37
|
-
|
|
38
|
-
reset_counts!
|
|
39
|
-
reset_registry!
|
|
40
|
-
|
|
63
|
+
|
|
41
64
|
@@pass_through_requests = true
|
|
42
65
|
|
|
43
66
|
def self.pass_through_requests=(pass_through_requests)
|
|
44
67
|
@@pass_through_requests = pass_through_requests
|
|
45
68
|
end
|
|
46
|
-
|
|
69
|
+
|
|
47
70
|
def self.pass_through_requests
|
|
48
71
|
@@pass_through_requests
|
|
49
72
|
end
|
|
50
|
-
|
|
51
|
-
def self.
|
|
52
|
-
|
|
53
|
-
headers =
|
|
54
|
-
|
|
73
|
+
|
|
74
|
+
def self.parse_register_args(args, &proc)
|
|
75
|
+
args << proc{|client| proc.call(client); ''} if proc
|
|
76
|
+
headers, data = case args.size
|
|
77
|
+
when 3
|
|
78
|
+
args[2].is_a?(Hash) ?
|
|
79
|
+
[args[2][:headers], args[2][:data]] :
|
|
80
|
+
[{}, args[2]]
|
|
81
|
+
when 4
|
|
82
|
+
[args[2], args[3]]
|
|
83
|
+
else
|
|
84
|
+
raise
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
url = args[0]
|
|
88
|
+
method = args[1]
|
|
89
|
+
[headers, url, method, data]
|
|
55
90
|
end
|
|
56
|
-
|
|
57
|
-
def self.
|
|
58
|
-
|
|
91
|
+
|
|
92
|
+
def self.register(*args, &proc)
|
|
93
|
+
headers, url, method, data = parse_register_args(args, &proc)
|
|
94
|
+
@@registry[RegisteredRequest.build(url, method, headers)] = data
|
|
59
95
|
end
|
|
60
|
-
|
|
61
|
-
def self.
|
|
62
|
-
method =
|
|
63
|
-
headers =
|
|
64
|
-
|
|
96
|
+
|
|
97
|
+
def self.register_file(*args)
|
|
98
|
+
headers, url, method, data = parse_register_args(args)
|
|
99
|
+
@@registry[RegisteredRequest.build(url, method, headers)] = File.read(data)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def self.count(url, method, headers = {})
|
|
103
|
+
@@registry_count[RegisteredRequest.build(url, method, headers)]
|
|
65
104
|
end
|
|
66
|
-
|
|
67
|
-
def self.registered?(
|
|
68
|
-
@@registry
|
|
105
|
+
|
|
106
|
+
def self.registered?(url, method, headers = {})
|
|
107
|
+
@@registry.key?(RegisteredRequest.build(url, method, headers))
|
|
69
108
|
end
|
|
70
|
-
|
|
71
|
-
def self.registered_content(
|
|
72
|
-
@@registry[
|
|
109
|
+
|
|
110
|
+
def self.registered_content(url, method, headers = {})
|
|
111
|
+
@@registry[RegisteredRequest.build(url, method, headers)]
|
|
73
112
|
end
|
|
74
|
-
|
|
75
|
-
def self.increment_access(
|
|
76
|
-
@@registry_count[
|
|
113
|
+
|
|
114
|
+
def self.increment_access(url, method, headers = {})
|
|
115
|
+
@@registry_count[RegisteredRequest.build(url, method, headers)] += 1
|
|
77
116
|
end
|
|
78
|
-
|
|
117
|
+
|
|
79
118
|
alias_method :real_send_request, :send_request
|
|
80
|
-
|
|
119
|
+
|
|
81
120
|
protected
|
|
82
121
|
def send_request(&blk)
|
|
83
|
-
query = "#{@req.uri.scheme}://#{@req.uri.host}:#{@req.uri.port}#{encode_query(@req.uri
|
|
84
|
-
headers = @req.options[:head]
|
|
122
|
+
query = "#{@req.uri.scheme}://#{@req.uri.host}:#{@req.uri.port}#{encode_query(@req.uri, @req.options[:query])}"
|
|
123
|
+
headers = @req.options[:head]
|
|
85
124
|
if self.class.registered?(query, @req.method, headers)
|
|
86
125
|
self.class.increment_access(query, @req.method, headers)
|
|
87
126
|
client = FakeHttpClient.new(nil)
|
|
88
|
-
|
|
127
|
+
content = self.class.registered_content(query, @req.method, headers)
|
|
128
|
+
client.setup(content, @req.uri)
|
|
89
129
|
client
|
|
90
130
|
elsif @@pass_through_requests
|
|
91
131
|
real_send_request
|