Skip to content

Commit 475c0fb

Browse files
committed
Refactor test servers to remove webrick dependency
1 parent fa8646c commit 475c0fb

10 files changed

Lines changed: 367 additions & 96 deletions

File tree

Gemfile

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@ ruby RUBY_VERSION
55

66
gem "rake"
77

8-
# Ruby 3.0 does not ship it anymore.
9-
# TODO: We should probably refactor tests to avoid need for it.
10-
gem "webrick"
11-
128
group :development do
139
gem "debug", platform: :mri
1410

lib/http/response/status.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ class << self
1919
# @api public
2020
def coerce(object)
2121
code = case object
22-
when String then SYMBOL_CODES.dig(symbolize(object))
23-
when Symbol then SYMBOL_CODES.dig(object)
22+
when String then SYMBOL_CODES[symbolize(object)]
23+
when Symbol then SYMBOL_CODES[object]
2424
when Numeric then object
2525
end
2626

test/support/black_hole.rb

Lines changed: 0 additions & 13 deletions
This file was deleted.

test/support/dummy_server.rb

Lines changed: 103 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,124 @@
11
# frozen_string_literal: true
22

3-
require "webrick"
4-
require "webrick/ssl"
3+
require "socket"
4+
require "openssl"
55

6-
require "support/black_hole"
76
require "support/dummy_server/servlet"
8-
require "support/servers/config"
97
require "support/servers/runner"
108
require "support/ssl_helper"
119

12-
class DummyServer < WEBrick::HTTPServer
13-
include ServerConfig
14-
15-
CONFIG = {
16-
BindAddress: "127.0.0.1",
17-
Port: 0,
18-
AccessLog: BlackHole,
19-
Logger: BlackHole
20-
}.freeze
21-
22-
SSL_CONFIG = CONFIG.merge(
23-
SSLEnable: true,
24-
SSLStartImmediately: true
25-
).freeze
26-
10+
class DummyServer
2711
def initialize(options = {})
28-
super(options[:ssl] ? SSL_CONFIG : CONFIG)
29-
@memo = {}
30-
mount("/", Servlet, @memo)
12+
@ssl = options[:ssl]
13+
@tcp_server = TCPServer.new("127.0.0.1", 0)
14+
@port = @tcp_server.addr[1]
15+
@memo = {}
16+
@servlet = Servlet.new(self, @memo)
17+
@running = false
18+
end
19+
20+
def addr
21+
"127.0.0.1"
3122
end
3223

24+
attr_reader :port
25+
3326
def endpoint
3427
"#{scheme}://#{addr}:#{port}"
3528
end
3629

3730
def scheme
38-
config[:SSLEnable] ? "https" : "http"
31+
@ssl ? "https" : "http"
32+
end
33+
34+
def start
35+
server = @ssl ? ssl_server : @tcp_server
36+
@running = true
37+
38+
while @running
39+
client = server.accept
40+
Thread.new(client) { |c| handle_connection(c) }
41+
end
42+
rescue IOError, Errno::EBADF
43+
# Server socket closed during shutdown
44+
end
45+
46+
def shutdown
47+
@running = false
48+
@tcp_server.close
49+
rescue
50+
nil
3951
end
4052

4153
def ssl_context
4254
@ssl_context ||= SSLHelper.server_context
4355
end
56+
57+
private
58+
59+
def ssl_server
60+
OpenSSL::SSL::SSLServer.new(@tcp_server, ssl_context)
61+
end
62+
63+
def handle_connection(client)
64+
loop do
65+
request = read_request(client)
66+
break unless request
67+
68+
Thread.pass
69+
respond(client, request)
70+
end
71+
rescue IOError, Errno::ECONNRESET, Errno::EPIPE, Errno::EPROTOTYPE, OpenSSL::SSL::SSLError
72+
# Connection closed or SSL error
73+
ensure
74+
client.close rescue nil # rubocop:disable Style/RescueModifier
75+
end
76+
77+
def respond(client, request)
78+
response = Response.new
79+
@servlet.dispatch(request, response)
80+
client.write(response.serialize(head_request: request.request_method == "HEAD"))
81+
end
82+
83+
def read_request(client)
84+
line = client.gets
85+
return unless line
86+
87+
method, uri, = line.split(" ", 3)
88+
return bad_request(client) unless uri.ascii_only?
89+
90+
raw_path, query_string = uri.split("?", 2)
91+
headers = read_headers(client)
92+
93+
Request.new({
94+
request_method: method, request_path: percent_decode(raw_path),
95+
query_string: query_string, headers: headers,
96+
body: read_body(client, headers), socket: client, unparsed_uri: uri
97+
})
98+
end
99+
100+
def read_headers(client)
101+
headers = {}
102+
while (header_line = client.gets)
103+
break if header_line == "\r\n"
104+
105+
key, value = header_line.split(": ", 2)
106+
headers[key.downcase] = value.strip
107+
end
108+
headers
109+
end
110+
111+
def read_body(client, headers)
112+
content_length = headers["content-length"]
113+
client.read(content_length.to_i) if content_length
114+
end
115+
116+
def bad_request(client)
117+
client.write("HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n")
118+
nil
119+
end
120+
121+
def percent_decode(str)
122+
str.b.gsub(/%([0-9A-Fa-f]{2})/) { [::Regexp.last_match(1)].pack("H2") }
123+
end
44124
end

test/support/dummy_server/encoding_routes.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# frozen_string_literal: true
22

