raptor-io 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +15 -0
- data/LICENSE +30 -0
- data/README.md +51 -0
- data/lib/rack/handler/raptor-io.rb +130 -0
- data/lib/raptor-io.rb +11 -0
- data/lib/raptor-io/error.rb +19 -0
- data/lib/raptor-io/protocol.rb +6 -0
- data/lib/raptor-io/protocol/error.rb +10 -0
- data/lib/raptor-io/protocol/http.rb +34 -0
- data/lib/raptor-io/protocol/http/client.rb +685 -0
- data/lib/raptor-io/protocol/http/error.rb +16 -0
- data/lib/raptor-io/protocol/http/headers.rb +132 -0
- data/lib/raptor-io/protocol/http/message.rb +67 -0
- data/lib/raptor-io/protocol/http/request.rb +307 -0
- data/lib/raptor-io/protocol/http/request/manipulator.rb +117 -0
- data/lib/raptor-io/protocol/http/request/manipulators.rb +217 -0
- data/lib/raptor-io/protocol/http/request/manipulators/authenticator.rb +110 -0
- data/lib/raptor-io/protocol/http/request/manipulators/authenticators/basic.rb +36 -0
- data/lib/raptor-io/protocol/http/request/manipulators/authenticators/digest.rb +135 -0
- data/lib/raptor-io/protocol/http/request/manipulators/authenticators/negotiate.rb +69 -0
- data/lib/raptor-io/protocol/http/request/manipulators/authenticators/ntlm.rb +29 -0
- data/lib/raptor-io/protocol/http/request/manipulators/redirect_follower.rb +65 -0
- data/lib/raptor-io/protocol/http/response.rb +166 -0
- data/lib/raptor-io/protocol/http/server.rb +446 -0
- data/lib/raptor-io/ruby.rb +4 -0
- data/lib/raptor-io/ruby/hash.rb +24 -0
- data/lib/raptor-io/ruby/ipaddr.rb +15 -0
- data/lib/raptor-io/ruby/openssl.rb +23 -0
- data/lib/raptor-io/ruby/string.rb +27 -0
- data/lib/raptor-io/socket.rb +175 -0
- data/lib/raptor-io/socket/comm.rb +143 -0
- data/lib/raptor-io/socket/comm/local.rb +94 -0
- data/lib/raptor-io/socket/comm/sapni.rb +75 -0
- data/lib/raptor-io/socket/comm/socks.rb +237 -0
- data/lib/raptor-io/socket/comm_chain.rb +30 -0
- data/lib/raptor-io/socket/error.rb +45 -0
- data/lib/raptor-io/socket/switch_board.rb +183 -0
- data/lib/raptor-io/socket/switch_board/route.rb +42 -0
- data/lib/raptor-io/socket/tcp.rb +231 -0
- data/lib/raptor-io/socket/tcp/ssl.rb +77 -0
- data/lib/raptor-io/socket/tcp_server.rb +16 -0
- data/lib/raptor-io/socket/tcp_server/ssl.rb +52 -0
- data/lib/raptor-io/socket/udp.rb +0 -0
- data/lib/raptor-io/version.rb +6 -0
- data/lib/tasks/yard.rake +26 -0
- data/spec/rack/handler/raptor_spec.rb +140 -0
- data/spec/raptor-io/protocol/http/client_spec.rb +671 -0
- data/spec/raptor-io/protocol/http/headers_spec.rb +189 -0
- data/spec/raptor-io/protocol/http/message_spec.rb +5 -0
- data/spec/raptor-io/protocol/http/request/manipulators/authenticator_spec.rb +193 -0
- data/spec/raptor-io/protocol/http/request/manipulators/authenticators/basic_spec.rb +32 -0
- data/spec/raptor-io/protocol/http/request/manipulators/authenticators/digest_spec.rb +76 -0
- data/spec/raptor-io/protocol/http/request/manipulators/authenticators/negotiate_spec.rb +52 -0
- data/spec/raptor-io/protocol/http/request/manipulators/authenticators/ntlm_spec.rb +37 -0
- data/spec/raptor-io/protocol/http/request/manipulators/redirect_follower_spec.rb +51 -0
- data/spec/raptor-io/protocol/http/request/manipulators_spec.rb +202 -0
- data/spec/raptor-io/protocol/http/request_spec.rb +965 -0
- data/spec/raptor-io/protocol/http/response_spec.rb +236 -0
- data/spec/raptor-io/protocol/http/server_spec.rb +345 -0
- data/spec/raptor-io/ruby/hash_spec.rb +20 -0
- data/spec/raptor-io/ruby/string_spec.rb +20 -0
- data/spec/raptor-io/socket/comm/local_spec.rb +50 -0
- data/spec/raptor-io/socket/switch_board/route_spec.rb +49 -0
- data/spec/raptor-io/socket/switch_board_spec.rb +87 -0
- data/spec/raptor-io/socket/tcp/ssl_spec.rb +18 -0
- data/spec/raptor-io/socket/tcp_server/ssl_spec.rb +59 -0
- data/spec/raptor-io/socket/tcp_server_spec.rb +19 -0
- data/spec/raptor-io/socket/tcp_spec.rb +14 -0
- data/spec/raptor-io/socket_spec.rb +16 -0
- data/spec/raptor-io/version_spec.rb +10 -0
- data/spec/spec_helper.rb +56 -0
- data/spec/support/fixtures/raptor/protocol/http/request/manipulators/manifoolators/fooer.rb +25 -0
- data/spec/support/fixtures/raptor/protocol/http/request/manipulators/niccolo_machiavelli.rb +20 -0
- data/spec/support/fixtures/raptor/protocol/http/request/manipulators/options_validator.rb +28 -0
- data/spec/support/fixtures/raptor/socket/ssl_server.crt +18 -0
- data/spec/support/fixtures/raptor/socket/ssl_server.key +15 -0
- data/spec/support/lib/path_helpers.rb +11 -0
- data/spec/support/lib/webserver_option_parser.rb +26 -0
- data/spec/support/lib/webservers.rb +120 -0
- data/spec/support/shared/contexts/with_ssl_server.rb +70 -0
- data/spec/support/shared/contexts/with_tcp_server.rb +58 -0
- data/spec/support/shared/examples/raptor/comm_examples.rb +26 -0
- data/spec/support/shared/examples/raptor/protocols/http/message.rb +106 -0
- data/spec/support/shared/examples/raptor/socket_examples.rb +135 -0
- data/spec/support/webservers/raptor/protocols/http/client.rb +100 -0
- data/spec/support/webservers/raptor/protocols/http/client_close_connection.rb +29 -0
- data/spec/support/webservers/raptor/protocols/http/client_https.rb +43 -0
- data/spec/support/webservers/raptor/protocols/http/request/manipulators/authenticators/basic.rb +9 -0
- data/spec/support/webservers/raptor/protocols/http/request/manipulators/authenticators/digest.rb +22 -0
- data/spec/support/webservers/raptor/protocols/http/request/manipulators/redirect_follower.rb +11 -0
- metadata +336 -0
|
@@ -0,0 +1,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
|
+
|