Class: ZMachine::HttpClient

Inherits:
Object
  • Object
show all
Includes:
Deferrable, HttpEncoding, HttpStatus
Defined in:
lib/z-http/client.rb

Defined Under Namespace

Classes: CookieJar

Constant Summary collapse

TRANSFER_ENCODING =
"TRANSFER_ENCODING"
CONTENT_ENCODING =
"CONTENT_ENCODING"
CONTENT_LENGTH =
"CONTENT_LENGTH"
CONTENT_TYPE =
"CONTENT_TYPE"
LAST_MODIFIED =
"LAST_MODIFIED"
KEEP_ALIVE =
"CONNECTION"
"SET_COOKIE"
LOCATION =
"LOCATION"
HOST =
"HOST"
ETAG =
"ETAG"
CRLF =
"\r\n"

Constants included from HttpStatus

ZMachine::HttpStatus::CODE

Constants included from HttpEncoding

ZMachine::HttpEncoding::FIELD_ENCODING, ZMachine::HttpEncoding::HTTP_REQUEST_HEADER

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from HttpEncoding

#bytesize, #encode_auth, #encode_cookie, #encode_field, #encode_headers, #encode_host, #encode_param, #encode_query, #encode_request, #escape, #form_encode_body, #munge_header_keys, #unescape

Constructor Details

#initialize(conn, options) ⇒ HttpClient

Returns a new instance of HttpClient.



27
28
29
30
31
32
33
34
35
36
37
# File 'lib/z-http/client.rb', line 27

def initialize(conn, options)
  @conn = conn
  @req  = options

  @stream    = nil
  @headers   = nil
  @cookies   = []
  @cookiejar = CookieJar.new

  reset!
end

Instance Attribute Details

#content_charsetObject (readonly)

Returns the value of attribute content_charset.



25
26
27
# File 'lib/z-http/client.rb', line 25

def content_charset
  @content_charset
end

#cookiesObject (readonly)

Returns the value of attribute cookies.



25
26
27
# File 'lib/z-http/client.rb', line 25

def cookies
  @cookies
end

#errorObject (readonly)

Returns the value of attribute error.



25
26
27
# File 'lib/z-http/client.rb', line 25

def error
  @error
end

#reqObject (readonly)

Returns the value of attribute req.



25
26
27
# File 'lib/z-http/client.rb', line 25

def req
  @req
end

#responseObject

Returns the value of attribute response.



24
25
26
# File 'lib/z-http/client.rb', line 24

def response
  @response
end

#response_headerObject (readonly)

Returns the value of attribute response_header.



25
26
27
# File 'lib/z-http/client.rb', line 25

def response_header
  @response_header
end

#stateObject

Returns the value of attribute state.



24
25
26
# File 'lib/z-http/client.rb', line 24

def state
  @state
end

Instance Method Details

#build_requestObject



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/z-http/client.rb', line 135

def build_request
  head    = @req.headers ? munge_header_keys(@req.headers) : {}

  if @conn.connopts.http_proxy?
    proxy = @conn.connopts.proxy
    head['proxy-authorization'] = proxy[:authorization] if proxy[:authorization]
  end

  # Set the cookie header if provided
  if cookie = head['cookie']
    @cookies << encode_cookie(cookie) if cookie
  end
  head['cookie'] = @cookies.compact.uniq.join("; ").squeeze(";") unless @cookies.empty?

  # Set connection close unless keepalive
  if !@req.keepalive
    head['connection'] = 'close'
  end

  # Set the Host header if it hasn't been specified already
  head['host'] ||= encode_host

  # Set the User-Agent if it hasn't been specified
  if !head.key?('user-agent')
    head['user-agent'] = "ZMachine HttpClient"
  elsif head['user-agent'].nil?
    head.delete('user-agent')
  end

  # Set the auth from the URI if given
  head['Authorization'] = @req.uri.userinfo.split(/:/, 2) if @req.uri.userinfo

  head
end

#connection_completedObject



53
54
55
56
57
58
59
60
61
62
# File 'lib/z-http/client.rb', line 53

def connection_completed
  @state = :response_header

  head, body = build_request, @req.body
  @conn.middleware.each do |m|
    head, body = m.request(self, head, body) if m.respond_to?(:request)
  end

  send_request(head, body)
end

#continue?Boolean

Returns:

  • (Boolean)


74
75
76
# File 'lib/z-http/client.rb', line 74

def continue?
  @response_header.status == 100 && (@req.method == 'POST' || @req.method == 'PUT')
end

#finished?Boolean

Returns:

  • (Boolean)


78
79
80
# File 'lib/z-http/client.rb', line 78

def finished?
  @state == :finished || (@state == :body && @response_header.content_length.nil?)
end

#headers(&blk) ⇒ Object



129
# File 'lib/z-http/client.rb', line 129

def headers(&blk); @headers = blk; end

#last_effective_urlObject



49
# File 'lib/z-http/client.rb', line 49

def last_effective_url; @req.uri; end

#normalize_body(body) ⇒ Object



131
132
133
# File 'lib/z-http/client.rb', line 131

def normalize_body(body)
  body.is_a?(Hash) ? form_encode_body(body) : body
end

#on_body_data(data) ⇒ Object



204
205
206
207
208
209
210
211
212
213
214
# File 'lib/z-http/client.rb', line 204

def on_body_data(data)
  if @content_decoder
    begin
      @content_decoder << data
    rescue HttpDecoders::DecoderError
      on_error "Content-decoder error"
    end
  else
    on_decoded_body_data(data)
  end
end

#on_decoded_body_data(data) ⇒ Object



216
217
218
219
220
221
222
223
# File 'lib/z-http/client.rb', line 216

