Class: Kronk::Request

Inherits:
Object
  • Object
show all
Defined in:
lib/kronk/request.rb

Overview

Performs HTTP requests and returns a Kronk::Response instance.

Defined Under Namespace

Classes: ParseError, VanillaRequest

Constant Summary collapse

REQUEST_LINE_MATCHER =

Matches the first line of an http request string or a fully qualified URL.

%r{(?:^|[\s'"])(?:([a-z]+)\s)?(?:(https?://[^/]+)(/[^\s'";]*)?|(/[^\s'";]*))}i

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(uri, opts = {}) ⇒ Request

Build an http request to the given uri and return a Response instance. Supports the following options:

:data

Hash/String - the data to pass to the http request body

:file

String - the path to a file to upload; overrides :data

:form

Hash/String - similar to :data but sets content-type header

:query

Hash/String - the data to append to the http request path

:user_agent

String - user agent string or alias; defaults to ‘kronk’

:auth

Hash - must contain :username and :password; defaults to nil

:headers

Hash - extra headers to pass to the request

:http_method

Symbol - the http method to use; defaults to :get

:proxy

Hash/String - http proxy to use; defaults to {}

:accept_encoding

Array/String - list of encodings the server can return

Note: if no http method is specified and data is given, will default to using a post request.



236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/kronk/request.rb', line 236

def initialize uri, opts={}
  @auth = opts[:auth]

  @connection = nil
  @response   = nil
  @body       = nil

  @headers = opts[:headers] || {}

  @headers["Accept-Encoding"] = [
    @headers["Accept-Encoding"].to_s.split(","),
    Array(opts[:accept_encoding])
  ].flatten.compact.uniq.join(",")
  @headers.delete "Accept-Encoding" if @headers["Accept-Encoding"].empty?

  @headers['Connection'] ||= 'Keep-Alive'

  @timeout = opts[:timeout] || Kronk.config[:timeout]

  @uri = self.class.build_uri uri, opts

  self.proxy = opts[:proxy]

  if opts[:file]
    self.body = opts[:file].respond_to?(:read) ?
                  opts[:file] : File.open(opts[:file], 'rb')

  elsif opts[:form_upload]
    self.body = build_multipart opts

  elsif opts[:form]
    self.form_data = opts[:form]

  elsif opts[:data]
    self.body = opts[:data]
  end

  self.user_agent ||= opts[:user_agent]

  self.http_method = opts[:http_method] || (@body ? "POST" : "GET")

  self.use_cookies = opts.has_key?(:no_cookies) ?
                      !opts[:no_cookies] : Kronk.config[:use_cookies]
end

Class Attribute Details

.multipart_boundaryObject

The boundary to use for multipart requests; default: AaB03x



199
200
201
# File 'lib/kronk/request.rb', line 199

def multipart_boundary
  @multipart_boundary
end

Instance Attribute Details

#bodyObject

Returns the value of attribute body.



217
218
219
# File 'lib/kronk/request.rb', line 217

def body
  @body
end

#headersObject

Returns the value of attribute headers.



215
216
217
# File 'lib/kronk/request.rb', line 215

def headers
  @headers
end

#http_methodObject

Returns the value of attribute http_method.



217
218
219
# File 'lib/kronk/request.rb', line 217

def http_method
  @http_method
end

#proxyObject

Returns the value of attribute proxy.



217
218
219
# File 'lib/kronk/request.rb', line 217

def proxy
  @proxy
end

#responseObject

Returns the value of attribute response.



215
216
217
# File 'lib/kronk/request.rb', line 215

def response
  @response
end

#timeoutObject

Returns the value of attribute timeout.



215
216
217
# File 'lib/kronk/request.rb', line 215

def timeout
  @timeout
end

#uriObject (readonly)

Returns the value of attribute uri.



217
218
219
# File 'lib/kronk/request.rb', line 217

def uri
  @uri
end

#use_cookiesObject

Returns the value of attribute use_cookies.



217
218
219
# File 'lib/kronk/request.rb', line 217

def use_cookies
  @use_cookies
end

Class Method Details

.build_query(data, param = nil, &block) ⇒ Object

Creates a query string from data.



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/kronk/request.rb', line 20

def self.build_query data, param=nil, &block
  return data.to_s unless param || Hash === data

  case data
  when Array
    out = data.map do |value|
      key = "#{param}[]"
      build_query value, key, &block
    end

    out.join "&"

  when Hash
    out = data.map do |key, value|
      key = param.nil? ? key : "#{param}[#{key}]"
      build_query value, key, &block
    end

    out.join "&"

  else
    yield param.to_s, data if block_given?
    "#{param}=#{data}"
  end
end

.build_uri(uri, opts = {}) ⇒ Object

Build the URI to use for the request from the given uri or path and options.



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/kronk/request.rb', line 51

