tp2 0.16 → 0.18
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 +10 -0
- data/README.md +1 -1
- data/TODO.md +3 -3
- data/lib/tp2/connection.rb +38 -15
- data/lib/tp2/request_extensions.rb +8 -0
- data/lib/tp2/server.rb +48 -23
- data/lib/tp2/version.rb +1 -1
- data/lib/tp2.rb +12 -12
- data/test/bm_connection.rb +83 -0
- data/test/test_connection.rb +109 -3
- data/test/test_server.rb +65 -6
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6b2eaad3a3794a09c6a10e69cc3813ad27789049bd71055f315edcc5ac61b1a6
|
4
|
+
data.tar.gz: 7a8529db038480936bbb44a2a9e059ea5625b6d3f1cbb9eb7d04c185ab4268b9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5a8c23907e0e4f4d9024bf2457efdfa1b003d3d4870a65ec6a44e8ab3cc67d5d7bf096a5adba39a0e552c0eefe7fe9306a1612c19382ff64a3689805c98ff903
|
7
|
+
data.tar.gz: c4183ad2dade4aaf4511fd4d9f856c2532bf8c374a99a632010c629112c1f1fc8020f7bc7580cf3295a25bfd2826ce79b99561348f35c727176b20ea4f559cb6
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
# 0.18 2025-09-14
|
2
|
+
|
3
|
+
- Add `Request#set_cookie` method
|
4
|
+
|
5
|
+
# 0.17 2025-09-14
|
6
|
+
|
7
|
+
- Add support for server extensions: `Date`, `Server` headers
|
8
|
+
- Add `Request#set_response_headers` for injecting response headers
|
9
|
+
- Add support for env[:server_headers]
|
10
|
+
|
1
11
|
# 0.16 2025-09-11
|
2
12
|
|
3
13
|
- Remove support for HTTP/0.9, HTTP/1.0, use chunked transfer encoding
|
data/README.md
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
# TP2 - a io_uring-based app server for Ruby
|
4
4
|
|
5
5
|
[](http://rubygems.org/gems/tp2)
|
6
|
-
[](https://github.com/noteflakes/tp2/actions/workflows/test.yml)
|
7
7
|
[](https://github.com/digital-fabric/tp2/blob/master/LICENSE)
|
8
8
|
|
9
9
|
TP2 is an experimental HTTP server based on
|
data/TODO.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
## Immediate
|
2
2
|
|
3
|
-
- [
|
4
|
-
- [
|
3
|
+
- [v] Remove support for HTTP/0.9, HTTP/1.0
|
4
|
+
- [v] Reply with 505 HTTP version not supported:
|
5
5
|
|
6
6
|
```
|
7
7
|
< GET / HTTP/0.9
|
@@ -10,7 +10,7 @@
|
|
10
10
|
> Connection: close
|
11
11
|
```
|
12
12
|
|
13
|
-
- [
|
13
|
+
- [v] Use chunked transfer encoding exclusively
|
14
14
|
- [ ] Cache rendered headers
|
15
15
|
- [ ] Look at sketch in ~/Desktop/docs/
|
16
16
|
|
data/lib/tp2/connection.rb
CHANGED
@@ -13,11 +13,12 @@ module TP2
|
|
13
13
|
class Connection
|
14
14
|
attr_reader :fd, :response_headers, :logger
|
15
15
|
|
16
|
-
def initialize(machine, fd,
|
16
|
+
def initialize(server, machine, fd, env, &app)
|
17
|
+
@server = server
|
17
18
|
@machine = machine
|
18
19
|
@fd = fd
|
19
|
-
@
|
20
|
-
@logger =
|
20
|
+
@env = env
|
21
|
+
@logger = env[:logger]
|
21
22
|
@stream = UM::Stream.new(machine, fd)
|
22
23
|
@app = app
|
23
24
|
|
@@ -63,7 +64,7 @@ module TP2
|
|
63
64
|
# Handles an error encountered while serving a request by logging the error
|
64
65
|
# and optionally sending an error response with the relevant HTTP status
|
65
66
|
# code. For I/O errors, no response is sent.
|
66
|
-
#
|
67
|
+
#
|
67
68
|
# @param request [Qeweney::Request] HTTP request
|
68
69
|
# @param err [Exception] error
|
69
70
|
# @return [void]
|
@@ -83,7 +84,7 @@ module TP2
|
|
83
84
|
end
|
84
85
|
|
85
86
|
# Logs the given err and given message.
|
86
|
-
#
|
87
|
+
#
|
87
88
|
# @param err [Exception] error
|
88
89
|
# @param message [String] error message
|
89
90
|
# @return [void]
|
@@ -131,6 +132,25 @@ module TP2
|
|
131
132
|
|
132
133
|
# response API
|
133
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
|
+
|
145
|
+
def set_cookie(*cookies)
|
146
|
+
existing_cookies = @response_headers && @response_headers['Set-Cookie']
|
147
|
+
if existing_cookies
|
148
|
+
@response_headers['Set-Cookie'] = existing_cookies + cookies
|
149
|
+
else
|
150
|
+
set_response_headers('Set-Cookie' => cookies)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
134
154
|
SEND_FLAGS = UM::MSG_NOSIGNAL | UM::MSG_WAITALL
|
135
155
|
|
136
156
|
# Sends response including headers and body. Waits for the request to complete
|
@@ -139,7 +159,10 @@ module TP2
|
|
139
159
|
# @param body [String] response body
|
140
160
|
# @param headers
|
141
161
|
def respond(request, body, headers)
|
162
|
+
headers = @response_headers.merge(headers) if @response_headers
|
163
|
+
|
142
164
|
formatted_headers = format_headers(headers, body)
|
165
|
+
@response_headers = headers
|
143
166
|
request&.tx_incr(formatted_headers.bytesize + (body ? body.bytesize : 0))
|
144
167
|
if body
|
145
168
|
buf = "#{formatted_headers}#{body.bytesize.to_s(16)}\r\n#{body}\r\n#{EMPTY_CHUNK}"
|
@@ -149,7 +172,6 @@ module TP2
|
|
149
172
|
end
|
150
173
|
@logger&.info(request: request, response_headers: headers) if request
|
151
174
|
@done = true
|
152
|
-
@response_headers = headers
|
153
175
|
end
|
154
176
|
|
155
177
|
# Sends response headers. If empty_response is truthy, the response status
|
@@ -201,28 +223,28 @@ module TP2
|
|
201
223
|
@done = true
|
202
224
|
end
|
203
225
|
|
204
|
-
def respond_with_static_file(req, path,
|
226
|
+
def respond_with_static_file(req, path, env, cache_headers)
|
205
227
|
fd = @machine.open(path, UM::O_RDONLY)
|
206
|
-
|
207
|
-
if
|
208
|
-
|
228
|
+
env ||= {}
|
229
|
+
if env[:headers]
|
230
|
+
env[:headers].merge!(cache_headers)
|
209
231
|
else
|
210
|
-
|
232
|
+
env[:headers] = cache_headers
|
211
233
|
end
|
212
234
|
|
213
|
-
maxlen =
|
235
|
+
maxlen = env[:max_len] || 65_536
|
214
236
|
buf = String.new(capacity: maxlen)
|
215
237
|
headers_sent = nil
|
216
238
|
loop do
|
217
239
|
res = @machine.read(fd, buf, maxlen, 0)
|
218
240
|
if res < maxlen && !headers_sent
|
219
|
-
return respond(req, buf,
|
241
|
+
return respond(req, buf, env[:headers])
|
220
242
|
elsif res == 0
|
221
243
|
return finish(req)
|
222
244
|
end
|
223
245
|
|
224
246
|
if !headers_sent
|
225
|
-
send_headers(req,
|
247
|
+
send_headers(req, env[:headers])
|
226
248
|
headers_sent = true
|
227
249
|
end
|
228
250
|
done = res < maxlen
|
@@ -230,7 +252,7 @@ module TP2
|
|
230
252
|
return if done
|
231
253
|
end
|
232
254
|
ensure
|
233
|
-
@machine.
|
255
|
+
@machine.close_async(fd) if fd
|
234
256
|
end
|
235
257
|
|
236
258
|
def close
|
@@ -332,6 +354,7 @@ module TP2
|
|
332
354
|
def format_headers(headers, body)
|
333
355
|
status = headers[':status'] || (body ? Qeweney::Status::OK : Qeweney::Status::NO_CONTENT)
|
334
356
|
lines = format_status_line(body, status)
|
357
|
+
lines << @env[:server_headers] if @env[:server_headers]
|
335
358
|
headers.each do |k, v|
|
336
359
|
next if k =~ INTERNAL_HEADER_REGEXP
|
337
360
|
|
data/lib/tp2/server.rb
CHANGED
@@ -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 = Connection.new(@machine, fd, @
|
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,22 +24,22 @@ 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
45
|
server.run
|
@@ -48,11 +48,11 @@ module TP2
|
|
48
48
|
end
|
49
49
|
end
|
50
50
|
|
51
|
-
def
|
52
|
-
return @
|
51
|
+
def env(env = nil, &app)
|
52
|
+
return @env if !env && !app
|
53
53
|
|
54
|
-
@
|
55
|
-
@
|
54
|
+
@env = env || {}
|
55
|
+
@env[:app] = app if app
|
56
56
|
end
|
57
57
|
|
58
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/test_connection.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative './helper'
|
4
|
+
require 'securerandom'
|
4
5
|
|
5
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 ConnectionTest < 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
|
@@ -341,7 +343,7 @@ class ConnectionTest < Minitest::Test
|
|
341
343
|
@adapter.run
|
342
344
|
response = read_client_side
|
343
345
|
|
344
|
-
expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\nd\r\nHello, world!\r\n0\r\n\r\n" +
|
346
|
+
expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\nd\r\nHello, world!\r\n0\r\n\r\n" +
|
345
347
|
"HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\ne\r\nHello, foobar!\r\n0\r\n\r\n"
|
346
348
|
assert_equal(expected, response)
|
347
349
|
end
|
@@ -520,4 +522,108 @@ class ConnectionTest < Minitest::Test
|
|
520
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"
|
521
523
|
assert_equal expected, response
|
522
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
|
585
|
+
|
586
|
+
def test_set_cookie_single
|
587
|
+
@hook = ->(req) {
|
588
|
+
req.set_cookie('foo=bar; HttpOnly')
|
589
|
+
req.respond('foo')
|
590
|
+
}
|
591
|
+
|
592
|
+
write_client_side("GET / HTTP/1.1\r\n\r\n")
|
593
|
+
@adapter.serve_request
|
594
|
+
response = read_client_side(65536)
|
595
|
+
expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\nSet-Cookie: foo=bar; HttpOnly\r\n\r\n3\r\nfoo\r\n0\r\n\r\n"
|
596
|
+
assert_equal expected, response
|
597
|
+
|
598
|
+
end
|
599
|
+
|
600
|
+
def test_set_cookie_multi1
|
601
|
+
@hook = ->(req) {
|
602
|
+
req.set_cookie('foo=bar; HttpOnly', 'bar=baz')
|
603
|
+
req.respond('foo')
|
604
|
+
}
|
605
|
+
|
606
|
+
write_client_side("GET / HTTP/1.1\r\n\r\n")
|
607
|
+
@adapter.serve_request
|
608
|
+
response = read_client_side(65536)
|
609
|
+
expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\nSet-Cookie: foo=bar; HttpOnly\r\nSet-Cookie: bar=baz\r\n\r\n3\r\nfoo\r\n0\r\n\r\n"
|
610
|
+
assert_equal expected, response
|
611
|
+
|
612
|
+
end
|
613
|
+
|
614
|
+
def test_set_cookie_multi2
|
615
|
+
@hook = ->(req) {
|
616
|
+
req.set_cookie('a=1', 'b=2')
|
617
|
+
req.set_cookie('c=3')
|
618
|
+
req.set_cookie('d=4', 'e=5')
|
619
|
+
req.respond('foo')
|
620
|
+
}
|
621
|
+
|
622
|
+
write_client_side("GET / HTTP/1.1\r\n\r\n")
|
623
|
+
@adapter.serve_request
|
624
|
+
response = read_client_side(65536)
|
625
|
+
expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\nSet-Cookie: a=1\r\nSet-Cookie: b=2\r\nSet-Cookie: c=3\r\nSet-Cookie: d=4\r\nSet-Cookie: e=5\r\n\r\n3\r\nfoo\r\n0\r\n\r\n"
|
626
|
+
assert_equal expected, response
|
627
|
+
|
628
|
+
end
|
523
629
|
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
|
@@ -94,8 +94,9 @@ class ServerTest < Minitest::Test
|
|
94
94
|
|
95
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"
|
96
96
|
|
97
|
+
@machine.sleep(0.1)
|
97
98
|
response = read_client_side
|
98
|
-
expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\nb\r\nmethod: get\r\n0\r\n\r\n" +
|
99
|
+
expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\nb\r\nmethod: get\r\n0\r\n\r\n" +
|
99
100
|
"HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\ne\r\nmethod: schmet\r\n0\r\n\r\n"
|
100
101
|
assert_equal(expected, response)
|
101
102
|
end
|
@@ -278,7 +279,7 @@ class ServerTest < Minitest::Test
|
|
278
279
|
def test_logging
|
279
280
|
skip
|
280
281
|
reqs = []
|
281
|
-
@
|
282
|
+
@env[:logger] = TestLogger.new
|
282
283
|
@app = ->(req) { reqs << req; req.respond('Hello, world!', {}) }
|
283
284
|
|
284
285
|
write_http_request "GET / HTTP/1.0\r\n\r\n"
|
@@ -286,10 +287,68 @@ class ServerTest < Minitest::Test
|
|
286
287
|
expected = "HTTP/1.1 200\r\nContent-Length: 13\r\n\r\nHello, world!"
|
287
288
|
assert_equal(expected, response)
|
288
289
|
|
289
|
-
entries = @
|
290
|
+
entries = @env[:logger].entries
|
290
291
|
assert_equal 1, entries.size
|
291
292
|
assert_equal 1, reqs.size
|
292
293
|
|
293
294
|
assert_equal reqs.first, entries.first[:request]
|
294
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
|
295
354
|
end
|
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.18'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sharon Rosner
|
@@ -78,6 +78,7 @@ files:
|
|
78
78
|
- lib/tp2/request_extensions.rb
|
79
79
|
- lib/tp2/server.rb
|
80
80
|
- lib/tp2/version.rb
|
81
|
+
- test/bm_connection.rb
|
81
82
|
- test/helper.rb
|
82
83
|
- test/run.rb
|
83
84
|
- test/test_connection.rb
|