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.
- data/.gitignore +1 -0
- data/Changelog.md +10 -0
- data/README.md +43 -160
- data/Rakefile +2 -73
- data/em-http-request.gemspec +7 -7
- data/examples/fetch.rb +30 -30
- data/examples/fibered-http.rb +38 -38
- data/examples/oauth-tweet.rb +49 -49
- data/lib/em-http.rb +4 -6
- data/lib/em-http/client.rb +101 -522
- data/lib/em-http/http_connection.rb +125 -0
- data/lib/em-http/http_encoding.rb +19 -12
- data/lib/em-http/http_header.rb +2 -17
- data/lib/em-http/http_options.rb +37 -19
- data/lib/em-http/request.rb +33 -66
- data/lib/em-http/version.rb +2 -2
- data/spec/client_spec.rb +575 -0
- data/spec/dns_spec.rb +41 -0
- data/spec/encoding_spec.rb +6 -6
- data/spec/external_spec.rb +99 -0
- data/spec/fixtures/google.ca +13 -17
- data/spec/helper.rb +17 -8
- data/spec/http_proxy_spec.rb +53 -0
- data/spec/middleware_spec.rb +114 -0
- data/spec/multi_spec.rb +11 -38
- data/spec/pipelining_spec.rb +38 -0
- data/spec/redirect_spec.rb +114 -0
- data/spec/socksify_proxy_spec.rb +24 -0
- data/spec/ssl_spec.rb +20 -0
- data/spec/stallion.rb +7 -63
- metadata +59 -39
- data/examples/websocket-handler.rb +0 -28
- data/examples/websocket-server.rb +0 -8
- data/ext/buffer/em_buffer.c +0 -639
- data/ext/buffer/extconf.rb +0 -53
- data/ext/http11_client/ext_help.h +0 -14
- data/ext/http11_client/extconf.rb +0 -6
- data/ext/http11_client/http11_client.c +0 -328
- data/ext/http11_client/http11_parser.c +0 -418
- data/ext/http11_client/http11_parser.h +0 -48
- data/ext/http11_client/http11_parser.rl +0 -170
- data/lib/em-http/mock.rb +0 -137
- data/spec/mock_spec.rb +0 -166
- data/spec/request_spec.rb +0 -1003
data/examples/oauth-tweet.rb
CHANGED
@@ -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 '
|
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'
|
data/lib/em-http/client.rb
CHANGED
@@ -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
|
-
|
13
|
-
include
|
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 :
|
30
|
-
attr_reader :
|
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
|
33
|
-
@parser = HttpClientParser.new
|
34
|
-
@data = EventMachine::Buffer.new
|
35
|
-
@chunk_header = HttpChunkHeader.new
|
35
|
+
def reset!
|
36
36
|
@response_header = HttpResponseHeader.new
|
37
|
-
@
|
38
|
-
|
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
|
-
|
45
|
+
def last_effective_url; @req.uri; end
|
46
|
+
def redirects; @req.options[:followed]; end
|
47
|
+
|
52
48
|
def connection_completed
|
53
|
-
|
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
|
-
|
71
|
-
|
72
|
-
|
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
|
-
|
66
|
+
unbind
|
88
67
|
end
|
89
68
|
|
90
|
-
|
91
|
-
|
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
|
-
|
106
|
-
|
107
|
-
@disconnect = blk
|
73
|
+
def redirect?
|
74
|
+
@response_header.location && @req.follow_redirect?
|
108
75
|
end
|
109
76
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
-
|
116
|
-
|
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
|
130
|
-
@
|
131
|
-
|
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
|
-
|
140
|
-
|
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
|
161
|
-
|
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
|
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
|
201
|
-
|
202
|
-
head['
|
203
|
-
|
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
|
-
|
223
|
-
|
224
|
-
|
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
|
-
|
235
|
-
|
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
|
-
|
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
|
-
|
245
|
-
|
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
|
282
|
-
|
283
|
-
|
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
|
-
|
195
|
+
@response_header.http_version = version.join('.')
|
196
|
+
@response_header.http_status = status
|
197
|
+
@response_header.http_reason = 'unknown'
|
357
198
|
|
358
|
-
#
|
359
|
-
|
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
|
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
|
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
|
-
|
421
|
-
return false
|
233
|
+
return
|
422
234
|
end
|
423
235
|
|
424
|
-
if
|
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
|
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
|