tipi 0.35 → 0.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|