z-http-request 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.gemtest +0 -0
  3. data/.gitignore +10 -0
  4. data/.rspec +3 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +8 -0
  7. data/Gemfile +17 -0
  8. data/README.md +38 -0
  9. data/Rakefile +3 -0
  10. data/benchmarks/clients.rb +170 -0
  11. data/benchmarks/em-excon.rb +87 -0
  12. data/benchmarks/em-profile.gif +0 -0
  13. data/benchmarks/em-profile.txt +65 -0
  14. data/benchmarks/server.rb +48 -0
  15. data/examples/.gitignore +1 -0
  16. data/examples/digest_auth/client.rb +25 -0
  17. data/examples/digest_auth/server.rb +28 -0
  18. data/examples/fetch.rb +30 -0
  19. data/examples/fibered-http.rb +51 -0
  20. data/examples/multi.rb +25 -0
  21. data/examples/oauth-tweet.rb +35 -0
  22. data/examples/socks5.rb +23 -0
  23. data/lib/z-http/client.rb +318 -0
  24. data/lib/z-http/core_ext/bytesize.rb +6 -0
  25. data/lib/z-http/decoders.rb +254 -0
  26. data/lib/z-http/http_client_options.rb +51 -0
  27. data/lib/z-http/http_connection.rb +214 -0
  28. data/lib/z-http/http_connection_options.rb +44 -0
  29. data/lib/z-http/http_encoding.rb +142 -0
  30. data/lib/z-http/http_header.rb +83 -0
  31. data/lib/z-http/http_status_codes.rb +57 -0
  32. data/lib/z-http/middleware/digest_auth.rb +112 -0
  33. data/lib/z-http/middleware/json_response.rb +15 -0
  34. data/lib/z-http/middleware/oauth.rb +40 -0
  35. data/lib/z-http/middleware/oauth2.rb +28 -0
  36. data/lib/z-http/multi.rb +57 -0
  37. data/lib/z-http/request.rb +23 -0
  38. data/lib/z-http/version.rb +5 -0
  39. data/lib/z-http-request.rb +1 -0
  40. data/lib/z-http.rb +18 -0
  41. data/spec/client_spec.rb +892 -0
  42. data/spec/digest_auth_spec.rb +48 -0
  43. data/spec/dns_spec.rb +44 -0
  44. data/spec/encoding_spec.rb +49 -0
  45. data/spec/external_spec.rb +150 -0
  46. data/spec/fixtures/google.ca +16 -0
  47. data/spec/fixtures/gzip-sample.gz +0 -0
  48. data/spec/gzip_spec.rb +68 -0
  49. data/spec/helper.rb +30 -0
  50. data/spec/middleware_spec.rb +143 -0
  51. data/spec/multi_spec.rb +104 -0
  52. data/spec/pipelining_spec.rb +66 -0
  53. data/spec/redirect_spec.rb +321 -0
  54. data/spec/socksify_proxy_spec.rb +60 -0
  55. data/spec/spec_helper.rb +6 -0
  56. data/spec/ssl_spec.rb +20 -0
  57. data/spec/stallion.rb +296 -0
  58. data/spec/stub_server.rb +42 -0
  59. data/z-http-request.gemspec +33 -0
  60. metadata +248 -0
@@ -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,6 @@
1
+ # bytesize was introduced in 1.8.7+
2
+ if RUBY_VERSION <= "1.8.6"
3
+ class String
4
+ def bytesize; self.size; end
5
+ end
6
+ 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