Class: Arachni::HTTP::Request

Inherits:
Message show all
Defined in:
lib/arachni/http/request.rb,
lib/arachni/http/request/scope.rb

Overview

HTTP Request representation.

Author:

Defined Under Namespace

Classes: Scope

Constant Summary collapse

REDIRECT_LIMIT =

Default redirect limit, RFC says 5 max.

5
MODES =

Supported modes of operation.

[
    # Asynchronous (non-blocking) (Default)
    :async,

    # Synchronous (blocking)
    :sync
]

Instance Attribute Summary collapse

Attributes inherited from Message

#body, #headers, #url

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Message

#parsed_url, #scope

Constructor Details

#initialize(options = {}) ⇒ Request

Returns a new instance of Request.

Parameters:

  • options (Hash) (defaults to: {})

    Request options.

Options Hash (options):

  • :url (String)

    URL.

  • :parameters (Hash) — default: {}

    Request parameters.

  • :body (String) — default: {}

    Request body.

  • :train (Bool) — default: false

    Force Arachni to analyze the response looking for new elements.

  • :mode (Symbol) — default: :async

    Mode in which to perform the request:

    • ‘:async` – Asynchronous (non-blocking) (Default).

    • ‘:sync` – Synchronous (blocking).

  • :headers (Hash) — default: {}

    Extra HTTP request headers.

  • :cookies (Hash) — default: {}

    Cookies for the request.



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/arachni/http/request.rb', line 123

def initialize( options = {} )
    options[:method] ||= :get

    super( options )

    @train           = false if @train.nil?
    @fingerprint     = true  if @fingerprint.nil?
    @update_cookies  = false if @update_cookies.nil?
    @follow_location = false if @follow_location.nil?
    @max_redirects   = (Options.http.request_redirect_limit || REDIRECT_LIMIT)
    @on_complete     = []

    @timeout       ||= Options.http.request_timeout
    @mode          ||= :async
    @parameters    ||= {}
    @cookies       ||= {}
end

Instance Attribute Details

#cookiesHash

Returns Cookies set for this request.

Returns:

  • (Hash)

    Cookies set for this request.



62
63
64
# File 'lib/arachni/http/request.rb', line 62

def cookies
  @cookies
end

#effective_bodyString

Note:

Available only via completed Arachni::HTTP::Response#request.

Returns Transmitted HTTP request body.

Returns:

  • (String)

    Transmitted HTTP request body.



80
81
82
# File 'lib/arachni/http/request.rb', line 80

def effective_body
  @effective_body
end

#follow_locationBool

Returns Follow ‘Location` headers.

Returns:

  • (Bool)

    Follow ‘Location` headers.



44
45
46
# File 'lib/arachni/http/request.rb', line 44

def follow_location
  @follow_location
end

#headers_stringString

Note:

Available only via completed Arachni::HTTP::Response#request.

Returns Transmitted HTTP request headers.

Returns:

  • (String)

    Transmitted HTTP request headers.



74
75
76
# File 'lib/arachni/http/request.rb', line 74

def headers_string
  @headers_string
end

#high_priorityBool

Returns:

  • (Bool)


98
99
100
# File 'lib/arachni/http/request.rb', line 98

def high_priority
  @high_priority
end

#idInteger

