kronk 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,254 +1,435 @@
1
1
  class Kronk
2
2
 
3
3
  ##
4
- # Wrapper to add a few niceties to the Net::HTTPResponse class.
4
+ # Mock File IO to allow rewinding on Windows platforms.
5
5
 
6
- class Response < Net::HTTPResponse
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
+ # Wraps an http response.
17
+
18
+ class Response
7
19
 
8
20
  class MissingParser < Exception; end
9
21
 
22
+ ENCODING_MATCHER = /(^|;\s?)charset=(.*?)\s*(;|$)/
23
+
10
24
  ##
11
- # Create a new Response instance from an IO object.
25
+ # Read http response from a file and return a HTTPResponse instance.
12
26
 
13
- def self.read_new io
14
- io = Net::BufferedIO.new io unless Net::BufferedIO === io
15
- io.debug_output = socket_io = StringIO.new
27
+ def self.read_file path
28
+ Kronk::Cmd.verbose "Reading file: #{path}\n"
16
29
 
17
- begin
18
- resp = super io
19
- resp.reading_body io, true do;end
20
- rescue EOFError
30
+ file = File.open(path, "rb")
31
+ resp = new file
32
+ resp.uri = path
33
+ file.close
34
+
35
+ resp
36
+ end
37
+
38
+
39
+ attr_accessor :body, :bytes, :byterate, :code, :headers, :parser,
40
+ :raw, :request, :uri
41
+
42
+ attr_reader :encoding, :time
43
+
44
+ alias to_hash headers
45
+ alias to_s raw
46
+
47
+ ##
48
+ # Create a new Response object from a String or IO.
49
+
50
+ def initialize io=nil, res=nil, request=nil
51
+ return unless io
52
+ io = StringIO.new io if String === io
53
+
54
+ if io && res
55
+ @_res, debug_io = res, io
56
+ else
57
+ @_res, debug_io = request_from_io(io)
21
58
  end
22
59
 
23
- resp.extend Helpers
24
- resp.set_helper_attribs socket_io, true, true
60
+ @headers = @_res.to_hash
25
61
 
26
- resp
62
+ @encoding = "utf-8" unless @_res["Content-Type"]
63
+ c_type = [*@headers["content-type"]].find{|ct| ct =~ ENCODING_MATCHER}
64
+ @encoding = $2 if c_type
65
+ @encoding ||= "ASCII-8BIT"
66
+ @encoding = Encoding.find(@encoding) if defined?(Encoding)
67
+
68
+ raw_req, raw_resp, bytes = read_raw_from debug_io
69
+ @raw = try_force_encoding raw_resp
70
+
71
+ @request = request ||
72
+ raw_req = try_force_encoding(raw_req) &&
73
+ Request.parse(raw_req)
74
+
75
+ @time = 0
76
+
77
+ @body = try_force_encoding(@_res.body) if @_res.body
78
+ @body ||= @raw.split("\r\n\r\n",2)[1]
79
+
80
+ @bytes = (@_res['Content-Length'] || @body.bytes.count).to_i
81
+
82
+ @code = @_res.code
83
+
84
+ @parser = Kronk.parser_for @_res['Content-Type']
85
+
86
+ @uri = @request.uri if @request && @request.uri
87
+
88
+ @byterate = 0
27
89
  end
28
90
 
29
91
 
92
+ ##
93
+ # Accessor for the HTTPResponse instance []
94
+
95
+ def [] key
96
+ @_res[key]
97
+ end
98
+
30
99
 
31
100
  ##
32
- # Helper methods for Net::HTTPResponse objects.
101
+ # Accessor for the HTTPResponse instance []
33
102
 
34
- module Helpers
103
+ def []= key, value
104
+ @_res[key] = value
105
+ end
35
106
 
36
- ##
37
- # Read the raw response from a debug_output instance and return an array
38
- # containing the raw request, response, and number of bytes received.
39
107
 
