em-http-request 0.3.0 → 1.0.0.beta.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.

Potentially problematic release.


This version of em-http-request might be problematic. Click here for more details.

Files changed (44) hide show
  1. data/.gitignore +1 -0
  2. data/Changelog.md +10 -0
  3. data/README.md +43 -160
  4. data/Rakefile +2 -73
  5. data/em-http-request.gemspec +7 -7
  6. data/examples/fetch.rb +30 -30
  7. data/examples/fibered-http.rb +38 -38
  8. data/examples/oauth-tweet.rb +49 -49
  9. data/lib/em-http.rb +4 -6
  10. data/lib/em-http/client.rb +101 -522
  11. data/lib/em-http/http_connection.rb +125 -0
  12. data/lib/em-http/http_encoding.rb +19 -12
  13. data/lib/em-http/http_header.rb +2 -17
  14. data/lib/em-http/http_options.rb +37 -19
  15. data/lib/em-http/request.rb +33 -66
  16. data/lib/em-http/version.rb +2 -2
  17. data/spec/client_spec.rb +575 -0
  18. data/spec/dns_spec.rb +41 -0
  19. data/spec/encoding_spec.rb +6 -6
  20. data/spec/external_spec.rb +99 -0
  21. data/spec/fixtures/google.ca +13 -17
  22. data/spec/helper.rb +17 -8
  23. data/spec/http_proxy_spec.rb +53 -0
  24. data/spec/middleware_spec.rb +114 -0
  25. data/spec/multi_spec.rb +11 -38
  26. data/spec/pipelining_spec.rb +38 -0
  27. data/spec/redirect_spec.rb +114 -0
  28. data/spec/socksify_proxy_spec.rb +24 -0
  29. data/spec/ssl_spec.rb +20 -0
  30. data/spec/stallion.rb +7 -63
  31. metadata +59 -39
  32. data/examples/websocket-handler.rb +0 -28
  33. data/examples/websocket-server.rb +0 -8
  34. data/ext/buffer/em_buffer.c +0 -639
  35. data/ext/buffer/extconf.rb +0 -53
  36. data/ext/http11_client/ext_help.h +0 -14
  37. data/ext/http11_client/extconf.rb +0 -6
  38. data/ext/http11_client/http11_client.c +0 -328
  39. data/ext/http11_client/http11_parser.c +0 -418
  40. data/ext/http11_client/http11_parser.h +0 -48
  41. data/ext/http11_client/http11_parser.rl +0 -170
  42. data/lib/em-http/mock.rb +0 -137
  43. data/spec/mock_spec.rb +0 -166
  44. data/spec/request_spec.rb +0 -1003
