diff --git a/Userland/Services/WebServer/Client.cpp b/Userland/Services/WebServer/Client.cpp index bef0da16e4..081ed297c2 100644 --- a/Userland/Services/WebServer/Client.cpp +++ b/Userland/Services/WebServer/Client.cpp @@ -80,6 +80,15 @@ void Client::handle_request(ReadonlyBytes raw_request) return; } + // Check for credentials if they are required + if (Configuration::the().credentials().has_value()) { + bool has_authenticated = verify_credentials(request.headers()); + if (!has_authenticated) { + send_error_response(401, request, { "WWW-Authenticate: Basic realm=\"WebServer\", charset=\"UTF-8\"" }); + return; + } + } + auto requested_path = LexicalPath::join("/", request.resource()).string(); dbgln_if(WEBSERVER_DEBUG, "Canonical requested path: '{}'", requested_path); @@ -267,13 +276,20 @@ void Client::handle_directory_listing(String const& requested_path, String const send_response(stream, request, "text/html"); } -void Client::send_error_response(unsigned code, HTTP::HttpRequest const& request) +void Client::send_error_response(unsigned code, HTTP::HttpRequest const& request, Vector const& headers) { auto reason_phrase = HTTP::HttpResponse::reason_phrase_for_code(code); StringBuilder builder; builder.appendff("HTTP/1.0 {} ", code); builder.append(reason_phrase); - builder.append("\r\n\r\n"); + builder.append("\r\n"); + + for (auto& header : headers) { + builder.append(header); + builder.append("\r\n"); + } + + builder.append("\r\n"); builder.append("

"); builder.appendff("{} ", code); builder.append(reason_phrase); @@ -288,4 +304,18 @@ void Client::log_response(unsigned code, HTTP::HttpRequest const& request) outln("{} :: {:03d} :: {} {}", Core::DateTime::now().to_string(), code, request.method_name(), request.resource()); } +bool Client::verify_credentials(Vector const& headers) +{ + VERIFY(Configuration::the().credentials().has_value()); + auto& configured_credentials = Configuration::the().credentials().value(); + for (auto& header : headers) { + if (header.name.equals_ignoring_case("Authorization")) { + auto provided_credentials = HTTP::HttpRequest::parse_http_basic_authentication_header(header.value); + if (provided_credentials.has_value() && configured_credentials.username == provided_credentials->username && configured_credentials.password == provided_credentials->password) + return true; + } + } + return false; +} + } diff --git a/Userland/Services/WebServer/Client.h b/Userland/Services/WebServer/Client.h index 991f2ec649..5318776b46 100644 --- a/Userland/Services/WebServer/Client.h +++ b/Userland/Services/WebServer/Client.h @@ -24,10 +24,11 @@ private: void handle_request(ReadonlyBytes); void send_response(InputStream&, HTTP::HttpRequest const&, String const& content_type); void send_redirect(StringView redirect, HTTP::HttpRequest const&); - void send_error_response(unsigned code, HTTP::HttpRequest const&); + void send_error_response(unsigned code, HTTP::HttpRequest const&, Vector const& headers = {}); void die(); void log_response(unsigned code, HTTP::HttpRequest const&); void handle_directory_listing(String const& requested_path, String const& real_path, HTTP::HttpRequest const&); + bool verify_credentials(Vector const&); NonnullRefPtr m_socket; }; diff --git a/Userland/Services/WebServer/Configuration.h b/Userland/Services/WebServer/Configuration.h index f4581fabce..9b6fe7dd71 100644 --- a/Userland/Services/WebServer/Configuration.h +++ b/Userland/Services/WebServer/Configuration.h @@ -6,7 +6,9 @@ #pragma once +#include #include +#include namespace WebServer { @@ -15,13 +17,16 @@ public: Configuration(String root_path); String const& root_path() const { return m_root_path; } + Optional const& credentials() const { return m_credentials; } void set_root_path(String root_path) { m_root_path = move(root_path); } + void set_credentials(Optional credentials) { m_credentials = move(credentials); } static Configuration const& the(); private: String m_root_path; + Optional m_credentials; }; } diff --git a/Userland/Services/WebServer/main.cpp b/Userland/Services/WebServer/main.cpp index 299a95f199..c2eb7d2cc3 100644 --- a/Userland/Services/WebServer/main.cpp +++ b/Userland/Services/WebServer/main.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -19,14 +20,18 @@ int main(int argc, char** argv) { String default_listen_address = "0.0.0.0"; u16 default_port = 8000; - const char* root_path = "/www"; + String root_path = "/www"; String listen_address = default_listen_address; int port = default_port; + String username; + String password; Core::ArgsParser args_parser; args_parser.add_option(listen_address, "IP address to listen on", "listen-address", 'l', "listen_address"); args_parser.add_option(port, "Port to listen on", "port", 'p', "port"); + args_parser.add_option(username, "HTTP basic authentication username", "user", 'U', "username"); + args_parser.add_option(password, "HTTP basic authentication password", "pass", 'P', "password"); args_parser.add_positional_argument(root_path, "Path to serve the contents of", "path", Core::ArgsParser::Required::No); args_parser.parse(argc, argv); @@ -41,6 +46,11 @@ int main(int argc, char** argv) return 1; } + if (username.is_empty() != password.is_empty()) { + warnln("Both username and password are required for HTTP basic authentication."); + return 1; + } + auto real_root_path = Core::File::real_path_for(root_path); if (!Core::File::exists(real_root_path)) { @@ -55,6 +65,9 @@ int main(int argc, char** argv) WebServer::Configuration configuration(real_root_path); + if (!username.is_empty() && !password.is_empty()) + configuration.set_credentials(HTTP::HttpRequest::BasicAuthenticationCredentials { username, password }); + Core::EventLoop loop; auto server = Core::TCPServer::construct();