module SimpleCov::CLI::Serve
def announce(stdout, server, dir)
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
def drain_headers(client)
def drain_headers(client) loop { break if client.readline.strip.empty? } end
def error(stderr, message)
def error(stderr, message) stderr.puts("simplecov serve: #{message}") 1 end
def handle_connection(client, root)
writes a status response. Wide rescue so a misbehaving client
Reads one HTTP request line, drains headers, serves the file or
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
def inside?(path, root)
def inside?(path, root) path == root || path.start_with?(root + File::SEPARATOR) end
def parse(args)
def parse(args) opts = {port: 0, host: "127.0.0.1"} #: Hash[Symbol, untyped] 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
def resolve(request_path, root)
a traversal attempt (including symlinks that escape root), or
Returns the absolute path of the file to serve, :forbidden for
def resolve(request_path, root) path = request_path.split("?", 2).first.to_s.sub(%r{^/}, "") absolute_root = File.realpath(root) candidate = File.expand_path(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
def respond(client, status, body = "", content_type = "text/plain")
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
def run(args, stdout:, stderr:, **)
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" with_server(opts) do |server| announce(stdout, server, dir) serve_loop(server, dir, stdout) 0 end end
def serve_loop(server, dir, stdout)
def serve_loop(server, dir, stdout) loop { handle_connection(server.accept, dir) } rescue Interrupt stdout.puts("\nsimplecov serve: stopping") end
def with_server(opts)
def with_server(opts) # The receiver cast works around an rbs stdlib gap: TCPSocket's # explicit `self.new` shadows TCPServer#initialize's (host, port) form. server = (_ = TCPServer).new(opts[:host], opts[:port]) #: TCPServer begin yield server ensure server.close end end