def self.build_uri uri, opts={}
  uri ||= opts[:host]

  uri = "#{uri}#{opts[:path]}#{opts[:uri_suffix]}"
  uri = "http://#{uri}" unless uri.to_s =~ %r{^(\w+://|/)}

  uri = URI.parse uri unless URI === uri

  unless uri.host
    host = Kronk.config[:default_host]
    host = "http://#{host}" unless host.to_s =~ %r{^\w+://}
    uri  = URI.parse(host) + uri
  end

  if opts[:query]
    query = build_query opts[:query]
    uri.query = [uri.query, query].compact.join "&"
  end

  uri.path = "/" if uri.path.empty?

  uri
end

.normalize_params(params, name, v = nil) ⇒ Object

Stolen from Rack.



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/kronk/request.rb', line 153

def self.normalize_params params, name, v=nil
  name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
  k = $1 || ''
  after = $' || ''

  return if k.empty?

  if after == ""
    params[k] = v

  elsif after == "[]"
    params[k] ||= []
    raise TypeError,
      "expected Array (got #{params[k].class.name}) for param `#{k}'" unless
        params[k].is_a?(Array)

    params[k] << v

  elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
    child_key = $1
    params[k] ||= []
    raise TypeError,
      "expected Array (got #{params[k].class.name}) for param `#{k}'" unless
        params[k].is_a?(Array)

    if params[k].last.is_a?(Hash) && !params[k].last.key?(child_key)
      normalize_params(params[k].last, child_key, v)
    else
      params[k] << normalize_params({}, child_key, v)
    end

  else
    params[k] ||= {}
    raise TypeError,
      "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless
        params[k].is_a?(Hash)

    params[k] = normalize_params(params[k], after, v)
  end

  return params
end

.parse(str, opts = {}) ⇒ Object

Parses a raw HTTP request-like string into a Kronk::Request instance. Options passed are used as default values for Request#new.

Raises:



80
81
82
83
84
85
# File 'lib/kronk/request.rb', line 80

def self.parse str, opts={}
  opts = parse_to_hash str, opts
  raise ParseError unless opts

  new opts.delete(:host), opts
end

.parse_nested_query(qs, d = nil) ⇒ Object

Parses a nested query. Stolen from Rack.



137
138
139
140
141
142
143
144
145
146
147
# File 'lib/kronk/request.rb', line 137

def self.parse_nested_query qs, d=nil
  params = {}
  d ||= "&;"

  (qs || '').split(%r{[#{d}] *}n).each do |p|
    k, v = CGI.unescape(p).split('=', 2)
    normalize_params(params, k, v)
  end

  params
end

.parse_to_hash(str, opts = {}) ⇒ Object

Parses a raw HTTP request-like string into a Kronk::Request options hash. Also parses most single access log entries. Options passed are used as default values for Request#new.



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/kronk/request.rb', line 93

def self.parse_to_hash str, opts={}
  lines = str.split("\n")
  return if lines.empty?

  body_start = nil

  opts[:headers] ||= {}

  lines.shift.strip =~ REQUEST_LINE_MATCHER
  opts.merge! :http_method => $1,
              :host        => $2,
              :path        => ($3 || $4)

  lines.each_with_index do |line, i|
    case line
    when /^Host: /
      opts[:host] = line.split(": ", 2)[1].strip

    when "", "\r"
      body_start = i+1
      break

    else
      name, value = line.split(": ", 2)
      opts[:headers][name] = value.strip if value
    end
  end

  opts[:data] = lines[body_start..-1].join("\n") if body_start

  opts.delete(:host)        if !opts[:host]
  opts.delete(:path)        if !opts[:path]
  opts.delete(:headers)     if opts[:headers].empty?
  opts.delete(:http_method) if !opts[:http_method]
  opts.delete(:data)        if opts[:data] && opts[:data].strip.empty?

  return if opts.empty?
  opts
end

Instance Method Details

#authObject

Returns the basic auth credentials if available.



285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/kronk/request.rb', line 285

def auth
  @auth ||= Hash.new

  if !@auth[:username] && @headers['Authorization']
    require 'base64'
    str = Base64.decode64 @headers['Authorization'].split[1]
    username, password = str.split(":", 2)
    @auth = {:username => username, :password => password}.merge @auth
  end

  @auth
end

#connectionObject

Retrieve or create an HTTP connection instance.



333
334
335
336
337
338
339
340
341
# File 'lib/kronk/request.rb', line 333

def connection
  conn = Kronk::HTTP.new @uri.host, @uri.port,
           :proxy => self.proxy,
           :ssl   => !!(@uri.scheme =~ /^https$/)

  conn.open_timeout = conn.read_timeout = @timeout if @timeout

  conn
end

#cookie=(cookie_str) ⇒ Object

Assigns the cookie string.



347
348
349
# File 'lib/kronk/request.rb', line 347

def cookie= cookie_str
  @headers['Cookie'] = cookie_str if @use_cookies
end

#form_data=(data) ⇒ Object

Assigns body of the request with form headers.



355
356
357
358
# File 'lib/kronk/request.rb', line 355

def form_data= data
  @headers['Content-Type'] = "application/x-www-form-urlencoded"
  @body = self.class.build_query data
end

#http_requestObject

Returns the Net::HTTPRequest subclass instance.



549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
# File 'lib/kronk/request.rb', line 549

def http_request
  req = VanillaRequest.new @http_method, @uri.request_uri, @headers

  req.basic_auth @auth[:username], @auth[:password] if
    @auth && @auth[:username]

  # Stream Multipart
  if Kronk::Multipart === @body
    req.body_stream = @body.to_io

  # Stream IO
  elsif @body.respond_to?(:read)
    req.body_stream = @body

  else
    req.body = @body
  end

  b = req.body || req.body_stream

  if b.respond_to?(:bytesize)
    req['Content-Length'] = b.bytesize.to_s
  elsif b.respond_to?(:size) && b.size
    req['Content-Length'] = b.size.to_s
  elsif b.nil?
    req['Content-Length'] = "0"
  end

  req['Transfer-Encoding'] = 'chunked' if !req['Content-Length']

  req
end

#inspectObject

Ruby inspect.



541
542
543
# File 'lib/kronk/request.rb', line 541

def inspect
  "#<#{self.class}:#{self.http_method} #{self.uri}>"
end

#retrieve(opts = {}, &block) ⇒ Object

Retrieve this requests’ response. Returns a Kronk::Response once the full HTTP response has been read. If a block is given, will yield the response and body chunks as they get received.

Note: Block will yield the full body if the response is compressed using Deflate as the Deflate format does not support streaming.

Options are passed directly to the Kronk::Response constructor.



446
447
448
449
450
451
452
453
454
455
# File 'lib/kronk/request.rb', line 446

def retrieve opts={}, &block
  start_time = Time.now

  @response = stream opts

  @response.body # make sure to read the full body from io
  @response.time = Time.now - start_time - @response.conn_time

  @response
end

#ssl=(bool) ⇒ Object

Assign whether to use ssl or not.



431
432
433
# File 'lib/kronk/request.rb', line 431

def ssl= bool
  @uri.scheme = bool ? "https" : "http"
end

#ssl?Boolean

Check if this is an SSL request.

Returns:

  • (Boolean)


423
424
425
# File 'lib/kronk/request.rb', line 423

def ssl?
  @uri.scheme == "https"
end

#stream(opts = {}, &block) ⇒ Object

Retrieve this requests’ response but only reads HTTP headers before returning and leaves the connection open.

Options are passed directly to the Kronk::Response constructor.

Connection must be closed using:

request.connection.finish


467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
# File 'lib/kronk/request.rb', line 467

def stream opts={}, &block
  retried = false

  begin
    start_time = Time.now
    conn = connection
    conn.start unless conn.started?
    conn_time  = Time.now - start_time

    @response           = conn.request http_request, nil, opts, &block
    @response.conn_time = conn_time
    @response.request   = self

    @response

  rescue EOFError, Errno::EPIPE
    raise if retried
    @connection = nil
    retried = true
    retry
  end
end

#to_hashObject

Returns this Request instance as an options hash.



494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
# File 'lib/kronk/request.rb', line 494

def to_hash
  hash = {
    :host        => "#{@uri.scheme}://#{@uri.host}:#{@uri.port}",
    :path        => @uri.request_uri,
    :user_agent  => self.user_agent,
    :timeout     => @timeout,
    :http_method => self.http_method,
    :no_cookies  => !self.use_cookies
  }

  hash[:auth]    = @auth if @auth
  hash[:data]    = @body if @body
  hash[:headers] = @headers   unless @headers.empty?
  hash[:proxy]   = self.proxy unless self.proxy.empty?

  hash
end

#to_sObject

Returns the raw HTTP request String. Warning: If the body is an IO instance or a Multipart instance, the full input will be read.



518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
# File 'lib/kronk/request.rb', line 518

def to_s
  out = "#{@http_method} #{@uri.request_uri} HTTP/1.1\r\n"
  out << "host: #{@uri.host}:#{@uri.port}\r\n"

  http_request.each do |name, value|
    out << "#{name}: #{value}\r\n" unless name =~ /host/i
  end

  out << "\r\n"

  if @body.respond_to?(:read)
    out << @body.read
  elsif Kronk::Multipart === @body
    out << @body.to_io.read
  else
    out << @body.to_s
  end
end

#user_agentObject

Read the User Agent header.



415
416
417
# File 'lib/kronk/request.rb', line 415

def user_agent
  @headers['User-Agent']
end

#user_agent=(new_ua) ⇒ Object

Assign a User Agent header.



405
406
407
408
409
# File 'lib/kronk/request.rb', line 405

def user_agent= new_ua
  @headers['User-Agent'] =
    new_ua && Kronk.config[:user_agents][new_ua.to_s] ||
    new_ua || Kronk::DEFAULT_USER_AGENT
end