raptor-io 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|