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
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
NjQ5NDlkZTFhNjE3YTUwMGYzMDIzMTcyYTgxOWUxZmM2MTkwMjc1MQ==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
NTFiOGZjZjYxNzRjZmJmNjgwZTIwOGVkYzMwNTUxNDMxZTdkMzRiNQ==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
ODU0Y2ZhYjQzMjBkNzEzNzgyNmFiMjBmMTZhMTVhNWNhZDUzOWFlYjAwMDFk
|
10
|
+
YTZkNjQ3ZTk5NzZlOTZlZTE3MTQ3ZWYzZGEzYjVmYTI2MWY1ZTRmNjNiM2E5
|
11
|
+
ZTYxMDJlMDg1NDEyYTA5OTc3OTUyMGY3OGU5ZTI2ZGE4MjI5M2Y=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
YjIzMjI0NGZmYjY3MzgwOTVlN2YyNmFkMTZkOGYxM2RhMDMwZTEzOTc0MzVj
|
14
|
+
NjlkZWI3YWY2OGY1NWRmMWE5MGQ4NjhjNjM1MTFlZWU0OGZhNzJlOTBhMWU5
|
15
|
+
NmZkZDg2NmY2MTg4ZTI5ZjFmYTkyNjc4MGViMDAyOWIwMjE4YjQ=
|
data/LICENSE
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
```
|
2
|
+
Copyright (c) 2013 Rapid7, Inc.
|
3
|
+
|
4
|
+
License: BSD-3-clause
|
5
|
+
|
6
|
+
Redistribution and use in source and binary forms, with or without modification,
|
7
|
+
are permitted provided that the following conditions are met:
|
8
|
+
.
|
9
|
+
* Redistributions of source code must retain the above copyright notice,
|
10
|
+
this list of conditions and the following disclaimer.
|
11
|
+
.
|
12
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
14
|
+
and/or other materials provided with the distribution.
|
15
|
+
.
|
16
|
+
* Neither the name of Rapid7 LLC nor the names of its contributors
|
17
|
+
may be used to endorse or promote products derived from this software
|
18
|
+
without specific prior written permission.
|
19
|
+
.
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
21
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
22
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
24
|
+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
25
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
26
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
27
|
+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
28
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
29
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
30
|
+
```
|
data/README.md
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# RaptorIO
|
2
|
+
|
3
|
+
Lighter, faster and smarter than REX, RaptorIO is the next evolutionary step in Metasploit's basic network IO functionality.
|
4
|
+
|
5
|
+
|
6
|
+
|
7
|
+
|
8
|
+
## FAQ
|
9
|
+
|
10
|
+
1. **What is RaptorIO?** RaptorIO is the next generation of the core networking libraries used by Metasploit. This includes the core Socket and Switchboard implementations as well as clients and servers for lots of protocols, both open source and proprietary. It is intended as a general-purpose library for doing network exploitation, research, and reverse engineering, but you can also use it to power the server hosting your collection of cat GIFs.
|
11
|
+
2. **Should I use it?** Sure! Just remember that it's not 1.0 yet and won't be for awhile. We are open-sourcing it now because the critical early stages have been completed and we want the Ruby community to be able to have a look and help us expand it.
|
12
|
+
3. **What are the next goals?** We want to continue building out client/server pairs for each of multiple protocols, starting with the SMB family (DCE/RPC, etc). Goals on the horizon include a less client-centric replacement for Net::SSH as well.
|
13
|
+
4. **I know network programming and Ruby - how can I help?** Pull requests are welcome! Implementations of client/server pairs must be fully spec'd (ideally from the protocol's RFC) and follow the One True Rule of RaptorIO: "*an implementation must follow the spec by default, but allow tinkering/abuse of the protocol at every level.*" Abstractions should be for convenience, but not lock users out of dealing with lower-level constructs.
|
14
|
+
5. **When will RaptorIO be live as the networking layer in Metasploit Framework?** We hesitate to put a firm date on it, but we are working toward that goal as fast as possible. There's lots of work to be done, and we hope you can help!
|
15
|
+
6. **Why RaptorIO::Socket? Why RaptorIO::Switchboard?** As the networking layer for an exploitation toolkit, RaptorIO has an an explicit design goal the ability to "pivot" new network traffic through established network connections. The Switchboard, Socket and Comm constructs exist to facilitate this capability.
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
### From source
|
20
|
+
|
21
|
+
gem install bundler
|
22
|
+
git clone git@github.com:rapid7/raptor.git
|
23
|
+
bundle install
|
24
|
+
|
25
|
+
### From RubyGems
|
26
|
+
*submission to RubyGems anticipated mid-February 2014*
|
27
|
+
|
28
|
+
## Usage
|
29
|
+
|
30
|
+
* [HTTP client and server operations](https://github.com/rapid7/raptor-io/wiki/HTTP-examples) with asychronous, synchronous, and manually queued interactions
|
31
|
+
* [Arbitrary network connections](https://github.com/rapid7/raptor-io/wiki/Raptor-Socket-Requirements) using the Comm(connection establishment) and Switchboard(routing) abstractions.
|
32
|
+
|
33
|
+
|
34
|
+
## Immediate Goals
|
35
|
+
* ~~Implement improved Switchboard (in-memory router)~~
|
36
|
+
* ~~Implement improved Socket (built on Ruby's TCPSocket)~~
|
37
|
+
* ~~Create basic Comm classes (for multi-channel communication abstraction)~~
|
38
|
+
* * ~~local~~
|
39
|
+
* * ~~SOCKS~~
|
40
|
+
* ~~HTTP client and server functionality~~
|
41
|
+
* SMB client and server functionality
|
42
|
+
* (Implement all protocol libraries supported by REX)
|
43
|
+
|
44
|
+
|
45
|
+
## Contributing
|
46
|
+
|
47
|
+
1. Fork it
|
48
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
49
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
50
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
51
|
+
5. Create new Pull Request
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'raptor-io'
|
2
|
+
require 'rack'
|
3
|
+
require 'stringio'
|
4
|
+
require 'rack/content_length'
|
5
|
+
|
6
|
+
# Rack handler for {RaptorIO::Protocol::HTTP::Server}.
|
7
|
+
class Rack::Handler::RaptorIO
|
8
|
+
|
9
|
+
Rack::Handler.register self.to_s.split( ':' ).last.downcase, self
|
10
|
+
|
11
|
+
# Starts the server and runs the `app`.
|
12
|
+
#
|
13
|
+
# @param [#call] app Rack Application to run.
|
14
|
+
# @param [Hash] options Rack options.
|
15
|
+
def self.run( app, options = {} )
|
16
|
+
return false if @server
|
17
|
+
|
18
|
+
options[:address] = options.delete(:Host) || default_host
|
19
|
+
options[:port] ||= options.delete(:Port) || 8080
|
20
|
+
|
21
|
+
@app = app
|
22
|
+
@server = ::RaptorIO::Protocol::HTTP::Server.new( options ) do |response|
|
23
|
+
service response
|
24
|
+
end
|
25
|
+
yield @server if block_given?
|
26
|
+
@server.run
|
27
|
+
|
28
|
+
true
|
29
|
+
end
|
30
|
+
|
31
|
+
# Shuts down the server.
|
32
|
+
def self.shutdown
|
33
|
+
return false if !@server
|
34
|
+
|
35
|
+
@server.stop
|
36
|
+
@server = nil
|
37
|
+
|
38
|
+
true
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def self.valid_options
|
44
|
+
{
|
45
|
+
'Host=HOST' => "Hostname to listen on (default: #{default_host})",
|
46
|
+
'Port=PORT' => 'Port to listen on (default: 8080)'
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.default_host
|
51
|
+
(ENV['RACK_ENV'] || 'development') == 'development' ? 'localhost' : '0.0.0.0'
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.service( response )
|
55
|
+
request = response.request
|
56
|
+
path = request.effective_url.path
|
57
|
+
http_version = "HTTP/#{request.version}"
|
58
|
+
|
59
|
+
query_string = request.effective_url.to_s.split( '?' ).last.to_s
|
60
|
+
query_string = '' if query_string == '/'
|
61
|
+
|
62
|
+
environment = {
|
63
|
+
'REQUEST_METHOD' => request.http_method.to_s.upcase,
|
64
|
+
'SCRIPT_NAME' => '',
|
65
|
+
'PATH_INFO' => path,
|
66
|
+
'REQUEST_PATH' => path,
|
67
|
+
'QUERY_STRING' => query_string,
|
68
|
+
'SERVER_NAME' => @server.address,
|
69
|
+
'SERVER_PORT' => @server.port.to_s,
|
70
|
+
'HTTP_VERSION' => http_version,
|
71
|
+
'REMOTE_ADDR' => request.client_address
|
72
|
+
}
|
73
|
+
|
74
|
+
request.headers.each do |k, v|
|
75
|
+
environment["HTTP_#{k.upcase.gsub( '-', '_' )}"] = v
|
76
|
+
end
|
77
|
+
|
78
|
+
if environment['HTTP_CONTENT_TYPE']
|
79
|
+
environment['CONTENT_TYPE'] = environment.delete( 'HTTP_CONTENT_TYPE' )
|
80
|
+
end
|
81
|
+
|
82
|
+
if environment['HTTP_CONTENT_LENGTH']
|
83
|
+
environment['CONTENT_LENGTH'] = environment.delete( 'HTTP_CONTENT_LENGTH' )
|
84
|
+
end
|
85
|
+
|
86
|
+
environment['SERVER_PROTOCOL'] = environment['HTTP_VERSION']
|
87
|
+
|
88
|
+
rack_input = StringIO.new( request.body.to_s )
|
89
|
+
rack_input.set_encoding( Encoding::BINARY ) if rack_input.respond_to?( :set_encoding )
|
90
|
+
|
91
|
+
environment.update(
|
92
|
+
'rack.version' => Rack::VERSION,
|
93
|
+
'rack.input' => rack_input,
|
94
|
+
'rack.errors' => $stderr,
|
95
|
+
'rack.multithread' => true,
|
96
|
+
'rack.multiprocess' => false,
|
97
|
+
'rack.run_once' => false,
|
98
|
+
'rack.url_scheme' => 'http',
|
99
|
+
'rack.hijack?' => false,
|
100
|
+
'raptor.request' => request
|
101
|
+
)
|
102
|
+
|
103
|
+
begin
|
104
|
+
status, headers, body = @app.call( environment )
|
105
|
+
body = '' if !body
|
106
|
+
|
107
|
+
response.code = status
|
108
|
+
|
109
|
+
if body.is_a? String
|
110
|
+
response.body = body
|
111
|
+
else
|
112
|
+
body.each { |part| (response.body ||= '') << part }
|
113
|
+
end
|
114
|
+
|
115
|
+
response.headers.merge! headers
|
116
|
+
rescue RuntimeError => e
|
117
|
+
response.code = 501
|
118
|
+
response.body = "#{e} (#{e.class})"
|
119
|
+
|
120
|
+
environment['rack.errors'].puts "#{e} (#{e.class})"
|
121
|
+
e.backtrace.each do |line|
|
122
|
+
environment['rack.errors'].puts line
|
123
|
+
end
|
124
|
+
|
125
|
+
response.headers['content-type'] = 'text/plain'
|
126
|
+
end
|
127
|
+
ensure
|
128
|
+
body.close if body.respond_to? :close
|
129
|
+
end
|
130
|
+
end
|
data/lib/raptor-io.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
|
2
|
+
module RaptorIO
|
3
|
+
|
4
|
+
#
|
5
|
+
# Represents a RaptorIO error base-class and also provides the namespace for all
|
6
|
+
# RaptorIO errors.
|
7
|
+
#
|
8
|
+
# @author Tasos Laskos <tasos_laskos@rapid7.com>
|
9
|
+
#
|
10
|
+
class Error < StandardError
|
11
|
+
|
12
|
+
# {RaptorIO} timeout error.
|
13
|
+
#
|
14
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
15
|
+
class Timeout < Error
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'raptor-io/socket'
|
3
|
+
require 'base64'
|
4
|
+
require 'cgi'
|
5
|
+
require 'uri'
|
6
|
+
|
7
|
+
#
|
8
|
+
# HTTP protocol implementation.
|
9
|
+
#
|
10
|
+
# @author Tasos Laskos <tasos_laskos@rapid7.com>
|
11
|
+
#
|
12
|
+
module RaptorIO::Protocol::HTTP
|
13
|
+
|
14
|
+
# Matches line separator characters for HTTP messages.
|
15
|
+
CRLF_PATTERN = /\r?\n/
|
16
|
+
|
17
|
+
# CRLF character sequence.
|
18
|
+
CRLF = "\r\n"
|
19
|
+
|
20
|
+
# Matches sequence used to separate headers from the body.
|
21
|
+
HEADER_SEPARATOR_PATTERN = /\r?\n\r?\n/
|
22
|
+
|
23
|
+
# Header separator character sequence.
|
24
|
+
HEADER_SEPARATOR = "\r\n\r\n"
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
require 'raptor-io/protocol/http/error'
|
29
|
+
require 'raptor-io/protocol/http/headers'
|
30
|
+
require 'raptor-io/protocol/http/message'
|
31
|
+
require 'raptor-io/protocol/http/request'
|
32
|
+
require 'raptor-io/protocol/http/response'
|
33
|
+
require 'raptor-io/protocol/http/server'
|
34
|
+
require 'raptor-io/protocol/http/client'
|
@@ -0,0 +1,685 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
module RaptorIO
|
5
|
+
module Protocol::HTTP
|
6
|
+
|
7
|
+
#
|
8
|
+
# HTTP Client class.
|
9
|
+
#
|
10
|
+
# @author Tasos Laskos <tasos_laskos@rapid7.com>
|
11
|
+
#
|
12
|
+
class Client
|
13
|
+
|
14
|
+
# @return [Integer, Float] Timeout in seconds.
|
15
|
+
attr_accessor :timeout
|
16
|
+
|
17
|
+
# @return [Integer] Maximum open sockets.
|
18
|
+
attr_accessor :concurrency
|
19
|
+
|
20
|
+
# @return [String] User-agent string to use.
|
21
|
+
attr_accessor :user_agent
|
22
|
+
|
23
|
+
# @return [Hash{Symbol=>Hash}]
|
24
|
+
# Request manipulators, and their options, to be run against each
|
25
|
+
# {#queue queued} request.
|
26
|
+
attr_accessor :manipulators
|
27
|
+
|
28
|
+
# @return [Hash] Persistent storage for the manipulators..
|
29
|
+
attr_accessor :datastore
|
30
|
+
|
31
|
+
# @return [Symbol] SSL version.
|
32
|
+
attr_accessor :ssl_version
|
33
|
+
|
34
|
+
# @return [Constant] Peer verification mode.
|
35
|
+
attr_accessor :ssl_verify_mode
|
36
|
+
|
37
|
+
# @return [OpenSSL::SSL::SSLContext] SSL context to use.
|
38
|
+
attr_accessor :ssl_context
|
39
|
+
|
40
|
+
# @return [SwitchBoard] The routing table from which this {Client}
|
41
|
+
# will {Socket::SwitchBoard#create_tcp make new TCP connections}.
|
42
|
+
attr_reader :switch_board
|
43
|
+
|
44
|
+
# Default client options.
|
45
|
+
DEFAULT_OPTIONS = {
|
46
|
+
concurrency: 20,
|
47
|
+
user_agent: "RaptorIO::HTTP/#{RaptorIO::VERSION}",
|
48
|
+
timeout: 10,
|
49
|
+
manipulators: {},
|
50
|
+
ssl_version: :TLSv1,
|
51
|
+
ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE,
|
52
|
+
ssl_context: nil
|
53
|
+
}.freeze
|
54
|
+
|
55
|
+
# @param [Hash] options Request options.
|
56
|
+
# @option options [Integer] :concurrency (20)
|
57
|
+
# Amount of open sockets at any given time.
|
58
|
+
# @option options [String] :user_agent ('RaptorIO::HTTP/<RaptorIO::VERSION>')
|
59
|
+
# User-agent string to include in the requests.
|
60
|
+
# @option options [Integer, Float] :timeout (10)
|
61
|
+
# Timeout in seconds.
|
62
|
+
# @option options [Hash{Symbol=>Hash}] :manipulators
|
63
|
+
# Request manipulators and their options.
|
64
|
+
# @option options [Symbol] :ssl_version (:TLSv1)
|
65
|
+
# @option options [Constant] :ssl_verify_mode (OpenSSL::SSL::VERIFY_NONE)
|
66
|
+
# Peer verification mode.
|
67
|
+
# @option options [OpenSSL::SSL::SSLContext] :ssl_context (nil)
|
68
|
+
# SSL context to use.
|
69
|
+
# @option options [Socket::SwitchBoard] :switch_board The switch board
|
70
|
+
# from which we can create new connections.
|
71
|
+
#
|
72
|
+
# @raise RaptorIO::Protocol::HTTP::Request::Manipulator::Error::InvalidOptions
|
73
|
+
# On invalid manipulator options.
|
74
|
+
def initialize( options = {} )
|
75
|
+
@switch_board = options.delete(:switch_board)
|
76
|
+
unless @switch_board.respond_to?(:create_tcp)
|
77
|
+
raise ArgumentError, 'Must provide a :switch_board'
|
78
|
+
end
|
79
|
+
|
80
|
+
DEFAULT_OPTIONS.merge( options ).each do |k, v|
|
81
|
+
begin
|
82
|
+
send( "#{k}=", try_dup( v ) )
|
83
|
+
rescue NoMethodError
|
84
|
+
instance_variable_set( "@#{k}".to_sym, try_dup( v ) )
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
validate_manipulators!( manipulators )
|
89
|
+
|
90
|
+
# Holds Request objects.
|
91
|
+
@queue = []
|
92
|
+
|
93
|
+
# Persistent storage for request manipulators.
|
94
|
+
@datastore = Hash.new { |h, k| h[k] = {} }
|
95
|
+
|
96
|
+
reset_sockets
|
97
|
+
reset_pending_responses
|
98
|
+
end
|
99
|
+
|
100
|
+
#
|
101
|
+
# Updates the client {#manipulators} and perform and sanity check on their
|
102
|
+
# options.
|
103
|
+
#
|
104
|
+
# @param [Hash{String=>Hash}] manipulators
|
105
|
+
# Manipulators and their options.
|
106
|
+
#
|
107
|
+
# @raise RaptorIO::Protocol::HTTP::Request::Manipulator::Error::InvalidOptions
|
108
|
+
def update_manipulators( manipulators )
|
109
|
+
validate_manipulators!( manipulators )
|
110
|
+
@manipulators.merge!( manipulators )
|
111
|
+
end
|
112
|
+
|
113
|
+
#
|
114
|
+
# Creates and {#queue queues} a {Request}.
|
115
|
+
#
|
116
|
+
# @param [String] url URL of the remote resource.
|
117
|
+
# @param [Hash] options {Request} options with the following extras:
|
118
|
+
# @option options [Symbol] :mode (:async)
|
119
|
+
# Mode to use for the request, available options are:
|
120
|
+
#
|
121
|
+
# * `:async` -- Adds the request to the queue.
|
122
|
+
# * `:sync` -- Performs the request in a blocking manner and returns the
|
123
|
+
# {Response}.
|
124
|
+
#
|
125
|
+
# @option options [Hash, String] :cookies
|
126
|
+
# Cookies as name=>pair values -- should already be escaped.
|
127
|
+
#
|
128
|
+
# @option options [Hash{Symbol=>Hash}] :manipulators
|
129
|
+
# Manipulator names for keys and their options as their values.
|
130
|
+
#
|
131
|
+
# @param [Block] block Callback to be passed the {Response}.
|
132
|
+
#
|
133
|
+
# @return [Request, Response]
|
134
|
+
# Queued {Request} when in `:async` `:mode`, {Response} when in `:sync`
|
135
|
+
# `:mode`.
|
136
|
+
#
|
137
|
+
# @see Request#initialize
|
138
|
+
# @see Request#on_complete
|
139
|
+
# @see #queue
|
140
|
+
#
|
141
|
+
def request( url, options = {}, &block )
|
142
|
+
options = options.dup
|
143
|
+
options[:timeout] ||= @timeout
|
144
|
+
|
145
|
+
req = Request.new( options.merge( url: url ) )
|
146
|
+
|
147
|
+
req.headers['User-Agent'] = @user_agent if !@user_agent.to_s.empty?
|
148
|
+
|
149
|
+
case options[:cookies]
|
150
|
+
when Hash
|
151
|
+
req.headers['Cookie'] =
|
152
|
+
options[:cookies].map { |k, v| "#{k}=#{v}" }.join( ';' )
|
153
|
+
when String
|
154
|
+
req.headers['Cookie'] = options[:cookies]
|
155
|
+
end
|
156
|
+
|
157
|
+
return sync_request( req, options[:manipulators] || {} ) if options[:mode] == :sync
|
158
|
+
|
159
|
+
req.on_complete( &block ) if block_given?
|
160
|
+
|
161
|
+
queue( req, options[:manipulators] || {} )
|
162
|
+
req
|
163
|
+
end
|
164
|
+
|
165
|
+
#
|
166
|
+
# Creates and {#queue queues} a GET {Request}.
|
167
|
+
#
|
168
|
+
# @param (see #request)
|
169
|
+
# @return (see #request)
|
170
|
+
#
|
171
|
+
def get( url, options = {}, &block )
|
172
|
+
request( url, options.merge( http_method: :get ), &block )
|
173
|
+
end
|
174
|
+
|
175
|
+
#
|
176
|
+
# Creates and {#queue queues} a POST {Request}.
|
177
|
+
#
|
178
|
+
# @param (see #request)
|
179
|
+
# @return (see #request)
|
180
|
+
#
|
181
|
+
def post( url, options = {}, &block )
|
182
|
+
request( url, options.merge( http_method: :post ), &block )
|
183
|
+
end
|
184
|
+
|
185
|
+
# @return [Integer] The amount of {#queue queued} requests.
|
186
|
+
def queue_size
|
187
|
+
@queue.size
|
188
|
+
end
|
189
|
+
|
190
|
+
#
|
191
|
+
# Queues a {Request}.
|
192
|
+
#
|
193
|
+
# @param [Request] request
|
194
|
+
# @param [Hash{Symbol=>Hash}] manipulators
|
195
|
+
# Manipulator names for keys and their options as their values.
|
196
|
+
# @return [Request] `request`
|
197
|
+
#
|
198
|
+
def queue( request, manipulators = {} )
|
199
|
+
validate_manipulators!( manipulators )
|
200
|
+
|
201
|
+
request.timeout ||= timeout
|
202
|
+
|
203
|
+
@manipulators.merge( manipulators ).each do |manipulator, options|
|
204
|
+
Request::Manipulators.process( manipulator, self, request, options )
|
205
|
+
end
|
206
|
+
|
207
|
+
@queue << request
|
208
|
+
request
|
209
|
+
end
|
210
|
+
alias :<< :queue
|
211
|
+
|
212
|
+
# Runs the {#queue queued} {Request}.
|
213
|
+
def run
|
214
|
+
while @queue.any?
|
215
|
+
# Get us some seeds.
|
216
|
+
consume_requests
|
217
|
+
|
218
|
+
while @sockets[:done].size != @sockets[:lookup_request].size
|
219
|
+
|
220
|
+
if @sockets[:reads].any?
|
221
|
+
# Use the lowest available timeout for #select.
|
222
|
+
lowest_timeout =
|
223
|
+
@sockets[:reads].map { |socket| @pending_responses[socket][:timeout] }.sort.first
|
224
|
+
|
225
|
+
clock = Time.now
|
226
|
+
res = select( @sockets[:reads], nil, @sockets[:reads], lowest_timeout )
|
227
|
+
waiting_time = Time.now - clock
|
228
|
+
|
229
|
+
# Adjust the timeouts for *all* sockets since they all benefited from
|
230
|
+
# the #select waiting period which just elapsed.
|
231
|
+
#
|
232
|
+
# And this is the whole reason for keeping track of timeouts externally.
|
233
|
+
@pending_responses.each do |_, pending_response|
|
234
|
+
pending_response[:timeout] -= waiting_time
|
235
|
+
pending_response[:timeout] = 0 if pending_response[:timeout] < 0
|
236
|
+
end
|
237
|
+
|
238
|
+
# #select timed out, go digging.
|
239
|
+
if !res
|
240
|
+
# Find and handle the sockets which timed out.
|
241
|
+
@sockets[:reads].each do |socket|
|
242
|
+
if waiting_time >= @pending_responses[socket][:timeout]
|
243
|
+
error = RaptorIO::Error::Timeout.new( 'Request timed-out.' )
|
244
|
+
error.set_backtrace( caller )
|
245
|
+
handle_error( @sockets[:lookup_request][socket], error, socket )
|
246
|
+
end
|
247
|
+
|
248
|
+
# Fill the available pool space.
|
249
|
+
consume_requests
|
250
|
+
end
|
251
|
+
|
252
|
+
# #select didn't time out, yay!
|
253
|
+
else
|
254
|
+
|
255
|
+
# Handle sockets with errors -- like reset connections.
|
256
|
+
if res[2].any?
|
257
|
+
res[2].each do |socket|
|
258
|
+
handle_error( @sockets[:lookup_request][socket], nil, socket )
|
259
|
+
|
260
|
+
# Fill the available pool space.
|
261
|
+
consume_requests
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
# Handle sockets which are ready to be read.
|
266
|
+
if res[0].any?
|
267
|
+
res[0].each do |socket|
|
268
|
+
# Buffer/handle the response for the given socket.
|
269
|
+
read( socket )
|
270
|
+
|
271
|
+
# Fill the available pool space.
|
272
|
+
consume_requests
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
next if @sockets[:writes].empty?
|
279
|
+
|
280
|
+
_, writes, errors = select( nil, @sockets[:writes], @sockets[:writes] )
|
281
|
+
|
282
|
+
errors.each do |socket|
|
283
|
+
handle_error( @sockets[:lookup_request][socket], nil, socket )
|
284
|
+
end
|
285
|
+
|
286
|
+
writes.each do |socket|
|
287
|
+
# Send the request for the given socket.
|
288
|
+
write( socket )
|
289
|
+
|
290
|
+
# Fully utilize our socket allowance.
|
291
|
+
consume_requests
|
292
|
+
end
|
293
|
+
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
reset_sockets
|
298
|
+
reset_pending_responses
|
299
|
+
|
300
|
+
nil
|
301
|
+
end
|
302
|
+
|
303
|
+
# @return [Integer] Amount of open sockets.
|
304
|
+
def open_socket_count
|
305
|
+
open_sockets.size
|
306
|
+
end
|
307
|
+
|
308
|
+
private
|
309
|
+
|
310
|
+
# @param [Request] request Request to perform in blocking mode.
|
311
|
+
# @return [Response] HTTP response.
|
312
|
+
def sync_request( request, manipulators = {} )
|
313
|
+
client = self.class.new(
|
314
|
+
switch_board: @switch_board,
|
315
|
+
user_agent: user_agent,
|
316
|
+
timeout: timeout
|
317
|
+
)
|
318
|
+
|
319
|
+
# The normal and sync clients should share these structures, that's why
|
320
|
+
# we're not passing them via the initializer.
|
321
|
+
client.manipulators = @manipulators
|
322
|
+
client.datastore = @datastore
|
323
|
+
|
324
|
+
res = nil
|
325
|
+
request.on_complete { |r| res = r }
|
326
|
+
client.queue( request, manipulators )
|
327
|
+
client.run
|
328
|
+
|
329
|
+
raise res.error if res.error
|
330
|
+
|
331
|
+
res
|
332
|
+
end
|
333
|
+
|
334
|
+
# @return [Array<Socket>] Sockets currently open.
|
335
|
+
def open_sockets
|
336
|
+
@sockets[:writes] + @sockets[:reads]
|
337
|
+
end
|
338
|
+
|
339
|
+
#
|
340
|
+
# Writes the associated {Request} to `socket`.
|
341
|
+
#
|
342
|
+
# @param [#write] socket Writable IO object.
|
343
|
+
#
|
344
|
+
def write( socket, retry_on_fail = true )
|
345
|
+
request = @sockets[:lookup_request][socket]
|
346
|
+
request_string = request.to_s.repack
|
347
|
+
|
348
|
+
# Send out the request, **all** of it.
|
349
|
+
loop do
|
350
|
+
begin
|
351
|
+
bytes_written = socket.write( request_string )
|
352
|
+
# All hope is lost.
|
353
|
+
rescue RaptorIO::Socket::Error::ConnectionError => error
|
354
|
+
handle_error( request, error, socket )
|
355
|
+
return
|
356
|
+
|
357
|
+
# The connection has been closed so retry but only if the request is
|
358
|
+
# idempotent.
|
359
|
+
rescue RaptorIO::Socket::Error::BrokenPipe => e
|
360
|
+
if request.idempotent? && retry_on_fail
|
361
|
+
@sockets[:writes].delete( socket )
|
362
|
+
|
363
|
+
fresh_socket = refresh_connection( socket )
|
364
|
+
@sockets[:writes] << fresh_socket
|
365
|
+
return write( fresh_socket, false )
|
366
|
+
else
|
367
|
+
error = Protocol::Error::BrokenPipe.new( e.to_s )
|
368
|
+
error.set_backtrace( e.backtrace )
|
369
|
+
handle_error( request, error, socket )
|
370
|
+
return
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
break if bytes_written == request_string.size
|
375
|
+
|
376
|
+
request_string = request_string[bytes_written..-1]
|
377
|
+
end
|
378
|
+
|
379
|
+
# Move it to the read list.
|
380
|
+
@sockets[:reads] << @sockets[:writes].delete( socket )
|
381
|
+
|
382
|
+
true
|
383
|
+
end
|
384
|
+
|
385
|
+
#
|
386
|
+
# Reads/buffers a response from `socket` and calls the callback of the
|
387
|
+
# associated request once the full response is received -- at which point it
|
388
|
+
# also closes the socket.
|
389
|
+
#
|
390
|
+
# @param [#gets] socket Readable IO object.
|
391
|
+
#
|
392
|
+
# @return [true, nil]
|
393
|
+
# `true` if the response finished being buffered, `nil` otherwise.
|
394
|
+
#
|
395
|
+
def read( socket )
|
396
|
+
reset_timeout( socket )
|
397
|
+
|
398
|
+
response = @pending_responses[socket]
|
399
|
+
|
400
|
+
if response[:has_full_headers]
|
401
|
+
headers = response[:parsed_headers]
|
402
|
+
|
403
|
+
if headers['Transfer-Encoding'].to_s.downcase == 'chunked'
|
404
|
+
read_size = socket.gets.to_s[0...-CRLF.size]
|
405
|
+
return if read_size.empty?
|
406
|
+
|
407
|
+
if (read_size = read_size.to_i( 16 )) > 0
|
408
|
+
response[:body] << socket.read( read_size + CRLF.size ).to_s[0...read_size]
|
409
|
+
return
|
410
|
+
end
|
411
|
+
else
|
412
|
+
# A Content-Type is not strictly necessary, the end of the response body
|
413
|
+
# can also be signaled by the server closing the connection. That's why
|
414
|
+
# the following code is so ugly.
|
415
|
+
|
416
|
+
read_size = nil
|
417
|
+
if (content_length = headers['Content-length'].to_i) > 0
|
418
|
+
read_size = content_length - response[:body].size
|
419
|
+
end
|
420
|
+
|
421
|
+
has_body = headers['Content-length'] != '0'
|
422
|
+
|
423
|
+
closed = false
|
424
|
+
if has_body
|
425
|
+
begin
|
426
|
+
line = (read_size ? socket.read(read_size) : socket.gets)
|
427
|
+
if line
|
428
|
+
response[:body] << line
|
429
|
+
else
|
430
|
+
raise RaptorIO::Socket::Error::BrokenPipe
|
431
|
+
end
|
432
|
+
rescue RaptorIO::Socket::Error::BrokenPipe
|
433
|
+
closed = true
|
434
|
+
response[:force_no_keep_alive] = true
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
438
|
+
# Return back to the #select loop if there's more data to be read
|
439
|
+
# and wait for our next turn.
|
440
|
+
return if has_body && ((!read_size && !closed) || (response[:body].size < content_length))
|
441
|
+
end
|
442
|
+
|
443
|
+
handle_success( socket )
|
444
|
+
return true
|
445
|
+
end
|
446
|
+
|
447
|
+
response[:headers] << socket.gets.to_s
|
448
|
+
|
449
|
+
# Keep going until we get all the headers.
|
450
|
+
return if !(response[:headers] =~ HEADER_SEPARATOR_PATTERN)
|
451
|
+
response[:has_full_headers] = true
|
452
|
+
|
453
|
+
# Perform some preliminary parsing to make our lives easier.
|
454
|
+
response[:partial_response] = Response.parse( response[:headers] )
|
455
|
+
response[:parsed_headers] = response[:partial_response].headers
|
456
|
+
|
457
|
+
# Some of the body may have gotten into the headers' buffer, sort them out.
|
458
|
+
response[:headers], response[:body] = response[:headers].split( HEADER_SEPARATOR_PATTERN, 2 )
|
459
|
+
|
460
|
+
# If there is no body to expect handle the response now.
|
461
|
+
if response[:partial_response].headers['Content-length'] == '0' ||
|
462
|
+
status_without_body?( response[:partial_response].code )
|
463
|
+
handle_success( socket )
|
464
|
+
return true
|
465
|
+
end
|
466
|
+
|
467
|
+
nil
|
468
|
+
end
|
469
|
+
|
470
|
+
def reset_timeout( socket )
|
471
|
+
@pending_responses[socket][:timeout] = @sockets[:lookup_request][socket].timeout
|
472
|
+
end
|
473
|
+
|
474
|
+
#
|
475
|
+
# @note Respects the {#concurrency} limit.
|
476
|
+
#
|
477
|
+
# Consumes requests from the queue and adds write-sockets for them.
|
478
|
+
#
|
479
|
+
# @return [Integer] Amount of requests consumed.
|
480
|
+
#
|
481
|
+
def consume_requests
|
482
|
+
added = 0
|
483
|
+
|
484
|
+
(@concurrency - open_socket_count).times do
|
485
|
+
return added if @queue.empty?
|
486
|
+
q_request = @queue.pop
|
487
|
+
|
488
|
+
socket = connection_for_request( q_request )
|
489
|
+
next if !socket
|
490
|
+
|
491
|
+
@sockets[:lookup_request][socket] = q_request
|
492
|
+
@sockets[:writes] << socket
|
493
|
+
|
494
|
+
added += 1
|
495
|
+
end
|
496
|
+
|
497
|
+
added
|
498
|
+
end
|
499
|
+
|
500
|
+
# @param [Request] request
|
501
|
+
def connection_for_request( request )
|
502
|
+
# If the request is idempotent grab a pool connection as we can risk a retry
|
503
|
+
# in case it has been closed...
|
504
|
+
socket = if request.idempotent?
|
505
|
+
# If there's an idling connection to that server, use it instead of opening
|
506
|
+
# a new one.
|
507
|
+
if connection_pool[request.connection_id].empty?
|
508
|
+
connect( request )
|
509
|
+
else
|
510
|
+
connection_pool[request.connection_id].pop
|
511
|
+
end
|
512
|
+
|
513
|
+
# ...otherwise establish and use a new connection (and make room
|
514
|
+
# in the queue).
|
515
|
+
else
|
516
|
+
if !connection_pool[request.connection_id].empty?
|
517
|
+
connection_pool[request.connection_id].pop.close
|
518
|
+
end
|
519
|
+
connect( request )
|
520
|
+
end
|
521
|
+
|
522
|
+
@sockets[:done].delete( socket )
|
523
|
+
socket
|
524
|
+
end
|
525
|
+
|
526
|
+
def self.reset
|
527
|
+
connection_pool.each do |_, q|
|
528
|
+
q.pop.close while !q.empty?
|
529
|
+
end
|
530
|
+
nil
|
531
|
+
end
|
532
|
+
|
533
|
+
def self.connection_pool
|
534
|
+
@connection_pool ||= Hash.new do |h, k|
|
535
|
+
h[k] = Queue.new
|
536
|
+
end
|
537
|
+
end
|
538
|
+
def connection_pool
|
539
|
+
self.class.connection_pool
|
540
|
+
end
|
541
|
+
|
542
|
+
# Opens up an non-blocking socket for the given `request`.
|
543
|
+
#
|
544
|
+
# @param [Request] request
|
545
|
+
def connect( request )
|
546
|
+
@addresses ||= {}
|
547
|
+
|
548
|
+
host = request.parsed_url.host
|
549
|
+
port = request.parsed_url.port
|
550
|
+
cid = request.connection_id
|
551
|
+
|
552
|
+
begin
|
553
|
+
socket = @switch_board.create_tcp(
|
554
|
+
peer_host: (@addresses[cid] ||= Socket.getaddrinfo( host, nil ))[0][3],
|
555
|
+
peer_port: port,
|
556
|
+
connect_timeout: @timeout
|
557
|
+
)
|
558
|
+
|
559
|
+
if request.parsed_url.scheme.to_s == 'https'
|
560
|
+
socket = socket.to_ssl(
|
561
|
+
ssl_context: @ssl_context,
|
562
|
+
ssl_version: @ssl_version,
|
563
|
+
ssl_verify_mode: @ssl_verify_mode
|
564
|
+
)
|
565
|
+
end
|
566
|
+
|
567
|
+
socket
|
568
|
+
rescue RaptorIO::Socket::Error => e
|
569
|
+
handle_error( request, e )
|
570
|
+
nil
|
571
|
+
end
|
572
|
+
end
|
573
|
+
|
574
|
+
def handle_success( socket )
|
575
|
+
response_data = @pending_responses.delete( socket )
|
576
|
+
@sockets[:done] << @sockets[:reads].delete( socket )
|
577
|
+
response_text = "#{response_data[:headers]}#{HEADER_SEPARATOR}#{response_data[:body]}"
|
578
|
+
response = Response.parse( response_text )
|
579
|
+
request = @sockets[:lookup_request][socket]
|
580
|
+
|
581
|
+
if response.keep_alive? && !response_data[:force_no_keep_alive]
|
582
|
+
connection_pool[request.connection_id] << socket
|
583
|
+
else
|
584
|
+
socket.close
|
585
|
+
end
|
586
|
+
|
587
|
+
if response.code == 100 && request.continue?
|
588
|
+
request = request.dup
|
589
|
+
request.headers.delete 'expect'
|
590
|
+
queue( request.dup )
|
591
|
+
return
|
592
|
+
end
|
593
|
+
|
594
|
+
request.handle_response response
|
595
|
+
end
|
596
|
+
|
597
|
+
def handle_error( request, error = nil, socket = nil )
|
598
|
+
if socket
|
599
|
+
socket.close
|
600
|
+
[:reads, :writes].each { |state| @sockets[state].delete( socket ) }
|
601
|
+
@sockets[:done] << socket
|
602
|
+
end
|
603
|
+
|
604
|
+
response = Response.new( error: error )
|
605
|
+
request.handle_response response
|
606
|
+
end
|
607
|
+
|
608
|
+
def reset_sockets
|
609
|
+
# A socket starts in `:writes`, once the request is written it gets moved
|
610
|
+
# to `:reads`, at which point it stays there while the response is being
|
611
|
+
# buffered. Once a full response is received, and keep-alive is disabled, the
|
612
|
+
# socket is closed and moved to `:done`.
|
613
|
+
#
|
614
|
+
# If keep-alive is enabled, the connection is moved to `:done` without
|
615
|
+
# first being closed and will be reused appropriately, at which point it
|
616
|
+
# will be removed from `:done` and start all over.
|
617
|
+
@sockets = {
|
618
|
+
# Socket => HTTP Request lookup
|
619
|
+
lookup_request: {},
|
620
|
+
|
621
|
+
# Sockets ready to read from.
|
622
|
+
reads: [],
|
623
|
+
|
624
|
+
# Sockets ready to write to.
|
625
|
+
writes: [],
|
626
|
+
|
627
|
+
# Sockets with errors.
|
628
|
+
errors: [],
|
629
|
+
|
630
|
+
# Sockets which have finished (or errored).
|
631
|
+
done: []
|
632
|
+
}
|
633
|
+
end
|
634
|
+
|
635
|
+
def reset_pending_responses
|
636
|
+
# Response buffer.
|
637
|
+
@pending_responses = Hash.new do |h, socket|
|
638
|
+
h[socket] = {
|
639
|
+
# Do we have full headers?
|
640
|
+
has_full_headers: false,
|
641
|
+
|
642
|
+
# HTTP headers buffer.
|
643
|
+
headers: '',
|
644
|
+
|
645
|
+
# Response body buffer.
|
646
|
+
body: '',
|
647
|
+
|
648
|
+
# We use this to keep track of individual socket timeouts.
|
649
|
+
timeout: @sockets[:lookup_request][socket].timeout
|
650
|
+
}
|
651
|
+
end
|
652
|
+
end
|
653
|
+
|
654
|
+
def refresh_connection( socket )
|
655
|
+
request = @sockets[:lookup_request].delete( socket )
|
656
|
+
fresh_socket = connection_for_request( request )
|
657
|
+
|
658
|
+
@sockets[:lookup_request][fresh_socket] = request
|
659
|
+
fresh_socket
|
660
|
+
ensure
|
661
|
+
socket.close
|
662
|
+
@pending_responses.delete( socket )
|
663
|
+
[:reads, :writes].each { |state| @sockets[state].delete( socket ) }
|
664
|
+
end
|
665
|
+
|
666
|
+
def status_without_body?( status_code )
|
667
|
+
status_code.to_s.start_with?( '1' ) || [204, 304].include?( status_code.to_i )
|
668
|
+
end
|
669
|
+
|
670
|
+
def validate_manipulators!( manipulators )
|
671
|
+
Request::Manipulators.validate_batch_options!( manipulators, self )
|
672
|
+
end
|
673
|
+
|
674
|
+
def validate_manipulators( manipulators )
|
675
|
+
Request::Manipulators.validate_batch_options( manipulators, self )
|
676
|
+
end
|
677
|
+
|
678
|
+
def try_dup( value )
|
679
|
+
value.dup rescue value
|
680
|
+
end
|
681
|
+
|
682
|
+
end
|
683
|
+
|
684
|
+
end
|
685
|
+
end
|