tipi 0.35 → 0.38
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +43 -27
- data/Gemfile.lock +5 -5
- data/TODO.md +3 -5
- data/e +0 -0
- data/examples/http_request_ws_server.rb +2 -1
- data/examples/http_server.rb +11 -3
- data/examples/https_server.rb +11 -1
- data/lib/tipi/digital_fabric/agent.rb +5 -5
- data/lib/tipi/digital_fabric/agent_proxy.rb +13 -8
- data/lib/tipi/digital_fabric/executive.rb +1 -1
- data/lib/tipi/digital_fabric/service.rb +7 -5
- data/lib/tipi/http1_adapter.rb +52 -32
- data/lib/tipi/http2_stream.rb +6 -6
- data/lib/tipi/version.rb +1 -1
- data/lib/tipi/websocket.rb +9 -22
- data/test/test_http_server.rb +22 -37
- data/test/test_request.rb +5 -5
- data/tipi.gemspec +2 -2
- metadata +9 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6a8c075ce0b769f014a20587dd061f17e32361f211aad5741f655dafe6687e37
|
4
|
+
data.tar.gz: d14c0fe026a7db1aa46edded54e5ee6f1a0f8598f626dd1aff342ad07b5cec39
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e306783fc83d9d7ebd0678c4d68a13f6f9bd77e4b9d83fc84d745e5cc3f53eb27b69286fb1ba9f1ca54fe57358009a4c11dc9374c8a8d9046dc4330e359dc988
|
7
|
+
data.tar.gz: 789c3b4e1374cc57e04e3bce4aa8d12022cfd7e31c9cba4f5777302f57943da0a967f17f8cb2f91551357deac1bbd07fe514c2cb2e002331f525d341c6b7f0e9
|
data/CHANGELOG.md
CHANGED
@@ -1,64 +1,80 @@
|
|
1
|
+
## 0.38 2021-03-09
|
2
|
+
|
3
|
+
- Don't use chunked transfer encoding for non-streaming responses
|
4
|
+
|
5
|
+
## 0.37.2 2021-03-08
|
6
|
+
|
7
|
+
- Fix header formatting when header value is an array
|
8
|
+
|
9
|
+
## 0.37 2021-02-15
|
10
|
+
|
11
|
+
- Update upgrade mechanism to work with updated Qeweney API
|
12
|
+
|
13
|
+
## 0.36 2021-02-12
|
14
|
+
|
15
|
+
- Use `Qeweney::Status` constants
|
16
|
+
|
1
17
|
## 0.35 2021-02-10
|
2
18
|
|
3
|
-
|
19
|
+
- Extract Request class into separate [qeweney](https://github.com/digital-fabric/qeweney) gem
|
4
20
|
|
5
21
|
## 0.34 2021-02-07
|
6
22
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
23
|
+
- Implement digital fabric service and agents
|
24
|
+
- Add multipart and urlencoded form data parsing
|
25
|
+
- Improve request body reading behaviour
|
26
|
+
- Add more `Request` information methods
|
27
|
+
- Add access to connection for HTTP2 requests
|
28
|
+
- Allow calling `Request#send_chunk` with empty chunk
|
29
|
+
- Add support for handling protocol upgrades from within request handler
|
14
30
|
|
15
31
|
## 0.33 2020-11-20
|
16
32
|
|
17
|
-
|
18
|
-
|
33
|
+
- Update code for Polyphony 0.47.5
|
34
|
+
- Add support for Rack::File body to Tipi::RackAdapter
|
19
35
|
|
20
36
|
## 0.32 2020-08-14
|
21
37
|
|
22
|
-
|
23
|
-
|
24
|
-
|
38
|
+
- Respond with array of strings instead of concatenating for HTTP 1
|
39
|
+
- Use read_loop instead of readpartial
|
40
|
+
- Fix http upgrade test
|
25
41
|
|
26
42
|
## 0.31 2020-07-28
|
27
43
|
|
28
|
-
|
29
|
-
|
30
|
-
|
44
|
+
- Fix websocket server code
|
45
|
+
- Implement configuration layer (WIP)
|
46
|
+
- Improve performance of rack adapter
|
31
47
|
|
32
48
|
## 0.30 2020-07-15
|
33
49
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
50
|
+
- Rename project to Tipi
|
51
|
+
- Rearrange source code
|
52
|
+
- Remove HTTP client code (to be developed eventually into a separate gem)
|
53
|
+
- Fix header rendering in rack adapter (#2)
|
38
54
|
|
39
55
|
## 0.29 2020-07-06
|
40
56
|
|
41
|
-
|
57
|
+
- Use IO#read_loop
|
42
58
|
|
43
59
|
## 0.28 2020-07-03
|
44
60
|
|
45
|
-
|
61
|
+
- Update with API changes from Polyphony >= 0.41
|
46
62
|
|
47
63
|
## 0.27 2020-04-14
|
48
64
|
|
49
|
-
|
65
|
+
- Remove modulation dependency
|
50
66
|
|
51
67
|
## 0.26 2020-03-03
|
52
68
|
|
53
|
-
|
69
|
+
- Fix `Server#listen`
|
54
70
|
|
55
71
|
## 0.25 2020-02-19
|
56
72
|
|
57
|
-
|
58
|
-
|
73
|
+
- Ensure server socket is closed upon stopping loop
|
74
|
+
- Fix `Request#format_header_lines`
|
59
75
|
|
60
76
|
## 0.24 2020-01-08
|
61
77
|
|
62
|
-
|
78
|
+
- Move HTTP to separate polyphony-http gem
|
63
79
|
|
64
80
|
For earlier changes look at the Polyphony changelog.
|
data/Gemfile.lock
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
tipi (0.
|
4
|
+
tipi (0.38)
|
5
5
|
http-2 (~> 0.10.0)
|
6
6
|
http_parser.rb (~> 0.6.0)
|
7
7
|
msgpack (~> 1.4.2)
|
8
|
-
polyphony (~> 0.
|
9
|
-
|
8
|
+
polyphony (~> 0.52.0)
|
9
|
+
qeweney (~> 0.6)
|
10
10
|
rack (>= 2.0.8, < 2.3.0)
|
11
11
|
websocket (~> 1.2.8)
|
12
12
|
|
@@ -28,8 +28,8 @@ GEM
|
|
28
28
|
minitest (>= 5.0)
|
29
29
|
ruby-progressbar
|
30
30
|
msgpack (1.4.2)
|
31
|
-
polyphony (0.
|
32
|
-
|
31
|
+
polyphony (0.52.0)
|
32
|
+
qeweney (0.7.5)
|
33
33
|
escape_utils (~> 1.2.1)
|
34
34
|
rack (2.2.3)
|
35
35
|
rake (12.3.3)
|
data/TODO.md
CHANGED
@@ -1,8 +1,6 @@
|
|
1
|
-
|
1
|
+
For immediate execution:
|
2
2
|
|
3
|
-
Problems to fix:
|
4
3
|
|
5
|
-
- Memory leak (in server? multi agent? multi client?)
|
6
4
|
|
7
5
|
# Roadmap
|
8
6
|
|
@@ -14,7 +12,7 @@ Problems to fix:
|
|
14
12
|
- https://gitlab.com/honeyryderchuck/http-2-next
|
15
13
|
- Open an issue there, ask what's the difference between the two gems?
|
16
14
|
|
17
|
-
## 0.
|
15
|
+
## 0.38
|
18
16
|
|
19
17
|
- Add more poly CLI commands and options:
|
20
18
|
|
@@ -26,7 +24,7 @@ Problems to fix:
|
|
26
24
|
- set port to bind to
|
27
25
|
- set forking process count
|
28
26
|
|
29
|
-
## 0.
|
27
|
+
## 0.39 Working Sinatra application
|
30
28
|
|
31
29
|
- app with database access (postgresql)
|
32
30
|
- benchmarks!
|
data/e
ADDED
File without changes
|
@@ -27,7 +27,8 @@ puts 'Listening on port 4411...'
|
|
27
27
|
|
28
28
|
Tipi.serve('0.0.0.0', 4411, opts) do |req|
|
29
29
|
if req.upgrade_protocol == 'websocket'
|
30
|
-
|
30
|
+
conn = req.upgrade_to_websocket
|
31
|
+
ws_handler(conn)
|
31
32
|
else
|
32
33
|
req.respond(HTML, 'Content-Type' => 'text/html')
|
33
34
|
end
|
data/examples/http_server.rb
CHANGED
@@ -13,9 +13,17 @@ puts 'Listening on port 4411...'
|
|
13
13
|
|
14
14
|
spin do
|
15
15
|
Tipi.serve('0.0.0.0', 4411, opts) do |req|
|
16
|
-
req.
|
17
|
-
|
18
|
-
|
16
|
+
p path: req.path
|
17
|
+
if req.path == '/stream'
|
18
|
+
req.send_headers('Foo' => 'Bar')
|
19
|
+
sleep 1
|
20
|
+
req.send_chunk("foo\n")
|
21
|
+
sleep 1
|
22
|
+
req.send_chunk("bar\n")
|
23
|
+
req.finish
|
24
|
+
else
|
25
|
+
req.respond("Hello world!\n")
|
26
|
+
end
|
19
27
|
end
|
20
28
|
p 'done...'
|
21
29
|
end.await
|
data/examples/https_server.rb
CHANGED
@@ -16,7 +16,17 @@ opts = {
|
|
16
16
|
puts "pid: #{Process.pid}"
|
17
17
|
puts 'Listening on port 1234...'
|
18
18
|
Tipi.serve('0.0.0.0', 1234, opts) do |req|
|
19
|
-
req.
|
19
|
+
p path: req.path
|
20
|
+
if req.path == '/stream'
|
21
|
+
req.send_headers('Foo' => 'Bar')
|
22
|
+
sleep 1
|
23
|
+
req.send_chunk("foo\n")
|
24
|
+
sleep 1
|
25
|
+
req.send_chunk("bar\n")
|
26
|
+
req.finish
|
27
|
+
else
|
28
|
+
req.respond("Hello world!\n")
|
29
|
+
end
|
20
30
|
# req.send_headers
|
21
31
|
# req.send_chunk("Method: #{req.method}\n")
|
22
32
|
# req.send_chunk("Path: #{req.path}\n")
|
@@ -171,7 +171,7 @@ module DigitalFabric
|
|
171
171
|
rescue IOError, Errno::ECONNREFUSED, Errno::EPIPE
|
172
172
|
# ignore
|
173
173
|
rescue Polyphony::Terminate
|
174
|
-
req.respond(nil, { ':status' =>
|
174
|
+
req.respond(nil, { ':status' => Qeweney::Status::SERVICE_UNAVAILABLE }) if Fiber.current.graceful_shutdown?
|
175
175
|
ensure
|
176
176
|
@requests.delete(id)
|
177
177
|
@long_running_requests.delete(id)
|
@@ -180,7 +180,7 @@ module DigitalFabric
|
|
180
180
|
end
|
181
181
|
|
182
182
|
def prepare_http_request(msg)
|
183
|
-
req =
|
183
|
+
req = Qeweney::Request.new(msg['headers'], RequestAdapter.new(self, msg))
|
184
184
|
req.buffer_body_chunk(msg['body']) if msg['body']
|
185
185
|
req.complete! if msg['complete']
|
186
186
|
req
|
@@ -199,7 +199,7 @@ module DigitalFabric
|
|
199
199
|
end
|
200
200
|
|
201
201
|
def recv_ws_request(msg)
|
202
|
-
req =
|
202
|
+
req = Qeweney::Request.new(msg['headers'], RequestAdapter.new(self, msg))
|
203
203
|
id = msg['id']
|
204
204
|
@requests[id] = @long_running_requests[id] = spin do
|
205
205
|
ws_request(req)
|
@@ -214,12 +214,12 @@ module DigitalFabric
|
|
214
214
|
|
215
215
|
# default handler for HTTP request
|
216
216
|
def http_request(req)
|
217
|
-
req.respond(nil, { ':status':
|
217
|
+
req.respond(nil, { ':status': Qeweney::Status::SERVICE_UNAVAILABLE })
|
218
218
|
end
|
219
219
|
|
220
220
|
# default handler for WS request
|
221
221
|
def ws_request(req)
|
222
|
-
req.respond(nil, { ':status':
|
222
|
+
req.respond(nil, { ':status': Qeweney::Status::SERVICE_UNAVAILABLE })
|
223
223
|
end
|
224
224
|
end
|
225
225
|
end
|
@@ -134,7 +134,7 @@ module DigitalFabric
|
|
134
134
|
end
|
135
135
|
end
|
136
136
|
rescue => e
|
137
|
-
req.respond("Error: #{e.inspect}", ':status' =>
|
137
|
+
req.respond("Error: #{e.inspect}", ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)
|
138
138
|
end
|
139
139
|
|
140
140
|
# @return [Boolean] true if response is complete
|
@@ -150,16 +150,21 @@ module DigitalFabric
|
|
150
150
|
headers = message['headers']
|
151
151
|
body = message['body']
|
152
152
|
done = message['complete']
|
153
|
-
|
154
|
-
|
155
|
-
|
153
|
+
if !req.headers_sent? && done
|
154
|
+
req.respond(body, headers|| {})
|
155
|
+
true
|
156
|
+
else
|
157
|
+
req.send_headers(headers) if headers && !req.headers_sent?
|
158
|
+
req.send_chunk(body, done: done) if body or done
|
159
|
+
done
|
160
|
+
end
|
156
161
|
else
|
157
162
|
# invalid message
|
158
163
|
true
|
159
164
|
end
|
160
165
|
end
|
161
166
|
|
162
|
-
HTTP_RESPONSE_UPGRADE_HEADERS = { ':status' =>
|
167
|
+
HTTP_RESPONSE_UPGRADE_HEADERS = { ':status' => Qeweney::Status::SWITCHING_PROTOCOLS }
|
163
168
|
|
164
169
|
def http_custom_upgrade(id, req, message)
|
165
170
|
# send upgrade response
|
@@ -228,15 +233,15 @@ module DigitalFabric
|
|
228
233
|
case response['kind']
|
229
234
|
when Protocol::WS_RESPONSE
|
230
235
|
headers = response['headers'] || {}
|
231
|
-
status = headers[':status'] ||
|
232
|
-
if status !=
|
236
|
+
status = headers[':status'] || Qeweney::Status::SWITCHING_PROTOCOLS
|
237
|
+
if status != Qeweney::Status::SWITCHING_PROTOCOLS
|
233
238
|
req.respond(nil, headers)
|
234
239
|
return
|
235
240
|
end
|
236
241
|
ws = Tipi::Websocket.new(req.adapter.conn, req.headers)
|
237
242
|
run_websocket_connection(id, ws)
|
238
243
|
else
|
239
|
-
req.respond(nil, ':status' =>
|
244
|
+
req.respond(nil, ':status' => Qeweney::Status::SERVICE_UNAVAILABLE)
|
240
245
|
end
|
241
246
|
end
|
242
247
|
end
|
@@ -95,7 +95,7 @@ module DigitalFabric
|
|
95
95
|
return req.respond('pong') if req.query[:q] == 'ping'
|
96
96
|
|
97
97
|
@counters[:errors] += 1
|
98
|
-
return req.respond(nil, ':status' =>
|
98
|
+
return req.respond(nil, ':status' => Qeweney::Status::SERVICE_UNAVAILABLE)
|
99
99
|
end
|
100
100
|
|
101
101
|
agent.http_request(req)
|
@@ -105,7 +105,7 @@ module DigitalFabric
|
|
105
105
|
@counters[:errors] += 1
|
106
106
|
p e
|
107
107
|
puts e.backtrace.join("\n")
|
108
|
-
req.respond(e.inspect, ':status' =>
|
108
|
+
req.respond(e.inspect, ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)
|
109
109
|
ensure
|
110
110
|
@current_request_count -= 1
|
111
111
|
req.adapter.conn.close if @shutdown
|
@@ -115,7 +115,7 @@ module DigitalFabric
|
|
115
115
|
req.headers['x-request-id'] = SecureRandom.uuid
|
116
116
|
conn = req.adapter.conn
|
117
117
|
req.headers['x-forwarded-for'] = conn.peeraddr(false)[2]
|
118
|
-
req.headers['x-forwarded-proto']
|
118
|
+
req.headers['x-forwarded-proto'] ||= conn.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'
|
119
119
|
end
|
120
120
|
|
121
121
|
def upgrade_request(req)
|
@@ -126,7 +126,7 @@ module DigitalFabric
|
|
126
126
|
agent = find_agent(req)
|
127
127
|
unless agent
|
128
128
|
@counters[:errors] += 1
|
129
|
-
return req.respond(nil, ':status' =>
|
129
|
+
return req.respond(nil, ':status' => Qeweney::Status::SERVICE_UNAVAILABLE)
|
130
130
|
end
|
131
131
|
|
132
132
|
agent.http_upgrade(req, protocol)
|
@@ -134,7 +134,9 @@ module DigitalFabric
|
|
134
134
|
end
|
135
135
|
|
136
136
|
def df_upgrade(req)
|
137
|
-
|
137
|
+
if req.headers['df-token'] != @token
|
138
|
+
return req.respond(nil, ':status' => Qeweney::Status::FORBIDDEN)
|
139
|
+
end
|
138
140
|
|
139
141
|
req.adapter.conn << Protocol.df_upgrade_response
|
140
142
|
AgentProxy.new(self, req)
|
data/lib/tipi/http1_adapter.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'http/parser'
|
4
4
|
require_relative './http2_adapter'
|
5
|
-
require '
|
5
|
+
require 'qeweney/request'
|
6
6
|
|
7
7
|
module Tipi
|
8
8
|
# HTTP1 protocol implementation
|
@@ -87,7 +87,10 @@ module Tipi
|
|
87
87
|
headers = normalize_headers(headers)
|
88
88
|
headers[':path'] = @parser.request_url
|
89
89
|
headers[':method'] = @parser.http_method.downcase
|
90
|
-
|
90
|
+
scheme = (proto = headers['x-forwarded-proto']) ?
|
91
|
+
proto.downcase : scheme_from_connection
|
92
|
+
headers[':scheme'] = scheme
|
93
|
+
queue_request(Qeweney::Request.new(headers, self))
|
91
94
|
end
|
92
95
|
|
93
96
|
def normalize_headers(headers)
|
@@ -131,6 +134,14 @@ module Tipi
|
|
131
134
|
# protocols, notably WebSocket, can be specified by passing a hash to the
|
132
135
|
# :upgrade option when starting a server:
|
133
136
|
#
|
137
|
+
# def ws_handler(conn)
|
138
|
+
# conn << 'hi'
|
139
|
+
# msg = conn.recv
|
140
|
+
# conn << "You said #{msg}"
|
141
|
+
# conn << 'bye'
|
142
|
+
# conn.close
|
143
|
+
# end
|
144
|
+
#
|
134
145
|
# opts = {
|
135
146
|
# upgrade: {
|
136
147
|
# websocket: Tipi::Websocket.handler(&method(:ws_handler))
|
@@ -154,7 +165,7 @@ module Tipi
|
|
154
165
|
|
155
166
|
def upgrade_with_handler(handler, headers)
|
156
167
|
@parser = @requests_head = @requests_tail = nil
|
157
|
-
handler.(
|
168
|
+
handler.(self, headers)
|
158
169
|
true
|
159
170
|
end
|
160
171
|
|
@@ -173,6 +184,14 @@ module Tipi
|
|
173
184
|
':authority' => headers['host']
|
174
185
|
)
|
175
186
|
end
|
187
|
+
|
188
|
+
def websocket_connection(req)
|
189
|
+
Tipi::Websocket.new(@conn, req.headers)
|
190
|
+
end
|
191
|
+
|
192
|
+
def scheme_from_connection
|
193
|
+
@conn.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'
|
194
|
+
end
|
176
195
|
|
177
196
|
# response API
|
178
197
|
|
@@ -185,30 +204,29 @@ module Tipi
|
|
185
204
|
# @param headers
|
186
205
|
def respond(body, headers)
|
187
206
|
consume_request if @parsing
|
188
|
-
data = format_headers(headers, body)
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
207
|
+
data = [format_headers(headers, body, false), body]
|
208
|
+
|
209
|
+
# if body
|
210
|
+
# if @parser.http_minor == 0
|
211
|
+
# data << body
|
212
|
+
# else
|
213
|
+
# # data << body.bytesize.to_s(16) << CRLF << body << CRLF_ZERO_CRLF_CRLF
|
214
|
+
# data << "#{body.bytesize.to_s(16)}\r\n#{body}\r\n0\r\n\r\n"
|
215
|
+
# end
|
216
|
+
# end
|
217
|
+
# Polyphony.backend_sendv(@conn, data, 0)
|
218
|
+
@conn.write(*data)
|
197
219
|
end
|
198
220
|
|
199
|
-
DEFAULT_HEADERS_OPTS = {
|
200
|
-
empty_response: false,
|
201
|
-
consume_request: true
|
202
|
-
}.freeze
|
203
|
-
|
204
221
|
# Sends response headers. If empty_response is truthy, the response status
|
205
222
|
# code will default to 204, otherwise to 200.
|
206
223
|
# @param headers [Hash] response headers
|
207
224
|
# @param empty_response [boolean] whether a response body will be sent
|
225
|
+
# @param chunked [boolean] whether to use chunked transfer encoding
|
208
226
|
# @return [void]
|
209
|
-
def send_headers(headers,
|
210
|
-
data = format_headers(headers,
|
211
|
-
@conn.write(data
|
227
|
+
def send_headers(headers, empty_response: false, chunked: true)
|
228
|
+
data = format_headers(headers, !empty_response, @parser.http_minor == 1 && chunked)
|
229
|
+
@conn.write(data)
|
212
230
|
end
|
213
231
|
|
214
232
|
# Sends a response body chunk. If no headers were sent, default headers are
|
@@ -221,7 +239,7 @@ module Tipi
|
|
221
239
|
data = []
|
222
240
|
data << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n" if chunk
|
223
241
|
data << "0\r\n\r\n" if done
|
224
|
-
@conn.write(data.join)
|
242
|
+
@conn.write(data.join) unless data.empty?
|
225
243
|
end
|
226
244
|
|
227
245
|
# Finishes the response to the current request. If no headers were sent,
|
@@ -240,11 +258,13 @@ module Tipi
|
|
240
258
|
# Formats response headers into an array. If empty_response is true(thy),
|
241
259
|
# the response status code will default to 204, otherwise to 200.
|
242
260
|
# @param headers [Hash] response headers
|
243
|
-
# @param
|
261
|
+
# @param body [boolean] whether a response body will be sent
|
262
|
+
# @param chunked [boolean] whether to use chunked transfer encoding
|
244
263
|
# @return [String] formatted response headers
|
245
|
-
def format_headers(headers, body)
|
246
|
-
status = headers[':status']
|
247
|
-
|
264
|
+
def format_headers(headers, body, chunked)
|
265
|
+
status = headers[':status']
|
266
|
+
status ||= (body ? Qeweney::Status::OK : Qeweney::Status::NO_CONTENT)
|
267
|
+
lines = format_status_line(body, status, chunked)
|
248
268
|
headers.each do |k, v|
|
249
269
|
next if k =~ /^:/
|
250
270
|
|
@@ -254,11 +274,11 @@ module Tipi
|
|
254
274
|
lines
|
255
275
|
end
|
256
276
|
|
257
|
-
def format_status_line(body, status)
|
277
|
+
def format_status_line(body, status, chunked)
|
258
278
|
if !body
|
259
279
|
empty_status_line(status)
|
260
280
|
else
|
261
|
-
with_body_status_line(status, body)
|
281
|
+
with_body_status_line(status, body, chunked)
|
262
282
|
end
|
263
283
|
end
|
264
284
|
|
@@ -270,17 +290,17 @@ module Tipi
|
|
270
290
|
end
|
271
291
|
end
|
272
292
|
|
273
|
-
def with_body_status_line(status, body)
|
274
|
-
if
|
275
|
-
+"HTTP/1.0 #{status}\r\nContent-Length: #{body.bytesize}\r\n"
|
276
|
-
else
|
293
|
+
def with_body_status_line(status, body, chunked)
|
294
|
+
if chunked
|
277
295
|
+"HTTP/1.1 #{status}\r\nTransfer-Encoding: chunked\r\n"
|
296
|
+
else
|
297
|
+
+"HTTP/1.1 #{status}\r\nContent-Length: #{body.bytesize}\r\n"
|
278
298
|
end
|
279
299
|
end
|
280
300
|
|
281
301
|
def collect_header_lines(lines, key, value)
|
282
302
|
if value.is_a?(Array)
|
283
|
-
value.inject(lines) { |
|
303
|
+
value.inject(lines) { |_, item| lines << "#{key}: #{item}\r\n" }
|
284
304
|
else
|
285
305
|
lines << "#{key}: #{value}\r\n"
|
286
306
|
end
|
data/lib/tipi/http2_stream.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'http/2'
|
4
|
-
require '
|
4
|
+
require 'qeweney/request'
|
5
5
|
|
6
6
|
module Tipi
|
7
7
|
# Manages an HTTP 2 stream
|
@@ -46,7 +46,7 @@ module Tipi
|
|
46
46
|
end
|
47
47
|
|
48
48
|
def on_headers(headers)
|
49
|
-
@request =
|
49
|
+
@request = Qeweney::Request.new(headers.to_h, self)
|
50
50
|
if @first
|
51
51
|
@request.headers[':first'] = true
|
52
52
|
@first = false
|
@@ -62,7 +62,7 @@ module Tipi
|
|
62
62
|
@request.buffer_body_chunk(data)
|
63
63
|
end
|
64
64
|
end
|
65
|
-
|
65
|
+
|
66
66
|
def on_half_close
|
67
67
|
if @waiting_for_body_chunk
|
68
68
|
@waiting_for_body_chunk = nil
|
@@ -103,7 +103,7 @@ module Tipi
|
|
103
103
|
|
104
104
|
# response API
|
105
105
|
def respond(chunk, headers)
|
106
|
-
headers[':status'] ||=
|
106
|
+
headers[':status'] ||= Qeweney::Status::OK
|
107
107
|
@stream.headers(headers, end_stream: false)
|
108
108
|
@stream.data(chunk, end_stream: true)
|
109
109
|
@headers_sent = true
|
@@ -112,7 +112,7 @@ module Tipi
|
|
112
112
|
def send_headers(headers, empty_response = false)
|
113
113
|
return if @headers_sent
|
114
114
|
|
115
|
-
headers[':status'] ||= (empty_response ?
|
115
|
+
headers[':status'] ||= (empty_response ? Qeweney::Status::NO_CONTENT : Qeweney::Status::OK).to_s
|
116
116
|
@stream.headers(headers, end_stream: false)
|
117
117
|
@headers_sent = true
|
118
118
|
end
|
@@ -131,7 +131,7 @@ module Tipi
|
|
131
131
|
if @headers_sent
|
132
132
|
@stream.close
|
133
133
|
else
|
134
|
-
headers[':status'] ||=
|
134
|
+
headers[':status'] ||= Qeweney::Status::NO_CONTENT
|
135
135
|
@stream.headers(headers, end_stream: true)
|
136
136
|
end
|
137
137
|
end
|
data/lib/tipi/version.rb
CHANGED
data/lib/tipi/websocket.rb
CHANGED
@@ -7,32 +7,17 @@ module Tipi
|
|
7
7
|
# Websocket connection
|
8
8
|
class Websocket
|
9
9
|
def self.handler(&block)
|
10
|
-
proc
|
11
|
-
|
12
|
-
|
13
|
-
|
10
|
+
proc do |adapter, headers|
|
11
|
+
req = Qeweney::Request.new(headers, adapter)
|
12
|
+
websocket = req.upgrade_to_websocket
|
13
|
+
block.(websocket)
|
14
|
+
end
|
15
|
+
end
|
14
16
|
|
15
17
|
def initialize(conn, headers)
|
16
18
|
@conn = conn
|
17
19
|
@headers = headers
|
18
|
-
setup(headers)
|
19
|
-
end
|
20
|
-
|
21
|
-
S_WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
|
22
|
-
UPGRADE_RESPONSE = <<~HTTP.gsub("\n", "\r\n")
|
23
|
-
HTTP/1.1 101 Switching Protocols
|
24
|
-
Upgrade: websocket
|
25
|
-
Connection: Upgrade
|
26
|
-
Sec-WebSocket-Accept: %<accept>s
|
27
|
-
|
28
|
-
HTTP
|
29
|
-
|
30
|
-
def setup(headers)
|
31
|
-
key = headers['sec-websocket-key']
|
32
20
|
@version = headers['sec-websocket-version'].to_i
|
33
|
-
accept = Digest::SHA1.base64digest([key, S_WS_GUID].join)
|
34
|
-
@conn << format(UPGRADE_RESPONSE, accept: accept)
|
35
|
-
|
36
21
|
@reader = ::WebSocket::Frame::Incoming::Server.new(version: @version)
|
37
22
|
end
|
38
23
|
|
@@ -64,8 +49,10 @@ module Tipi
|
|
64
49
|
end
|
65
50
|
end
|
66
51
|
|
52
|
+
OutgoingFrame = ::WebSocket::Frame::Outgoing::Server
|
53
|
+
|
67
54
|
def send(data)
|
68
|
-
frame =
|
55
|
+
frame = OutgoingFrame.new(
|
69
56
|
version: @version, data: data, type: :text
|
70
57
|
)
|
71
58
|
@conn << frame.to_s
|
data/test/test_http_server.rb
CHANGED
@@ -60,8 +60,8 @@ class HTTP1ServerTest < MiniTest::Test
|
|
60
60
|
connection << "GET / HTTP/1.0\r\n\r\n"
|
61
61
|
|
62
62
|
response = connection.readpartial(8192)
|
63
|
-
expected = <<~HTTP.chomp.http_lines
|
64
|
-
HTTP/1.
|
63
|
+
expected = <<~HTTP.chomp.http_lines.chomp
|
64
|
+
HTTP/1.1 200
|
65
65
|
Content-Length: 13
|
66
66
|
|
67
67
|
Hello, world!
|
@@ -78,14 +78,11 @@ class HTTP1ServerTest < MiniTest::Test
|
|
78
78
|
connection << "GET / HTTP/1.1\r\n\r\n"
|
79
79
|
|
80
80
|
response = connection.readpartial(8192)
|
81
|
-
expected = <<~HTTP.http_lines
|
81
|
+
expected = <<~HTTP.http_lines.chomp
|
82
82
|
HTTP/1.1 200
|
83
|
-
|
83
|
+
Content-Length: 13
|
84
84
|
|
85
|
-
d
|
86
85
|
Hello, world!
|
87
|
-
0
|
88
|
-
|
89
86
|
HTTP
|
90
87
|
assert_equal(expected, response)
|
91
88
|
end
|
@@ -98,26 +95,23 @@ class HTTP1ServerTest < MiniTest::Test
|
|
98
95
|
connection << "GET / HTTP/1.0\r\nConnection: keep-alive\r\n\r\n"
|
99
96
|
response = connection.readpartial(8192)
|
100
97
|
assert !connection.eof?
|
101
|
-
assert_equal("HTTP/1.
|
98
|
+
assert_equal("HTTP/1.1 200\r\nContent-Length: 2\r\n\r\nHi", response)
|
102
99
|
|
103
100
|
connection << "GET / HTTP/1.1\r\n\r\n"
|
104
101
|
response = connection.readpartial(8192)
|
105
102
|
assert !connection.eof?
|
106
|
-
expected = <<~HTTP.http_lines
|
103
|
+
expected = <<~HTTP.http_lines.chomp
|
107
104
|
HTTP/1.1 200
|
108
|
-
|
105
|
+
Content-Length: 2
|
109
106
|
|
110
|
-
2
|
111
107
|
Hi
|
112
|
-
0
|
113
|
-
|
114
108
|
HTTP
|
115
109
|
assert_equal(expected, response)
|
116
110
|
|
117
111
|
connection << "GET / HTTP/1.0\r\n\r\n"
|
118
112
|
response = connection.readpartial(8192)
|
119
113
|
assert connection.eof?
|
120
|
-
assert_equal("HTTP/1.
|
114
|
+
assert_equal("HTTP/1.1 200\r\nContent-Length: 2\r\n\r\nHi", response)
|
121
115
|
end
|
122
116
|
|
123
117
|
def test_pipelining_client
|
@@ -130,24 +124,17 @@ class HTTP1ServerTest < MiniTest::Test
|
|
130
124
|
end
|
131
125
|
|
132
126
|
connection << "GET / HTTP/1.1\r\n\r\nGET / HTTP/1.1\r\nFoo: bar\r\n\r\n"
|
133
|
-
|
127
|
+
sleep 0.01
|
134
128
|
response = connection.readpartial(8192)
|
135
129
|
|
136
|
-
expected = <<~HTTP.http_lines
|
130
|
+
expected = <<~HTTP.http_lines.chomp
|
137
131
|
HTTP/1.1 200
|
138
|
-
|
139
|
-
|
140
|
-
d
|
141
|
-
Hello, world!
|
142
|
-
0
|
132
|
+
Content-Length: 13
|
143
133
|
|
144
|
-
HTTP/1.1 200
|
145
|
-
|
134
|
+
Hello, world!HTTP/1.1 200
|
135
|
+
Content-Length: 14
|
146
136
|
|
147
|
-
e
|
148
137
|
Hello, foobar!
|
149
|
-
0
|
150
|
-
|
151
138
|
HTTP
|
152
139
|
assert_equal(expected, response)
|
153
140
|
end
|
@@ -172,22 +159,22 @@ class HTTP1ServerTest < MiniTest::Test
|
|
172
159
|
6
|
173
160
|
foobar
|
174
161
|
HTTP
|
175
|
-
|
162
|
+
sleep 0.01
|
176
163
|
assert request
|
177
164
|
assert_equal %w[foobar], chunks
|
178
165
|
assert !request.complete?
|
179
166
|
|
180
167
|
connection << "6\r\nbazbud\r\n"
|
181
|
-
|
168
|
+
sleep 0.01
|
182
169
|
assert_equal %w[foobar bazbud], chunks
|
183
170
|
assert !request.complete?
|
184
171
|
|
185
172
|
connection << "0\r\n\r\n"
|
186
|
-
|
173
|
+
sleep 0.01
|
187
174
|
assert_equal %w[foobar bazbud], chunks
|
188
175
|
assert request.complete?
|
189
176
|
|
190
|
-
|
177
|
+
sleep 0.01
|
191
178
|
|
192
179
|
response = connection.readpartial(8192)
|
193
180
|
|
@@ -210,7 +197,8 @@ class HTTP1ServerTest < MiniTest::Test
|
|
210
197
|
|
211
198
|
opts = {
|
212
199
|
upgrade: {
|
213
|
-
echo: lambda do |
|
200
|
+
echo: lambda do |adapter, _headers|
|
201
|
+
conn = adapter.conn
|
214
202
|
conn << <<~HTTP.http_lines
|
215
203
|
HTTP/1.1 101 Switching Protocols
|
216
204
|
Upgrade: echo
|
@@ -231,14 +219,11 @@ class HTTP1ServerTest < MiniTest::Test
|
|
231
219
|
connection << "GET / HTTP/1.1\r\n\r\n"
|
232
220
|
response = connection.readpartial(8192)
|
233
221
|
assert !connection.eof?
|
234
|
-
expected = <<~HTTP.http_lines
|
222
|
+
expected = <<~HTTP.http_lines.chomp
|
235
223
|
HTTP/1.1 200
|
236
|
-
|
224
|
+
Content-Length: 2
|
237
225
|
|
238
|
-
2
|
239
226
|
Hi
|
240
|
-
0
|
241
|
-
|
242
227
|
HTTP
|
243
228
|
assert_equal(expected, response)
|
244
229
|
|
@@ -271,7 +256,7 @@ class HTTP1ServerTest < MiniTest::Test
|
|
271
256
|
connection.close
|
272
257
|
assert !done
|
273
258
|
|
274
|
-
|
259
|
+
sleep 0.01
|
275
260
|
assert done
|
276
261
|
end
|
277
262
|
|
data/test/test_request.rb
CHANGED
@@ -60,9 +60,9 @@ class RequestHeadersTest < MiniTest::Test
|
|
60
60
|
|
61
61
|
connection << "GET /titi HTTP/1.1\r\nHost: blah.com\r\nFoo: bar\r\nhi: 1\r\nHi: 2\r\nhi: 3\r\n\r\n"
|
62
62
|
|
63
|
-
|
63
|
+
sleep 0.01
|
64
64
|
|
65
|
-
assert_kind_of
|
65
|
+
assert_kind_of Qeweney::Request, req
|
66
66
|
assert_equal 'blah.com', req.headers['host']
|
67
67
|
assert_equal 'bar', req.headers['foo']
|
68
68
|
assert_equal ['1', '3', '2'], req.headers['hi']
|
@@ -78,7 +78,7 @@ class RequestHeadersTest < MiniTest::Test
|
|
78
78
|
end
|
79
79
|
|
80
80
|
connection << "GET /titi HTTP/1.1\nHost: blah.com\nFoo: bar\nhi: 1\nHi: 2\nhi: 3\n\n"
|
81
|
-
|
81
|
+
sleep 0.01
|
82
82
|
assert_equal 'blah.com', req.host
|
83
83
|
end
|
84
84
|
|
@@ -90,7 +90,7 @@ class RequestHeadersTest < MiniTest::Test
|
|
90
90
|
end
|
91
91
|
|
92
92
|
connection << "GET /titi HTTP/1.1\nConnection: keep-alive\nFoo: bar\nhi: 1\nHi: 2\nhi: 3\n\n"
|
93
|
-
|
93
|
+
sleep 0.01
|
94
94
|
assert_equal 'keep-alive', req.connection
|
95
95
|
end
|
96
96
|
|
@@ -102,7 +102,7 @@ class RequestHeadersTest < MiniTest::Test
|
|
102
102
|
end
|
103
103
|
|
104
104
|
connection << "GET /titi HTTP/1.1\nConnection: upgrade\nUpgrade: foobar\n\n"
|
105
|
-
|
105
|
+
sleep 0.01
|
106
106
|
assert_equal 'foobar', req.upgrade_protocol
|
107
107
|
end
|
108
108
|
end
|
data/tipi.gemspec
CHANGED
@@ -19,8 +19,8 @@ Gem::Specification.new do |s|
|
|
19
19
|
|
20
20
|
s.executables = ['tipi']
|
21
21
|
|
22
|
-
s.add_runtime_dependency 'polyphony', '~>0.
|
23
|
-
s.add_runtime_dependency '
|
22
|
+
s.add_runtime_dependency 'polyphony', '~>0.52.0'
|
23
|
+
s.add_runtime_dependency 'qeweney', '~>0.6'
|
24
24
|
|
25
25
|
s.add_runtime_dependency 'http_parser.rb', '~>0.6.0'
|
26
26
|
s.add_runtime_dependency 'http-2', '~>0.10.0'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tipi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.38'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sharon Rosner
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-03-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: polyphony
|
@@ -16,28 +16,28 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.
|
19
|
+
version: 0.52.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 0.
|
26
|
+
version: 0.52.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: qeweney
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '0.
|
33
|
+
version: '0.6'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '0.
|
40
|
+
version: '0.6'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: http_parser.rb
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -214,6 +214,7 @@ files:
|
|
214
214
|
- df/ws_page.html
|
215
215
|
- docs/README.md
|
216
216
|
- docs/tipi-logo.png
|
217
|
+
- e
|
217
218
|
- examples/cuba.ru
|
218
219
|
- examples/hanami-api.ru
|
219
220
|
- examples/hello.ru
|
@@ -291,7 +292,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
291
292
|
- !ruby/object:Gem::Version
|
292
293
|
version: '0'
|
293
294
|
requirements: []
|
294
|
-
rubygems_version: 3.
|
295
|
+
rubygems_version: 3.0.8
|
295
296
|
signing_key:
|
296
297
|
specification_version: 4
|
297
298
|
summary: Tipi - the All-in-one Web Server for Ruby Apps
|