Serenity Operating System
at master 392 lines 14 kB view raw
1/* 2 * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> 3 * Copyright (c) 2021, Max Wipfli <mail@maxwipfli.ch> 4 * Copyright (c) 2022, Thomas Keppler <serenity@tkeppler.de> 5 * 6 * SPDX-License-Identifier: BSD-2-Clause 7 */ 8 9#include <AK/Base64.h> 10#include <AK/Debug.h> 11#include <AK/LexicalPath.h> 12#include <AK/MemoryStream.h> 13#include <AK/QuickSort.h> 14#include <AK/StringBuilder.h> 15#include <AK/URL.h> 16#include <LibCore/DateTime.h> 17#include <LibCore/DeprecatedFile.h> 18#include <LibCore/DirIterator.h> 19#include <LibCore/File.h> 20#include <LibCore/MappedFile.h> 21#include <LibCore/MimeData.h> 22#include <LibHTTP/HttpRequest.h> 23#include <LibHTTP/HttpResponse.h> 24#include <WebServer/Client.h> 25#include <WebServer/Configuration.h> 26#include <stdio.h> 27#include <sys/stat.h> 28#include <unistd.h> 29 30namespace WebServer { 31 32Client::Client(NonnullOwnPtr<Core::BufferedTCPSocket> socket, Core::Object* parent) 33 : Core::Object(parent) 34 , m_socket(move(socket)) 35{ 36} 37 38void Client::die() 39{ 40 m_socket->close(); 41 deferred_invoke([this] { remove_from_parent(); }); 42} 43 44void Client::start() 45{ 46 m_socket->on_ready_to_read = [this] { 47 StringBuilder builder; 48 49 auto maybe_buffer = ByteBuffer::create_uninitialized(m_socket->buffer_size()); 50 if (maybe_buffer.is_error()) { 51 warnln("Could not create buffer for client: {}", maybe_buffer.error()); 52 die(); 53 return; 54 } 55 56 auto buffer = maybe_buffer.release_value(); 57 for (;;) { 58 auto maybe_can_read = m_socket->can_read_without_blocking(); 59 if (maybe_can_read.is_error()) { 60 warnln("Failed to get the blocking status for the socket: {}", maybe_can_read.error()); 61 die(); 62 return; 63 } 64 65 if (!maybe_can_read.value()) 66 break; 67 68 auto maybe_bytes_read = m_socket->read_until_any_of(buffer, Array { "\r"sv, "\n"sv, "\r\n"sv }); 69 if (maybe_bytes_read.is_error()) { 70 warnln("Failed to read a line from the request: {}", maybe_bytes_read.error()); 71 die(); 72 return; 73 } 74 75 if (m_socket->is_eof()) { 76 die(); 77 break; 78 } 79 80 builder.append(StringView { maybe_bytes_read.value() }); 81 builder.append("\r\n"sv); 82 } 83 84 auto request = builder.to_byte_buffer().release_value_but_fixme_should_propagate_errors(); 85 dbgln_if(WEBSERVER_DEBUG, "Got raw request: '{}'", DeprecatedString::copy(request)); 86 87 auto maybe_did_handle = handle_request(request); 88 if (maybe_did_handle.is_error()) { 89 warnln("Failed to handle the request: {}", maybe_did_handle.error()); 90 } 91 92 die(); 93 }; 94} 95 96ErrorOr<bool> Client::handle_request(ReadonlyBytes raw_request) 97{ 98 auto request_or_error = HTTP::HttpRequest::from_raw_request(raw_request); 99 if (!request_or_error.has_value()) 100 return false; 101 auto& request = request_or_error.value(); 102 auto resource_decoded = URL::percent_decode(request.resource()); 103 104 if constexpr (WEBSERVER_DEBUG) { 105 dbgln("Got HTTP request: {} {}", request.method_name(), request.resource()); 106 for (auto& header : request.headers()) { 107 dbgln(" {} => {}", header.name, header.value); 108 } 109 } 110 111 if (request.method() != HTTP::HttpRequest::Method::GET) { 112 TRY(send_error_response(501, request)); 113 return false; 114 } 115 116 // Check for credentials if they are required 117 if (Configuration::the().credentials().has_value()) { 118 bool has_authenticated = verify_credentials(request.headers()); 119 if (!has_authenticated) { 120 auto const basic_auth_header = TRY("WWW-Authenticate: Basic realm=\"WebServer\", charset=\"UTF-8\""_string); 121 Vector<String> headers {}; 122 TRY(headers.try_append(basic_auth_header)); 123 TRY(send_error_response(401, request, move(headers))); 124 return false; 125 } 126 } 127 128 auto requested_path = TRY(String::from_deprecated_string(LexicalPath::join("/"sv, resource_decoded).string())); 129 dbgln_if(WEBSERVER_DEBUG, "Canonical requested path: '{}'", requested_path); 130 131 StringBuilder path_builder; 132 path_builder.append(Configuration::the().document_root_path()); 133 path_builder.append(requested_path); 134 auto real_path = TRY(path_builder.to_string()); 135 136 if (Core::DeprecatedFile::is_directory(real_path.bytes_as_string_view())) { 137 if (!resource_decoded.ends_with('/')) { 138 StringBuilder red; 139 140 red.append(requested_path); 141 red.append("/"sv); 142 143 TRY(send_redirect(red.to_deprecated_string(), request)); 144 return true; 145 } 146 147 StringBuilder index_html_path_builder; 148 index_html_path_builder.append(real_path); 149 index_html_path_builder.append("/index.html"sv); 150 auto index_html_path = TRY(index_html_path_builder.to_string()); 151 if (!Core::DeprecatedFile::exists(index_html_path)) { 152 TRY(handle_directory_listing(requested_path, real_path, request)); 153 return true; 154 } 155 real_path = index_html_path; 156 } 157 158 auto file = Core::DeprecatedFile::construct(real_path.bytes_as_string_view()); 159 if (!file->open(Core::OpenMode::ReadOnly)) { 160 TRY(send_error_response(404, request)); 161 return false; 162 } 163 164 if (file->is_device()) { 165 TRY(send_error_response(403, request)); 166 return false; 167 } 168 169 auto stream = TRY(Core::File::open(real_path.bytes_as_string_view(), Core::File::OpenMode::Read)); 170 171 auto const info = ContentInfo { 172 .type = TRY(String::from_utf8(Core::guess_mime_type_based_on_filename(real_path.bytes_as_string_view()))), 173 .length = TRY(Core::DeprecatedFile::size(real_path.bytes_as_string_view())) 174 }; 175 TRY(send_response(*stream, request, move(info))); 176 return true; 177} 178 179ErrorOr<void> Client::send_response(Stream& response, HTTP::HttpRequest const& request, ContentInfo content_info) 180{ 181 StringBuilder builder; 182 builder.append("HTTP/1.0 200 OK\r\n"sv); 183 builder.append("Server: WebServer (SerenityOS)\r\n"sv); 184 builder.append("X-Frame-Options: SAMEORIGIN\r\n"sv); 185 builder.append("X-Content-Type-Options: nosniff\r\n"sv); 186 builder.append("Pragma: no-cache\r\n"sv); 187 if (content_info.type == "text/plain") 188 builder.appendff("Content-Type: {}; charset=utf-8\r\n", content_info.type); 189 else 190 builder.appendff("Content-Type: {}\r\n", content_info.type); 191 builder.appendff("Content-Length: {}\r\n", content_info.length); 192 builder.append("\r\n"sv); 193 194 auto builder_contents = TRY(builder.to_byte_buffer()); 195 TRY(m_socket->write_until_depleted(builder_contents)); 196 log_response(200, request); 197 198 char buffer[PAGE_SIZE]; 199 do { 200 auto size = TRY(response.read_some({ buffer, sizeof(buffer) })).size(); 201 if (response.is_eof() && size == 0) 202 break; 203 204 ReadonlyBytes write_buffer { buffer, size }; 205 while (!write_buffer.is_empty()) { 206 auto nwritten = TRY(m_socket->write_some(write_buffer)); 207 208 if (nwritten == 0) { 209 dbgln("EEEEEE got 0 bytes written!"); 210 } 211 212 write_buffer = write_buffer.slice(nwritten); 213 } 214 } while (true); 215 216 auto keep_alive = false; 217 if (auto it = request.headers().find_if([](auto& header) { return header.name.equals_ignoring_ascii_case("Connection"sv); }); !it.is_end()) { 218 if (it->value.trim_whitespace().equals_ignoring_ascii_case("keep-alive"sv)) 219 keep_alive = true; 220 } 221 if (!keep_alive) 222 m_socket->close(); 223 224 return {}; 225} 226 227ErrorOr<void> Client::send_redirect(StringView redirect_path, HTTP::HttpRequest const& request) 228{ 229 StringBuilder builder; 230 builder.append("HTTP/1.0 301 Moved Permanently\r\n"sv); 231 builder.append("Location: "sv); 232 builder.append(redirect_path); 233 builder.append("\r\n"sv); 234 builder.append("\r\n"sv); 235 236 auto builder_contents = TRY(builder.to_byte_buffer()); 237 TRY(m_socket->write_until_depleted(builder_contents)); 238 239 log_response(301, request); 240 return {}; 241} 242 243static DeprecatedString folder_image_data() 244{ 245 static DeprecatedString cache; 246 if (cache.is_empty()) { 247 auto file = Core::MappedFile::map("/res/icons/16x16/filetype-folder.png"sv).release_value_but_fixme_should_propagate_errors(); 248 // FIXME: change to TRY() and make method fallible 249 cache = MUST(encode_base64(file->bytes())).to_deprecated_string(); 250 } 251 return cache; 252} 253 254static DeprecatedString file_image_data() 255{ 256 static DeprecatedString cache; 257 if (cache.is_empty()) { 258 auto file = Core::MappedFile::map("/res/icons/16x16/filetype-unknown.png"sv).release_value_but_fixme_should_propagate_errors(); 259 // FIXME: change to TRY() and make method fallible 260 cache = MUST(encode_base64(file->bytes())).to_deprecated_string(); 261 } 262 return cache; 263} 264 265ErrorOr<void> Client::handle_directory_listing(String const& requested_path, String const& real_path, HTTP::HttpRequest const& request) 266{ 267 StringBuilder builder; 268 269 builder.append("<!DOCTYPE html>\n"sv); 270 builder.append("<html>\n"sv); 271 builder.append("<head><meta charset=\"utf-8\">\n"sv); 272 builder.append("<title>Index of "sv); 273 builder.append(escape_html_entities(requested_path)); 274 builder.append("</title><style>\n"sv); 275 builder.append(".folder { width: 16px; height: 16px; background-image: url('data:image/png;base64,"sv); 276 builder.append(folder_image_data()); 277 builder.append("'); }\n"sv); 278 builder.append(".file { width: 16px; height: 16px; background-image: url('data:image/png;base64,"sv); 279 builder.append(file_image_data()); 280 builder.append("'); }\n"sv); 281 builder.append("</style></head><body>\n"sv); 282 builder.append("<h1>Index of "sv); 283 builder.append(escape_html_entities(requested_path)); 284 builder.append("</h1>\n"sv); 285 builder.append("<hr>\n"sv); 286 builder.append("<code><table>\n"sv); 287 288 Core::DirIterator dt(real_path.bytes_as_string_view()); 289 Vector<DeprecatedString> names; 290 while (dt.has_next()) 291 names.append(dt.next_path()); 292 quick_sort(names); 293 294 for (auto& name : names) { 295 StringBuilder path_builder; 296 path_builder.append(real_path); 297 path_builder.append('/'); 298 // NOTE: In the root directory of the webserver, ".." should be equal to ".", since we don't want 299 // the user to see e.g. the size of the parent directory (and it isn't unveiled, so stat fails). 300 if (requested_path == "/" && name == "..") 301 path_builder.append("."sv); 302 else 303 path_builder.append(name); 304 305 struct stat st; 306 memset(&st, 0, sizeof(st)); 307 int rc = stat(path_builder.to_deprecated_string().characters(), &st); 308 if (rc < 0) { 309 perror("stat"); 310 } 311 312 bool is_directory = S_ISDIR(st.st_mode); 313 314 builder.append("<tr>"sv); 315 builder.appendff("<td><div class=\"{}\"></div></td>", is_directory ? "folder" : "file"); 316 builder.append("<td><a href=\""sv); 317 builder.append(URL::percent_encode(name)); 318 // NOTE: For directories, we append a slash so we don't always hit the redirect case, 319 // which adds a slash anyways. 320 if (is_directory) 321 builder.append('/'); 322 builder.append("\">"sv); 323 builder.append(escape_html_entities(name)); 324 builder.append("</a></td><td>&nbsp;</td>"sv); 325 326 builder.appendff("<td>{:10}</td><td>&nbsp;</td>", st.st_size); 327 builder.append("<td>"sv); 328 builder.append(Core::DateTime::from_timestamp(st.st_mtime).to_deprecated_string()); 329 builder.append("</td>"sv); 330 builder.append("</tr>\n"sv); 331 } 332 333 builder.append("</table></code>\n"sv); 334 builder.append("<hr>\n"sv); 335 builder.append("<i>Generated by WebServer (SerenityOS)</i>\n"sv); 336 builder.append("</body>\n"sv); 337 builder.append("</html>\n"sv); 338 339 auto response = builder.to_deprecated_string(); 340 FixedMemoryStream stream { response.bytes() }; 341 return send_response(stream, request, { .type = TRY("text/html"_string), .length = response.length() }); 342} 343 344ErrorOr<void> Client::send_error_response(unsigned code, HTTP::HttpRequest const& request, Vector<String> const& headers) 345{ 346 auto reason_phrase = HTTP::HttpResponse::reason_phrase_for_code(code); 347 348 StringBuilder content_builder; 349 content_builder.append("<!DOCTYPE html><html><body><h1>"sv); 350 content_builder.appendff("{} ", code); 351 content_builder.append(reason_phrase); 352 content_builder.append("</h1></body></html>"sv); 353 354 StringBuilder header_builder; 355 header_builder.appendff("HTTP/1.0 {} ", code); 356 header_builder.append(reason_phrase); 357 header_builder.append("\r\n"sv); 358 359 for (auto& header : headers) { 360 header_builder.append(header); 361 header_builder.append("\r\n"sv); 362 } 363 header_builder.append("Content-Type: text/html; charset=UTF-8\r\n"sv); 364 header_builder.appendff("Content-Length: {}\r\n", content_builder.length()); 365 header_builder.append("\r\n"sv); 366 TRY(m_socket->write_until_depleted(TRY(header_builder.to_byte_buffer()))); 367 TRY(m_socket->write_until_depleted(TRY(content_builder.to_byte_buffer()))); 368 369 log_response(code, request); 370 return {}; 371} 372 373void Client::log_response(unsigned code, HTTP::HttpRequest const& request) 374{ 375 outln("{} :: {:03d} :: {} {}", Core::DateTime::now().to_deprecated_string(), code, request.method_name(), request.url().serialize().substring(1)); 376} 377 378bool Client::verify_credentials(Vector<HTTP::HttpRequest::Header> const& headers) 379{ 380 VERIFY(Configuration::the().credentials().has_value()); 381 auto& configured_credentials = Configuration::the().credentials().value(); 382 for (auto& header : headers) { 383 if (header.name.equals_ignoring_ascii_case("Authorization"sv)) { 384 auto provided_credentials = HTTP::HttpRequest::parse_http_basic_authentication_header(header.value); 385 if (provided_credentials.has_value() && configured_credentials.username == provided_credentials->username && configured_credentials.password == provided_credentials->password) 386 return true; 387 } 388 } 389 return false; 390} 391 392}