kronk 1.7.8 → 1.8.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.
data/lib/kronk/request.rb CHANGED
@@ -6,7 +6,7 @@ class Kronk
6
6
  class Request
7
7
 
8
8
  # Raised by Request.parse when parsing invalid http request string.
9
- class ParseError < Kronk::Exception; end
9
+ class ParseError < Kronk::Error; end
10
10
 
11
11
  # Matches the first line of an http request string or a fully
12
12
  # qualified URL.
@@ -46,20 +46,22 @@ class Kronk
46
46
  # Build the URI to use for the request from the given uri or
47
47
  # path and options.
48
48
 
49
- def self.build_uri uri, options={}
50
- uri ||= options[:host] || Kronk.config[:default_host]
51
- suffix = options[:uri_suffix]
49
+ def self.build_uri uri, opts={}
50
+ uri ||= opts[:host] || Kronk.config[:default_host]
51
+ suffix = opts[:uri_suffix]
52
52
 
53
53
  uri = "http://#{uri}" unless uri.to_s =~ %r{^(\w+://|/)}
54
54
  uri = "#{uri}#{suffix}" if suffix
55
55
  uri = URI.parse uri unless URI === uri
56
56
  uri = URI.parse(Kronk.config[:default_host]) + uri unless uri.host
57
57
 
58
- if options[:query]
59
- query = build_query options[:query]
58
+ if opts[:query]
59
+ query = build_query opts[:query]
60
60
  uri.query = [uri.query, query].compact.join "&"
61
61
  end
62
62
 
63
+ uri.path = "/" if uri.path.empty?
64
+
63
65
  uri
64
66
  end
65
67
 
@@ -185,6 +187,18 @@ class Kronk
185
187
  end
186
188
 
187
189
 
190
+ class << self
191
+ %w{get post put delete trace head options}.each do |name|
192
+ class_eval <<-"END"
193
+ def #{name} uri, opts={}, &block
194
+ opts[:http_method] = "#{name}"
195
+ new(uri, opts).retrieve(&block)
196
+ end
197
+ END
198
+ end
199
+ end
200
+
201
+
188
202
  attr_accessor :body, :headers, :proxy, :response, :timeout
189
203
 
190
204
  attr_reader :http_method, :uri, :use_cookies
@@ -199,34 +213,42 @@ class Kronk
199
213
  # :headers:: Hash - extra headers to pass to the request
200
214
  # :http_method:: Symbol - the http method to use; defaults to :get
201
215
  # :proxy:: Hash/String - http proxy to use; defaults to {}
216
+ # :accept_encoding:: Array/String - list of encodings the server can return
202
217
  #
203
218
  # Note: if no http method is specified and data is given, will default
204
219
  # to using a post request.
205
220
 
206
- def initialize uri, options={}
207
- @auth = options[:auth]
221
+ def initialize uri, opts={}
222
+ @auth = opts[:auth]
208
223
 
209
224
  @body = nil
210
- @body = self.class.build_query options[:data] if options[:data]
225
+ @body = self.class.build_query opts[:data] if opts[:data]
211
226
 
227
+ @connection = nil
212
228
  @response = nil
213
229
  @_req = nil
214
- @_res = nil
215
230
 
216
- @headers = options[:headers] || {}
217
- @timeout = options[:timeout] || Kronk.config[:timeout]
231
+ @headers = opts[:headers] || {}
232
+
233
+ @headers["Accept-Encoding"] = [
234
+ @headers["Accept-Encoding"].to_s.split(","),
235
+ Array(opts[:accept_encoding])
236
+ ].flatten.compact.uniq.join(",")
237
+ @headers.delete "Accept-Encoding" if @headers["Accept-Encoding"].empty?
238
+
239
+ @timeout = opts[:timeout] || Kronk.config[:timeout]
218
240
 
219
- @uri = self.class.build_uri uri, options
241
+ @uri = self.class.build_uri uri, opts
220
242
 
221
- @proxy = options[:proxy] || {}
243
+ @proxy = opts[:proxy] || {}
222
244
  @proxy = {:host => @proxy} unless Hash === @proxy
223
245
 
224
- self.user_agent ||= options[:user_agent]
246
+ self.user_agent ||= opts[:user_agent]
225
247
 
226
- self.http_method = options[:http_method] || (@body ? "POST" : "GET")
248
+ self.http_method = opts[:http_method] || (@body ? "POST" : "GET")
227
249
 
228
- self.use_cookies = options.has_key?(:no_cookies) ?
229
- !options[:no_cookies] : Kronk.config[:use_cookies]
250
+ self.use_cookies = opts.has_key?(:no_cookies) ?
251
+ !opts[:no_cookies] : Kronk.config[:use_cookies]
230
252
  end
231
253
 
232
254
 
@@ -247,59 +269,33 @@ class Kronk
247
269
 
248
270
 
249
271
  ##
250
- # Assigns the cookie string.
251
-
252
- def cookie= cookie_str
253
- @headers['Cookie'] = cookie_str if @use_cookies
254
- end
255
-
272
+ # Reference to the HTTP connection instance.
256
273
 
257
- ##
258
- # Assigns the http method.
259
-
260
- def http_method= new_verb
261
- @http_method = new_verb.to_s.upcase
262
- end
263
-
264
-
265
- ##
266
- # Returns the HTTP request object.
267
-
268
- def http_request
269
- req = VanillaRequest.new @http_method, @uri.request_uri, @headers
274
+ def connection
275
+ return @connection if @connection
276
+ http_class = http_proxy @proxy[:host], @proxy
270
277
 
271
- req.basic_auth @auth[:username], @auth[:password] if
272
- @auth && @auth[:username]
278
+ @connection = http_class.new @uri.host, @uri.port
279
+ @connection.open_timeout = @connection.read_timeout = @timeout if @timeout
280
+ @connection.use_ssl = true if @uri.scheme =~ /^https$/
273
281
 
274
- req
282
+ @connection
275
283
  end
276
284
 
277
285
 
278
286
  ##
279
- # Assign the use of a proxy.
280
- # The proxy_opts arg can be a uri String or a Hash with the :address key
281
- # and optional :username and :password keys.
282
-
283
- def http_proxy addr, opts={}
284
- return Net::HTTP unless addr
285
-
286
- host, port = addr.split ":"
287
- port ||= opts[:port] || 8080
288
-
289
- user = opts[:username]
290
- pass = opts[:password]
291
-
292
- Kronk::Cmd.verbose "Using proxy #{addr}\n" if host
287
+ # Assigns the cookie string.
293
288
 
294
- Net::HTTP::Proxy host, port, user, pass
289
+ def cookie= cookie_str
290
+ @headers['Cookie'] = cookie_str if @use_cookies
295
291
  end
296
292
 
297
293
 
298
294
  ##
299
- # Assign the uri and io based on if the uri is a file, io, or url.
295
+ # Assigns the http method.
300
296
 
301
- def uri= new_uri
302
- @uri = self.class.build_uri new_uri
297
+ def http_method= new_verb
298
+ @http_method = new_verb.to_s.upcase
303
299
  end
304
300
 
305
301
 
@@ -354,41 +350,73 @@ class Kronk
354
350
 
355
351
 
356
352
  ##
357
- # Retrieve this requests' response.
358
-
359
- def retrieve
360
- http_class = http_proxy @proxy[:host], @proxy
361
-
362
- @_req = http_class.new @uri.host, @uri.port
363
-
364
- @_req.read_timeout = @timeout if @timeout
365
- @_req.use_ssl = true if @uri.scheme =~ /^https$/
366
-
367
- elapsed_time = nil
368
- socket = nil
369
- socket_io = nil
353
+ # Retrieve this requests' response. Returns a Kronk::Response once the
354
+ # full HTTP response has been read. If a block is given, will yield
355
+ # the response and body chunks as they get received.
356
+ #
357
+ # Note: Block will yield the full body if the response is compressed
358
+ # using Deflate as the Deflate format does not support streaming.
359
+ #
360
+ # Options are passed directly to the Kronk::Response constructor.
370
361
 
371
- @_res = @_req.start do |http|
372
- socket = http.instance_variable_get "@socket"
373
- socket.debug_output = socket_io = StringIO.new
362
+ def retrieve opts={}, &block
363
+ start_time = nil
364
+ opts = opts.merge :request => self
374
365
 
366
+ @response = connection.start do |http|
375
367
  start_time = Time.now
376
- res = http.request self.http_request, @body
377
- elapsed_time = Time.now - start_time
378
-
368
+ res = http.request http_request, @body, opts, &block
369
+ res.body # make sure to read the full body from io
370
+ res.time = Time.now - start_time
371
+ res.request = self
379
372
  res
380
373
  end
381
374
 
382
- Kronk.cookie_jar.set_cookies_from_headers @uri.to_s, @_res.to_hash if
383
- self.use_cookies
375
+ @response
376
+ end
377
+
384
378
 
385
- @response = Response.new socket_io, @_res, self
386
- @response.time = elapsed_time
379
+ ##
380
+ # Retrieve this requests' response but only reads HTTP headers before
381
+ # returning and leaves the connection open.
382
+ #
383
+ # Options are passed directly to the Kronk::Response constructor.
384
+ #
385
+ # Connection must be closed using:
386
+ # request.connection.finish
387
+
388
+ def stream opts={}
389
+ opts = opts.merge :request => self
390
+ http = connection.started? ? connection : connection.start
391
+ @response = http.request http_request, @body, opts
392
+ @response.request = self
387
393
 
388
394
  @response
389
395
  end
390
396
 
391
397
 
398
+ ##
399
+ # Returns this Request instance as an options hash.
400
+
401
+ def to_hash
402
+ hash = {
403
+ :host => "#{@uri.scheme}://#{@uri.host}:#{@uri.port}",
404
+ :uri_suffix => @uri.request_uri,
405
+ :user_agent => self.user_agent,
406
+ :timeout => @timeout,
407
+ :http_method => self.http_method,
408
+ :no_cookies => !self.use_cookies
409
+ }
410
+
411
+ hash[:auth] = @auth if @auth
412
+ hash[:data] = @body if @body
413
+ hash[:headers] = @headers unless @headers.empty?
414
+ hash[:proxy] = @proxy unless @proxy.empty?
415
+
416
+ hash
417
+ end
418
+
419
+
392
420
  ##
393
421
  # Returns the raw HTTP request String.
394
422
 
@@ -396,7 +424,7 @@ class Kronk
396
424
  out = "#{@http_method} #{@uri.request_uri} HTTP/1.1\r\n"
397
425
  out << "host: #{@uri.host}:#{@uri.port}\r\n"
398
426
 
399
- self.http_request.each do |name, value|
427
+ http_request.each do |name, value|
400
428
  out << "#{name}: #{value}\r\n" unless name =~ /host/i
401
429
  end
402
430
 
@@ -413,6 +441,40 @@ class Kronk
413
441
  end
414
442
 
415
443
 
444
+ ##
445
+ # Returns the HTTP request object.
446
+
447
+ def http_request
448
+ req = VanillaRequest.new @http_method, @uri.request_uri, @headers
449
+
450
+ req.basic_auth @auth[:username], @auth[:password] if
451
+ @auth && @auth[:username]
452
+
453
+ req
454
+ end
455
+
456
+
457
+ ##
458
+ # Assign the use of a proxy.
459
+ # The proxy_opts arg can be a uri String or a Hash with the :address key
460
+ # and optional :username and :password keys.
461
+
462
+ def http_proxy addr, opts={}
463
+ return Kronk::HTTP unless addr
464
+
465
+ host, port = addr.split ":"
466
+ port ||= opts[:port] || 8080
467
+
468
+ user = opts[:username]
469
+ pass = opts[:password]
470
+
471
+ Kronk::Cmd.verbose "Using proxy #{addr}\n" if host
472
+
473
+ Kronk::HTTP::Proxy host, port, user, pass
474
+ end
475
+
476
+
477
+
416
478
  ##
417
479
  # Allow any http method to be sent
418
480
 
@@ -1,25 +1,12 @@
1
1
  class Kronk
2
2
 
3
- ##
4
- # Mock File IO to allow rewinding on Windows platforms.
5
-
6
- class WinFileIO < StringIO
7
- attr_accessor :path
8
-
9
- def initialize path, str=""
10
- @path = path
11
- super str
12
- end
13
- end
14
-
15
-
16
3
  ##
17
4
  # Standard Kronk response object.
18
5
 
19
6
  class Response
20
7
 
21
- class MissingParser < Kronk::Exception; end
22
- class InvalidParser < Kronk::Exception; end
8
+ class MissingParser < Kronk::Error; end
9
+ class InvalidParser < Kronk::Error; end
23
10
 
24
11
 
25
12
  ENCODING_MATCHER = /(^|;\s?)charset=(.*?)\s*(;|$)/
@@ -27,68 +14,145 @@ class Kronk
27
14
  ##
28
15
  # Read http response from a file and return a Kronk::Response instance.
29
16
 
30
- def self.read_file path
31
- file = File.open(path, "rb")
32
- resp = new file
33
- resp.uri = path
17
+ def self.read_file path, opts={}, &block
18
+ file = File.open(path, "rb")
19
+ resp = new(file, opts)
20
+ resp.body(&block)
34
21
  file.close
35
22
 
36
23
  resp
37
24
  end
38
25
 
39
26
 
40
- attr_accessor :body, :code,
41
- :raw, :request, :stringify_opts, :time, :uri
42
-
43
- alias to_s raw
27
+ attr_reader :code, :io, :cookies, :headers
28
+ attr_accessor :read, :request, :stringify_opts, :time
44
29
 
45
30
  ##
46
31
  # Create a new Response object from a String or IO.
32
+ # Options supported are:
33
+ # :request:: The Kronk::Request instance for this response.
34
+ # :timeout:: The read timeout value in seconds.
35
+ # :no_body:: Ignore reading the body of the response.
36
+ # :force_gzip:: Force decoding body with gzip.
37
+ # :force_inflate:: Force decoding body with inflate.
38
+ # :allow_headless:: Allow headless responses (won't raise for invalid HTTP).
47
39
 
48
- def initialize io=nil, res=nil, request=nil
49
- return unless io
50
- io = StringIO.new io if String === io
40
+ def initialize input, opts={}, &block
41
+ @request = opts[:request]
42
+ @headers = {}
43
+ @encoding = @parser = @body = @gzip = @gzip_io = nil
51
44
 
52
- if io && res
53
- @_res, debug_io = res, io
54
- else
55
- @_res, debug_io = request_from_io(io)
56
- end
45
+ @headless = false
57
46
 
58
- @headers = @encoding = @parser = nil
47
+ @stringify_opts = {}
59
48
 
49
+ @raw = ""
60
50
  @time = 0
61
51
 
62
- raw_req, raw_resp, = read_raw_from debug_io
63
- @raw = try_force_encoding raw_resp
52
+ input ||= ""
53
+ input = StringIO.new(input) if String === input
64
54
 
65
- @request = request || raw_req && Request.parse(try_force_encoding raw_req)
55
+ @io = BufferedIO === input ? input : BufferedIO.new(input)
66
56
 
67
- @body = try_force_encoding(@_res.body) if @_res.body
68
- @body ||= @raw.split("\r\n\r\n",2)[1]
57
+ @io.raw_output = @raw
58
+ @io.response = self
59
+ @io.read_timeout = opts[:timeout] if opts[:timeout]
69
60
 
70
- @code = @_res.code
61
+ allow_headless = opts.has_key?(:allow_headless) ?
62
+ opts[:allow_headless] :
63
+ headless_ok?(@io.io)
71
64
 
72
- @uri = @request.uri if @request && @request.uri
73
- @uri = URI.parse io.path if File === io
65
+ response_from_io @io, allow_headless
74
66
 
75
- @stringify_opts = {}
67
+ @cookies = []
68
+
69
+ if URI::HTTP === uri
70
+ jar = CookieJar::Jar.new
71
+ jar.set_cookies_from_headers uri, @headers
72
+
73
+ jar.to_a.each do |cookie|
74
+ @cookies << cookie.to_hash
75
+ Kronk.cookie_jar.add_cookie cookie unless opts[:no_cookies]
76
+ end
77
+ end
78
+
79
+ self.gzip = opts[:force_gzip]
80
+ self.inflate = opts[:force_inflate]
81
+ gzip?
82
+ deflated?
83
+
84
+ @read = !!opts[:no_body]
85
+ body(&block) if block_given?
76
86
  end
77
87
 
78
88
 
79
89
  ##
80
- # Accessor for the HTTPResponse instance []
90
+ # Accessor for the HTTP headers []
81
91
 
82
92
  def [] key
83
- @_res[key]
93
+ @headers[key.to_s.downcase]
84
94
  end
85
95
 
86
96
 
87
97
  ##
88
- # Accessor for the HTTPResponse instance []
98
+ # Setter for the HTTP headers []
89
99
 
90
100
  def []= key, value
91
- @_res[key] = value
101
+ @headers[key.to_s.downcase] = value
102
+ end
103
+
104
+
105
+ ##
106
+ # Returns the body of the response. Will wait for the socket to finish
107
+ # reading if the body hasn't finished loading.
108
+ #
109
+ # If a block is given and the body hasn't been read yet, will iterate
110
+ # yielding a chunk of the body as it becomes available.
111
+ #
112
+ # Note: Block will yield the full body if the response is compressed
113
+ # using Deflate as the Deflate format does not support streaming.
114
+ #
115
+ # resp = Kronk::Response.new io
116
+ # resp.body do |chunk|
117
+ # # handle stream
118
+ # end
119
+
120
+ def body &block
121
+ return @body if @read
122
+
123
+ raise IOError, 'Socket closed.' if @io.closed?
124
+
125
+ error = false
126
+ last_pos = 0
127
+
128
+ begin
129
+ read_body do |chunk|
130
+ chunk = unzip chunk if gzip?
131
+
132
+ try_force_encoding chunk
133
+ (@body ||= "") << chunk
134
+ yield chunk if block_given? && !deflated?
135
+ end
136
+
137
+ rescue IOError, EOFError => e
138
+ error = e
139
+ last_pos = @body.to_s.size
140
+
141
+ @io.read_all
142
+ @body = headless? ? @raw : @raw.split("\r\n\r\n", 2)[1]
143
+ @body = unzip @body, true if gzip?
144
+ end
145
+
146
+ @body = Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(@body) if deflated?
147
+
148
+ try_force_encoding @raw unless gzip? || deflated?
149
+ try_force_encoding @body
150
+
151
+ @read = true
152
+
153
+ yield @body[last_pos..-1] if block_given? && (deflated? || error)
154
+
155
+ @body
92
156
  end
93
157
 
94
158
 
@@ -97,16 +161,72 @@ class Kronk
97
161
  # including headers.
98
162
 
99
163
  def byterate
100
- return 0 unless @raw && @time.to_f > 0
164
+ return 0 unless raw && @time.to_f > 0
101
165
  @byterate = self.total_bytes / @time.to_f
102
166
  end
103
167
 
104
168
 
105
169
  ##
106
- # Size of the body in bytes.
170
+ # Size of the raw body in bytes.
107
171
 
108
172
  def bytes
109
- (headers["content-length"] || @body.bytes.count).to_i
173
+ (headers["content-length"] || self.raw_body.bytes.count).to_i
174
+ end
175
+
176
+
177
+ ##
178
+ # Is this a chunked streaming response?
179
+
180
+ def chunked?
181
+ return false unless @headers['transfer-encoding']
182
+ field = @headers['transfer-encoding']
183
+ (/(?:\A|[^\-\w])chunked(?![\-\w])/i =~ field) ? true : false
184
+ end
185
+
186
+
187
+ ##
188
+ # Get the content length header.
189
+
190
+ def content_length
191
+ return nil unless @headers.has_key?('content-length')
192
+ len = @headers['content-length'].slice(/\d+/) or
193
+ raise HTTPHeaderSyntaxError, 'wrong Content-Length format'
194
+ len.to_i
195
+ end
196
+
197
+
198
+ ##
199
+ # Assign the expected content length.
200
+
201
+ def content_length= len
202
+ unless len
203
+ @headers.delete 'content-length'
204
+ return nil
205
+ end
206
+ @headers['content-length'] = len.to_i.to_s
207
+ end
208
+
209
+
210
+ ##
211
+ # Returns a Range object which represents the value of the Content-Range:
212
+ # header field.
213
+ # For a partial entity body, this indicates where this fragment
214
+ # fits inside the full entity body, as range of byte offsets.
215
+
216
+ def content_range
217
+ return nil unless @headers['content-range']
218
+ m = %r<bytes\s+(\d+)-(\d+)/(\d+|\*)>i.match(@headers['content-range']) or
219
+ raise HTTPHeaderSyntaxError, 'wrong Content-Range format'
220
+ m[1].to_i .. m[2].to_i
221
+ end
222
+
223
+
224
+ ##
225
+ # The length of the range represented in Content-Range: header.
226
+
227
+ def range_length
228
+ r = content_range() or return nil
229
+ r.end - r.begin + 1
110
230
  end
111
231
 
112
232
 
@@ -114,7 +234,25 @@ class Kronk
114
234
  # Cookie header accessor.
115
235
 
116
236
  def cookie
117
- @_res['Cookie']
237
+ headers['cookie']
238
+ end
239
+
240
+
241
+ ##
242
+ # Check if content encoding is deflated.
243
+
244
+ def deflated?
245
+ return !gzip? && @use_inflate unless @use_inflate.nil?
246
+ @use_inflate = headers["content-encoding"] == "deflate" if
247
+ headers["content-encoding"]
248
+ end
249
+
250
+
251
+ ##
252
+ # Force the use of inflate.
253
+
254
+ def inflate= value
255
+ @use_inflate = value
118
256
  end
119
257
 
120
258
 
@@ -139,31 +277,29 @@ class Kronk
139
277
  def force_encoding new_encoding
140
278
  new_encoding = Encoding.find new_encoding unless Encoding === new_encoding
141
279
  @encoding = new_encoding
142
- try_force_encoding @body
280
+ try_force_encoding self.body
143
281
  try_force_encoding @raw
144
282
  @encoding
145
283
  end
146
284
 
147
285
 
148
- ##
149
- # Accessor for downcased headers.
150
-
151
- def headers
152
- return @headers if @headers
153
- @headers = @_res.to_hash.dup
154
- @headers.keys.each{|h| @headers[h] = @headers[h].join(", ")}
155
- @headers
156
- end
157
-
158
286
  alias to_hash headers
159
287
 
160
288
 
161
289
  ##
162
290
  # If there was an error parsing the input as a standard http response,
163
- # the input is assumed to be a body and HeadlessResponse is used.
291
+ # the input is assumed to be a body.
164
292
 
165
293
  def headless?
166
- HeadlessResponse === @_res
294
+ @headless
295
+ end
296
+
297
+
298
+ ##
299
+ # The version of the HTTP protocol returned.
300
+
301
+ def http_version
302
+ @http_version
167
303
  end
168
304
 
169
305
 
@@ -171,9 +307,32 @@ class Kronk
171
307
  # Ruby inspect.
172
308
 
173
309
  def inspect
174
- "#<#{self.class}:#{@code} #{self['Content-Type']} #{total_bytes}bytes>"
310
+ content_type = headers['content-type'] || "text/html"
311
+ "#<#{self.class}:#{@code} #{content_type} #{total_bytes}bytes>"
312
+ end
313
+
314
+
315
+ ##
316
+ # Check if connection should be closed or not.
317
+
318
+ def close?
319
+ @headers['connection'].to_s.include?('close') ||
320
+ @headers['proxy-connection'].to_s.include?('close')
175
321
  end
176
322
 
323
+ alias connection_close? close?
324
+
325
+
326
+ ##
327
+ # Check if connection should stay alive.
328
+
329
+ def keep_alive?
330
+ @headers['connection'].to_s.include?('keep-alive') ||
331
+ @headers['proxy-connection'].to_s.include?('keep-alive')
332
+ end
333
+
334
+ alias connection_keep_alive? keep_alive?
335
+
177
336
 
178
337
  ##
179
338
  # Returns the body data parsed according to the content type.
@@ -181,6 +340,7 @@ class Kronk
181
340
  # the Content-Type, or will return the cached parsed body if available.
182
341
 
183
342
  def parsed_body new_parser=nil
343
+ return unless body
184
344
  @parsed_body ||= nil
185
345
 
186
346
  return @parsed_body if @parsed_body && !new_parser
@@ -195,16 +355,16 @@ class Kronk
195
355
  end if String === new_parser
196
356
 
197
357
  raise MissingParser,
198
- "No parser for Content-Type: #{@_res['Content-Type']}" unless new_parser
358
+ "No parser for: #{@headers['content-type']}" unless new_parser
199
359
 
200
360
  begin
201
361
  @parsed_body = new_parser.parse(self.body) or raise RuntimeError
202
362
 
203
- rescue RuntimeError, ::Exception => e
363
+ rescue => e
204
364
  msg = ParserError === e ?
205
365
  e.message : "#{new_parser} failed parsing body"
206
366
 
207
- msg << " returned by #{@uri}" if @uri
367
+ msg << " returned by #{uri}" if uri
208
368
  raise ParserError, msg
209
369
  end
210
370
  end
@@ -215,8 +375,10 @@ class Kronk
215
375
 
216
376
  def parsed_header include_headers=true
217
377
  out_headers = headers.dup
218
- out_headers['status'] = @code
219
- out_headers['http-version'] = @_res.http_version
378
+ out_headers['status'] = @code
379
+ out_headers['http-version'] = http_version
380
+ out_headers['set-cookie'] &&= @cookies.select{|c| c['version'].nil? }
381
+ out_headers['set-cookie2'] &&= @cookies.select{|c| c['version'] == 1 }
220
382
 
221
383
  case include_headers
222
384
  when nil, false
@@ -256,18 +418,34 @@ class Kronk
256
418
  end
257
419
 
258
420
 
421
+ ##
422
+ # Returns the full raw HTTP response string after the full response
423
+ # has been read.
424
+
425
+ def raw
426
+ body
427
+ @raw
428
+ end
429
+
430
+
431
+ ##
432
+ # Returns the body portion of the raw http response.
433
+
434
+ def raw_body
435
+ headless? ? raw : raw.split("\r\n\r\n", 2)[1]
436
+ end
437
+
438
+
259
439
  ##
260
440
  # Returns the header portion of the raw http response.
261
441
 
262
- def raw_header include_headers=true
442
+ def raw_header show=true
443
+ return if !show || headless?
263
444
  headers = "#{@raw.split("\r\n\r\n", 2)[0]}\r\n"
264
445
 
265
- case include_headers
266
- when nil, false
267
- nil
268
-
446
+ case show
269
447
  when Array, String
270
- includes = [*include_headers].join("|")
448
+ includes = [*show].join("|")
271
449
  headers.scan(%r{^((?:#{includes}): [^\n]*\n)}im).flatten.join
272
450
 
273
451
  when true
@@ -276,13 +454,30 @@ class Kronk
276
454
  end
277
455
 
278
456
 
457
+ ##
458
+ # Maximum time to wait on IO.
459
+
460
+ def read_timeout
461
+ @io.read_timeout
462
+ end
463
+
464
+
465
+ ##
466
+ # Assign maximum time to wait for IO data.
467
+
468
+ def read_timeout= val
469
+ @io.read_timeout = val
470
+ end
471
+
472
+
279
473
  ##
280
474
  # Returns the location to redirect to. Prepends request url if location
281
475
  # header is relative.
282
476
 
283
477
  def location
284
- return @_res['Location'] if !@request || !@request.uri
285
- @request.uri.merge @_res['Location']
478
+ return unless @headers['location']
479
+ return @headers['location'] if !@request || !@request.uri
480
+ @request.uri.merge @headers['location']
286
481
  end
287
482
 
288
483
 
@@ -297,28 +492,39 @@ class Kronk
297
492
  ##
298
493
  # Follow the redirect and return a new Response instance.
299
494
  # Returns nil if not redirect-able.
495
+ # Supports all Request#new options, plus:
496
+ # :trust_location:: Forwards HTTP auth to different host when true.
300
497
 
301
- def follow_redirect opts={}
498
+ def follow_redirect opts={}, &block
302
499
  return if !redirect?
303
- Request.new(self.location, opts).retrieve
500
+ new_opts = @request ? @request.to_hash : {}
501
+ new_opts[:http_method] = "GET" if @code == "303"
502
+ new_opts.merge!(opts)
503
+ new_opts.delete(:auth) if !opts[:trust_location] &&
504
+ (!@request || self.location.host != self.uri.host)
505
+
506
+ Request.new(self.location, new_opts).retrieve(new_opts, &block)
304
507
  end
305
508
 
306
509
 
307
510
  ##
308
511
  # Returns the raw response with selective headers and/or the body of
309
512
  # the response. Supports the following options:
310
- # :no_body:: Bool - Don't return the body; default nil
311
- # :show_headers:: Bool/String/Array - Return headers; default nil
513
+ # :body:: Bool - Return the body; default true
514
+ # :headers:: Bool/String/Array - Return headers; default true
515
+
516
+ def to_s opts={}
517
+ return raw if opts[:raw] &&
518
+ (opts[:headers].nil? || opts[:headers] == true)
312
519
 
313
- def selective_string options={}
314
- str = @body unless options[:no_body]
520
+ str = opts[:raw] ? self.raw_body : self.body unless opts[:body] == false
315
521
 
316
- if options[:show_headers]
317
- header = raw_header(options[:show_headers])
318
- str = [header, str].compact.join "\r\n"
522
+ if opts[:headers] || opts[:headers].nil?
523
+ hstr = raw_header(opts[:headers] || true)
524
+ str = [hstr, str].compact.join "\r\n"
319
525
  end
320
526
 
321
- str
527
+ str.to_s
322
528
  end
323
529
 
324
530
 
@@ -335,32 +541,32 @@ class Kronk
335
541
  # :only_data:: String/Array - Extracts the data from given data paths
336
542
  #
337
543
  # Example:
338
- # response.selective_data :transform => [:delete, ["foo/0", "bar/1"]]
339
- # response.selective_data do |trans|
544
+ # response.data :transform => [:delete, ["foo/0", "bar/1"]]
545
+ # response.data do |trans|
340
546
  # trans.delete "foo/0", "bar/1"
341
547
  # end
342
548
  #
343
549
  # See Kronk::Path::Transaction for supported transform actions.
344
550
 
345
- def selective_data options={}
551
+ def data opts={}
346
552
  data = nil
347
553
 
348
- unless options[:no_body]
349
- data = parsed_body options[:parser]
554
+ unless opts[:no_body]
555
+ data = parsed_body opts[:parser]
350
556
  end
351
557
 
352
- if options[:show_headers]
353
- header_data = parsed_header(options[:show_headers])
558
+ if opts[:show_headers]
559
+ header_data = parsed_header(opts[:show_headers])
354
560
  data &&= [header_data, data]
355
561
  data ||= header_data
356
562
  end
357
563
 
358
- Path::Transaction.run data, options do |t|
564
+ Path::Transaction.run data, opts do |t|
359
565
  # Backward compatibility support
360
- t.select(*options[:only_data]) if options[:only_data]
361
- t.delete(*options[:ignore_data]) if options[:ignore_data]
566
+ t.select(*opts[:only_data]) if opts[:only_data]
567
+ t.delete(*opts[:ignore_data]) if opts[:ignore_data]
362
568
 
363
- t.actions.concat options[:transform] if options[:transform]
569
+ t.actions.concat opts[:transform] if opts[:transform]
364
570
 
365
571
  yield t if block_given?
366
572
  end
@@ -380,45 +586,52 @@ class Kronk
380
586
  # :show_headers:: Boolean/String/Array - defines which headers to include
381
587
  #
382
588
  # If block is given, yields a Kronk::Path::Transaction instance to make
383
- # transformations on the data. See Kronk::Response#selective_data
589
+ # transformations on the data. See Kronk::Response#data
384
590
 
385
- def stringify options={}, &block
386
- options = options.empty? ? @stringify_opts : merge_stringify_opts(options)
591
+ def stringify opts={}, &block
592
+ opts = merge_stringify_opts opts
593
+
594
+ if !opts[:raw] && (opts[:parser] || parser || opts[:no_body])
595
+ data = self.data opts, &block
596
+ DataString.new data, opts
387
597
 
388
- if !options[:raw] && (options[:parser] || parser || options[:no_body])
389
- data = selective_data options, &block
390
- DataString.new data, options
391
598
  else
392
- selective_string options
599
+ self.to_s :body => !opts[:no_body],
600
+ :headers => (opts[:show_headers] || false),
601
+ :raw => opts[:raw]
393
602
  end
394
603
 
395
604
  rescue MissingParser
396
- Cmd.verbose "Warning: No parser for #{@_res['Content-Type']} [#{@uri}]"
397
- selective_string options
605
+ Cmd.verbose "Warning: No parser for #{@headers['content-type']} [#{uri}]"
606
+ self.to_s :body => !opts[:no_body],
607
+ :headers => (opts[:show_headers] || false),
608
+ :raw => opts[:raw]
398
609
  end
399
610
 
400
611
 
401
- def merge_stringify_opts options # :nodoc:
402
- options = options.dup
612
+ def merge_stringify_opts opts # :nodoc:
613
+ return @stringify_opts if opts.empty?
614
+
615
+ opts = opts.dup
403
616
  @stringify_opts.each do |key, val|
404
617
  case key
405
618
  # Response headers - Boolean, String, or Array
406
619
  when :show_headers
407
- next if options.has_key?(key) &&
408
- (options[key].class != Array || val == true || val == false)
620
+ next if opts.has_key?(key) &&
621
+ (opts[key].class != Array || val == true || val == false)
409
622
 
410
- options[key] = (val == true || val == false) ? val :
411
- [*options[key]] | [*val]
623
+ opts[key] = (val == true || val == false) ? val :
624
+ [*opts[key]] | [*val]
412
625
 
413
626
  # String or Array
414
627
  when :only_data, :ignore_data
415
- options[key] = [*options[key]] | [*val]
628
+ opts[key] = [*opts[key]] | [*val]
416
629
 
417
630
  else
418
- options[key] = val if options[key].nil?
631
+ opts[key] = val if opts[key].nil?
419
632
  end
420
633
  end
421
- options
634
+ opts
422
635
  end
423
636
 
424
637
 
@@ -430,144 +643,249 @@ class Kronk
430
643
  end
431
644
 
432
645
 
646
+ ##
647
+ # Check if the Response body has been read.
648
+
649
+ def read?
650
+ @read
651
+ end
652
+
653
+
433
654
  ##
434
655
  # Number of bytes of the response including the header.
435
656
 
436
657
  def total_bytes
437
- self.raw.bytes.count
658
+ return raw.bytes.count if @read
659
+ return raw_header.bytes.count unless body_permitted?
660
+ raw_header.bytes.count + (content_length || range_length).to_i + 2
438
661
  end
439
662
 
440
663
 
441
- private
664
+ ##
665
+ # The URI of the request if or the file read if available.
666
+
667
+ def uri
668
+ @request && @request.uri || File === @io.io && URI.parse(@io.io.path)
669
+ end
442
670
 
443
671
 
444
672
  ##
445
- # Creates a Net::HTTPRequest instance from an IO instance.
673
+ # Require the use of gzip for reading the body.
446
674
 
447
- def request_from_io resp_io
448
- # On windows, read the full file and insert contents into
449
- # a StringIO to avoid failures with IO#read_nonblock
450
- if Kronk::Cmd.windows? && File === resp_io
451
- resp_io = WinFileIO.new resp_io.path, io.read
452
- end
675
+ def gzip= value
676
+ @use_gzip = value
677
+ end
453
678
 
454
- io = Net::BufferedIO === resp_io ? resp_io : Net::BufferedIO.new(resp_io)
455
- io.debug_output = debug_io = StringIO.new
456
679
 
457
- begin
458
- resp = Net::HTTPResponse.read_new io
459
- resp.reading_body io, true do;end
460
-
461
- rescue Net::HTTPBadResponse
462
- ext = "text/html"
463
- ext = File.extname(resp_io.path)[1..-1] if
464
- WinFileIO === resp_io || File === resp_io
465
-
466
- resp_io.rewind
467
- resp = HeadlessResponse.new resp_io.read, ext
468
-
469
- rescue EOFError
470
- # If no response was read because it's too short
471
- unless resp
472
- resp_io.rewind
473
- resp = HeadlessResponse.new resp_io.read, "html"
474
- end
475
- end
680
+ ##
681
+ # Check if gzip should be used.
476
682
 
477
- resp.instance_eval do
478
- @socket ||= true
479
- @read ||= true
480
- end
683
+ def gzip?
684
+ return @use_gzip unless @use_gzip.nil?
685
+ @use_gzip = headers["content-encoding"] == "gzip" if
686
+ headers["content-encoding"]
687
+ end
688
+
689
+
690
+ private
691
+
692
+
693
+ ##
694
+ # Check if the response should have a body or not.
695
+
696
+ def body_permitted?
697
+ Net::HTTPResponse::CODE_TO_OBJ[@code].const_get(:HAS_BODY) rescue true
698
+ end
699
+
700
+
701
+ ##
702
+ # Check if a headless response is allowable based on the IO given
703
+ # to the constructor.
481
704
 
482
- [resp, debug_io]
705
+ def headless_ok? io
706
+ File === io || String === io || StringIO === io
483
707
  end
484
708
 
485
709
 
486
710
  ##
487
- # Read the raw response from a debug_output instance and return an array
488
- # containing the raw request, response, and number of bytes received.
711
+ # Get response status and headers from BufferedIO instance.
489
712
 
490
- def read_raw_from debug_io
491
- req = nil
492
- resp = ""
493
- bytes = nil
713
+ def response_from_io buff_io, allow_headless=false
714
+ begin
715
+ @http_version, @code, @msg = read_status_line buff_io
716
+ @headers = read_headers buff_io
494
717
 
495
- debug_io.rewind
496
- output = debug_io.read.split "\n"
718
+ rescue EOFError, Kronk::HTTPBadResponse
719
+ raise unless allow_headless
720
+ @http_version, @code, @msg = ["1.0", "200", "OK"]
497
721
 
498
- if output.first =~ %r{<-\s(.*)}
499
- req = instance_eval $1
500
- output.delete_at 0
501
- end
722
+ ext = File === buff_io.io ?
723
+ File.extname(buff_io.io.path)[1..-1] : "html"
502
724
 
503
- if output.last =~ %r{read (\d+) bytes}
504
- bytes = $1.to_i
505
- output.delete_at(-1)
506
- end
725
+ encoding = buff_io.io.respond_to?(:external_encoding) ?
726
+ buff_io.io.external_encoding : "UTF-8"
727
+ @headers = {
728
+ 'content-type' => "text/#{ext}; charset=#{encoding}",
729
+ }
507
730
 
508
- output.map do |line|
509
- next unless line[0..2] == "-> "
510
- resp << instance_eval(line[2..-1])
731
+ @headless = true
511
732
  end
512
733
 
513
- [req, resp, bytes]
734
+ @read = true unless body_permitted?
514
735
  end
515
736
 
516
737
 
517
738
  ##
518
- # Assigns self.encoding to the passed string if
519
- # it responds to 'force_encoding'.
520
- # Returns the string given with the new encoding.
739
+ # Read the body from IO.
521
740
 
522
- def try_force_encoding str
523
- str.force_encoding encoding if str.respond_to? :force_encoding
524
- str
741
+ def read_body target=nil
742
+ block = lambda do |str|
743
+ if block_given?
744
+ yield str
745
+ else
746
+ target << str
747
+ end
748
+ end
749
+
750
+ dest = Net::ReadAdapter.new block
751
+
752
+ if chunked?
753
+ read_chunked dest
754
+ return
755
+ end
756
+ clen = content_length()
757
+ if clen
758
+ @io.read clen, dest, true # ignore EOF
759
+ return
760
+ end
761
+ clen = range_length()
762
+ if clen
763
+ @io.read clen, dest
764
+ return
765
+ end
766
+ @io.read_all dest
525
767
  end
526
- end
527
768
 
528
769
 
529
- ##
530
- # Mock response object without a header for body-only http responses.
770
+ ##
771
+ # Unzip a chunk of the body being read.
772
+
773
+ def unzip str, force_new=false
774
+ return str if str.empty?
531
775
 
532
- class HeadlessResponse
776
+ @gzip_io = StringIO.new if !@gzip_io || force_new
777
+ pos = @gzip_io.pos
778
+ @gzip_io << str
779
+ @gzip_io.pos = pos
533
780
 
534
- attr_accessor :body, :code
781
+ @gzip = Zlib::GzipReader.new @gzip_io if !@gzip || force_new
535
782
 
536
- def initialize body, file_ext=nil
537
- @body = body
538
- @raw = body
539
- @code = "200"
783
+ @gzip.read rescue ""
784
+ end
540
785
 
541
- encoding = body.respond_to?(:encoding) ? body.encoding : "UTF-8"
542
786
 
543
- @header = {
544
- 'Content-Type' => "text/#{file_ext}; charset=#{encoding}"
545
- }
787
+ ##
788
+ # Read a chunked response body.
789
+
790
+ def read_chunked dest
791
+ len = nil
792
+ total = 0
793
+ while true
794
+ line = @io.readline
795
+ hexlen = line.slice(/[0-9a-fA-F]+/) or
796
+ raise Kronk::HTTPBadResponse, "wrong chunk size line: #{line}"
797
+ len = hexlen.hex
798
+ break if len == 0
799
+ begin
800
+ @io.read len, dest
801
+ ensure
802
+ total += len
803
+ @io.read 2 # \r\n
804
+ end
805
+ end
806
+ until @io.readline.empty?
807
+ # none
808
+ end
546
809
  end
547
810
 
548
811
 
549
812
  ##
550
- # Interface method only. Returns nil for all but content type.
813
+ # Read the first line of the response. (Stolen from Net::HTTP)
551
814
 
552
- def [] key
553
- @header[key]
815
+ def read_status_line sock
816
+ str = sock.readline until str && !str.empty?
817
+ m = /\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)\s*(.*)\z/in.match(str) or
818
+ raise Kronk::HTTPBadResponse, "wrong status line: #{str.dump}"
819
+ m.captures
554
820
  end
555
821
 
556
- def []= key, value
557
- @header[key] = value
822
+
823
+ ##
824
+ # Read response headers. (Stolen from Net::HTTP)
825
+
826
+ def read_headers sock
827
+ res_headers = {}
828
+ key = value = nil
829
+ while true
830
+ line = sock.readuntil("\n", true).sub(/\s+\z/, '')
831
+ break if line.empty?
832
+ if line[0] == ?\s or line[0] == ?\t and value
833
+ value << ' ' unless value.empty?
834
+ value << line.strip
835
+ else
836
+ assign_header(res_headers, key, value) if key
837
+ key, value = line.strip.split(/\s*:\s*/, 2)
838
+ key = key.downcase
839
+ raise Kronk::HTTPBadResponse, 'wrong header line format' if value.nil?
840
+ end
841
+ end
842
+ assign_header(res_headers, key, value) if key
843
+ res_headers
558
844
  end
559
845
 
560
846
 
561
- ##
562
- # Interface method only. Returns empty hash.
847
+ def assign_header res_headers, key, value
848
+ res_headers[key] = Array(res_headers[key]) if res_headers[key]
849
+ Array === res_headers[key] ?
850
+ res_headers[key] << value :
851
+ res_headers[key] = value
852
+ end
563
853
 
564
- def to_hash
565
- head_out = @header.dup
566
- head_out.keys.each do |key|
567
- head_out[key.downcase] = [head_out.delete(key)]
568
- end
569
854
 
570
- head_out
855
+ ##
856
+ # Assigns self.encoding to the passed string if
857
+ # it responds to 'force_encoding'.
858
+ # Returns the string given with the new encoding.
859
+
860
+ def try_force_encoding str
861
+ str.force_encoding encoding if str.respond_to? :force_encoding
862
+ str
571
863
  end
572
864
  end
573
865
  end
866
+
867
+
868
+ class CookieJar::Cookie
869
+ def to_hash
870
+ result = {
871
+ 'name' => @name,
872
+ 'value' => @value,
873
+ 'domain' => @domain,
874
+ 'path' => @path,
875
+ }
876
+ {
877
+ 'expiry' => @expiry,
878
+ 'secure' => (true if @secure),
879
+ 'http_only' => (true if @http_only),
880
+ 'version' => (@version if version != 0),
881
+ 'comment' => @comment,
882
+ 'comment_url' => @comment_url,
883
+ 'discard' => (true if @discard),
884
+ 'ports' => @ports
885
+ }.each do |name, value|
886
+ result[name] = value if value
887
+ end
888
+
889
+ result
890
+ end
891
+ end