forked from luizluca/bridge
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbridge
executable file
·351 lines (319 loc) · 11.1 KB
/
bridge
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
#!/usr/bin/ruby
# vim: noai:ts=4:sw=4
#
# Copyright (C) 2010-2017 by Luiz Angelo Daros de Luca
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# Created on 2010-08-10
#
# Changelog:
#
# * 2017-03-17
# - Add support for http proxy authentication using auto-detect support
#
# * 2010-09-01
# - Add TCP keepalive flags for local socket (thanks Jan Adámek)
# - Increased local buffer to 640k
# - Fixed sync problem with stdin/out when using as ProxyCommand inside ssh config
# - Reconnection on client-webrick disconnection
# - Fixed proxy absence
# - Specified webrick timeout (10m)
# - Treated more signals
# - Divide connection (open, read, write)
#
#
#################
#
# Port Forward client/server over HTTP
#
#################
class String
def strip_heredoc
min = scan(/^[ \t]*(?=\S)/).min
if min == nil
indent=0
else
indent=min.size
end
gsub(/^[ \t]{#{indent}}/, '')
end
end
case ARGV.size
when 2
server=true
location=ARGV[1]
when 4
server=false
url=ARGV[1]
remote_host=ARGV[2]
remote_port=ARGV[3].to_i
else
$stderr.puts <<-EOF.strip_heredoc
Use as server:
#$0 localPort bridgeLocation
Use as client:
#$0 localPort|STDIN bridgeURL remoteAddress remotePort
Ex:
bridge$ #$0 8080 /bridge
client$ #$0 8022 http://mybridge:8080/bridge remoteServer 22
client$ ssh localhost -p 8022
EOF
exit
end
localPort=ARGV[0]
require 'socket'
require 'thread'
Thread.abort_on_exception = true
MAXBUF=1024*640 # 640k
READ_TIMEOUT=60
if server
require 'webrick'
class BridgeServlet < WEBrick::HTTPServlet::AbstractServlet
@@connections = {}
def get_socket(req,res)
conn_id = req.path.split("/").last
conn = @@connections[conn_id]
if not conn
$stderr.puts "[#{conn_id}] Connection is not open for #{conn_id}=#{conn}"
res.status=404
return nil
end
conn
end
def close_socket(req,res)
conn_id = req.path.split("/").last
return if not conn=@@connections[conn_id]
conn.close if not conn.closed?
@@connections.delete(conn_id)
end
def do_POST(req, res)
conn_id = req.path.split("/").last
(remote_host,remote_port)=req.body.split(":")
remote_port=remote_port.to_i
$stderr.puts "[#{conn_id}] Opening connection to #{remote_host}:#{remote_port} for #{req.peeraddr}..."
begin
conn=TCPSocket.open(remote_host,remote_port)
conn.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
@@connections[conn_id]=conn
$stderr.puts "[#{conn_id}] Connected to #{remote_host}:#{remote_port} #{conn}"
res.status=201
rescue
res['Content-Type'] = "text/plain"
res.body=$!.message
$stderr.puts "[#{conn_id}] Connection failed: #$!"
res.status=406 # Not Acceptable
end
end
def do_PUT(req, res)
conn_id = req.path.split("/").last
return if not s=get_socket(req,res)
begin
s.print req.body
rescue Errno::EPIPE, IOError
$stderr.puts "[#{conn_id}] Connection closed in remote destination (PUT)"
close_socket(req, res)
res.status=410 # GONE
end
end
def do_GET(req, res)
conn_id = req.path.split("/").last
return if not conn=get_socket(req,res)
begin
res.body = conn.read_nonblock(MAXBUF)
rescue IO::WaitReadable
if IO.select([conn],[],[],READ_TIMEOUT-3)
retry
else # Timeout
res.body = ''
res.status=204 # No Content
end
rescue EOFError, Errno::EPIPE, IOError
if conn.closed?
$stderr.puts "[#{conn_id}] Connection closed by remote destination (GET)"
else
$stderr.puts "[#{conn_id}] Connection closed (GET) #{$!}"
end
close_socket(req, res)
res.status=410 # Gone
rescue
$stderr.puts $!.class
end
end
def do_DELETE(req, res)
conn_id = req.path.split("/").last
$stderr.puts "[#{conn_id}] Connection closed in client. Trying to close it in remote destination"
close_socket(req, res)
res.status=200
end
end
s = WEBrick::HTTPServer.new(
:Port => localPort.to_i,
:RequestTimeout => 600
)
s.mount(location, BridgeServlet)
trap("INT"){ s.shutdown }
s.start
else # Client
require "net/http"
require 'uri'
# HACK! https://bugs.ruby-lang.org/issues/12921
class Net::HTTP
def proxy_user
if @proxy_from_env then
proxy_uri && proxy_uri.user
else
@proxy_user
end
end
def proxy_pass
if @proxy_from_env then
proxy_uri && proxy_uri.password
else
@proxy_pass
end
end
end
url = URI(url)
if url.find_proxy
$stderr.puts "Using proxy: #{url.find_proxy}"
end
case localPort
when "STDIN","-"
_in=$stdin
_out=$stdout
# Keeps this unbuffered
_in.sync=true
_out.sync=true
else
$stderr.puts "Opening local port #{localPort}"
local_server = TCPServer.open(localPort.to_i)
$stderr.puts "Waiting for local connection to port #{localPort}"
local_server_conn = local_server.accept
_in=_out=local_server_conn
# Keep connection alive
local_server_conn.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
end
#unique Connection IDentifier
conn_id=`uuidgen`.chomp
connected=false
# Open the connection
begin
begin
$stderr.puts "Opening connection over HTTP bridge for #{remote_host}:#{remote_port} (#{conn_id})"
# HACK! new().start() because of https://bugs.ruby-lang.org/issues/13351
Net::HTTP.new(url.host, url.port).start do |http|
error=nil
res = http.post("#{url.path}/#{conn_id}","#{remote_host}:#{remote_port}")
if res.kind_of? Net::HTTPCreated
connected=true
else
$stderr.puts "The bridge failed to connect to the remote location (#{res}): #{res.body}"
connected=false
break
end
end
rescue Errno::EPIPE
$stderr.puts "Connection to bridge #{url} closed"
retry
end
# Not connected, nothing more to do
exit 1 if not connected
# Launch Local write/Remote read loop
Thread.new do
$stderr.puts "Local write/Remote read loop started"
begin
Net::HTTP.new(url.host, url.port).start do |http|
http.read_timeout=READ_TIMEOUT+3
while connected
res = http.get("#{url.path}/#{conn_id}") {|res| _out.print res }
if res.kind_of? Net::HTTPGone
$stderr.puts "Connection closed in remote location (lw/rr)"
connected=false
_in.close if not _in.closed?
break
elsif res.kind_of? Net::HTTPNotFound
$stderr.puts "Connection not opened on bridge"
connected=false
break
end
end
end
rescue Errno::EPIPE
_in.close if not _in.closed?
rescue EOFError
# retry if local connection is still open
retry if not _in.closed?
$stderr.puts "Connection to bridge closed (lw/rr)"
rescue Errno::ECONNREFUSED
$stderr.puts "Connection to bridge failed (lw/rr)"
_in.close if not _in.closed?
rescue Timeout::Error
$stderr.puts "Timeout (lw/rr)"
retry if not _in.closed?
rescue Net::ReadTimeout
$stderr.puts "Net::ReadTimeout (lw/rr)"
retry if not _in.closed?
rescue Net::HTTPServerError
raise res.message
end
end
# If CTRL+C, SIGHUP or SIGTERM, close _in (and itself)
trap("INT"){ _in.close }
trap("SIGHUP"){ _in.close }
trap("SIGTERM"){ _in.close }
# Launch "Local read/Remote write loop started"
$stderr.puts "Local read/Remote write loop started"
# Keep buffer between retries
buf=nil
begin
Net::HTTP.new(url.host, url.port).start do |http|
while connected
begin
# first read from _in ONLY if buf is empty
buf=_in.readpartial(MAXBUF) if !buf
rescue IO::WaitReadable
IO.select([_in])
retry
rescue EOFError
$stderr.puts "Local connection closed (lr/rw)"
$stderr.puts "Closing bridge connection to remote location"
http.delete("#{url.path}/#{conn_id}")
connected=false
break
end
res = http.put("#{url.path}/#{conn_id}",buf)
if res.kind_of? Net::HTTPGone
$stderr.puts "Connection closed in remote location (lr/rw)"
_in.close if not _in.closed?
connected=false
break
end
# Buffer sent, clear it
buf=nil
end
end
rescue Net::ReadTimeout
$stderr.puts "Net::ReadTimeout (lr/rw)"
retry if not _in.closed? and connected
rescue Net::HTTPServerError
raise "Server said: '#{res.message}'"
rescue Errno::EPIPE, IOError
# retry if local connection is still open
retry if not _in.closed?
$stderr.puts "Connection to bridge closed (lr/rw)"
end
rescue Errno::ECONNREFUSED
$stderr.puts "Connection to bridge failed"
end
end
$stderr.puts "Program Finished!"