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,75 @@
|
|
1
|
+
|
2
|
+
# Communication through a SAPRouter
|
3
|
+
#
|
4
|
+
# By default SAPRouter listens on port 3299.
|
5
|
+
#
|
6
|
+
# @see https://labs.mwrinfosecurity.com/blog/2012/09/13/sap-smashing-internet-windows/
|
7
|
+
# @see http://conference.hitb.org/hitbsecconf2010ams/materials/D2T2%20-%20Mariano%20Nunez%20Di%20Croce%20-%20SAProuter%20.pdf
|
8
|
+
class RaptorIO::Socket::Comm::SAPNI < RaptorIO::Socket::Comm
|
9
|
+
|
10
|
+
# The bits of the packet that don't change
|
11
|
+
NI_ROUTE_HEADER = [
|
12
|
+
"NI_ROUTE",
|
13
|
+
2, # route info version
|
14
|
+
39, # NI version
|
15
|
+
2, # number of entries
|
16
|
+
1, # talk mode (NI_MSG_IO: 0; NI_RAW_IO; 1; NI_ROUT_IO: 2)
|
17
|
+
0, # unused
|
18
|
+
0, # unused
|
19
|
+
1, # number of rest nodes
|
20
|
+
].pack("Z*C7")
|
21
|
+
|
22
|
+
# @param options [Hash]
|
23
|
+
# @option options :sap_host [String,IPAddr]
|
24
|
+
# @option options :sap_port [Fixnum] (3299)
|
25
|
+
# @option options :sap_comm [Comm]
|
26
|
+
def initialize(options = {})
|
27
|
+
@sap_host = options[:sap_host]
|
28
|
+
@sap_port = (options[:sap_port] || 3299).to_i
|
29
|
+
@sap_comm = options[:sap_comm]
|
30
|
+
end
|
31
|
+
|
32
|
+
# Connect to a SAPRouter and use its routing capabilities to create a
|
33
|
+
# TCP connection to `:peer_host`.
|
34
|
+
#
|
35
|
+
# @param (see Comm#create_tcp)
|
36
|
+
def create_tcp(options)
|
37
|
+
@sap_socket = @sap_comm.create_tcp(
|
38
|
+
peer_host: @sap_host,
|
39
|
+
peer_port: @sap_port
|
40
|
+
)
|
41
|
+
|
42
|
+
first_route_item = [
|
43
|
+
@sap_host, @sap_port.to_s, 0
|
44
|
+
].pack("Z*Z*C")
|
45
|
+
|
46
|
+
second_route_item = [
|
47
|
+
options[:peer_host], options[:peer_port].to_s, 0
|
48
|
+
].pack("Z*Z*C")
|
49
|
+
|
50
|
+
route_data =
|
51
|
+
# This is *not* a length, it is the
|
52
|
+
# "current position as an offset into the route string"
|
53
|
+
# according to
|
54
|
+
# http://help.sap.com/saphelp_nwpi711/helpdata/en/48/6a29785bed4e6be10000000a421937/content.htm
|
55
|
+
[ first_route_item.length ].pack("N") +
|
56
|
+
first_route_item +
|
57
|
+
second_route_item
|
58
|
+
route_data = [ route_data.length - 4 ].pack("N") + route_data
|
59
|
+
|
60
|
+
ni_packet = NI_ROUTE_HEADER.dup + route_data
|
61
|
+
ni_packet = [ni_packet.length].pack('N') + ni_packet
|
62
|
+
|
63
|
+
@sap_socket.write(ni_packet)
|
64
|
+
res_length = @sap_socket.read(4)
|
65
|
+
res = @sap_socket.read(res_length.unpack("N").first)
|
66
|
+
|
67
|
+
unless res == "NI_PONG\x00"
|
68
|
+
raise RaptorIO::Socket::Error::ConnectionError
|
69
|
+
end
|
70
|
+
|
71
|
+
RaptorIO::Socket::TCP.new(@sap_socket, options)
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
@@ -0,0 +1,237 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
require 'socket'
|
3
|
+
|
4
|
+
# Communication through a SOCKS proxy
|
5
|
+
#
|
6
|
+
# @see http://openssh.org/txt/socks4.protocol
|
7
|
+
# @see https://tools.ietf.org/html/rfc1928
|
8
|
+
class RaptorIO::Socket::Comm::SOCKS < RaptorIO::Socket::Comm
|
9
|
+
|
10
|
+
# @!attribute socks_host
|
11
|
+
# The SOCKS server's address
|
12
|
+
# @return [String]
|
13
|
+
attr_accessor :socks_host
|
14
|
+
# @!attribute socks_port
|
15
|
+
# The SOCKS server's port
|
16
|
+
# @return [Fixnum]
|
17
|
+
attr_accessor :socks_port
|
18
|
+
# @!attribute socks_comm
|
19
|
+
# The {Comm} used to connect to the SOCKS server
|
20
|
+
# @return [Comm]
|
21
|
+
attr_accessor :socks_comm
|
22
|
+
|
23
|
+
# Constants for address types ("ATYP" in the RFC)
|
24
|
+
module AddressTypes
|
25
|
+
# 4-byte IPv4 address
|
26
|
+
ATYP_IPv4 = 1
|
27
|
+
# DNS name as a Pascal string
|
28
|
+
ATYP_DOMAINNAME = 3
|
29
|
+
# 16-byte IPv6 address
|
30
|
+
ATYP_IPv6 = 4
|
31
|
+
end
|
32
|
+
|
33
|
+
# Constants for reply codes
|
34
|
+
module ReplyCodes
|
35
|
+
# `X'00' succeeded`
|
36
|
+
SUCCEEDED = 0
|
37
|
+
# `X'01' general SOCKS server failure`
|
38
|
+
GENERAL_FAILURE = 1
|
39
|
+
# `X'02' connection not allowed by ruleset`
|
40
|
+
NOT_ALLOWED = 2
|
41
|
+
# `X'03' Network unreachable`
|
42
|
+
NETUNREACH = 3
|
43
|
+
# `X'04' Host unreachable`
|
44
|
+
HOSTUNREACH = 4
|
45
|
+
# `X'05' Connection refused`
|
46
|
+
CONNREFUSED = 5
|
47
|
+
# `X'06' TTL expired`
|
48
|
+
TTL_EXPIRED = 6
|
49
|
+
# `X'07' Command not supported`
|
50
|
+
CMD_NOT_SUPPORTED = 7
|
51
|
+
# `X'08' Address type not supported`
|
52
|
+
ATYP_NOT_SUPPORTED = 8
|
53
|
+
end
|
54
|
+
|
55
|
+
# @param options [Hash]
|
56
|
+
# @option options :socks_host [String,IPAddr]
|
57
|
+
# @option options :socks_port [Fixnum]
|
58
|
+
# @option options :socks_comm [Comm]
|
59
|
+
def initialize(options = {})
|
60
|
+
@socks_host = options[:socks_host]
|
61
|
+
@socks_port = options[:socks_port].to_i
|
62
|
+
@socks_comm = options[:socks_comm]
|
63
|
+
end
|
64
|
+
|
65
|
+
# (see Comm#support_ipv6?)
|
66
|
+
def support_ipv6?
|
67
|
+
begin
|
68
|
+
tcp = create_tcp("::1", {})
|
69
|
+
tcp.close
|
70
|
+
true
|
71
|
+
rescue RaptorIO::Error
|
72
|
+
nil
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Connect to `:peer_host`
|
77
|
+
#
|
78
|
+
# @option (see Comm#create_tcp)
|
79
|
+
#
|
80
|
+
# @return [Socket::TCP]
|
81
|
+
#
|
82
|
+
# @raise [RaptorIO::Socket::Error::ConnectTimeout]
|
83
|
+
def create_tcp(options)
|
84
|
+
@socks_socket = socks_comm.create_tcp(
|
85
|
+
peer_host: socks_host,
|
86
|
+
peer_port: socks_port
|
87
|
+
)
|
88
|
+
|
89
|
+
negotiate_connection(options[:peer_host], options[:peer_port])
|
90
|
+
|
91
|
+
if options[:ssl_context]
|
92
|
+
RaptorIO::Socket::TCP::SSL.new(@socks_socket, options)
|
93
|
+
else
|
94
|
+
RaptorIO::Socket::TCP.new(@socks_socket, options)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
# Attempt to create a connection to `peer_host`:`peer_port` via the
|
102
|
+
# SOCKS server at {#socks_host}:{#socks_port}.
|
103
|
+
#
|
104
|
+
# @param peer_host [String] An address or hostname
|
105
|
+
# @param peer_port [Fixnum] TCP port to connect to
|
106
|
+
#
|
107
|
+
# @raise [Error::ConnectionError] When the connection fails
|
108
|
+
def negotiate_connection(peer_host, peer_port)
|
109
|
+
# From RFC1928:
|
110
|
+
# ```
|
111
|
+
# o X'00' NO AUTHENTICATION REQUIRED
|
112
|
+
# o X'01' GSSAPI
|
113
|
+
# o X'02' USERNAME/PASSWORD
|
114
|
+
# o X'03' to X'7F' IANA ASSIGNED
|
115
|
+
# o X'80' to X'FE' RESERVED FOR PRIVATE METHODS
|
116
|
+
# o X'FF' NO ACCEPTABLE METHODS
|
117
|
+
# ```
|
118
|
+
auth_methods = [ 0 ]
|
119
|
+
# [ version ][ N methods ][ methods ... ]
|
120
|
+
v5_pkt = [ 5, auth_methods.count, *auth_methods ].pack("CCC*")
|
121
|
+
|
122
|
+
@socks_socket.write(v5_pkt)
|
123
|
+
response = @socks_socket.read(2)
|
124
|
+
|
125
|
+
case response
|
126
|
+
when "\x05\x00".force_encoding('binary')
|
127
|
+
# Then they accepted NO AUTHENTICATION and we can send a connect
|
128
|
+
# request *without* a password
|
129
|
+
request = pack_v5_connect_packet(peer_host, peer_port.to_i)
|
130
|
+
else
|
131
|
+
# Then they didn't like what we had to offer.
|
132
|
+
@socks_socket.close
|
133
|
+
raise RaptorIO::Socket::Error::ConnectionError, "Proxy connection failed"
|
134
|
+
end
|
135
|
+
|
136
|
+
@socks_socket.write(request)
|
137
|
+
|
138
|
+
reply_pkt = @socks_socket.read(4)
|
139
|
+
if reply_pkt.nil?
|
140
|
+
# ssh(1) likes to just sever the connection if it can't connect
|
141
|
+
raise RaptorIO::Socket::Error::ConnectionError
|
142
|
+
end
|
143
|
+
|
144
|
+
handle_reply(reply_pkt)
|
145
|
+
end
|
146
|
+
|
147
|
+
def pack_v5_connect_packet(peer_host, peer_port)
|
148
|
+
begin
|
149
|
+
ip = IPAddr.parse(peer_host)
|
150
|
+
rescue ArgumentError
|
151
|
+
type = AddressTypes::ATYP_DOMAINNAME
|
152
|
+
# Packed as a Pascal string
|
153
|
+
packed_addr = [peer_host.length, peer_host].pack("Ca*")
|
154
|
+
else
|
155
|
+
if ip.to_range.count != 1
|
156
|
+
raise ArgumentError, "Invalid host"
|
157
|
+
end
|
158
|
+
type = if ip.ipv4?
|
159
|
+
AddressTypes::ATYP_IPv4
|
160
|
+
elsif ip.ipv6?
|
161
|
+
AddressTypes::ATYP_IPv6
|
162
|
+
end
|
163
|
+
packed_addr = ip.hton
|
164
|
+
end
|
165
|
+
connect_packet = [
|
166
|
+
5, # Version
|
167
|
+
1, # CMD, CONNECT X'01'
|
168
|
+
0, # reserved
|
169
|
+
type,
|
170
|
+
packed_addr,
|
171
|
+
peer_port
|
172
|
+
].pack("CCCCa*n")
|
173
|
+
|
174
|
+
connect_packet
|
175
|
+
end
|
176
|
+
|
177
|
+
def handle_reply(reply_pkt)
|
178
|
+
|
179
|
+
# [ version ][ reply code ][ reserved ][ atyp ]
|
180
|
+
_, reply, _, type = reply_pkt.unpack("C4")
|
181
|
+
|
182
|
+
# X'00' succeeded
|
183
|
+
# X'01' general SOCKS server failure
|
184
|
+
# X'02' connection not allowed by ruleset
|
185
|
+
# X'03' Network unreachable
|
186
|
+
# X'04' Host unreachable
|
187
|
+
# X'05' Connection refused
|
188
|
+
# X'06' TTL expired
|
189
|
+
# X'07' Command not supported
|
190
|
+
# X'08' Address type not supported
|
191
|
+
# X'09' to X'FF' unassigned
|
192
|
+
case reply
|
193
|
+
when ReplyCodes::SUCCEEDED
|
194
|
+
# Read in the bind addr. The protocol spec says this is supposed
|
195
|
+
# to be the getsockname(2) address of the sockfd on the server,
|
196
|
+
# which isn't all that useful to begin with. SSH(1) always
|
197
|
+
# populates it with NULL bytes, making it completely pointless.
|
198
|
+
# Read it off the socket and ignore it so it doesn't get in the
|
199
|
+
# way of the proxied traffic.
|
200
|
+
case type
|
201
|
+
when AddressTypes::ATYP_IPv4
|
202
|
+
@socks_socket.read(4)
|
203
|
+
when AddressTypes::ATYP_IPv6
|
204
|
+
@socks_socket.read(16)
|
205
|
+
when AddressTypes::ATYP_DOMAINNAME
|
206
|
+
# Pascal string, so read in the length and then read that many
|
207
|
+
len = @socks_socket.read(1).to_i
|
208
|
+
@socks_socket.read(len)
|
209
|
+
end
|
210
|
+
# bind port
|
211
|
+
@socks_socket.read(2)
|
212
|
+
|
213
|
+
when ReplyCodes::NETUNREACH, ReplyCodes::HOSTUNREACH
|
214
|
+
@socks_socket.close
|
215
|
+
raise RaptorIO::Socket::Error::HostUnreachable
|
216
|
+
when ReplyCodes::CONNREFUSED
|
217
|
+
@socks_socket.close
|
218
|
+
raise RaptorIO::Socket::Error::ConnectionRefused
|
219
|
+
when ReplyCodes::GENERAL_FAILURE,
|
220
|
+
ReplyCodes::NOT_ALLOWED,
|
221
|
+
ReplyCodes::TTL_EXPIRED,
|
222
|
+
ReplyCodes::CMD_NOT_SUPPORTED,
|
223
|
+
ReplyCodes::ATYP_NOT_SUPPORTED
|
224
|
+
# Then this is a kind of failure that doesn't map well to standard
|
225
|
+
# socket errors. Just call it a ConnectionError.
|
226
|
+
@socks_socket.close
|
227
|
+
raise RaptorIO::Socket::Error::ConnectionError
|
228
|
+
else
|
229
|
+
# Then this is an unassigned error code. No idea what it is, so
|
230
|
+
# just call it a ConnectionError
|
231
|
+
@socks_socket.close
|
232
|
+
raise RaptorIO::Socket::Error::ConnectionError
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
end
|
237
|
+
|
@@ -0,0 +1,30 @@
|
|
1
|
+
|
2
|
+
class RaptorIO::Socket::CommChain
|
3
|
+
attr_accessor :comms
|
4
|
+
|
5
|
+
# @param uris [Array] A list of URIs (as `URI` objects or as `String`s)
|
6
|
+
def initialize(*uris)
|
7
|
+
@comms = [ RaptorIO::Socket::SwitchBoard::DEFAULT_ROUTE.comm ]
|
8
|
+
uris.each do |arg|
|
9
|
+
begin
|
10
|
+
arg_uri = (arg.kind_of? URI) ? arg : URI.parse(arg)
|
11
|
+
rescue URI::InvalidURIError
|
12
|
+
raise ArgumentError.new("Invalid URI (#{arg.inspect})")
|
13
|
+
end
|
14
|
+
|
15
|
+
next_comm = RaptorIO::Socket::Comm.from_uri(arg_uri, prev_comm: @comms.last)
|
16
|
+
|
17
|
+
if next_comm.kind_of? RaptorIO::Socket::Comm
|
18
|
+
@comms << next_comm
|
19
|
+
else
|
20
|
+
raise ArgumentError.new("Invalid Comm: unknown scheme (#{arg_uri.scheme.inspect})")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
def create_tcp(opts = {})
|
27
|
+
@comms.last.create_tcp(opts)
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# Base class for all socket-related errors
|
2
|
+
class RaptorIO::Socket::Error < RaptorIO::Error
|
3
|
+
|
4
|
+
# Hostname resolution error.
|
5
|
+
#
|
6
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
7
|
+
class CouldNotResolve < RaptorIO::Socket::Error
|
8
|
+
end
|
9
|
+
|
10
|
+
# Base class for errors that cause a connection to fail.
|
11
|
+
class ConnectionError < RaptorIO::Socket::Error
|
12
|
+
end
|
13
|
+
|
14
|
+
# Raised when a socket receives no SYN/ACK before timeout.
|
15
|
+
class ConnectionTimeout < RaptorIO::Socket::Error::ConnectionError
|
16
|
+
end
|
17
|
+
|
18
|
+
# Raised when a socket receives a RST during connect.
|
19
|
+
class ConnectionRefused < RaptorIO::Socket::Error::ConnectionError
|
20
|
+
end
|
21
|
+
|
22
|
+
# Host reachability error.
|
23
|
+
#
|
24
|
+
# Occurs when the remote host cannot be reached.
|
25
|
+
#
|
26
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
27
|
+
class HostUnreachable < RaptorIO::Socket::Error::ConnectionError
|
28
|
+
end
|
29
|
+
|
30
|
+
# Broken-pipe error.
|
31
|
+
#
|
32
|
+
# Occurs when a connection dies unexpectedly.
|
33
|
+
#
|
34
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
35
|
+
class BrokenPipe < RaptorIO::Socket::Error
|
36
|
+
end
|
37
|
+
|
38
|
+
# Not connected error.
|
39
|
+
#
|
40
|
+
# Occurs when attempting to transmit data over a not connected transport endpoint.
|
41
|
+
#
|
42
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
43
|
+
class NotConnected < RaptorIO::Socket::Error
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
# -*- coding: binary -*-
|
2
|
+
|
3
|
+
require 'thread'
|
4
|
+
|
5
|
+
###
|
6
|
+
#
|
7
|
+
# A routing table that associates subnets with {Comm} objects. Comm
|
8
|
+
# classes are used to instantiate objects that are tied to remote
|
9
|
+
# network entities. For example, {Comm::Local} is used to build network
|
10
|
+
# connections directly from the local machine whereas, for instance, a
|
11
|
+
# SOCKS Comm would build a local socket pair that is associated with a
|
12
|
+
# connection established by a remote SOCKS server. This can be seen as
|
13
|
+
# a uniform way of communicating with hosts through arbitrary channels.
|
14
|
+
#
|
15
|
+
###
|
16
|
+
class RaptorIO::Socket::SwitchBoard
|
17
|
+
|
18
|
+
require 'raptor-io/socket/comm'
|
19
|
+
require 'raptor-io/socket/comm_chain'
|
20
|
+
require 'raptor-io/socket/switch_board/route'
|
21
|
+
|
22
|
+
include Enumerable
|
23
|
+
|
24
|
+
# If no route matches the host/netmask when searching for
|
25
|
+
# {#best_comm}, this will be the fallback - always route through the
|
26
|
+
# local machine creating Ruby ::Sockets.
|
27
|
+
DEFAULT_ROUTE = Route.new("0.0.0.0", "0.0.0.0", RaptorIO::Socket::Comm::Local.new)
|
28
|
+
|
29
|
+
# The list of routes this swithboard knows about
|
30
|
+
#
|
31
|
+
# @return [Array<Route>]
|
32
|
+
attr_reader :routes
|
33
|
+
|
34
|
+
def initialize
|
35
|
+
@routes = Array.new
|
36
|
+
@mutex = Mutex.new
|
37
|
+
end
|
38
|
+
|
39
|
+
# Adds a route for a given subnet and netmask destined through a given comm
|
40
|
+
# instance.
|
41
|
+
#
|
42
|
+
# @param (see RaptorIO::Socket::SwitchBoard::Route#new)
|
43
|
+
# @return [Boolean] Whether the route was added. This may fail if a
|
44
|
+
# route already {#route_exists? existed} or if the given `comm` does
|
45
|
+
# not support the address family of the `subnet` (e.g., an IPv6
|
46
|
+
# address for a comm that does not support IPv6)
|
47
|
+
def add_route(subnet, netmask, comm)
|
48
|
+
rv = true
|
49
|
+
subnet = IPAddr.parse(subnet)
|
50
|
+
if subnet.ipv6? and comm.respond_to?(:support_ipv6?)
|
51
|
+
return false unless comm.support_ipv6?
|
52
|
+
end
|
53
|
+
|
54
|
+
synchronize do
|
55
|
+
# If the route already exists, return false to the caller.
|
56
|
+
if route_exists?(subnet, netmask)
|
57
|
+
rv = false
|
58
|
+
else
|
59
|
+
@routes << Route.new(subnet, netmask, comm)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
rv
|
64
|
+
end
|
65
|
+
|
66
|
+
# Finds the best possible comm for the supplied target address.
|
67
|
+
#
|
68
|
+
# @param addr [String,IPAddr] The address to which we want to talk
|
69
|
+
# @return [Comm]
|
70
|
+
def best_comm(addr)
|
71
|
+
addr = IPAddr.parse(addr)
|
72
|
+
|
73
|
+
# Find the most specific route that this address fits in. If none,
|
74
|
+
# use the default, i.e., local.
|
75
|
+
best_route = reduce(DEFAULT_ROUTE) do |best, route|
|
76
|
+
if route.subnet.include?(addr) && route.netmask >= best.netmask
|
77
|
+
route
|
78
|
+
else
|
79
|
+
best
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
best_route.comm
|
84
|
+
end
|
85
|
+
|
86
|
+
# Enumerates each entry in the routing table.
|
87
|
+
#
|
88
|
+
def each(&block)
|
89
|
+
@routes.each(&block)
|
90
|
+
end
|
91
|
+
|
92
|
+
alias each_route each
|
93
|
+
|
94
|
+
# Clears all established routes.
|
95
|
+
#
|
96
|
+
# @return [void]
|
97
|
+
def flush_routes
|
98
|
+
# Remove each of the individual routes so the comms don't think they're
|
99
|
+
# still routing after a flush.
|
100
|
+
@routes.each do |r|
|
101
|
+
if r.comm.respond_to? :routes
|
102
|
+
r.comm.routes.delete("#{r.subnet}/#{r.netmask}")
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Re-initialize to an empty array
|
107
|
+
@routes.clear
|
108
|
+
end
|
109
|
+
|
110
|
+
#
|
111
|
+
# Remove all routes that go through the supplied `comm`.
|
112
|
+
#
|
113
|
+
# @param comm [Comm]
|
114
|
+
# @return [void]
|
115
|
+
def remove_by_comm(comm)
|
116
|
+
synchronize do
|
117
|
+
@routes.delete_if { |route| route.comm == comm }
|
118
|
+
end
|
119
|
+
|
120
|
+
nil
|
121
|
+
end
|
122
|
+
|
123
|
+
#
|
124
|
+
# Removes a route for a given subnet and netmask destined through a given
|
125
|
+
# comm instance.
|
126
|
+
#
|
127
|
+
# @param (see Route.new)
|
128
|
+
# @return [Boolean] Whether we found one to delete
|
129
|
+
def remove_route(subnet, netmask, comm)
|
130
|
+
rv = false
|
131
|
+
|
132
|
+
other = Route.new(subnet, netmask, comm)
|
133
|
+
synchronize do
|
134
|
+
@routes.delete_if do |route|
|
135
|
+
if route == other
|
136
|
+
rv = true
|
137
|
+
else
|
138
|
+
false
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
rv
|
144
|
+
end
|
145
|
+
|
146
|
+
#
|
147
|
+
# Checks to see if a route already exists for the supplied subnet and
|
148
|
+
# netmask.
|
149
|
+
#
|
150
|
+
def route_exists?(subnet, netmask)
|
151
|
+
each do |route|
|
152
|
+
return true if route.subnet == subnet and route.netmask == netmask
|
153
|
+
end
|
154
|
+
|
155
|
+
false
|
156
|
+
end
|
157
|
+
|
158
|
+
# Create a TCP client socket on the {#best_comm best comm} available
|
159
|
+
# for `:peer_host`.
|
160
|
+
#
|
161
|
+
# @param (see Comm#create_tcp)
|
162
|
+
# @option opts (see Comm#create_tcp)
|
163
|
+
def create_tcp(opts)
|
164
|
+
best_comm(opts[:peer_host]).create_tcp(opts)
|
165
|
+
end
|
166
|
+
|
167
|
+
# Create a TCP server socket on the {#best_comm best comm} available
|
168
|
+
# for `:local_host`.
|
169
|
+
#
|
170
|
+
# @param (see Comm#create_tcp_server)
|
171
|
+
# @option opts (see Comm#create_tcp_server)
|
172
|
+
def create_tcp_server(opts)
|
173
|
+
best_comm(opts[:local_host]).create_tcp_server(opts)
|
174
|
+
end
|
175
|
+
|
176
|
+
private
|
177
|
+
|
178
|
+
def synchronize( &block )
|
179
|
+
@mutex.synchronize( &block )
|
180
|
+
end
|
181
|
+
|
182
|
+
end
|
183
|
+
|