tp2 0.16 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9b39e332d3140ec95dee3edc265fec814873208d8b17797fccd5513b843b614b
4
- data.tar.gz: f39b677a8e703b9d9f8a48aa223365b6355eb5810735812cf600ae1645351d89
3
+ metadata.gz: 7f064819fba5d42c4376f3895ee0cce07745149e6b9ed06e864acb1ac3515d9e
4
+ data.tar.gz: 84767b1d2cd87c6006ba7202f48c2bf1cdc9345ddf258c8fcd716bcf2023c264
5
5
  SHA512:
6
- metadata.gz: 0cab6849c05ca00890073e2ef7fe2db7410fbda9ac10f4d908fba7c04e02452dc31ec4f9c24c56dd5719ca11c6bcd57de325b1e422fab4114515674e3273c854
7
- data.tar.gz: 7cb455c7da2207024e6ce8d41435dee1c6ed2d4357b259d9b34a750f829504576304aac7cc8e998a5bd06eeca61db8e643311bcf67c0726528bf3a0f7d04699a
6
+ metadata.gz: 72b97e276e4abb593566d761f44ac7bfa0a9af5d9ff5dcbece407412dd07eac84a9dfbe6d983c832010fb9a365be27053df471d2b9e76a2955b7332a33b06e69
7
+ data.tar.gz: 6a13102f1f2008b32aa34fec7da9b2cda496e19f5445525f398b6c4f0a863b15bb68bf94b457e56f0b3c6ace41d3caca4722158ea69fc638d6a828f39e047bbd
data/.gitignore CHANGED
@@ -54,3 +54,4 @@ Gemfile.lock
54
54
 
55
55
  # Used by RuboCop. Remote config files pulled in from inherit_from directive.
56
56
  # .rubocop-https?--*
57
+ *.json
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
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
+
1
7
  # 0.16 2025-09-11
2
8
 