def on_decoded_body_data(data)
  data.force_encoding @content_charset if @content_charset
  if @stream
    @stream.call(data)
  else
    @response << data
  end
end

#on_error(msg = nil) ⇒ Object Also known as: close



122
123
124
125
# File 'lib/z-http/client.rb', line 122

def on_error(msg = nil)
  @error = msg
  fail(self)
end

#on_request_completeObject



64
65
66
67
68
69
70
71
72
# File 'lib/z-http/client.rb', line 64

def on_request_complete
  begin
    @content_decoder.finalize! if @content_decoder
  rescue HttpDecoders::DecoderError
    on_error "Content-decoder error"
  end

  unbind
end

#parse_response_header(header, version, status) ⇒ Object



225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# File 'lib/z-http/client.rb', line 225

def parse_response_header(header, version, status)
  @response_header.raw = header
  header.each do |key, val|
    @response_header[key.upcase.gsub('-','_')] = val
  end

  @response_header.http_version = version.join('.')
  @response_header.http_status  = status
  @response_header.http_reason  = CODE[status] || 'unknown'

  # invoke headers callback after full parse
  # if one is specified by the user
  @headers.call(@response_header) if @headers

  unless @response_header.http_status and @response_header.http_reason
    @state = :invalid
    on_error "no HTTP response"
    return
  end

  # add set-cookie's to cookie list
  if @response_header.cookie && @req.pass_cookies
    [@response_header.cookie].flatten.each {|cookie| @cookiejar.set(cookie, @req.uri)}
  end

  # correct location header - some servers will incorrectly give a relative URI
  if @response_header.location
    begin
      location = Addressable::URI.parse(@response_header.location)
      location.path = "/" if location.path.empty?

      if location.relative?
        location = @req.uri.join(location)
      else
        # if redirect is to an absolute url, check for correct URI structure
        raise if location.host.nil?
      end

      @response_header[LOCATION] = location.to_s

    rescue
      on_error "Location header format error"
      return
    end
  end

  # Fire callbacks immediately after recieving header requests
  # if the request method is HEAD. In case of a redirect, terminate
  # current connection and reinitialize the process.
  if @req.method == "HEAD"
    @state = :finished
    return
  end

  if @response_header.chunked_encoding?
    @state = :chunk_header
  elsif @response_header.content_length
    @state = :body
  else
    @state = :body
  end

  if @req.decoding && decoder_class = HttpDecoders.decoder_for_encoding(response_header[CONTENT_ENCODING])
    begin
      @content_decoder = decoder_class.new do |s| on_decoded_body_data(s) end
    rescue HttpDecoders::DecoderError
      on_error "Content-decoder error"
    end
  end

  # handle malformed header - Content-Type repetitions.
  content_type = [response_header[CONTENT_TYPE]].flatten.first

  if String.method_defined?(:force_encoding) && /;\s*charset=\s*(.+?)\s*(;|$)/.match(content_type)
    @content_charset = Encoding.find($1.gsub(/^\"|\"$/, '')) rescue Encoding.default_external
  end
end

#peerObject



51
# File 'lib/z-http/client.rb', line 51

def peer; @conn.peer; end

#redirect?Boolean

Returns:

  • (Boolean)


82
83
84
# File 'lib/z-http/client.rb', line 82

def redirect?
  @response_header.redirection? && @req.follow_redirect?
end

#redirectsObject



50
# File 'lib/z-http/client.rb', line 50

def redirects; @req.followed; end

#reset!Object



39
40
41
42
43
44
45
46
47
# File 'lib/z-http/client.rb', line 39

def reset!
  @response_header = HttpResponseHeader.new
  @state = :response_header

  @response = ''
  @error = nil
  @content_decoder = nil
  @content_charset = nil
end

#send_request(head, body) ⇒ Object



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/z-http/client.rb', line 170

def send_request(head, body)
  body    = normalize_body(body)
  file    = @req.file
  query   = @req.query

  # Set the Content-Length if file is given
  head['content-length'] = File.size(file) if file

  # Set the Content-Length if body is given,
  # or we're doing an empty post or put
  if body
    head['content-length'] = body.bytesize
  elsif @req.method == 'POST' or @req.method == 'PUT'
    # wont happen if body is set and we already set content-length above
    head['content-length'] ||= 0
  end

  # Set content-type header if missing and body is a Ruby hash
  if !head['content-type'] and @req.body.is_a? Hash
    head['content-type'] = 'application/x-www-form-urlencoded'
  end

  request_header ||= encode_request(@req.method, @req.uri, query, @conn.connopts.proxy)
  request_header << encode_headers(head)
  request_header << CRLF
  @conn.send_data request_header

  if body
    @conn.send_data body
  elsif @req.file
    @conn.stream_file_data @req.file, :http_chunks => false
  end
end

#stream(&blk) ⇒ Object



128
# File 'lib/z-http/client.rb', line 128

def stream(&blk); @stream = blk; end

#unbind(reason = nil) ⇒ Object



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/z-http/client.rb', line 86

def unbind(reason = nil)
  if finished?
    if redirect?

      begin
        @conn.middleware.each do |m|
          m.response(self) if m.respond_to?(:response)
        end

        # one of the injected middlewares could have changed
        # our redirect settings, check if we still want to
        # follow the location header
        if redirect?
          @req.followed += 1

          @cookies.clear
          @cookies = @cookiejar.get(@response_header.location).map(&:to_s) if @req.pass_cookies
          @req.set_uri(@response_header.location)

          @conn.redirect(self)
        else
          succeed(self)
        end

      rescue Exception => e
        on_error(e.message)
      end
    else
      succeed(self)
    end

  else
    on_error(reason || 'connection closed by server')
  end
end