em-http-request 0.3.0 → 1.0.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.

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