@@ -1,50 +1,50 @@
1
- # Courtesy of Darcy Laycock:
2
- # http://gist.github.com/265261
3
- #
4
-
5
- require 'rubygems'
6
-
7
- require 'em-http'
8
- require 'oauth'
9
-
10
- # At a minimum, require 'oauth/request_proxy/em_http_request'
11
- # for this example, we'll use Net::HTTP like support.
12
- require 'oauth/client/em_http'
13
-
14
- # You need two things: an oauth consumer and an access token.
15
- # You need to generate an access token, I suggest looking elsewhere how to do that or wait for a full tutorial.
16
- # For a consumer key / consumer secret, signup for an app at:
17
- # http://twitter.com/apps/new
18
-
19
- # Edit in your details.
20
- CONSUMER_KEY = ""
21
- CONSUMER_SECRET = ""
22
- ACCESS_TOKEN = ""
23
- ACCESS_TOKEN_SECRET = ""
24
-
25
- def twitter_oauth_consumer
26
- @twitter_oauth_consumer ||= OAuth::Consumer.new(CONSUMER_KEY, CONSUMER_SECRET, :site => "http://twitter.com")
27
- end
28
-
29
- def twitter_oauth_access_token
30
- @twitter_oauth_access_token ||= OAuth::AccessToken.new(twitter_oauth_consumer, ACCESS_TOKEN, ACCESS_TOKEN_SECRET)
31
- end
32
-
33
- EM.run do
34
-
35
- request = EventMachine::HttpRequest.new('http://twitter.com/statuses/update.json')
36
- http = request.post(:body => {'status' => 'Hello Twitter from em-http-request with OAuth'}, :head => {"Content-Type" => "application/x-www-form-urlencoded"}) do |client|
37
- twitter_oauth_consumer.sign!(client, twitter_oauth_access_token)
38
- end
39
-
40
- http.callback do
41
- puts "Response: #{http.response} (Code: #{http.response_header.status})"
42
- EM.stop_event_loop
43
- end
44
-
45
- http.errback do
46
- puts "Failed to post"
47
- EM.stop_event_loop
48
- end
49
-
1
+ # Courtesy of Darcy Laycock:
2
+ # http://gist.github.com/265261
3
+ #
4
+
5
+ require 'rubygems'
6
+
7
+ require 'em-http'
8
+ require 'oauth'
9
+
10
+ # At a minimum, require 'oauth/request_proxy/em_http_request'
11
+ # for this example, we'll use Net::HTTP like support.
12
+ require 'oauth/client/em_http'
13
+
14
+ # You need two things: an oauth consumer and an access token.
15
+ # You need to generate an access token, I suggest looking elsewhere how to do that or wait for a full tutorial.
16
+ # For a consumer key / consumer secret, signup for an app at:
17
+ # http://twitter.com/apps/new
18
+
19
+ # Edit in your details.
20
+ CONSUMER_KEY = ""
21
+ CONSUMER_SECRET = ""
22
+ ACCESS_TOKEN = ""
23
+ ACCESS_TOKEN_SECRET = ""
24
+
25
+ def twitter_oauth_consumer
26
+ @twitter_oauth_consumer ||= OAuth::Consumer.new(CONSUMER_KEY, CONSUMER_SECRET, :site => "http://twitter.com")
27
+ end
28
+
29
+ def twitter_oauth_access_token
30
+ @twitter_oauth_access_token ||= OAuth::AccessToken.new(twitter_oauth_consumer, ACCESS_TOKEN, ACCESS_TOKEN_SECRET)
31
+ end
32
+
33
+ EM.run do
34
+
35
+ request = EventMachine::HttpRequest.new('http://twitter.com/statuses/update.json')
36
+ http = request.post(:body => {'status' => 'Hello Twitter from em-http-request with OAuth'}, :head => {"Content-Type" => "application/x-www-form-urlencoded"}) do |client|
37
+ twitter_oauth_consumer.sign!(client, twitter_oauth_access_token)
38
+ end
39
+
40
+ http.callback do
41
+ puts "Response: #{http.response} (Code: #{http.response_header.status})"
42
+ EM.stop_event_loop
43
+ end
44
+
45
+ http.errback do
46
+ puts "Failed to post"
47
+ EM.stop_event_loop
48
+ end
49
+
50
50
  end
data/lib/em-http.rb CHANGED
@@ -1,19 +1,17 @@
1
1
  require 'eventmachine'
2
- require 'escape_utils'
2
+ require 'em-socksify'
3
3
  require 'addressable/uri'
4
+ require 'http/parser'
4
5
 
5
6
  require 'base64'
6
7
  require 'socket'
7
8
 
8
- require 'http11_client'
9
- require 'em_buffer'
10
-
11
9
  require 'em-http/core_ext/bytesize'
10
+ require 'em-http/http_connection'
12
11
  require 'em-http/http_header'
13
12
  require 'em-http/http_encoding'
14
13
  require 'em-http/http_options'
15
14
  require 'em-http/client'
16
15
  require 'em-http/multi'
17
16
  require 'em-http/request'
18
- require 'em-http/decoders'
19
- require 'em-http/mock'
17
+ require 'em-http/decoders'
@@ -1,17 +1,7 @@
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
1
  module EventMachine
