kronk 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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