z-http-request 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gemtest +0 -0
- data/.gitignore +10 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +8 -0
- data/Gemfile +17 -0
- data/README.md +38 -0
- data/Rakefile +3 -0
- data/benchmarks/clients.rb +170 -0
- data/benchmarks/em-excon.rb +87 -0
- data/benchmarks/em-profile.gif +0 -0
- data/benchmarks/em-profile.txt +65 -0
- data/benchmarks/server.rb +48 -0
- data/examples/.gitignore +1 -0
- data/examples/digest_auth/client.rb +25 -0
- data/examples/digest_auth/server.rb +28 -0
- data/examples/fetch.rb +30 -0
- data/examples/fibered-http.rb +51 -0
- data/examples/multi.rb +25 -0
- data/examples/oauth-tweet.rb +35 -0
- data/examples/socks5.rb +23 -0
- data/lib/z-http/client.rb +318 -0
- data/lib/z-http/core_ext/bytesize.rb +6 -0
- data/lib/z-http/decoders.rb +254 -0
- data/lib/z-http/http_client_options.rb +51 -0
- data/lib/z-http/http_connection.rb +214 -0
- data/lib/z-http/http_connection_options.rb +44 -0
- data/lib/z-http/http_encoding.rb +142 -0
- data/lib/z-http/http_header.rb +83 -0
- data/lib/z-http/http_status_codes.rb +57 -0
- data/lib/z-http/middleware/digest_auth.rb +112 -0
- data/lib/z-http/middleware/json_response.rb +15 -0
- data/lib/z-http/middleware/oauth.rb +40 -0
- data/lib/z-http/middleware/oauth2.rb +28 -0
- data/lib/z-http/multi.rb +57 -0
- data/lib/z-http/request.rb +23 -0
- data/lib/z-http/version.rb +5 -0
- data/lib/z-http-request.rb +1 -0
- data/lib/z-http.rb +18 -0
- data/spec/client_spec.rb +892 -0
- data/spec/digest_auth_spec.rb +48 -0
- data/spec/dns_spec.rb +44 -0
- data/spec/encoding_spec.rb +49 -0
- data/spec/external_spec.rb +150 -0
- data/spec/fixtures/google.ca +16 -0
- data/spec/fixtures/gzip-sample.gz +0 -0
- data/spec/gzip_spec.rb +68 -0
- data/spec/helper.rb +30 -0
- data/spec/middleware_spec.rb +143 -0
- data/spec/multi_spec.rb +104 -0
- data/spec/pipelining_spec.rb +66 -0
- data/spec/redirect_spec.rb +321 -0
- data/spec/socksify_proxy_spec.rb +60 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/ssl_spec.rb +20 -0
- data/spec/stallion.rb +296 -0
- data/spec/stub_server.rb +42 -0
- data/z-http-request.gemspec +33 -0
- metadata +248 -0
data/examples/socks5.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'ZMachine'
|
3
|
+
require '../lib/z-http'
|
4
|
+
|
5
|
+
ZMachine.run do
|
6
|
+
# Establish a SOCKS5 tunnel via SSH
|
7
|
+
# ssh -D 8000 some_remote_machine
|
8
|
+
|
9
|
+
connection_options = {:proxy => {:host => '127.0.0.1', :port => 8000, :type => :socks5}}
|
10
|
+
http = ZMachine::HttpRequest.new('http://igvita.com/', connection_options).get :redirects => 2
|
11
|
+
|
12
|
+
http.callback {
|
13
|
+
puts "#{http.response_header.status} - #{http.response.length} bytes\n"
|
14
|
+
puts http.response
|
15
|
+
ZMachine.stop
|
16
|
+
}
|
17
|
+
|
18
|
+
http.errback {
|
19
|
+
puts "Error: " + http.error
|
20
|
+
puts http.inspect
|
21
|
+
ZMachine.stop
|
22
|
+
}
|
23
|
+
end
|
@@ -0,0 +1,318 @@
|
|
1
|
+
require 'cookiejar'
|
2
|
+
|
3
|
+
module ZMachine
|
4
|
+
|
5
|
+
|
6
|
+
class HttpClient
|
7
|
+
include Deferrable
|
8
|
+
include HttpEncoding
|
9
|
+
include HttpStatus
|
10
|
+
|
11
|
+
TRANSFER_ENCODING="TRANSFER_ENCODING"
|
12
|
+
CONTENT_ENCODING="CONTENT_ENCODING"
|
13
|
+
CONTENT_LENGTH="CONTENT_LENGTH"
|
14
|
+
CONTENT_TYPE="CONTENT_TYPE"
|
15
|
+
LAST_MODIFIED="LAST_MODIFIED"
|
16
|
+
KEEP_ALIVE="CONNECTION"
|
17
|
+
SET_COOKIE="SET_COOKIE"
|
18
|
+
LOCATION="LOCATION"
|
19
|
+
HOST="HOST"
|
20
|
+
ETAG="ETAG"
|
21
|
+
|
22
|
+
CRLF="\r\n"
|
23
|
+
|
24
|
+
attr_accessor :state, :response
|
25
|
+
attr_reader :response_header, :error, :content_charset, :req, :cookies
|
26
|
+
|
27
|
+
def initialize(conn, options)
|
28
|
+
@conn = conn
|
29
|
+
@req = options
|
30
|
+
|
31
|
+
@stream = nil
|
32
|
+
@headers = nil
|
33
|
+
@cookies = []
|
34
|
+
@cookiejar = CookieJar.new
|
35
|
+
|
36
|
+
reset!
|
37
|
+
end
|
38
|
+
|
39
|
+
def reset!
|
40
|
+
@response_header = HttpResponseHeader.new
|
41
|
+
@state = :response_header
|
42
|
+
|
43
|
+
@response = ''
|
44
|
+
@error = nil
|
45
|
+
@content_decoder = nil
|
46
|
+
@content_charset = nil
|
47
|
+
end
|
48
|
+
|
49
|
+
def last_effective_url; @req.uri; end
|
50
|
+
def redirects; @req.followed; end
|
51
|
+
def peer; @conn.peer; end
|
52
|
+
|
53
|
+
def connection_completed
|
54
|
+
@state = :response_header
|
55
|
+
|
56
|
+
head, body = build_request, @req.body
|
57
|
+
@conn.middleware.each do |m|
|
58
|
+
head, body = m.request(self, head, body) if m.respond_to?(:request)
|
59
|
+
end
|
60
|
+
|
61
|
+
send_request(head, body)
|
62
|
+
end
|
63
|
+
|
64
|
+
def on_request_complete
|
65
|
+
begin
|
66
|
+
@content_decoder.finalize! if @content_decoder
|
67
|
+
rescue HttpDecoders::DecoderError
|
68
|
+
on_error "Content-decoder error"
|
69
|
+
end
|
70
|
+
|
71
|
+
unbind
|
72
|
+
end
|
73
|
+
|
74
|
+
def continue?
|
75
|
+
@response_header.status == 100 && (@req.method == 'POST' || @req.method == 'PUT')
|
76
|
+
end
|
77
|
+
|
78
|
+
def finished?
|
79
|
+
@state == :finished || (@state == :body && @response_header.content_length.nil?)
|
80
|
+
end
|
81
|
+
|
82
|
+
def redirect?
|
83
|
+
@response_header.redirection? && @req.follow_redirect?
|
84
|
+
end
|
85
|
+
|
86
|
+
def unbind(reason = nil)
|
87
|
+
if finished?
|
88
|
+
if redirect?
|
89
|
+
|
90
|
+
begin
|
91
|
+
@conn.middleware.each do |m|
|
92
|
+
m.response(self) if m.respond_to?(:response)
|
93
|
+
end
|
94
|
+
|
95
|
+
# one of the injected middlewares could have changed
|
96
|
+
# our redirect settings, check if we still want to
|
97
|
+
# follow the location header
|
98
|
+
if redirect?
|
99
|
+
@req.followed += 1
|
100
|
+
|
101
|
+
@cookies.clear
|
102
|
+
@cookies = @cookiejar.get(@response_header.location).map(&:to_s) if @req.pass_cookies
|
103
|
+
@req.set_uri(@response_header.location)
|
104
|
+
|
105
|
+
@conn.redirect(self)
|
106
|
+
else
|
107
|
+
succeed(self)
|
108
|
+
end
|
109
|
+
|
110
|
+
rescue Exception => e
|
111
|
+
on_error(e.message)
|
112
|
+
end
|
113
|
+
else
|
114
|
+
succeed(self)
|
115
|
+
end
|
116
|
+
|
117
|
+
else
|
118
|
+
on_error(reason || 'connection closed by server')
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def on_error(msg = nil)
|
123
|
+
@error = msg
|
124
|
+
fail(self)
|
125
|
+
end
|
126
|
+
alias :close :on_error
|
127
|
+
|
128
|
+
def stream(&blk); @stream = blk; end
|
129
|
+
def headers(&blk); @headers = blk; end
|
130
|
+
|
131
|
+
def normalize_body(body)
|
132
|
+
body.is_a?(Hash) ? form_encode_body(body) : body
|
133
|
+
end
|
134
|
+
|
135
|
+
def build_request
|
136
|
+
head = @req.headers ? munge_header_keys(@req.headers) : {}
|
137
|
+
|
138
|
+
if @conn.connopts.http_proxy?
|
139
|
+
proxy = @conn.connopts.proxy
|
140
|
+
head['proxy-authorization'] = proxy[:authorization] if proxy[:authorization]
|
141
|
+
end
|
142
|
+
|
143
|
+
# Set the cookie header if provided
|
144
|
+
if cookie = head['cookie']
|
145
|
+
@cookies << encode_cookie(cookie) if cookie
|
146
|
+
end
|
147
|
+
head['cookie'] = @cookies.compact.uniq.join("; ").squeeze(";") unless @cookies.empty?
|
148
|
+
|
149
|
+
# Set connection close unless keepalive
|
150
|
+
if !@req.keepalive
|
151
|
+
head['connection'] = 'close'
|
152
|
+
end
|
153
|
+
|
154
|
+
# Set the Host header if it hasn't been specified already
|
155
|
+
head['host'] ||= encode_host
|
156
|
+
|
157
|
+
# Set the User-Agent if it hasn't been specified
|
158
|
+
if !head.key?('user-agent')
|
159
|
+
head['user-agent'] = "ZMachine HttpClient"
|
160
|
+
elsif head['user-agent'].nil?
|
161
|
+
head.delete('user-agent')
|
162
|
+
end
|
163
|
+
|
164
|
+
# Set the auth from the URI if given
|
165
|
+
head['Authorization'] = @req.uri.userinfo.split(/:/, 2) if @req.uri.userinfo
|
166
|
+
|
167
|
+
head
|
168
|
+
end
|
169
|
+
|
170
|
+
def send_request(head, body)
|
171
|
+
body = normalize_body(body)
|
172
|
+
file = @req.file
|
173
|
+
query = @req.query
|
174
|
+
|
175
|
+
# Set the Content-Length if file is given
|
176
|
+
head['content-length'] = File.size(file) if file
|
177
|
+
|
178
|
+
# Set the Content-Length if body is given,
|
179
|
+
# or we're doing an empty post or put
|
180
|
+
if body
|
181
|
+
head['content-length'] = body.bytesize
|
182
|
+
elsif @req.method == 'POST' or @req.method == 'PUT'
|
183
|
+
# wont happen if body is set and we already set content-length above
|
184
|
+
head['content-length'] ||= 0
|
185
|
+
end
|
186
|
+
|
187
|
+
# Set content-type header if missing and body is a Ruby hash
|
188
|
+
if !head['content-type'] and @req.body.is_a? Hash
|
189
|
+
head['content-type'] = 'application/x-www-form-urlencoded'
|
190
|
+
end
|
191
|
+
|
192
|
+
request_header ||= encode_request(@req.method, @req.uri, query, @conn.connopts.proxy)
|
193
|
+
request_header << encode_headers(head)
|
194
|
+
request_header << CRLF
|
195
|
+
@conn.send_data request_header
|
196
|
+
|
197
|
+
if body
|
198
|
+
@conn.send_data body
|
199
|
+
elsif @req.file
|
200
|
+
@conn.stream_file_data @req.file, :http_chunks => false
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def on_body_data(data)
|
205
|
+
if @content_decoder
|
206
|
+
begin
|
207
|
+
@content_decoder << data
|
208
|
+
rescue HttpDecoders::DecoderError
|
209
|
+
on_error "Content-decoder error"
|
210
|
+
end
|
211
|
+
else
|
212
|
+
on_decoded_body_data(data)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def on_decoded_body_data(data)
|
217
|
+
data.force_encoding @content_charset if @content_charset
|
218
|
+
if @stream
|
219
|
+
@stream.call(data)
|
220
|
+
else
|
221
|
+
@response << data
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def parse_response_header(header, version, status)
|
226
|
+
@response_header.raw = header
|
227
|
+
header.each do |key, val|
|
228
|
+
@response_header[key.upcase.gsub('-','_')] = val
|
229
|
+
end
|
230
|
+
|
231
|
+
@response_header.http_version = version.join('.')
|
232
|
+
@response_header.http_status = status
|
233
|
+
@response_header.http_reason = CODE[status] || 'unknown'
|
234
|
+
|
235
|
+
# invoke headers callback after full parse
|
236
|
+
# if one is specified by the user
|
237
|
+
@headers.call(@response_header) if @headers
|
238
|
+
|
239
|
+
unless @response_header.http_status and @response_header.http_reason
|
240
|
+
@state = :invalid
|
241
|
+
on_error "no HTTP response"
|
242
|
+
return
|
243
|
+
end
|
244
|
+
|
245
|
+
# add set-cookie's to cookie list
|
246
|
+
if @response_header.cookie && @req.pass_cookies
|
247
|
+
[@response_header.cookie].flatten.each {|cookie| @cookiejar.set(cookie, @req.uri)}
|
248
|
+
end
|
249
|
+
|
250
|
+
# correct location header - some servers will incorrectly give a relative URI
|
251
|
+
if @response_header.location
|
252
|
+
begin
|
253
|
+
location = Addressable::URI.parse(@response_header.location)
|
254
|
+
location.path = "/" if location.path.empty?
|
255
|
+
|
256
|
+
if location.relative?
|
257
|
+
location = @req.uri.join(location)
|
258
|
+
else
|
259
|
+
# if redirect is to an absolute url, check for correct URI structure
|
260
|
+
raise if location.host.nil?
|
261
|
+
end
|
262
|
+
|
263
|
+
@response_header[LOCATION] = location.to_s
|
264
|
+
|
265
|
+
rescue
|
266
|
+
on_error "Location header format error"
|
267
|
+
return
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
# Fire callbacks immediately after recieving header requests
|
272
|
+
# if the request method is HEAD. In case of a redirect, terminate
|
273
|
+
# current connection and reinitialize the process.
|
274
|
+
if @req.method == "HEAD"
|
275
|
+
@state = :finished
|
276
|
+
return
|
277
|
+
end
|
278
|
+
|
279
|
+
if @response_header.chunked_encoding?
|
280
|
+
@state = :chunk_header
|
281
|
+
elsif @response_header.content_length
|
282
|
+
@state = :body
|
283
|
+
else
|
284
|
+
@state = :body
|
285
|
+
end
|
286
|
+
|
287
|
+
if @req.decoding && decoder_class = HttpDecoders.decoder_for_encoding(response_header[CONTENT_ENCODING])
|
288
|
+
begin
|
289
|
+
@content_decoder = decoder_class.new do |s| on_decoded_body_data(s) end
|
290
|
+
rescue HttpDecoders::DecoderError
|
291
|
+
on_error "Content-decoder error"
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
# handle malformed header - Content-Type repetitions.
|
296
|
+
content_type = [response_header[CONTENT_TYPE]].flatten.first
|
297
|
+
|
298
|
+
if String.method_defined?(:force_encoding) && /;\s*charset=\s*(.+?)\s*(;|$)/.match(content_type)
|
299
|
+
@content_charset = Encoding.find($1.gsub(/^\"|\"$/, '')) rescue Encoding.default_external
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
class CookieJar
|
304
|
+
def initialize
|
305
|
+
@jar = ::CookieJar::Jar.new
|
306
|
+
end
|
307
|
+
|
308
|
+
def set string, uri
|
309
|
+
@jar.set_cookie(uri, string) rescue nil # drop invalid cookies
|
310
|
+
end
|
311
|
+
|
312
|
+
def get uri
|
313
|
+
uri = URI.parse(uri) rescue nil
|
314
|
+
uri ? @jar.get_cookies(uri) : []
|
315
|
+
end
|
316
|
+
end # CookieJar
|
317
|
+
end
|
318
|
+
end
|
@@ -0,0 +1,254 @@
|
|
1
|
+
require 'zlib'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
##
|
5
|
+
# Provides a unified callback interface to decompression libraries.
|
6
|
+
module ZMachine::HttpDecoders
|
7
|
+
|
8
|
+
class DecoderError < StandardError
|
9
|
+
end
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def accepted_encodings
|
13
|
+
DECODERS.inject([]) { |r, d| r + d.encoding_names }
|
14
|
+
end
|
15
|
+
|
16
|
+
def decoder_for_encoding(encoding)
|
17
|
+
DECODERS.each { |d|
|
18
|
+
return d if d.encoding_names.include? encoding
|
19
|
+
}
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class Base
|
25
|
+
def self.encoding_names
|
26
|
+
name = to_s.split('::').last.downcase
|
27
|
+
[name]
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# chunk_callback:: [Block] To handle a decompressed chunk
|
32
|
+
def initialize(&chunk_callback)
|
33
|
+
@chunk_callback = chunk_callback
|
34
|
+
end
|
35
|
+
|
36
|
+
def <<(compressed)
|
37
|
+
return unless compressed && compressed.size > 0
|
38
|
+
|
39
|
+
decompressed = decompress(compressed)
|
40
|
+
receive_decompressed decompressed
|
41
|
+
end
|
42
|
+
|
43
|
+
def finalize!
|
44
|
+
decompressed = finalize
|
45
|
+
receive_decompressed decompressed
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def receive_decompressed(decompressed)
|
51
|
+
if decompressed && decompressed.size > 0
|
52
|
+
@chunk_callback.call(decompressed)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
protected
|
57
|
+
|
58
|
+
##
|
59
|
+
# Must return a part of decompressed
|
60
|
+
def decompress(compressed)
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# May return last part
|
66
|
+
def finalize
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class Deflate < Base
|
72
|
+
def decompress(compressed)
|
73
|
+
begin
|
74
|
+
@zstream ||= Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
75
|
+
@zstream.inflate(compressed)
|
76
|
+
rescue Zlib::Error
|
77
|
+
raise DecoderError
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def finalize
|
82
|
+
return nil unless @zstream
|
83
|
+
|
84
|
+
begin
|
85
|
+
r = @zstream.inflate(nil)
|
86
|
+
@zstream.close
|
87
|
+
r
|
88
|
+
rescue Zlib::Error
|
89
|
+
raise DecoderError
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
##
|
95
|
+
# Partial implementation of RFC 1952 to extract the deflate stream from a gzip file
|
96
|
+
class GZipHeader
|
97
|
+
def initialize
|
98
|
+
@state = :begin
|
99
|
+
@data = ""
|
100
|
+
@pos = 0
|
101
|
+
end
|
102
|
+
|
103
|
+
def finished?
|
104
|
+
@state == :finish
|
105
|
+
end
|
106
|
+
|
107
|
+
def read(n, buffer)
|
108
|
+
if (@pos + n) <= @data.size
|
109
|
+
buffer << @data[@pos..(@pos + n - 1)]
|
110
|
+
@pos += n
|
111
|
+
return true
|
112
|
+
else
|
113
|
+
return false
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def readbyte
|
118
|
+
if (@pos + 1) <= @data.size
|
119
|
+
@pos += 1
|
120
|
+
@data.getbyte(@pos - 1)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def eof?
|
125
|
+
@pos >= @data.size
|
126
|
+
end
|
127
|
+
|
128
|
+
def extract_stream(compressed)
|
129
|
+
@data << compressed
|
130
|
+
pos = @pos
|
131
|
+
|
132
|
+
while !eof? && !finished?
|
133
|
+
buffer = ""
|
134
|
+
|
135
|
+
case @state
|
136
|
+
when :begin
|
137
|
+
break if !read(10, buffer)
|
138
|
+
|
139
|
+
if buffer.getbyte(0) != 0x1f || buffer.getbyte(1) != 0x8b
|
140
|
+
raise DecoderError.new("magic header not found")
|
141
|
+
end
|
142
|
+
|
143
|
+
if buffer.getbyte(2) != 0x08
|
144
|
+
raise DecoderError.new("unknown compression method")
|
145
|
+
end
|
146
|
+
|
147
|
+
@flags = buffer.getbyte(3)
|
148
|
+
if (@flags & 0xe0).nonzero?
|
149
|
+
raise DecoderError.new("unknown header flags set")
|
150
|
+
end
|
151
|
+
|
152
|
+
# We don't care about these values, I'm leaving the code for reference
|
153
|
+
# @time = buffer[4..7].unpack("V")[0] # little-endian uint32
|
154
|
+
# @extra_flags = buffer.getbyte(8)
|
155
|
+
# @os = buffer.getbyte(9)
|
156
|
+
|
157
|
+
@state = :extra_length
|
158
|
+
|
159
|
+
when :extra_length
|
160
|
+
if (@flags & 0x04).nonzero?
|
161
|
+
break if !read(2, buffer)
|
162
|
+
@extra_length = buffer.unpack("v")[0] # little-endian uint16
|
163
|
+
@state = :extra
|
164
|
+
else
|
165
|
+
@state = :extra
|
166
|
+
end
|
167
|
+
|
168
|
+
when :extra
|
169
|
+
if (@flags & 0x04).nonzero?
|
170
|
+
break if read(@extra_length, buffer)
|
171
|
+
@state = :name
|
172
|
+
else
|
173
|
+
@state = :name
|
174
|
+
end
|
175
|
+
|
176
|
+
when :name
|
177
|
+
if (@flags & 0x08).nonzero?
|
178
|
+
while !(buffer = readbyte).nil?
|
179
|
+
if buffer == 0
|
180
|
+
@state = :comment
|
181
|
+
break
|
182
|
+
end
|
183
|
+
end
|
184
|
+
else
|
185
|
+
@state = :comment
|
186
|
+
end
|
187
|
+
|
188
|
+
when :comment
|
189
|
+
if (@flags & 0x10).nonzero?
|
190
|
+
while !(buffer = readbyte).nil?
|
191
|
+
if buffer == 0
|
192
|
+
@state = :hcrc
|
193
|
+
break
|
194
|
+
end
|
195
|
+
end
|
196
|
+
else
|
197
|
+
@state = :hcrc
|
198
|
+
end
|
199
|
+
|
200
|
+
when :hcrc
|
201
|
+
if (@flags & 0x02).nonzero?
|
202
|
+
break if !read(2, buffer)
|
203
|
+
@state = :finish
|
204
|
+
else
|
205
|
+
@state = :finish
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
if finished?
|
211
|
+
compressed[(@pos - pos)..-1]
|
212
|
+
else
|
213
|
+
""
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
class GZip < Base
|
219
|
+
def self.encoding_names
|
220
|
+
%w(gzip compressed)
|
221
|
+
end
|
222
|
+
|
223
|
+
def decompress(compressed)
|
224
|
+
compressed.force_encoding('BINARY')
|
225
|
+
@header ||= GZipHeader.new
|
226
|
+
if !@header.finished?
|
227
|
+
compressed = @header.extract_stream(compressed)
|
228
|
+
end
|
229
|
+
|
230
|
+
@zstream ||= Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
231
|
+
@zstream.inflate(compressed)
|
232
|
+
rescue Zlib::Error
|
233
|
+
raise DecoderError
|
234
|
+
end
|
235
|
+
|
236
|
+
def finalize
|
237
|
+
if @zstream
|
238
|
+
if !@zstream.finished?
|
239
|
+
r = @zstream.finish
|
240
|
+
end
|
241
|
+
@zstream.close
|
242
|
+
r
|
243
|
+
else
|
244
|
+
nil
|
245
|
+
end
|
246
|
+
rescue Zlib::Error
|
247
|
+
raise DecoderError
|
248
|
+
end
|
249
|
+
|
250
|
+
end
|
251
|
+
|
252
|
+
DECODERS = [Deflate, GZip]
|
253
|
+
|
254
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
class HttpClientOptions
|
2
|
+
attr_reader :uri, :method, :host, :port
|
3
|
+
attr_reader :headers, :file, :body, :query, :path
|
4
|
+
attr_reader :keepalive, :pass_cookies, :decoding
|
5
|
+
|
6
|
+
attr_accessor :followed, :redirects
|
7
|
+
|
8
|
+
def initialize(uri, options, method)
|
9
|
+
@keepalive = options[:keepalive] || false # default to single request per connection
|
10
|
+
@redirects = options[:redirects] ||= 0 # default number of redirects to follow
|
11
|
+
@followed = options[:followed] ||= 0 # keep track of number of followed requests
|
12
|
+
|
13
|
+
@method = method.to_s.upcase
|
14
|
+
@headers = options[:head] || {}
|
15
|
+
@query = options[:query]
|
16
|
+
|
17
|
+
|
18
|
+
@file = options[:file]
|
19
|
+
@body = options[:body]
|
20
|
+
|
21
|
+
@pass_cookies = options.fetch(:pass_cookies, true) # pass cookies between redirects
|
22
|
+
@decoding = options.fetch(:decoding, true) # auto-decode compressed response
|
23
|
+
|
24
|
+
set_uri(uri, options[:path])
|
25
|
+
end
|
26
|
+
|
27
|
+
def follow_redirect?; @followed < @redirects; end
|
28
|
+
def ssl?; @uri.scheme == "https" || @uri.port == 443; end
|
29
|
+
def no_body?; @method == "HEAD"; end
|
30
|
+
|
31
|
+
def set_uri(uri, path = nil)
|
32
|
+
uri = uri.kind_of?(Addressable::URI) ? uri : Addressable::URI::parse(uri.to_s)
|
33
|
+
uri.path = path if path
|
34
|
+
uri.path = '/' if uri.path.empty?
|
35
|
+
|
36
|
+
@uri = uri
|
37
|
+
@path = uri.path
|
38
|
+
|
39
|
+
# Make sure the ports are set as Addressable::URI doesn't
|
40
|
+
# set the port if it isn't there
|
41
|
+
if @uri.scheme == "https"
|
42
|
+
@uri.port ||= 443
|
43
|
+
else
|
44
|
+
@uri.port ||= 80
|
45
|
+
end
|
46
|
+
|
47
|
+
@host = @uri.host
|
48
|
+
@port = @uri.port
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|