11
-
12
- class HttpClient < Connection
13
- include EventMachine::Deferrable
14
- include EventMachine::HttpEncoding
2
+ class HttpClient
3
+ include Deferrable
4
+ include HttpEncoding
15
5
 
16
6
  TRANSFER_ENCODING="TRANSFER_ENCODING"
17
7
  CONTENT_ENCODING="CONTENT_ENCODING"
@@ -26,57 +16,46 @@ module EventMachine
26
16
 
27
17
  CRLF="\r\n"
28
18
 
29
- attr_accessor :method, :options, :uri
30
- attr_reader :response, :response_header, :error, :redirects, :last_effective_url, :content_charset
19
+ attr_accessor :state, :response
20
+ attr_reader :response_header, :error, :content_charset, :req
21
+
22
+ def initialize(conn, req, options)
23
+ @conn = conn
24
+
25
+ @req = req
26
+ @method = req.method
27
+ @options = options
28
+
29
+ @stream = nil
30
+ @headers = nil
31
+
32
+ reset!
33
+ end
31
34
 
32
- def post_init
33
- @parser = HttpClientParser.new
34
- @data = EventMachine::Buffer.new
35
- @chunk_header = HttpChunkHeader.new
35
+ def reset!
36
36
  @response_header = HttpResponseHeader.new
37
- @parser_nbytes = 0
38
- @redirects = 0
37
+ @state = :response_header
38
+
39
39
  @response = ''
40
40
  @error = ''
41
- @headers = nil
42
- @last_effective_url = nil
43
41
  @content_decoder = nil
44
42
  @content_charset = nil
45
- @stream = nil
46
- @disconnect = nil
47
- @state = :response_header
48
- @socks_state = nil
49
43
  end
50
44
 
51
- # start HTTP request once we establish connection to host
45
+ def last_effective_url; @req.uri; end
46
+ def redirects; @req.options[:followed]; end
47
+
52
48
  def connection_completed
53
- # if a socks proxy is specified, then a connection request
54
- # has to be made to the socks server and we need to wait
55
- # for a response code
56
- if socks_proxy? and @state == :response_header
57
- @state = :connect_socks_proxy
58
- send_socks_handshake
59
-
60
- # if we need to negotiate the proxy connection first, then
61
- # issue a CONNECT query and wait for 200 response
62
- elsif connect_proxy? and @state == :response_header
63
- @state = :connect_http_proxy
64
- send_request_header
65
-
66
- # if connecting via proxy, then state will be :proxy_connected,
67
- # indicating successful tunnel. from here, initiate normal http
68
- # exchange
49
+ @state = :response_header
69
50
 
70
- else
71
- @state = :response_header
72
- ssl = @options[:tls] || @options[:ssl] || {}
73
- start_tls(ssl) if @uri.scheme == "https" or @uri.port == 443
74
- send_request_header
75
- send_request_body
51
+ head, body = build_request, @options[:body]
52
+ @conn.middleware.each do |m|
53
+ head, body = m.request(head, body) if m.respond_to?(:request)
76
54
  end
55
+
56
+ send_request(head, body)
77
57
  end
78
58
 
79
- # request is done, invoke the callback
80
59
  def on_request_complete
81
60
  begin
82
61
  @content_decoder.finalize! if @content_decoder
@@ -84,145 +63,70 @@ module EventMachine
84
63
  on_error "Content-decoder error"
85
64
  end
86
65
 
87
- close_connection
66
+ unbind
88
67
  end
89
68
 
90
- # request failed, invoke errback
91
- def on_error(msg, dns_error = false)
92
- @error = msg
93
-
94
- # no connection signature on DNS failures
95
- # fail the connection directly
96
- dns_error == true ? fail(self) : unbind
97
- end
98
- alias :close :on_error
99
-
100
- # assign a stream processing block
101
- def stream(&blk)
102
- @stream = blk
69
+ def finished?
70
+ @state == :finished || (@state == :body && @response_header.content_length.nil?)
103
71
  end
