Module: SimpleCov::CLI::Serve::StaticFileHandler
- Defined in:
- lib/simplecov/cli/serve/static_file_handler.rb
Overview
The HTTP mechanics behind simplecov serve: reads one request
per connection and answers it from the report directory, with
traversal-safe path resolution. Kept apart from the CLI wiring
(option parsing, binding, the accept loop) in serve.rb.
Constant Summary collapse
- MIME =
{ ".html" => "text/html; charset=utf-8", ".htm" => "text/html; charset=utf-8", ".css" => "text/css", ".js" => "application/javascript", ".json" => "application/json", ".svg" => "image/svg+xml", ".png" => "image/png", ".gif" => "image/gif", ".jpg" => "image/jpeg", ".jpeg" => "image/jpeg", ".ico" => "image/x-icon", ".txt" => "text/plain; charset=utf-8" }.freeze
- STATUS_TEXT =
{200 => "OK", 400 => "Bad Request", 403 => "Forbidden", 404 => "Not Found", 405 => "Method Not Allowed"}.freeze
- READ_TIMEOUT =
Seconds a connection may sit idle mid-request before its reads raise IO::TimeoutError and the connection is dropped.
5
Class Method Summary collapse
- .drain_headers(client) ⇒ Object
-
.handle_connection(client, root) ⇒ Object
Reads one HTTP request line, drains headers, serves the file or writes a status response.
- .inside?(path, root) ⇒ Boolean
-
.resolve(request_path, root) ⇒ Object
Returns the absolute path of the file to serve, :forbidden for a traversal attempt (including symlinks that escape root), or nil for "not found".
- .respond(client, status, body = "", content_type = "text/plain") ⇒ Object
- .serve_file(client, path, root) ⇒ Object
Class Method Details
.drain_headers(client) ⇒ Object
73 74 75 |
# File 'lib/simplecov/cli/serve/static_file_handler.rb', line 73 def drain_headers(client) loop { break if client.readline.strip.empty? } end |
.handle_connection(client, root) ⇒ Object
Reads one HTTP request line, drains headers, serves the file or writes a status response. Wide rescue so a misbehaving client can't crash the server. The read timeout keeps idle connections from pinning their threads forever.
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
# File 'lib/simplecov/cli/serve/static_file_handler.rb', line 37 def handle_connection(client, root) # simplecov:disable branch — support for IO#timeout= is fixed by the engine # JRuby and TruffleRuby don't implement IO#timeout=. Without # the guard the NoMethodError lands in the wide rescue below # and every connection closes with an empty response. On those # engines an idle connection pins its thread instead of timing # out, which only leaks a thread in an interactive dev server. client.timeout = READ_TIMEOUT if client.respond_to?(:timeout=) # simplecov:enable branch method, path = client.readline.split drain_headers(client) # A request line without both tokens used to raise on # `path.split` inside the wide rescue, closing the connection # with an empty response instead of the 400 defined below. return respond(client, 400) if path.nil? return respond(client, 405) unless method == "GET" serve_file(client, path, root) rescue StandardError # Misbehaving clients (truncated requests, connection resets, # invalid encoding) shouldn't take the whole server down. nil ensure # simplecov:disable — `client` is the parameter, never nil here; # the `&.` is purely defensive in case of future refactors client&.close # simplecov:enable end |
.inside?(path, root) ⇒ Boolean
108 109 110 |
# File 'lib/simplecov/cli/serve/static_file_handler.rb', line 108 def inside?(path, root) path == root || path.start_with?(root + File::SEPARATOR) end |
.resolve(request_path, root) ⇒ Object
Returns the absolute path of the file to serve, :forbidden for a traversal attempt (including symlinks that escape root), or nil for "not found".
The request path is deliberately NOT percent-decoded: filenames
needing escapes don't occur in generated reports, and keeping
%2e%2e%2f as literal bytes is part of the traversal defense.
If decoding is ever added, it must happen BEFORE the inside?
check below.
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
# File 'lib/simplecov/cli/serve/static_file_handler.rb', line 86 def resolve(request_path, root) path = request_path.split("?", 2).first.to_s.sub(%r{^/}, "") absolute_root = File.realpath(root) candidate = File.(path.empty? ? "index.html" : path, absolute_root) # Reject `..` traversal and absolute-path attempts before # touching disk so they're 403, not 404. return :forbidden unless inside?(candidate, absolute_root) candidate = File.join(candidate, "index.html") if File.directory?(candidate) return nil unless File.file?(candidate) # Resolve symlinks last and re-check: a file inside root could # be a symlink pointing outside (e.g. /etc/passwd). real = File.realpath(candidate) inside?(real, absolute_root) ? real : :forbidden rescue Errno::ENOENT # simplecov:disable — TOCTOU: candidate vanished between # File.file? and File.realpath. Treat as "not found". nil # simplecov:enable end |
.respond(client, status, body = "", content_type = "text/plain") ⇒ Object
112 113 114 115 116 117 118 |
# File 'lib/simplecov/cli/serve/static_file_handler.rb', line 112 def respond(client, status, body = "", content_type = "text/plain") client.write("HTTP/1.1 #{status} #{STATUS_TEXT[status] || 'Error'}\r\n", "Content-Type: #{content_type || 'application/octet-stream'}\r\n", "Content-Length: #{body.bytesize}\r\n", "Connection: close\r\n\r\n") client.write(body) end |
.serve_file(client, path, root) ⇒ Object
66 67 68 69 70 71 |
# File 'lib/simplecov/cli/serve/static_file_handler.rb', line 66 def serve_file(client, path, root) file = resolve(path, root) return respond(client, file == :forbidden ? 403 : 404) unless file.is_a?(String) respond(client, 200, File.binread(file), MIME[File.extname(file).downcase]) end |