kronk 1.4.0 → 1.5.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.
@@ -11,6 +11,10 @@ class Kronk
11
11
  def self.parse plist
12
12
  require 'plist'
13
13
  Plist.parse_xml plist
14
+
15
+ rescue LoadError => e
16
+ raise unless e.message =~ /-- plist/
17
+ raise MissingDependency, "Please install the plist gem and try again"
14
18
  end
15
19
  end
16
20
  end
data/lib/kronk/request.rb CHANGED
@@ -1,387 +1,411 @@
1
1
  class Kronk
2
2
 
3
3
  ##
4
- # Request wrapper class for net/http.
4
+ # Performs HTTP requests or retrieves HTTP responses.
5
5
 
6
6
  class Request
7
7
 
8
- # Generic Request exception.
9
- class Exception < ::Exception; end
8
+ # Raised by Request.parse when parsing invalid http request string.
9
+ class ParseError < Kronk::Exception; end
10
10
 
11
- # Raised when the URI was not resolvable.
12
- class NotFoundError < Exception; end
13
-
14
- # Raised when HTTP times out.
15
- class TimeoutError < Exception; end
11
+ # Matches the first line of an http request string.
12
+ REQUEST_LINE_MATCHER = %r{([A-Za-z]+)?(^|[\s'"])(/[^\s'";]+)[\s"']*}
16
13
 
17
14
  ##
18
- # Follows the redirect from a 30X response object and decrease the
19
- # number of redirects left if it's an Integer.
20
-
21
- def self.follow_redirect resp, options={}
22
- Kronk::Cmd.verbose "Following redirect..."
15
+ # Creates a query string from data.
23
16
 
24
- rdir = options[:follow_redirects]
25
- rdir = rdir - 1 if Integer === rdir && rdir > 0
17
+ def self.build_query data, param=nil
18
+ return data.to_s unless param || Hash === data
26
19
 
27
- options = options.merge :follow_redirects => rdir,
28
- :http_method => :get
20
+ case data
21
+ when Array
22
+ out = data.map do |value|
23
+ key = "#{param}[]"
24
+ build_query value, key
25
+ end
29
26
 
30
- retrieve_uri resp['Location'], options
31
- end
27
+ out.join "&"
32
28
 
29
+ when Hash
30
+ out = data.map do |key, value|
31
+ key = param.nil? ? key : "#{param}[#{key}]"
32
+ build_query value, key
33
+ end
33
34
 
34
- ##
35
- # Check the rdir value to figure out if redirect should be followed.
35
+ out.join "&"
36
36
 
37
- def self.follow_redirect? resp, rdir
38
- resp.code.to_s =~ /^30\d$/ &&
39
- (rdir == true || Integer === rdir && rdir > 0)
37
+ else
38
+ "#{param}=#{data}"
39
+ end
40
40
  end
41
41
 
42
42
 
43
43
  ##
44
- # Returns the value from a url, file, or IO as a String.
45
- # Options supported are:
46
- # :data:: Hash/String - the data to pass to the http request
47
- # :query:: Hash/String - the data to append to the http request path
48
- # :follow_redirects:: Integer/Bool - number of times to follow redirects
49
- # :user_agent:: String - user agent string or alias; defaults to 'kronk'
50
- # :auth:: Hash - must contain :username and :password; defaults to nil
51
- # :headers:: Hash - extra headers to pass to the request
52
- # :http_method:: Symbol - the http method to use; defaults to :get
53
- # :proxy:: Hash/String - http proxy to use; defaults to nil
44
+ # Build the URI to use for the request from the given uri or
45
+ # path and options.
54
46
 
55
- def self.retrieve uri, options={}
56
- if IO === uri || StringIO === uri
57
- resp = retrieve_io uri, options
58
- elsif File.file? uri
59
- resp = retrieve_file uri, options
60
- else
61
- resp = retrieve_uri uri, options
62
- Kronk.history << uri
63
- end
47
+ def self.build_uri uri, options={}
48
+ uri ||= Kronk.config[:default_host]
49
+ suffix = options[:uri_suffix]
64
50
 
65
- begin
66
- File.open(options[:cache_response], "wb+") do |file|
67
- file.write resp.raw
68
- end if options[:cache_response]
69
- rescue => e
70
- $stderr << "#{e.class}: #{e.message}"
51
+ uri = "http://#{uri}" unless uri.to_s =~ %r{^(\w+://|/)}
52
+ uri = "#{uri}#{suffix}" if suffix
53
+ uri = URI.parse uri unless URI === uri
54
+ uri = URI.parse(Kronk.config[:default_host]) + uri unless uri.host
55
+
56
+ if options[:query]
57
+ query = build_query options[:query]
58
+ uri.query = [uri.query, query].compact.join "&"
71
59
  end
72
60
 
73
- resp
61
+ uri
62
+ end
63
+
64
+
65
+ ##
66
+ # Parses a raw HTTP request-like string into a Kronk::Request instance.
74
67
 
75
- rescue SocketError, Errno::ENOENT, Errno::ECONNREFUSED
76
- raise NotFoundError, "#{uri} could not be found"
68
+ def self.parse str, opts={}
69
+ opts = parse_to_hash str, opts
70
+ raise ParseError unless opts
77
71
 
78
- rescue Timeout::Error
79
- raise TimeoutError, "#{uri} took too long to respond"
72
+ new opts.delete(:host), opts
80
73
  end
81
74
 
82
75
 
83
76
  ##
84
- # Read http response from a file and return a HTTPResponse instance.
77
+ # Parses a raw HTTP request-like string into a Kronk::Request options hash.
78
+ # Also parses most single access log entries.
79
+
80
+ def self.parse_to_hash str, opts={}
81
+ lines = str.split("\n")
82
+ return if lines.empty?
85
83
 
86
- def self.retrieve_file path, options={}
87
- Kronk::Cmd.verbose "Reading file: #{path}\n"
84
+ body_start = nil
88
85
 
89
- options = options.dup
90
- resp = nil
86
+ opts[:headers] ||= {}
91
87
 
92
- File.open(path, "rb") do |file|
88
+ lines.shift.strip =~ REQUEST_LINE_MATCHER
89
+ opts[:http_method], opts[:uri_suffix] = $1, $3
93
90
 
94
- # On windows, read the full file and insert contents into
95
- # a StringIO to avoid failures with IO#read_nonblock
96
- file = StringIO.new file.read if Kronk::Cmd.windows?
91
+ lines.each_with_index do |line, i|
92
+ case line
93
+ when /^Host: /
94
+ opts[:host] = line.split(": ", 2)[1].strip
97
95
 
98
- begin
99
- resp = Response.read_new file
96
+ when "", "\r"
97
+ body_start = i+1
98
+ break
100
99
 
101
- rescue Net::HTTPBadResponse
102
- file.rewind
103
- resp = HeadlessResponse.new file.read, File.extname(path)
100
+ else
101
+ name, value = line.split(": ", 2)
102
+ opts[:headers][name] = value.strip if value
104
103
  end
105
104
  end
106
105
 
107
- resp = follow_redirect resp, options if
108
- follow_redirect? resp, options[:follow_redirects]
106
+ opts[:data] = lines[body_start..-1].join("\n") if body_start
107
+
108
+ opts.delete(:uri_suffix) if !opts[:uri_suffix]
109
+ opts.delete(:headers) if opts[:headers].empty?
110
+ opts.delete(:http_method) if !opts[:http_method]
111
+ opts.delete(:data) if opts[:data] && opts[:data].strip.empty?
109
112
 
110
- resp
113
+ return if opts.empty?
114
+ opts
111
115
  end
112
116
 
113
117
 
114
118
  ##
115
- # Read the http response from an IO instance and return a HTTPResponse.
119
+ # Parses a nested query. Stolen from Rack.
116
120
 
117
- def self.retrieve_io io, options={}
118
- Kronk::Cmd.verbose "Reading IO..."
121
+ def self.parse_nested_query qs, d=nil
122
+ params = {}
123
+ d ||= "&;"
119
124
 
120
- options = options.dup
125
+ (qs || '').split(%r{[#{d}] *}n).each do |p|
126
+ k, v = CGI.unescape(p).split('=', 2)
127
+ normalize_params(params, k, v)
128
+ end
121
129
 
122
- resp = nil
130
+ params
131
+ end
123
132
 
124
- begin
125
- resp = Response.read_new io
126
133
 
127
- rescue Net::HTTPBadResponse
128
- io.rewind
129
- resp = HeadlessResponse.new io.read
130
- end
134
+ ##
135
+ # Stolen from Rack.
131
136
 
132
- resp = follow_redirect resp, options if
133
- follow_redirect? resp, options[:follow_redirects]
137
+ def self.normalize_params params, name, v=nil
138
+ name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
139
+ k = $1 || ''
140
+ after = $' || ''
134
141
 
135
- resp
136
- end
142
+ return if k.empty?
137
143
 
144
+ if after == ""
145
+ params[k] = v
138
146
 
139
- ##
140
- # Make an http request to the given uri and return a HTTPResponse instance.
141
- # Supports the following options:
142
- # :data:: Hash/String - the data to pass to the http request
143
- # :query:: Hash/String - the data to append to the http request path
144
- # :follow_redirects:: Integer/Bool - number of times to follow redirects
145
- # :user_agent:: String - user agent string or alias; defaults to 'kronk'
146
- # :auth:: Hash - must contain :username and :password; defaults to nil
147
- # :headers:: Hash - extra headers to pass to the request
148
- # :http_method:: Symbol - the http method to use; defaults to :get
149
- # :proxy:: Hash/String - http proxy to use; defaults to nil
150
- #
151
- # Note: if no http method is specified and data is given, will default
152
- # to using a post request.
147
+ elsif after == "[]"
148
+ params[k] ||= []
149
+ raise TypeError,
150
+ "expected Array (got #{params[k].class.name}) for param `#{k}'" unless
151
+ params[k].is_a?(Array)
153
152
 
154
- def self.retrieve_uri uri, options={}
155
- options = options.dup
156
- http_method = options.delete(:http_method)
157
- http_method ||= options[:data] ? :post : :get
153
+ params[k] << v
158
154
 
159
- resp = self.call http_method, uri, options
155
+ elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
156
+ child_key = $1
157
+ params[k] ||= []
158
+ raise TypeError,
159
+ "expected Array (got #{params[k].class.name}) for param `#{k}'" unless
160
+ params[k].is_a?(Array)
160
161
 
161
- resp = follow_redirect resp, options if
162
- follow_redirect? resp, options[:follow_redirects]
162
+ if params[k].last.is_a?(Hash) && !params[k].last.key?(child_key)
163
+ normalize_params(params[k].last, child_key, v)
164
+ else
165
+ params[k] << normalize_params({}, child_key, v)
166
+ end
163
167
 
164
- resp
168
+ else
169
+ params[k] ||= {}
170
+ raise TypeError,
171
+ "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless
172
+ params[k].is_a?(Hash)
173
+
174
+ params[k] = normalize_params(params[k], after, v)
175
+ end
176
+
177
+ return params
165
178
  end
166
179
 
167
180
 
181
+ attr_accessor :body, :headers, :response, :timeout
182
+
183
+ attr_reader :http_method, :uri, :use_cookies
184
+
168
185
  ##
169
- # Make an http request to the given uri and return a HTTPResponse instance.
186
+ # Build an http request to the given uri and return a Response instance.
170
187
  # Supports the following options:
171
- # :data:: Hash/String - the data to pass to the http request body
188
+ # :data:: Hash/String - the data to pass to the http request
172
189
  # :query:: Hash/String - the data to append to the http request path
190
+ # :follow_redirects:: Integer/Bool - number of times to follow redirects
173
191
  # :user_agent:: String - user agent string or alias; defaults to 'kronk'
174
192
  # :auth:: Hash - must contain :username and :password; defaults to nil
175
193
  # :headers:: Hash - extra headers to pass to the request
176
194
  # :http_method:: Symbol - the http method to use; defaults to :get
177
195
  # :proxy:: Hash/String - http proxy to use; defaults to nil
196
+ #
197
+ # Note: if no http method is specified and data is given, will default
198
+ # to using a post request.
178
199
 
179
- def self.call http_method, uri, options={}
180
- uri = build_uri uri, options
181
-
182
- data = options[:data]
183
- data &&= build_query data
200
+ def initialize uri, options={}
201
+ @HTTP = Net::HTTP
202
+ @auth = options[:auth]
184
203
 
185
- options[:headers] ||= Hash.new
186
- options[:headers]['User-Agent'] ||= get_user_agent options[:user_agent]
204
+ @body = nil
205
+ @body = self.class.build_query options[:data] if options[:data]
187
206
 
188
- unless options[:headers]['Cookie'] || !use_cookies?(options)
189
- cookie = Kronk.cookie_jar.get_cookie_header uri.to_s
190
- options[:headers]['Cookie'] = cookie unless cookie.empty?
191
- end
207
+ @response = nil
208
+ @_req = nil
209
+ @_res = nil
192
210
 
193
- socket = socket_io = nil
211
+ @headers = options[:headers] || {}
212
+ @timeout = options[:timeout] || Kronk.config[:timeout]
194
213
 
195
- proxy_addr, proxy_opts =
196
- if Hash === options[:proxy]
197
- [options[:proxy][:address], options[:proxy]]
198
- else
199
- [options[:proxy], {}]
200
- end
214
+ @uri = self.class.build_uri uri, options
201
215
 
202
- http_class = proxy proxy_addr, proxy_opts
216
+ self.user_agent ||= options[:user_agent]
203
217
 
204
- req = http_class.new uri.host, uri.port
205
- req.use_ssl = true if uri.scheme =~ /^https$/
218
+ self.http_method = options[:http_method] || (@body ? "POST" : "GET")
206
219
 
207
- options[:timeout] ||= Kronk.config[:timeout]
208
- req.read_timeout = options[:timeout] if options[:timeout]
220
+ self.use_cookies = options.has_key?(:no_cookies) ?
221
+ !options[:no_cookies] : Kronk.config[:use_cookies]
209
222
 
210
- resp = req.start do |http|
211
- socket = http.instance_variable_get "@socket"
212
- socket.debug_output = socket_io = StringIO.new
223
+ if Hash === options[:proxy]
224
+ self.use_proxy options[:proxy][:address], options[:proxy]
225
+ else
226
+ self.use_proxy options[:proxy]
227
+ end
228
+ end
213
229
 
214
- req = VanillaRequest.new http_method.to_s.upcase,
215
- uri.request_uri, options[:headers]
216
230
 
217
- if options[:auth] && options[:auth][:username]
218
- req.basic_auth options[:auth][:username],
219
- options[:auth][:password]
220
- end
231
+ ##
232
+ # Returns the basic auth credentials if available.
221
233
 
222
- Kronk::Cmd.verbose "Retrieving URL: #{uri}\n"
234
+ def auth
235
+ @auth ||= Hash.new
223
236
 
224
- http.request req, data
237
+ if !@auth[:username] && @headers['Authorization']
238
+ str = Base64.decode64 @headers['Authorization'].split[1]
239
+ username, password = str.split(":", 2)
240
+ @auth = {:username => username, :password => password}.merge @auth
225
241
  end
226
242
 
227
- Kronk.cookie_jar.set_cookies_from_headers uri.to_s, resp.to_hash if
228
- use_cookies? options
229
-
230
- resp.extend Response::Helpers
231
- resp.set_helper_attribs socket_io
232
-
233
- resp
243
+ @auth
234
244
  end
235
245
 
236
246
 
237
247
  ##
238
- # Build the URI to use for the request from the given uri or
239
- # path and options.
248
+ # Assigns the cookie string.
240
249
 
241
- def self.build_uri uri, options={}
242
- suffix = options[:uri_suffix]
243
-
244
- uri = "http://#{uri}" unless uri =~ %r{^(\w+://|/)}
245
- uri = "#{uri}#{suffix}" if suffix
246
- uri = URI.parse uri unless URI === uri
247
- uri = URI.parse(Kronk.config[:default_host]) + uri unless uri.host
248
-
249
- if options[:query]
250
- query = build_query options[:query]
251
- uri.query = [uri.query, query].compact.join "&"
252
- end
253
-
254
- uri
250
+ def cookie= cookie_str
251
+ @headers['Cookie'] = cookie_str if @use_cookies
255
252
  end
256
253
 
257
254
 
258
255
  ##
259
- # Checks if cookies should be used and set.
256
+ # Assigns the http method.
260
257
 
261
- def self.use_cookies? options
262
- return !options[:no_cookies] if options.has_key? :no_cookies
263
- Kronk.config[:use_cookies]
258
+ def http_method= new_verb
259
+ @http_method = new_verb.to_s.upcase
264
260
  end
265
261
 
266
262
 
267
263
  ##
268
- # Gets the user agent to use for the request.
264
+ # Returns the HTTP request object.
269
265
 
270
- def self.get_user_agent agent
271
- agent && Kronk.config[:user_agents][agent.to_s] || agent ||
272
- Kronk.config[:user_agents]['kronk']
266
+ def http_request
267
+ req = VanillaRequest.new @http_method, @uri.request_uri, @headers
268
+
269
+ req.basic_auth @auth[:username], @auth[:password] if
270
+ @auth && @auth[:username]
271
+
272
+ req
273
273
  end
274
274
 
275
275
 
276
276
  ##
277
- # Return proxy http class.
277
+ # Assign the use of a proxy.
278
278
  # The proxy_opts arg can be a uri String or a Hash with the :address key
279
279
  # and optional :username and :password keys.
280
280
 
281
- def self.proxy addr, proxy_opts={}
282
- return Net::HTTP unless addr
281
+ def use_proxy addr, opts={}
282
+ return @HTTP = Net::HTTP unless addr
283
283
 
284
284
  host, port = addr.split ":"
285
- port ||= proxy_opts[:port] || 8080
285
+ port ||= opts[:port] || 8080
286
286
 
287
- user = proxy_opts[:username]
288
- pass = proxy_opts[:password]
287
+ user = opts[:username]
288
+ pass = opts[:password]
289
289
 
290
290
  Kronk::Cmd.verbose "Using proxy #{addr}\n" if host
291
291
 
292
- Net::HTTP::Proxy host, port, user, pass
292
+ @HTTP = Net::HTTP::Proxy host, port, user, pass
293
293
  end
294
294
 
295
295
 
296
296
  ##
297
- # Creates a query string from data.
297
+ # Assign the uri and io based on if the uri is a file, io, or url.
298
298
 
299
- def self.build_query data, param=nil
300
- return data.to_s unless param || Hash === data
301
-
302
- case data
303
- when Array
304
- out = data.map do |value|
305
- key = "#{param}[]"
306
- build_query value, key
307
- end
299
+ def uri= new_uri
300
+ @uri = self.class.build_uri new_uri
301
+ end
308
302
 
309
- out.join "&"
310
303
 
311
- when Hash
312
- out = data.map do |key, value|
313
- key = param.nil? ? key : "#{param}[#{key}]"
314
- build_query value, key
315
- end
304
+ ##
305
+ # Decide whether to use cookies or not.
316
306
 
317
- out.join "&"
307
+ def use_cookies= bool
308
+ if bool && (!@headers['Cookie'] || @headers['Cookie'].empty?)
309
+ cookie = Kronk.cookie_jar.get_cookie_header @uri.to_s
310
+ @headers['Cookie'] = cookie unless cookie.empty?
318
311
 
319
312
  else
320
- "#{param}=#{data}"
313
+ @headers.delete 'Cookie'
321
314
  end
315
+
316
+ @use_cookies = bool
322
317
  end
323
318
 
324
319
 
325
320
  ##
326
- # Parses a nested query. Stolen from Rack.
321
+ # Assign a User Agent header.
327
322
 
328
- def self.parse_nested_query qs, d=nil
329
- params = {}
330
- d ||= "&;"
323
+ def user_agent= new_ua
324
+ @headers['User-Agent'] =
325
+ new_ua && Kronk.config[:user_agents][new_ua.to_s] ||
326
+ new_ua || Kronk.config[:user_agents]['kronk']
327
+ end
331
328
 
332
- (qs || '').split(%r{[#{d}] *}n).each do |p|
333
- k, v = CGI.unescape(p).split('=', 2)
334
- normalize_params(params, k, v)
335
- end
336
329
 
337
- params
330
+ ##
331
+ # Read the User Agent header.
332
+
333
+ def user_agent
334
+ @headers['User-Agent']
338
335
  end
339
336
 
340
337
 
341
338
  ##
342
- # Stolen from Rack.
339
+ # Check if this is an SSL request.
343
340
 
344
- def self.normalize_params params, name, v=nil
345
- name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
346
- k = $1 || ''
347
- after = $' || ''
341
+ def ssl?
342
+ @uri.scheme == "https"
343
+ end
348
344
 
349
- return if k.empty?
350
345
 
351
- if after == ""
352
- params[k] = v
346
+ ##
347
+ # Assign whether to use ssl or not.
353
348
 
354
- elsif after == "[]"
355
- params[k] ||= []
356
- raise TypeError,
357
- "expected Array (got #{params[k].class.name}) for param `#{k}'" unless
358
- params[k].is_a?(Array)
349
+ def ssl= bool
350
+ @uri.scheme = bool ? "https" : "http"
351
+ end
359
352
 
360
- params[k] << v
361
353
 
362
- elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
363
- child_key = $1
364
- params[k] ||= []
365
- raise TypeError,
366
- "expected Array (got #{params[k].class.name}) for param `#{k}'" unless
367
- params[k].is_a?(Array)
354
+ ##
355
+ # Retrieve this requests' response.
368
356
 
369
- if params[k].last.is_a?(Hash) && !params[k].last.key?(child_key)
370
- normalize_params(params[k].last, child_key, v)
371
- else
372
- params[k] << normalize_params({}, child_key, v)
373
- end
357
+ def retrieve
358
+ @_req = @HTTP.new @uri.host, @uri.port
374
359
 
375
- else
376
- params[k] ||= {}
377
- raise TypeError,
378
- "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless
379
- params[k].is_a?(Hash)
360
+ @_req.read_timeout = @timeout if @timeout
361
+ @_req.use_ssl = true if @uri.scheme =~ /^https$/
380
362
 
381
- params[k] = normalize_params(params[k], after, v)
363
+ elapsed_time = nil
364
+ socket = nil
365
+ socket_io = nil
366
+
367
+ @_res = @_req.start do |http|
368
+ socket = http.instance_variable_get "@socket"
369
+ socket.debug_output = socket_io = StringIO.new
370
+
371
+ start_time = Time.now
372
+ res = http.request self.http_request, @body
373
+ elapsed_time = Time.now - start_time
374
+
375
+ res
382
376
  end
383
377
 
384
- return params
378
+ Kronk.cookie_jar.set_cookies_from_headers @uri.to_s, @_res.to_hash if
379
+ self.use_cookies
380
+
381
+ @response = Response.new socket_io, @_res, self
382
+ @response.time = elapsed_time
383
+
384
+ @response
385
+ end
386
+
387
+
388
+ ##
389
+ # Returns the raw HTTP request String.
390
+
391
+ def to_s
392
+ out = "#{@http_method} #{@uri.request_uri} HTTP/1.1\r\n"
393
+ out << "host: #{@uri.host}:#{@uri.port}\r\n"
394
+
395
+ self.http_request.each do |name, value|
396
+ out << "#{name}: #{value}\r\n" unless name =~ /host/i
397
+ end
398
+
399
+ out << "\r\n"
400
+ out << @body.to_s
401
+ end
402
+
403
+
404
+ ##
405
+ # Ruby inspect.
406
+
407
+ def inspect
408
+ "#<#{self.class}:#{self.http_method} #{self.uri}>"
385
409
  end
386
410
 
387
411