3-
class DummyServer < WEBrick::HTTPServer
3+
class DummyServer
44
class Servlet
55
post "/encoded-body" do |req, res|
66
res.status = 200

test/support/dummy_server/routes.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# frozen_string_literal: true
22

3-
class DummyServer < WEBrick::HTTPServer
3+
class DummyServer
44
class Servlet
55
get "/" do |req, res|
66
res.status = 200
@@ -65,12 +65,12 @@ class Servlet
6565

6666
get "/redirect-301" do |_req, res|
6767
res.status = 301
68-
res["Location"] = "http://#{@server.config[:BindAddress]}:#{@server.config[:Port]}/"
68+
res["Location"] = "http://#{@server.addr}:#{@server.port}/"
6969
end
7070

7171
get "/redirect-302" do |_req, res|
7272
res.status = 302
73-
res["Location"] = "http://#{@server.config[:BindAddress]}:#{@server.config[:Port]}/"
73+
res["Location"] = "http://#{@server.addr}:#{@server.port}/"
7474
end
7575

7676
post "/form" do |req, res|

test/support/dummy_server/servlet.rb

Lines changed: 111 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,134 @@
22

33
require "uri"
44

5-
class DummyServer < WEBrick::HTTPServer
6-
class Servlet < WEBrick::HTTPServlet::AbstractServlet
5+
class DummyServer
6+
Cookie = Struct.new(:name, :value)
7+
8+
class Request
9+
attr_reader :request_method
10+
attr_reader :path
11+
attr_reader :query_string
12+
attr_reader :body
13+
attr_reader :unparsed_uri
14+
15+
def initialize(attrs)
16+
@request_method = attrs[:request_method]
17+
@path = attrs[:request_path]
18+
@query_string = attrs[:query_string]
19+
@headers = attrs[:headers]
20+
@body = attrs[:body]
21+
@socket = attrs[:socket]
22+
@unparsed_uri = attrs[:unparsed_uri]
23+
end
24+
25+
def [](header)
26+
@headers[header.downcase]
27+
end
28+
29+
def query
30+
@query ||= if body && @headers["content-type"]&.include?("application/x-www-form-urlencoded")
31+
URI.decode_www_form(body).to_h
32+
elsif query_string
33+
URI.decode_www_form(query_string).to_h
34+
else
35+
{}
36+
end
37+
end
38+
39+
def cookies
40+
@cookies ||= parse_cookies
41+
end
42+
43+
private
44+
45+
def parse_cookies
46+
cookie_header = @headers["cookie"]
47+
return [] unless cookie_header
48+
49+
cookie_header.split("; ").map do |pair|
50+
name, value = pair.split("=", 2)
51+
Cookie.new(name, value)
52+
end
53+
end
54+
end
55+
56+
class Response
57+
attr_accessor :status
58+
attr_accessor :body
59+
60+
STATUS_TEXTS = {
61+
200 => "OK",
62+
204 => "No Content",
63+
301 => "Moved Permanently",
64+
302 => "Found",
65+
400 => "Bad Request",
66+
404 => "Not Found",
67+
500 => "Internal Server Error"
68+
}.freeze
69+
70+
def initialize
71+
@status = 200
72+
@headers = {}
73+
@body = ""
74+
end
75+
76+
def []=(header, value)
77+
@headers[header] = value
78+
end
79+
80+
def [](header)
81+
@headers[header]
82+
end
83+
84+
def serialize(head_request: false)
85+
status_text = STATUS_TEXTS[@status] || "OK"
86+
body_bytes = @body.to_s.b
87+
88+
lines = "HTTP/1.1 #{@status} #{status_text}\r\n"
89+
@headers.each { |k, v| lines << "#{k}: #{v}\r\n" }
90+
lines << "Content-Length: #{body_bytes.bytesize}\r\n" unless @headers.key?("Content-Length")
91+
lines << "Connection: keep-alive\r\n"
92+
lines << "\r\n"
93+
lines << body_bytes unless head_request
94+
lines
95+
end
96+
end
97+
98+
class Servlet
799
def self.sockets
8100
@sockets ||= []
9101
end
10102

103+
def self.handlers
104+
@handlers ||= {}
105+
end
106+
107+
def initialize(server, memo)
108+
@server = server
109+
@memo = memo
110+
end
111+
11112
def not_found(req, res)
12113
res.status = 404
13114
res.body = "#{req.unparsed_uri} not found"
14115
end
15116

16-
def self.handlers
17-
@handlers ||= {}
18-
end
117+
def dispatch(req, res)
118+
method = req.request_method.downcase
119+
handler = self.class.handlers["#{method}:#{req.path}"]
19120

20-
def initialize(server, memo)
21-
super(server)
22-
@memo = memo
121+
if handler
122+
instance_exec(req, res, &handler)
123+
else
124+
not_found(req, res)
125+
end
23126
end
24127

25128
%w[get post head].each do |method|
26129
class_eval <<-RUBY, __FILE__, __LINE__ + 1
27130
def self.#{method}(path, &block)
28131
handlers["#{method}:\#{path}"] = block
29132
end
30-
31-
def do_#{method.upcase}(req, res)
32-
handler = self.class.handlers["#{method}:\#{req.path}"]
33-
return instance_exec(req, res, &handler) if handler
34-
not_found(req, res)
35-
end
36133
RUBY
37134
end
38135
end

0 commit comments

Comments
 (0)