Returns Auto-incremented ID for this request (set by Client#request).

Returns:

  • (Integer)

    Auto-incremented ID for this request (set by Client#request).



32
33
34
# File 'lib/arachni/http/request.rb', line 32

def id
  @id
end

#max_redirectsInteger

Returns Maximum number of redirects to follow.

Returns:

  • (Integer)

    Maximum number of redirects to follow.

See Also:



50
51
52
# File 'lib/arachni/http/request.rb', line 50

def max_redirects
  @max_redirects
end

#modeSymbol

Returns Mode of operation for the request.

Returns:

  • (Symbol)

    Mode of operation for the request.

See Also:



68
69
70
# File 'lib/arachni/http/request.rb', line 68

def mode
  @mode
end

#parametersHash

Returns Request parameters.

Returns:

  • (Hash)

    Request parameters.



36
37
38
# File 'lib/arachni/http/request.rb', line 36

def parameters
  @parameters
end

#passwordString

Returns HTTP password.

Returns:



58
59
60
# File 'lib/arachni/http/request.rb', line 58

def password
  @password
end

#performerObject

Entity which performed the request – mostly used to track which response was a result of which submitted element.



84
85
86
# File 'lib/arachni/http/request.rb', line 84

def performer
  @performer
end

#proxyString

Returns ‘host:port`.

Returns:



88
89
90
# File 'lib/arachni/http/request.rb', line 88

def proxy
  @proxy
end

#proxy_typeString

Returns:



95
96
97
# File 'lib/arachni/http/request.rb', line 95

def proxy_type
  @proxy_type
end

#proxy_user_passwordString

Returns ‘user:password`.

Returns:

  • (String)

    ‘user:password`



92
93
94
# File 'lib/arachni/http/request.rb', line 92

def proxy_user_password
  @proxy_user_password
end

#response_max_sizeInteger

Returns Maximum HTTP response size to accept, in bytes.

Returns:

  • (Integer)

    Maximum HTTP response size to accept, in bytes.



102
103
104
# File 'lib/arachni/http/request.rb', line 102

def response_max_size
  @response_max_size
end

#timeoutInteger

Returns Timeout in milliseconds.

Returns:

  • (Integer)

    Timeout in milliseconds.



40
41
42
# File 'lib/arachni/http/request.rb', line 40

def timeout
  @timeout
end

#usernameString

Returns HTTP username.

Returns:



54
55
56
# File 'lib/arachni/http/request.rb', line 54

def username
  @username
end

Class Method Details

.encode(string) ⇒ Object



501
502
503
504
# File 'lib/arachni/http/request.rb', line 501

def encode( string )
    @easy ||= Ethon::Easy.new( url: 'www.example.com' )
    @easy.escape string
end

.from_rpc_data(data) ⇒ Request

Parameters:

Returns:



468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
# File 'lib/arachni/http/request.rb', line 468

def from_rpc_data( data )
    instance = allocate
    data.each do |name, value|

        value = case name
                    when 'method', 'mode'
                        value.to_sym

                    else
                        value
                end

        instance.instance_variable_set( "@#{name}", value )
    end
    instance
end

.parse_body(body) ⇒ Hash

Parses an HTTP request body generated by submitting a form.

Parameters:

Returns:

  • (Hash)

    Parameters.



491
492
493
494
495
496
497
498
499
# File 'lib/arachni/http/request.rb', line 491

def parse_body( body )
    return {} if !body

    body.to_s.split( '&' ).inject( {} ) do |h, pair|
        name, value = pair.split( '=', 2 )
        h[Form.decode( name.to_s )] = Form.decode( value )
        h
    end
end

Instance Method Details

#==(other) ⇒ Object



429
430
431
# File 'lib/arachni/http/request.rb', line 429

def ==( other )
    hash == other.hash
end

#asynchronous?Boolean

Returns ‘true` if #mode is `:async`, `false` otherwise.

Returns:

  • (Boolean)

    ‘true` if #mode is `:async`, `false` otherwise.



170
171
172
# File 'lib/arachni/http/request.rb', line 170

def asynchronous?
    mode == :async
end

#blocking?Boolean

Returns ‘true` if #mode is `:sync`, `false` otherwise.

Returns:

  • (Boolean)

    ‘true` if #mode is `:sync`, `false` otherwise.



176
177
178
# File 'lib/arachni/http/request.rb', line 176

def blocking?
    mode == :sync
end

#body_parametersObject



222
223
224
225
# File 'lib/arachni/http/request.rb', line 222

def body_parameters
    return {} if method != :post
    parameters.any? ? parameters : self.class.parse_body( body )
end

#clear_callbacksObject

Clears #on_complete callbacks.



256
257
258
# File 'lib/arachni/http/request.rb', line 256

def clear_callbacks
    @on_complete.clear
end

#effective_cookiesObject



211
212
213
214
215
216
# File 'lib/arachni/http/request.rb', line 211

def effective_cookies
    Cookie.from_string( url, headers['Cookie'] || '' ).inject({}) do |h, cookie|
        h[cookie.name] = cookie.value
        h
    end.merge( cookies )
end

#effective_parametersObject



218
219
220
# File 'lib/arachni/http/request.rb', line 218

def effective_parameters
    Utilities.uri_parse_query( url ).merge( parameters || {} )
end

#fingerprint?Bool

Returns ‘true` if the Arachni::HTTP::Response should be fingerprinted for platforms, `false` otherwise.

Returns:



269
270
271
# File 'lib/arachni/http/request.rb', line 269

def fingerprint?
    @fingerprint
end

#follow_location?Bool

Returns ‘true` if redirects should be followed, `false` otherwise.

Returns:

  • (Bool)

    ‘true` if redirects should be followed, `false` otherwise.



262
263
264
# File 'lib/arachni/http/request.rb', line 262

def follow_location?
    !!@follow_location
end

#handle_response(response) ⇒ Object



307
308
309
310
311
# File 'lib/arachni/http/request.rb', line 307

def handle_response( response )
    response.request = self
    @on_complete.each { |b| b.call response }
    response
end

#hashObject



433
434
435
# File 'lib/arachni/http/request.rb', line 433

def hash
    to_h.hash
end

#high_priority?Boolean

Returns:

  • (Boolean)


141
142
143
# File 'lib/arachni/http/request.rb', line 141

def high_priority?
    !!@high_priority
end

#inspectObject



233
234
235
236
237
238
239
240
241
242
243
# File 'lib/arachni/http/request.rb', line 233

def inspect
    s = "#<#{self.class} "
    s << "@id=#{id} "
    s << "@mode=#{mode} "
    s << "@method=#{method} "
    s << "@url=#{url.inspect} "
    s << "@parameters=#{parameters.inspect} "
    s << "@high_priority=#{high_priority} "
    s << "@performer=#{performer.inspect}"
    s << '>'
end

#marshal_dumpObject



437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
# File 'lib/arachni/http/request.rb', line 437

def marshal_dump
    callbacks = @on_complete.dup
    performer = @performer

    @performer   = nil
    @on_complete = []

    instance_variables.inject( {} ) do |h, iv|
        next h if iv == :@scope
        h[iv.to_s.gsub('@','')] = instance_variable_get( iv )
        h
    end
ensure
    @on_complete = callbacks
    @performer   = performer
end

#marshal_load(h) ⇒ Object



454
455
456
# File 'lib/arachni/http/request.rb', line 454

def marshal_load( h )
    h.each { |k, v| instance_variable_set( "@#{k}", v ) }
end

#method(*args) ⇒ Symbol

Returns HTTP method.

Returns:

  • (Symbol)

    HTTP method.



182
183
184
185
# File 'lib/arachni/http/request.rb', line 182

def method( *args )
    return super( *args ) if args.any? # Preserve Object#method.
    @method
end

#method=(verb) ⇒ Symbol

Note:

Method will be normalized to a lower-case symbol.

Sets the request HTTP method.

Parameters:

  • verb (#to_s)

    HTTP method.

Returns:

  • (Symbol)

    HTTP method.



196
197
198
# File 'lib/arachni/http/request.rb', line 196

def method=( verb )
    @method = verb.to_s.downcase.to_sym
end

#on_complete(&block) ⇒ Object

Note:

Can be invoked multiple times.

Parameters:

  • block (Block)

    Callback to be passed the response.



249
250
251
252
253
# File 'lib/arachni/http/request.rb', line 249

def on_complete( &block )
    fail 'Block is missing.' if !block_given?
    @on_complete << block
    self
end

#prepare_headersObject



507
508
509
510
511
512
513
514
515
516
517
518
519
520
# File 'lib/arachni/http/request.rb', line 507

def prepare_headers
    headers['User-Agent'] ||= Options.http.user_agent
    headers['Accept']     ||= 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
    headers['From']       ||= Options.authorized_by if Options.authorized_by

    headers.each { |k, v| headers[k] = Header.encode( v ) if v }

    headers['Cookie'] = effective_cookies.
        map { |k, v| "#{Cookie.encode( k )}=#{Cookie.encode( v )}" }.
        join( ';' )
    headers.delete( 'Cookie' ) if headers['Cookie'].empty?

    headers
end

#runResponse

Note:

Will call #on_complete callbacks.

Performs the Arachni::HTTP::Request without going through Client.

Returns:



303
304
305
# File 'lib/arachni/http/request.rb', line 303

def run
    client_run.tap { |r| r.request = self }
end

#to_hObject



417
418
419
420
421
422
423
424
425
426
427
# File 'lib/arachni/http/request.rb', line 417

def to_h
    {
        url:            url,
        parameters:     parameters,
        headers:        headers,
        headers_string: headers_string,
        effective_body: effective_body,
        body:           body,
        method:         method
    }
end

#to_rpc_dataHash

Returns Data representing this instance that are suitable the RPC transmission.

Returns:

  • (Hash)

    Data representing this instance that are suitable the RPC transmission.



460
461
462
# File 'lib/arachni/http/request.rb', line 460

def to_rpc_data
    marshal_dump
end

#to_sString

Returns HTTP request string.

Returns:

  • (String)

    HTTP request string.



229
230
231
# File 'lib/arachni/http/request.rb', line 229

def to_s
    "#{headers_string}#{effective_body}"
end

#to_typhoeusTyphoeus::Response

Returns ‘self` converted to a `Typhoeus::Request`.

Returns:

  • (Typhoeus::Response)

    ‘self` converted to a `Typhoeus::Request`.



315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
# File 'lib/arachni/http/request.rb', line 315

def to_typhoeus
    prepare_headers

    if (userpwd = (@username || Options.http.authentication_username))
        if (passwd = (@password || Options.http.authentication_password))
            userpwd += ":#{passwd}"
        end
    end

    max_size = @response_max_size || Options.http.response_max_size
    # Weird I know, for some reason 0 gets ignored.
    max_size = 1   if max_size == 0
    max_size = nil if max_size < 0

    options = {
        method:          method,
        headers:         headers,
        body:            body,
        params:          effective_parameters,
        userpwd:         userpwd,
        followlocation:  follow_location?,
        maxredirs:       @max_redirects,

        ssl_verifypeer:  !!Options.http.ssl_verify_peer,
        ssl_verifyhost:  Options.http.ssl_verify_host ? 2 : 0,
        sslcert:         Options.http.ssl_certificate_filepath,
        sslcerttype:     Options.http.ssl_certificate_type,
        sslkey:          Options.http.ssl_key_filepath,
        sslkeytype:      Options.http.ssl_key_type,
        sslkeypasswd:    Options.http.ssl_key_password,
        cainfo:          Options.http.ssl_ca_filepath,
        capath:          Options.http.ssl_ca_directory,
        sslversion:      Options.http.ssl_version,

        accept_encoding: 'gzip, deflate',
        nosignal:        true,

        # If Content-Length is missing this option will have no effect, so
        # we'll also stream the body to make sure that we can at least abort
        # the reading of the response body if it exceeds this limit.
        maxfilesize:     max_size,

        # Don't keep the socket alive if this is a blocking request because
        # it's going to be performed by an one-off Hydra.
        forbid_reuse:    blocking?,
        verbose:         true
    }

    options[:timeout_ms] = timeout if timeout

    # This will allow GSS-Negotiate to work out of the box but shouldn't
    # have any adverse effects.
    if !options[:userpwd] && !parsed_url.user
        options[:userpwd]  = ':'
        options[:httpauth] = :gssnegotiate
    else
        options[:httpauth] = :auto
    end

    if proxy
        options.merge!(
            proxy:     proxy,
            proxytype: (proxy_type || :http).to_sym
        )

        if proxy_user_password
            options[:proxyuserpwd] = proxy_user_password
        end

    elsif Options.http.proxy_host && Options.http.proxy_port
        options.merge!(
            proxy:     "#{Options.http.proxy_host}:#{Options.http.proxy_port}",
            proxytype: (Options.http.proxy_type || :http).to_sym
        )

        if Options.http.proxy_username && Options.http.proxy_password
            options[:proxyuserpwd] =
                "#{Options.http.proxy_username}:#{Options.http.proxy_password}"
        end
    end

    curl             = parsed_url.query ? url.gsub( "?#{parsed_url.query}", '' ) : url
    typhoeus_request = Typhoeus::Request.new( curl, options )

    if @on_complete.any?
        response_body_buffer = ''
        set_body_reader( typhoeus_request, response_body_buffer )

        typhoeus_request.on_complete do |typhoeus_response|
            if typhoeus_request.options[:maxfilesize]
                typhoeus_response.options[:response_body] =
                    response_body_buffer
            end

            fill_in_data_from_typhoeus_response typhoeus_response
            handle_response Response.from_typhoeus( typhoeus_response )
        end
    end

    typhoeus_request
end

#trainObject

Flags that the response should be analyzed by the Trainer for new elements.



282
283
284
# File 'lib/arachni/http/request.rb', line 282

def train
    @train = true
end

#train?Bool

Returns ‘true` if the Arachni::HTTP::Response should be analyzed by the Trainer for new elements, `false` otherwise.

Returns:



276
277
278
# File 'lib/arachni/http/request.rb', line 276

def train?
    @train
end

#update_cookiesObject

Flags that the CookieJar should be updated with the Arachni::HTTP::Response cookies.



294
295
296
# File 'lib/arachni/http/request.rb', line 294

def update_cookies
    @update_cookies = true
end

#update_cookies?Bool

Returns ‘true` if the CookieJar should be updated with the Arachni::HTTP::Response cookies, `false` otherwise.

Returns:



289
290
291
# File 'lib/arachni/http/request.rb', line 289

def update_cookies?
    @update_cookies
end