104
72
 
105
- # assign disconnect callback for websocket
106
- def disconnect(&blk)
107
- @disconnect = blk
73
+ def redirect?
74
+ @response_header.location && @req.follow_redirect?
108
75
  end
109
76
 
110
- # assign a headers parse callback
111
- def headers(&blk)
112
- @headers = blk
113
- end
77
+ def unbind
78
+ if finished?
79
+ if redirect?
80
+ @req.options[:followed] += 1
81
+ @conn.redirect(self, @response_header.location)
82
+ else
83
+ succeed(self)
84
+ end
114
85
 
115
- # raw data push from the client (WebSocket) should
116
- # only be invoked after handshake, otherwise it will
117
- # inject data into the header exchange
118
- #
119
- # frames need to start with 0x00-0x7f byte and end with
120
- # an 0xFF byte. Per spec, we can also set the first
121
- # byte to a value betweent 0x80 and 0xFF, followed by
122
- # a leading length indicator
123
- def send(data)
124
- if @state == :websocket
125
- send_data("\x00#{data}\xff")
86
+ else
87
+ fail(self)
126
88
  end
127
89
  end
128
90
 
129
- def normalize_body
130
- @normalized_body ||= begin
131
- if @options[:body].is_a? Hash
132
- form_encode_body(@options[:body])
133
- else
134
- @options[:body]
135
- end
136
- end
91
+ def on_error(msg = '')
92
+ @error = msg
93
+ fail(self)
137
94
  end
95
+ alias :close :on_error
96
+
97
+ def stream(&blk); @stream = blk; end
98
+ def headers(&blk); @headers = blk; end
138
99
 
139
- # determines if there is enough data in the buffer
140
- def has_bytes?(num)
141
- @data.size >= num
100
+ def normalize_body(body)
101
+ body.is_a?(Hash) ? form_encode_body(body) : body
142
102
  end
143
103
 
144
- def websocket?; @uri.scheme == 'ws'; end
145
104
  def proxy?; !@options[:proxy].nil?; end
146
-
147
- # determines if a proxy should be used that uses
148
- # http-headers as proxy-mechanism
149
- #
150
- # this is the default proxy type if none is specified
151
105
  def http_proxy?; proxy? && [nil, :http].include?(@options[:proxy][:type]); end
152
-
153
- # determines if a http-proxy should be used with
154
- # the CONNECT verb
155
- def connect_proxy?; http_proxy? && (@options[:proxy][:use_connect] == true); end
156
-
157
- # determines if a SOCKS5 proxy should be used
158
106
  def socks_proxy?; proxy? && (@options[:proxy][:type] == :socks); end
159
107
 
160
- def socks_methods
161
- methods = []
162
- methods << 2 if !options[:proxy][:authorization].nil? # 2 => Username/Password Authentication
163
- methods << 0 # 0 => No Authentication Required
164
-
165
- methods
166
- end
167
-
168
- def send_socks_handshake
169
- # Method Negotiation as described on
170
- # http://www.faqs.org/rfcs/rfc1928.html Section 3
171
-
172
- @socks_state = :method_negotiation
173
-
174
- methods = socks_methods
175
- send_data [5, methods.size].pack('CC') + methods.pack('C*')
108
+ def continue?
109
+ @response_header.status == 100 && (@method == 'POST' || @method == 'PUT')
176
110
  end
177
111
 
178
- def send_request_header
179
- query = @options[:query]
112
+ def build_request
180
113
  head = @options[:head] ? munge_header_keys(@options[:head]) : {}
181
- file = @options[:file]
182
114
  proxy = @options[:proxy]
183
- body = normalize_body
184
-
185
- request_header = nil
186
115
 
187
116
  if http_proxy?
188
117
  # initialize headers for the http proxy
189
118
  head = proxy[:head] ? munge_header_keys(proxy[:head]) : {}
190
119
  head['proxy-authorization'] = proxy[:authorization] if proxy[:authorization]