3
9
  - 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
  [![Gem Version](https://badge.fury.io/rb/tp2.svg)](http://rubygems.org/gems/tp2)
6
- [![Tipi Test](https://github.com/digital-fabric/tp2/workflows/Tests/badge.svg)](https://github.com/digital-fabric/tp2/actions?query=workflow%3ATests)
6
+ [![Tests](https://github.com/noteflakes/tp2/actions/workflows/test.yml/badge.svg)](https://github.com/noteflakes/tp2/actions/workflows/test.yml)
7
7
  [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](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
- - [ ] Remove support for HTTP/0.9, HTTP/1.0
4
- - [ ] Reply with 505 HTTP version not supported:
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
- - [ ] Use chunked transfer encoding exclusively
13
+ - [v] Use chunked transfer encoding exclusively
14
14
  - [ ] Cache rendered headers
15
15
  - [ ] Look at sketch in ~/Desktop/docs/
16
16
 
@@ -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, opts, &app)
16
+ def initialize(server, machine, fd, env, &app)
17
+ @server = server
17
18
  @machine = machine
18
19
  @fd = fd
19
- @opts = opts
20
- @logger = opts[: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,16 @@ 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
+
134
145
  SEND_FLAGS = UM::MSG_NOSIGNAL | UM::MSG_WAITALL
135
146
 
136
147
  # Sends response including headers and body. Waits for the request to complete
@@ -139,7 +150,10 @@ module TP2
139
150
  # @param body [String] response body
140
151
  # @param headers
141
152
  def respond(request, body, headers)
153
+ headers = @response_headers.merge(headers) if @response_headers
154
+
142
155
  formatted_headers = format_headers(headers, body)
156
+ @response_headers = headers
143
157
  request&.tx_incr(formatted_headers.bytesize + (body ? body.bytesize : 0))
144
158
  if body
145
159
  buf = "#{formatted_headers}#{body.bytesize.to_s(16)}\r\n#{body}\r\n#{EMPTY_CHUNK}"
@@ -149,7 +163,6 @@ module TP2
149
163
  end
150
164
  @logger&.info(request: request, response_headers: headers) if request
151
165
  @done = true
152
- @response_headers = headers
153
166
  end
154
167
 
155
168
  # Sends response headers. If empty_response is truthy, the response status
@@ -201,28 +214,28 @@ module TP2
201
214
  @done = true
202
215
  end
203
216
 
204
- def respond_with_static_file(req, path, opts, cache_headers)
217
+ def respond_with_static_file(req, path, env, cache_headers)
205
218
  fd = @machine.open(path, UM::O_RDONLY)
206
- opts ||= {}
207
- if opts[:headers]
208
- opts[:headers].merge!(cache_headers)
219
+ env ||= {}
220
+ if env[:headers]
221
+ env[:headers].merge!(cache_headers)
209
222
  else
210
- opts[:headers] = cache_headers
223
+ env[:headers] = cache_headers
211
224
  end
212
225
 
213
- maxlen = opts[:max_len] || 65_536
226
+ maxlen = env[:max_len] || 65_536
214
227
  buf = String.new(capacity: maxlen)
215
228
  headers_sent = nil
216
229
  loop do
217
230
  res = @machine.read(fd, buf, maxlen, 0)
218
231
  if res < maxlen && !headers_sent
219
- return respond(req, buf, opts[:headers])
232
+ return respond(req, buf, env[:headers])
220
233
  elsif res == 0
221
234
  return finish(req)
222
235
  end
223
236
 
224
237
  if !headers_sent
225
- send_headers(req, opts[:headers])
238
+ send_headers(req, env[:headers])
226
239
  headers_sent = true
227
240
  end
228
241
  done = res < maxlen
@@ -230,7 +243,7 @@ module TP2
230
243
  return if done
231
244
  end
232
245
  ensure
233
- @machine.close(fd) if fd
246
+ @machine.close_async(fd) if fd
234
247
  end
235
248
 
236
249
  def close
@@ -332,6 +345,7 @@ module TP2
332
345
  def format_headers(headers, body)
333
346
  status = headers[':status'] || (body ? Qeweney::Status::OK : Qeweney::Status::NO_CONTENT)
334
347
  lines = format_status_line(body, status)
348
+ lines << @env[:server_headers] if @env[:server_headers]
335
349
  headers.each do |k, v|
336
350
  next if k =~ INTERNAL_HEADER_REGEXP
337
351
 
@@ -11,4 +11,8 @@ class Qeweney::Request
11
11
 
12
12
  adapter.respond_with_static_file(self, path, opts, cache_headers)
13
13
  end
14
+
15
+ def set_response_headers(headers)
16
+ adapter.set_response_headers(headers)
17
+ end
14
18
  end
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(opts)
13
- raise 'Missing app location' if !opts[:app_location]
12
+ def self.rack_app(env)
13
+ raise 'Missing app location' if !env[:app_location]
14
14
 
15
- TP2::RackAdapter.load(opts[:app_location])
15
+ TP2::RackAdapter.load(env[:app_location])
16
16
  end
17
17
 
18
- def self.tp2_app(_machine, opts)
19
- if opts[:app_location]
20
- opts[:logger]&.info(message: 'Loading web app', location: opts[:app_location])
21
- require opts[:app_location]
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
- opts.merge!(TP2.config)
23
+ env.merge!(TP2.config)
24
24
  end
25
- opts[:app]
25
+ env[:app]
26
26
  end
27
27
 
28
- def self.static_app(opts); end
28
+ def self.static_app(env); end
29
29
 
30
- def initialize(machine, opts, &app)
30
+ def initialize(machine, env, &app)
31
31
  @machine = machine
32
- @opts = opts
33
- @app = app || app_from_opts
32
+ @env = env
33
+ @app = app || app_from_env
34
34
  @server_fds = []
35
35
  @accept_fibers = []
36
36
  end
37
37
 
38
- def app_from_opts
39
- case @opts[:app_type]
38
+ def app_from_env
39
+ case @env[:app_type]
40
40
  when nil, :tp2
41
- Server.tp2_app(@machine, @opts)
41
+ Server.tp2_app(@machine, @env)
42
42
  when :rack
43
- Server.rack_app(@opts)
43
+ Server.rack_app(@env)
44
44
  when :static
45
- Server.static_app(@opts)
45
+ Server.static_app(@env)
46
46
  else
47
- raise "Invalid app type #{@opts[:app_type].inspect}"
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
- @opts[:logger]&.info(message: "Listening on #{bind_string}")
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 = @opts[: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, @opts, &@app)
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
- @opts[:logger]&.info(message: 'Shutting down gracefully...')
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
@@ -1,3 +1,3 @@
1
1
  module TP2
2
- VERSION = '0.16'
2
+ VERSION = '0.17'
3
3
  end
data/lib/tp2.rb CHANGED
@@ -24,22 +24,22 @@ module TP2
24
24
  )
25
25
 
26
26
  class << self
27
- def run(opts = nil, &app)
27
+ def run(env = {}, &app)
28
28
  if @in_run
29
- @config = opts if opts
30
- @config[:app] = app if app
29
+ @env = env
30
+ @env[:app] = app if app
31
31
  return
32
32
  end
33
33
 
34
- opts ||= @config || {}
34
+ env ||= @env || {}
35
35
  begin
36
36
  @in_run = true
37
- machine = opts[:machine] || UM.new
38
- machine.puts(opts[:banner]) if opts[:banner]
37
+ machine = env[:machine] || UM.new
38
+ machine.puts(env[:banner]) if env[:banner]
39
39
 
40
- opts[:logger]&.info(message: "Running TP2 #{TP2::VERSION}, UringMachine #{UM::VERSION}, Ruby #{RUBY_VERSION}")
40
+ env[:logger]&.info(message: "Running TP2 #{TP2::VERSION}, UringMachine #{UM::VERSION}, Ruby #{RUBY_VERSION}")
41
41
 
42
- server = Server.new(machine, opts, &app)
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 config(opts = nil, &app)
52
- return @config if !opts && !app
51
+ def env(env = nil, &app)
52
+ return @env if !env && !app
53
53
 
54
- @config = opts || {}
55
- @config[:app] = app if app
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
@@ -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 + rand(30000)
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
- @adapter = TP2::Connection.new(@machine, @s_fd, {}, &@app)
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,64 @@ 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
523
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
- @opts = { bind: "127.0.0.1:#{@port}" }
29
- @server = TP2::Server.new(@machine, @opts) { @app&.call(it) }
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
- @opts[:logger] = TestLogger.new
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 = @opts[:logger].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.16'
4
+ version: '0.17'
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