40
- def read_raw_from debug_io
41
- req = nil
42
- resp = ""
43
- bytes = nil
108
+ ##
109
+ # Cookie header accessor.
44
110
 
45
- debug_io.rewind
46
- output = debug_io.read.split "\n"
111
+ def cookie
112
+ @_res['Cookie']
113
+ end
47
114
 
48
- if output.first =~ %r{<-\s(.*)}
49
- req = instance_eval $1
50
- output.delete_at 0
51
- end
52
115
 
53
- if output.last =~ %r{read (\d+) bytes}
54
- bytes = $1.to_i
55
- output.delete_at(-1)
56
- end
116
+ ##
117
+ # Force the encoding of the raw response and body
118
+
119
+ def force_encoding new_encoding
120
+ new_encoding = Encoding.find new_encoding unless Encoding === new_encoding
121
+ @encoding = new_encoding
122
+ try_force_encoding @body
123
+ try_force_encoding @raw
124
+ @encoding
125
+ end
57
126
 
58
- output.map do |line|
59
- next unless line[0..2] == "-> "
60
- resp << instance_eval(line[2..-1])
61
- end
62
127
 
63
- [req, resp, bytes]
64
- end
128
+ ##
129
+ # If there was an error parsing the input as a standard http response,
130
+ # the input is assumed to be a body and HeadlessResponse is used.
131
+
132
+ def headless?
133
+ HeadlessResponse === @_res
134
+ end
135
+
65
136
 
137
+ ##
138
+ # Ruby inspect.
139
+
140
+ def inspect
141
+ "#<#{self.class}:#{self.code} #{self['Content-Type']}>"
142
+ end
66
143
 
67
- ##
68
- # Instantiates helper attributes from the debug socket io.
69
144
 
70
- def set_helper_attribs socket_io, socket=nil, body_read=nil
71
- @raw = udpate_encoding read_raw_from(socket_io)[1]
145
+ ##
146
+ # Returns the body data parsed according to the content type.
147
+ # If no parser is given will look for the default parser based on
148
+ # the Content-Type, or will return the cached parsed body if available.
72
149
 
73
- @read = body_read unless body_read.nil?
74
- @socket ||= socket
75
- @body ||= @raw.split("\r\n\r\n",2)[1]
150
+ def parsed_body parser=nil
151
+ @parsed_body ||= nil
76
152
 
77
- udpate_encoding @body
153
+ return @parsed_body if @parsed_body && !parser
78
154
 
79
- puts "#{@raw.length} - #{@body.length}" if
80
- @raw.length == 0 && @body.length != 0
81
- self
155
+ if String === parser
156
+ parser = Kronk.parser_for(parser) || Kronk.find_const(parser)
82
157
  end
83
158
 
159
+ parser ||= @parser
84
160
 
85
- ##
86
- # Returns the encoding provided in the Content-Type header or
87
- # "binary" if charset is unavailable.
88
- # Returns "utf-8" if no content type header is missing.
161
+ raise MissingParser,
162
+ "No parser for Content-Type: #{@_res['Content-Type']}" unless parser
89
163
 
90
- def encoding
91
- content_types = self.to_hash["content-type"]
164
+ @parsed_body = parser.parse self.body
165
+ end
166
+
167
+
168
+ ##
169
+ # Returns the parsed header hash.
92
170
 
93
- return "utf-8" if !content_types
171
+ def parsed_header include_headers=true
172
+ headers = @_res.to_hash.dup
94
173
 
95
- content_types.each do |c_type|
96
- return $2 if c_type =~ /(^|;\s?)charset=(.*?)\s*(;|$)/
174
+ case include_headers
175
+ when nil, false
176
+ nil
177
+
178
+ when Array, String
179
+ include_headers = [*include_headers].map{|h| h.to_s.downcase}
180
+
181
+ headers.each do |key, value|
182
+ headers.delete key unless
183
+ include_headers.include? key.to_s.downcase
97
184
  end
