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.
@@ -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