reverse-tunnel 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.
- data/.gitignore +20 -0
- data/Gemfile +6 -0
- data/Guardfile +9 -0
- data/LICENSE.txt +22 -0
- data/README.md +99 -0
- data/Rakefile +4 -0
- data/bin/reverse-tunnel +5 -0
- data/lib/reverse-tunnel.rb +35 -0
- data/lib/reverse-tunnel/cli.rb +170 -0
- data/lib/reverse-tunnel/client.rb +304 -0
- data/lib/reverse-tunnel/message.rb +140 -0
- data/lib/reverse-tunnel/server.rb +406 -0
- data/lib/reverse-tunnel/version.rb +3 -0
- data/reverse-tunnel.gemspec +33 -0
- data/spec/reverse-tunnel/cli_spec.rb +118 -0
- data/spec/reverse-tunnel/client/api_server_spec.rb +72 -0
- data/spec/reverse-tunnel/client_spec.rb +79 -0
- data/spec/reverse-tunnel/message_spec.rb +55 -0
- data/spec/reverse-tunnel/server/api_server_spec.rb +125 -0
- data/spec/reverse-tunnel/server/tunnels_spec.rb +66 -0
- data/spec/reverse-tunnel/server_spec.rb +57 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/support/log.rb +9 -0
- data/tasks/rspec.rake +2 -0
- metadata +280 -0
@@ -0,0 +1,140 @@
|
|
1
|
+
module ReverseTunnel
|
2
|
+
class Message
|
3
|
+
|
4
|
+
def self.type
|
5
|
+
name.split("::").last.gsub(/^([A-Z])/) {$1.downcase}.gsub(/([A-Z])/) { "_#{$1.downcase}" }.to_sym
|
6
|
+
end
|
7
|
+
def type
|
8
|
+
self.class.type
|
9
|
+
end
|
10
|
+
|
11
|
+
@@types = [:open_tunnel, :open_session, :data, :ping]
|
12
|
+
def self.types
|
13
|
+
@@types
|
14
|
+
end
|
15
|
+
|
16
|
+
types.each do |type|
|
17
|
+
define_method "#{type}?" do
|
18
|
+
self.type == type
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.type_id
|
23
|
+
types.index(type)
|
24
|
+
end
|
25
|
+
def type_id
|
26
|
+
self.class.type_id
|
27
|
+
end
|
28
|
+
|
29
|
+
def pack
|
30
|
+
[type_id, *payload].to_msgpack
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.create(type)
|
34
|
+
type = types.at(type) if Fixnum === type
|
35
|
+
const_get(type.to_s.gsub(/(^|_)(.)/) { $2.capitalize }).new
|
36
|
+
end
|
37
|
+
|
38
|
+
class Unpacker
|
39
|
+
|
40
|
+
attr_reader :unpacker
|
41
|
+
|
42
|
+
def initialize
|
43
|
+
@unpacker = MessagePack::Unpacker.new
|
44
|
+
end
|
45
|
+
|
46
|
+
def feed(data)
|
47
|
+
unpacker.feed data
|
48
|
+
end
|
49
|
+
|
50
|
+
include Enumerable
|
51
|
+
def each(&block)
|
52
|
+
unpacker.each do |data|
|
53
|
+
type_id = data.shift
|
54
|
+
payload = data
|
55
|
+
|
56
|
+
Message.create(type_id).tap do |message|
|
57
|
+
message.load(payload)
|
58
|
+
|
59
|
+
yield message
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.unpack(data)
|
67
|
+
Unpacker.new.tap do |packer|
|
68
|
+
packer.feed data
|
69
|
+
end.first
|
70
|
+
end
|
71
|
+
|
72
|
+
class Data < Message
|
73
|
+
attr_accessor :session_id, :data
|
74
|
+
|
75
|
+
def initialize(session_id = nil, data = nil)
|
76
|
+
self.session_id = session_id
|
77
|
+
self.data = data
|
78
|
+
end
|
79
|
+
|
80
|
+
def payload
|
81
|
+
[session_id, data]
|
82
|
+
end
|
83
|
+
|
84
|
+
def load(payload)
|
85
|
+
self.session_id, self.data = payload
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class OpenSession < Message
|
90
|
+
attr_accessor :session_id
|
91
|
+
|
92
|
+
def initialize(session_id = nil)
|
93
|
+
self.session_id = session_id
|
94
|
+
end
|
95
|
+
|
96
|
+
def payload
|
97
|
+
[session_id]
|
98
|
+
end
|
99
|
+
|
100
|
+
def load(payload)
|
101
|
+
self.session_id = payload.first
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
class OpenTunnel < Message
|
107
|
+
attr_accessor :token
|
108
|
+
|
109
|
+
def initialize(token = nil)
|
110
|
+
self.token = token
|
111
|
+
end
|
112
|
+
|
113
|
+
def payload
|
114
|
+
[token]
|
115
|
+
end
|
116
|
+
|
117
|
+
def load(payload)
|
118
|
+
self.token = payload.first
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
class Ping < Message
|
123
|
+
attr_accessor :sequence_number
|
124
|
+
|
125
|
+
def initialize(sequence_number = nil)
|
126
|
+
self.sequence_number = sequence_number
|
127
|
+
end
|
128
|
+
|
129
|
+
def payload
|
130
|
+
[sequence_number]
|
131
|
+
end
|
132
|
+
|
133
|
+
def load(payload)
|
134
|
+
self.sequence_number = payload.first
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
@@ -0,0 +1,406 @@
|
|
1
|
+
require 'evma_httpserver'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module ReverseTunnel
|
5
|
+
class Server
|
6
|
+
|
7
|
+
class ApiServer < EM::Connection
|
8
|
+
include EM::HttpServer
|
9
|
+
|
10
|
+
attr_accessor :server
|
11
|
+
|
12
|
+
def initialize(server)
|
13
|
+
@server = server
|
14
|
+
end
|
15
|
+
|
16
|
+
def post_init
|
17
|
+
super
|
18
|
+
no_environment_strings
|
19
|
+
end
|
20
|
+
|
21
|
+
def process_http_request
|
22
|
+
# the http request details are available via the following instance variables:
|
23
|
+
# @http_protocol
|
24
|
+
# @http_request_method
|
25
|
+
# @http_cookie
|
26
|
+
# @http_if_none_match
|
27
|
+
# @http_content_type
|
28
|
+
# @http_path_info
|
29
|
+
# @http_request_uri
|
30
|
+
# @http_query_string
|
31
|
+
# @http_post_content
|
32
|
+
# @http_headers
|
33
|
+
|
34
|
+
ReverseTunnel.logger.debug "Process http request #{@http_request_uri}"
|
35
|
+
|
36
|
+
response = EM::DelegatedHttpResponse.new(self)
|
37
|
+
response.status = 200
|
38
|
+
response.content_type 'application/json'
|
39
|
+
|
40
|
+
begin
|
41
|
+
|
42
|
+
case @http_request_uri
|
43
|
+
when %r{^/tunnels(.json)?$}
|
44
|
+
case @http_request_method
|
45
|
+
when "GET"
|
46
|
+
response.content = server.tunnels.to_json
|
47
|
+
when "POST"
|
48
|
+
params = @http_post_content ? JSON.parse(@http_post_content) : {}
|
49
|
+
tunnel = server.tunnels.create params
|
50
|
+
response.content = tunnel.to_json
|
51
|
+
end
|
52
|
+
when %r{^/tunnels/([0-9A-F]+)(.json)?$}
|
53
|
+
tunnel_id = $1
|
54
|
+
tunnel = server.tunnels.find(tunnel_id)
|
55
|
+
|
56
|
+
if tunnel
|
57
|
+
case @http_request_method
|
58
|
+
when "GET"
|
59
|
+
response.content = tunnel.to_json
|
60
|
+
when "DELETE"
|
61
|
+
tunnel = server.tunnels.destroy(tunnel_id)
|
62
|
+
response.content = tunnel.to_json
|
63
|
+
end
|
64
|
+
end
|
65
|
+
else
|
66
|
+
end
|
67
|
+
rescue => e
|
68
|
+
ReverseTunnel.logger.error "Error in http request processing: #{e}"
|
69
|
+
response.status = 500
|
70
|
+
end
|
71
|
+
|
72
|
+
if response.content.nil?
|
73
|
+
response.status = 404
|
74
|
+
end
|
75
|
+
|
76
|
+
response.send_response
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
class Tunnel
|
82
|
+
|
83
|
+
attr_accessor :token, :local_port, :local_host
|
84
|
+
|
85
|
+
def initialize(attributes)
|
86
|
+
attributes.each { |k,v| send "#{k}=", v }
|
87
|
+
end
|
88
|
+
|
89
|
+
def local_host
|
90
|
+
@local_host ||= "127.0.0.1"
|
91
|
+
end
|
92
|
+
|
93
|
+
attr_accessor :connection
|
94
|
+
|
95
|
+
def connection=(connection)
|
96
|
+
if @connection and @connection != connection
|
97
|
+
@connection.close_connection
|
98
|
+
local_connections.each(&:close_connection)
|
99
|
+
end
|
100
|
+
|
101
|
+
@connection = connection
|
102
|
+
|
103
|
+
if @connection
|
104
|
+
open
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def connection_closed(connection)
|
109
|
+
self.connection = nil if self.connection == connection
|
110
|
+
end
|
111
|
+
|
112
|
+
attr_accessor :local_server
|
113
|
+
|
114
|
+
def close
|
115
|
+
if local_server
|
116
|
+
ReverseTunnel.logger.info "Close local connections on #{local_port}"
|
117
|
+
EventMachine.stop_server local_server
|
118
|
+
self.local_server = nil
|
119
|
+
end
|
120
|
+
|
121
|
+
if connection
|
122
|
+
ReverseTunnel.logger.info "Close tunnel connection #{token}"
|
123
|
+
self.connection.tap do |connection|
|
124
|
+
@connection = nil
|
125
|
+
connection.close_connection
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def open
|
131
|
+
unless local_server
|
132
|
+
ReverseTunnel.logger.info "Listen on #{local_host}:#{local_port} for #{token}"
|
133
|
+
self.local_server = EventMachine.start_server local_host, local_port, LocalConnection, self
|
134
|
+
end
|
135
|
+
rescue => e
|
136
|
+
ReverseTunnel.logger.error "Can't listen on #{local_host}:#{local_port} for #{token} : #{e}"
|
137
|
+
end
|
138
|
+
|
139
|
+
def open_session(session_id)
|
140
|
+
if connection
|
141
|
+
ReverseTunnel.logger.debug "Send open session #{session_id}"
|
142
|
+
connection.send_data Message::OpenSession.new(session_id).pack
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def ping_received(ping)
|
147
|
+
ReverseTunnel.logger.debug "Receive ping #{token}/#{ping.sequence_number}"
|
148
|
+
connection.send_data Message::Ping.new(ping.sequence_number).pack
|
149
|
+
end
|
150
|
+
|
151
|
+
def send_data(session_id, data)
|
152
|
+
if connection
|
153
|
+
ReverseTunnel.logger.debug "Send data to local connection #{session_id}"
|
154
|
+
connection.send_data Message::Data.new(session_id,data).pack
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def local_connections
|
159
|
+
@local_connections ||= []
|
160
|
+
end
|
161
|
+
|
162
|
+
def receive_data(session_id, data)
|
163
|
+
local_connection = local_connections.find { |c| c.session_id == session_id }
|
164
|
+
if local_connection
|
165
|
+
ReverseTunnel.logger.debug "Send data for local connection #{session_id}"
|
166
|
+
local_connection.send_data data
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def next_session_id
|
171
|
+
@next_session_id ||= 0
|
172
|
+
@next_session_id += 1
|
173
|
+
end
|
174
|
+
|
175
|
+
def to_json(*args)
|
176
|
+
{ :token => token, :local_port => local_port }.tap do |attributes|
|
177
|
+
attributes[:connection] = connection.to_json if connection
|
178
|
+
end.to_json(*args)
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
|
183
|
+
class TunnelConnection < EventMachine::Connection
|
184
|
+
attr_accessor :server, :created_at
|
185
|
+
|
186
|
+
def initialize(server)
|
187
|
+
@server = server
|
188
|
+
end
|
189
|
+
|
190
|
+
def post_init
|
191
|
+
ReverseTunnel.logger.info "New tunnel connection from #{peer}"
|
192
|
+
self.created_at = Time.now
|
193
|
+
|
194
|
+
EventMachine.add_timer(10) do
|
195
|
+
unless open? or closed?
|
196
|
+
ReverseTunnel.logger.info "Force close of unopened tunnel connection from #{peer}"
|
197
|
+
close_connection
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def message_unpacker
|
203
|
+
@message_unpacker ||= Message::Unpacker.new
|
204
|
+
end
|
205
|
+
|
206
|
+
def receive_data(data)
|
207
|
+
message_unpacker.feed data
|
208
|
+
|
209
|
+
message_unpacker.each do |message|
|
210
|
+
if message.data?
|
211
|
+
tunnel.receive_data message.session_id, message.data
|
212
|
+
elsif message.open_tunnel?
|
213
|
+
open_tunnel message.token
|
214
|
+
elsif message.ping?
|
215
|
+
tunnel.ping_received message
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
attr_accessor :tunnel
|
221
|
+
|
222
|
+
def open?
|
223
|
+
!!tunnel
|
224
|
+
end
|
225
|
+
|
226
|
+
def closed?
|
227
|
+
@closed ||= false
|
228
|
+
end
|
229
|
+
|
230
|
+
def close_connection(after_writing = false)
|
231
|
+
super
|
232
|
+
@closed = true
|
233
|
+
end
|
234
|
+
|
235
|
+
def open_tunnel(token)
|
236
|
+
self.tunnel = server.tunnels.find token
|
237
|
+
if tunnel
|
238
|
+
ReverseTunnel.logger.info "Open tunnel #{token}"
|
239
|
+
tunnel.connection = self
|
240
|
+
else
|
241
|
+
ReverseTunnel.logger.warn "Refuse tunnel connection #{token}"
|
242
|
+
close_connection
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
def unbind
|
247
|
+
tunnel.connection_closed self if tunnel
|
248
|
+
end
|
249
|
+
|
250
|
+
def peer
|
251
|
+
@peer ||=
|
252
|
+
begin
|
253
|
+
port, ip = Socket.unpack_sockaddr_in(get_peername)
|
254
|
+
"#{ip}:#{port}"
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def to_json(*args)
|
259
|
+
{ :peer => peer, :created_at => created_at }.json
|
260
|
+
end
|
261
|
+
|
262
|
+
end
|
263
|
+
|
264
|
+
class LocalConnection < EventMachine::Connection
|
265
|
+
attr_accessor :tunnel
|
266
|
+
|
267
|
+
def initialize(tunnel)
|
268
|
+
@tunnel = tunnel
|
269
|
+
end
|
270
|
+
|
271
|
+
def post_init
|
272
|
+
ReverseTunnel.logger.debug "New local connection"
|
273
|
+
tunnel.local_connections << self
|
274
|
+
tunnel.open_session(session_id)
|
275
|
+
end
|
276
|
+
|
277
|
+
def receive_data(data)
|
278
|
+
ReverseTunnel.logger.debug "Received data in local #{session_id}"
|
279
|
+
tunnel.send_data session_id, data
|
280
|
+
end
|
281
|
+
|
282
|
+
def session_id
|
283
|
+
@session_id ||= tunnel.next_session_id
|
284
|
+
end
|
285
|
+
|
286
|
+
def unbind
|
287
|
+
ReverseTunnel.logger.debug "Close local connection"
|
288
|
+
tunnel.local_connections.delete self
|
289
|
+
end
|
290
|
+
|
291
|
+
end
|
292
|
+
|
293
|
+
def tunnels
|
294
|
+
@tunnels ||= Tunnels.new
|
295
|
+
end
|
296
|
+
|
297
|
+
def local_host=(local_host)
|
298
|
+
tunnels.local_host = local_host
|
299
|
+
end
|
300
|
+
|
301
|
+
def local_port_range=(local_port_range)
|
302
|
+
tunnels.local_port_range = local_port_range
|
303
|
+
end
|
304
|
+
|
305
|
+
class Tunnels
|
306
|
+
|
307
|
+
def tunnels
|
308
|
+
@tunnels ||= []
|
309
|
+
end
|
310
|
+
|
311
|
+
attr_accessor :local_host, :local_port_range
|
312
|
+
|
313
|
+
def local_port_range
|
314
|
+
@local_port_range ||= 10000..10200
|
315
|
+
end
|
316
|
+
|
317
|
+
def find(token)
|
318
|
+
tunnels.find { |t| t.token == token }
|
319
|
+
end
|
320
|
+
|
321
|
+
def create(attributes = {})
|
322
|
+
attributes = default_attributes.merge(attributes).merge(:local_host => local_host)
|
323
|
+
Tunnel.new(attributes).tap do |tunnel|
|
324
|
+
ReverseTunnel.logger.info "Create tunnel #{tunnel.inspect}"
|
325
|
+
tunnels << tunnel
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
def destroy(token)
|
330
|
+
tunnel = find(token)
|
331
|
+
if tunnel
|
332
|
+
tunnel.close
|
333
|
+
tunnels.delete tunnel
|
334
|
+
tunnel
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
def default_attributes
|
339
|
+
{ "token" => create_token, "local_port" => available_local_port }
|
340
|
+
end
|
341
|
+
|
342
|
+
def create_token
|
343
|
+
rand(10e32).to_s(16).ljust(28, '0').upcase
|
344
|
+
end
|
345
|
+
|
346
|
+
def used_local_ports
|
347
|
+
tunnels.map(&:local_port)
|
348
|
+
end
|
349
|
+
|
350
|
+
def available_local_ports
|
351
|
+
local_port_range.to_a - used_local_ports
|
352
|
+
end
|
353
|
+
|
354
|
+
def available_local_port
|
355
|
+
available_local_ports.tap do |ports|
|
356
|
+
ports.shuffle if respond_to?(:shuffle)
|
357
|
+
end.first
|
358
|
+
end
|
359
|
+
|
360
|
+
def to_json(*args)
|
361
|
+
tunnels.to_json(*args)
|
362
|
+
end
|
363
|
+
|
364
|
+
end
|
365
|
+
|
366
|
+
def start
|
367
|
+
tunnels.create "token" => "6B833D3F561369156820B4240C7C2657", "local_port" => 10000
|
368
|
+
|
369
|
+
EventMachine.run do
|
370
|
+
start_server
|
371
|
+
start_api
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
attr_accessor :server_host, :server_port
|
376
|
+
|
377
|
+
def server_host
|
378
|
+
@server_host ||= "0.0.0.0"
|
379
|
+
end
|
380
|
+
|
381
|
+
def server_port
|
382
|
+
@server_port ||= 4893
|
383
|
+
end
|
384
|
+
|
385
|
+
def start_server
|
386
|
+
ReverseTunnel.logger.info "Wait tunnel connections on #{server_host}:#{server_port}"
|
387
|
+
EventMachine.start_server server_host, server_port, TunnelConnection, self
|
388
|
+
end
|
389
|
+
|
390
|
+
attr_accessor :api_host, :api_port
|
391
|
+
|
392
|
+
def api_host
|
393
|
+
@api_host ||= "127.0.0.1"
|
394
|
+
end
|
395
|
+
|
396
|
+
def api_port
|
397
|
+
@api_port ||= 4894
|
398
|
+
end
|
399
|
+
|
400
|
+
def start_api
|
401
|
+
ReverseTunnel.logger.info "Wait api requests #{api_host}:#{api_port}"
|
402
|
+
EventMachine.start_server api_host, api_port, ApiServer, self
|
403
|
+
end
|
404
|
+
|
405
|
+
end
|
406
|
+
end
|