191
-
192
- # if we need to negotiate the tunnel connection first, then
193
- # issue a CONNECT query to the proxy first. This is an optional
194
- # flag, by default we will provide full URIs to the proxy
195
- if @state == :connect_http_proxy
196
- request_header = HTTP_REQUEST_HEADER % ['CONNECT', "#{@uri.host}:#{@uri.port}"]
197
- end
198
120
  end
199
121
 
200
- if websocket?
201
- head['upgrade'] = 'WebSocket'
202
- head['connection'] = 'Upgrade'
203
- head['origin'] = @options[:origin] || @uri.host
204
-
205
- else
206
- # Set the Content-Length if file is given
207
- head['content-length'] = File.size(file) if file
208
-
209
- # Set the Content-Length if body is given
210
- head['content-length'] = body.bytesize if body
211
-
212
- # Set the cookie header if provided
213
- if cookie = head.delete('cookie')
214
- head['cookie'] = encode_cookie(cookie)
215
- end
216
-
217
- # Set content-type header if missing and body is a Ruby hash
218
- if not head['content-type'] and options[:body].is_a? Hash
219
- head['content-type'] = 'application/x-www-form-urlencoded'
220
- end
122
+ # Set the cookie header if provided
123
+ if cookie = head.delete('cookie')
124
+ head['cookie'] = encode_cookie(cookie)
125
+ end
221
126
 
222
- # Set connection close unless keepalive
223
- unless options[:keepalive]
224
- head['connection'] = 'close'
225
- end
127
+ # Set connection close unless keepalive
128
+ unless @options[:keepalive]
129
+ head['connection'] = 'close'
226
130
  end
227
131
 
228
132
  # Set the Host header if it hasn't been specified already
@@ -231,32 +135,37 @@ module EventMachine
231
135
  # Set the User-Agent if it hasn't been specified
232
136
  head['user-agent'] ||= "EventMachine HttpClient"
233
137
 
234
- # Record last seen URL
235
- @last_effective_url = @uri
138
+ head
139
+ end
140
+
141
+ def send_request(head, body)
142
+ body = normalize_body(body)
143
+ file = @options[:file]
144
+ query = @options[:query]
145
+
146
+ # Set the Content-Length if file is given
147
+ head['content-length'] = File.size(file) if file
148
+
149
+ # Set the Content-Length if body is given
150
+ head['content-length'] = body.bytesize if body
151
+
152
+ # Set content-type header if missing and body is a Ruby hash
153
+ if not head['content-type'] and @options[:body].is_a? Hash
154
+ head['content-type'] = 'application/x-www-form-urlencoded'
155
+ end
236
156
 
237
- # Build the request headers
238
- request_header ||= encode_request(@method, @uri, query, proxy)
157
+ request_header ||= encode_request(@method, @req.uri, query, @conn.opts.proxy)
239
158
  request_header << encode_headers(head)
240
159
  request_header << CRLF
241
- send_data request_header
242
- end
160
+ @conn.send_data request_header
243
161
 
244
- def send_request_body
245
- if @options[:body]
246
- body = normalize_body
247
- send_data body
248
- return
162
+ if body
163
+ @conn.send_data body
249
164
  elsif @options[:file]
250
- stream_file_data @options[:file], :http_chunks => false
165
+ @conn.stream_file_data @options[:file], :http_chunks => false
251
166
  end
252
167
  end
253
168
 
254
- def receive_data(data)
255
- @data << data
256
- dispatch
257
- end
258
-
259
- # Called when part of the body has been read
260
169
  def on_body_data(data)
261
170
  if @content_decoder
262
171
  begin
@@ -278,116 +187,23 @@ module EventMachine
278
187
  end
279
188
  end
280
189
 
