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
         |