tp2 0.15 → 0.17
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/.gitignore +1 -0
- data/CHANGELOG.md +12 -0
- data/README.md +7 -1
- data/TODO.md +22 -0
- data/lib/tp2/{http1_connection.rb → connection.rb} +89 -67
- data/lib/tp2/errors.rb +15 -0
- data/lib/tp2/request_extensions.rb +4 -0
- data/lib/tp2/server.rb +49 -24
- data/lib/tp2/version.rb +1 -1
- data/lib/tp2.rb +12 -13
- data/test/bm_connection.rb +83 -0
- data/test/helper.rb +23 -0
- data/test/{test_http1_connection.rb → test_connection.rb} +100 -85
- data/test/test_server.rb +82 -11
- data/tp2-logo.png +0 -0
- metadata +7 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7f064819fba5d42c4376f3895ee0cce07745149e6b9ed06e864acb1ac3515d9e
|
4
|
+
data.tar.gz: 84767b1d2cd87c6006ba7202f48c2bf1cdc9345ddf258c8fcd716bcf2023c264
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 72b97e276e4abb593566d761f44ac7bfa0a9af5d9ff5dcbece407412dd07eac84a9dfbe6d983c832010fb9a365be27053df471d2b9e76a2955b7332a33b06e69
|
7
|
+
data.tar.gz: 6a13102f1f2008b32aa34fec7da9b2cda496e19f5445525f398b6c4f0a863b15bb68bf94b457e56f0b3c6ace41d3caca4722158ea69fc638d6a828f39e047bbd
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
# 0.17 2025-09-14
|
2
|
+
|
3
|
+
- Add support for server extensions: `Date`, `Server` headers
|
4
|
+
- Add `Request#set_response_headers` for injecting response headers
|
5
|
+
- Add support for env[:server_headers]
|
6
|
+
|
7
|
+
# 0.16 2025-09-11
|
8
|
+
|
9
|
+
- Remove support for HTTP/0.9, HTTP/1.0, use chunked transfer encoding
|
10
|
+
exclusively for HTTP responses
|
11
|
+
- Remove call to app.ready
|
12
|
+
|
1
13
|
# 0.15 2025-08-30
|
2
14
|
|
3
15
|
- Call app.ready after setting up, if method available
|
data/README.md
CHANGED
@@ -1,4 +1,10 @@
|
|
1
|
-
|
1
|
+
<p align="center"><img src="tp2-logo.png" /></p>
|
2
|
+
|
3
|
+
# TP2 - a io_uring-based app server for Ruby
|
4
|
+
|
5
|
+
[](http://rubygems.org/gems/tp2)
|
6
|
+
[](https://github.com/noteflakes/tp2/actions/workflows/test.yml)
|
7
|
+
[](https://github.com/digital-fabric/tp2/blob/master/LICENSE)
|
2
8
|
|
3
9
|
TP2 is an experimental HTTP server based on
|
4
10
|
[UringMachine](https://github.com/digital-fabric/uringmachine) and [Qeweney](https://github.com/digital-fabric/qeweney).
|
data/TODO.md
CHANGED
@@ -1,3 +1,25 @@
|
|
1
|
+
## Immediate
|
2
|
+
|
3
|
+
- [v] Remove support for HTTP/0.9, HTTP/1.0
|
4
|
+
- [v] Reply with 505 HTTP version not supported:
|
5
|
+
|
6
|
+
```
|
7
|
+
< GET / HTTP/0.9
|
8
|
+
|
9
|
+
> HTTP/1.1 505
|
10
|
+
> Connection: close
|
11
|
+
```
|
12
|
+
|
13
|
+
- [v] Use chunked transfer encoding exclusively
|
14
|
+
- [ ] Cache rendered headers
|
15
|
+
- [ ] Look at sketch in ~/Desktop/docs/
|
16
|
+
|
17
|
+
- [ ] Add options for:
|
18
|
+
- [ ] Date header
|
19
|
+
- [ ] Server header
|
20
|
+
|
21
|
+
##
|
22
|
+
|
1
23
|
- Add failing tests for request bombs (requests with lots of bytes)
|
2
24
|
- Add test for `ProtocolError` handling
|
3
25
|
- Add limits with tests for:
|
@@ -2,16 +2,23 @@
|
|
2
2
|
|
3
3
|
require 'qeweney'
|
4
4
|
require 'stringio'
|
5
|
+
require 'tp2/errors'
|
5
6
|
|
6
7
|
module TP2
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
8
|
+
# Implements an HTTP/1.1 connection received by the TP2 server. This
|
9
|
+
# implementation rejects incoming HTTP/0.9 or HTTP/1.0 requests. The response
|
10
|
+
# body is sent exclusively using chunked transfer encoding. Request bodies are
|
11
|
+
# accepted using either fixed length (Content-Length header) or chunked
|
12
|
+
# transfer encoding.
|
13
|
+
class Connection
|
14
|
+
attr_reader :fd, :response_headers, :logger
|
15
|
+
|
16
|
+
def initialize(server, machine, fd, env, &app)
|
17
|
+
@server = server
|
11
18
|
@machine = machine
|
12
19
|
@fd = fd
|
13
|
-
@
|
14
|
-
@logger =
|
20
|
+
@env = env
|
21
|
+
@logger = env[:logger]
|
15
22
|
@stream = UM::Stream.new(machine, fd)
|
16
23
|
@app = app
|
17
24
|
|
@@ -37,37 +44,52 @@ module TP2
|
|
37
44
|
@machine.close_async(@fd)
|
38
45
|
end
|
39
46
|
|
40
|
-
#
|
47
|
+
# Processes an incoming request by parsing the headers, creating a request
|
48
|
+
# object and handing it off to the app handler. Returns true if the
|
49
|
+
# connection should be persisted.
|
41
50
|
def serve_request
|
42
51
|
headers = parse_headers
|
43
52
|
return false if !headers
|
44
53
|
|
45
54
|
request = Qeweney::Request.new(headers, self)
|
55
|
+
|
46
56
|
request.start_stamp = monotonic_clock
|
47
57
|
@app.call(request)
|
48
58
|
persist_connection?(headers)
|
49
|
-
rescue ProtocolError => e
|
50
|
-
@logger&.error(
|
51
|
-
message: 'Protocol error, closing connection',
|
52
|
-
error: e
|
53
|
-
)
|
54
|
-
false
|
55
|
-
rescue SystemCallError => e
|
56
|
-
@logger&.error(
|
57
|
-
message: 'I/O error, closing connection',
|
58
|
-
error: e
|
59
|
-
)
|
60
|
-
false
|
61
59
|
rescue StandardError => e
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
60
|
+
handle_error(request, e)
|
61
|
+
false
|
62
|
+
end
|
63
|
+
|
64
|
+
# Handles an error encountered while serving a request by logging the error
|
65
|
+
# and optionally sending an error response with the relevant HTTP status
|
66
|
+
# code. For I/O errors, no response is sent.
|
67
|
+
#
|
68
|
+
# @param request [Qeweney::Request] HTTP request
|
69
|
+
# @param err [Exception] error
|
70
|
+
# @return [void]
|
71
|
+
def handle_error(request, err)
|
72
|
+
case err
|
73
|
+
when SystemCallError
|
74
|
+
log_error(err, 'I/O error')
|
75
|
+
when ProtocolError
|
76
|
+
log_error(err, err.message)
|
77
|
+
respond(request, err.message, ':status' => err.http_status)
|
78
|
+
else
|
79
|
+
log_error(err, 'Internal error')
|
80
|
+
return if !request || @done
|
66
81
|
|
67
|
-
if request && !@done
|
68
82
|
respond(request, 'Internal server error', ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)
|
69
83
|
end
|
70
|
-
|
84
|
+
end
|
85
|
+
|
86
|
+
# Logs the given err and given message.
|
87
|
+
#
|
88
|
+
# @param err [Exception] error
|
89
|
+
# @param message [String] error message
|
90
|
+
# @return [void]
|
91
|
+
def log_error(err, message)
|
92
|
+
@logger&.error(message: "#{message}, closing connection", error: err)
|
71
93
|
end
|
72
94
|
|
73
95
|
def get_body(req)
|
@@ -110,6 +132,16 @@ module TP2
|
|
110
132
|
|
111
133
|
# response API
|
112
134
|
|
135
|
+
# Sets response headers before sending any response. This method is used to
|
136
|
+
# add headers such as Set-Cookie or cache control headers to a response
|
137
|
+
# before actually responding, specifically in middleware hooks.
|
138
|
+
#
|
139
|
+
# @param headers [Hash] response headers
|
140
|
+
# @return [void]
|
141
|
+
def set_response_headers(headers)
|
142
|
+
@response_headers ? @response_headers.merge!(headers) : @response_headers = headers
|
143
|
+
end
|
144
|
+
|
113
145
|
SEND_FLAGS = UM::MSG_NOSIGNAL | UM::MSG_WAITALL
|
114
146
|
|
115
147
|
# Sends response including headers and body. Waits for the request to complete
|
@@ -118,18 +150,19 @@ module TP2
|
|
118
150
|
# @param body [String] response body
|
119
151
|
# @param headers
|
120
152
|
def respond(request, body, headers)
|
121
|
-
|
122
|
-
|
153
|
+
headers = @response_headers.merge(headers) if @response_headers
|
154
|
+
|
155
|
+
formatted_headers = format_headers(headers, body)
|
156
|
+
@response_headers = headers
|
157
|
+
request&.tx_incr(formatted_headers.bytesize + (body ? body.bytesize : 0))
|
123
158
|
if body
|
124
|
-
buf = formatted_headers
|
159
|
+
buf = "#{formatted_headers}#{body.bytesize.to_s(16)}\r\n#{body}\r\n#{EMPTY_CHUNK}"
|
125
160
|
@machine.send(@fd, buf, buf.bytesize, SEND_FLAGS)
|
126
|
-
# handle_write(formatted_headers + body)
|
127
161
|
else
|
128
162
|
@machine.send(@fd, formatted_headers, formatted_headers.bytesize, SEND_FLAGS)
|
129
163
|
end
|
130
|
-
@logger&.info(request: request, response_headers: headers)
|
164
|
+
@logger&.info(request: request, response_headers: headers) if request
|
131
165
|
@done = true
|
132
|
-
@response_headers = headers
|
133
166
|
end
|
134
167
|
|
135
168
|
# Sends response headers. If empty_response is truthy, the response status
|
@@ -137,10 +170,9 @@ module TP2
|
|
137
170
|
# @param request [Qeweney::Request] HTTP request
|
138
171
|
# @param headers [Hash] response headers
|
139
172
|
# @param empty_response [boolean] whether a response body will be sent
|
140
|
-
# @param chunked [boolean] whether to use chunked transfer encoding
|
141
173
|
# @return [void]
|
142
|
-
def send_headers(request, headers, empty_response: false
|
143
|
-
formatted_headers = format_headers(headers, !empty_response
|
174
|
+
def send_headers(request, headers, empty_response: false)
|
175
|
+
formatted_headers = format_headers(headers, !empty_response)
|
144
176
|
request.tx_incr(formatted_headers.bytesize)
|
145
177
|
@machine.send(@fd, formatted_headers, formatted_headers.bytesize, SEND_FLAGS)
|
146
178
|
@response_headers = headers
|
@@ -182,28 +214,28 @@ module TP2
|
|
182
214
|
@done = true
|
183
215
|
end
|
184
216
|
|
185
|
-
def respond_with_static_file(req, path,
|
217
|
+
def respond_with_static_file(req, path, env, cache_headers)
|
186
218
|
fd = @machine.open(path, UM::O_RDONLY)
|
187
|
-
|
188
|
-
if
|
189
|
-
|
219
|
+
env ||= {}
|
220
|
+
if env[:headers]
|
221
|
+
env[:headers].merge!(cache_headers)
|
190
222
|
else
|
191
|
-
|
223
|
+
env[:headers] = cache_headers
|
192
224
|
end
|
193
225
|
|
194
|
-
maxlen =
|
226
|
+
maxlen = env[:max_len] || 65_536
|
195
227
|
buf = String.new(capacity: maxlen)
|
196
228
|
headers_sent = nil
|
197
229
|
loop do
|
198
230
|
res = @machine.read(fd, buf, maxlen, 0)
|
199
231
|
if res < maxlen && !headers_sent
|
200
|
-
return respond(req, buf,
|
232
|
+
return respond(req, buf, env[:headers])
|
201
233
|
elsif res == 0
|
202
234
|
return finish(req)
|
203
235
|
end
|
204
236
|
|
205
237
|
if !headers_sent
|
206
|
-
send_headers(req,
|
238
|
+
send_headers(req, env[:headers])
|
207
239
|
headers_sent = true
|
208
240
|
end
|
209
241
|
done = res < maxlen
|
@@ -211,7 +243,7 @@ module TP2
|
|
211
243
|
return if done
|
212
244
|
end
|
213
245
|
ensure
|
214
|
-
@machine.
|
246
|
+
@machine.close_async(fd) if fd
|
215
247
|
end
|
216
248
|
|
217
249
|
def close
|
@@ -224,20 +256,15 @@ module TP2
|
|
224
256
|
|
225
257
|
private
|
226
258
|
|
227
|
-
RE_REQUEST_LINE =
|
259
|
+
RE_REQUEST_LINE = /^([a-z]+)\s+([^\s]+)\s+http\/([019\.]{1,3})/i
|
228
260
|
RE_HEADER_LINE = /^([a-z0-9-]+):\s+(.+)/i
|
229
261
|
MAX_REQUEST_LINE_LEN = 1 << 14 # 16KB
|
230
262
|
MAX_HEADER_LINE_LEN = 1 << 10 # 1KB
|
231
263
|
MAX_CHUNK_SIZE_LEN = 16
|
232
264
|
|
233
|
-
class ProtocolError < StandardError
|
234
|
-
end
|
235
|
-
|
236
265
|
def persist_connection?(headers)
|
237
266
|
connection = headers['connection']&.downcase
|
238
|
-
return connection != 'close'
|
239
|
-
|
240
|
-
connection && connection != 'close'
|
267
|
+
return connection != 'close'
|
241
268
|
end
|
242
269
|
|
243
270
|
def parse_headers
|
@@ -263,12 +290,15 @@ module TP2
|
|
263
290
|
return nil if !line
|
264
291
|
|
265
292
|
m = line.match(RE_REQUEST_LINE)
|
266
|
-
raise ProtocolError,
|
293
|
+
raise ProtocolError, 'Invalid request line' if !m
|
294
|
+
|
295
|
+
http_version = m[3]
|
296
|
+
raise UnsupportedHTTPVersionError, 'HTTP version not supported' if http_version != '1.1'
|
267
297
|
|
268
298
|
{
|
269
299
|
':method' => m[1].downcase,
|
270
300
|
':path' => m[2],
|
271
|
-
':protocol' =>
|
301
|
+
':protocol' => 'http/1.1'
|
272
302
|
}
|
273
303
|
end
|
274
304
|
|
@@ -305,21 +335,17 @@ module TP2
|
|
305
335
|
buffer ? (buffer << chunk) : chunk
|
306
336
|
end
|
307
337
|
|
308
|
-
def http1_1?(request)
|
309
|
-
request.headers[':protocol'] == 'http/1.1'
|
310
|
-
end
|
311
|
-
|
312
338
|
INTERNAL_HEADER_REGEXP = /^:/
|
313
339
|
|
314
340
|
# Formats response headers into an array. If empty_response is true(thy),
|
315
341
|
# the response status code will default to 204, otherwise to 200.
|
316
342
|
# @param headers [Hash] response headers
|
317
343
|
# @param body [boolean] whether a response body will be sent
|
318
|
-
# @param chunked [boolean] whether to use chunked transfer encoding
|
319
344
|
# @return [String] formatted response headers
|
320
|
-
def format_headers(headers, body
|
345
|
+
def format_headers(headers, body)
|
321
346
|
status = headers[':status'] || (body ? Qeweney::Status::OK : Qeweney::Status::NO_CONTENT)
|
322
|
-
lines = format_status_line(body, status
|
347
|
+
lines = format_status_line(body, status)
|
348
|
+
lines << @env[:server_headers] if @env[:server_headers]
|
323
349
|
headers.each do |k, v|
|
324
350
|
next if k =~ INTERNAL_HEADER_REGEXP
|
325
351
|
|
@@ -329,11 +355,11 @@ module TP2
|
|
329
355
|
lines
|
330
356
|
end
|
331
357
|
|
332
|
-
def format_status_line(body, status
|
358
|
+
def format_status_line(body, status)
|
333
359
|
if !body
|
334
360
|
empty_status_line(status)
|
335
361
|
else
|
336
|
-
with_body_status_line(status, body
|
362
|
+
with_body_status_line(status, body)
|
337
363
|
end
|
338
364
|
end
|
339
365
|
|
@@ -345,12 +371,8 @@ module TP2
|
|
345
371
|
end
|
346
372
|
end
|
347
373
|
|
348
|
-
def with_body_status_line(status, body
|
349
|
-
|
350
|
-
+"HTTP/1.1 #{status}\r\nTransfer-Encoding: chunked\r\n"
|
351
|
-
else
|
352
|
-
+"HTTP/1.1 #{status}\r\nContent-Length: #{body.bytesize}\r\n"
|
353
|
-
end
|
374
|
+
def with_body_status_line(status, body)
|
375
|
+
+"HTTP/1.1 #{status}\r\nTransfer-Encoding: chunked\r\n"
|
354
376
|
end
|
355
377
|
|
356
378
|
def collect_header_lines(lines, key, value)
|
data/lib/tp2/errors.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TP2
|
4
|
+
class ProtocolError < StandardError
|
5
|
+
def http_status
|
6
|
+
Qeweney::Status::BAD_REQUEST
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class UnsupportedHTTPVersionError < ProtocolError
|
11
|
+
def http_status
|
12
|
+
Qeweney::Status::HTTP_VERSION_NOT_SUPPORTED
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/tp2/server.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'tp2/
|
3
|
+
require 'tp2/connection'
|
4
4
|
require 'tp2/request_extensions'
|
5
5
|
require 'tp2/rack_adapter'
|
6
6
|
|
@@ -9,42 +9,42 @@ module TP2
|
|
9
9
|
PENDING_REQUESTS_GRACE_PERIOD = 0.1
|
10
10
|
PENDING_REQUESTS_TIMEOUT_PERIOD = 5
|
11
11
|
|
12
|
-
def self.rack_app(
|
13
|
-
raise 'Missing app location' if !
|
12
|
+
def self.rack_app(env)
|
13
|
+
raise 'Missing app location' if !env[:app_location]
|
14
14
|
|
15
|
-
TP2::RackAdapter.load(
|
15
|
+
TP2::RackAdapter.load(env[:app_location])
|
16
16
|
end
|
17
17
|
|
18
|
-
def self.tp2_app(_machine,
|
19
|
-
if
|
20
|
-
|
21
|
-
require
|
18
|
+
def self.tp2_app(_machine, env)
|
19
|
+
if env[:app_location]
|
20
|
+
env[:logger]&.info(message: 'Loading web app', location: env[:app_location])
|
21
|
+
require env[:app_location]
|
22
22
|
|
23
|
-
|
23
|
+
env.merge!(TP2.config)
|
24
24
|
end
|
25
|
-
|
25
|
+
env[:app]
|
26
26
|
end
|
27
27
|
|
28
|
-
def self.static_app(
|
28
|
+
def self.static_app(env); end
|
29
29
|
|
30
|
-
def initialize(machine,
|
30
|
+
def initialize(machine, env, &app)
|
31
31
|
@machine = machine
|
32
|
-
@
|
33
|
-
@app = app ||
|
32
|
+
@env = env
|
33
|
+
@app = app || app_from_env
|
34
34
|
@server_fds = []
|
35
35
|
@accept_fibers = []
|
36
36
|
end
|
37
37
|
|
38
|
-
def
|
39
|
-
case @
|
38
|
+
def app_from_env
|
39
|
+
case @env[:app_type]
|
40
40
|
when nil, :tp2
|
41
|
-
Server.tp2_app(@machine, @
|
41
|
+
Server.tp2_app(@machine, @env)
|
42
42
|
when :rack
|
43
|
-
Server.rack_app(@
|
43
|
+
Server.rack_app(@env)
|
44
44
|
when :static
|
45
|
-
Server.static_app(@
|
45
|
+
Server.static_app(@env)
|
46
46
|
else
|
47
|
-
raise "Invalid app type #{@
|
47
|
+
raise "Invalid app type #{@env[:app_type].inspect}"
|
48
48
|
end
|
49
49
|
end
|
50
50
|
|
@@ -65,14 +65,15 @@ module TP2
|
|
65
65
|
@accept_fibers << @machine.spin { accept_incoming(fd) }
|
66
66
|
end
|
67
67
|
bind_string = bind_info.map { it.join(':') }.join(', ')
|
68
|
-
@
|
68
|
+
@env[:logger]&.info(message: "Listening on #{bind_string}")
|
69
|
+
setup_server_extensions
|
69
70
|
|
70
71
|
# map fibers
|
71
72
|
@connection_fiber_map = {}
|
72
73
|
end
|
73
74
|
|
74
75
|
def get_bind_entries
|
75
|
-
bind = @
|
76
|
+
bind = @env[:bind]
|
76
77
|
case bind
|
77
78
|
when Array
|
78
79
|
bind.map { bind_info(it) }
|
@@ -98,9 +99,33 @@ module TP2
|
|
98
99
|
fd
|
99
100
|
end
|
100
101
|
|
102
|
+
def setup_server_extensions
|
103
|
+
extensions = @env[:server_extensions]
|
104
|
+
return if !extensions
|
105
|
+
|
106
|
+
server_name = extensions[:name]
|
107
|
+
if extensions[:date]
|
108
|
+
@date_header_fiber = @machine.spin {
|
109
|
+
@machine.periodically(1) { update_server_headers(server_name) }
|
110
|
+
}
|
111
|
+
update_server_headers(server_name)
|
112
|
+
elsif server_name
|
113
|
+
@env[:server_headers] = "Server: #{server_name}\r\n"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def update_server_headers(server_name)
|
118
|
+
@env[:server_date] = Time.now
|
119
|
+
if server_name
|
120
|
+
@env[:server_headers] = "Server: #{server_name}\r\nDate: #{@env[:server_date].httpdate}\r\n"
|
121
|
+
else
|
122
|
+
@env[:server_headers] = "Date: #{Time.now.httpdate}\r\n"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
101
126
|
def accept_incoming(listen_fd)
|
102
127
|
@machine.accept_each(listen_fd) do |fd|
|
103
|
-
conn =
|
128
|
+
conn = Connection.new(self, @machine, fd, @env, &@app)
|
104
129
|
f = @machine.spin(conn) do
|
105
130
|
it.run
|
106
131
|
ensure
|
@@ -121,7 +146,7 @@ module TP2
|
|
121
146
|
end
|
122
147
|
|
123
148
|
def graceful_shutdown
|
124
|
-
@
|
149
|
+
@env[:logger]&.info(message: 'Shutting down gracefully...')
|
125
150
|
|
126
151
|
# stop listening
|
127
152
|
close_all_server_fds
|
data/lib/tp2/version.rb
CHANGED
data/lib/tp2.rb
CHANGED
@@ -24,36 +24,35 @@ module TP2
|
|
24
24
|
)
|
25
25
|
|
26
26
|
class << self
|
27
|
-
def run(
|
27
|
+
def run(env = {}, &app)
|
28
28
|
if @in_run
|
29
|
-
@
|
30
|
-
@
|
29
|
+
@env = env
|
30
|
+
@env[:app] = app if app
|
31
31
|
return
|
32
32
|
end
|
33
33
|
|
34
|
-
|
34
|
+
env ||= @env || {}
|
35
35
|
begin
|
36
36
|
@in_run = true
|
37
|
-
machine =
|
38
|
-
machine.puts(
|
37
|
+
machine = env[:machine] || UM.new
|
38
|
+
machine.puts(env[:banner]) if env[:banner]
|
39
39
|
|
40
|
-
|
40
|
+
env[:logger]&.info(message: "Running TP2 #{TP2::VERSION}, UringMachine #{UM::VERSION}, Ruby #{RUBY_VERSION}")
|
41
41
|
|
42
|
-
server = Server.new(machine,
|
42
|
+
server = Server.new(machine, env, &app)
|
43
43
|
|
44
44
|
setup_signal_handling(machine, Fiber.current)
|
45
|
-
app.ready if app.respond_to?(:ready)
|
46
45
|
server.run
|
47
46
|
ensure
|
48
47
|
@in_run = false
|
49
48
|
end
|
50
49
|
end
|
51
50
|
|
52
|
-
def
|
53
|
-
return @
|
51
|
+
def env(env = nil, &app)
|
52
|
+
return @env if !env && !app
|
54
53
|
|
55
|
-
@
|
56
|
-
@
|
54
|
+
@env = env || {}
|
55
|
+
@env[:app] = app if app
|
57
56
|
end
|
58
57
|
|
59
58
|
private
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/inline'
|
4
|
+
|
5
|
+
gemfile {
|
6
|
+
gem 'tp2', path: '..'
|
7
|
+
gem 'benchmark'
|
8
|
+
gem 'benchmark-ips'
|
9
|
+
gem 'vernier'
|
10
|
+
}
|
11
|
+
|
12
|
+
require 'vernier'
|
13
|
+
require 'securerandom'
|
14
|
+
require 'benchmark/ips'
|
15
|
+
|
16
|
+
class ConnectionTester
|
17
|
+
def make_socket_pair
|
18
|
+
port = SecureRandom.random_number(10000..40000)
|
19
|
+
server_fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
|
20
|
+
@machine.setsockopt(server_fd, UM::SOL_SOCKET, UM::SO_REUSEADDR, true)
|
21
|
+
@machine.bind(server_fd, '127.0.0.1', port)
|
22
|
+
@machine.listen(server_fd, UM::SOMAXCONN)
|
23
|
+
|
24
|
+
client_conn_fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
|
25
|
+
@machine.connect(client_conn_fd, '127.0.0.1', port)
|
26
|
+
|
27
|
+
server_conn_fd = @machine.accept(server_fd)
|
28
|
+
|
29
|
+
@machine.close(server_fd)
|
30
|
+
[client_conn_fd, server_conn_fd]
|
31
|
+
end
|
32
|
+
|
33
|
+
def setup
|
34
|
+
@machine = UM.new
|
35
|
+
@c_fd, @s_fd = make_socket_pair
|
36
|
+
@reqs = []
|
37
|
+
@app = ->(req) { req.respond('foobar', 'Content-Type' => 'text/plain') }
|
38
|
+
@adapter = TP2::Connection.new(nil, @machine, @s_fd, {}, &@app)
|
39
|
+
end
|
40
|
+
|
41
|
+
def teardown
|
42
|
+
@machine.close(@c_fd) rescue nil
|
43
|
+
@machine.close(@s_fd) rescue nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def write_http_request(msg, shutdown_wr = true)
|
47
|
+
@machine.send(@c_fd, msg, msg.bytesize, UM::MSG_WAITALL)
|
48
|
+
@machine.shutdown(@c_fd, UM::SHUT_WR) if shutdown_wr
|
49
|
+
end
|
50
|
+
|
51
|
+
def write_client_side(msg)
|
52
|
+
@machine.send(@c_fd, msg, msg.bytesize, UM::MSG_WAITALL)
|
53
|
+
end
|
54
|
+
|
55
|
+
def read_client_side(len = 65536)
|
56
|
+
buf = +''
|
57
|
+
res = @machine.recv(@c_fd, buf, len, 0)
|
58
|
+
res == 0 ? nil : buf
|
59
|
+
end
|
60
|
+
|
61
|
+
def run_request
|
62
|
+
write_http_request "GET / HTTP/1.1\r\n\r\n", false
|
63
|
+
@adapter.serve_request
|
64
|
+
read_client_side(len = 65536)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
tester = ConnectionTester.new
|
69
|
+
tester.setup
|
70
|
+
|
71
|
+
# puts '*' * 40
|
72
|
+
# p tester.run_request
|
73
|
+
# p tester.run_request
|
74
|
+
# puts '*' * 40
|
75
|
+
|
76
|
+
# Vernier.profile(out: "profile.vernier.json") {
|
77
|
+
# 100000.times { tester.run_request }
|
78
|
+
# }
|
79
|
+
|
80
|
+
Benchmark.ips do |x|
|
81
|
+
x.report('request') { tester.run_request }
|
82
|
+
x.compare!(order: :baseline)
|
83
|
+
end
|
data/test/helper.rb
CHANGED
@@ -36,6 +36,29 @@ module Kernel
|
|
36
36
|
end
|
37
37
|
end
|
38
38
|
|
39
|
+
class Qeweney::Request
|
40
|
+
def response_headers
|
41
|
+
adapter.headers
|
42
|
+
end
|
43
|
+
|
44
|
+
def response_status
|
45
|
+
adapter.status
|
46
|
+
end
|
47
|
+
|
48
|
+
def response_body
|
49
|
+
adapter.body
|
50
|
+
end
|
51
|
+
|
52
|
+
def response_json
|
53
|
+
raise if response_content_type != 'application/json'
|
54
|
+
JSON.parse(response_body, symbolize_names: true)
|
55
|
+
end
|
56
|
+
|
57
|
+
def response_content_type
|
58
|
+
response_headers['Content-Type']
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
39
62
|
module Minitest::Assertions
|
40
63
|
def assert_in_range exp_range, act
|
41
64
|
msg = message(msg) { "Expected #{mu_pp(act)} to be in range #{mu_pp(exp_range)}" }
|
@@ -1,10 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative './helper'
|
4
|
+
require 'securerandom'
|
4
5
|
|
5
|
-
class
|
6
|
+
class ConnectionTest < Minitest::Test
|
6
7
|
def make_socket_pair
|
7
|
-
port = 10000
|
8
|
+
port = SecureRandom.random_number(10000..40000)
|
8
9
|
server_fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
|
9
10
|
@machine.setsockopt(server_fd, UM::SOL_SOCKET, UM::SO_REUSEADDR, true)
|
10
11
|
@machine.bind(server_fd, '127.0.0.1', port)
|
@@ -25,7 +26,8 @@ class HTTP1ConnectionTest < Minitest::Test
|
|
25
26
|
@reqs = []
|
26
27
|
@hook = nil
|
27
28
|
@app = ->(req) { @hook&.call(req); @reqs << req }
|
28
|
-
@
|
29
|
+
@env = {}
|
30
|
+
@adapter = TP2::Connection.new(nil, @machine, @s_fd, @env, &@app)
|
29
31
|
end
|
30
32
|
|
31
33
|
def teardown
|
@@ -48,8 +50,31 @@ class HTTP1ConnectionTest < Minitest::Test
|
|
48
50
|
res == 0 ? nil : buf
|
49
51
|
end
|
50
52
|
|
51
|
-
def
|
53
|
+
def test_http_unsupported_versions
|
54
|
+
write_http_request "GET / HTTP/0.9\r\n\r\n"
|
55
|
+
@adapter.serve_request
|
56
|
+
response = read_client_side
|
57
|
+
assert_equal "HTTP/1.1 505\r\nTransfer-Encoding: chunked\r\n\r\n1a\r\nHTTP version not supported\r\n0\r\n\r\n", response
|
58
|
+
|
59
|
+
setup
|
60
|
+
|
52
61
|
write_http_request "GET / HTTP/1.0\r\n\r\n"
|
62
|
+
@adapter.serve_request
|
63
|
+
response = read_client_side
|
64
|
+
assert_equal "HTTP/1.1 505\r\nTransfer-Encoding: chunked\r\n\r\n1a\r\nHTTP version not supported\r\n0\r\n\r\n", response
|
65
|
+
|
66
|
+
setup
|
67
|
+
|
68
|
+
@hook = ->(req) { req.respond('hi') }
|
69
|
+
write_http_request "GET / HTTP/1.1\r\n\r\n"
|
70
|
+
@adapter.serve_request
|
71
|
+
@machine.close(@s_fd)
|
72
|
+
response = read_client_side
|
73
|
+
assert_equal "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\n2\r\nhi\r\n0\r\n\r\n", response
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_basic_request_parsing
|
77
|
+
write_http_request "GET / HTTP/1.1\r\n\r\n"
|
53
78
|
|
54
79
|
@adapter.serve_request
|
55
80
|
assert_equal 1, @reqs.size
|
@@ -58,7 +83,7 @@ class HTTP1ConnectionTest < Minitest::Test
|
|
58
83
|
assert_equal({
|
59
84
|
':method' => 'get',
|
60
85
|
':path' => '/',
|
61
|
-
':protocol' => 'http/1.
|
86
|
+
':protocol' => 'http/1.1'
|
62
87
|
}, headers)
|
63
88
|
end
|
64
89
|
|
@@ -196,34 +221,6 @@ class HTTP1ConnectionTest < Minitest::Test
|
|
196
221
|
assert_equal '123456789abcdefghijklmnopqrstuv', body
|
197
222
|
end
|
198
223
|
|
199
|
-
def test_body_to_eof
|
200
|
-
skip
|
201
|
-
|
202
|
-
write_http_request <<~HTTP.crlf_lines
|
203
|
-
POST /foo HTTP/1.1
|
204
|
-
Server: foo.com
|
205
|
-
|
206
|
-
barbaz
|
207
|
-
HTTP
|
208
|
-
|
209
|
-
@bodies = []
|
210
|
-
@hook = ->(req) { @bodies << req.read }
|
211
|
-
|
212
|
-
@adapter.run
|
213
|
-
assert_equal 1, @reqs.size
|
214
|
-
|
215
|
-
req0 = @reqs.shift
|
216
|
-
headers = req0.headers
|
217
|
-
assert_equal({
|
218
|
-
':method' => 'post',
|
219
|
-
':path' => '/foo',
|
220
|
-
':protocol' => 'http/1.1',
|
221
|
-
'server' => 'foo.com'
|
222
|
-
}, headers)
|
223
|
-
body = @bodies.shift
|
224
|
-
assert_equal 'barbaz', body
|
225
|
-
end
|
226
|
-
|
227
224
|
def test_each_chunk
|
228
225
|
write_http_request <<~HTTP.crlf_lines
|
229
226
|
POST /foo HTTP/1.1
|
@@ -278,30 +275,12 @@ class HTTP1ConnectionTest < Minitest::Test
|
|
278
275
|
assert_equal ['123456789abcdefghijklmnopqrstuv'], chunks
|
279
276
|
end
|
280
277
|
|
281
|
-
def test_that_server_uses_content_length_in_http_1_0
|
282
|
-
@hook = ->(req) {
|
283
|
-
req.respond('Hello, world!', {})
|
284
|
-
}
|
285
|
-
|
286
|
-
write_http_request "GET / HTTP/1.0\r\n\r\n"
|
287
|
-
@adapter.run
|
288
|
-
response = read_client_side
|
289
|
-
|
290
|
-
expected = <<~HTTP.crlf_lines
|
291
|
-
HTTP/1.1 200
|
292
|
-
Content-Length: 13
|
293
|
-
|
294
|
-
Hello, world!
|
295
|
-
HTTP
|
296
|
-
assert_equal(expected, response)
|
297
|
-
end
|
298
|
-
|
299
278
|
def test_204_status_on_empty_response
|
300
279
|
@hook = ->(req) {
|
301
280
|
req.respond(nil, {})
|
302
281
|
}
|
303
282
|
|
304
|
-
write_http_request "GET / HTTP/1.
|
283
|
+
write_http_request "GET / HTTP/1.1\r\n\r\n"
|
305
284
|
@adapter.run
|
306
285
|
response = read_client_side
|
307
286
|
|
@@ -325,46 +304,29 @@ class HTTP1ConnectionTest < Minitest::Test
|
|
325
304
|
@adapter.run
|
326
305
|
|
327
306
|
response = read_client_side
|
328
|
-
expected =
|
329
|
-
HTTP/1.1 200
|
330
|
-
Content-Length: 13
|
331
|
-
|
332
|
-
Hello, world!
|
333
|
-
HTTP
|
307
|
+
expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\nd\r\nHello, world!\r\n0\r\n\r\n"
|
334
308
|
assert_equal(expected, response)
|
335
309
|
end
|
336
310
|
|
337
|
-
def
|
311
|
+
def test_that_server_maintains_connection_if_no_connection_close_header
|
338
312
|
@hook = ->(req) {
|
339
313
|
req.respond('Hi', {})
|
340
314
|
}
|
341
315
|
|
342
|
-
write_http_request "GET / HTTP/1.
|
316
|
+
write_http_request "GET / HTTP/1.1\r\nConnection: close\r\n\r\n", false
|
343
317
|
res = @adapter.serve_request
|
344
|
-
assert_equal
|
318
|
+
assert_equal false, res
|
345
319
|
|
346
320
|
response = read_client_side
|
347
|
-
assert_equal("HTTP/1.1 200\r\
|
321
|
+
assert_equal("HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\n2\r\nHi\r\n0\r\n\r\n", response)
|
348
322
|
|
349
323
|
write_http_request "GET / HTTP/1.1\r\n\r\n", false
|
350
324
|
res = @adapter.serve_request
|
351
325
|
assert_equal true, res
|
352
326
|
|
353
327
|
response = read_client_side
|
354
|
-
expected =
|
355
|
-
HTTP/1.1 200
|
356
|
-
Content-Length: 2
|
357
|
-
|
358
|
-
Hi
|
359
|
-
HTTP
|
328
|
+
expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\n2\r\nHi\r\n0\r\n\r\n"
|
360
329
|
assert_equal(expected, response)
|
361
|
-
|
362
|
-
write_http_request "GET / HTTP/1.0\r\n\r\n"
|
363
|
-
res = @adapter.serve_request
|
364
|
-
assert_equal false, !!res
|
365
|
-
|
366
|
-
response = read_client_side
|
367
|
-
assert_equal("HTTP/1.1 200\r\nContent-Length: 2\r\n\r\nHi", response)
|
368
330
|
end
|
369
331
|
|
370
332
|
def test_pipelining_client
|
@@ -381,15 +343,8 @@ class HTTP1ConnectionTest < Minitest::Test
|
|
381
343
|
@adapter.run
|
382
344
|
response = read_client_side
|
383
345
|
|
384
|
-
expected =
|
385
|
-
|
386
|
-
Content-Length: 13
|
387
|
-
|
388
|
-
Hello, world!HTTP/1.1 200
|
389
|
-
Content-Length: 14
|
390
|
-
|
391
|
-
Hello, foobar!
|
392
|
-
HTTP
|
346
|
+
expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\nd\r\nHello, world!\r\n0\r\n\r\n" +
|
347
|
+
"HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\ne\r\nHello, foobar!\r\n0\r\n\r\n"
|
393
348
|
assert_equal(expected, response)
|
394
349
|
end
|
395
350
|
|
@@ -538,7 +493,7 @@ class HTTP1ConnectionTest < Minitest::Test
|
|
538
493
|
|
539
494
|
content = IO.read(__FILE__)
|
540
495
|
file_size = content.bytesize
|
541
|
-
expected = "HTTP/1.1 200\r\
|
496
|
+
expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\n6\r\nfoobar\r\n0\r\n\r\n"
|
542
497
|
|
543
498
|
assert_equal expected, response
|
544
499
|
end
|
@@ -567,4 +522,64 @@ class HTTP1ConnectionTest < Minitest::Test
|
|
567
522
|
expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nfoo\r\n3\r\nbar\r\n0\r\n\r\n"
|
568
523
|
assert_equal expected, response
|
569
524
|
end
|
525
|
+
|
526
|
+
def test_connection_server_headers
|
527
|
+
@env[:server_headers] = "Server: TP2\r\n"
|
528
|
+
|
529
|
+
@hook = ->(req) do
|
530
|
+
req.respond('foo')
|
531
|
+
end
|
532
|
+
|
533
|
+
write_client_side("GET / HTTP/1.1\r\n\r\n")
|
534
|
+
@adapter.serve_request
|
535
|
+
response = read_client_side(65536)
|
536
|
+
expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\nServer: TP2\r\n\r\n3\r\nfoo\r\n0\r\n\r\n"
|
537
|
+
assert_equal expected, response
|
538
|
+
|
539
|
+
@env[:server_headers] = "Server: TP3\r\n"
|
540
|
+
|
541
|
+
write_client_side("GET / HTTP/1.1\r\n\r\n")
|
542
|
+
@adapter.serve_request
|
543
|
+
response = read_client_side(65536)
|
544
|
+
expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\nServer: TP3\r\n\r\n3\r\nfoo\r\n0\r\n\r\n"
|
545
|
+
assert_equal expected, response
|
546
|
+
end
|
547
|
+
|
548
|
+
def test_set_response_headers
|
549
|
+
@hook = ->(req) {
|
550
|
+
req.set_response_headers("Set-Cookie" => 'foo=bar')
|
551
|
+
req.respond('foo')
|
552
|
+
}
|
553
|
+
|
554
|
+
write_client_side("GET / HTTP/1.1\r\n\r\n")
|
555
|
+
@adapter.serve_request
|
556
|
+
response = read_client_side(65536)
|
557
|
+
expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\nSet-Cookie: foo=bar\r\n\r\n3\r\nfoo\r\n0\r\n\r\n"
|
558
|
+
assert_equal expected, response
|
559
|
+
|
560
|
+
@hook = ->(req) {
|
561
|
+
req.set_response_headers("Set-Cookie" => 'foo=bar')
|
562
|
+
req.respond('foo', 'Content-Type' => 'text/plain')
|
563
|
+
}
|
564
|
+
|
565
|
+
write_client_side("GET / HTTP/1.1\r\n\r\n")
|
566
|
+
@adapter.serve_request
|
567
|
+
response = read_client_side(65536)
|
568
|
+
expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\nSet-Cookie: foo=bar\r\nContent-Type: text/plain\r\n\r\n3\r\nfoo\r\n0\r\n\r\n"
|
569
|
+
assert_equal expected, response
|
570
|
+
end
|
571
|
+
|
572
|
+
def test_set_response_headers
|
573
|
+
@hook = ->(req) {
|
574
|
+
req.set_response_headers("Set-Cookie" => 'foo=bar')
|
575
|
+
req.set_response_headers("Foo" => 'bar')
|
576
|
+
req.respond('foo', 'Content-Type' => 'text/plain')
|
577
|
+
}
|
578
|
+
|
579
|
+
write_client_side("GET / HTTP/1.1\r\n\r\n")
|
580
|
+
@adapter.serve_request
|
581
|
+
response = read_client_side(65536)
|
582
|
+
expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\nSet-Cookie: foo=bar\r\nFoo: bar\r\nContent-Type: text/plain\r\n\r\n3\r\nfoo\r\n0\r\n\r\n"
|
583
|
+
assert_equal expected, response
|
584
|
+
end
|
570
585
|
end
|
data/test/test_server.rb
CHANGED
@@ -22,11 +22,11 @@ class ServerTest < Minitest::Test
|
|
22
22
|
class STOP < StandardError
|
23
23
|
end
|
24
24
|
|
25
|
-
def setup
|
25
|
+
def setup(opts = {})
|
26
26
|
@machine = UM.new
|
27
27
|
@port = 10000 + rand(30000)
|
28
|
-
@
|
29
|
-
@server = TP2::Server.new(@machine, @
|
28
|
+
@env = { bind: "127.0.0.1:#{@port}" }.merge(opts)
|
29
|
+
@server = TP2::Server.new(@machine, @env) { @app&.call(it) }
|
30
30
|
@f_server = @machine.spin { run_server }
|
31
31
|
|
32
32
|
# let server spin and listen to incoming connections
|
@@ -65,14 +65,25 @@ class ServerTest < Minitest::Test
|
|
65
65
|
res == 0 ? nil : buf
|
66
66
|
end
|
67
67
|
|
68
|
-
def
|
68
|
+
def test_http_1_0_response
|
69
69
|
@app = ->(req) {
|
70
70
|
req.respond('Hello, world!', {})
|
71
71
|
}
|
72
72
|
|
73
73
|
write_http_request "GET / HTTP/1.0\r\n\r\n"
|
74
74
|
response = read_client_side
|
75
|
-
expected = "HTTP/1.1
|
75
|
+
expected = "HTTP/1.1 505\r\nTransfer-Encoding: chunked\r\n\r\n1a\r\nHTTP version not supported\r\n0\r\n\r\n"
|
76
|
+
assert_equal(expected, response)
|
77
|
+
end
|
78
|
+
|
79
|
+
def test_basic_app_response
|
80
|
+
@app = ->(req) {
|
81
|
+
req.respond('Hello, world!', {})
|
82
|
+
}
|
83
|
+
|
84
|
+
write_http_request "GET / HTTP/1.1\r\n\r\n"
|
85
|
+
response = read_client_side
|
86
|
+
expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\nd\r\nHello, world!\r\n0\r\n\r\n"
|
76
87
|
assert_equal(expected, response)
|
77
88
|
end
|
78
89
|
|
@@ -83,8 +94,10 @@ class ServerTest < Minitest::Test
|
|
83
94
|
|
84
95
|
write_http_request "GET /foo HTTP/1.1\r\nServer: foo.com\r\n\r\nSCHmet /bar HTTP/1.1\r\n\r\n"
|
85
96
|
|
97
|
+
@machine.sleep(0.1)
|
86
98
|
response = read_client_side
|
87
|
-
expected = "HTTP/1.1 200\r\
|
99
|
+
expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\nb\r\nmethod: get\r\n0\r\n\r\n" +
|
100
|
+
"HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\ne\r\nmethod: schmet\r\n0\r\n\r\n"
|
88
101
|
assert_equal(expected, response)
|
89
102
|
end
|
90
103
|
|
@@ -104,7 +117,7 @@ class ServerTest < Minitest::Test
|
|
104
117
|
@machine.snooze
|
105
118
|
|
106
119
|
response = read_client_side
|
107
|
-
expected = "HTTP/1.1 200\r\
|
120
|
+
expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\nb\r\nTerminated!\r\n0\r\n\r\n"
|
108
121
|
assert_equal(expected, response)
|
109
122
|
end
|
110
123
|
|
@@ -143,7 +156,7 @@ class ServerTest < Minitest::Test
|
|
143
156
|
':protocol' => 'http/1.1',
|
144
157
|
'server' => 'foo.com',
|
145
158
|
'content-length' => '3',
|
146
|
-
':tx' =>
|
159
|
+
':tx' => 56,
|
147
160
|
}, headers)
|
148
161
|
body = @bodies.shift
|
149
162
|
assert_equal 'abc', body
|
@@ -156,7 +169,7 @@ class ServerTest < Minitest::Test
|
|
156
169
|
':protocol' => 'http/1.1',
|
157
170
|
'server' => 'bar.com',
|
158
171
|
'content-length' => '6',
|
159
|
-
':tx' =>
|
172
|
+
':tx' => 59,
|
160
173
|
}, headers)
|
161
174
|
body = @bodies.shift
|
162
175
|
assert_equal 'defghi', body
|
@@ -266,7 +279,7 @@ class ServerTest < Minitest::Test
|
|
266
279
|
def test_logging
|
267
280
|
skip
|
268
281
|
reqs = []
|
269
|
-
@
|
282
|
+
@env[:logger] = TestLogger.new
|
270
283
|
@app = ->(req) { reqs << req; req.respond('Hello, world!', {}) }
|
271
284
|
|
272
285
|
write_http_request "GET / HTTP/1.0\r\n\r\n"
|
@@ -274,10 +287,68 @@ class ServerTest < Minitest::Test
|
|
274
287
|
expected = "HTTP/1.1 200\r\nContent-Length: 13\r\n\r\nHello, world!"
|
275
288
|
assert_equal(expected, response)
|
276
289
|
|
277
|
-
entries = @
|
290
|
+
entries = @env[:logger].entries
|
278
291
|
assert_equal 1, entries.size
|
279
292
|
assert_equal 1, reqs.size
|
280
293
|
|
281
294
|
assert_equal reqs.first, entries.first[:request]
|
282
295
|
end
|
296
|
+
|
297
|
+
def test_server_headers
|
298
|
+
@env[:server_headers] = "Server: Tipi\r\n"
|
299
|
+
|
300
|
+
@app = ->(req) {
|
301
|
+
req.respond('Hello, world!', {})
|
302
|
+
}
|
303
|
+
|
304
|
+
write_http_request "GET / HTTP/1.1\r\n\r\n"
|
305
|
+
response = read_client_side
|
306
|
+
expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\nServer: Tipi\r\n\r\nd\r\nHello, world!\r\n0\r\n\r\n"
|
307
|
+
assert_equal(expected, response)
|
308
|
+
end
|
309
|
+
|
310
|
+
def test_server_headers_date
|
311
|
+
setup({ server_extensions: { date: true } })
|
312
|
+
@machine.sleep(0.1)
|
313
|
+
assert_kind_of Time, @env[:server_date]
|
314
|
+
|
315
|
+
@app = ->(req) {
|
316
|
+
req.respond('foo', {})
|
317
|
+
}
|
318
|
+
|
319
|
+
write_http_request "GET / HTTP/1.1\r\n\r\n"
|
320
|
+
response = read_client_side
|
321
|
+
expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\nDate: #{@env[:server_date].httpdate}\r\n\r\n3\r\nfoo\r\n0\r\n\r\n"
|
322
|
+
assert_equal(expected, response)
|
323
|
+
end
|
324
|
+
|
325
|
+
def test_server_headers_date_and_server_name
|
326
|
+
setup({ server_extensions: { date: true, name: 'Foo' } })
|
327
|
+
@machine.sleep(0.1)
|
328
|
+
assert_kind_of Time, @env[:server_date]
|
329
|
+
|
330
|
+
@app = ->(req) {
|
331
|
+
req.respond('foo', {})
|
332
|
+
}
|
333
|
+
|
334
|
+
write_http_request "GET / HTTP/1.1\r\n\r\n"
|
335
|
+
response = read_client_side
|
336
|
+
expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\nServer: Foo\r\nDate: #{@env[:server_date].httpdate}\r\n\r\n3\r\nfoo\r\n0\r\n\r\n"
|
337
|
+
assert_equal(expected, response)
|
338
|
+
end
|
339
|
+
|
340
|
+
def test_server_headers_server_name
|
341
|
+
setup({ server_extensions: { name: 'Bar' } })
|
342
|
+
@machine.sleep(0.1)
|
343
|
+
assert_nil @env[:server_date]
|
344
|
+
|
345
|
+
@app = ->(req) {
|
346
|
+
req.respond('foo', {})
|
347
|
+
}
|
348
|
+
|
349
|
+
write_http_request "GET / HTTP/1.1\r\n\r\n"
|
350
|
+
response = read_client_side
|
351
|
+
expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\nServer: Bar\r\n\r\n3\r\nfoo\r\n0\r\n\r\n"
|
352
|
+
assert_equal(expected, response)
|
353
|
+
end
|
283
354
|
end
|
data/tp2-logo.png
ADDED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tp2
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.17'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sharon Rosner
|
@@ -71,16 +71,19 @@ files:
|
|
71
71
|
- examples/rack.ru
|
72
72
|
- examples/simple.rb
|
73
73
|
- lib/tp2.rb
|
74
|
-
- lib/tp2/
|
74
|
+
- lib/tp2/connection.rb
|
75
|
+
- lib/tp2/errors.rb
|
75
76
|
- lib/tp2/logger.rb
|
76
77
|
- lib/tp2/rack_adapter.rb
|
77
78
|
- lib/tp2/request_extensions.rb
|
78
79
|
- lib/tp2/server.rb
|
79
80
|
- lib/tp2/version.rb
|
81
|
+
- test/bm_connection.rb
|
80
82
|
- test/helper.rb
|
81
83
|
- test/run.rb
|
82
|
-
- test/
|
84
|
+
- test/test_connection.rb
|
83
85
|
- test/test_server.rb
|
86
|
+
- tp2-logo.png
|
84
87
|
- tp2.gemspec
|
85
88
|
homepage: https://github.com/noteflakes/tp2
|
86
89
|
licenses:
|
@@ -107,7 +110,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
107
110
|
- !ruby/object:Gem::Version
|
108
111
|
version: '0'
|
109
112
|
requirements: []
|
110
|
-
rubygems_version: 3.
|
113
|
+
rubygems_version: 3.7.0.dev
|
111
114
|
specification_version: 4
|
112
115
|
summary: Experimental HTTP/1 server for UringMachine
|
113
116
|
test_files: []
|