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 ADDED
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *~
19
+ README.html
20
+ log/*
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in reverse-tunnel.gemspec
4
+ gemspec
5
+
6
+ gem 'rb-inotify', '~> 0.8.8' if RUBY_PLATFORM =~ /linux/
data/Guardfile ADDED
@@ -0,0 +1,9 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'rspec' do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ end
9
+
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Alban Peignier
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # Reverse::Tunnel
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'reverse-tunnel'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install reverse-tunnel
18
+
19
+ ## Usage
20
+
21
+ reverse-tunnel server --public=88.190.240.120:4893 --api=172.20.11.9:3000 --range=172.20.11.9:10000-10100
22
+
23
+ reverse-tunnel client --server=88.190.240.120:4893 --local-port=22 6B833D3F561369156820B4240C7C2657
24
+
25
+ reverse-tunnel rt://console.tryphon.eu:4893/6B833D3F561369156820B4240C7C2657/22
26
+ reverse-tunnel cnQ6Ly9jb25zb2xlLnRyeXBob24uZXU6NDg5My82QjgzM0QzRjU2MTM2OTE1NjgyMEI0MjQwQzdDMjY1Ny8yMg==
27
+
28
+ ## API
29
+
30
+ Create a new tunnel :
31
+
32
+ $ curl -X POST -d '{"token":"156820B4240C7C26576B833D3F561369","local_port":10001}' http://localhost:5000/tunnels.json
33
+ {
34
+ "token": "156820B4240C7C26576B833D3F561369",
35
+ "local_port": 10001
36
+ }
37
+
38
+ Retrieve current tunnels :
39
+
40
+ $ curl http://localhost:5000/tunnels.json
41
+ [
42
+ {
43
+ "token": "6B833D3F561369156820B4240C7C2657",
44
+ "local_port": 10000,
45
+ },
46
+ {
47
+ "token": "156820B4240C7C26576B833D3F561369",
48
+ "local_port": 10001,
49
+ "connection": {
50
+ "peer": "127.0.0.1:42782",
51
+ "created_at": "2012-12-22 17:29:55 +0100"
52
+ }
53
+ }
54
+ ]
55
+
56
+
57
+ ## Protocol
58
+
59
+ ### Create tunnel
60
+
61
+ * POST /tunnels on server API (with optional token and local port)
62
+ * API returns token and local port
63
+
64
+ ### Open tunnel
65
+
66
+ * client send tunnel token to server (on public ip:port)
67
+ * server opens a tcp server on local port (associated to token)
68
+ * client receives a confirmation message
69
+
70
+ ### Ping tunnel
71
+
72
+ * every 30s (?) client sends a ping message
73
+ * server responds a pong message
74
+
75
+ ### Use tunnel
76
+
77
+ * server receives a connection on tcp server on local port
78
+ * server creates a session (with id)
79
+ * server sends received data to client
80
+ * client creates local connection to local port (if not exist)
81
+ * client send received data to local connection
82
+ * client send back to server data received on local connection
83
+
84
+ ### Messages
85
+
86
+ * OPEN_TUNNEL:tunnel_token
87
+ * PING
88
+ * PONG
89
+ * OPEN_SESSION:session_id
90
+ * DATA:session_id:data
91
+ * CLOSE_SESSION:session_id
92
+
93
+ ## Contributing
94
+
95
+ 1. Fork it
96
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
97
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
98
+ 4. Push to the branch (`git push origin my-new-feature`)
99
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ FileList['tasks/**/*.rake'].each { |task| import task }
4
+
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "reverse-tunnel"
4
+
5
+ ReverseTunnel::CLI.new(ARGV).run
@@ -0,0 +1,35 @@
1
+ require "reverse-tunnel/version"
2
+
3
+ require "logger"
4
+
5
+ module ReverseTunnel
6
+
7
+ def self.default_logger
8
+ Logger.new($stderr).tap do |logger|
9
+ logger.level = Logger::INFO
10
+ end
11
+ end
12
+
13
+ @@logger = default_logger
14
+ def self.logger
15
+ @@logger
16
+ end
17
+ def self.logger=(logger)
18
+ @@logger = logger
19
+ end
20
+
21
+ def self.reset_logger!
22
+ @@logger = default_logger
23
+ end
24
+
25
+ end
26
+
27
+ require "eventmachine"
28
+ require "msgpack"
29
+ require "trollop"
30
+ require "syslog/logger"
31
+
32
+ require "reverse-tunnel/message"
33
+ require "reverse-tunnel/server"
34
+ require "reverse-tunnel/client"
35
+ require "reverse-tunnel/cli"
@@ -0,0 +1,170 @@
1
+ module ReverseTunnel
2
+ class CLI
3
+
4
+ attr_accessor :arguments
5
+
6
+ def initialize(arguments = [])
7
+ @arguments = arguments
8
+ end
9
+
10
+ def mode
11
+ @mode ||= arguments.shift
12
+ end
13
+
14
+ class Base
15
+
16
+ attr_reader :arguments
17
+
18
+ def initialize(arguments = [])
19
+ @arguments = arguments
20
+ end
21
+
22
+ def options
23
+ @options ||= Trollop::with_standard_exception_handling(parser) do
24
+ parser.parse arguments
25
+ end
26
+ end
27
+
28
+ def configure(object = self)
29
+ options.each do |k,v|
30
+ unless [:help, :version].include? k or k.to_s =~ /_given$/
31
+ object.send "#{k.to_s.gsub('-','_')}=", v
32
+ end
33
+ end
34
+
35
+ object
36
+ end
37
+
38
+ end
39
+
40
+ class Global < Base
41
+
42
+ def parser
43
+ @parser ||= Trollop::Parser.new do
44
+ banner <<-EOS
45
+ Usage:
46
+ reverse-client [global options] server|client [options]
47
+
48
+ where [global options] are:
49
+ EOS
50
+
51
+ opt :debug, "Enable debug messages"
52
+ opt :syslog, "Send log messages to syslog"
53
+
54
+ version ReverseTunnel::VERSION
55
+
56
+ stop_on "server", "client"
57
+ end
58
+ end
59
+
60
+ end
61
+
62
+ class Configurator < Base
63
+
64
+ def instance
65
+ @instance ||= ReverseTunnel.const_get(self.class.name.split("::").last).new
66
+ end
67
+
68
+ def method_missing(name, *args)
69
+ instance.send name, *args
70
+ end
71
+
72
+ def parse_host_port(string)
73
+ host, port = string.split(':')
74
+ [ host, port.to_i ]
75
+ end
76
+
77
+ def parse_host_port_range(string)
78
+ if string =~ /(.*):(\d+)-(\d+)$/
79
+ [ $1, ($2.to_i)..($3.to_i) ]
80
+ end
81
+ end
82
+
83
+ def server=(server)
84
+ self.server_host, self.server_port = parse_host_port(server)
85
+ end
86
+
87
+ def self.configure(arguments)
88
+ new(arguments).configure
89
+ end
90
+
91
+ end
92
+
93
+ class Client < Configurator
94
+
95
+ def parser
96
+ @parser ||= Trollop::Parser.new do
97
+ opt :server, "Host and port of ReverseTunnel server", :type => :string
98
+ opt :api, "Host and port of status HTTP api", :type => :string
99
+ opt :"local-port", "Port to forward incoming connection", :default => 22
100
+ end
101
+ end
102
+
103
+ def token
104
+ arguments.first
105
+ end
106
+
107
+ def api=(api)
108
+ self.api_host, self.api_port = parse_host_port(api) if api
109
+ end
110
+
111
+ def configure(object = self)
112
+ super
113
+ self.token = token
114
+ instance
115
+ end
116
+
117
+ end
118
+
119
+ class Server < Configurator
120
+
121
+ def parser
122
+ @parser ||= Trollop::Parser.new do
123
+ opt :server, "Host and port of ReverseTunnel server", :default => "0.0.0.0:4893"
124
+ opt :api, "Host and port of ReverseTunnel HTTP api", :default => "127.0.0.1:4894"
125
+ opt :local, "Host and port range to listen forwarded connections", :default => "127.0.0.1:10000-10010"
126
+ end
127
+ end
128
+
129
+ def api=(api)
130
+ self.api_host, self.api_port = parse_host_port(api)
131
+ end
132
+
133
+ def local=(local)
134
+ self.local_host, self.local_port_range = parse_host_port_range(local)
135
+ end
136
+
137
+ end
138
+
139
+ def debug=(debug)
140
+ level = debug ? Logger::DEBUG : Logger::INFO
141
+ ReverseTunnel.logger.level = level
142
+ end
143
+
144
+ def syslog=(syslog)
145
+ if syslog
146
+ syslog_logger = Syslog::Logger.new("rtunnel").tap do |logger|
147
+ logger.level = ReverseTunnel.logger.level
148
+ end
149
+ ReverseTunnel.logger = syslog_logger
150
+ end
151
+ end
152
+
153
+ def configurator_class
154
+ mode == "server" ? ReverseTunnel::CLI::Server : ReverseTunnel::CLI::Client
155
+ end
156
+
157
+ def instance
158
+ configurator_class.configure(arguments)
159
+ end
160
+
161
+ def configure
162
+ Global.new(arguments).configure(self)
163
+ end
164
+
165
+ def run
166
+ configure
167
+ instance.start
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,304 @@
1
+ module ReverseTunnel
2
+ class Client
3
+
4
+ class ApiServer < EM::Connection
5
+ include EM::HttpServer
6
+
7
+ attr_accessor :tunnel
8
+
9
+ def initialize(tunnel)
10
+ @tunnel = tunnel
11
+ end
12
+
13
+ def post_init
14
+ super
15
+ no_environment_strings
16
+ end
17
+
18
+ def process_http_request
19
+ ReverseTunnel.logger.debug "Process http request #{@http_request_uri}"
20
+
21
+ response = EM::DelegatedHttpResponse.new(self)
22
+ response.status = 200
23
+ response.content_type 'application/json'
24
+
25
+ begin
26
+ if @http_request_uri =~ %r{^/status(.json)?$} and @http_request_method == "GET"
27
+ response.content = tunnel.to_json
28
+ end
29
+ rescue => e
30
+ ReverseTunnel.logger.error "Error in http request processing: #{e}"
31
+ response.status = 500
32
+ end
33
+
34
+ if response.content.nil?
35
+ response.status = 404
36
+ end
37
+
38
+ response.send_response
39
+ end
40
+
41
+ end
42
+
43
+ class Tunnel
44
+
45
+ attr_accessor :host, :port
46
+ attr_accessor :token, :local_port
47
+
48
+ def initialize(attributes = {})
49
+ attributes.each { |k,v| send "#{k}=", v }
50
+ end
51
+
52
+ attr_accessor :connection
53
+
54
+ def connection=(connection)
55
+ if connection.nil?
56
+ EventMachine.add_timer(30) do
57
+ start
58
+ end
59
+
60
+ local_connections.close_all
61
+ @hearbeat.cancel
62
+ else
63
+ @hearbeat = EventMachine.add_periodic_timer(5) do
64
+ ping
65
+ end
66
+ end
67
+
68
+ @connection = connection
69
+ end
70
+
71
+ def start
72
+ ReverseTunnel.logger.debug "Connect to #{host}:#{port}"
73
+ EventMachine.connect host, port, TunnelConnection, self
74
+ end
75
+
76
+ def open
77
+ connection.send_data Message::OpenTunnel.new(token).pack
78
+ end
79
+
80
+ attr_accessor :sequence_number
81
+ def sequence_number
82
+ @sequence_number ||= 0
83
+ end
84
+
85
+ def ping
86
+ next_number = self.sequence_number += 1
87
+ ReverseTunnel.logger.debug "Send ping #{next_number}"
88
+ connection.send_data Message::Ping.new(next_number).pack if connection
89
+ end
90
+
91
+ def ping_received(ping)
92
+ ReverseTunnel.logger.info "Receive ping #{ping.sequence_number}"
93
+ end
94
+
95
+ def open_session(session_id)
96
+ local_host = "localhost"
97
+ EventMachine.connect local_host, local_port, LocalConnection, self, session_id
98
+ end
99
+
100
+ def send_data(session_id, data)
101
+ if connection
102
+ ReverseTunnel.logger.debug "Send data to local connection #{session_id}"
103
+ connection.send_data Message::Data.new(session_id,data).pack
104
+ end
105
+ end
106
+
107
+ def local_connections
108
+ @local_connections ||= LocalConnections.new
109
+ end
110
+
111
+ def receive_data(session_id, data)
112
+ local_connection = local_connections.find(session_id)
113
+ if local_connection
114
+ ReverseTunnel.logger.debug "Send data to local connection #{session_id}"
115
+ local_connection.send_data data
116
+ else
117
+ local_connections.bufferize session_id, data
118
+ end
119
+ end
120
+
121
+ def to_json
122
+ { :token => token,
123
+ :local_port => local_port,
124
+ :server_host => host,
125
+ :server_port => port,
126
+ :local_connections => local_connections.as_json
127
+ }.tap do |attributes|
128
+ attributes[:connection] = connection.as_json if connection
129
+ end.to_json
130
+ end
131
+
132
+ end
133
+
134
+ class LocalConnections
135
+
136
+ attr_reader :connections
137
+
138
+ def initialize
139
+ @connections = []
140
+ end
141
+
142
+ def find(session_id)
143
+ connections.find { |c| c.session_id == session_id }
144
+ end
145
+
146
+ def push(connection)
147
+ connections << connection
148
+
149
+ session_id = connection.session_id
150
+ ReverseTunnel.logger.debug "Clear buffer for #{session_id}"
151
+
152
+ (buffers.delete(session_id) or []).each do |data|
153
+ connection.send_data data
154
+ end
155
+ end
156
+ alias_method :<<, :push
157
+
158
+ def buffers
159
+ @buffers ||= Hash.new { |h,k| h[k] = [] }
160
+ end
161
+
162
+ def bufferize(session_id, data)
163
+ ReverseTunnel.logger.debug "Push buffer for #{session_id}"
164
+ buffers[session_id] << data
165
+ end
166
+
167
+ def delete(connection)
168
+ connections.delete connection
169
+ end
170
+
171
+ def close_all
172
+ connections.each(&:close_connection)
173
+ end
174
+
175
+ def as_json
176
+ connections.map(&:as_json)
177
+ end
178
+
179
+ end
180
+
181
+ class TunnelConnection < EventMachine::Connection
182
+ attr_accessor :tunnel, :created_at
183
+
184
+ attr_reader :hearbeat
185
+
186
+ def initialize(tunnel)
187
+ @tunnel = tunnel
188
+ end
189
+
190
+ def post_init
191
+ ReverseTunnel.logger.debug "New tunnel connection"
192
+ self.created_at = Time.now
193
+
194
+ tunnel.connection = self
195
+ tunnel.open
196
+ end
197
+
198
+ def message_unpacker
199
+ @message_unpacker ||= Message::Unpacker.new
200
+ end
201
+
202
+ def as_json
203
+ { :created_at => created_at }
204
+ end
205
+
206
+ def receive_data(data)
207
+ ReverseTunnel.logger.debug "Received data '#{data.unpack('H*').join}'"
208
+ message_unpacker.feed data
209
+
210
+ message_unpacker.each do |message|
211
+ ReverseTunnel.logger.debug "Received message in tunnel #{message.inspect}"
212
+
213
+ if message.data?
214
+ tunnel.receive_data message.session_id, message.data
215
+ elsif message.open_session?
216
+ tunnel.open_session message.session_id
217
+ elsif message.ping?
218
+ tunnel.ping_received message
219
+ end
220
+ end
221
+ end
222
+
223
+ def unbind
224
+ ReverseTunnel.logger.debug "Close tunnel connection"
225
+ tunnel.connection = nil
226
+ end
227
+
228
+ end
229
+
230
+ class LocalConnection < EventMachine::Connection
231
+ attr_accessor :tunnel, :session_id
232
+
233
+ attr_reader :created_at, :received_size, :send_size
234
+
235
+ def initialize(tunnel, session_id)
236
+ @tunnel, @session_id = tunnel, session_id
237
+ @received_size = @send_size = 0
238
+ end
239
+
240
+ def post_init
241
+ ReverseTunnel.logger.debug "New local connection"
242
+ @created_at = Time.now
243
+ tunnel.local_connections << self
244
+ end
245
+
246
+ def receive_data(data)
247
+ ReverseTunnel.logger.debug "Received data in local connection #{session_id}"
248
+ @received_size += data.size
249
+ tunnel.send_data session_id, data
250
+ end
251
+
252
+ def unbind
253
+ ReverseTunnel.logger.debug "Close local connection #{session_id}"
254
+ tunnel.local_connections.delete self
255
+ end
256
+
257
+ def send_data(data)
258
+ ReverseTunnel.logger.debug "Send data '#{data.unpack('H*').join}'"
259
+ @send_size += data.size
260
+ super
261
+ end
262
+
263
+ def as_json
264
+ { :session_id => session_id, :created_at => created_at, :received_size => received_size, :send_size => send_size }
265
+ end
266
+
267
+ end
268
+
269
+ def start
270
+ EventMachine.run do
271
+ tunnel.start
272
+ start_api
273
+ end
274
+ end
275
+
276
+ def start_api
277
+ if api_host
278
+ ReverseTunnel.logger.info "Wait api requests #{api_host}:#{api_port}"
279
+ EventMachine.start_server api_host, api_port, ApiServer, tunnel
280
+ end
281
+ end
282
+
283
+ def tunnel
284
+ @tunnel ||= Tunnel.new(:token => token, :local_port => local_port, :host => server_host, :port => server_port)
285
+ end
286
+
287
+ attr_accessor :token, :local_port
288
+ attr_accessor :server_host, :server_port
289
+ attr_accessor :api_host, :api_port
290
+
291
+ def server_port
292
+ @server_port ||= 4893
293
+ end
294
+
295
+ def api_port
296
+ @api_port ||= 4895
297
+ end
298
+
299
+ def local_port
300
+ @local_port ||= 22
301
+ end
302
+
303
+ end
304
+ end