wsv 0.10.1 → 0.12.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 +57 -0
- data/README.md +9 -3
- data/lib/wsv/app.rb +12 -56
- data/lib/wsv/cli.rb +24 -4
- data/lib/wsv/cors.rb +7 -5
- data/lib/wsv/range_request.rb +91 -0
- data/lib/wsv/response/sse_body.rb +35 -0
- data/lib/wsv/response/sse_builder.rb +42 -0
- data/lib/wsv/response.rb +12 -0
- data/lib/wsv/server/access_log.rb +48 -0
- data/lib/wsv/server/connection.rb +59 -21
- data/lib/wsv/server.rb +22 -5
- data/lib/wsv/version.rb +1 -1
- data/lib/wsv.rb +1 -0
- data/test/access_log_test.rb +94 -0
- data/test/app_test.rb +23 -29
- data/test/banner_test.rb +74 -0
- data/test/cli_test.rb +72 -1
- data/test/cors_test.rb +76 -0
- data/test/custom_app_test.rb +128 -0
- data/test/path_resolver_test.rb +8 -0
- data/test/range_request_test.rb +124 -0
- data/test/server_test.rb +92 -70
- data/test/sse_test.rb +88 -0
- data/test/test_helper.rb +8 -0
- metadata +12 -2
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "test_helper"
|
|
4
|
+
|
|
5
|
+
class RangeRequestTest < Minitest::Test
|
|
6
|
+
def test_nil_header_is_full
|
|
7
|
+
result = Wsv::RangeRequest.parse(nil, 100)
|
|
8
|
+
|
|
9
|
+
assert_predicate result, :full?
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_empty_header_is_full
|
|
13
|
+
result = Wsv::RangeRequest.parse("", 100)
|
|
14
|
+
|
|
15
|
+
assert_predicate result, :full?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_unparseable_syntax_is_full
|
|
19
|
+
# Per RFC 7233 an unparseable Range is treated as if absent.
|
|
20
|
+
result = Wsv::RangeRequest.parse("garbage", 100)
|
|
21
|
+
|
|
22
|
+
assert_predicate result, :full?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def test_empty_range_is_full
|
|
26
|
+
# `bytes=-` matches the regex but yields no bounds; treat as absent.
|
|
27
|
+
result = Wsv::RangeRequest.parse("bytes=-", 100)
|
|
28
|
+
|
|
29
|
+
assert_predicate result, :full?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_bounded_range
|
|
33
|
+
result = Wsv::RangeRequest.parse("bytes=2-5", 100)
|
|
34
|
+
|
|
35
|
+
assert_predicate result, :partial?
|
|
36
|
+
assert_equal 2..5, result.bounds
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def test_open_range
|
|
40
|
+
result = Wsv::RangeRequest.parse("bytes=5-", 10)
|
|
41
|
+
|
|
42
|
+
assert_predicate result, :partial?
|
|
43
|
+
assert_equal 5..9, result.bounds
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def test_suffix_range
|
|
47
|
+
result = Wsv::RangeRequest.parse("bytes=-3", 10)
|
|
48
|
+
|
|
49
|
+
assert_predicate result, :partial?
|
|
50
|
+
assert_equal 7..9, result.bounds
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_suffix_larger_than_file_clamps_to_zero
|
|
54
|
+
result = Wsv::RangeRequest.parse("bytes=-99", 10)
|
|
55
|
+
|
|
56
|
+
assert_predicate result, :partial?
|
|
57
|
+
assert_equal 0..9, result.bounds
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def test_bounded_last_past_file_clamps_to_end
|
|
61
|
+
result = Wsv::RangeRequest.parse("bytes=5-99", 10)
|
|
62
|
+
|
|
63
|
+
assert_predicate result, :partial?
|
|
64
|
+
assert_equal 5..9, result.bounds
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def test_zero_byte_suffix_is_unsatisfiable
|
|
68
|
+
result = Wsv::RangeRequest.parse("bytes=-0", 10)
|
|
69
|
+
|
|
70
|
+
assert_predicate result, :unsatisfiable?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def test_suffix_against_empty_file_is_unsatisfiable
|
|
74
|
+
result = Wsv::RangeRequest.parse("bytes=-3", 0)
|
|
75
|
+
|
|
76
|
+
assert_predicate result, :unsatisfiable?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def test_open_range_past_file_is_unsatisfiable
|
|
80
|
+
result = Wsv::RangeRequest.parse("bytes=10-", 5)
|
|
81
|
+
|
|
82
|
+
assert_predicate result, :unsatisfiable?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def test_bounded_first_past_file_is_unsatisfiable
|
|
86
|
+
result = Wsv::RangeRequest.parse("bytes=10-20", 5)
|
|
87
|
+
|
|
88
|
+
assert_predicate result, :unsatisfiable?
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def test_inverted_bounded_range_is_unsatisfiable
|
|
92
|
+
result = Wsv::RangeRequest.parse("bytes=5-3", 100)
|
|
93
|
+
|
|
94
|
+
assert_predicate result, :unsatisfiable?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def test_bounded_range_at_exact_file_boundary
|
|
98
|
+
result = Wsv::RangeRequest.parse("bytes=0-9", 10)
|
|
99
|
+
|
|
100
|
+
assert_predicate result, :partial?
|
|
101
|
+
assert_equal(0..9, result.bounds)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def test_suffix_range_equal_to_file_size
|
|
105
|
+
result = Wsv::RangeRequest.parse("bytes=-10", 10)
|
|
106
|
+
|
|
107
|
+
assert_predicate result, :partial?
|
|
108
|
+
assert_equal(0..9, result.bounds)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def test_single_byte_range_at_last_position
|
|
112
|
+
result = Wsv::RangeRequest.parse("bytes=9-9", 10)
|
|
113
|
+
|
|
114
|
+
assert_predicate result, :partial?
|
|
115
|
+
assert_equal(9..9, result.bounds)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def test_multipart_range_is_full
|
|
119
|
+
# `bytes=0-2,5-7` doesn't match the single-range regex; treat as absent.
|
|
120
|
+
result = Wsv::RangeRequest.parse("bytes=0-2,5-7", 100)
|
|
121
|
+
|
|
122
|
+
assert_predicate result, :full?
|
|
123
|
+
end
|
|
124
|
+
end
|
data/test/server_test.rb
CHANGED
|
@@ -5,6 +5,8 @@ require "socket"
|
|
|
5
5
|
require_relative "test_helper"
|
|
6
6
|
|
|
7
7
|
class ServerTest < Minitest::Test
|
|
8
|
+
include TlsTestHelpers
|
|
9
|
+
|
|
8
10
|
def setup
|
|
9
11
|
@dir = Dir.mktmpdir
|
|
10
12
|
@server = nil
|
|
@@ -164,55 +166,74 @@ class ServerTest < Minitest::Test
|
|
|
164
166
|
socket&.close
|
|
165
167
|
end
|
|
166
168
|
|
|
167
|
-
def
|
|
168
|
-
|
|
169
|
-
start_server(read_timeout: 5)
|
|
170
|
-
|
|
171
|
-
slow_socket = TCPSocket.open("127.0.0.1", @server.port)
|
|
169
|
+
def test_408_carries_cors_header_when_cors_enabled
|
|
170
|
+
start_server(read_timeout: 0.1, cors: true)
|
|
172
171
|
|
|
173
|
-
|
|
174
|
-
response =
|
|
175
|
-
elapsed = Time.now - started
|
|
172
|
+
socket = TCPSocket.open("127.0.0.1", @server.port)
|
|
173
|
+
response = socket.read
|
|
176
174
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
assert_operator elapsed, :<, 1.0, "request should not be serialized behind slow client"
|
|
175
|
+
assert_includes response, "HTTP/1.1 408"
|
|
176
|
+
assert_includes response, "Access-Control-Allow-Origin: *"
|
|
180
177
|
ensure
|
|
181
|
-
|
|
178
|
+
socket&.close
|
|
182
179
|
end
|
|
183
180
|
|
|
184
|
-
def
|
|
185
|
-
|
|
186
|
-
server = Wsv::Server.new(host: "0.0.0.0", port: 0, root: @dir, out: StringIO.new, err: err)
|
|
187
|
-
server.send(:log_startup)
|
|
181
|
+
def test_414_carries_cors_header_when_cors_enabled
|
|
182
|
+
start_server(cors: true)
|
|
188
183
|
|
|
189
|
-
|
|
190
|
-
|
|
184
|
+
long_path = "/" + ("a" * 9000)
|
|
185
|
+
socket = TCPSocket.open("127.0.0.1", @server.port)
|
|
186
|
+
socket.write("GET #{long_path} HTTP/1.1\r\nHost: localhost\r\n\r\n")
|
|
187
|
+
response = socket.read
|
|
188
|
+
|
|
189
|
+
assert_includes response, "HTTP/1.1 414"
|
|
190
|
+
assert_includes response, "Access-Control-Allow-Origin: *"
|
|
191
|
+
ensure
|
|
192
|
+
socket&.close
|
|
191
193
|
end
|
|
192
194
|
|
|
193
|
-
def
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
195
|
+
def test_200_carries_cors_header_when_cors_enabled
|
|
196
|
+
File.write(File.join(@dir, "x.txt"), "hi")
|
|
197
|
+
start_server(cors: true)
|
|
198
|
+
|
|
199
|
+
response = get("/x.txt")
|
|
197
200
|
|
|
198
|
-
|
|
201
|
+
assert_equal "200", response.code
|
|
202
|
+
assert_equal "*", response["access-control-allow-origin"]
|
|
203
|
+
assert_equal "Origin", response["vary"]
|
|
199
204
|
end
|
|
200
205
|
|
|
201
|
-
def
|
|
202
|
-
|
|
203
|
-
server = Wsv::Server.new(host: "::1", port: 8000, root: @dir, out: out, err: StringIO.new)
|
|
204
|
-
server.send(:log_startup)
|
|
206
|
+
def test_options_preflight_carries_cors_headers_when_cors_enabled
|
|
207
|
+
start_server(cors: true)
|
|
205
208
|
|
|
206
|
-
|
|
207
|
-
|
|
209
|
+
socket = TCPSocket.open("127.0.0.1", @server.port)
|
|
210
|
+
socket.write("OPTIONS /x.txt HTTP/1.1\r\nHost: localhost\r\n" \
|
|
211
|
+
"Access-Control-Request-Method: GET\r\n\r\n")
|
|
212
|
+
response = socket.read
|
|
213
|
+
|
|
214
|
+
assert_includes response, "HTTP/1.1 204"
|
|
215
|
+
assert_includes response, "Access-Control-Allow-Origin: *"
|
|
216
|
+
assert_includes response, "Access-Control-Allow-Methods: GET, HEAD, OPTIONS"
|
|
217
|
+
assert_includes response, "Vary: Origin"
|
|
218
|
+
ensure
|
|
219
|
+
socket&.close
|
|
208
220
|
end
|
|
209
221
|
|
|
210
|
-
def
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
222
|
+
def test_slow_client_does_not_block_other_clients
|
|
223
|
+
File.write(File.join(@dir, "x.txt"), "ok")
|
|
224
|
+
start_server(read_timeout: 5)
|
|
225
|
+
|
|
226
|
+
slow_socket = TCPSocket.open("127.0.0.1", @server.port)
|
|
227
|
+
|
|
228
|
+
started = Time.now
|
|
229
|
+
response = get("/x.txt")
|
|
230
|
+
elapsed = Time.now - started
|
|
214
231
|
|
|
215
|
-
|
|
232
|
+
assert_equal "200", response.code
|
|
233
|
+
assert_equal "ok", response.body
|
|
234
|
+
assert_operator elapsed, :<, 1.0, "request should not be serialized behind slow client"
|
|
235
|
+
ensure
|
|
236
|
+
slow_socket&.close
|
|
216
237
|
end
|
|
217
238
|
|
|
218
239
|
def test_accept_loop_survives_transient_accept_error
|
|
@@ -272,7 +293,7 @@ class ServerTest < Minitest::Test
|
|
|
272
293
|
|
|
273
294
|
def test_serves_over_tls
|
|
274
295
|
File.write(File.join(@dir, "x.txt"), "secret")
|
|
275
|
-
start_server(tls:
|
|
296
|
+
start_server(tls: ephemeral_tls)
|
|
276
297
|
|
|
277
298
|
response = Net::HTTP.start("127.0.0.1", @server.port, use_ssl: true,
|
|
278
299
|
verify_mode: OpenSSL::SSL::VERIFY_NONE) do |http|
|
|
@@ -283,36 +304,44 @@ class ServerTest < Minitest::Test
|
|
|
283
304
|
assert_equal "secret", response.body
|
|
284
305
|
end
|
|
285
306
|
|
|
286
|
-
def
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
@server.send(:log_startup)
|
|
307
|
+
def test_unsupported_method
|
|
308
|
+
start_server
|
|
309
|
+
|
|
310
|
+
response = raw_request("POST / HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Length: 0\r\n\r\n")
|
|
291
311
|
|
|
292
|
-
assert_includes
|
|
312
|
+
assert_includes response, "HTTP/1.1 405 Method Not Allowed"
|
|
313
|
+
assert_includes response, "Allow: GET, HEAD"
|
|
293
314
|
end
|
|
294
315
|
|
|
295
|
-
def
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
@server.
|
|
316
|
+
def test_emits_access_log_by_default
|
|
317
|
+
File.write(File.join(@dir, "ok.txt"), "hello")
|
|
318
|
+
out = StringIO.new
|
|
319
|
+
@server = Wsv::Server.new(host: "127.0.0.1", port: free_port, root: @dir, out: out, err: StringIO.new)
|
|
320
|
+
@thread = Thread.new { @server.start }
|
|
321
|
+
wait_until_ready
|
|
300
322
|
|
|
301
|
-
|
|
323
|
+
get("/ok.txt")
|
|
324
|
+
|
|
325
|
+
assert_includes out.string, %("GET /ok.txt HTTP/1.1" 200 5)
|
|
302
326
|
end
|
|
303
327
|
|
|
304
|
-
def
|
|
305
|
-
|
|
328
|
+
def test_quiet_suppresses_access_log
|
|
329
|
+
File.write(File.join(@dir, "ok.txt"), "hello")
|
|
330
|
+
out = StringIO.new
|
|
331
|
+
@server = Wsv::Server.new(host: "127.0.0.1", port: free_port, root: @dir,
|
|
332
|
+
out: out, err: StringIO.new, quiet: true)
|
|
333
|
+
@thread = Thread.new { @server.start }
|
|
334
|
+
wait_until_ready
|
|
335
|
+
baseline = out.string.dup
|
|
306
336
|
|
|
307
|
-
|
|
337
|
+
get("/ok.txt")
|
|
308
338
|
|
|
309
|
-
|
|
310
|
-
assert_includes response, "Allow: GET, HEAD"
|
|
339
|
+
assert_equal baseline, out.string
|
|
311
340
|
end
|
|
312
341
|
|
|
313
342
|
private
|
|
314
343
|
|
|
315
|
-
def start_server(read_timeout: Wsv::Server::DEFAULT_READ_TIMEOUT, tls: nil)
|
|
344
|
+
def start_server(read_timeout: Wsv::Server::DEFAULT_READ_TIMEOUT, tls: nil, cors: false)
|
|
316
345
|
@server = Wsv::Server.new(
|
|
317
346
|
host: "127.0.0.1",
|
|
318
347
|
port: free_port,
|
|
@@ -320,39 +349,32 @@ class ServerTest < Minitest::Test
|
|
|
320
349
|
out: StringIO.new,
|
|
321
350
|
err: StringIO.new,
|
|
322
351
|
read_timeout: read_timeout,
|
|
323
|
-
tls: tls
|
|
352
|
+
tls: tls,
|
|
353
|
+
cors: cors
|
|
324
354
|
)
|
|
325
355
|
@thread = Thread.new { @server.start }
|
|
326
356
|
wait_until_ready
|
|
327
357
|
end
|
|
328
358
|
|
|
359
|
+
# Wrap accept_loop so the very first call to @server.accept raises a
|
|
360
|
+
# transient error. Avoids redefining Server#start, which would silently
|
|
361
|
+
# drift if start grew new steps (open_in_browser, etc.).
|
|
329
362
|
def inject_one_accept_error(server, error_class)
|
|
363
|
+
original_accept_loop = server.method(:accept_loop)
|
|
330
364
|
fired = false
|
|
331
|
-
server.define_singleton_method(:
|
|
332
|
-
|
|
333
|
-
original = @server.method(:accept)
|
|
365
|
+
server.define_singleton_method(:accept_loop) do
|
|
366
|
+
original_accept = @server.method(:accept)
|
|
334
367
|
@server.define_singleton_method(:accept) do
|
|
335
368
|
unless fired
|
|
336
369
|
fired = true
|
|
337
370
|
raise error_class, "injected"
|
|
338
371
|
end
|
|
339
|
-
|
|
372
|
+
original_accept.call
|
|
340
373
|
end
|
|
341
|
-
|
|
342
|
-
log_startup
|
|
343
|
-
trap_signals
|
|
344
|
-
accept_loop
|
|
345
|
-
ensure
|
|
346
|
-
close
|
|
374
|
+
original_accept_loop.call
|
|
347
375
|
end
|
|
348
376
|
end
|
|
349
377
|
|
|
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
|
-
|
|
356
378
|
def free_port
|
|
357
379
|
server = TCPServer.new("127.0.0.1", 0)
|
|
358
380
|
server.addr[1]
|
data/test/sse_test.rb
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "test_helper"
|
|
4
|
+
|
|
5
|
+
class SseBodyTest < Minitest::Test
|
|
6
|
+
def test_requires_a_block
|
|
7
|
+
assert_raises(ArgumentError) { Wsv::Response::SseBody.new }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def test_to_s_raises_not_implemented
|
|
11
|
+
body = Wsv::Response::SseBody.new { |io| io }
|
|
12
|
+
assert_raises(NotImplementedError) { body.to_s }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_bytesize_is_zero
|
|
16
|
+
body = Wsv::Response::SseBody.new { |io| io }
|
|
17
|
+
|
|
18
|
+
assert_equal 0, body.bytesize
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_write_to_invokes_producer_with_io
|
|
22
|
+
received_io = nil
|
|
23
|
+
body = Wsv::Response::SseBody.new { |io| received_io = io }
|
|
24
|
+
buffer = StringIO.new
|
|
25
|
+
body.write_to(buffer)
|
|
26
|
+
|
|
27
|
+
assert_equal buffer, received_io
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def test_producer_can_write_multiple_chunks
|
|
31
|
+
body = Wsv::Response::SseBody.new do |io|
|
|
32
|
+
io.write("data: hello\n\n")
|
|
33
|
+
io.write("data: world\n\n")
|
|
34
|
+
end
|
|
35
|
+
buffer = StringIO.new
|
|
36
|
+
body.write_to(buffer)
|
|
37
|
+
|
|
38
|
+
assert_equal "data: hello\n\ndata: world\n\n", buffer.string
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class ResponseSseTest < Minitest::Test
|
|
43
|
+
def test_sse_helper_returns_response_with_sse_defaults
|
|
44
|
+
response = Wsv::Response.sse { |io| io }
|
|
45
|
+
|
|
46
|
+
assert_equal 200, response.status
|
|
47
|
+
assert_equal "text/event-stream; charset=utf-8", response.headers["Content-Type"]
|
|
48
|
+
assert_equal "no-cache", response.headers["Cache-Control"]
|
|
49
|
+
assert_equal "no", response.headers["X-Accel-Buffering"]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def test_sse_helper_allows_custom_status
|
|
53
|
+
response = Wsv::Response.sse(status: 503) { |io| io }
|
|
54
|
+
|
|
55
|
+
assert_equal 503, response.status
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_sse_helper_merges_extra_headers
|
|
59
|
+
response = Wsv::Response.sse(headers: { "X-Custom" => "v" }) { |io| io }
|
|
60
|
+
|
|
61
|
+
assert_equal "v", response.headers["X-Custom"]
|
|
62
|
+
assert_equal "no-cache", response.headers["Cache-Control"]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def test_sse_helper_overrides_default_headers_when_supplied
|
|
66
|
+
response = Wsv::Response.sse(headers: { "Content-Type" => "application/x-ndjson" }) { |io| io }
|
|
67
|
+
|
|
68
|
+
assert_equal "application/x-ndjson", response.headers["Content-Type"]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_response_write_to_does_not_inject_content_length_for_sse
|
|
72
|
+
response = Wsv::Response.sse do |io|
|
|
73
|
+
io.write("data: hi\n\n")
|
|
74
|
+
end
|
|
75
|
+
buffer = StringIO.new
|
|
76
|
+
response.write_to(buffer)
|
|
77
|
+
|
|
78
|
+
refute_match(/^Content-Length:/i, buffer.string)
|
|
79
|
+
assert_match(%r{^Content-Type: text/event-stream}i, buffer.string)
|
|
80
|
+
assert_match(/data: hi\n\n\z/, buffer.string)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def test_bytesize_of_sse_response_is_zero
|
|
84
|
+
response = Wsv::Response.sse { |io| io }
|
|
85
|
+
|
|
86
|
+
assert_equal 0, response.bytesize
|
|
87
|
+
end
|
|
88
|
+
end
|
data/test/test_helper.rb
CHANGED
|
@@ -7,3 +7,11 @@ require "fileutils"
|
|
|
7
7
|
require "stringio"
|
|
8
8
|
require "tmpdir"
|
|
9
9
|
require "wsv"
|
|
10
|
+
|
|
11
|
+
module TlsTestHelpers
|
|
12
|
+
def ephemeral_tls
|
|
13
|
+
key = OpenSSL::PKey::RSA.new(2048)
|
|
14
|
+
cert = Wsv::TlsContext::SelfSignedCert.build(key)
|
|
15
|
+
Wsv::TlsContext.new(cert: cert, key: key, ephemeral: true)
|
|
16
|
+
end
|
|
17
|
+
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.12.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- takahashim
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-05-
|
|
10
|
+
date: 2026-05-16 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
12
|
description: 'wsv is a Ruby CLI that previews a directory over HTTP/HTTPS. Stdlib-only,
|
|
13
13
|
no runtime dependencies. Defensive by design: blocks dotfiles, binds to loopback,
|
|
@@ -29,15 +29,19 @@ files:
|
|
|
29
29
|
- lib/wsv/cors.rb
|
|
30
30
|
- lib/wsv/mime_types.rb
|
|
31
31
|
- lib/wsv/path_resolver.rb
|
|
32
|
+
- lib/wsv/range_request.rb
|
|
32
33
|
- lib/wsv/request.rb
|
|
33
34
|
- lib/wsv/request/parser.rb
|
|
34
35
|
- lib/wsv/request/too_large.rb
|
|
35
36
|
- lib/wsv/response.rb
|
|
36
37
|
- lib/wsv/response/file_body.rb
|
|
37
38
|
- lib/wsv/response/file_builder.rb
|
|
39
|
+
- lib/wsv/response/sse_body.rb
|
|
40
|
+
- lib/wsv/response/sse_builder.rb
|
|
38
41
|
- lib/wsv/response/string_body.rb
|
|
39
42
|
- lib/wsv/response/text_builder.rb
|
|
40
43
|
- lib/wsv/server.rb
|
|
44
|
+
- lib/wsv/server/access_log.rb
|
|
41
45
|
- lib/wsv/server/banner.rb
|
|
42
46
|
- lib/wsv/server/browser_launcher.rb
|
|
43
47
|
- lib/wsv/server/connection.rb
|
|
@@ -49,13 +53,19 @@ files:
|
|
|
49
53
|
- lib/wsv/tls_context/resolver.rb
|
|
50
54
|
- lib/wsv/tls_context/self_signed_cert.rb
|
|
51
55
|
- lib/wsv/version.rb
|
|
56
|
+
- test/access_log_test.rb
|
|
52
57
|
- test/app_test.rb
|
|
58
|
+
- test/banner_test.rb
|
|
53
59
|
- test/browser_launcher_test.rb
|
|
54
60
|
- test/cli_test.rb
|
|
61
|
+
- test/cors_test.rb
|
|
62
|
+
- test/custom_app_test.rb
|
|
55
63
|
- test/path_resolver_test.rb
|
|
64
|
+
- test/range_request_test.rb
|
|
56
65
|
- test/request_test.rb
|
|
57
66
|
- test/response_test.rb
|
|
58
67
|
- test/server_test.rb
|
|
68
|
+
- test/sse_test.rb
|
|
59
69
|
- test/test_helper.rb
|
|
60
70
|
- test/tls_context_test.rb
|
|
61
71
|
- wsv.gemspec
|