Serenity Operating System
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> </td>"sv);
325
326 builder.appendff("<td>{:10}</td><td> </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}