reverse-tunnel 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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