tp2 0.3 → 0.5
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 +8 -0
- data/Gemfile.lock +11 -13
- data/TODO.md +14 -1
- data/examples/simple.rb +12 -4
- data/lib/tp2/{http.rb → http1_adapter.rb} +171 -46
- data/lib/tp2/server.rb +49 -7
- data/lib/tp2/version.rb +1 -1
- data/test/helper.rb +38 -0
- data/test/run.rb +5 -0
- data/test/test_http1_adapter.rb +532 -0
- data/test/test_server.rb +238 -0
- data/tp2.gemspec +1 -2
- metadata +11 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a9c8c52c6ec2e5db6393b54f0ccf96010c9c626984ea943075f1675a078e9eae
|
4
|
+
data.tar.gz: 80cf94801b882350e72d50b21e9e004c544d239e3a05e04e99db4543e88deb73
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 48484025b54dc3a7ea959c795ed6575d62eae1165f97ed5171343616af84da1fce9cc2e818f7e3fb540dab146b6f0bd8e1a076fba6050e03cb78cfd479615449
|
7
|
+
data.tar.gz: a3a65e353b9a5f4c1d4d3af7a4a6977f77ed7c38a1c10eb85c4a4afbbc6cc4a4c8e244be482c66b9e1ce6c61b52ffd0755959e3dff545d87b310011598baec9a
|
data/CHANGELOG.md
ADDED
data/Gemfile.lock
CHANGED
@@ -1,25 +1,23 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
tp2 (0.
|
5
|
-
http_parser.rb (= 0.8.0)
|
4
|
+
tp2 (0.5)
|
6
5
|
qeweney (= 0.21)
|
7
|
-
uringmachine (= 0.
|
6
|
+
uringmachine (= 0.8)
|
8
7
|
|
9
8
|
GEM
|
10
9
|
remote: https://rubygems.org/
|
11
10
|
specs:
|
12
11
|
docile (1.4.1)
|
13
12
|
escape_utils (1.3.0)
|
14
|
-
ffi (1.17.
|
15
|
-
ffi (1.17.
|
16
|
-
ffi (1.17.
|
17
|
-
ffi (1.17.
|
18
|
-
ffi (1.17.
|
19
|
-
ffi (1.17.
|
20
|
-
ffi (1.17.
|
21
|
-
ffi (1.17.
|
22
|
-
http_parser.rb (0.8.0)
|
13
|
+
ffi (1.17.2-aarch64-linux-gnu)
|
14
|
+
ffi (1.17.2-aarch64-linux-musl)
|
15
|
+
ffi (1.17.2-arm-linux-gnu)
|
16
|
+
ffi (1.17.2-arm-linux-musl)
|
17
|
+
ffi (1.17.2-arm64-darwin)
|
18
|
+
ffi (1.17.2-x86_64-darwin)
|
19
|
+
ffi (1.17.2-x86_64-linux-gnu)
|
20
|
+
ffi (1.17.2-x86_64-linux-musl)
|
23
21
|
listen (3.9.0)
|
24
22
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
25
23
|
rb-inotify (~> 0.9, >= 0.9.10)
|
@@ -38,7 +36,7 @@ GEM
|
|
38
36
|
simplecov_json_formatter (~> 0.1)
|
39
37
|
simplecov-html (0.13.1)
|
40
38
|
simplecov_json_formatter (0.1.4)
|
41
|
-
uringmachine (0.
|
39
|
+
uringmachine (0.8)
|
42
40
|
yard (0.9.37)
|
43
41
|
|
44
42
|
PLATFORMS
|
data/TODO.md
CHANGED
@@ -1 +1,14 @@
|
|
1
|
-
-
|
1
|
+
- Add failing tests for request bombs (requests with lots of bytes)
|
2
|
+
- Add limits for:
|
3
|
+
- total request size (pre body)
|
4
|
+
- line size
|
5
|
+
- request method size
|
6
|
+
- request path size
|
7
|
+
- header size
|
8
|
+
- limits expressed inside regexps done as follows:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
MAX_HEADER_KEY_BYTES = 256
|
12
|
+
|
13
|
+
HEADER_RE = /^\s([^\s^\:]{1, #{MAX_HEADER_KEY_BYTES}}).../
|
14
|
+
```
|
data/examples/simple.rb
CHANGED
@@ -14,13 +14,21 @@ app = ->(req) {
|
|
14
14
|
|
15
15
|
machine = UM.new
|
16
16
|
server = TP2::Server.new(machine, '0.0.0.0', 1234, &app)
|
17
|
+
puts "Listening on port 1234..."
|
17
18
|
|
18
|
-
machine.spin { server.run }
|
19
|
+
server_fiber = machine.spin { server.run }
|
19
20
|
|
20
|
-
|
21
|
-
trap('SIGINT') { machine.
|
21
|
+
sig_queue = UM::Queue.new
|
22
|
+
trap('SIGINT') { machine.push(sig_queue, :SIGINT) }
|
22
23
|
|
23
24
|
puts "Running... (pid: #{Process.pid})"
|
24
25
|
STDOUT.flush
|
25
|
-
|
26
|
+
|
27
|
+
# wait for signal
|
28
|
+
sig = machine.shift(sig_queue)
|
29
|
+
|
30
|
+
puts "Got signal (#{sig}), shutting down gracefully..."
|
31
|
+
machine.schedule(server_fiber, UM::Terminate.new)
|
32
|
+
machine.join(server_fiber)
|
33
|
+
|
26
34
|
puts
|
@@ -1,69 +1,81 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'http/parser'
|
4
3
|
require 'qeweney'
|
4
|
+
require 'stringio'
|
5
5
|
|
6
6
|
module TP2
|
7
|
-
|
8
|
-
|
9
|
-
SEND_FLAGS = UM::MSG_NOSIGNAL | UM::MSG_WAITALL
|
7
|
+
class HTTP1Adapter
|
8
|
+
attr_reader :fd
|
10
9
|
|
11
|
-
def initialize(machine, fd,
|
10
|
+
def initialize(machine, fd, &app)
|
12
11
|
@machine = machine
|
13
12
|
@fd = fd
|
14
|
-
@
|
15
|
-
@
|
13
|
+
@buffer = String.new('', capacity: 4096)
|
14
|
+
@sio = StringIO.new(@buffer)
|
16
15
|
@app = app
|
17
16
|
end
|
18
17
|
|
19
|
-
def
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
headers[':scheme'] = get_scheme_from_headers(headers)
|
24
|
-
headers[':protocol'] = "http/#{@parser.http_major}.#{@parser.http_minor}"
|
25
|
-
@request = Qeweney::Request.new(headers, self)
|
26
|
-
end
|
27
|
-
|
28
|
-
def get_scheme_from_headers(headers)
|
29
|
-
if (proto = headers['x-forwarded-proto'])
|
30
|
-
proto.downcase
|
31
|
-
else
|
32
|
-
'http'
|
18
|
+
def run
|
19
|
+
while true
|
20
|
+
persist = serve_request
|
21
|
+
break if !persist
|
33
22
|
end
|
23
|
+
rescue UM::Terminate
|
24
|
+
# do nothing
|
25
|
+
rescue => e
|
26
|
+
puts '!' * 40
|
27
|
+
p e
|
28
|
+
puts e.backtrace.join("\n")
|
29
|
+
exit!
|
30
|
+
ensure
|
31
|
+
@machine.close(@fd)
|
34
32
|
end
|
35
33
|
|
36
|
-
def
|
37
|
-
headers
|
38
|
-
|
39
|
-
hk = h[k]
|
40
|
-
if hk
|
41
|
-
hk = h[k] = [hk] unless hk.is_a?(Array)
|
42
|
-
v.is_a?(Array) ? hk.concat(v) : hk << v
|
43
|
-
else
|
44
|
-
h[k] = v
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
34
|
+
def serve_request
|
35
|
+
headers = parse_headers
|
36
|
+
return false if !headers
|
48
37
|
|
49
|
-
|
50
|
-
@
|
38
|
+
request = Qeweney::Request.new(headers, self)
|
39
|
+
@app.call(request)
|
40
|
+
persist_connection?(headers)
|
51
41
|
end
|
52
42
|
|
53
|
-
def
|
54
|
-
|
55
|
-
|
43
|
+
def get_body(req)
|
44
|
+
headers = req.headers
|
45
|
+
content_length = headers['content-length']
|
46
|
+
return read(content_length.to_i) if content_length
|
47
|
+
|
48
|
+
chunked_encoding = headers['transfer-encoding']&.downcase == 'chunked'
|
49
|
+
return get_body_chunked_encoding(headers) if chunked_encoding
|
50
|
+
|
51
|
+
# if content-length is not specified, we read to EOF, up to max 1MB size
|
52
|
+
read(1 << 20, nil, false)
|
56
53
|
end
|
57
54
|
|
58
|
-
def
|
59
|
-
|
60
|
-
|
61
|
-
|
55
|
+
def get_body_chunk(req, buffered_only = false)
|
56
|
+
headers = req.headers
|
57
|
+
content_length = headers['content-length']
|
58
|
+
if content_length
|
59
|
+
return nil if headers[':body-done-reading']
|
60
|
+
|
61
|
+
chunk = read(content_length.to_i)
|
62
|
+
headers[':body-done-reading'] = true
|
63
|
+
return chunk
|
62
64
|
end
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
65
|
+
|
66
|
+
chunked_encoding = headers['transfer-encoding']&.downcase == 'chunked'
|
67
|
+
return read_chunk(headers, nil) if chunked_encoding
|
68
|
+
|
69
|
+
return nil if headers[':body-done-reading']
|
70
|
+
|
71
|
+
# if content-length is not specified, we read to EOF, up to max 1MB size
|
72
|
+
chunk = read(1 << 20, nil, false)
|
73
|
+
headers[':body-done-reading'] = true
|
74
|
+
chunk
|
75
|
+
end
|
76
|
+
|
77
|
+
def complete?(req)
|
78
|
+
req.headers[':body-done-reading']
|
67
79
|
end
|
68
80
|
|
69
81
|
# response API
|
@@ -72,6 +84,8 @@ module TP2
|
|
72
84
|
ZERO_CRLF_CRLF = "0\r\n\r\n"
|
73
85
|
CRLF_ZERO_CRLF_CRLF = "\r\n0\r\n\r\n"
|
74
86
|
|
87
|
+
SEND_FLAGS = UM::MSG_NOSIGNAL | UM::MSG_WAITALL
|
88
|
+
|
75
89
|
# Sends response including headers and body. Waits for the request to complete
|
76
90
|
# if not yet completed. The body is sent using chunked transfer encoding.
|
77
91
|
# @param request [Qeweney::Request] HTTP request
|
@@ -128,6 +142,117 @@ module TP2
|
|
128
142
|
@machine.send(@fd, ZERO_CRLF_CRLF, ZERO_CRLF_CRLF.bytesize, SEND_FLAGS)
|
129
143
|
end
|
130
144
|
|
145
|
+
private
|
146
|
+
|
147
|
+
RE_REQUEST_LINE = /^([a-z]+)\s+([^\s]+)\s+(http\/[0-9\.]{1,3})/i
|
148
|
+
RE_HEADER_LINE = /^([a-z0-9\-]+)\:\s+(.+)/i
|
149
|
+
|
150
|
+
class ProtocolError < StandardError
|
151
|
+
end
|
152
|
+
|
153
|
+
def persist_connection?(headers)
|
154
|
+
connection = headers['connection']&.downcase
|
155
|
+
if headers[':protocol'] == 'http/1.1'
|
156
|
+
return connection != 'close'
|
157
|
+
else
|
158
|
+
return connection && connection != 'close'
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def parse_headers
|
163
|
+
headers = get_request_line
|
164
|
+
return nil if !headers
|
165
|
+
|
166
|
+
while true
|
167
|
+
line = get_line
|
168
|
+
break if line.nil? || line.empty?
|
169
|
+
|
170
|
+
m = line.match(RE_HEADER_LINE)
|
171
|
+
raise ProtocolError, 'Invalid header' if !m
|
172
|
+
|
173
|
+
headers[m[1].downcase] = m[2]
|
174
|
+
end
|
175
|
+
|
176
|
+
headers
|
177
|
+
end
|
178
|
+
|
179
|
+
def get_line
|
180
|
+
while true
|
181
|
+
line = @sio.gets(chomp: true)
|
182
|
+
return line if line
|
183
|
+
|
184
|
+
res = @machine.read(@fd, @buffer, 65536, -1)
|
185
|
+
return nil if res == 0
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def get_request_line
|
190
|
+
line = get_line
|
191
|
+
return nil if !line
|
192
|
+
|
193
|
+
m = line.match(RE_REQUEST_LINE)
|
194
|
+
raise ProtocolError, 'Invalid request line' if !m
|
195
|
+
|
196
|
+
{
|
197
|
+
':method' => m[1].downcase,
|
198
|
+
':path' => m[2],
|
199
|
+
':protocol' => m[3].downcase
|
200
|
+
}
|
201
|
+
end
|
202
|
+
|
203
|
+
def get_body_chunked_encoding(headers)
|
204
|
+
buf = String.new(capacity: 65536)
|
205
|
+
while read_chunk(headers, buf)
|
206
|
+
end
|
207
|
+
|
208
|
+
buf
|
209
|
+
end
|
210
|
+
|
211
|
+
def read(len, buf = nil, raise_on_eof = true)
|
212
|
+
from_sio = @sio.read(len)
|
213
|
+
if from_sio
|
214
|
+
left = len - from_sio&.bytesize
|
215
|
+
if buf
|
216
|
+
buf << from_sio
|
217
|
+
else
|
218
|
+
buf = +from_sio
|
219
|
+
end
|
220
|
+
else
|
221
|
+
left = len
|
222
|
+
buf ||= +''
|
223
|
+
end
|
224
|
+
|
225
|
+
while left > 0
|
226
|
+
res = @machine.read(@fd, buf, left, -1)
|
227
|
+
if res == 0
|
228
|
+
raise ProtocolError, "Incomplete body" if raise_on_eof
|
229
|
+
|
230
|
+
return buf
|
231
|
+
end
|
232
|
+
|
233
|
+
|
234
|
+
left -= res
|
235
|
+
end
|
236
|
+
buf
|
237
|
+
end
|
238
|
+
|
239
|
+
def read_chunk(headers, buf)
|
240
|
+
chunk_size = get_line
|
241
|
+
return nil if !chunk_size
|
242
|
+
|
243
|
+
chunk_size = chunk_size.to_i(16)
|
244
|
+
if chunk_size == 0
|
245
|
+
headers[':body-done-reading'] = true
|
246
|
+
get_line
|
247
|
+
return nil
|
248
|
+
end
|
249
|
+
|
250
|
+
chunk = read(chunk_size, buf)
|
251
|
+
get_line
|
252
|
+
|
253
|
+
chunk
|
254
|
+
end
|
255
|
+
|
131
256
|
def http1_1?(request)
|
132
257
|
request.headers[':protocol'] == 'http/1.1'
|
133
258
|
end
|
data/lib/tp2/server.rb
CHANGED
@@ -1,10 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'tp2/
|
3
|
+
require 'tp2/http1_adapter'
|
4
4
|
require 'tp2/request_extensions'
|
5
5
|
|
6
6
|
module TP2
|
7
7
|
class Server
|
8
|
+
PENDING_REQUESTS_GRACE_PERIOD = 0.1
|
9
|
+
PENDING_REQUESTS_TIMEOUT_PERIOD = 5
|
10
|
+
|
8
11
|
def initialize(machine, hostname, port, &app)
|
9
12
|
@machine = machine
|
10
13
|
@hostname = hostname
|
@@ -12,22 +15,61 @@ module TP2
|
|
12
15
|
@app = app
|
13
16
|
end
|
14
17
|
|
18
|
+
def run
|
19
|
+
setup
|
20
|
+
accept_incoming
|
21
|
+
rescue UM::Terminate
|
22
|
+
graceful_shutdown
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
15
27
|
def setup
|
16
28
|
@server_fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
|
17
29
|
@machine.setsockopt(@server_fd, UM::SOL_SOCKET, UM::SO_REUSEADDR, true)
|
18
30
|
@machine.bind(@server_fd, @hostname, @port)
|
19
31
|
@machine.listen(@server_fd, UM::SOMAXCONN)
|
20
32
|
|
21
|
-
|
22
|
-
|
33
|
+
# map fibers
|
34
|
+
@fiber_map = {}
|
35
|
+
|
36
|
+
# puts "Listening on #{@hostname}:#{@port}"
|
23
37
|
end
|
24
38
|
|
25
|
-
def
|
26
|
-
setup
|
39
|
+
def accept_incoming
|
27
40
|
@machine.accept_each(@server_fd) do |fd|
|
28
|
-
conn =
|
29
|
-
@machine.spin(conn)
|
41
|
+
conn = HTTP1Adapter.new(@machine, fd, &@app)
|
42
|
+
f = @machine.spin(conn) do
|
43
|
+
it.run
|
44
|
+
ensure
|
45
|
+
@fiber_map.delete(f)
|
46
|
+
end
|
47
|
+
@fiber_map[f] = true
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def graceful_shutdown
|
52
|
+
# stop listening
|
53
|
+
@machine.close(@server_fd)
|
54
|
+
|
55
|
+
return if @fiber_map.empty?
|
56
|
+
|
57
|
+
# sleep for a bit, let requests finish
|
58
|
+
@machine.sleep(PENDING_REQUESTS_GRACE_PERIOD)
|
59
|
+
return if @fiber_map.empty?
|
60
|
+
|
61
|
+
# terminate pending fibers
|
62
|
+
pending = @fiber_map.keys
|
63
|
+
signal = UM::Terminate.new
|
64
|
+
pending.each { @machine.schedule(it, signal) }
|
65
|
+
|
66
|
+
@machine.timeout(PENDING_REQUESTS_TIMEOUT_PERIOD, UM::Terminate) do
|
67
|
+
@machine.join(*@fiber_map.keys)
|
68
|
+
rescue UM::Terminate
|
69
|
+
# timeout on waiting for adapters to finish running, do nothing
|
30
70
|
end
|
71
|
+
ensure
|
72
|
+
@machine.close(@server_fd)
|
31
73
|
end
|
32
74
|
end
|
33
75
|
end
|
data/lib/tp2/version.rb
CHANGED
data/test/helper.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'minitest/autorun'
|
5
|
+
require 'tp2'
|
6
|
+
|
7
|
+
require_relative './coverage' if ENV['COVERAGE']
|
8
|
+
|
9
|
+
module Kernel
|
10
|
+
def capture_exception
|
11
|
+
yield
|
12
|
+
rescue Exception => e
|
13
|
+
e
|
14
|
+
end
|
15
|
+
|
16
|
+
def trace(*args)
|
17
|
+
STDOUT.orig_write(format_trace(args))
|
18
|
+
end
|
19
|
+
|
20
|
+
def format_trace(args)
|
21
|
+
if args.first.is_a?(String)
|
22
|
+
if args.size > 1
|
23
|
+
format("%s: %p\n", args.shift, args)
|
24
|
+
else
|
25
|
+
format("%s\n", args.first)
|
26
|
+
end
|
27
|
+
else
|
28
|
+
format("%p\n", args.size == 1 ? args.first : args)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
module Minitest::Assertions
|
34
|
+
def assert_in_range exp_range, act
|
35
|
+
msg = message(msg) { "Expected #{mu_pp(act)} to be in range #{mu_pp(exp_range)}" }
|
36
|
+
assert exp_range.include?(act), msg
|
37
|
+
end
|
38
|
+
end
|
data/test/run.rb
ADDED
@@ -0,0 +1,532 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './helper'
|
4
|
+
|
5
|
+
class String
|
6
|
+
def crlf_lines
|
7
|
+
chomp.gsub("\n", "\r\n").chomp
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class HTTP1AdapterTest < Minitest::Test
|
12
|
+
def make_socket_pair
|
13
|
+
port = 10000 + rand(30000)
|
14
|
+
server_fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
|
15
|
+
@machine.setsockopt(server_fd, UM::SOL_SOCKET, UM::SO_REUSEADDR, true)
|
16
|
+
@machine.bind(server_fd, '127.0.0.1', port)
|
17
|
+
@machine.listen(server_fd, UM::SOMAXCONN)
|
18
|
+
|
19
|
+
client_conn_fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
|
20
|
+
@machine.connect(client_conn_fd, '127.0.0.1', port)
|
21
|
+
|
22
|
+
server_conn_fd = @machine.accept(server_fd)
|
23
|
+
|
24
|
+
@machine.close(server_fd)
|
25
|
+
[client_conn_fd, server_conn_fd]
|
26
|
+
end
|
27
|
+
|
28
|
+
def setup
|
29
|
+
@machine = UM.new
|
30
|
+
@c_fd, @s_fd = make_socket_pair
|
31
|
+
@reqs = []
|
32
|
+
@hook = nil
|
33
|
+
@app = ->(req) { @hook&.call(req); @reqs << req }
|
34
|
+
@adapter = TP2::HTTP1Adapter.new(@machine, @s_fd, &@app)
|
35
|
+
end
|
36
|
+
|
37
|
+
def teardown
|
38
|
+
@machine.close(@c_fd) rescue nil
|
39
|
+
@machine.close(@s_fd) rescue nil
|
40
|
+
end
|
41
|
+
|
42
|
+
def write_http_request(msg, shutdown_wr = true)
|
43
|
+
@machine.send(@c_fd, msg, msg.bytesize, UM::MSG_WAITALL)
|
44
|
+
@machine.shutdown(@c_fd, UM::SHUT_WR) if shutdown_wr
|
45
|
+
end
|
46
|
+
|
47
|
+
def write_client_side(msg)
|
48
|
+
@machine.send(@c_fd, msg, msg.bytesize, UM::MSG_WAITALL)
|
49
|
+
end
|
50
|
+
|
51
|
+
def read_client_side(len = 65536)
|
52
|
+
buf = +''
|
53
|
+
res = @machine.recv(@c_fd, buf, len, 0)
|
54
|
+
res == 0 ? nil : buf
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_basic_request_parsing
|
58
|
+
write_http_request "GET / HTTP/1.0\r\n\r\n"
|
59
|
+
|
60
|
+
@adapter.serve_request
|
61
|
+
assert_equal 1, @reqs.size
|
62
|
+
req = @reqs.shift
|
63
|
+
headers = req.headers
|
64
|
+
assert_equal({
|
65
|
+
':method' => 'get',
|
66
|
+
':path' => '/',
|
67
|
+
':protocol' => 'http/1.0'
|
68
|
+
}, headers)
|
69
|
+
end
|
70
|
+
|
71
|
+
def test_pipelined_requests
|
72
|
+
write_http_request <<~HTTP.crlf_lines
|
73
|
+
GET /foo HTTP/1.1
|
74
|
+
Server: foo.com
|
75
|
+
|
76
|
+
SCHMET /bar HTTP/1.1
|
77
|
+
|
78
|
+
HTTP
|
79
|
+
|
80
|
+
@adapter.run
|
81
|
+
assert_equal 2, @reqs.size
|
82
|
+
req0 = @reqs.shift
|
83
|
+
headers = req0.headers
|
84
|
+
assert_equal({
|
85
|
+
':method' => 'get',
|
86
|
+
':path' => '/foo',
|
87
|
+
':protocol' => 'http/1.1',
|
88
|
+
'server' => 'foo.com'
|
89
|
+
}, headers)
|
90
|
+
|
91
|
+
req1 = @reqs.shift
|
92
|
+
headers = req1.headers
|
93
|
+
assert_equal({
|
94
|
+
':method' => 'schmet',
|
95
|
+
':path' => '/bar',
|
96
|
+
':protocol' => 'http/1.1'
|
97
|
+
}, headers)
|
98
|
+
end
|
99
|
+
|
100
|
+
def test_pipelined_requests_with_body
|
101
|
+
write_http_request <<~HTTP.crlf_lines
|
102
|
+
POST /foo HTTP/1.1
|
103
|
+
Server: foo.com
|
104
|
+
Content-Length: 3
|
105
|
+
|
106
|
+
abcSCHMOST /bar HTTP/1.1
|
107
|
+
Server: bar.com
|
108
|
+
Content-Length: 6
|
109
|
+
|
110
|
+
defghi
|
111
|
+
HTTP
|
112
|
+
|
113
|
+
@bodies = []
|
114
|
+
@hook = ->(req) { @bodies << req.read }
|
115
|
+
|
116
|
+
@adapter.run
|
117
|
+
assert_equal 2, @reqs.size
|
118
|
+
|
119
|
+
req0 = @reqs.shift
|
120
|
+
headers = req0.headers
|
121
|
+
assert_equal({
|
122
|
+
':method' => 'post',
|
123
|
+
':path' => '/foo',
|
124
|
+
':protocol' => 'http/1.1',
|
125
|
+
'server' => 'foo.com',
|
126
|
+
'content-length' => '3'
|
127
|
+
}, headers)
|
128
|
+
body = @bodies.shift
|
129
|
+
assert_equal 'abc', body
|
130
|
+
|
131
|
+
req1 = @reqs.shift
|
132
|
+
headers = req1.headers
|
133
|
+
assert_equal({
|
134
|
+
':method' => 'schmost',
|
135
|
+
':path' => '/bar',
|
136
|
+
':protocol' => 'http/1.1',
|
137
|
+
'server' => 'bar.com',
|
138
|
+
'content-length' => '6'
|
139
|
+
}, headers)
|
140
|
+
body = @bodies.shift
|
141
|
+
assert_equal 'defghi', body
|
142
|
+
end
|
143
|
+
|
144
|
+
def test_pipelined_requests_with_body_chunked
|
145
|
+
write_http_request <<~HTTP.crlf_lines
|
146
|
+
POST /foo HTTP/1.1
|
147
|
+
Server: foo.com
|
148
|
+
Transfer-Encoding: chunked
|
149
|
+
|
150
|
+
3
|
151
|
+
abc
|
152
|
+
2
|
153
|
+
de
|
154
|
+
0
|
155
|
+
|
156
|
+
SCHMOST /bar HTTP/1.1
|
157
|
+
Server: bar.com
|
158
|
+
Transfer-Encoding: chunked
|
159
|
+
|
160
|
+
1f
|
161
|
+
123456789abcdefghijklmnopqrstuv
|
162
|
+
0
|
163
|
+
|
164
|
+
HTTP
|
165
|
+
|
166
|
+
@bodies = []
|
167
|
+
@hook = ->(req) { @bodies << req.read }
|
168
|
+
|
169
|
+
@adapter.run
|
170
|
+
assert_equal 2, @reqs.size
|
171
|
+
|
172
|
+
req0 = @reqs.shift
|
173
|
+
headers = req0.headers
|
174
|
+
assert_equal({
|
175
|
+
':method' => 'post',
|
176
|
+
':path' => '/foo',
|
177
|
+
':protocol' => 'http/1.1',
|
178
|
+
'server' => 'foo.com',
|
179
|
+
'transfer-encoding' => 'chunked',
|
180
|
+
':body-done-reading' => true
|
181
|
+
}, headers)
|
182
|
+
body = @bodies.shift
|
183
|
+
assert_equal 'abcde', body
|
184
|
+
|
185
|
+
req1 = @reqs.shift
|
186
|
+
headers = req1.headers
|
187
|
+
assert_equal({
|
188
|
+
':method' => 'schmost',
|
189
|
+
':path' => '/bar',
|
190
|
+
':protocol' => 'http/1.1',
|
191
|
+
'server' => 'bar.com',
|
192
|
+
'transfer-encoding' => 'chunked',
|
193
|
+
':body-done-reading' => true
|
194
|
+
}, headers)
|
195
|
+
body = @bodies.shift
|
196
|
+
assert_equal '123456789abcdefghijklmnopqrstuv', body
|
197
|
+
end
|
198
|
+
|
199
|
+
def test_body_to_eof
|
200
|
+
write_http_request <<~HTTP.crlf_lines
|
201
|
+
POST /foo HTTP/1.1
|
202
|
+
Server: foo.com
|
203
|
+
|
204
|
+
barbaz
|
205
|
+
HTTP
|
206
|
+
|
207
|
+
@bodies = []
|
208
|
+
@hook = ->(req) { @bodies << req.read }
|
209
|
+
|
210
|
+
@adapter.run
|
211
|
+
assert_equal 1, @reqs.size
|
212
|
+
|
213
|
+
req0 = @reqs.shift
|
214
|
+
headers = req0.headers
|
215
|
+
assert_equal({
|
216
|
+
':method' => 'post',
|
217
|
+
':path' => '/foo',
|
218
|
+
':protocol' => 'http/1.1',
|
219
|
+
'server' => 'foo.com'
|
220
|
+
}, headers)
|
221
|
+
body = @bodies.shift
|
222
|
+
assert_equal 'barbaz', body
|
223
|
+
end
|
224
|
+
|
225
|
+
def test_each_chunk
|
226
|
+
write_http_request <<~HTTP.crlf_lines
|
227
|
+
POST /foo HTTP/1.1
|
228
|
+
Server: foo.com
|
229
|
+
Transfer-Encoding: chunked
|
230
|
+
|
231
|
+
3
|
232
|
+
abc
|
233
|
+
2
|
234
|
+
de
|
235
|
+
0
|
236
|
+
|
237
|
+
SCHMOST /bar HTTP/1.1
|
238
|
+
Server: bar.com
|
239
|
+
Content-Length: 31
|
240
|
+
|
241
|
+
123456789abcdefghijklmnopqrstuv
|
242
|
+
HTTP
|
243
|
+
|
244
|
+
chunks = []
|
245
|
+
@hook = ->(req) { req.each_chunk { chunks << it } }
|
246
|
+
|
247
|
+
@adapter.serve_request
|
248
|
+
assert_equal 1, @reqs.size
|
249
|
+
|
250
|
+
req0 = @reqs.shift
|
251
|
+
headers = req0.headers
|
252
|
+
assert_equal({
|
253
|
+
':method' => 'post',
|
254
|
+
':path' => '/foo',
|
255
|
+
':protocol' => 'http/1.1',
|
256
|
+
'server' => 'foo.com',
|
257
|
+
'transfer-encoding' => 'chunked',
|
258
|
+
':body-done-reading' => true
|
259
|
+
}, headers)
|
260
|
+
assert_equal ['abc', 'de'], chunks
|
261
|
+
|
262
|
+
chunks.clear
|
263
|
+
@adapter.serve_request
|
264
|
+
assert_equal 1, @reqs.size
|
265
|
+
|
266
|
+
req1 = @reqs.shift
|
267
|
+
headers = req1.headers
|
268
|
+
assert_equal({
|
269
|
+
':method' => 'schmost',
|
270
|
+
':path' => '/bar',
|
271
|
+
':protocol' => 'http/1.1',
|
272
|
+
'server' => 'bar.com',
|
273
|
+
'content-length' => '31',
|
274
|
+
':body-done-reading' => true
|
275
|
+
}, headers)
|
276
|
+
assert_equal ['123456789abcdefghijklmnopqrstuv'], chunks
|
277
|
+
end
|
278
|
+
|
279
|
+
def test_that_server_uses_content_length_in_http_1_0
|
280
|
+
@hook = ->(req) {
|
281
|
+
req.respond('Hello, world!', {})
|
282
|
+
}
|
283
|
+
|
284
|
+
write_http_request "GET / HTTP/1.0\r\n\r\n"
|
285
|
+
@adapter.run
|
286
|
+
response = read_client_side
|
287
|
+
|
288
|
+
expected = <<~HTTP.crlf_lines
|
289
|
+
HTTP/1.1 200
|
290
|
+
Content-Length: 13
|
291
|
+
|
292
|
+
Hello, world!
|
293
|
+
HTTP
|
294
|
+
assert_equal(expected, response)
|
295
|
+
end
|
296
|
+
|
297
|
+
def test_204_status_on_empty_response
|
298
|
+
@hook = ->(req) {
|
299
|
+
req.respond(nil, {})
|
300
|
+
}
|
301
|
+
|
302
|
+
write_http_request "GET / HTTP/1.0\r\n\r\n"
|
303
|
+
@adapter.run
|
304
|
+
response = read_client_side
|
305
|
+
|
306
|
+
expected = <<~HTTP.crlf_lines
|
307
|
+
HTTP/1.1 204
|
308
|
+
|
309
|
+
|
310
|
+
|
311
|
+
HTTP
|
312
|
+
assert_equal(expected, response)
|
313
|
+
|
314
|
+
end
|
315
|
+
|
316
|
+
def test_that_server_uses_chunked_encoding_in_http_1_1
|
317
|
+
@hook = ->(req) {
|
318
|
+
req.respond('Hello, world!')
|
319
|
+
}
|
320
|
+
|
321
|
+
# using HTTP 1.0, server should close connection after responding
|
322
|
+
write_http_request "GET / HTTP/1.1\r\n\r\n"
|
323
|
+
@adapter.run
|
324
|
+
|
325
|
+
response = read_client_side
|
326
|
+
expected = <<~HTTP.crlf_lines.chomp
|
327
|
+
HTTP/1.1 200
|
328
|
+
Content-Length: 13
|
329
|
+
|
330
|
+
Hello, world!
|
331
|
+
HTTP
|
332
|
+
assert_equal(expected, response)
|
333
|
+
end
|
334
|
+
|
335
|
+
def test_that_server_maintains_connection_when_using_keep_alives
|
336
|
+
@hook = ->(req) {
|
337
|
+
req.respond('Hi', {})
|
338
|
+
}
|
339
|
+
|
340
|
+
write_http_request "GET / HTTP/1.0\r\nConnection: keep-alive\r\n\r\n", false
|
341
|
+
res = @adapter.serve_request
|
342
|
+
assert_equal true, res
|
343
|
+
|
344
|
+
response = read_client_side
|
345
|
+
assert_equal("HTTP/1.1 200\r\nContent-Length: 2\r\n\r\nHi", response)
|
346
|
+
|
347
|
+
write_http_request "GET / HTTP/1.1\r\n\r\n", false
|
348
|
+
res = @adapter.serve_request
|
349
|
+
assert_equal true, res
|
350
|
+
|
351
|
+
response = read_client_side
|
352
|
+
expected = <<~HTTP.crlf_lines
|
353
|
+
HTTP/1.1 200
|
354
|
+
Content-Length: 2
|
355
|
+
|
356
|
+
Hi
|
357
|
+
HTTP
|
358
|
+
assert_equal(expected, response)
|
359
|
+
|
360
|
+
write_http_request "GET / HTTP/1.0\r\n\r\n"
|
361
|
+
res = @adapter.serve_request
|
362
|
+
assert_equal false, !!res
|
363
|
+
|
364
|
+
response = read_client_side
|
365
|
+
assert_equal("HTTP/1.1 200\r\nContent-Length: 2\r\n\r\nHi", response)
|
366
|
+
end
|
367
|
+
|
368
|
+
def test_pipelining_client
|
369
|
+
@hook = ->(req) {
|
370
|
+
if req.headers['foo'] == 'bar'
|
371
|
+
req.respond('Hello, foobar!', {})
|
372
|
+
else
|
373
|
+
req.respond('Hello, world!', {})
|
374
|
+
end
|
375
|
+
|
376
|
+
}
|
377
|
+
|
378
|
+
write_http_request "GET / HTTP/1.1\r\n\r\nGET / HTTP/1.1\r\nFoo: bar\r\n\r\n"
|
379
|
+
@adapter.run
|
380
|
+
response = read_client_side
|
381
|
+
|
382
|
+
expected = <<~HTTP.crlf_lines.chomp
|
383
|
+
HTTP/1.1 200
|
384
|
+
Content-Length: 13
|
385
|
+
|
386
|
+
Hello, world!HTTP/1.1 200
|
387
|
+
Content-Length: 14
|
388
|
+
|
389
|
+
Hello, foobar!
|
390
|
+
HTTP
|
391
|
+
assert_equal(expected, response)
|
392
|
+
end
|
393
|
+
|
394
|
+
def test_body_chunks
|
395
|
+
chunks = []
|
396
|
+
request = nil
|
397
|
+
|
398
|
+
@hook = ->(req) {
|
399
|
+
request = req
|
400
|
+
req.send_headers
|
401
|
+
req.each_chunk do |c|
|
402
|
+
chunks << c
|
403
|
+
req << c.upcase
|
404
|
+
end
|
405
|
+
req.finish
|
406
|
+
}
|
407
|
+
|
408
|
+
msg = "POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n6\r\nfoobar\r\n"
|
409
|
+
write_http_request msg, false
|
410
|
+
@machine.spin { @adapter.serve_request rescue nil }
|
411
|
+
@machine.sleep(0.01)
|
412
|
+
|
413
|
+
assert request
|
414
|
+
assert_equal %w[foobar], chunks
|
415
|
+
assert !request.complete?
|
416
|
+
|
417
|
+
write_http_request "6\r\nbazbud\r\n", false
|
418
|
+
@machine.sleep(0.01)
|
419
|
+
assert_equal %w[foobar bazbud], chunks
|
420
|
+
assert !request.complete?
|
421
|
+
|
422
|
+
write_http_request "0\r\n\r\n"
|
423
|
+
@machine.sleep(0.01)
|
424
|
+
assert_equal %w[foobar bazbud], chunks
|
425
|
+
assert request.complete?
|
426
|
+
|
427
|
+
@machine.sleep(0.01)
|
428
|
+
response = read_client_side
|
429
|
+
|
430
|
+
expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\n6\r\nFOOBAR\r\n6\r\nBAZBUD\r\n0\r\n\r\n"
|
431
|
+
assert_equal(expected, response)
|
432
|
+
end
|
433
|
+
|
434
|
+
def test_upgrade
|
435
|
+
done = nil
|
436
|
+
|
437
|
+
@hook = ->(req) do
|
438
|
+
return if req.upgrade_protocol != 'echo'
|
439
|
+
|
440
|
+
req.upgrade(:echo)
|
441
|
+
@machine.sleep(0.01)
|
442
|
+
buf = +''
|
443
|
+
while true
|
444
|
+
res = @machine.recv(@s_fd, buf, 4096, 0)
|
445
|
+
break if res == 0
|
446
|
+
|
447
|
+
res = @machine.send(@s_fd, buf, res, 0)
|
448
|
+
end
|
449
|
+
done = true
|
450
|
+
rescue Exception > e
|
451
|
+
p e
|
452
|
+
p e.backtrace.join("\n")
|
453
|
+
end
|
454
|
+
|
455
|
+
opts = {
|
456
|
+
upgrade: {
|
457
|
+
echo: lambda do |adapter, _headers|
|
458
|
+
conn = adapter.conn
|
459
|
+
conn << <<~HTTP.crlf_lines
|
460
|
+
HTTP/1.1 101 Switching Protocols
|
461
|
+
Upgrade: echo
|
462
|
+
Connection: Upgrade
|
463
|
+
|
464
|
+
HTTP
|
465
|
+
|
466
|
+
conn.read_loop { |data| conn << data }
|
467
|
+
done = true
|
468
|
+
end
|
469
|
+
}
|
470
|
+
}
|
471
|
+
|
472
|
+
msg = "GET / HTTP/1.1\r\nUpgrade: echo\r\nConnection: upgrade\r\n\r\n"
|
473
|
+
write_http_request(msg, false)
|
474
|
+
@machine.spin { @adapter.serve_request rescue nil }
|
475
|
+
@machine.sleep(0.01)
|
476
|
+
|
477
|
+
response = read_client_side
|
478
|
+
expected = "HTTP/1.1 101\r\nContent-Length: 0\r\nUpgrade: echo\r\nConnection: upgrade\r\n\r\n"
|
479
|
+
assert_equal(expected, response)
|
480
|
+
|
481
|
+
assert !done
|
482
|
+
|
483
|
+
write_client_side 'foo'
|
484
|
+
assert_equal 'foo', read_client_side
|
485
|
+
|
486
|
+
write_client_side 'bar'
|
487
|
+
assert_equal 'bar', read_client_side
|
488
|
+
|
489
|
+
@machine.close(@c_fd)
|
490
|
+
assert !done
|
491
|
+
|
492
|
+
@machine.sleep(0.01)
|
493
|
+
assert done
|
494
|
+
end
|
495
|
+
|
496
|
+
def test_big_download
|
497
|
+
chunk_size = 1000
|
498
|
+
chunk_count = 1000
|
499
|
+
chunk = '*' * chunk_size
|
500
|
+
|
501
|
+
@hook = ->(req) do
|
502
|
+
req.send_headers
|
503
|
+
chunk_count.times do |i|
|
504
|
+
req << chunk
|
505
|
+
@machine.snooze
|
506
|
+
end
|
507
|
+
req.finish
|
508
|
+
@machine.close(@s_fd)
|
509
|
+
rescue Exception => e
|
510
|
+
p e
|
511
|
+
p e.backtrace.join("\n")
|
512
|
+
end
|
513
|
+
|
514
|
+
response = +''
|
515
|
+
count = 0
|
516
|
+
|
517
|
+
write_client_side("GET / HTTP/1.1\r\n\r\n")
|
518
|
+
@machine.spin { @adapter.serve_request }
|
519
|
+
|
520
|
+
while (data = read_client_side(chunk_size))
|
521
|
+
response << data
|
522
|
+
count += 1
|
523
|
+
@machine.snooze
|
524
|
+
end
|
525
|
+
|
526
|
+
chunks = "#{chunk_size.to_s(16)}\r\n#{'*' * chunk_size}\r\n" * chunk_count
|
527
|
+
expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\n#{chunks}0\r\n\r\n"
|
528
|
+
|
529
|
+
assert_equal expected, response
|
530
|
+
assert count >= chunk_count
|
531
|
+
end
|
532
|
+
end
|
data/test/test_server.rb
ADDED
@@ -0,0 +1,238 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './helper'
|
4
|
+
|
5
|
+
class String
|
6
|
+
end
|
7
|
+
|
8
|
+
class ServerTest < Minitest::Test
|
9
|
+
def make_socket_pair
|
10
|
+
port = 10000 + rand(30000)
|
11
|
+
server_fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
|
12
|
+
@machine.setsockopt(server_fd, UM::SOL_SOCKET, UM::SO_REUSEADDR, true)
|
13
|
+
@machine.bind(server_fd, '127.0.0.1', port)
|
14
|
+
@machine.listen(server_fd, UM::SOMAXCONN)
|
15
|
+
|
16
|
+
client_conn_fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
|
17
|
+
@machine.connect(client_conn_fd, '127.0.0.1', port)
|
18
|
+
|
19
|
+
server_conn_fd = @machine.accept(server_fd)
|
20
|
+
|
21
|
+
@machine.close(server_fd)
|
22
|
+
[client_conn_fd, server_conn_fd]
|
23
|
+
end
|
24
|
+
|
25
|
+
class STOP < StandardError
|
26
|
+
end
|
27
|
+
|
28
|
+
def setup
|
29
|
+
@machine = UM.new
|
30
|
+
@port = 10000 + rand(30000)
|
31
|
+
@server = TP2::Server.new(@machine, '127.0.0.1', @port) { @app&.call(it) }
|
32
|
+
@f_server = @machine.spin do
|
33
|
+
@server.run
|
34
|
+
rescue STOP
|
35
|
+
ensure
|
36
|
+
@server_done = true
|
37
|
+
end
|
38
|
+
|
39
|
+
@machine.sleep(0.01)
|
40
|
+
@client_fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
|
41
|
+
@machine.connect(@client_fd, '127.0.0.1', @port)
|
42
|
+
end
|
43
|
+
|
44
|
+
def teardown
|
45
|
+
@machine.close(@client_fd) rescue nil
|
46
|
+
@machine.schedule(@f_server, STOP.new)
|
47
|
+
@machine.snooze until @server_done
|
48
|
+
end
|
49
|
+
|
50
|
+
def write_http_request(msg, shutdown_wr = true)
|
51
|
+
@machine.send(@client_fd, msg, msg.bytesize, UM::MSG_WAITALL)
|
52
|
+
@machine.shutdown(@client_fd, UM::SHUT_WR) if shutdown_wr
|
53
|
+
end
|
54
|
+
|
55
|
+
def write_client_side(msg)
|
56
|
+
@machine.send(@client_fd, msg, msg.bytesize, UM::MSG_WAITALL)
|
57
|
+
end
|
58
|
+
|
59
|
+
def read_client_side(len = 65536)
|
60
|
+
buf = +''
|
61
|
+
res = @machine.recv(@client_fd, buf, len, 0)
|
62
|
+
res == 0 ? nil : buf
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_basic_app_response
|
66
|
+
@app = ->(req) {
|
67
|
+
req.respond('Hello, world!', {})
|
68
|
+
}
|
69
|
+
|
70
|
+
write_http_request "GET / HTTP/1.0\r\n\r\n"
|
71
|
+
response = read_client_side
|
72
|
+
expected = "HTTP/1.1 200\r\nContent-Length: 13\r\n\r\nHello, world!"
|
73
|
+
assert_equal(expected, response)
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_pipelined_requests
|
77
|
+
@app = ->(req) {
|
78
|
+
req.respond("method: #{req.method}")
|
79
|
+
}
|
80
|
+
|
81
|
+
write_http_request "GET /foo HTTP/1.1\r\nServer: foo.com\r\n\r\nSCHMET /bar HTTP/1.1\r\n\r\n"
|
82
|
+
|
83
|
+
response = read_client_side
|
84
|
+
expected = "HTTP/1.1 200\r\nContent-Length: 11\r\n\r\nmethod: getHTTP/1.1 200\r\nContent-Length: 14\r\n\r\nmethod: schmet"
|
85
|
+
assert_equal(expected, response)
|
86
|
+
end
|
87
|
+
|
88
|
+
def test_graceful_shutdown
|
89
|
+
@app = ->(req) do
|
90
|
+
@machine.sleep(1)
|
91
|
+
req.respond('Hello, world!', {})
|
92
|
+
rescue UM::Terminate
|
93
|
+
req.respond('Terminated!', {})
|
94
|
+
raise
|
95
|
+
end
|
96
|
+
|
97
|
+
write_http_request "GET /foo HTTP/1.1\r\nServer: foo.com\r\n\r\nSCHMET /bar HTTP/1.1\r\n\r\n"
|
98
|
+
|
99
|
+
@machine.sleep(0.01)
|
100
|
+
@machine.schedule(@f_server, UM::Terminate.new)
|
101
|
+
@machine.snooze
|
102
|
+
|
103
|
+
response = read_client_side
|
104
|
+
expected = "HTTP/1.1 200\r\nContent-Length: 11\r\n\r\nTerminated!"
|
105
|
+
assert_equal(expected, response)
|
106
|
+
end
|
107
|
+
|
108
|
+
def test_pipelined_requests_with_body
|
109
|
+
skip
|
110
|
+
|
111
|
+
write_http_request <<~HTTP.crlf_lines
|
112
|
+
POST /foo HTTP/1.1
|
113
|
+
Server: foo.com
|
114
|
+
Content-Length: 3
|
115
|
+
|
116
|
+
abcSCHMOST /bar HTTP/1.1
|
117
|
+
Server: bar.com
|
118
|
+
Content-Length: 6
|
119
|
+
|
120
|
+
defghi
|
121
|
+
HTTP
|
122
|
+
|
123
|
+
@bodies = []
|
124
|
+
@hook = ->(req) { @bodies << req.read }
|
125
|
+
|
126
|
+
@adapter.run
|
127
|
+
assert_equal 2, @reqs.size
|
128
|
+
|
129
|
+
req0 = @reqs.shift
|
130
|
+
headers = req0.headers
|
131
|
+
assert_equal({
|
132
|
+
':method' => 'post',
|
133
|
+
':path' => '/foo',
|
134
|
+
':protocol' => 'http/1.1',
|
135
|
+
'server' => 'foo.com',
|
136
|
+
'content-length' => '3'
|
137
|
+
}, headers)
|
138
|
+
body = @bodies.shift
|
139
|
+
assert_equal 'abc', body
|
140
|
+
|
141
|
+
req1 = @reqs.shift
|
142
|
+
headers = req1.headers
|
143
|
+
assert_equal({
|
144
|
+
':method' => 'schmost',
|
145
|
+
':path' => '/bar',
|
146
|
+
':protocol' => 'http/1.1',
|
147
|
+
'server' => 'bar.com',
|
148
|
+
'content-length' => '6'
|
149
|
+
}, headers)
|
150
|
+
body = @bodies.shift
|
151
|
+
assert_equal 'defghi', body
|
152
|
+
end
|
153
|
+
|
154
|
+
def test_pipelined_requests_with_body_chunked
|
155
|
+
skip
|
156
|
+
|
157
|
+
write_http_request <<~HTTP.crlf_lines
|
158
|
+
POST /foo HTTP/1.1
|
159
|
+
Server: foo.com
|
160
|
+
Transfer-Encoding: chunked
|
161
|
+
|
162
|
+
3
|
163
|
+
abc
|
164
|
+
2
|
165
|
+
de
|
166
|
+
0
|
167
|
+
|
168
|
+
SCHMOST /bar HTTP/1.1
|
169
|
+
Server: bar.com
|
170
|
+
Transfer-Encoding: chunked
|
171
|
+
|
172
|
+
1f
|
173
|
+
123456789abcdefghijklmnopqrstuv
|
174
|
+
0
|
175
|
+
|
176
|
+
HTTP
|
177
|
+
|
178
|
+
@bodies = []
|
179
|
+
@hook = ->(req) { @bodies << req.read }
|
180
|
+
|
181
|
+
@adapter.run
|
182
|
+
assert_equal 2, @reqs.size
|
183
|
+
|
184
|
+
req0 = @reqs.shift
|
185
|
+
headers = req0.headers
|
186
|
+
assert_equal({
|
187
|
+
':method' => 'post',
|
188
|
+
':path' => '/foo',
|
189
|
+
':protocol' => 'http/1.1',
|
190
|
+
'server' => 'foo.com',
|
191
|
+
'transfer-encoding' => 'chunked',
|
192
|
+
':body-done-reading' => true
|
193
|
+
}, headers)
|
194
|
+
body = @bodies.shift
|
195
|
+
assert_equal 'abcde', body
|
196
|
+
|
197
|
+
req1 = @reqs.shift
|
198
|
+
headers = req1.headers
|
199
|
+
assert_equal({
|
200
|
+
':method' => 'schmost',
|
201
|
+
':path' => '/bar',
|
202
|
+
':protocol' => 'http/1.1',
|
203
|
+
'server' => 'bar.com',
|
204
|
+
'transfer-encoding' => 'chunked',
|
205
|
+
':body-done-reading' => true
|
206
|
+
}, headers)
|
207
|
+
body = @bodies.shift
|
208
|
+
assert_equal '123456789abcdefghijklmnopqrstuv', body
|
209
|
+
end
|
210
|
+
|
211
|
+
def test_body_to_eof
|
212
|
+
skip
|
213
|
+
|
214
|
+
write_http_request <<~HTTP.crlf_lines
|
215
|
+
POST /foo HTTP/1.1
|
216
|
+
Server: foo.com
|
217
|
+
|
218
|
+
barbaz
|
219
|
+
HTTP
|
220
|
+
|
221
|
+
@bodies = []
|
222
|
+
@hook = ->(req) { @bodies << req.read }
|
223
|
+
|
224
|
+
@adapter.run
|
225
|
+
assert_equal 1, @reqs.size
|
226
|
+
|
227
|
+
req0 = @reqs.shift
|
228
|
+
headers = req0.headers
|
229
|
+
assert_equal({
|
230
|
+
':method' => 'post',
|
231
|
+
':path' => '/foo',
|
232
|
+
':protocol' => 'http/1.1',
|
233
|
+
'server' => 'foo.com'
|
234
|
+
}, headers)
|
235
|
+
body = @bodies.shift
|
236
|
+
assert_equal 'barbaz', body
|
237
|
+
end
|
238
|
+
end
|
data/tp2.gemspec
CHANGED
@@ -19,7 +19,6 @@ Gem::Specification.new do |s|
|
|
19
19
|
s.require_paths = ['lib']
|
20
20
|
s.required_ruby_version = '>= 3.4'
|
21
21
|
|
22
|
-
s.add_dependency '
|
23
|
-
s.add_dependency 'uringmachine', '0.5.1'
|
22
|
+
s.add_dependency 'uringmachine', '0.8'
|
24
23
|
s.add_dependency 'qeweney', '0.21'
|
25
24
|
end
|
metadata
CHANGED
@@ -1,42 +1,28 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tp2
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.5'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sharon Rosner
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
|
-
- !ruby/object:Gem::Dependency
|
13
|
-
name: http_parser.rb
|
14
|
-
requirement: !ruby/object:Gem::Requirement
|
15
|
-
requirements:
|
16
|
-
- - '='
|
17
|
-
- !ruby/object:Gem::Version
|
18
|
-
version: 0.8.0
|
19
|
-
type: :runtime
|
20
|
-
prerelease: false
|
21
|
-
version_requirements: !ruby/object:Gem::Requirement
|
22
|
-
requirements:
|
23
|
-
- - '='
|
24
|
-
- !ruby/object:Gem::Version
|
25
|
-
version: 0.8.0
|
26
12
|
- !ruby/object:Gem::Dependency
|
27
13
|
name: uringmachine
|
28
14
|
requirement: !ruby/object:Gem::Requirement
|
29
15
|
requirements:
|
30
16
|
- - '='
|
31
17
|
- !ruby/object:Gem::Version
|
32
|
-
version: 0.
|
18
|
+
version: '0.8'
|
33
19
|
type: :runtime
|
34
20
|
prerelease: false
|
35
21
|
version_requirements: !ruby/object:Gem::Requirement
|
36
22
|
requirements:
|
37
23
|
- - '='
|
38
24
|
- !ruby/object:Gem::Version
|
39
|
-
version: 0.
|
25
|
+
version: '0.8'
|
40
26
|
- !ruby/object:Gem::Dependency
|
41
27
|
name: qeweney
|
42
28
|
requirement: !ruby/object:Gem::Requirement
|
@@ -58,6 +44,7 @@ extra_rdoc_files:
|
|
58
44
|
- README.md
|
59
45
|
files:
|
60
46
|
- ".gitignore"
|
47
|
+
- CHANGELOG.md
|
61
48
|
- Gemfile
|
62
49
|
- Gemfile.lock
|
63
50
|
- README.md
|
@@ -65,10 +52,14 @@ files:
|
|
65
52
|
- TODO.md
|
66
53
|
- examples/simple.rb
|
67
54
|
- lib/tp2.rb
|
68
|
-
- lib/tp2/
|
55
|
+
- lib/tp2/http1_adapter.rb
|
69
56
|
- lib/tp2/request_extensions.rb
|
70
57
|
- lib/tp2/server.rb
|
71
58
|
- lib/tp2/version.rb
|
59
|
+
- test/helper.rb
|
60
|
+
- test/run.rb
|
61
|
+
- test/test_http1_adapter.rb
|
62
|
+
- test/test_server.rb
|
72
63
|
- tp2.gemspec
|
73
64
|
homepage: https://github.com/noteflakes/tp2
|
74
65
|
licenses:
|
@@ -95,7 +86,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
95
86
|
- !ruby/object:Gem::Version
|
96
87
|
version: '0'
|
97
88
|
requirements: []
|
98
|
-
rubygems_version: 3.6.
|
89
|
+
rubygems_version: 3.6.8
|
99
90
|
specification_version: 4
|
100
91
|
summary: Experimental HTTP/1 server for UringMachine
|
101
92
|
test_files: []
|