wsv 0.10.0 → 0.11.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 +40 -0
- data/README.md +19 -13
- data/lib/wsv/app.rb +12 -56
- data/lib/wsv/cors.rb +7 -5
- data/lib/wsv/range_request.rb +91 -0
- data/lib/wsv/response/file_builder.rb +4 -9
- data/lib/wsv/server/banner.rb +3 -6
- data/lib/wsv/server/browser_launcher.rb +2 -8
- data/lib/wsv/server/connection.rb +104 -0
- data/lib/wsv/server/connection_throttle.rb +50 -0
- data/lib/wsv/server/url_host.rb +16 -0
- data/lib/wsv/server.rb +17 -107
- data/lib/wsv/version.rb +1 -1
- data/lib/wsv.rb +1 -0
- data/test/app_test.rb +12 -29
- data/test/banner_test.rb +59 -0
- data/test/cors_test.rb +76 -0
- data/test/range_request_test.rb +103 -0
- data/test/server_test.rb +67 -71
- data/test/test_helper.rb +8 -0
- data/wsv.gemspec +7 -6
- metadata +16 -8
- /data/{bin → exe}/wsv +0 -0
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
|
-
|
|
187
|
-
|
|
181
|
+
def test_414_carries_cors_header_when_cors_enabled
|
|
182
|
+
start_server(cors: true)
|
|
183
|
+
|
|
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
188
|
|
|
189
|
-
assert_includes
|
|
190
|
-
assert_includes
|
|
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
|
-
|
|
204
|
-
server.
|
|
206
|
+
def test_options_preflight_carries_cors_headers_when_cors_enabled
|
|
207
|
+
start_server(cors: true)
|
|
208
|
+
|
|
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
|
|
205
213
|
|
|
206
|
-
assert_includes
|
|
207
|
-
|
|
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,24 +304,6 @@ class ServerTest < Minitest::Test
|
|
|
283
304
|
assert_equal "secret", response.body
|
|
284
305
|
end
|
|
285
306
|
|
|
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
|
-
|
|
304
307
|
def test_unsupported_method
|
|
305
308
|
start_server
|
|
306
309
|
|
|
@@ -312,7 +315,7 @@ class ServerTest < Minitest::Test
|
|
|
312
315
|
|
|
313
316
|
private
|
|
314
317
|
|
|
315
|
-
def start_server(read_timeout: Wsv::Server::DEFAULT_READ_TIMEOUT, tls: nil)
|
|
318
|
+
def start_server(read_timeout: Wsv::Server::DEFAULT_READ_TIMEOUT, tls: nil, cors: false)
|
|
316
319
|
@server = Wsv::Server.new(
|
|
317
320
|
host: "127.0.0.1",
|
|
318
321
|
port: free_port,
|
|
@@ -320,39 +323,32 @@ class ServerTest < Minitest::Test
|
|
|
320
323
|
out: StringIO.new,
|
|
321
324
|
err: StringIO.new,
|
|
322
325
|
read_timeout: read_timeout,
|
|
323
|
-
tls: tls
|
|
326
|
+
tls: tls,
|
|
327
|
+
cors: cors
|
|
324
328
|
)
|
|
325
329
|
@thread = Thread.new { @server.start }
|
|
326
330
|
wait_until_ready
|
|
327
331
|
end
|
|
328
332
|
|
|
333
|
+
# Wrap accept_loop so the very first call to @server.accept raises a
|
|
334
|
+
# transient error. Avoids redefining Server#start, which would silently
|
|
335
|
+
# drift if start grew new steps (open_in_browser, etc.).
|
|
329
336
|
def inject_one_accept_error(server, error_class)
|
|
337
|
+
original_accept_loop = server.method(:accept_loop)
|
|
330
338
|
fired = false
|
|
331
|
-
server.define_singleton_method(:
|
|
332
|
-
|
|
333
|
-
original = @server.method(:accept)
|
|
339
|
+
server.define_singleton_method(:accept_loop) do
|
|
340
|
+
original_accept = @server.method(:accept)
|
|
334
341
|
@server.define_singleton_method(:accept) do
|
|
335
342
|
unless fired
|
|
336
343
|
fired = true
|
|
337
344
|
raise error_class, "injected"
|
|
338
345
|
end
|
|
339
|
-
|
|
346
|
+
original_accept.call
|
|
340
347
|
end
|
|
341
|
-
|
|
342
|
-
log_startup
|
|
343
|
-
trap_signals
|
|
344
|
-
accept_loop
|
|
345
|
-
ensure
|
|
346
|
-
close
|
|
348
|
+
original_accept_loop.call
|
|
347
349
|
end
|
|
348
350
|
end
|
|
349
351
|
|
|
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
352
|
def free_port
|
|
357
353
|
server = TCPServer.new("127.0.0.1", 0)
|
|
358
354
|
server.addr[1]
|
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
|
data/wsv.gemspec
CHANGED
|
@@ -8,15 +8,16 @@ Gem::Specification.new do |spec|
|
|
|
8
8
|
spec.authors = ["takahashim"]
|
|
9
9
|
spec.email = ["takahashimm@gmail.com"]
|
|
10
10
|
|
|
11
|
-
spec.summary = "A
|
|
12
|
-
spec.description = "wsv
|
|
13
|
-
|
|
11
|
+
spec.summary = "A zero-dependency static preview server for Ruby projects."
|
|
12
|
+
spec.description = "wsv is a Ruby CLI that previews a directory over HTTP/HTTPS. " \
|
|
13
|
+
"Stdlib-only, no runtime dependencies. Defensive by design: " \
|
|
14
|
+
"blocks dotfiles, binds to loopback, ships with TLS and CORS."
|
|
15
|
+
spec.homepage = "https://github.com/takahashim/wsv"
|
|
14
16
|
spec.license = "MIT"
|
|
15
17
|
spec.required_ruby_version = ">= 3.2"
|
|
16
18
|
|
|
17
19
|
spec.metadata = {
|
|
18
20
|
"homepage_uri" => spec.homepage,
|
|
19
|
-
"source_code_uri" => "https://github.com/takahashim/wsv",
|
|
20
21
|
"changelog_uri" => "https://github.com/takahashim/wsv/blob/main/CHANGELOG.md",
|
|
21
22
|
"rubygems_mfa_required" => "true"
|
|
22
23
|
}
|
|
@@ -25,12 +26,12 @@ Gem::Specification.new do |spec|
|
|
|
25
26
|
"CHANGELOG.md",
|
|
26
27
|
"LICENSE.txt",
|
|
27
28
|
"README.md",
|
|
28
|
-
"
|
|
29
|
+
"exe/wsv",
|
|
29
30
|
"lib/**/*.rb",
|
|
30
31
|
"test/**/*.rb",
|
|
31
32
|
"wsv.gemspec"
|
|
32
33
|
]
|
|
33
|
-
spec.bindir = "
|
|
34
|
+
spec.bindir = "exe"
|
|
34
35
|
spec.executables = ["wsv"]
|
|
35
36
|
spec.require_paths = ["lib"]
|
|
36
37
|
end
|
metadata
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: wsv
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- takahashim
|
|
8
|
-
bindir:
|
|
8
|
+
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 2026-05-07 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
|
-
description: wsv
|
|
12
|
+
description: 'wsv is a Ruby CLI that previews a directory over HTTP/HTTPS. Stdlib-only,
|
|
13
|
+
no runtime dependencies. Defensive by design: blocks dotfiles, binds to loopback,
|
|
14
|
+
ships with TLS and CORS.'
|
|
13
15
|
email:
|
|
14
16
|
- takahashimm@gmail.com
|
|
15
17
|
executables:
|
|
@@ -20,13 +22,14 @@ files:
|
|
|
20
22
|
- CHANGELOG.md
|
|
21
23
|
- LICENSE.txt
|
|
22
24
|
- README.md
|
|
23
|
-
-
|
|
25
|
+
- exe/wsv
|
|
24
26
|
- lib/wsv.rb
|
|
25
27
|
- lib/wsv/app.rb
|
|
26
28
|
- lib/wsv/cli.rb
|
|
27
29
|
- lib/wsv/cors.rb
|
|
28
30
|
- lib/wsv/mime_types.rb
|
|
29
31
|
- lib/wsv/path_resolver.rb
|
|
32
|
+
- lib/wsv/range_request.rb
|
|
30
33
|
- lib/wsv/request.rb
|
|
31
34
|
- lib/wsv/request/parser.rb
|
|
32
35
|
- lib/wsv/request/too_large.rb
|
|
@@ -38,28 +41,33 @@ files:
|
|
|
38
41
|
- lib/wsv/server.rb
|
|
39
42
|
- lib/wsv/server/banner.rb
|
|
40
43
|
- lib/wsv/server/browser_launcher.rb
|
|
44
|
+
- lib/wsv/server/connection.rb
|
|
45
|
+
- lib/wsv/server/connection_throttle.rb
|
|
41
46
|
- lib/wsv/server/deadline_reader.rb
|
|
47
|
+
- lib/wsv/server/url_host.rb
|
|
42
48
|
- lib/wsv/status.rb
|
|
43
49
|
- lib/wsv/tls_context.rb
|
|
44
50
|
- lib/wsv/tls_context/resolver.rb
|
|
45
51
|
- lib/wsv/tls_context/self_signed_cert.rb
|
|
46
52
|
- lib/wsv/version.rb
|
|
47
53
|
- test/app_test.rb
|
|
54
|
+
- test/banner_test.rb
|
|
48
55
|
- test/browser_launcher_test.rb
|
|
49
56
|
- test/cli_test.rb
|
|
57
|
+
- test/cors_test.rb
|
|
50
58
|
- test/path_resolver_test.rb
|
|
59
|
+
- test/range_request_test.rb
|
|
51
60
|
- test/request_test.rb
|
|
52
61
|
- test/response_test.rb
|
|
53
62
|
- test/server_test.rb
|
|
54
63
|
- test/test_helper.rb
|
|
55
64
|
- test/tls_context_test.rb
|
|
56
65
|
- wsv.gemspec
|
|
57
|
-
homepage: https://
|
|
66
|
+
homepage: https://github.com/takahashim/wsv
|
|
58
67
|
licenses:
|
|
59
68
|
- MIT
|
|
60
69
|
metadata:
|
|
61
|
-
homepage_uri: https://
|
|
62
|
-
source_code_uri: https://github.com/takahashim/wsv
|
|
70
|
+
homepage_uri: https://github.com/takahashim/wsv
|
|
63
71
|
changelog_uri: https://github.com/takahashim/wsv/blob/main/CHANGELOG.md
|
|
64
72
|
rubygems_mfa_required: 'true'
|
|
65
73
|
rdoc_options: []
|
|
@@ -78,5 +86,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
78
86
|
requirements: []
|
|
79
87
|
rubygems_version: 3.6.2
|
|
80
88
|
specification_version: 4
|
|
81
|
-
summary: A
|
|
89
|
+
summary: A zero-dependency static preview server for Ruby projects.
|
|
82
90
|
test_files: []
|
/data/{bin → exe}/wsv
RENAMED
|
File without changes
|