raptor-io 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. checksums.yaml +15 -0
  2. data/LICENSE +30 -0
  3. data/README.md +51 -0
  4. data/lib/rack/handler/raptor-io.rb +130 -0
  5. data/lib/raptor-io.rb +11 -0
  6. data/lib/raptor-io/error.rb +19 -0
  7. data/lib/raptor-io/protocol.rb +6 -0
  8. data/lib/raptor-io/protocol/error.rb +10 -0
  9. data/lib/raptor-io/protocol/http.rb +34 -0
  10. data/lib/raptor-io/protocol/http/client.rb +685 -0
  11. data/lib/raptor-io/protocol/http/error.rb +16 -0
  12. data/lib/raptor-io/protocol/http/headers.rb +132 -0
  13. data/lib/raptor-io/protocol/http/message.rb +67 -0
  14. data/lib/raptor-io/protocol/http/request.rb +307 -0
  15. data/lib/raptor-io/protocol/http/request/manipulator.rb +117 -0
  16. data/lib/raptor-io/protocol/http/request/manipulators.rb +217 -0
  17. data/lib/raptor-io/protocol/http/request/manipulators/authenticator.rb +110 -0
  18. data/lib/raptor-io/protocol/http/request/manipulators/authenticators/basic.rb +36 -0
  19. data/lib/raptor-io/protocol/http/request/manipulators/authenticators/digest.rb +135 -0
  20. data/lib/raptor-io/protocol/http/request/manipulators/authenticators/negotiate.rb +69 -0
  21. data/lib/raptor-io/protocol/http/request/manipulators/authenticators/ntlm.rb +29 -0
  22. data/lib/raptor-io/protocol/http/request/manipulators/redirect_follower.rb +65 -0
  23. data/lib/raptor-io/protocol/http/response.rb +166 -0
  24. data/lib/raptor-io/protocol/http/server.rb +446 -0
  25. data/lib/raptor-io/ruby.rb +4 -0
  26. data/lib/raptor-io/ruby/hash.rb +24 -0
  27. data/lib/raptor-io/ruby/ipaddr.rb +15 -0
  28. data/lib/raptor-io/ruby/openssl.rb +23 -0
  29. data/lib/raptor-io/ruby/string.rb +27 -0
  30. data/lib/raptor-io/socket.rb +175 -0
  31. data/lib/raptor-io/socket/comm.rb +143 -0
  32. data/lib/raptor-io/socket/comm/local.rb +94 -0
  33. data/lib/raptor-io/socket/comm/sapni.rb +75 -0
  34. data/lib/raptor-io/socket/comm/socks.rb +237 -0
  35. data/lib/raptor-io/socket/comm_chain.rb +30 -0
  36. data/lib/raptor-io/socket/error.rb +45 -0
  37. data/lib/raptor-io/socket/switch_board.rb +183 -0
  38. data/lib/raptor-io/socket/switch_board/route.rb +42 -0
  39. data/lib/raptor-io/socket/tcp.rb +231 -0
  40. data/lib/raptor-io/socket/tcp/ssl.rb +77 -0
  41. data/lib/raptor-io/socket/tcp_server.rb +16 -0
  42. data/lib/raptor-io/socket/tcp_server/ssl.rb +52 -0
  43. data/lib/raptor-io/socket/udp.rb +0 -0
  44. data/lib/raptor-io/version.rb +6 -0
  45. data/lib/tasks/yard.rake +26 -0
  46. data/spec/rack/handler/raptor_spec.rb +140 -0
  47. data/spec/raptor-io/protocol/http/client_spec.rb +671 -0
  48. data/spec/raptor-io/protocol/http/headers_spec.rb +189 -0
  49. data/spec/raptor-io/protocol/http/message_spec.rb +5 -0
  50. data/spec/raptor-io/protocol/http/request/manipulators/authenticator_spec.rb +193 -0
  51. data/spec/raptor-io/protocol/http/request/manipulators/authenticators/basic_spec.rb +32 -0
  52. data/spec/raptor-io/protocol/http/request/manipulators/authenticators/digest_spec.rb +76 -0
  53. data/spec/raptor-io/protocol/http/request/manipulators/authenticators/negotiate_spec.rb +52 -0
  54. data/spec/raptor-io/protocol/http/request/manipulators/authenticators/ntlm_spec.rb +37 -0
  55. data/spec/raptor-io/protocol/http/request/manipulators/redirect_follower_spec.rb +51 -0
  56. data/spec/raptor-io/protocol/http/request/manipulators_spec.rb +202 -0
  57. data/spec/raptor-io/protocol/http/request_spec.rb +965 -0
  58. data/spec/raptor-io/protocol/http/response_spec.rb +236 -0
  59. data/spec/raptor-io/protocol/http/server_spec.rb +345 -0
  60. data/spec/raptor-io/ruby/hash_spec.rb +20 -0
  61. data/spec/raptor-io/ruby/string_spec.rb +20 -0
  62. data/spec/raptor-io/socket/comm/local_spec.rb +50 -0
  63. data/spec/raptor-io/socket/switch_board/route_spec.rb +49 -0
  64. data/spec/raptor-io/socket/switch_board_spec.rb +87 -0
  65. data/spec/raptor-io/socket/tcp/ssl_spec.rb +18 -0
  66. data/spec/raptor-io/socket/tcp_server/ssl_spec.rb +59 -0
  67. data/spec/raptor-io/socket/tcp_server_spec.rb +19 -0
  68. data/spec/raptor-io/socket/tcp_spec.rb +14 -0
  69. data/spec/raptor-io/socket_spec.rb +16 -0
  70. data/spec/raptor-io/version_spec.rb +10 -0
  71. data/spec/spec_helper.rb +56 -0
  72. data/spec/support/fixtures/raptor/protocol/http/request/manipulators/manifoolators/fooer.rb +25 -0
  73. data/spec/support/fixtures/raptor/protocol/http/request/manipulators/niccolo_machiavelli.rb +20 -0
  74. data/spec/support/fixtures/raptor/protocol/http/request/manipulators/options_validator.rb +28 -0
  75. data/spec/support/fixtures/raptor/socket/ssl_server.crt +18 -0
  76. data/spec/support/fixtures/raptor/socket/ssl_server.key +15 -0
  77. data/spec/support/lib/path_helpers.rb +11 -0
  78. data/spec/support/lib/webserver_option_parser.rb +26 -0
  79. data/spec/support/lib/webservers.rb +120 -0
  80. data/spec/support/shared/contexts/with_ssl_server.rb +70 -0
  81. data/spec/support/shared/contexts/with_tcp_server.rb +58 -0
  82. data/spec/support/shared/examples/raptor/comm_examples.rb +26 -0
  83. data/spec/support/shared/examples/raptor/protocols/http/message.rb +106 -0
  84. data/spec/support/shared/examples/raptor/socket_examples.rb +135 -0
  85. data/spec/support/webservers/raptor/protocols/http/client.rb +100 -0
  86. data/spec/support/webservers/raptor/protocols/http/client_close_connection.rb +29 -0
  87. data/spec/support/webservers/raptor/protocols/http/client_https.rb +43 -0
  88. data/spec/support/webservers/raptor/protocols/http/request/manipulators/authenticators/basic.rb +9 -0
  89. data/spec/support/webservers/raptor/protocols/http/request/manipulators/authenticators/digest.rb +22 -0
  90. data/spec/support/webservers/raptor/protocols/http/request/manipulators/redirect_follower.rb +11 -0
  91. metadata +336 -0
@@ -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
+ ```
@@ -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
@@ -0,0 +1,11 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ #Smaller, more agile, and it can open doors.
5
+ module RaptorIO
6
+ end
7
+
8
+ require 'raptor-io/version'
9
+ require 'raptor-io/error'
10
+ require 'raptor-io/ruby'
11
+ require 'raptor-io/protocol'
@@ -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,6 @@
1
+ # A collection of useful protocols
2
+ module RaptorIO::Protocol
3
+ end
4
+
5
+ require 'raptor-io/protocol/error'
6
+ require 'raptor-io/protocol/http'
@@ -0,0 +1,10 @@
1
+ #
2
+ # {Protocol} error namespace.
3
+ #
4
+ # All {Protocol} errors inherit from and live under it.
5
+ #
6
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
7
+ #
8
+ class RaptorIO::Protocol::Error < RaptorIO::Error
9
+
10
+ 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