diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/main.zig | 194 |
1 files changed, 194 insertions, 0 deletions
diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..dc762d1 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,194 @@ +const std = @import("std"); +const mime = @import("mime.zig"); + +const Address = std.net.Address; +const ArrayList = std.ArrayList; +const debug = std.debug; +const fmt = std.fmt; +const heap = std.heap; +const http = std.http; +const fs = std.fs; +const log = std.log.scoped(.server); +const mem = std.mem; +const bufferedReader = std.io.bufferedReader; +const getcwd = std.os.getcwd; + +const MAX_PATH_BYTES = fs.MAX_PATH_BYTES; + +const server_addr = "127.0.0.1"; +const server_port = 8080; + +const BUFFER_LIMIT = 1 << 21; + +const resp = "HTTP/1.0 200 OK\r\nServer: zhttpd\r\nContent-type: text/html\r\n\r\n"; +const def = "<html>Hello, World</html>"; + +fn read_files(target: []const u8, buffer: []u8, allocator: mem.Allocator) !usize { + 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 getcwd(&cwd_buffer); + file_path = try fmt.allocPrint(allocator, "{s}{s}", .{ cwd, target }); + log.info("Loading file {s}...", .{file_path}); + // Determine if the requested file exists + if (fs.cwd().openFile(file_path, .{})) |file| { + defer file.close(); + var stat = try file.stat(); + switch (stat.kind) { + .directory => { + if (mem.endsWith(u8, target, "/")) { + file_path = try fmt.allocPrint(allocator, "{s}index.html", .{target}); + } else { + file_path = try fmt.allocPrint(allocator, "{s}/index.html", .{target}); + } + return read_files(file_path, buffer, allocator); + }, + .file => { + return try file.readAll(buffer); + }, + else => { + return 0; + }, + } + } else |err| { + switch (err) { + error.FileNotFound => { + return 0; + }, + else => { + return err; + }, + } + } +} + +fn handle_request(response: *http.Server.Response, allocator: mem.Allocator) !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 = try read_files(response.request.target, &read, allocator); + 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 (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 (mem.indexOf(u8, &read, "<html")) |_| { + try response.headers.append("content-type", "text/html"); + } else { + try response.headers.append("content-type", "text/plain"); + } + } else { + log.info("Extension {s} detected!", .{response.request.target[i..]}); + mime_type = mime.extension_map.get(response.request.target[i..]); + if (mime_type) |mime_val| { + try response.headers.append("content-type", @tagName(mime_val)); + } else { + try response.headers.append("content-type", "text/plain"); + } + } + } + // Create a []u8 that is exactly the intended size and no larger + const body = read[0..size]; + log.info("{}", .{body.len}); + // Check if the request target contains "?chunked" + if (mem.indexOf(u8, response.request.target, "?chunked") != null) { + response.transfer_encoding = .chunked; + } else { + response.transfer_encoding = .{ .content_length = size }; + } + // Transmit the response + try response.do(); + if (response.request.method != .HEAD) { + try response.writeAll(body); + try response.finish(); + } + } else { + // If the file was not found, return error 404 + response.status = .not_found; + response.transfer_encoding = .{ .content_length = 0 }; + try response.do(); + } +} + +fn run_server(server: *http.Server, allocator: mem.Allocator) !void { + outer: while (true) { + // Accept incoming connection + var response = try server.accept(.{ + .allocator = allocator, + }); + defer response.deinit(); + + while (response.reset() != .closing) { + // Handle errors during request processing + response.wait() catch |err| switch (err) { + error.HttpHeadersInvalid => continue :outer, + error.EndOfStream => continue, + else => return err, + }; + + // Process the request + try handle_request(&response, allocator); + } + } +} + +pub fn main() !void { + // Define allocator + var gpa = heap.GeneralPurposeAllocator(.{}){}; + defer debug.assert(gpa.deinit() == .ok); + const allocator = gpa.allocator(); + + // Initialize the server + var server = http.Server.init(allocator, .{ .reuse_address = true }); + defer server.deinit(); + + // Log the server address and port + log.info("Server is running at {s}:{d}", .{ server_addr, server_port }); + + // Parse the server address + const address = Address.parseIp(server_addr, server_port) catch unreachable; + try server.listen(address); + + // Run the server + run_server(&server, allocator) catch |err| { + // Handle server errors + log.err("server error: {}\n", .{err}); + if (@errorReturnTrace()) |trace| { + debug.dumpStackTrace(trace.*); + } + std.os.exit(1); + }; +} |