//! zhttpd is a very basic http server written in Zig, by ZachIR. It is single //! threaded, and does not yet support every error code, nor does it support //! TLS/SSL, but it does display web pages. // Copyright (C) 2024 ZachIR // // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation; either version 2 of the License, or (at your option) // any later version. // // This program is distributed in the hope that it will be useful, but WITHOUT // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or // FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for // more details. // // You should have received a copy of the GNU General Public License along with // this program; if not, write to the Free Software Foundation, Inc., 51 // Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. const std = @import("std"); const log = std.log.scoped(.server); /// zhttpd relies on mime in addition to std, which is included as made by /// andrewrk at https://github.com/andrewrk/mime.git const mime = @import("mime/mime.zig"); /// The server has a hardcoded address and port of 127.0.0.1:8080 /// The path requested for a file has the limit of fs.MAX_PATH_BYTES, which is /// the maxmimum length for a file path in the OS /// The maximum file size is 1 << 21, aka (1 * 2^20), which is 2 MB. const server_addr = "127.0.0.1"; const server_port = 8080; const max_path_bytes = std.fs.MAX_PATH_BYTES; const buffer_limit = 1 << 21; /// readFiles() reads the file to a provided buffer, and returns the number of /// bytes read. /// readFiles() can fail from std.fmt.allocPrint(), std.fs.cwd().openFile(), /// file.stat(), and file.readAll() /// std.fmt.allocPrint() will return an AllocPrintError /// std.fs.cwd().openFile() will return a File.OpenError /// file.stat() will return a StatError /// file.readAll() will return a ReadError fn readFiles(target: []const u8, buffer: []u8) !usize { var fba_buffer: [max_path_bytes * 2]u8 = undefined; var fba = std.heap.FixedBufferAllocator.init(&fba_buffer); const allocator = fba.allocator(); var file_path = try allocator.alloc(u8, max_path_bytes); defer allocator.free(file_path); var cwd_buffer = [_]u8{0} ** max_path_bytes; const cwd = try std.os.getcwd(&cwd_buffer); file_path = try std.fmt.allocPrint(allocator, "{s}{s}", .{ cwd, target }); log.info("Loading file {s}...", .{file_path}); // Determine if the requested file exists const file = try std.fs.cwd().openFile(file_path, .{}); defer file.close(); var stat = try file.stat(); switch (stat.kind) { .directory => { if (std.mem.endsWith(u8, target, "/")) { file_path = try std.fmt.allocPrint(allocator, "{s}index.html", .{target}); } else { file_path = try std.fmt.allocPrint(allocator, "{s}/index.html", .{target}); } return readFiles(file_path, buffer); }, .file => { return try file.readAll(buffer); }, else => { return 0; }, } } /// handleRequest() handles the requests from the server and, if necessary, /// calls readFiles to read requested files. /// handleRequest() can fail from response.headers.append(), response.do(), /// response.writeAll(), response.finish(), and readFiles() /// response.headers.append() does not define what error types it will return /// response.do() does not define what error types it will return /// response.writeAll() will return a WriteError /// response.finish() will return a FinishError /// readFiles() does not define what error types it will return /// handleRequest handles the following status codes: /// - 200 OK /// - 403 Forbidden /// - 404 Not Found /// - 413 Payload Too Large /// - 414 URI Too Long fn handleRequest(response: *std.http.Server.Response) !void { // Log the request details log.info("{s} {s} {s}", .{ @tagName(response.request.method), @tagName(response.request.version), response.request.target }); // Create a []u8 to read up to buffer_limit characters var read = [_]u8{0} ** buffer_limit; // Set "connection" header to "keep-alive" if present in request headers if (response.request.headers.contains("connection")) { try response.headers.append("connection", "keep-alive"); } const size = readFiles(response.request.target, &read) catch |err| { switch (err) { error.AccessDenied => { response.status = .forbidden; }, error.FileNotFound => { response.status = .not_found; }, error.OutOfMemory => { response.status = .uri_too_long; }, else => { return err; }, } response.transfer_encoding = .{ .content_length = 0 }; try response.do(); return; }; if (size >= buffer_limit) { response.status = .payload_too_large; response.transfer_encoding = .{ .content_length = 0 }; try response.do(); return; } else if (size > 0) { // Get the file extension, and set the content-type header if it exists // (using mime.zig) // To do this, we iterate through response.request.target in reverse // looking for a '/' or a '.' // a '/' indicates a directory, so there is no extension // a '.' indicates a file extension var i: usize = response.request.target.len; var mime_type: ?mime.Type = undefined; if (std.mem.endsWith(u8, response.request.target, "/")) { log.warn("Dir requested, returning index.html!", .{}); try response.headers.append("content-type", "text/html"); } else { while (i > 0) { i -= 1; switch (response.request.target[i]) { '/' => { i = 0; break; }, '.' => { break; }, else => { continue; }, } } if (i <= 0) { log.warn("No extension detected!", .{}); if (std.mem.indexOf(u8, &read, " continue :outer, error.EndOfStream => continue, else => return err, }; // Process the request handleRequest(&response) catch |err| { response.status = .internal_server_error; response.transfer_encoding = .{ .content_length = 0 }; try response.do(); return err; }; } } } pub fn printInfo(stderr: std.fs.File.Writer) !void { try stderr.print("zhttpd version 0.1.0, Copyright (C) 2024 ZachIR\n", .{}); try stderr.print("zhttpd comes with ABSOLUTELY NO WARRANTY. This is ", .{}); try stderr.print("free software, and you are welcome to ", .{}); try stderr.print("redistribute it under certain conditions; see the ", .{}); try stderr.print("included LICENSE for more details.\n", .{}); } /// main() initializes the server, parses the IP and port, and begins /// runServer(). /// main() can fail exit from server.listen() and runServer(), which do not /// specify the error types they can return. pub fn main() !void { var fba_buffer: [@sizeOf(std.http.Server)]u8 = undefined; var fba = std.heap.FixedBufferAllocator.init(&fba_buffer); const allocator = fba.allocator(); //const stdout = std.io.getStdOut().writer(); const stderr = std.io.getStdErr().writer(); try printInfo(stderr); // Initialize the server var server = std.http.Server.init(allocator, .{ .reuse_address = true }); defer server.deinit(); // Log the server address and port try stderr.print("Server is running at {s}:{d}\n", .{ server_addr, server_port }); // Parse the server address const address = std.net.Address.parseIp(server_addr, server_port) catch unreachable; try server.listen(address); // Run the server runServer(&server) catch |err| { // Handle server errors log.err("server error: {}\n", .{err}); if (@errorReturnTrace()) |trace| { std.debug.dumpStackTrace(trace.*); } std.os.exit(1); }; }