98
185
 
99
- "binary"
186
+ headers
187
+
188
+ when true
189
+ headers
100
190
  end
191
+ end
101
192
 
102
193
 
103
- ##
104
- # Assigns self.encoding to the passed string if
105
- # it responds to 'force_encoding'.
106
- # Returns the string given with the new encoding.
194
+ ##
195
+ # Returns the header portion of the raw http response.
107
196
 
108
- def udpate_encoding str
109
- str.force_encoding self.encoding if str.respond_to? :force_encoding
110
- str
111
- end
197
+ def raw_header include_headers=true
198
+ headers = "#{@raw.split("\r\n\r\n", 2)[0]}\r\n"
112
199
 
200
+ case include_headers
201
+ when nil, false
202
+ nil
113
203
 
114
- ##
115
- # Returns the raw http response.
204
+ when Array, String
205
+ includes = [*include_headers].join("|")
206
+ headers.scan(%r{^((?:#{includes}): [^\n]*\n)}im).flatten.join
116
207
 
117
- def raw
118
- @raw
208
+ when true
209
+ headers
119
210
  end
211
+ end
120
212
 
121
213
 
122
- ##
123
- # Returns the body data parsed according to the content type.
124
- # If no parser is given will look for the default parser based on
125
- # the Content-Type, or will return the cached parsed body if available.
214
+ ##
215
+ # Check if this is a redirect response.
126
216
 
127
- def parsed_body parser=nil
128
- @parsed_body ||= nil
217
+ def redirect?
218
+ @code.to_s =~ /^30\d$/
219
+ end
129
220
 
130
- return @parsed_body if @parsed_body && !parser
131
221
 
132
- if String === parser
133
- parser = Kronk.parser_for(parser) || Kronk.find_const(parser)
134
- end
222
+ ##
223
+ # Follow the redirect and return a new Response instance.
224
+ # Returns nil if not redirect-able.
135
225
 
136
- parser ||= Kronk.parser_for self['Content-Type']
226
+ def follow_redirect opts={}
227
+ return if !redirect?
228
+ Request.new(@_res['Location'], opts).retrieve
229
+ end
137
230
 
138
- raise MissingParser,
139
- "No parser for Content-Type: #{self['Content-Type']}" unless parser
140
231
 
141
- @parsed_body = parser.parse self.body
142
- end
232
+ ##
233
+ # Returns the raw response with selective headers and/or the body of
234
+ # the response. Supports the following options:
235
+ # :no_body:: Bool - Don't return the body; default nil
236
+ # :with_headers:: Bool/String/Array - Return headers; default nil
143
237
 
238
+ def selective_string options={}
239
+ str = @body unless options[:no_body]
144
240
 
145
- ##
146
- # Returns the parsed header hash.
241
+ if options[:with_headers]
242
+ header = raw_header(options[:with_headers])
243
+ str = [header, str].compact.join "\r\n"
244
+ end
245
+
246
+ str
247
+ end
147
248
 
148
- def parsed_header include_headers=true
149
- headers = self.to_hash.dup
150
249
 
151
- case include_headers
152
- when nil, false
153
- nil
250
+ ##
251
+ # Returns the parsed response with selective headers and/or the body of
252
+ # the response. Supports the following options:
253
+ # :no_body:: Bool - Don't return the body; default nil
254
+ # :with_headers:: Bool/String/Array - Return headers; default nil
255
+ # :parser:: Object - The parser to use for the body; default nil
256
+ # :ignore_data:: String/Array - Removes the data from given data paths
257
+ # :only_data:: String/Array - Extracts the data from given data paths
258
+
259
+ def selective_data options={}
260
+ data = nil
261
+
262
+ unless options[:no_body]
263
+ data = parsed_body options[:parser]
264
+ end
154
265
 
155
- when Array, String
156
- include_headers = [*include_headers].map{|h| h.to_s.downcase}
266
+ if options[:with_headers]
267
+ data = [parsed_header(options[:with_headers]), data].compact
268
+ end
157
269
 
158
- headers.each do |key, value|
159
- headers.delete key unless
160
- include_headers.include? key.to_s.downcase
161
- end
270
+ Path::Transaction.run data, options do |t|
271
+ t.select(*options[:only_data])
272
+ t.delete(*options[:ignore_data])
273
+ end
274
+ end
162
275
 
163
- headers
164
276
 
165
- when true
166
- headers
167
- end
277
+ ##
278
+ # Returns a String representation of the response, the response body,
279
+ # or the response headers, parsed or in raw format.
280
+ # Options supported are:
281
+ # :parser:: Object/String - the parser for the body; default nil (raw)
282
+ # :struct:: Boolean - Return data types instead of values
283
+ # :only_data:: String/Array - extracts the data from given data paths
284
+ # :ignore_data:: String/Array - defines which data points to exclude
285
+ # :raw:: Boolean - Force using the unparsed raw response
286
+ # :keep_indicies:: Boolean - indicies of modified arrays display as hashes.
287
+ # :with_headers:: Boolean/String/Array - defines which headers to include
288
+
289
+ def stringify options={}
290
+ if !options[:raw] && (options[:parser] || @parser)
291
+ data = selective_data options
292
+ Diff.ordered_data_string data, options[:struct]
293
+ else
294
+ selective_string options
168
295
  end
169
296
 
297
+ rescue MissingParser
298
+ Cmd.verbose "Warning: No parser for #{@_res['Content-Type']} [#{@uri}]"
299
+ selective_string options
300
+ end
170
301
 
171
- ##
172
- # Returns the header portion of the raw http response.
173
302
 
174
- def raw_header include_headers=true
175
- headers = "#{raw.split("\r\n\r\n", 2)[0]}\r\n"
303
+ ##
304
+ # Check if this is a 2XX response.
176
305
 
177
- case include_headers
178
- when nil, false
179
- nil
306
+ def success?
307
+ @code.to_s =~ /^2\d\d$/
308
+ end
180
309
 
181
- when Array, String
182
- includes = [*include_headers].join("|")
183
- headers.scan(%r{^((?:#{includes}): [^\n]*\n)}im).flatten.join
184
310
 
185
- when true
186
- headers
187
- end
311
+ ##
312
+ # Assign how long the request took in seconds.
313
+
314
+ def time= new_time
315
+ @time = new_time
316
+ @byterate = self.total_bytes / @time.to_f if @raw && @time > 0
317
+ @time
318
+ end
319
+
320
+
321
+ ##
322
+ # Number of bytes of the response including the header.
323
+
324
+ def total_bytes
325
+ self.raw.bytes.count
326
+ end
327
+
328
+
329
+ private
330
+
331
+
332
+ ##
333
+ # Creates a Net::HTTPRequest instance from an IO instance.
334
+
335
+ def request_from_io resp_io
336
+ # On windows, read the full file and insert contents into
337
+ # a StringIO to avoid failures with IO#read_nonblock
338
+ if Kronk::Cmd.windows? && File === resp_io
339
+ resp_io = WinFileIO.new resp_io.path, io.read
188
340
  end
189
341
 
342
+ io = Net::BufferedIO === resp_io ? resp_io : Net::BufferedIO.new(resp_io)
343
+ io.debug_output = debug_io = StringIO.new
344
+
345
+ begin
346
+ resp = Net::HTTPResponse.read_new io
347
+ resp.reading_body io, true do;end
190
348
 
191
- ##
192
- # Returns the raw response with selective headers and/or the body of
193
- # the response. Supports the following options:
194
- # :no_body:: Bool - Don't return the body; default nil
195
- # :with_headers:: Bool/String/Array - Return headers; default nil
349
+ rescue Net::HTTPBadResponse
350
+ ext = "text/html"
351
+ ext = File.extname(resp_io.path) if WinFileIO === resp_io
196
352
 
197
- def selective_string options={}
198
- str = self.body unless options[:no_body]
353
+ resp_io.rewind
354
+ resp = HeadlessResponse.new resp_io.read, ext
199
355
 
200
- if options[:with_headers]
201
- header = raw_header(options[:with_headers])
202
- str = [header, str].compact.join "\r\n"
356
+ rescue EOFError
357
+ # If no response was read because it's too short
358
+ unless resp
359
+ resp_io.rewind
360
+ resp = HeadlessResponse.new resp_io.read, "html"
203
361
  end
362
+ end
204
363
 
205
- str
364
+ resp.instance_eval do
365
+ @socket ||= true
366
+ @read ||= true
206
367
  end
207
368
 
369
+ [resp, debug_io]
370
+ end
371
+
208
372
 
209
- ##
210
- # Returns the parsed response with selective headers and/or the body of
211
- # the response. Supports the following options:
212
- # :no_body:: Bool - Don't return the body; default nil
213
- # :with_headers:: Bool/String/Array - Return headers; default nil
214
- # :parser:: Object - The parser to use for the body; default nil
215
- # :ignore_data:: String/Array - Removes the data from given data paths
216
- # :only_data:: String/Array - Extracts the data from given data paths
373
+ ##
374
+ # Read the raw response from a debug_output instance and return an array
375
+ # containing the raw request, response, and number of bytes received.
217
376
 
218
- def selective_data options={}
219
- data = nil
377
+ def read_raw_from debug_io
378
+ req = nil
379
+ resp = ""
380
+ bytes = nil
220
381
 
221
- unless options[:no_body]
222
- data = parsed_body options[:parser]
223
- end
382
+ debug_io.rewind
383
+ output = debug_io.read.split "\n"
224
384
 
225
- if options[:with_headers]
226
- data = [parsed_header(options[:with_headers]), data].compact
227
- end
385
+ if output.first =~ %r{<-\s(.*)}
386
+ req = instance_eval $1
387
+ output.delete_at 0
388
+ end
389
+
390
+ if output.last =~ %r{read (\d+) bytes}
391
+ bytes = $1.to_i
392
+ output.delete_at(-1)
393
+ end
228
394
 
229
- DataSet.new(data).fetch options
395
+ output.map do |line|
396
+ next unless line[0..2] == "-> "
397
+ resp << instance_eval(line[2..-1])
230
398
  end
399
+
400
+ [req, resp, bytes]
401
+ end
402
+
403
+
404
+ ##
405
+ # Assigns self.encoding to the passed string if
406
+ # it responds to 'force_encoding'.
407
+ # Returns the string given with the new encoding.
408
+
409
+ def try_force_encoding str
410
+ str.force_encoding @encoding if str.respond_to? :force_encoding
411
+ str
231
412
  end
232
413
  end
233
414
 
234
415
 
416
+
235
417
  ##
236
418
  # Mock response object without a header for body-only http responses.
237
419
 
238
420
  class HeadlessResponse
239
421
 
240
- include Response::Helpers
241
-
242
422
  attr_accessor :body, :code
243
423
 
244
424
  def initialize body, file_ext=nil
245
425
  @body = body
246
426
  @raw = body
427
+ @code = "200"
247
428
 
248
- encoding = body.encoding rescue "UTF-8"
429
+ encoding = body.respond_to?(:encoding) ? body.encoding : "UTF-8"
249
430
 
250
431
  @header = {
251
- 'Content-Type' => ["#{file_ext}; charset=#{encoding}"]
432
+ 'Content-Type' => ["text/#{file_ext}; charset=#{encoding}"]
252
433
  }
253
434
  end
254
435
 
@@ -269,7 +450,7 @@ class Kronk
269
450
  # Interface method only. Returns empty hash.
270
451
 
271
452
  def to_hash
272
- Hash.new
453
+ @header
273
454
  end
274
455
  end
275
456
  end