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.
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,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
+