281
- def finished?
282
- @state == :finished || (@state == :body && @bytes_remaining.nil?)
283
- end
284
-
285
- def unbind
286
- if finished? && (@last_effective_url != @uri) && (@redirects < @options[:redirects])
287
- begin
288
- # update uri to redirect location if we're allowed to traverse deeper
289
- @uri = @last_effective_url
290
-
291
- # keep track of the depth of requests we made in this session
292
- @redirects += 1
293
-
294
- # swap current connection and reassign current handler
295
- req = HttpOptions.new(@method, @uri, @options)
296
- reconnect(req.host, req.port)
297
-
298
- @response_header = HttpResponseHeader.new
299
- @state = :response_header
300
- @response = ''
301
- @data.clear
302
- rescue EventMachine::ConnectionError => e
303
- on_error(e.message, true)
304
- end
305
- else
306
- if finished?
307
- succeed(self)
308
- else
309
- @disconnect.call(self) if @state == :websocket and @disconnect
310
- fail(self)
311
- end
312
- end
313
- end
314
-
315
- #
316
- # Response processing
317
- #
318
-
319
- def dispatch
320
- while case @state
321
- when :connect_socks_proxy
322
- parse_socks_response
323
- when :connect_http_proxy
324
- parse_response_header
325
- when :response_header
326
- parse_response_header
327
- when :chunk_header
328
- parse_chunk_header
329
- when :chunk_body
330
- process_chunk_body
331
- when :chunk_footer
332
- process_chunk_footer
333
- when :response_footer
334
- process_response_footer
335
- when :body
336
- process_body
337
- when :websocket
338
- process_websocket
339
- when :finished, :invalid
340
- break
341
- else raise RuntimeError, "invalid state: #{@state}"
342
- end
343
- end
344
- end
345
-
346
- def parse_header(header)
347
- return false if @data.empty?
348
-
349
- begin
350
- @parser_nbytes = @parser.execute(header, @data.to_str, @parser_nbytes)
351
- rescue EventMachine::HttpClientParserError
352
- @state = :invalid
353
- on_error "invalid HTTP format, parsing fails"
190
+ def parse_response_header(header, version, status)
191
+ header.each do |key, val|
192
+ @response_header[key.upcase.gsub('-','_')] = val
354
193
  end
355
194
 
356
- return false unless @parser.finished?
195
+ @response_header.http_version = version.join('.')
196
+ @response_header.http_status = status
197
+ @response_header.http_reason = 'unknown'
357
198
 
358
- # Clear parsed data from the buffer
359
- @data.read(@parser_nbytes)
360
- @parser.reset
361
- @parser_nbytes = 0
362
-
363
- true
364
- end
365
-
366
- def parse_response_header
367
- return false unless parse_header(@response_header)
368
-
369
- # invoke headers callback after full parse if one
370
- # is specified by the user
199
+ # invoke headers callback after full parse
200
+ # if one is specified by the user
371
201
  @headers.call(@response_header) if @headers
372
202
 
373
203
  unless @response_header.http_status and @response_header.http_reason
374
204
  @state = :invalid
375
205
  on_error "no HTTP response"
376
- return false
377
- end
378
-
379
- if @state == :connect_http_proxy
380
- # when a successfull tunnel is established, the proxy responds with a
381
- # 200 response code. from here, the tunnel is transparent.
382
- if @response_header.http_status.to_i == 200
383
- @response_header = HttpResponseHeader.new
384
- connection_completed
385
- return true
386
- else
387
- @state = :invalid
388
- on_error "proxy not accessible"
389
- return false
390
- end
206
+ return
391
207
  end
392
208
 
393
209
  # correct location header - some servers will incorrectly give a relative URI
@@ -396,19 +212,16 @@ module EventMachine
396
212
  location = Addressable::URI.parse(@response_header.location)
397
213
 
398
214
  if location.relative?
399
- location = @uri.join(location)
215
+ location = @req.uri.join(location)
400
216
  @response_header[LOCATION] = location.to_s
401
217
  else
402
218
  # if redirect is to an absolute url, check for correct URI structure
403
219
  raise if location.host.nil?
404
220
  end
405
221
 
