Module: SimpleCov::CLI::Serve
| Relationships & Source Files | |
| Defined in: | lib/simplecov/cli/serve.rb |
Overview
simplecov serve [--port N] [--host HOST] — serve the coverage
report over HTTP. A 30-line static file server backed by stdlib
socket, so there's no extra dependency just for "view a local
report on a CI box where file:// doesn't work."
Constant Summary
-
MIME =
# File 'lib/simplecov/cli/serve.rb', line 12{ ".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 =
# File 'lib/simplecov/cli/serve.rb', line 26{ 200 => "OK", 400 => "Bad Request", 403 => "Forbidden", 404 => "Not Found", 405 => "Method Not Allowed" }.freeze
Class Method Summary
- .announce(stdout, server, dir) mod_func
- .drain_headers(client) mod_func
- .error(stderr, message) mod_func
-
.handle_connection(client, root)
mod_func
Reads one HTTP request line, drains headers, serves the file or writes a status response.
- .inside?(path, root) ⇒ Boolean mod_func
- .parse(args) mod_func
-
.resolve(request_path, root)
mod_func
Returns the absolute path of the file to serve,
:forbiddenfor a traversal attempt (including symlinks that escape root), or nil for "not found". - .respond(client, status, body = "", content_type = "text/plain") mod_func
- .run(args, stdout:, stderr:) mod_func
- .serve_loop(server, dir, stdout) mod_func
Class Method Details
.announce(stdout, server, dir) (mod_func)
[ GitHub ]# File 'lib/simplecov/cli/serve.rb', line 56
def announce(stdout, server, dir) port = server.addr[1] host = server.addr[3] stdout.puts("simplecov serve: serving #{dir} at http://#{host}:#{port}/") stdout.puts("Press Ctrl-C to stop.") end
.drain_headers(client) (mod_func)
[ GitHub ]# File 'lib/simplecov/cli/serve.rb', line 92
def drain_headers(client) loop { break if client.readline.strip.empty? } end
.error(stderr, message) (mod_func)
[ GitHub ]# File 'lib/simplecov/cli/serve.rb', line 133
def error(stderr, ) stderr.puts("simplecov serve: #{}") 1 end
.handle_connection(client, root) (mod_func)
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.
# File 'lib/simplecov/cli/serve.rb', line 72
def handle_connection(client, root) method, path = client.readline.split drain_headers(client) return respond(client, 405) unless method == "GET" 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]) 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 (mod_func)
.parse(args) (mod_func)
[ GitHub ]# File 'lib/simplecov/cli/serve.rb', line 47
def parse(args) opts = {port: 0, host: "127.0.0.1"} OptionParser.new do |o| o.on("--port N", Integer) { |v| opts[:port] = v } o.on("--host HOST") { |v| opts[:host] = v } end.parse(args) opts end
.resolve(request_path, root) (mod_func)
Returns the absolute path of the file to serve, :forbidden for
a traversal attempt (including symlinks that escape root), or
nil for "not found".
# File 'lib/simplecov/cli/serve.rb', line 99
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") (mod_func)
[ GitHub ]# File 'lib/simplecov/cli/serve.rb', line 125
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
.run(args, stdout:, stderr:) (mod_func)
[ GitHub ]# File 'lib/simplecov/cli/serve.rb', line 33
def run(args, stdout:, stderr:, **) opts = parse(args) dir = SimpleCov::CLI.coverage_dir return error(stderr, "#{dir} doesn't exist; run your test suite first") unless File.directory?(dir) require "socket" server = TCPServer.new(opts[:host], opts[:port]) announce(stdout, server, dir) serve_loop(server, dir, stdout) 0 ensure server&.close end
.serve_loop(server, dir, stdout) (mod_func)
[ GitHub ]# File 'lib/simplecov/cli/serve.rb', line 63
def serve_loop(server, dir, stdout) loop { handle_connection(server.accept, dir) } rescue Interrupt stdout.puts("\nsimplecov serve: stopping") end