tipi 0.32 → 0.37
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -0
- data/Gemfile.lock +10 -4
- data/LICENSE +1 -1
- data/TODO.md +13 -47
- data/bin/tipi +13 -0
- data/df/agent.rb +63 -0
- data/df/etc_benchmark.rb +15 -0
- data/df/multi_agent_supervisor.rb +87 -0
- data/df/multi_client.rb +84 -0
- data/df/routing_benchmark.rb +60 -0
- data/df/sample_agent.rb +89 -0
- data/df/server.rb +54 -0
- data/df/sse_page.html +29 -0
- data/df/stress.rb +24 -0
- data/df/ws_page.html +38 -0
- data/e +0 -0
- data/examples/http_request_ws_server.rb +35 -0
- data/examples/http_server.rb +6 -6
- data/examples/http_server_forked.rb +4 -5
- data/examples/http_server_form.rb +23 -0
- data/examples/http_server_throttled_accept.rb +23 -0
- data/examples/http_unix_socket_server.rb +17 -0
- data/examples/http_ws_server.rb +10 -12
- data/examples/routing_server.rb +34 -0
- data/examples/ws_page.html +1 -2
- data/lib/tipi.rb +7 -5
- data/lib/tipi/configuration.rb +1 -1
- data/lib/tipi/digital_fabric.rb +7 -0
- data/lib/tipi/digital_fabric/agent.rb +225 -0
- data/lib/tipi/digital_fabric/agent_proxy.rb +265 -0
- data/lib/tipi/digital_fabric/executive.rb +100 -0
- data/lib/tipi/digital_fabric/executive/index.html +69 -0
- data/lib/tipi/digital_fabric/protocol.rb +90 -0
- data/lib/tipi/digital_fabric/request_adapter.rb +48 -0
- data/lib/tipi/digital_fabric/service.rb +230 -0
- data/lib/tipi/http1_adapter.rb +50 -16
- data/lib/tipi/http2_adapter.rb +5 -3
- data/lib/tipi/http2_stream.rb +19 -7
- data/lib/tipi/rack_adapter.rb +11 -3
- data/lib/tipi/version.rb +1 -1
- data/lib/tipi/websocket.rb +33 -29
- data/test/helper.rb +1 -2
- data/test/test_http_server.rb +3 -2
- data/test/test_request.rb +108 -0
- data/tipi.gemspec +7 -3
- metadata +59 -7
- data/lib/tipi/request.rb +0 -118
data/examples/http_ws_server.rb
CHANGED
@@ -5,14 +5,14 @@ require 'tipi'
|
|
5
5
|
require 'tipi/websocket'
|
6
6
|
|
7
7
|
def ws_handler(conn)
|
8
|
-
timer =
|
9
|
-
|
10
|
-
conn << Time.now.to_s
|
11
|
-
end
|
8
|
+
timer = spin_loop(interval: 1) do
|
9
|
+
conn << Time.now.to_s
|
12
10
|
end
|
13
11
|
while (msg = conn.recv)
|
14
12
|
conn << "you said: #{msg}"
|
15
13
|
end
|
14
|
+
rescue Exception => e
|
15
|
+
p e
|
16
16
|
ensure
|
17
17
|
timer.stop
|
18
18
|
end
|
@@ -21,17 +21,15 @@ opts = {
|
|
21
21
|
reuse_addr: true,
|
22
22
|
dont_linger: true,
|
23
23
|
upgrade: {
|
24
|
-
websocket:
|
24
|
+
websocket: Tipi::Websocket.handler(&method(:ws_handler))
|
25
25
|
}
|
26
26
|
}
|
27
27
|
|
28
28
|
HTML = IO.read(File.join(__dir__, 'ws_page.html'))
|
29
29
|
|
30
|
-
spin do
|
31
|
-
Tipi.serve('0.0.0.0', 1234, opts) do |req|
|
32
|
-
req.respond(HTML, 'Content-Type' => 'text/html')
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
30
|
puts "pid: #{Process.pid}"
|
37
|
-
puts 'Listening on port
|
31
|
+
puts 'Listening on port 4411...'
|
32
|
+
|
33
|
+
Tipi.serve('0.0.0.0', 4411, opts) do |req|
|
34
|
+
req.respond(HTML, 'Content-Type' => 'text/html')
|
35
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'tipi'
|
5
|
+
|
6
|
+
opts = {
|
7
|
+
reuse_addr: true,
|
8
|
+
dont_linger: true
|
9
|
+
}
|
10
|
+
|
11
|
+
puts "pid: #{Process.pid}"
|
12
|
+
puts 'Listening on port 4411...'
|
13
|
+
|
14
|
+
app = Tipi.route do |r|
|
15
|
+
r.root do
|
16
|
+
r.redirect '/hello'
|
17
|
+
end
|
18
|
+
r.on 'hello' do
|
19
|
+
r.get 'world' do
|
20
|
+
r.respond 'Hello world'
|
21
|
+
end
|
22
|
+
r.get do
|
23
|
+
r.respond 'Hello'
|
24
|
+
end
|
25
|
+
r.post do
|
26
|
+
puts 'Someone said Hello'
|
27
|
+
r.redirect '/'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
spin do
|
33
|
+
Tipi.serve('0.0.0.0', 4411, opts, &app)
|
34
|
+
end.await
|
data/examples/ws_page.html
CHANGED
@@ -6,14 +6,13 @@
|
|
6
6
|
<body>
|
7
7
|
<script>
|
8
8
|
var connect = function () {
|
9
|
-
var exampleSocket = new WebSocket("
|
9
|
+
var exampleSocket = new WebSocket("wss://dev.realiteq.net/");
|
10
10
|
|
11
11
|
exampleSocket.onopen = function (event) {
|
12
12
|
document.querySelector('#status').innerText = 'connected';
|
13
13
|
exampleSocket.send("Can you hear me?");
|
14
14
|
};
|
15
15
|
exampleSocket.onclose = function (event) {
|
16
|
-
console.log('onclose');
|
17
16
|
document.querySelector('#status').innerText = 'disconnected';
|
18
17
|
setTimeout(function () {
|
19
18
|
// exampleSocket.removeAllListeners();
|
data/lib/tipi.rb
CHANGED
@@ -28,10 +28,8 @@ module Tipi
|
|
28
28
|
end
|
29
29
|
|
30
30
|
def accept_loop(server, opts, &handler)
|
31
|
-
|
32
|
-
client = server.accept
|
31
|
+
server.accept_loop do |client|
|
33
32
|
spin { client_loop(client, opts, &handler) }
|
34
|
-
snooze
|
35
33
|
rescue OpenSSL::SSL::SSLError
|
36
34
|
# disregard
|
37
35
|
end
|
@@ -42,14 +40,18 @@ module Tipi
|
|
42
40
|
adapter = protocol_adapter(client, opts)
|
43
41
|
adapter.each(&handler)
|
44
42
|
ensure
|
45
|
-
client.close
|
43
|
+
client.close rescue nil
|
46
44
|
end
|
47
45
|
|
48
46
|
def protocol_adapter(socket, opts)
|
49
47
|
use_http2 = socket.respond_to?(:alpn_protocol) &&
|
50
|
-
|
48
|
+
socket.alpn_protocol == H2_PROTOCOL
|
51
49
|
klass = use_http2 ? HTTP2Adapter : HTTP1Adapter
|
52
50
|
klass.new(socket, opts)
|
53
51
|
end
|
52
|
+
|
53
|
+
def route(&block)
|
54
|
+
proc { |req| req.route(&block) }
|
55
|
+
end
|
54
56
|
end
|
55
57
|
end
|
data/lib/tipi/configuration.rb
CHANGED
@@ -0,0 +1,225 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './protocol'
|
4
|
+
require_relative './request_adapter'
|
5
|
+
|
6
|
+
require 'msgpack'
|
7
|
+
require 'tipi/websocket'
|
8
|
+
require 'tipi/request'
|
9
|
+
|
10
|
+
module DigitalFabric
|
11
|
+
class Agent
|
12
|
+
def initialize(server_url, route, token)
|
13
|
+
@server_url = server_url
|
14
|
+
@route = route
|
15
|
+
@token = token
|
16
|
+
@requests = {}
|
17
|
+
@long_running_requests = {}
|
18
|
+
@name = '<unknown>'
|
19
|
+
end
|
20
|
+
|
21
|
+
class TimeoutError < RuntimeError
|
22
|
+
end
|
23
|
+
|
24
|
+
class GracefulShutdown < RuntimeError
|
25
|
+
end
|
26
|
+
|
27
|
+
def run
|
28
|
+
@fiber = Fiber.current
|
29
|
+
@keep_alive_timer = spin_loop(interval: 5) { keep_alive }
|
30
|
+
while true
|
31
|
+
connect_and_process_incoming_requests
|
32
|
+
return if @shutdown
|
33
|
+
sleep 5
|
34
|
+
end
|
35
|
+
ensure
|
36
|
+
@keep_alive_timer.stop
|
37
|
+
end
|
38
|
+
|
39
|
+
def connect_and_process_incoming_requests
|
40
|
+
# log 'Connecting...'
|
41
|
+
@socket = connect_to_server
|
42
|
+
@last_recv = @last_send = Time.now
|
43
|
+
|
44
|
+
df_upgrade
|
45
|
+
@connected = true
|
46
|
+
@msgpack_reader = MessagePack::Unpacker.new
|
47
|
+
|
48
|
+
process_incoming_requests
|
49
|
+
rescue IOError, Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EPIPE, TimeoutError
|
50
|
+
log 'Disconnected' if @connected
|
51
|
+
@connected = nil
|
52
|
+
end
|
53
|
+
|
54
|
+
def connect_to_server
|
55
|
+
if @server_url =~ /^([^\:]+)\:(\d+)$/
|
56
|
+
host = Regexp.last_match(1)
|
57
|
+
port = Regexp.last_match(2)
|
58
|
+
Polyphony::Net.tcp_connect(host, port)
|
59
|
+
else
|
60
|
+
UNIXSocket.new(@server_url)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
UPGRADE_REQUEST = <<~HTTP
|
65
|
+
GET / HTTP/1.1
|
66
|
+
Host: localhost
|
67
|
+
Connection: upgrade
|
68
|
+
Upgrade: df
|
69
|
+
DF-Token: %s
|
70
|
+
DF-Mount: %s
|
71
|
+
|
72
|
+
HTTP
|
73
|
+
|
74
|
+
def df_upgrade
|
75
|
+
@socket << format(UPGRADE_REQUEST, @token, mount_point)
|
76
|
+
while (line = @socket.gets)
|
77
|
+
break if line.chomp.empty?
|
78
|
+
end
|
79
|
+
# log 'Connection upgraded'
|
80
|
+
end
|
81
|
+
|
82
|
+
def mount_point
|
83
|
+
if @route[:host]
|
84
|
+
"host=#{@route[:host]}"
|
85
|
+
elsif @route[:path]
|
86
|
+
"path=#{@route[:path]}"
|
87
|
+
else
|
88
|
+
nil
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def log(msg)
|
93
|
+
puts "#{Time.now} (#{@name}) #{msg}"
|
94
|
+
end
|
95
|
+
|
96
|
+
def process_incoming_requests
|
97
|
+
@socket.feed_loop(@msgpack_reader, :feed_each) do |msg|
|
98
|
+
recv_df_message(msg)
|
99
|
+
return if @shutdown && @requests.empty?
|
100
|
+
end
|
101
|
+
rescue IOError, SystemCallError, TimeoutError
|
102
|
+
# ignore
|
103
|
+
end
|
104
|
+
|
105
|
+
def keep_alive
|
106
|
+
return unless @connected
|
107
|
+
|
108
|
+
now = Time.now
|
109
|
+
if now - @last_send >= Protocol::SEND_TIMEOUT
|
110
|
+
send_df_message(Protocol.ping)
|
111
|
+
end
|
112
|
+
# if now - @last_recv >= Protocol::RECV_TIMEOUT
|
113
|
+
# raise TimeoutError
|
114
|
+
# end
|
115
|
+
rescue IOError, SystemCallError => e
|
116
|
+
# transmit exception to fiber running the agent
|
117
|
+
@fiber.raise(e)
|
118
|
+
end
|
119
|
+
|
120
|
+
def recv_df_message(msg)
|
121
|
+
@last_recv = Time.now
|
122
|
+
case msg['kind']
|
123
|
+
when Protocol::SHUTDOWN
|
124
|
+
recv_shutdown
|
125
|
+
when Protocol::HTTP_REQUEST
|
126
|
+
recv_http_request(msg)
|
127
|
+
when Protocol::HTTP_REQUEST_BODY
|
128
|
+
recv_http_request_body(msg)
|
129
|
+
when Protocol::WS_REQUEST
|
130
|
+
recv_ws_request(msg)
|
131
|
+
when Protocol::CONN_DATA, Protocol::CONN_CLOSE,
|
132
|
+
Protocol::WS_DATA, Protocol::WS_CLOSE
|
133
|
+
fiber = @requests[msg['id']]
|
134
|
+
fiber << msg if fiber
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def send_df_message(msg)
|
139
|
+
# we mark long-running requests by applying simple heuristics to sent DF
|
140
|
+
# messages. This is so we can correctly stop long-running requests
|
141
|
+
# upon graceful shutdown
|
142
|
+
if is_long_running_request_response?(msg)
|
143
|
+
id = msg[:id]
|
144
|
+
@long_running_requests[id] = @requests[id]
|
145
|
+
end
|
146
|
+
@last_send = Time.now
|
147
|
+
@socket << msg.to_msgpack
|
148
|
+
end
|
149
|
+
|
150
|
+
def is_long_running_request_response?(msg)
|
151
|
+
case msg[:kind]
|
152
|
+
when Protocol::HTTP_UPGRADE
|
153
|
+
true
|
154
|
+
when Protocol::HTTP_RESPONSE
|
155
|
+
msg[:body] && !msg[:complete]
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def recv_shutdown
|
160
|
+
# puts "Received shutdown message (#{@requests.size} pending requests)"
|
161
|
+
# puts " (Long running requests: #{@long_running_requests.size})"
|
162
|
+
@shutdown = true
|
163
|
+
@long_running_requests.values.each { |f| f.terminate(true) }
|
164
|
+
end
|
165
|
+
|
166
|
+
def recv_http_request(msg)
|
167
|
+
req = prepare_http_request(msg)
|
168
|
+
id = msg['id']
|
169
|
+
@requests[id] = spin do
|
170
|
+
http_request(req)
|
171
|
+
rescue IOError, Errno::ECONNREFUSED, Errno::EPIPE
|
172
|
+
# ignore
|
173
|
+
rescue Polyphony::Terminate
|
174
|
+
req.respond(nil, { ':status' => Qeweney::Status::SERVICE_UNAVAILABLE }) if Fiber.current.graceful_shutdown?
|
175
|
+
ensure
|
176
|
+
@requests.delete(id)
|
177
|
+
@long_running_requests.delete(id)
|
178
|
+
@fiber.terminate if @shutdown && @requests.empty?
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def prepare_http_request(msg)
|
183
|
+
req = Qeweney::Request.new(msg['headers'], RequestAdapter.new(self, msg))
|
184
|
+
req.buffer_body_chunk(msg['body']) if msg['body']
|
185
|
+
req.complete! if msg['complete']
|
186
|
+
req
|
187
|
+
end
|
188
|
+
|
189
|
+
def recv_http_request_body(msg)
|
190
|
+
fiber = @requests[msg['id']]
|
191
|
+
return unless fiber
|
192
|
+
|
193
|
+
fiber << msg['body']
|
194
|
+
end
|
195
|
+
|
196
|
+
def get_http_request_body(id, limit)
|
197
|
+
send_df_message(Protocol.http_get_request_body(id, limit))
|
198
|
+
receive
|
199
|
+
end
|
200
|
+
|
201
|
+
def recv_ws_request(msg)
|
202
|
+
req = Qeweney::Request.new(msg['headers'], RequestAdapter.new(self, msg))
|
203
|
+
id = msg['id']
|
204
|
+
@requests[id] = @long_running_requests[id] = spin do
|
205
|
+
ws_request(req)
|
206
|
+
rescue IOError, Errno::ECONNREFUSED, Errno::EPIPE
|
207
|
+
# ignore
|
208
|
+
ensure
|
209
|
+
@requests.delete(id)
|
210
|
+
@long_running_requests.delete(id)
|
211
|
+
@fiber.terminate if @shutdown && @requests.empty?
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
# default handler for HTTP request
|
216
|
+
def http_request(req)
|
217
|
+
req.respond(nil, { ':status': Qeweney::Status::SERVICE_UNAVAILABLE })
|
218
|
+
end
|
219
|
+
|
220
|
+
# default handler for WS request
|
221
|
+
def ws_request(req)
|
222
|
+
req.respond(nil, { ':status': Qeweney::Status::SERVICE_UNAVAILABLE })
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
@@ -0,0 +1,265 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './protocol'
|
4
|
+
require 'msgpack'
|
5
|
+
require 'tipi/websocket'
|
6
|
+
|
7
|
+
module DigitalFabric
|
8
|
+
class AgentProxy
|
9
|
+
def initialize(service, req)
|
10
|
+
@service = service
|
11
|
+
@req = req
|
12
|
+
@conn = req.adapter.conn
|
13
|
+
@msgpack_reader = MessagePack::Unpacker.new
|
14
|
+
@requests = {}
|
15
|
+
@current_request_count = 0
|
16
|
+
@last_request_id = 0
|
17
|
+
@last_recv = @last_send = Time.now
|
18
|
+
run
|
19
|
+
end
|
20
|
+
|
21
|
+
def current_request_count
|
22
|
+
@current_request_count
|
23
|
+
end
|
24
|
+
|
25
|
+
class TimeoutError < RuntimeError
|
26
|
+
end
|
27
|
+
|
28
|
+
class GracefulShutdown < RuntimeError
|
29
|
+
end
|
30
|
+
|
31
|
+
def run
|
32
|
+
@fiber = Fiber.current
|
33
|
+
@service.mount(route, self)
|
34
|
+
keep_alive_timer = spin_loop(interval: 5) { keep_alive }
|
35
|
+
process_incoming_messages(false)
|
36
|
+
rescue GracefulShutdown
|
37
|
+
puts "Proxy got graceful shutdown, left: #{@requests.size} requests" if @requests.size > 0
|
38
|
+
process_incoming_messages(true)
|
39
|
+
ensure
|
40
|
+
keep_alive_timer&.stop
|
41
|
+
@service.unmount(self)
|
42
|
+
end
|
43
|
+
|
44
|
+
def process_incoming_messages(shutdown = false)
|
45
|
+
return if shutdown && @requests.empty?
|
46
|
+
|
47
|
+
@conn.feed_loop(@msgpack_reader, :feed_each) do |msg|
|
48
|
+
recv_df_message(msg)
|
49
|
+
return if shutdown && @requests.empty?
|
50
|
+
end
|
51
|
+
rescue TimeoutError, IOError
|
52
|
+
end
|
53
|
+
|
54
|
+
def shutdown
|
55
|
+
send_df_message(Protocol.shutdown)
|
56
|
+
@fiber.raise GracefulShutdown.new
|
57
|
+
end
|
58
|
+
|
59
|
+
def keep_alive
|
60
|
+
now = Time.now
|
61
|
+
if now - @last_send >= Protocol::SEND_TIMEOUT
|
62
|
+
send_df_message(Protocol.ping)
|
63
|
+
end
|
64
|
+
# if now - @last_recv >= Protocol::RECV_TIMEOUT
|
65
|
+
# raise TimeoutError
|
66
|
+
# end
|
67
|
+
rescue TimeoutError, IOError
|
68
|
+
end
|
69
|
+
|
70
|
+
def route
|
71
|
+
case @req.headers['df-mount']
|
72
|
+
when /^\s*host\s*=\s*([^\s]+)/
|
73
|
+
{ host: Regexp.last_match(1) }
|
74
|
+
when /^\s*path\s*=\s*([^\s]+)/
|
75
|
+
{ path: Regexp.last_match(1) }
|
76
|
+
when /catch_all/
|
77
|
+
{ catch_all: true }
|
78
|
+
else
|
79
|
+
nil
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def recv_df_message(message)
|
84
|
+
@last_recv = Time.now
|
85
|
+
return if message['kind'] == Protocol::PING
|
86
|
+
|
87
|
+
handler = @requests[message['id']]
|
88
|
+
if !handler
|
89
|
+
# puts "Unknown request id in #{message}"
|
90
|
+
return
|
91
|
+
end
|
92
|
+
|
93
|
+
handler << message
|
94
|
+
end
|
95
|
+
|
96
|
+
def send_df_message(message)
|
97
|
+
@last_send = Time.now
|
98
|
+
@conn << message.to_msgpack
|
99
|
+
end
|
100
|
+
|
101
|
+
# HTTP / WebSocket
|
102
|
+
|
103
|
+
def register_request_fiber
|
104
|
+
id = (@last_request_id += 1)
|
105
|
+
@requests[id] = Fiber.current
|
106
|
+
id
|
107
|
+
end
|
108
|
+
|
109
|
+
def unregister_request_fiber(id)
|
110
|
+
@requests.delete(id)
|
111
|
+
end
|
112
|
+
|
113
|
+
def with_request
|
114
|
+
@current_request_count += 1
|
115
|
+
id = (@last_request_id += 1)
|
116
|
+
@requests[id] = Fiber.current
|
117
|
+
yield id
|
118
|
+
ensure
|
119
|
+
@current_request_count -= 1
|
120
|
+
@requests.delete(id)
|
121
|
+
end
|
122
|
+
|
123
|
+
def http_request(req)
|
124
|
+
t0 = Time.now
|
125
|
+
t1 = nil
|
126
|
+
with_request do |id|
|
127
|
+
send_df_message(Protocol.http_request(id, req))
|
128
|
+
while (message = receive)
|
129
|
+
unless t1
|
130
|
+
t1 = Time.now
|
131
|
+
@service.record_latency_measurement(t1 - t0)
|
132
|
+
end
|
133
|
+
return if http_request_message(id, req, message)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
rescue => e
|
137
|
+
req.respond("Error: #{e.inspect}", ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)
|
138
|
+
end
|
139
|
+
|
140
|
+
# @return [Boolean] true if response is complete
|
141
|
+
def http_request_message(id, req, message)
|
142
|
+
case message['kind']
|
143
|
+
when Protocol::HTTP_UPGRADE
|
144
|
+
http_custom_upgrade(id, req, message)
|
145
|
+
true
|
146
|
+
when Protocol::HTTP_GET_REQUEST_BODY
|
147
|
+
http_get_request_body(id, req, message)
|
148
|
+
false
|
149
|
+
when Protocol::HTTP_RESPONSE
|
150
|
+
headers = message['headers']
|
151
|
+
body = message['body']
|
152
|
+
done = message['complete']
|
153
|
+
req.send_headers(headers) if headers && !req.headers_sent?
|
154
|
+
req.send_chunk(body, done: done) if body or done
|
155
|
+
done
|
156
|
+
else
|
157
|
+
# invalid message
|
158
|
+
true
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
HTTP_RESPONSE_UPGRADE_HEADERS = { ':status' => Qeweney::Status::SWITCHING_PROTOCOLS }
|
163
|
+
|
164
|
+
def http_custom_upgrade(id, req, message)
|
165
|
+
# send upgrade response
|
166
|
+
upgrade_headers = message['headers'] ?
|
167
|
+
message['headers'].merge(HTTP_RESPONSE_UPGRADE_HEADERS) :
|
168
|
+
HTTP_RESPONSE_UPGRADE_HEADERS
|
169
|
+
req.send_headers(upgrade_headers, true)
|
170
|
+
|
171
|
+
conn = req.adapter.conn
|
172
|
+
reader = spin do
|
173
|
+
conn.recv_loop do |data|
|
174
|
+
send_df_message(Protocol.conn_data(id, data))
|
175
|
+
end
|
176
|
+
end
|
177
|
+
while (message = receive)
|
178
|
+
return if http_custom_upgrade_message(conn, message)
|
179
|
+
end
|
180
|
+
ensure
|
181
|
+
reader.stop
|
182
|
+
end
|
183
|
+
|
184
|
+
def http_custom_upgrade_message(conn, message)
|
185
|
+
case message['kind']
|
186
|
+
when Protocol::CONN_DATA
|
187
|
+
conn << message['data']
|
188
|
+
false
|
189
|
+
when Protocol::CONN_CLOSE
|
190
|
+
true
|
191
|
+
else
|
192
|
+
# invalid message
|
193
|
+
true
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def http_get_request_body(id, req, message)
|
198
|
+
case (limit = message['limit'])
|
199
|
+
when nil
|
200
|
+
body = req.read
|
201
|
+
else
|
202
|
+
limit = limit.to_i
|
203
|
+
body = nil
|
204
|
+
req.each_chunk do |chunk|
|
205
|
+
(body ||= +'') << chunk
|
206
|
+
break if body.bytesize >= limit
|
207
|
+
end
|
208
|
+
end
|
209
|
+
send_df_message(Protocol.http_request_body(id, body, req.complete?))
|
210
|
+
end
|
211
|
+
|
212
|
+
def http_upgrade(req, protocol)
|
213
|
+
if protocol == 'websocket'
|
214
|
+
handle_websocket_upgrade(req)
|
215
|
+
else
|
216
|
+
# other protocol upgrades should be handled by the agent, so we just run
|
217
|
+
# the request normally. The agent is expected to upgrade the connection
|
218
|
+
# using a http_upgrade message. From that moment on, two-way
|
219
|
+
# communication is handled using conn_data and conn_close messages.
|
220
|
+
http_request(req)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def handle_websocket_upgrade(req)
|
225
|
+
with_request do |id|
|
226
|
+
send_df_message(Protocol.ws_request(id, req.headers))
|
227
|
+
response = receive
|
228
|
+
case response['kind']
|
229
|
+
when Protocol::WS_RESPONSE
|
230
|
+
headers = response['headers'] || {}
|
231
|
+
status = headers[':status'] || Qeweney::Status::SWITCHING_PROTOCOLS
|
232
|
+
if status != Qeweney::Status::SWITCHING_PROTOCOLS
|
233
|
+
req.respond(nil, headers)
|
234
|
+
return
|
235
|
+
end
|
236
|
+
ws = Tipi::Websocket.new(req.adapter.conn, req.headers)
|
237
|
+
run_websocket_connection(id, ws)
|
238
|
+
else
|
239
|
+
req.respond(nil, ':status' => Qeweney::Status::SERVICE_UNAVAILABLE)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
def run_websocket_connection(id, websocket)
|
245
|
+
reader = spin do
|
246
|
+
websocket.recv_loop do |data|
|
247
|
+
send_df_message(Protocol.ws_data(id, data))
|
248
|
+
end
|
249
|
+
end
|
250
|
+
while (message = receive)
|
251
|
+
case message['kind']
|
252
|
+
when Protocol::WS_DATA
|
253
|
+
websocket << message['data']
|
254
|
+
when Protocol::WS_CLOSE
|
255
|
+
return
|
256
|
+
else
|
257
|
+
raise "Unexpected websocket message #{message.inspect}"
|
258
|
+
end
|
259
|
+
end
|
260
|
+
ensure
|
261
|
+
reader.stop
|
262
|
+
websocket.close
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|