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

:oauth

Hash - :consumer_key, :token, :consumer_secret, :token_secret

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



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
280
281
# File 'lib/kronk/request.rb', line 237

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

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



287
288
289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/kronk/request.rb', line 287

def auth
  if (!@auth || !@auth[:username]) && @headers['Authorization'] &&
      @headers['Authorization'] !~ /^OAuth\s/

    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.



353
354
355
356
357
358
359
360
361
# File 'lib/kronk/request.rb', line 353

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.



367
368
369
# File 'lib/kronk/request.rb', line 367

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

#form_data=(data) ⇒ Object

Assigns body of the request with form headers.



375
376
377
378
# File 'lib/kronk/request.rb', line 375

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.



570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
# File 'lib/kronk/request.rb', line 570

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

  if @oauth
    req['Authorization'] =
      SimpleOAuth::Header.new(@http_method, @uri, {}, self.oauth).to_s
  elsif @auth && @auth[:username]
    req.basic_auth @auth[:username], @auth[:password]
  end

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



562
563
564
# File 'lib/kronk/request.rb', line 562

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

#oauthObject

Returns the oauth credentials if available.



306
307
308
309
310
311
312
313
314
315
316
# File 'lib/kronk/request.rb', line 306

def oauth
  if (!@oauth || !@oauth[:token] || !@oauth[:consumer_key]) &&
      @headers['Authorization'].to_s =~ /^OAuth\s/

    @oauth =
      SimpleOAuth::Header.parse(@headers['Authorization']).
        merge(@oauth || {})
  end

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



466
467
468
469
470
471
472
473
474
475
# File 'lib/kronk/request.rb', line 466

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

  @response = stream opts, &block

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



451
452
453
# File 'lib/kronk/request.rb', line 451

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

#ssl?Boolean

Check if this is an SSL request.

Returns:

  • (Boolean)


443
444
445
# File 'lib/kronk/request.rb', line 443

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


487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
# File 'lib/kronk/request.rb', line 487

def stream opts={}, &block
  retried = false
  opts    = opts.merge(:request => self)

  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

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

#to_hashObject

Returns this Request instance as an options hash.



514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
# File 'lib/kronk/request.rb', line 514

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[:oauth]   = @oauth if @oauth
  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.



539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
# File 'lib/kronk/request.rb', line 539

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.



435
436
437
# File 'lib/kronk/request.rb', line 435

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

#user_agent=(new_ua) ⇒ Object

Assign a User Agent header.



425
426
427
428
429
# File 'lib/kronk/request.rb', line 425

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