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.
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 test_slow_client_does_not_block_other_clients
168
- File.write(File.join(@dir, "x.txt"), "ok")
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
- started = Time.now
174
- response = get("/x.txt")
175
- elapsed = Time.now - started
172
+ socket = TCPSocket.open("127.0.0.1", @server.port)
173
+ response = socket.read
176
174
 
177
- assert_equal "200", response.code
178
- assert_equal "ok", response.body
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
- slow_socket&.close
178
+ socket&.close
182
179
  end
183
180
 
184
- def test_warns_when_binding_to_non_loopback
185
- err = StringIO.new
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)
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 err.string, "WARNING"
190
- assert_includes err.string, "0.0.0.0"
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 test_no_warning_for_loopback_bind
194
- err = StringIO.new
195
- server = Wsv::Server.new(host: "127.0.0.1", port: 0, root: @dir, out: StringIO.new, err: err)
196
- server.send(:log_startup)
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
- refute_includes err.string, "WARNING"
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 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)
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 out.string, "http://[::1]:8000/"
207
- refute_includes out.string, "http://::1:8000/"
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 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)
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
- assert_includes out.string, "http://[fe80::1%25eth0]:8000/"
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: build_ephemeral_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(:start) do
332
- @server = TCPServer.new(host, port)
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
- original.call
346
+ original_accept.call
340
347
  end
341
- @running = true
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 tiny static web server for local previews."
12
- spec.description = "wsv serves a local directory over HTTP from a zero-config CLI."
13
- spec.homepage = "https://rubygems.org/gems/wsv"
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
- "bin/wsv",
29
+ "exe/wsv",
29
30
  "lib/**/*.rb",
30
31
  "test/**/*.rb",
31
32
  "wsv.gemspec"
32
33
  ]
33
- spec.bindir = "bin"
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.10.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - takahashim
8
- bindir: bin
8
+ bindir: exe
9
9
  cert_chain: []
10
10
  date: 2026-05-07 00:00:00.000000000 Z
11
11
  dependencies: []
12
- description: wsv serves a local directory over HTTP from a zero-config CLI.
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
- - bin/wsv
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://rubygems.org/gems/wsv
66
+ homepage: https://github.com/takahashim/wsv
58
67
  licenses:
59
68
  - MIT
60
69
  metadata:
61
- homepage_uri: https://rubygems.org/gems/wsv
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 tiny static web server for local previews.
89
+ summary: A zero-dependency static preview server for Ruby projects.
82
90
  test_files: []
/data/{bin → exe}/wsv RENAMED
File without changes