raptor-io 0.0.1
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.
- checksums.yaml +15 -0
- data/LICENSE +30 -0
- data/README.md +51 -0
- data/lib/rack/handler/raptor-io.rb +130 -0
- data/lib/raptor-io.rb +11 -0
- data/lib/raptor-io/error.rb +19 -0
- data/lib/raptor-io/protocol.rb +6 -0
- data/lib/raptor-io/protocol/error.rb +10 -0
- data/lib/raptor-io/protocol/http.rb +34 -0
- data/lib/raptor-io/protocol/http/client.rb +685 -0
- data/lib/raptor-io/protocol/http/error.rb +16 -0
- data/lib/raptor-io/protocol/http/headers.rb +132 -0
- data/lib/raptor-io/protocol/http/message.rb +67 -0
- data/lib/raptor-io/protocol/http/request.rb +307 -0
- data/lib/raptor-io/protocol/http/request/manipulator.rb +117 -0
- data/lib/raptor-io/protocol/http/request/manipulators.rb +217 -0
- data/lib/raptor-io/protocol/http/request/manipulators/authenticator.rb +110 -0
- data/lib/raptor-io/protocol/http/request/manipulators/authenticators/basic.rb +36 -0
- data/lib/raptor-io/protocol/http/request/manipulators/authenticators/digest.rb +135 -0
- data/lib/raptor-io/protocol/http/request/manipulators/authenticators/negotiate.rb +69 -0
- data/lib/raptor-io/protocol/http/request/manipulators/authenticators/ntlm.rb +29 -0
- data/lib/raptor-io/protocol/http/request/manipulators/redirect_follower.rb +65 -0
- data/lib/raptor-io/protocol/http/response.rb +166 -0
- data/lib/raptor-io/protocol/http/server.rb +446 -0
- data/lib/raptor-io/ruby.rb +4 -0
- data/lib/raptor-io/ruby/hash.rb +24 -0
- data/lib/raptor-io/ruby/ipaddr.rb +15 -0
- data/lib/raptor-io/ruby/openssl.rb +23 -0
- data/lib/raptor-io/ruby/string.rb +27 -0
- data/lib/raptor-io/socket.rb +175 -0
- data/lib/raptor-io/socket/comm.rb +143 -0
- data/lib/raptor-io/socket/comm/local.rb +94 -0
- data/lib/raptor-io/socket/comm/sapni.rb +75 -0
- data/lib/raptor-io/socket/comm/socks.rb +237 -0
- data/lib/raptor-io/socket/comm_chain.rb +30 -0
- data/lib/raptor-io/socket/error.rb +45 -0
- data/lib/raptor-io/socket/switch_board.rb +183 -0
- data/lib/raptor-io/socket/switch_board/route.rb +42 -0
- data/lib/raptor-io/socket/tcp.rb +231 -0
- data/lib/raptor-io/socket/tcp/ssl.rb +77 -0
- data/lib/raptor-io/socket/tcp_server.rb +16 -0
- data/lib/raptor-io/socket/tcp_server/ssl.rb +52 -0
- data/lib/raptor-io/socket/udp.rb +0 -0
- data/lib/raptor-io/version.rb +6 -0
- data/lib/tasks/yard.rake +26 -0
- data/spec/rack/handler/raptor_spec.rb +140 -0
- data/spec/raptor-io/protocol/http/client_spec.rb +671 -0
- data/spec/raptor-io/protocol/http/headers_spec.rb +189 -0
- data/spec/raptor-io/protocol/http/message_spec.rb +5 -0
- data/spec/raptor-io/protocol/http/request/manipulators/authenticator_spec.rb +193 -0
- data/spec/raptor-io/protocol/http/request/manipulators/authenticators/basic_spec.rb +32 -0
- data/spec/raptor-io/protocol/http/request/manipulators/authenticators/digest_spec.rb +76 -0
- data/spec/raptor-io/protocol/http/request/manipulators/authenticators/negotiate_spec.rb +52 -0
- data/spec/raptor-io/protocol/http/request/manipulators/authenticators/ntlm_spec.rb +37 -0
- data/spec/raptor-io/protocol/http/request/manipulators/redirect_follower_spec.rb +51 -0
- data/spec/raptor-io/protocol/http/request/manipulators_spec.rb +202 -0
- data/spec/raptor-io/protocol/http/request_spec.rb +965 -0
- data/spec/raptor-io/protocol/http/response_spec.rb +236 -0
- data/spec/raptor-io/protocol/http/server_spec.rb +345 -0
- data/spec/raptor-io/ruby/hash_spec.rb +20 -0
- data/spec/raptor-io/ruby/string_spec.rb +20 -0
- data/spec/raptor-io/socket/comm/local_spec.rb +50 -0
- data/spec/raptor-io/socket/switch_board/route_spec.rb +49 -0
- data/spec/raptor-io/socket/switch_board_spec.rb +87 -0
- data/spec/raptor-io/socket/tcp/ssl_spec.rb +18 -0
- data/spec/raptor-io/socket/tcp_server/ssl_spec.rb +59 -0
- data/spec/raptor-io/socket/tcp_server_spec.rb +19 -0
- data/spec/raptor-io/socket/tcp_spec.rb +14 -0
- data/spec/raptor-io/socket_spec.rb +16 -0
- data/spec/raptor-io/version_spec.rb +10 -0
- data/spec/spec_helper.rb +56 -0
- data/spec/support/fixtures/raptor/protocol/http/request/manipulators/manifoolators/fooer.rb +25 -0
- data/spec/support/fixtures/raptor/protocol/http/request/manipulators/niccolo_machiavelli.rb +20 -0
- data/spec/support/fixtures/raptor/protocol/http/request/manipulators/options_validator.rb +28 -0
- data/spec/support/fixtures/raptor/socket/ssl_server.crt +18 -0
- data/spec/support/fixtures/raptor/socket/ssl_server.key +15 -0
- data/spec/support/lib/path_helpers.rb +11 -0
- data/spec/support/lib/webserver_option_parser.rb +26 -0
- data/spec/support/lib/webservers.rb +120 -0
- data/spec/support/shared/contexts/with_ssl_server.rb +70 -0
- data/spec/support/shared/contexts/with_tcp_server.rb +58 -0
- data/spec/support/shared/examples/raptor/comm_examples.rb +26 -0
- data/spec/support/shared/examples/raptor/protocols/http/message.rb +106 -0
- data/spec/support/shared/examples/raptor/socket_examples.rb +135 -0
- data/spec/support/webservers/raptor/protocols/http/client.rb +100 -0
- data/spec/support/webservers/raptor/protocols/http/client_close_connection.rb +29 -0
- data/spec/support/webservers/raptor/protocols/http/client_https.rb +43 -0
- data/spec/support/webservers/raptor/protocols/http/request/manipulators/authenticators/basic.rb +9 -0
- data/spec/support/webservers/raptor/protocols/http/request/manipulators/authenticators/digest.rb +22 -0
- data/spec/support/webservers/raptor/protocols/http/request/manipulators/redirect_follower.rb +11 -0
- metadata +336 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module RaptorIO
|
|
2
|
+
|
|
3
|
+
module Protocol::HTTP
|
|
4
|
+
|
|
5
|
+
#
|
|
6
|
+
# {HTTP} error namespace.
|
|
7
|
+
#
|
|
8
|
+
# All {HTTP} errors inherit from and live under it.
|
|
9
|
+
#
|
|
10
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
|
11
|
+
#
|
|
12
|
+
class Error < Protocol::Error
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
require 'webrick'
|
|
2
|
+
require 'uri'
|
|
3
|
+
|
|
4
|
+
module RaptorIO
|
|
5
|
+
module Protocol::HTTP
|
|
6
|
+
|
|
7
|
+
#
|
|
8
|
+
# HTTP Headers, holds shared attributes of {Request} and {Response}.
|
|
9
|
+
#
|
|
10
|
+
# For convenience, Hash-like getters and setters provide case-insensitive access.
|
|
11
|
+
#
|
|
12
|
+
# @author Tasos Laskos <tasos_laskos@rapid7.com>
|
|
13
|
+
#
|
|
14
|
+
class Headers < Hash
|
|
15
|
+
|
|
16
|
+
# @param [Headers, Hash] headers
|
|
17
|
+
def initialize( headers = {} )
|
|
18
|
+
(headers || {}).each do |k, v|
|
|
19
|
+
self[k] = v
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @note `field` will be capitalized appropriately before storing.
|
|
24
|
+
# @param [String] field Field name
|
|
25
|
+
# @return [String] Field value.
|
|
26
|
+
def delete( field )
|
|
27
|
+
super format_field_name( field.to_s.downcase )
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @note `field` will be capitalized appropriately before storing.
|
|
31
|
+
# @param [String] field Field name
|
|
32
|
+
# @return [String] Field value.
|
|
33
|
+
def include?( field )
|
|
34
|
+
super format_field_name( field.to_s.downcase )
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @note `field` will be capitalized appropriately before storing.
|
|
38
|
+
# @param [String] field Field name
|
|
39
|
+
# @return [String] Field value.
|
|
40
|
+
def []( field )
|
|
41
|
+
super format_field_name( field.to_s.downcase )
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @note `field` will be capitalized appropriately before storing.
|
|
45
|
+
# @param [String] field Field name
|
|
46
|
+
# @param [Array<String>, String] value Field value.
|
|
47
|
+
# @return [String] Field `value`.
|
|
48
|
+
def []=( field, value )
|
|
49
|
+
super format_field_name( field.to_s.downcase ),
|
|
50
|
+
value.is_a?( Array ) ? value : value.to_s
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @return [Array<String>] Set-cookie strings.
|
|
54
|
+
def set_cookie
|
|
55
|
+
return [] if self['set-cookie'].to_s.empty?
|
|
56
|
+
[self['set-cookie']].flatten
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @return [Array<Hash>] Cookies as hashes.
|
|
60
|
+
def parsed_set_cookie
|
|
61
|
+
return [] if set_cookie.empty?
|
|
62
|
+
|
|
63
|
+
set_cookie.map { |set_cookie_string|
|
|
64
|
+
WEBrick::Cookie.parse_set_cookies( set_cookie_string ).flatten.uniq.map do |cookie|
|
|
65
|
+
cookie_hash = {}
|
|
66
|
+
cookie.instance_variables.each do |var|
|
|
67
|
+
cookie_hash[var.to_s.gsub( /@/, '' ).to_sym] = cookie.instance_variable_get( var )
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Replace the string with a Time object.
|
|
71
|
+
cookie_hash[:expires] = cookie.expires
|
|
72
|
+
|
|
73
|
+
cookie_hash
|
|
74
|
+
end
|
|
75
|
+
}.flatten.compact
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @return [Array<Hash>] Request cookies.
|
|
79
|
+
def cookies
|
|
80
|
+
return [] if !self['cookie']
|
|
81
|
+
|
|
82
|
+
WEBrick::Cookie.parse( self['cookie'] ).flatten.uniq.map do |cookie|
|
|
83
|
+
cookie_hash = {}
|
|
84
|
+
cookie.instance_variables.each do |var|
|
|
85
|
+
cookie_hash[var.to_s.gsub( /@/, '' ).to_sym] = cookie.instance_variable_get( var )
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Replace the string with a Time object.
|
|
89
|
+
cookie_hash[:expires] = cookie.expires
|
|
90
|
+
|
|
91
|
+
cookie_hash
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @return [String] HTTP headers formatted for transmission.
|
|
96
|
+
def to_s
|
|
97
|
+
map { |k, v|
|
|
98
|
+
if v.is_a? Array
|
|
99
|
+
v.map do |cv|
|
|
100
|
+
"#{k}: #{cv}"
|
|
101
|
+
end
|
|
102
|
+
else
|
|
103
|
+
"#{k}: #{v}"
|
|
104
|
+
end
|
|
105
|
+
}.flatten.join( CRLF )
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# @param [String] headers_string
|
|
109
|
+
# @return [Headers]
|
|
110
|
+
def self.parse( headers_string )
|
|
111
|
+
return Headers.new if headers_string.to_s.empty?
|
|
112
|
+
|
|
113
|
+
headers = Hash.new { |h, k| h[k] = [] }
|
|
114
|
+
headers_string.split( CRLF_PATTERN ).each do |header|
|
|
115
|
+
k, v = header.split( ':', 2 )
|
|
116
|
+
headers[k.to_s.strip] << v.to_s.strip
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
headers.each { |k, v| headers[k] = v.first if v.size == 1 }
|
|
120
|
+
new headers
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def format_field_name( field )
|
|
126
|
+
field.to_s.split( '-' ).map( &:capitalize ).join( '-' )
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module RaptorIO
|
|
2
|
+
module Protocol::HTTP
|
|
3
|
+
|
|
4
|
+
#
|
|
5
|
+
# HTTP message, holds shared attributes of {Request} and {Response}.
|
|
6
|
+
#
|
|
7
|
+
# @author Tasos Laskos <tasos_laskos@rapid7.com>
|
|
8
|
+
#
|
|
9
|
+
class Message
|
|
10
|
+
|
|
11
|
+
# @return [String] HTTP version.
|
|
12
|
+
attr_reader :version
|
|
13
|
+
|
|
14
|
+
# @return [Headers<String, String>] HTTP headers as a Hash-like object.
|
|
15
|
+
attr_reader :headers
|
|
16
|
+
|
|
17
|
+
# @return [String] {Request}/{Response} body.
|
|
18
|
+
attr_accessor :body
|
|
19
|
+
|
|
20
|
+
#
|
|
21
|
+
# @note All options will be sent through the class setters whenever
|
|
22
|
+
# possible to allow for normalization.
|
|
23
|
+
#
|
|
24
|
+
# @param [Hash] options Message options.
|
|
25
|
+
# @option options [String] :url The URL of the remote resource.
|
|
26
|
+
# @option options [Hash] :headers HTTP headers.
|
|
27
|
+
# @option options [String] :body Body.
|
|
28
|
+
# @option options [String] :version (1.1) HTTP version.
|
|
29
|
+
#
|
|
30
|
+
def initialize( options = {} )
|
|
31
|
+
options.each do |k, v|
|
|
32
|
+
begin
|
|
33
|
+
send( "#{k}=", v )
|
|
34
|
+
rescue NoMethodError
|
|
35
|
+
instance_variable_set( "@#{k}".to_sym, v )
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
@headers = Headers.new( @headers )
|
|
40
|
+
@version ||= '1.1'
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @return [Bool]
|
|
44
|
+
# `true` if the connections should be reused, `false` otherwise.
|
|
45
|
+
def keep_alive?
|
|
46
|
+
connection = headers['Connection'].to_s.downcase
|
|
47
|
+
|
|
48
|
+
return connection == 'keep-alive' if version.to_f < 1.1
|
|
49
|
+
connection != 'close'
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @return [Boolean]
|
|
53
|
+
# `true` when {#version} is `1.1`, `false` otherwise.
|
|
54
|
+
def http_1_1?
|
|
55
|
+
version == '1.1'
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @return [Boolean]
|
|
59
|
+
# `true` when {#version} is `1.0`, `false` otherwise.
|
|
60
|
+
def http_1_0?
|
|
61
|
+
version == '1.0'
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
module RaptorIO
|
|
2
|
+
module Protocol::HTTP
|
|
3
|
+
|
|
4
|
+
#
|
|
5
|
+
# HTTP Request.
|
|
6
|
+
#
|
|
7
|
+
# @author Tasos Laskos <tasos_laskos@rapid7.com>
|
|
8
|
+
#
|
|
9
|
+
class Request < Message
|
|
10
|
+
|
|
11
|
+
#
|
|
12
|
+
# {HTTP::Request} error namespace.
|
|
13
|
+
#
|
|
14
|
+
# All {HTTP::Request} errors inherit from and live under it.
|
|
15
|
+
#
|
|
16
|
+
# @author Tasos "Zapotek" Laskos
|
|
17
|
+
#
|
|
18
|
+
class Error < Protocol::HTTP::Error
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
require_relative 'request/manipulators'
|
|
22
|
+
|
|
23
|
+
# Acceptable response callback types.
|
|
24
|
+
CALLBACK_TYPES = [:on_complete, :on_failure, :on_success]
|
|
25
|
+
|
|
26
|
+
# @return [Symbol] HTTP method.
|
|
27
|
+
attr_reader :http_method
|
|
28
|
+
|
|
29
|
+
# @return [String] URL of the targeted resource.
|
|
30
|
+
attr_reader :url
|
|
31
|
+
|
|
32
|
+
# @return [URI] Parsed version of {#url}.
|
|
33
|
+
attr_reader :parsed_url
|
|
34
|
+
|
|
35
|
+
# @return [Hash] Request parameters.
|
|
36
|
+
attr_reader :parameters
|
|
37
|
+
|
|
38
|
+
# @return [Integer, Float] Timeout in seconds.
|
|
39
|
+
attr_accessor :timeout
|
|
40
|
+
|
|
41
|
+
# @note Defaults to `true`.
|
|
42
|
+
# @return [Bool]
|
|
43
|
+
# Whether or not to automatically continue on responses with status 100.
|
|
44
|
+
attr_reader :continue
|
|
45
|
+
|
|
46
|
+
# @note Defaults to `false`.
|
|
47
|
+
# @return [Bool]
|
|
48
|
+
# Whether or not encode any of the given data for HTTP transmission.
|
|
49
|
+
attr_accessor :raw
|
|
50
|
+
|
|
51
|
+
attr_accessor :callbacks
|
|
52
|
+
|
|
53
|
+
# @private
|
|
54
|
+
attr_accessor :root_redirect_id
|
|
55
|
+
|
|
56
|
+
# @return [String] IP address -- populated by {Server}.
|
|
57
|
+
attr_accessor :client_address
|
|
58
|
+
|
|
59
|
+
#
|
|
60
|
+
# @note This class' options are in addition to {Message#initialize}.
|
|
61
|
+
#
|
|
62
|
+
# @param [Hash] options Request options.
|
|
63
|
+
# @option options [String] :version ('1.1') HTTP version to use.
|
|
64
|
+
# @option options [Symbol, String] :http_method (:get) HTTP method to use.
|
|
65
|
+
# @option options [Hash] :parameters ({})
|
|
66
|
+
# Parameters to send. If performing a GET request and the URL has parameters
|
|
67
|
+
# of its own they will be merged and overwritten.
|
|
68
|
+
# @option options [Integer] :timeout
|
|
69
|
+
# Max time to wait for a response in seconds.
|
|
70
|
+
# @option options [Bool] :continue
|
|
71
|
+
# Whether or not to automatically continue on responses with status 100.
|
|
72
|
+
# Only applicable when the 'Expect' header has been set to '100-continue'.
|
|
73
|
+
# @option options [Bool] :raw (false)
|
|
74
|
+
# `true` to not encode any of the given data for HTTP transmission, `false`
|
|
75
|
+
# otherwise.
|
|
76
|
+
#
|
|
77
|
+
# @see Message#initialize
|
|
78
|
+
# @see #parameters=
|
|
79
|
+
# @see #http_method=
|
|
80
|
+
#
|
|
81
|
+
def initialize( options = {} )
|
|
82
|
+
super( options )
|
|
83
|
+
|
|
84
|
+
clear_callbacks
|
|
85
|
+
|
|
86
|
+
fail ArgumentError, "Missing ':url' option." if !@url
|
|
87
|
+
|
|
88
|
+
@parameters ||= {}
|
|
89
|
+
@http_method ||= :get
|
|
90
|
+
@continue = true if @continue.nil?
|
|
91
|
+
@raw = false if @raw.nil?
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Clears all callbacks.
|
|
95
|
+
def clear_callbacks
|
|
96
|
+
@callbacks = CALLBACK_TYPES.inject( {} ) { |h, type| h[type] = []; h }
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @return [Bool]
|
|
101
|
+
# Whether or not encode any of the given data for HTTP transmission.
|
|
102
|
+
def raw?
|
|
103
|
+
!!@raw
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# @return [Bool]
|
|
107
|
+
# Whether or not to automatically continue on responses with status 100.
|
|
108
|
+
def continue?
|
|
109
|
+
!!@continue
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# @param [String] uri Request URL.
|
|
113
|
+
# @return [String] `uri`
|
|
114
|
+
def url=( uri )
|
|
115
|
+
@url = uri
|
|
116
|
+
@parsed_url= URI(@url)
|
|
117
|
+
@url
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# @return [Integer] Identification for the remote host:port.
|
|
121
|
+
def connection_id
|
|
122
|
+
"#{parsed_url.host}:#{parsed_url.port}".hash
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
#
|
|
126
|
+
# @note All keys and values will be recursively converted to strings.
|
|
127
|
+
#
|
|
128
|
+
# Sets request parameters.
|
|
129
|
+
#
|
|
130
|
+
# @param [Hash] params
|
|
131
|
+
# Parameters to assign to this request.
|
|
132
|
+
# If performing a GET request and the URL has parameters of its own they
|
|
133
|
+
# will be merged and overwritten.
|
|
134
|
+
#
|
|
135
|
+
# @return [Hash] Normalized parameters.
|
|
136
|
+
#
|
|
137
|
+
def parameters=( params )
|
|
138
|
+
@parameters = params.stringify
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @return [Hash] Parameters to be used for the query part of the resource.
|
|
142
|
+
def query_parameters
|
|
143
|
+
query = parsed_url.query
|
|
144
|
+
if !query
|
|
145
|
+
return http_method == :get ? parameters : {}
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
qparams = query.split('&').inject({}) do |h, pair|
|
|
149
|
+
k, v = pair.split('=', 2)
|
|
150
|
+
h.merge( decode_if_not_raw(k) => decode_if_not_raw(v) )
|
|
151
|
+
end
|
|
152
|
+
return qparams if http_method != :get
|
|
153
|
+
|
|
154
|
+
qparams.merge( parameters )
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# @return [URI] Location of the resource to request.
|
|
158
|
+
def effective_url
|
|
159
|
+
cparsed_url = parsed_url.dup
|
|
160
|
+
cparsed_url.query = query_parameters.map do |k, v|
|
|
161
|
+
"#{encode_if_not_raw(k)}=#{encode_if_not_raw(v)}"
|
|
162
|
+
end.join('&') if query_parameters.any?
|
|
163
|
+
|
|
164
|
+
cparsed_url.normalize
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# @return [String] Response body to use.
|
|
168
|
+
def effective_body
|
|
169
|
+
return '' if headers['Expect'] == '100-continue'
|
|
170
|
+
return encode_if_not_raw(body.to_s)
|
|
171
|
+
|
|
172
|
+
body_params = if !body.to_s.empty?
|
|
173
|
+
body.split('&').inject({}) do |h, pair|
|
|
174
|
+
k, v = pair.split('=', 2)
|
|
175
|
+
h.merge( decode_if_not_raw(k) => decode_if_not_raw(v) )
|
|
176
|
+
end
|
|
177
|
+
else
|
|
178
|
+
{}
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
return '' if body_params.empty? && parameters.empty?
|
|
182
|
+
|
|
183
|
+
body_params.merge( parameters ).map do |k, v|
|
|
184
|
+
"#{encode_if_not_raw(k)}=#{encode_if_not_raw(v)}"
|
|
185
|
+
end.join('&')
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
#
|
|
189
|
+
# @note Method will be normalized to a lower-case symbol.
|
|
190
|
+
#
|
|
191
|
+
# Sets the request HTTP method.
|
|
192
|
+
#
|
|
193
|
+
# @param [#to_s] http_verb HTTP method.
|
|
194
|
+
#
|
|
195
|
+
# @return [Symbol] HTTP method.
|
|
196
|
+
#
|
|
197
|
+
def http_method=( http_verb )
|
|
198
|
+
@http_method = http_verb.to_s.downcase.to_sym
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# @return [Bool] `true` if the request if idempotent, `false` otherwise.
|
|
202
|
+
def idempotent?
|
|
203
|
+
http_method != :post
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# @return [String] Server-side resource to request.
|
|
207
|
+
def resource
|
|
208
|
+
req_resource = "#{effective_url.path}"
|
|
209
|
+
req_resource << "?#{effective_url.query}" if effective_url.query
|
|
210
|
+
req_resource
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# @return [String]
|
|
214
|
+
# String representation of the request, ready for HTTP transmission.
|
|
215
|
+
def to_s
|
|
216
|
+
final_body = effective_body
|
|
217
|
+
|
|
218
|
+
computed_headers = Headers.new( 'Host' => "#{effective_url.host}:#{effective_url.port}" )
|
|
219
|
+
computed_headers['Content-Length'] = final_body.size.to_s if !final_body.to_s.empty?
|
|
220
|
+
|
|
221
|
+
request = "#{http_method.to_s.upcase} #{resource} HTTP/#{version}#{CRLF}"
|
|
222
|
+
request << computed_headers.merge(headers).to_s
|
|
223
|
+
request << HEADER_SEPARATOR
|
|
224
|
+
|
|
225
|
+
return request if final_body.to_s.empty?
|
|
226
|
+
|
|
227
|
+
request << final_body.to_s
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
CALLBACK_TYPES.each do |type|
|
|
231
|
+
define_method type, ->( &block ) do
|
|
232
|
+
return @callbacks[type] if !block
|
|
233
|
+
@callbacks[type] << block
|
|
234
|
+
self
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
define_method "#{type}=" do |callbacks|
|
|
238
|
+
@callbacks[type] = [callbacks].flatten.compact
|
|
239
|
+
self
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# @!method on_complete( &block )
|
|
244
|
+
# Assigns a block to be called with the response.
|
|
245
|
+
# @param [Block] block Block to be passed the response.
|
|
246
|
+
|
|
247
|
+
# @!method on_success( &block )
|
|
248
|
+
# Assigns a block to be called with the response if the request was successful.
|
|
249
|
+
# @param [Block] block Block to be passed the response.
|
|
250
|
+
|
|
251
|
+
# @!method on_failure( &block )
|
|
252
|
+
# Assigns a block to be called if the request fails.
|
|
253
|
+
# @param [Block] block Block to call on failure.
|
|
254
|
+
|
|
255
|
+
#
|
|
256
|
+
# Handles the `response` to `self` by passing to the appropriate callbacks.
|
|
257
|
+
#
|
|
258
|
+
# @param [Response] response
|
|
259
|
+
#
|
|
260
|
+
# @private
|
|
261
|
+
def handle_response( response )
|
|
262
|
+
response.request = self
|
|
263
|
+
|
|
264
|
+
type = (response.code.to_i == 0) ? :on_failure : :on_success
|
|
265
|
+
|
|
266
|
+
@callbacks[type].each { |block| block.call response }
|
|
267
|
+
@callbacks[:on_complete].each { |block| block.call response }
|
|
268
|
+
true
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# @return [Request] Duplicate of `self`.
|
|
272
|
+
def dup
|
|
273
|
+
r = self.class.new( url: url )
|
|
274
|
+
instance_variables.each do |iv|
|
|
275
|
+
r.instance_variable_set iv, instance_variable_get( iv )
|
|
276
|
+
end
|
|
277
|
+
r
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# @param [String] request HTTP request message to parse.
|
|
281
|
+
# @return [Request]
|
|
282
|
+
def self.parse( request )
|
|
283
|
+
data = {}
|
|
284
|
+
first_line, headers_and_body = request.split( CRLF_PATTERN, 2 )
|
|
285
|
+
data[:http_method], data[:url], data[:version] = first_line.scan( /([A-Z]+)\s+(.*)\s+HTTP\/([0-9\.]+)/ ).flatten
|
|
286
|
+
headers, data[:body] = headers_and_body.split( HEADER_SEPARATOR_PATTERN, 2 )
|
|
287
|
+
|
|
288
|
+
# Use Host to fill in the parsed_uri stuff.
|
|
289
|
+
data[:headers] = Headers.parse( headers.to_s )
|
|
290
|
+
|
|
291
|
+
new data
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
private
|
|
295
|
+
|
|
296
|
+
def encode_if_not_raw( str )
|
|
297
|
+
raw? ? str : CGI.escape( str )
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def decode_if_not_raw( str )
|
|
301
|
+
raw? ? str : CGI.unescape( str )
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
end
|
|
307
|
+
end
|