caldecott 0.0.1 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +1 -2
- data/lib/caldecott.rb +11 -0
- data/lib/caldecott/client.rb +7 -0
- data/lib/caldecott/client/client.rb +79 -0
- data/lib/caldecott/client/http_tunnel.rb +236 -0
- data/lib/caldecott/client/tunnel.rb +23 -0
- data/lib/caldecott/client/websocket_tunnel.rb +36 -0
- data/lib/caldecott/server.rb +4 -0
- data/lib/caldecott/server/http_tunnel.rb +212 -0
- data/lib/caldecott/server/websocket_tunnel.rb +57 -0
- data/lib/caldecott/session_logger.rb +39 -0
- data/lib/caldecott/tcp_connection.rb +38 -0
- data/lib/caldecott/version.rb +1 -1
- metadata +137 -6
data/README.md
CHANGED
data/lib/caldecott.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
# Copyright (c) 2009-2011 VMware, Inc.
|
2
|
+
|
3
|
+
require 'eventmachine'
|
4
|
+
|
5
|
+
module Caldecott
|
6
|
+
module Client
|
7
|
+
def self.sanitize_url(tun_url)
|
8
|
+
tun_url = tun_url =~ /(http|https|ws).*/i ? tun_url : "https://#{tun_url}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.start(opts)
|
12
|
+
local_port = opts[:local_port]
|
13
|
+
tun_url = opts[:tun_url]
|
14
|
+
dst_host = opts[:dst_host]
|
15
|
+
dst_port = opts[:dst_port]
|
16
|
+
log_file = opts[:log_file]
|
17
|
+
log_level = opts[:log_level]
|
18
|
+
auth_token = opts[:auth_token]
|
19
|
+
|
20
|
+
@quiet = opts[:quiet]
|
21
|
+
|
22
|
+
trap("TERM") { stop }
|
23
|
+
trap("INT") { stop }
|
24
|
+
|
25
|
+
tun_url = sanitize_url(tun_url)
|
26
|
+
|
27
|
+
EM.run do
|
28
|
+
unless @quiet
|
29
|
+
puts "Starting local server on port #{local_port} to #{tun_url}"
|
30
|
+
end
|
31
|
+
|
32
|
+
EM.start_server("localhost", local_port, TcpConnection) do |conn|
|
33
|
+
# avoid races between tunnel setup and incoming local data
|
34
|
+
conn.pause
|
35
|
+
|
36
|
+
log = SessionLogger.new("client", log_file)
|
37
|
+
log.level = SessionLogger.severity_from_string(log_level)
|
38
|
+
|
39
|
+
tun = nil
|
40
|
+
|
41
|
+
conn.onopen do
|
42
|
+
log.debug "local connected"
|
43
|
+
tun = Tunnel.start(log, tun_url, dst_host, dst_port, auth_token)
|
44
|
+
end
|
45
|
+
|
46
|
+
tun.onopen do
|
47
|
+
log.debug "tunnel connected"
|
48
|
+
conn.resume
|
49
|
+
end
|
50
|
+
|
51
|
+
conn.onreceive do |data|
|
52
|
+
log.debug "l -> t #{data.length}"
|
53
|
+
tun.send_data(data)
|
54
|
+
end
|
55
|
+
|
56
|
+
tun.onreceive do |data|
|
57
|
+
log.debug("l <- t #{data.length}")
|
58
|
+
conn.send_data(data)
|
59
|
+
end
|
60
|
+
|
61
|
+
conn.onclose do
|
62
|
+
log.debug "local closed"
|
63
|
+
tun.close
|
64
|
+
end
|
65
|
+
|
66
|
+
tun.onclose do
|
67
|
+
log.debug "tunnel closed"
|
68
|
+
conn.close_connection_after_writing
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.stop
|
75
|
+
puts "Caldecott shutting down" unless @quiet
|
76
|
+
EM.stop
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,236 @@
|
|
1
|
+
# Copyright (c) 2009-2011 VMware, Inc.
|
2
|
+
|
3
|
+
require 'em-http'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module Caldecott
|
7
|
+
module Client
|
8
|
+
class HttpTunnel
|
9
|
+
MAX_RETRIES = 10
|
10
|
+
|
11
|
+
def initialize(logger, url, dst_host, dst_port, auth_token)
|
12
|
+
@log, @auth_token = logger, auth_token
|
13
|
+
@closing = false
|
14
|
+
@retries = 0
|
15
|
+
init_msg = ""
|
16
|
+
|
17
|
+
# FIXME: why is this optional?
|
18
|
+
if dst_host
|
19
|
+
init_msg = { :host => dst_host, :port => dst_port }.to_json
|
20
|
+
end
|
21
|
+
|
22
|
+
start(url, init_msg)
|
23
|
+
end
|
24
|
+
|
25
|
+
def onopen(&blk)
|
26
|
+
@onopen = blk
|
27
|
+
@onopen.call if @opened
|
28
|
+
end
|
29
|
+
|
30
|
+
def onclose(&blk)
|
31
|
+
@onclose = blk
|
32
|
+
@onclose.call if @closed
|
33
|
+
end
|
34
|
+
|
35
|
+
def onreceive(&blk)
|
36
|
+
@onreceive = blk
|
37
|
+
end
|
38
|
+
|
39
|
+
def send_data(data)
|
40
|
+
@writer.send_data(data)
|
41
|
+
end
|
42
|
+
|
43
|
+
def close
|
44
|
+
return if @closing or @closed
|
45
|
+
@closing = true
|
46
|
+
@writer.close if @writer
|
47
|
+
@reader.close if @reader
|
48
|
+
stop
|
49
|
+
end
|
50
|
+
|
51
|
+
def trigger_on_open
|
52
|
+
@opened = true
|
53
|
+
@onopen.call if @onopen
|
54
|
+
end
|
55
|
+
|
56
|
+
def trigger_on_close
|
57
|
+
close
|
58
|
+
@closed = true
|
59
|
+
@onclose.call if @onclose
|
60
|
+
@onclose = nil
|
61
|
+
end
|
62
|
+
|
63
|
+
def trigger_on_receive(data)
|
64
|
+
@onreceive.call(data)
|
65
|
+
end
|
66
|
+
|
67
|
+
def start(base_uri, init_msg)
|
68
|
+
if (@retries += 1) > MAX_RETRIES
|
69
|
+
trigger_on_close
|
70
|
+
return
|
71
|
+
end
|
72
|
+
|
73
|
+
begin
|
74
|
+
parsed_uri = Addressable::URI.parse(base_uri)
|
75
|
+
parsed_uri.path = '/tunnels'
|
76
|
+
|
77
|
+
@log.debug "post #{parsed_uri.to_s}"
|
78
|
+
req = EM::HttpRequest.new(parsed_uri.to_s).post :body => init_msg, :head => { "Auth-Token" => @auth_token }
|
79
|
+
|
80
|
+
req.callback do
|
81
|
+
@log.debug "post #{parsed_uri.to_s} #{req.response_header.status}"
|
82
|
+
unless [200, 201, 204].include?(req.response_header.status)
|
83
|
+
start(base_uri, init_msg)
|
84
|
+
else
|
85
|
+
@retries = 0
|
86
|
+
resp = JSON.parse(req.response)
|
87
|
+
|
88
|
+
parsed_uri.path = resp["path"]
|
89
|
+
@tun_uri = parsed_uri.to_s
|
90
|
+
|
91
|
+
parsed_uri.path = resp["path_out"]
|
92
|
+
@reader = Reader.new(@log, parsed_uri.to_s, self, @auth_token)
|
93
|
+
|
94
|
+
parsed_uri.path = resp["path_in"]
|
95
|
+
@writer = Writer.new(@log, parsed_uri.to_s, self, @auth_token)
|
96
|
+
trigger_on_open
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
req.errback do
|
101
|
+
@log.debug "post #{parsed_uri.to_s} error"
|
102
|
+
start(base_uri, init_msg)
|
103
|
+
end
|
104
|
+
|
105
|
+
rescue Exception => e
|
106
|
+
@log.error e
|
107
|
+
trigger_on_close
|
108
|
+
raise e
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def stop
|
113
|
+
if (@retries += 1) > MAX_RETRIES
|
114
|
+
trigger_on_close
|
115
|
+
return
|
116
|
+
end
|
117
|
+
|
118
|
+
return if @tun_uri.nil?
|
119
|
+
|
120
|
+
@log.debug "delete #{@tun_uri}"
|
121
|
+
req = EM::HttpRequest.new("#{@tun_uri}").delete :head => { "Auth-Token" => @auth_token }
|
122
|
+
|
123
|
+
req.errback do
|
124
|
+
@log.debug "delete #{@tun_uri} error"
|
125
|
+
stop
|
126
|
+
end
|
127
|
+
|
128
|
+
req.callback do
|
129
|
+
@log.debug "delete #{@tun_uri} #{req.response_header.status}"
|
130
|
+
if [200, 202, 204, 404].include?(req.response_header.status)
|
131
|
+
trigger_on_close
|
132
|
+
else
|
133
|
+
stop
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
class Reader
|
139
|
+
def initialize(log, uri, conn, auth_token)
|
140
|
+
@log, @base_uri, @conn, @auth_token = log, uri, conn, auth_token
|
141
|
+
@retries = 0
|
142
|
+
@closing = false
|
143
|
+
start
|
144
|
+
end
|
145
|
+
|
146
|
+
def close
|
147
|
+
@closing = true
|
148
|
+
end
|
149
|
+
|
150
|
+
def start(seq = 1)
|
151
|
+
if (@retries += 1) > MAX_RETRIES
|
152
|
+
@conn.trigger_on_close
|
153
|
+
return
|
154
|
+
end
|
155
|
+
|
156
|
+
return if @closing
|
157
|
+
uri = "#{@base_uri}/#{seq}"
|
158
|
+
@log.debug "get #{uri}"
|
159
|
+
req = EM::HttpRequest.new(uri).get :timeout => 0, :head => { "Auth-Token" => @auth_token }
|
160
|
+
|
161
|
+
req.errback do
|
162
|
+
@log.debug "get #{uri} error"
|
163
|
+
start(seq)
|
164
|
+
end
|
165
|
+
|
166
|
+
req.callback do
|
167
|
+
@log.debug "get #{uri} #{req.response_header.status}"
|
168
|
+
case req.response_header.status
|
169
|
+
when 200
|
170
|
+
@conn.trigger_on_receive(req.response)
|
171
|
+
@retries = 0
|
172
|
+
start(seq + 1)
|
173
|
+
when 404
|
174
|
+
@conn.trigger_on_close
|
175
|
+
else
|
176
|
+
start(seq)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
class Writer
|
183
|
+
def initialize(log, uri, conn, auth_token)
|
184
|
+
@log, @uri, @conn, @auth_token = log, uri, conn, auth_token
|
185
|
+
@retries = 0
|
186
|
+
@seq, @write_buffer = 1, ""
|
187
|
+
@closing = @writing = false
|
188
|
+
end
|
189
|
+
|
190
|
+
def send_data(data)
|
191
|
+
@write_buffer << data
|
192
|
+
send_data_buffered
|
193
|
+
end
|
194
|
+
|
195
|
+
def close
|
196
|
+
@closing = true
|
197
|
+
end
|
198
|
+
|
199
|
+
def send_data_buffered
|
200
|
+
if (@retries += 1) > MAX_RETRIES
|
201
|
+
@conn.trigger_on_close
|
202
|
+
return
|
203
|
+
end
|
204
|
+
|
205
|
+
return if @closing
|
206
|
+
data, @write_buffer = @write_buffer, "" unless @writing
|
207
|
+
|
208
|
+
@writing = true
|
209
|
+
uri = "#{@uri}/#{@seq}"
|
210
|
+
@log.debug "put #{uri}"
|
211
|
+
req = EM::HttpRequest.new(uri).put :body => data, :head => { "Auth-Token" => @auth_token }
|
212
|
+
|
213
|
+
req.errback do
|
214
|
+
@log.debug "put #{uri} error"
|
215
|
+
send_data_buffered
|
216
|
+
end
|
217
|
+
|
218
|
+
req.callback do
|
219
|
+
@log.debug "put #{uri} #{req.response_header.status}"
|
220
|
+
case req.response_header.status
|
221
|
+
when 200, 202, 204
|
222
|
+
@writing = false
|
223
|
+
@seq += 1
|
224
|
+
@retries = 0
|
225
|
+
send_data_buffered unless @write_buffer.empty?
|
226
|
+
when 404
|
227
|
+
@conn.trigger_on_close
|
228
|
+
else
|
229
|
+
send_data_buffered
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# Copyright (c) 2009-2011 VMware, Inc.
|
2
|
+
|
3
|
+
require 'addressable/uri'
|
4
|
+
|
5
|
+
module Caldecott
|
6
|
+
module Client
|
7
|
+
module Tunnel
|
8
|
+
# Note: I wanted to do this with self#new but had issues
|
9
|
+
# with getting send :initialize to figure out the right
|
10
|
+
# number of arguments
|
11
|
+
def self.start(logger, tun_url, dst_host, dst_port, auth_token)
|
12
|
+
case Addressable::URI.parse(tun_url).normalized_scheme
|
13
|
+
when "http", "https"
|
14
|
+
HttpTunnel.new(logger, tun_url, dst_host, dst_port, auth_token)
|
15
|
+
when "ws"
|
16
|
+
WebSocketTunnel.new(logger, tun_url, dst_host, dst_port, auth_token)
|
17
|
+
else
|
18
|
+
raise "invalid url"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# Copyright (c) 2009-2011 VMware, Inc.
|
2
|
+
|
3
|
+
require 'em-http'
|
4
|
+
|
5
|
+
module Caldecott
|
6
|
+
module Client
|
7
|
+
class WebSocketTunnel
|
8
|
+
def initialize(logger, url, dst_host, dst_port, auth_token)
|
9
|
+
@ws = EM::HttpRequest.new("#{url}/websocket/#{dst_host}/#{dst_port}").get :timeout => 0
|
10
|
+
end
|
11
|
+
|
12
|
+
def onopen(&blk)
|
13
|
+
@ws.callback { blk.call }
|
14
|
+
end
|
15
|
+
|
16
|
+
def onclose(&blk)
|
17
|
+
@ws.errback { blk.call }
|
18
|
+
@ws.disconnect { blk.call }
|
19
|
+
end
|
20
|
+
|
21
|
+
def onreceive(&blk)
|
22
|
+
@ws.stream { |data| blk.call(Base64.decode64(data)) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def send_data(data)
|
26
|
+
# Um.. as soon as the em websocket object adds a better named
|
27
|
+
# method for this, start using it.
|
28
|
+
@ws.send(Base64.encode64(data))
|
29
|
+
end
|
30
|
+
|
31
|
+
def close
|
32
|
+
@ws.close_connection_after_writing
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,212 @@
|
|
1
|
+
# Copyright (c) 2009-2011 VMware, Inc.
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'logger'
|
5
|
+
require 'sinatra'
|
6
|
+
require 'sinatra/async'
|
7
|
+
require 'json'
|
8
|
+
require 'uuidtools'
|
9
|
+
require 'eventmachine'
|
10
|
+
require 'caldecott/tcp_connection.rb'
|
11
|
+
require 'caldecott/session_logger.rb'
|
12
|
+
|
13
|
+
module Caldecott
|
14
|
+
module Server
|
15
|
+
class Tunnel
|
16
|
+
attr_reader :tun_id, :log, :last_active_at
|
17
|
+
DEFAULT_MAX_DATA_TO_BUFFER = 1 * 1024 * 1024 # 1MB
|
18
|
+
|
19
|
+
def initialize(log, tunnels, host, port, max_data_to_buffer = DEFAULT_MAX_DATA_TO_BUFFER)
|
20
|
+
@log, @tunnels, @host, @port = log, tunnels, host, port
|
21
|
+
@tun_id = UUIDTools::UUID.random_create.to_s
|
22
|
+
@data = @data_next = ""
|
23
|
+
@seq_out = @seq_in = 0
|
24
|
+
@max_data_to_buffer = max_data_to_buffer
|
25
|
+
@last_active_at = Time.now
|
26
|
+
end
|
27
|
+
|
28
|
+
def open(resp)
|
29
|
+
EM::connect(@host, @port, TcpConnection) do |dst_conn|
|
30
|
+
@dst_conn = dst_conn
|
31
|
+
|
32
|
+
@dst_conn.onopen do
|
33
|
+
@log.debug "dst connected"
|
34
|
+
@tunnels[@tun_id] = self
|
35
|
+
resp.content_type :json
|
36
|
+
resp.status 201
|
37
|
+
resp.body safe_hash.to_json
|
38
|
+
end
|
39
|
+
|
40
|
+
@dst_conn.onreceive do |data|
|
41
|
+
@log.debug "t <- d #{data.length}"
|
42
|
+
@data_next << data
|
43
|
+
trigger_reader
|
44
|
+
@dst_conn.pause if @data_next.length > @max_data_to_buffer
|
45
|
+
end
|
46
|
+
|
47
|
+
@dst_conn.onclose do
|
48
|
+
@log.debug "target disconnected"
|
49
|
+
@dst_conn = nil
|
50
|
+
trigger_reader
|
51
|
+
@tunnels.delete(@tun_id) if @data_next.empty?
|
52
|
+
end
|
53
|
+
end
|
54
|
+
@tunnel_created_at = Time.now
|
55
|
+
end
|
56
|
+
|
57
|
+
def delete
|
58
|
+
@log.debug "target disconnected"
|
59
|
+
if @dst_conn
|
60
|
+
@dst_conn.close_connection_after_writing
|
61
|
+
else
|
62
|
+
@tunnels.delete(@tun_id)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def get(resp, seq)
|
67
|
+
@last_active_at = Time.now
|
68
|
+
resp.halt(400, "invalid sequence #{seq} for server seq #{@seq_out}") unless (seq == @seq_out or seq == @seq_out + 1)
|
69
|
+
if seq == @seq_out + 1
|
70
|
+
@data, @data_next = @data_next, ""
|
71
|
+
@seq_out = seq
|
72
|
+
end
|
73
|
+
|
74
|
+
if @data.empty?
|
75
|
+
resp.halt(410, "destination socket closed\n") if @dst_conn.nil?
|
76
|
+
@log.debug "get: waiting for data"
|
77
|
+
@reader = EM.Callback do
|
78
|
+
@data, @data_next = @data_next, ""
|
79
|
+
resp.ahalt(410, "destination socket closed\n") if @data.empty?
|
80
|
+
@log.debug "get: returning data (async)"
|
81
|
+
resp.body @data
|
82
|
+
end
|
83
|
+
else
|
84
|
+
@log.debug "get: returning data (immediate)"
|
85
|
+
resp.body @data
|
86
|
+
@dst_conn.resume
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def put(resp, seq)
|
91
|
+
@last_active_at = Time.now
|
92
|
+
resp.halt(400, "invalid sequence #{seq} for server seq #{@seq_in}") unless (seq == @seq_in or seq == @seq_in + 1)
|
93
|
+
if seq == @seq_in
|
94
|
+
resp.status 201
|
95
|
+
else
|
96
|
+
@seq_in = seq
|
97
|
+
@log.debug "t -> d #{resp.request.body.length}"
|
98
|
+
@dst_conn.send_data(resp.request.body.read)
|
99
|
+
resp.status 202
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def trigger_reader
|
104
|
+
return unless @reader
|
105
|
+
reader = @reader
|
106
|
+
@reader = nil
|
107
|
+
reader.call
|
108
|
+
end
|
109
|
+
|
110
|
+
def safe_hash
|
111
|
+
{
|
112
|
+
:path => "/tunnels/#{@tun_id}",
|
113
|
+
:path_in => "/tunnels/#{@tun_id}/in",
|
114
|
+
:path_out => "/tunnels/#{@tun_id}/out",
|
115
|
+
:dst_host => @host,
|
116
|
+
:dst_port => @port,
|
117
|
+
:dst_connected => @dst_conn.nil? == false,
|
118
|
+
:seq_out => @seq_out,
|
119
|
+
:seq_in => @seq_in
|
120
|
+
}
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
class HttpTunnel < Sinatra::Base
|
126
|
+
register Sinatra::Async
|
127
|
+
|
128
|
+
@@tunnels = {}
|
129
|
+
|
130
|
+
def self.tunnels
|
131
|
+
@@tunnels
|
132
|
+
end
|
133
|
+
|
134
|
+
# defaults are 1 hour of inactivity with sweeps every 5 minutes
|
135
|
+
def self.start_timer(inactive_timeout = 3600, sweep_interval = 300)
|
136
|
+
EventMachine::add_periodic_timer sweep_interval do
|
137
|
+
# This is needed because there seems to have a bug on the
|
138
|
+
# Connection#set_comm_inactivity_timeout (int overflow )
|
139
|
+
# Look at eventmachine/ext/em.cpp 2289
|
140
|
+
# It reaps the inactive connections
|
141
|
+
#
|
142
|
+
# We also can not seem to add our own timer per tunnel instance.
|
143
|
+
# When we do, the ruby interpreter freaks out and starts throwing
|
144
|
+
#
|
145
|
+
# errors like:
|
146
|
+
# undefined method `cancel' for 57:Fixnum
|
147
|
+
#
|
148
|
+
# for code like the following during shutdown:
|
149
|
+
# @inactivity_timer.cancel if @inactivity_timer
|
150
|
+
# @inactivity_timer.cancel
|
151
|
+
# @inactivity_timer = nil
|
152
|
+
@@tunnels.each do |id, t|
|
153
|
+
t.delete if (Time.now - t.last_active_at) > inactive_timeout
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def tunnel_from_id(tun_id)
|
159
|
+
tun = @@tunnels[tun_id]
|
160
|
+
not_found("tunnel #{tun_id} does not exist\n") unless tun
|
161
|
+
tun.log.debug "#{request.request_method} #{request.url}"
|
162
|
+
tun
|
163
|
+
end
|
164
|
+
|
165
|
+
before do
|
166
|
+
@log = SessionLogger.new("server", STDOUT)
|
167
|
+
@log.debug "#{request.request_method} #{request.url}"
|
168
|
+
if env['HTTP_AUTH_TOKEN'] != settings.auth_token
|
169
|
+
@log.debug "AUTH FAILURE #{env.inspect}"
|
170
|
+
not_found
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
get '/' do
|
175
|
+
return "Caldecott Tunnel (HTTP Transport) #{VERSION}\n"
|
176
|
+
end
|
177
|
+
|
178
|
+
get '/tunnels' do
|
179
|
+
content_type :json
|
180
|
+
resp = @@tunnels.values.collect { |t| t.safe_hash }
|
181
|
+
resp.to_json
|
182
|
+
end
|
183
|
+
|
184
|
+
apost '/tunnels' do
|
185
|
+
req = JSON.parse(request.body.read, :symbolize_names => true)
|
186
|
+
Tunnel.new(@log, @@tunnels, req[:host], req[:port]).open(self)
|
187
|
+
end
|
188
|
+
|
189
|
+
get '/tunnels/:tun' do |tun_id|
|
190
|
+
tun = tunnel_from_id(tun_id)
|
191
|
+
tun.safe_hash.to_json
|
192
|
+
end
|
193
|
+
|
194
|
+
delete '/tunnels/:tun' do |tun_id|
|
195
|
+
tun = tunnel_from_id(tun_id)
|
196
|
+
tun.delete
|
197
|
+
end
|
198
|
+
|
199
|
+
aget '/tunnels/:tun_id/out/:seq' do |tun_id, seq|
|
200
|
+
tun = tunnel_from_id(tun_id)
|
201
|
+
seq = seq.to_i
|
202
|
+
tun.get(self, seq)
|
203
|
+
end
|
204
|
+
|
205
|
+
put '/tunnels/:tun_id/in/:seq' do |tun_id, seq|
|
206
|
+
tun = tunnel_from_id(tun_id)
|
207
|
+
seq = seq.to_i
|
208
|
+
tun.put(self, seq)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# Copyright (c) 2009-2011 VMware, Inc.
|
2
|
+
|
3
|
+
require 'em-websocket'
|
4
|
+
require 'base64'
|
5
|
+
|
6
|
+
module Caldecott
|
7
|
+
module Server
|
8
|
+
class WebSocketTunnel
|
9
|
+
|
10
|
+
# quack like sinatra
|
11
|
+
def self.run!(opts)
|
12
|
+
WebSocketTunnel.new.start(opts[:port])
|
13
|
+
end
|
14
|
+
|
15
|
+
def start(port)
|
16
|
+
EM::WebSocket.start(:host => "0.0.0.0", :port => port) do |ws|
|
17
|
+
log = SessionLogger::new("server", STDOUT)
|
18
|
+
dst_conn = nil
|
19
|
+
|
20
|
+
ws.onopen do
|
21
|
+
log.debug "tunnel connected"
|
22
|
+
slash, tunnel, host, port = ws.request['Path'].split('/')
|
23
|
+
|
24
|
+
EM::connect(host, port, TcpConnection) do |d|
|
25
|
+
dst_conn = d
|
26
|
+
|
27
|
+
dst_conn.onopen do
|
28
|
+
log.debug "target connected"
|
29
|
+
end
|
30
|
+
|
31
|
+
dst_conn.onreceive do |data|
|
32
|
+
log.debug("t <- d #{data.length}")
|
33
|
+
ws.send(Base64.encode64(data))
|
34
|
+
end
|
35
|
+
|
36
|
+
dst_conn.onclose do
|
37
|
+
log.debug "target disconnected"
|
38
|
+
ws.close_connection
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
ws.onmessage do |msg|
|
44
|
+
decoded = Base64.decode64(msg)
|
45
|
+
log.debug("t -> d #{decoded.length}")
|
46
|
+
dst_conn.send_data(decoded)
|
47
|
+
end
|
48
|
+
|
49
|
+
ws.onclose do
|
50
|
+
log.debug "tunnel disconnected"
|
51
|
+
dst_conn.close_connection_after_writing if dst_conn
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# Copyright (c) 2009-2011 VMware, Inc.
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module Caldecott
|
6
|
+
class SessionLogger < Logger
|
7
|
+
attr_reader :component, :session
|
8
|
+
@@session = 0
|
9
|
+
|
10
|
+
def initialize(component, *args)
|
11
|
+
super(*args)
|
12
|
+
@component = component
|
13
|
+
@session = @@session += 1
|
14
|
+
end
|
15
|
+
|
16
|
+
def format_message(severity, timestamp, progname, msg)
|
17
|
+
"#{@component} [#{@session}] #{msg}\n"
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.severity_from_string(str)
|
21
|
+
case str.upcase
|
22
|
+
when 'DEBUG'
|
23
|
+
Logger::DEBUG
|
24
|
+
when 'INFO'
|
25
|
+
Logger::INFO
|
26
|
+
when 'WARN'
|
27
|
+
Logger::WARN
|
28
|
+
when 'ERROR'
|
29
|
+
Logger::ERROR
|
30
|
+
when 'FATAL'
|
31
|
+
Logger::FATAL
|
32
|
+
when 'UNKNOWN'
|
33
|
+
Logger::UNKNOWN
|
34
|
+
else
|
35
|
+
Logger::ERROR
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# Copyright (c) 2009-2011 VMware, Inc.
|
2
|
+
|
3
|
+
require 'eventmachine'
|
4
|
+
|
5
|
+
module Caldecott
|
6
|
+
# wrapper to avoid callback and state passing spaghetti
|
7
|
+
class TcpConnection < EventMachine::Connection
|
8
|
+
@initialzied = false
|
9
|
+
|
10
|
+
# callbacks
|
11
|
+
def onopen(&blk)
|
12
|
+
@initialized ? blk.call : @onopen = blk
|
13
|
+
end
|
14
|
+
|
15
|
+
def onreceive(&blk)
|
16
|
+
@onreceive = blk
|
17
|
+
end
|
18
|
+
|
19
|
+
def onclose(&blk)
|
20
|
+
@onclose = blk
|
21
|
+
end
|
22
|
+
|
23
|
+
# handle EventMachine::Connection methods
|
24
|
+
def post_init
|
25
|
+
@initialized = true
|
26
|
+
@onopen.call if @onopen
|
27
|
+
end
|
28
|
+
|
29
|
+
def receive_data(data)
|
30
|
+
@onreceive.call(data) if @onreceive
|
31
|
+
end
|
32
|
+
|
33
|
+
def unbind
|
34
|
+
@onclose.call if @onclose
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
data/lib/caldecott/version.rb
CHANGED
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: caldecott
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.0.
|
5
|
+
version: 0.0.3
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- VMware
|
@@ -10,11 +10,131 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2011-
|
13
|
+
date: 2011-11-08 00:00:00 -08:00
|
14
14
|
default_executable:
|
15
|
-
dependencies:
|
16
|
-
|
17
|
-
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: em-http-request
|
18
|
+
prerelease: false
|
19
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
|
+
none: false
|
21
|
+
requirements:
|
22
|
+
- - "="
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: 0.3.0
|
25
|
+
type: :runtime
|
26
|
+
version_requirements: *id001
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: em-websocket
|
29
|
+
prerelease: false
|
30
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
31
|
+
none: false
|
32
|
+
requirements:
|
33
|
+
- - "="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: 0.3.1
|
36
|
+
type: :runtime
|
37
|
+
version_requirements: *id002
|
38
|
+
- !ruby/object:Gem::Dependency
|
39
|
+
name: async_sinatra
|
40
|
+
prerelease: false
|
41
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - "="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 0.5.0
|
47
|
+
type: :runtime
|
48
|
+
version_requirements: *id003
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: addressable
|
51
|
+
prerelease: false
|
52
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - "="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: 2.2.6
|
58
|
+
type: :runtime
|
59
|
+
version_requirements: *id004
|
60
|
+
- !ruby/object:Gem::Dependency
|
61
|
+
name: json
|
62
|
+
prerelease: false
|
63
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
64
|
+
none: false
|
65
|
+
requirements:
|
66
|
+
- - "="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.6.1
|
69
|
+
type: :runtime
|
70
|
+
version_requirements: *id005
|
71
|
+
- !ruby/object:Gem::Dependency
|
72
|
+
name: uuidtools
|
73
|
+
prerelease: false
|
74
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
75
|
+
none: false
|
76
|
+
requirements:
|
77
|
+
- - "="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: 2.1.2
|
80
|
+
type: :runtime
|
81
|
+
version_requirements: *id006
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: rake
|
84
|
+
prerelease: false
|
85
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
86
|
+
none: false
|
87
|
+
requirements:
|
88
|
+
- - "="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: 0.9.2
|
91
|
+
type: :development
|
92
|
+
version_requirements: *id007
|
93
|
+
- !ruby/object:Gem::Dependency
|
94
|
+
name: rcov
|
95
|
+
prerelease: false
|
96
|
+
requirement: &id008 !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - "="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: 0.9.10
|
102
|
+
type: :development
|
103
|
+
version_requirements: *id008
|
104
|
+
- !ruby/object:Gem::Dependency
|
105
|
+
name: rack-test
|
106
|
+
prerelease: false
|
107
|
+
requirement: &id009 !ruby/object:Gem::Requirement
|
108
|
+
none: false
|
109
|
+
requirements:
|
110
|
+
- - "="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: 0.6.1
|
113
|
+
type: :development
|
114
|
+
version_requirements: *id009
|
115
|
+
- !ruby/object:Gem::Dependency
|
116
|
+
name: rspec
|
117
|
+
prerelease: false
|
118
|
+
requirement: &id010 !ruby/object:Gem::Requirement
|
119
|
+
none: false
|
120
|
+
requirements:
|
121
|
+
- - "="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: 2.6.0
|
124
|
+
type: :development
|
125
|
+
version_requirements: *id010
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: webmock
|
128
|
+
prerelease: false
|
129
|
+
requirement: &id011 !ruby/object:Gem::Requirement
|
130
|
+
none: false
|
131
|
+
requirements:
|
132
|
+
- - "="
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: 1.7.6
|
135
|
+
type: :development
|
136
|
+
version_requirements: *id011
|
137
|
+
description: Caldecott HTTP/Websocket Tunneling Library
|
18
138
|
email: support@vmware.com
|
19
139
|
executables: []
|
20
140
|
|
@@ -26,7 +146,18 @@ extra_rdoc_files:
|
|
26
146
|
files:
|
27
147
|
- LICENSE
|
28
148
|
- README.md
|
149
|
+
- lib/caldecott/client/client.rb
|
150
|
+
- lib/caldecott/client/http_tunnel.rb
|
151
|
+
- lib/caldecott/client/tunnel.rb
|
152
|
+
- lib/caldecott/client/websocket_tunnel.rb
|
153
|
+
- lib/caldecott/client.rb
|
154
|
+
- lib/caldecott/server/http_tunnel.rb
|
155
|
+
- lib/caldecott/server/websocket_tunnel.rb
|
156
|
+
- lib/caldecott/server.rb
|
157
|
+
- lib/caldecott/session_logger.rb
|
158
|
+
- lib/caldecott/tcp_connection.rb
|
29
159
|
- lib/caldecott/version.rb
|
160
|
+
- lib/caldecott.rb
|
30
161
|
has_rdoc: true
|
31
162
|
homepage: http://vmware.com
|
32
163
|
licenses: []
|
@@ -54,6 +185,6 @@ rubyforge_project:
|
|
54
185
|
rubygems_version: 1.6.2
|
55
186
|
signing_key:
|
56
187
|
specification_version: 3
|
57
|
-
summary:
|
188
|
+
summary: Caldecott HTTP/Websocket Tunneling Library
|
58
189
|
test_files: []
|
59
190
|
|