summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--Makefile27
-rw-r--r--build.zig71
-rw-r--r--src/main.zig194
4 files changed, 295 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b6357e7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+# Ignore zig compiler generated dirs
+zig-cache/
+zig-out/
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..bcb7224
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,27 @@
+ZIG_DOPTIMIZE := -Doptimize=ReleaseSafe
+ZIG_DTARGET := -Dtarget=native
+ZIG_DCPU := -Dcpu=native
+
+all:
+ zig build ${ZIG_DOPTIMIZE} ${ZIG_DTARGET} ${ZIG_DCPU}
+
+debug:
+ zig build -Doptimize=Debug ${ZIG_DTARGET} ${ZIG_DCPU}
+
+fast:
+ zig build -Doptimize=ReleaseFast ${ZIG_DTARGET} ${ZIG_DCPU}
+
+safe:
+ zig build -Doptimize=ReleaseSafe ${ZIG_DTARGET} ${ZIG_DCPU}
+
+small:
+ zig build -Doptimize=ReleaseSmall ${ZIG_DTARGET} ${ZIG_DCPU}
+
+clean:
+ rm -rf zig-cache/ zig-out/
+
+run:
+ zig build run
+
+install: zig-out/bin/zhttpd
+ install -Dm755 zig-out/bin/zhttpd ~/.local/bin/zhttpd
diff --git a/build.zig b/build.zig
new file mode 100644
index 0000000..a02d270
--- /dev/null
+++ b/build.zig
@@ -0,0 +1,71 @@
+const std = @import("std");
+
+// Although this function looks imperative, note that its job is to
+// declaratively construct a build graph that will be executed by an external
+// runner.
+pub fn build(b: *std.Build) void {
+ // Standard target options allows the person running `zig build` to choose
+ // what target to build for. Here we do not override the defaults, which
+ // means any target is allowed, and the default is native. Other options
+ // for restricting supported target set are available.
+ const target = b.standardTargetOptions(.{});
+
+ // Standard optimization options allow the person running `zig build` to select
+ // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
+ // set a preferred release mode, allowing the user to decide how to optimize.
+ const optimize = b.standardOptimizeOption(.{});
+
+ const exe = b.addExecutable(.{
+ .name = "zhttpd",
+ // In this case the main source file is merely a path, however, in more
+ // complicated build scripts, this could be a generated file.
+ .root_source_file = .{ .path = "src/main.zig" },
+ .target = target,
+ .link_libc = true,
+ .optimize = optimize,
+ });
+
+ // This declares intent for the executable to be installed into the
+ // standard location when the user invokes the "install" step (the default
+ // step when running `zig build`).
+ b.installArtifact(exe);
+
+ // This *creates* a Run step in the build graph, to be executed when another
+ // step is evaluated that depends on it. The next line below will establish
+ // such a dependency.
+ const run_cmd = b.addRunArtifact(exe);
+
+ // By making the run step depend on the install step, it will be run from the
+ // installation directory rather than directly from within the cache directory.
+ // This is not necessary, however, if the application depends on other installed
+ // files, this ensures they will be present and in the expected location.
+ run_cmd.step.dependOn(b.getInstallStep());
+
+ // This allows the user to pass arguments to the application in the build
+ // command itself, like this: `zig build run -- arg1 arg2 etc`
+ if (b.args) |args| {
+ run_cmd.addArgs(args);
+ }
+
+ // This creates a build step. It will be visible in the `zig build --help` menu,
+ // and can be selected like this: `zig build run`
+ // This will evaluate the `run` step rather than the default, which is "install".
+ const run_step = b.step("run", "Run the app");
+ run_step.dependOn(&run_cmd.step);
+
+ // Creates a step for unit testing. This only builds the test executable
+ // but does not run it.
+ const unit_tests = b.addTest(.{
+ .root_source_file = .{ .path = "src/main.zig" },
+ .target = target,
+ .optimize = optimize,
+ });
+
+ const run_unit_tests = b.addRunArtifact(unit_tests);
+
+ // Similar to creating the run step earlier, this exposes a `test` step to
+ // the `zig build --help` menu, providing a way for the user to request
+ // running the unit tests.
+ const test_step = b.step("test", "Run unit tests");
+ test_step.dependOn(&run_unit_tests.step);
+}
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);
+ };
+}