406
- # store last url on any sign of redirect
407
- @last_effective_url = location
408
-
409
222
  rescue
410
223
  on_error "Location header format error"
411
- return false
224
+ return
412
225
  end
413
226
  end
414
227
 
@@ -417,26 +230,15 @@ module EventMachine
417
230
  # current connection and reinitialize the process.
418
231
  if @method == "HEAD"
419
232
  @state = :finished
420
- close_connection
421
- return false
233
+ return
422
234
  end
423
235
 
424
- if websocket?
425
- if @response_header.status == 101
426
- @state = :websocket
427
- succeed
428
- else
429
- fail "websocket handshake failed"
430
- end
431
-
432
- elsif @response_header.chunked_encoding?
236
+ if @response_header.chunked_encoding?
433
237
  @state = :chunk_header
434
238
  elsif @response_header.content_length
435
239
  @state = :body
436
- @bytes_remaining = @response_header.content_length
437
240
  else
438
241
  @state = :body
439
- @bytes_remaining = nil
440
242
  end
441
243
 
442
244
  if decoder_class = HttpDecoders.decoder_for_encoding(response_header[CONTENT_ENCODING])
@@ -447,233 +249,10 @@ module EventMachine
447
249
  end
448
250
  end
449
251
 
450
- if ''.respond_to?(:force_encoding) && /;\s*charset=\s*(.+?)\s*(;|$)/.match(response_header[CONTENT_TYPE])
252
+ if String.method_defined?(:force_encoding) && /;\s*charset=\s*(.+?)\s*(;|$)/.match(response_header[CONTENT_TYPE])
451
253
  @content_charset = Encoding.find($1.gsub(/^\"|\"$/, '')) rescue Encoding.default_external
452
254
  end
453
-
454
- true
455
- end
456
-
457
- def send_socks_connect_request
458
- # TO-DO: Implement address types for IPv6 and Domain
459
- begin
460
- ip_address = Socket.gethostbyname(@uri.host).last
461
- send_data [5, 1, 0, 1, ip_address, @uri.port].flatten.pack('CCCCA4n')
462
-
463
- rescue
464
- @state = :invalid
465
- on_error "could not resolve host", true
466
- return false
467
- end
468
-
469
- true
470
- end
471
-
472
- # parses socks 5 server responses as specified
473
- # on http://www.faqs.org/rfcs/rfc1928.html
474
- def parse_socks_response
475
- if @socks_state == :method_negotiation
476
- return false unless has_bytes? 2
477
-
478
- _, method = @data.read(2).unpack('CC')
479
-
480
- if socks_methods.include?(method)
481
- if method == 0
482
- @socks_state = :connecting
483
-
484
- return send_socks_connect_request
485
-
486
- elsif method == 2
487
- @socks_state = :authenticating
488
-
489
- credentials = @options[:proxy][:authorization]
490
- if credentials.size < 2
491
- @state = :invalid
492
- on_error "username and password are not supplied"
493
- return false
494
- end
495
-
496
- username, password = credentials
497
-
498
- send_data [5, username.length, username, password.length, password].pack('CCA*CA*')
499
- end
500
-
501
- else
502
- @state = :invalid
503
- on_error "proxy did not accept method"
504
- return false
505
- end
506
-
507
- elsif @socks_state == :authenticating
508
- return false unless has_bytes? 2
509
-
510
- _, status_code = @data.read(2).unpack('CC')
511
-
512
- if status_code == 0
513
- # success
514
- @socks_state = :connecting
515
-
516
- return send_socks_connect_request
517
-
518
- else
519
- # error
520
- @state = :invalid
521
- on_error "access denied by proxy"
522
- return false
523
- end
524
-
525
- elsif @socks_state == :connecting
526
- return false unless has_bytes? 10
527
-
528
- _, response_code, _, address_type, _, _ = @data.read(10).unpack('CCCCNn')
529
-
530
- if response_code == 0
531
- # success
532
- @socks_state = :connected
533
- @state = :proxy_connected
534
-
535
- @response_header = HttpResponseHeader.new
536
-
537
- # connection_completed will invoke actions to
538
- # start sending all http data transparently
539
- # over the socks connection
540
- connection_completed
541
-
542
- else
543
- # error
544
- @state = :invalid
545
-
546
- error_messages = {
547
- 1 => "general socks server failure",
548
- 2 => "connection not allowed by ruleset",
549
- 3 => "network unreachable",
550
- 4 => "host unreachable",
551
- 5 => "connection refused",
552
- 6 => "TTL expired",
553
- 7 => "command not supported",
554
- 8 => "address type not supported"
555
- }
556
- error_message = error_messages[response_code] || "unknown error (code: #{response_code})"
557
- on_error "socks5 connect error: #{error_message}"
558
- return false
559
- end
560
- end
561
-
562
- true
563
255
  end
564
256
 
565
- def parse_chunk_header
566
- return false unless parse_header(@chunk_header)
567
-
568
- @bytes_remaining = @chunk_header.chunk_size
569
- @chunk_header = HttpChunkHeader.new
570
-
571
- @state = @bytes_remaining > 0 ? :chunk_body : :response_footer
572
- true
573
- end
574
-
575
- def process_chunk_body
576
- if @data.size < @bytes_remaining
577
- @bytes_remaining -= @data.size
578
- on_body_data @data.read
579
- return false
580
- end
581
-
582
- on_body_data @data.read(@bytes_remaining)
583
- @bytes_remaining = 0
584
-
585
- @state = :chunk_footer
586
- true
587
- end
588
-
589
- def process_chunk_footer
590
- return false if @data.size < 2
591
-
592
- if @data.read(2) == CRLF
593
- @state = :chunk_header
594
- else
595
- @state = :invalid
596
- on_error "non-CRLF chunk footer"
597
- end
598
-
599
- true
600
- end
601
-
602
- def process_response_footer
603
- return false if @data.size < 2
604
-
605
- if @data.read(2) == CRLF
606
- if @data.empty?
607
- @state = :finished
608
- on_request_complete
609
- else
610
- @state = :invalid
611
- on_error "garbage at end of chunked response"
612
- end
613
- else
614
- @state = :invalid
615
- on_error "non-CRLF response footer"
616
- end
617
-
618
- false
619
- end
620
-
621
- def process_body
622
- if @bytes_remaining.nil?
623
- on_body_data @data.read
624
- return false
625
- end
626
-
627
- if @bytes_remaining.zero?
628
- @state = :finished
629
- on_request_complete
630
- return false
631
- end
632
-
633
- if @data.size < @bytes_remaining
634
- @bytes_remaining -= @data.size
635
- on_body_data @data.read
636
- return false
637
- end
638
-
639
- on_body_data @data.read(@bytes_remaining)
640
- @bytes_remaining = 0
641
-
642
- # If Keep-Alive is enabled, the server may be pushing more data to us
643
- # after the first request is complete. Hence, finish first request, and
644
- # reset state.
645
- if @response_header.keep_alive?
646
- @data.clear # hard reset, TODO: add support for keep-alive connections!
647
- @state = :finished
648
- on_request_complete
649
-
650
- else
651
-
652
- @data.clear
653
- @state = :finished
654
- on_request_complete
655
- end
656
-
657
- false
658
- end
659
-
660
- def process_websocket
661
- return false if @data.empty?
662
-
663
- # slice the message out of the buffer and pass in
664
- # for processing, and buffer data otherwise
665
- buffer = @data.read
666
- while msg = buffer.slice!(/\000([^\377]*)\377/n)
667
- msg.gsub!(/\A\x00|\xff\z/n, '')
668
- @stream.call(msg)
669
- end
670
-
671
- # store remainder if message boundary has not yet
672
- # been received
673
- @data << buffer if not buffer.empty?
674
-
675
- false
676
- end
677
257
  end
678
-
679
258
  end