wsv 0.8.0 → 0.10.0
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 +76 -1
- data/README.md +91 -8
- data/lib/wsv/app.rb +109 -6
- data/lib/wsv/cli.rb +49 -3
- data/lib/wsv/cors.rb +30 -0
- data/lib/wsv/path_resolver.rb +14 -1
- data/lib/wsv/request/parser.rb +52 -0
- data/lib/wsv/request/too_large.rb +18 -0
- data/lib/wsv/request.rb +4 -47
- data/lib/wsv/response/file_body.rb +31 -0
- data/lib/wsv/response/file_builder.rb +63 -0
- data/lib/wsv/response/string_body.rb +23 -0
- data/lib/wsv/response/text_builder.rb +35 -0
- data/lib/wsv/response.rb +43 -29
- data/lib/wsv/server/banner.rb +56 -0
- data/lib/wsv/server/browser_launcher.rb +63 -0
- data/lib/wsv/server/deadline_reader.rb +23 -0
- data/lib/wsv/server.rb +70 -38
- data/lib/wsv/status.rb +4 -0
- data/lib/wsv/tls_context/resolver.rb +71 -0
- data/lib/wsv/tls_context/self_signed_cert.rb +38 -0
- data/lib/wsv/tls_context.rb +29 -0
- data/lib/wsv/version.rb +1 -1
- data/lib/wsv.rb +2 -0
- data/test/app_test.rb +356 -2
- data/test/browser_launcher_test.rb +57 -0
- data/test/cli_test.rb +64 -0
- data/test/path_resolver_test.rb +35 -0
- data/test/response_test.rb +49 -0
- data/test/server_test.rb +84 -2
- data/test/tls_context_test.rb +169 -0
- metadata +17 -2
data/test/path_resolver_test.rb
CHANGED
|
@@ -127,6 +127,41 @@ class PathResolverTest < Minitest::Test
|
|
|
127
127
|
assert_equal 400, result.status
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
+
def test_returns_400_for_nul_byte_in_path
|
|
131
|
+
result = @resolver.resolve("/foo%00bar")
|
|
132
|
+
|
|
133
|
+
assert_predicate result, :error?
|
|
134
|
+
assert_equal 400, result.status
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def test_returns_400_for_cr_in_path
|
|
138
|
+
result = @resolver.resolve("/foo%0Dbar")
|
|
139
|
+
|
|
140
|
+
assert_predicate result, :error?
|
|
141
|
+
assert_equal 400, result.status
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def test_returns_400_for_lf_in_path
|
|
145
|
+
result = @resolver.resolve("/foo%0Abar")
|
|
146
|
+
|
|
147
|
+
assert_predicate result, :error?
|
|
148
|
+
assert_equal 400, result.status
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def test_returns_400_for_tab_in_path
|
|
152
|
+
result = @resolver.resolve("/foo%09bar")
|
|
153
|
+
|
|
154
|
+
assert_predicate result, :error?
|
|
155
|
+
assert_equal 400, result.status
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def test_returns_400_for_del_in_path
|
|
159
|
+
result = @resolver.resolve("/foo%7Fbar")
|
|
160
|
+
|
|
161
|
+
assert_predicate result, :error?
|
|
162
|
+
assert_equal 400, result.status
|
|
163
|
+
end
|
|
164
|
+
|
|
130
165
|
def test_rejects_symlink_to_dotfile
|
|
131
166
|
File.write(File.join(@dir, ".env"), "secret")
|
|
132
167
|
File.symlink(".env", File.join(@dir, "config"))
|
data/test/response_test.rb
CHANGED
|
@@ -45,4 +45,53 @@ class ResponseTest < Minitest::Test
|
|
|
45
45
|
|
|
46
46
|
assert_includes io.string, "HTTP/1.1 404 Not Found"
|
|
47
47
|
end
|
|
48
|
+
|
|
49
|
+
def test_write_to_emits_nosniff_header
|
|
50
|
+
response = Wsv::Response.text(200)
|
|
51
|
+
io = StringIO.new
|
|
52
|
+
response.write_to(io)
|
|
53
|
+
|
|
54
|
+
assert_includes io.string, "X-Content-Type-Options: nosniff"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def test_file_response_streams_via_io_copy_stream
|
|
58
|
+
Dir.mktmpdir do |dir|
|
|
59
|
+
path = File.join(dir, "data.bin")
|
|
60
|
+
File.binwrite(path, "abcdefghij")
|
|
61
|
+
|
|
62
|
+
response = Wsv::Response.file(path)
|
|
63
|
+
io = StringIO.new
|
|
64
|
+
response.write_to(io)
|
|
65
|
+
|
|
66
|
+
assert_includes io.string, "HTTP/1.1 200 OK"
|
|
67
|
+
assert io.string.end_with?("abcdefghij"), "expected file bytes at end of response"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_file_response_does_not_eagerly_read_file
|
|
72
|
+
Dir.mktmpdir do |dir|
|
|
73
|
+
path = File.join(dir, "data.bin")
|
|
74
|
+
File.binwrite(path, "abcdefghij")
|
|
75
|
+
|
|
76
|
+
response = Wsv::Response.file(path)
|
|
77
|
+
File.delete(path)
|
|
78
|
+
|
|
79
|
+
assert_equal 200, response.status
|
|
80
|
+
assert_equal "10", response.headers["Content-Length"]
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def test_file_response_streams_byte_range
|
|
85
|
+
Dir.mktmpdir do |dir|
|
|
86
|
+
path = File.join(dir, "data.bin")
|
|
87
|
+
File.binwrite(path, "abcdefghij")
|
|
88
|
+
|
|
89
|
+
response = Wsv::Response.file(path, range: 2..5)
|
|
90
|
+
io = StringIO.new
|
|
91
|
+
response.write_to(io)
|
|
92
|
+
|
|
93
|
+
assert_includes io.string, "HTTP/1.1 206 Partial Content"
|
|
94
|
+
assert io.string.end_with?("cdef"), "expected only the requested range"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
48
97
|
end
|
data/test/server_test.rb
CHANGED
|
@@ -198,6 +198,23 @@ class ServerTest < Minitest::Test
|
|
|
198
198
|
refute_includes err.string, "WARNING"
|
|
199
199
|
end
|
|
200
200
|
|
|
201
|
+
def test_banner_brackets_ipv6_address_in_url
|
|
202
|
+
out = StringIO.new
|
|
203
|
+
server = Wsv::Server.new(host: "::1", port: 8000, root: @dir, out: out, err: StringIO.new)
|
|
204
|
+
server.send(:log_startup)
|
|
205
|
+
|
|
206
|
+
assert_includes out.string, "http://[::1]:8000/"
|
|
207
|
+
refute_includes out.string, "http://::1:8000/"
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def test_banner_percent_encodes_ipv6_zone_identifier
|
|
211
|
+
out = StringIO.new
|
|
212
|
+
server = Wsv::Server.new(host: "fe80::1%eth0", port: 8000, root: @dir, out: out, err: StringIO.new)
|
|
213
|
+
server.send(:log_startup)
|
|
214
|
+
|
|
215
|
+
assert_includes out.string, "http://[fe80::1%25eth0]:8000/"
|
|
216
|
+
end
|
|
217
|
+
|
|
201
218
|
def test_accept_loop_survives_transient_accept_error
|
|
202
219
|
File.write(File.join(@dir, "x.txt"), "ok")
|
|
203
220
|
err = StringIO.new
|
|
@@ -226,6 +243,64 @@ class ServerTest < Minitest::Test
|
|
|
226
243
|
socket&.close
|
|
227
244
|
end
|
|
228
245
|
|
|
246
|
+
def test_serves_byte_range_e2e
|
|
247
|
+
File.write(File.join(@dir, "data.bin"), "abcdefghij")
|
|
248
|
+
start_server
|
|
249
|
+
|
|
250
|
+
uri = URI("http://127.0.0.1:#{@server.port}/data.bin")
|
|
251
|
+
response = Net::HTTP.start(uri.host, uri.port) do |http|
|
|
252
|
+
http.get(uri.path, "Range" => "bytes=2-5")
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
assert_equal "206", response.code
|
|
256
|
+
assert_equal "cdef", response.body
|
|
257
|
+
assert_equal "bytes 2-5/10", response["content-range"]
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def test_returns_304_when_if_modified_since_matches_e2e
|
|
261
|
+
path = File.join(@dir, "x.txt")
|
|
262
|
+
File.write(path, "hi")
|
|
263
|
+
start_server
|
|
264
|
+
|
|
265
|
+
uri = URI("http://127.0.0.1:#{@server.port}/x.txt")
|
|
266
|
+
response = Net::HTTP.start(uri.host, uri.port) do |http|
|
|
267
|
+
http.get(uri.path, "If-Modified-Since" => File.mtime(path).httpdate)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
assert_equal "304", response.code
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def test_serves_over_tls
|
|
274
|
+
File.write(File.join(@dir, "x.txt"), "secret")
|
|
275
|
+
start_server(tls: build_ephemeral_tls)
|
|
276
|
+
|
|
277
|
+
response = Net::HTTP.start("127.0.0.1", @server.port, use_ssl: true,
|
|
278
|
+
verify_mode: OpenSSL::SSL::VERIFY_NONE) do |http|
|
|
279
|
+
http.get("/x.txt")
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
assert_equal "200", response.code
|
|
283
|
+
assert_equal "secret", response.body
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def test_logs_https_scheme_when_tls_enabled
|
|
287
|
+
out = StringIO.new
|
|
288
|
+
@server = Wsv::Server.new(host: "127.0.0.1", port: 0, root: @dir,
|
|
289
|
+
out: out, err: StringIO.new, tls: build_ephemeral_tls)
|
|
290
|
+
@server.send(:log_startup)
|
|
291
|
+
|
|
292
|
+
assert_includes out.string, "https://"
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def test_warns_about_self_signed_cert
|
|
296
|
+
err = StringIO.new
|
|
297
|
+
@server = Wsv::Server.new(host: "127.0.0.1", port: 0, root: @dir,
|
|
298
|
+
out: StringIO.new, err: err, tls: build_ephemeral_tls)
|
|
299
|
+
@server.send(:log_startup)
|
|
300
|
+
|
|
301
|
+
assert_includes err.string, "self-signed"
|
|
302
|
+
end
|
|
303
|
+
|
|
229
304
|
def test_unsupported_method
|
|
230
305
|
start_server
|
|
231
306
|
|
|
@@ -237,14 +312,15 @@ class ServerTest < Minitest::Test
|
|
|
237
312
|
|
|
238
313
|
private
|
|
239
314
|
|
|
240
|
-
def start_server(read_timeout: Wsv::Server::DEFAULT_READ_TIMEOUT)
|
|
315
|
+
def start_server(read_timeout: Wsv::Server::DEFAULT_READ_TIMEOUT, tls: nil)
|
|
241
316
|
@server = Wsv::Server.new(
|
|
242
317
|
host: "127.0.0.1",
|
|
243
318
|
port: free_port,
|
|
244
319
|
root: @dir,
|
|
245
320
|
out: StringIO.new,
|
|
246
321
|
err: StringIO.new,
|
|
247
|
-
read_timeout: read_timeout
|
|
322
|
+
read_timeout: read_timeout,
|
|
323
|
+
tls: tls
|
|
248
324
|
)
|
|
249
325
|
@thread = Thread.new { @server.start }
|
|
250
326
|
wait_until_ready
|
|
@@ -271,6 +347,12 @@ class ServerTest < Minitest::Test
|
|
|
271
347
|
end
|
|
272
348
|
end
|
|
273
349
|
|
|
350
|
+
def build_ephemeral_tls
|
|
351
|
+
key = OpenSSL::PKey::RSA.new(2048)
|
|
352
|
+
cert = Wsv::TlsContext::SelfSignedCert.build(key)
|
|
353
|
+
Wsv::TlsContext.new(cert: cert, key: key, ephemeral: true)
|
|
354
|
+
end
|
|
355
|
+
|
|
274
356
|
def free_port
|
|
275
357
|
server = TCPServer.new("127.0.0.1", 0)
|
|
276
358
|
server.addr[1]
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "test_helper"
|
|
4
|
+
|
|
5
|
+
class TlsContextTest < Minitest::Test
|
|
6
|
+
def test_to_ssl_context_returns_configured_ssl_context
|
|
7
|
+
key = OpenSSL::PKey::RSA.new(2048)
|
|
8
|
+
cert = Wsv::TlsContext::SelfSignedCert.build(key)
|
|
9
|
+
tls = Wsv::TlsContext.new(cert: cert, key: key)
|
|
10
|
+
|
|
11
|
+
ssl = tls.to_ssl_context
|
|
12
|
+
|
|
13
|
+
assert_kind_of OpenSSL::SSL::SSLContext, ssl
|
|
14
|
+
assert_equal cert, ssl.cert
|
|
15
|
+
assert_equal key, ssl.key
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_ephemeral_predicate
|
|
19
|
+
key = OpenSSL::PKey::RSA.new(2048)
|
|
20
|
+
cert = Wsv::TlsContext::SelfSignedCert.build(key)
|
|
21
|
+
|
|
22
|
+
refute_predicate Wsv::TlsContext.new(cert: cert, key: key), :ephemeral?
|
|
23
|
+
assert_predicate Wsv::TlsContext.new(cert: cert, key: key, ephemeral: true), :ephemeral?
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class SelfSignedCertTest < Minitest::Test
|
|
28
|
+
def test_subject_is_localhost
|
|
29
|
+
cert = build
|
|
30
|
+
|
|
31
|
+
assert_equal "/CN=localhost", cert.subject.to_s
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def test_san_includes_localhost_and_loopback
|
|
35
|
+
san = build.extensions.find { |ext| ext.oid == "subjectAltName" }.value
|
|
36
|
+
|
|
37
|
+
assert_includes san, "DNS:localhost"
|
|
38
|
+
assert_includes san, "IP Address:127.0.0.1"
|
|
39
|
+
assert_includes san, "IP Address:0:0:0:0:0:0:0:1"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def test_signed_with_provided_key
|
|
43
|
+
key = OpenSSL::PKey::RSA.new(2048)
|
|
44
|
+
cert = Wsv::TlsContext::SelfSignedCert.build(key)
|
|
45
|
+
|
|
46
|
+
assert cert.verify(key.public_key)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def build
|
|
52
|
+
Wsv::TlsContext::SelfSignedCert.build(OpenSSL::PKey::RSA.new(2048))
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class TlsContextResolverTest < Minitest::Test
|
|
57
|
+
def test_resolves_explicit_paths
|
|
58
|
+
Dir.mktmpdir do |dir|
|
|
59
|
+
cert_path, key_path = write_self_signed(dir)
|
|
60
|
+
|
|
61
|
+
tls = Wsv::TlsContext::Resolver.resolve(cert_path: cert_path, key_path: key_path)
|
|
62
|
+
|
|
63
|
+
refute_predicate tls, :ephemeral?
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def test_requires_both_cert_and_key
|
|
68
|
+
assert_raises(ArgumentError) do
|
|
69
|
+
Wsv::TlsContext::Resolver.resolve(cert_path: "/some/cert.pem", key_path: nil)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
assert_raises(ArgumentError) do
|
|
73
|
+
Wsv::TlsContext::Resolver.resolve(cert_path: nil, key_path: "/some/key.pem")
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def test_uses_xdg_when_present
|
|
78
|
+
Dir.mktmpdir do |xdg|
|
|
79
|
+
wsv_dir = File.join(xdg, "wsv")
|
|
80
|
+
FileUtils.mkdir_p(wsv_dir)
|
|
81
|
+
write_self_signed(wsv_dir, cert_name: "cert.pem", key_name: "key.pem")
|
|
82
|
+
|
|
83
|
+
with_env("XDG_CONFIG_HOME" => xdg) do
|
|
84
|
+
tls = Wsv::TlsContext::Resolver.resolve
|
|
85
|
+
|
|
86
|
+
refute_predicate tls, :ephemeral?
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def test_falls_back_to_ephemeral_when_no_xdg
|
|
92
|
+
Dir.mktmpdir do |xdg|
|
|
93
|
+
with_env("XDG_CONFIG_HOME" => xdg) do
|
|
94
|
+
tls = Wsv::TlsContext::Resolver.resolve
|
|
95
|
+
|
|
96
|
+
assert_predicate tls, :ephemeral?
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def test_errors_when_only_one_xdg_file_present
|
|
102
|
+
Dir.mktmpdir do |xdg|
|
|
103
|
+
wsv_dir = File.join(xdg, "wsv")
|
|
104
|
+
FileUtils.mkdir_p(wsv_dir)
|
|
105
|
+
File.write(File.join(wsv_dir, "cert.pem"), "stub")
|
|
106
|
+
|
|
107
|
+
with_env("XDG_CONFIG_HOME" => xdg) do
|
|
108
|
+
assert_raises(ArgumentError) { Wsv::TlsContext::Resolver.resolve }
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def test_raises_openssl_error_for_malformed_cert
|
|
114
|
+
Dir.mktmpdir do |dir|
|
|
115
|
+
_cert_path, key_path = write_self_signed(dir)
|
|
116
|
+
bad_cert = File.join(dir, "bad-cert.pem")
|
|
117
|
+
File.write(bad_cert, "this is not a PEM\n")
|
|
118
|
+
|
|
119
|
+
assert_raises(OpenSSL::X509::CertificateError) do
|
|
120
|
+
Wsv::TlsContext::Resolver.resolve(cert_path: bad_cert, key_path: key_path)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def test_raises_openssl_error_for_malformed_key
|
|
126
|
+
Dir.mktmpdir do |dir|
|
|
127
|
+
cert_path, _key_path = write_self_signed(dir)
|
|
128
|
+
bad_key = File.join(dir, "bad-key.pem")
|
|
129
|
+
File.write(bad_key, "this is not a PEM\n")
|
|
130
|
+
|
|
131
|
+
assert_raises(OpenSSL::PKey::PKeyError) do
|
|
132
|
+
Wsv::TlsContext::Resolver.resolve(cert_path: cert_path, key_path: bad_key)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def test_raises_argument_error_when_cert_and_key_do_not_match
|
|
138
|
+
Dir.mktmpdir do |dir|
|
|
139
|
+
cert_path, = write_self_signed(dir, cert_name: "a.pem", key_name: "a.key")
|
|
140
|
+
_, foreign_key = write_self_signed(dir, cert_name: "b.pem", key_name: "b.key")
|
|
141
|
+
|
|
142
|
+
err = assert_raises(ArgumentError) do
|
|
143
|
+
Wsv::TlsContext::Resolver.resolve(cert_path: cert_path, key_path: foreign_key)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
assert_includes err.message, "does not match"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
def write_self_signed(dir, cert_name: "test-cert.pem", key_name: "test-key.pem")
|
|
153
|
+
key = OpenSSL::PKey::RSA.new(2048)
|
|
154
|
+
cert = Wsv::TlsContext::SelfSignedCert.build(key)
|
|
155
|
+
cert_path = File.join(dir, cert_name)
|
|
156
|
+
key_path = File.join(dir, key_name)
|
|
157
|
+
File.write(cert_path, cert.to_pem)
|
|
158
|
+
File.write(key_path, key.to_pem)
|
|
159
|
+
[cert_path, key_path]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def with_env(overrides)
|
|
163
|
+
original = ENV.to_h
|
|
164
|
+
overrides.each { |k, v| ENV[k] = v }
|
|
165
|
+
yield
|
|
166
|
+
ensure
|
|
167
|
+
ENV.replace(original)
|
|
168
|
+
end
|
|
169
|
+
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: wsv
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- takahashim
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-05-
|
|
10
|
+
date: 2026-05-07 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
12
|
description: wsv serves a local directory over HTTP from a zero-config CLI.
|
|
13
13
|
email:
|
|
@@ -24,20 +24,35 @@ files:
|
|
|
24
24
|
- lib/wsv.rb
|
|
25
25
|
- lib/wsv/app.rb
|
|
26
26
|
- lib/wsv/cli.rb
|
|
27
|
+
- lib/wsv/cors.rb
|
|
27
28
|
- lib/wsv/mime_types.rb
|
|
28
29
|
- lib/wsv/path_resolver.rb
|
|
29
30
|
- lib/wsv/request.rb
|
|
31
|
+
- lib/wsv/request/parser.rb
|
|
32
|
+
- lib/wsv/request/too_large.rb
|
|
30
33
|
- lib/wsv/response.rb
|
|
34
|
+
- lib/wsv/response/file_body.rb
|
|
35
|
+
- lib/wsv/response/file_builder.rb
|
|
36
|
+
- lib/wsv/response/string_body.rb
|
|
37
|
+
- lib/wsv/response/text_builder.rb
|
|
31
38
|
- lib/wsv/server.rb
|
|
39
|
+
- lib/wsv/server/banner.rb
|
|
40
|
+
- lib/wsv/server/browser_launcher.rb
|
|
41
|
+
- lib/wsv/server/deadline_reader.rb
|
|
32
42
|
- lib/wsv/status.rb
|
|
43
|
+
- lib/wsv/tls_context.rb
|
|
44
|
+
- lib/wsv/tls_context/resolver.rb
|
|
45
|
+
- lib/wsv/tls_context/self_signed_cert.rb
|
|
33
46
|
- lib/wsv/version.rb
|
|
34
47
|
- test/app_test.rb
|
|
48
|
+
- test/browser_launcher_test.rb
|
|
35
49
|
- test/cli_test.rb
|
|
36
50
|
- test/path_resolver_test.rb
|
|
37
51
|
- test/request_test.rb
|
|
38
52
|
- test/response_test.rb
|
|
39
53
|
- test/server_test.rb
|
|
40
54
|
- test/test_helper.rb
|
|
55
|
+
- test/tls_context_test.rb
|
|
41
56
|
- wsv.gemspec
|
|
42
57
|
homepage: https://rubygems.org/gems/wsv
|
|
43
58
|
licenses:
|