This commit is contained in:
2026-05-29 09:51:27 +12:00
commit 0853d10684
5 changed files with 426 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.zig-cache/
zig-out/
zig-pkg/

0
README.md Normal file
View File

44
build.zig Normal file
View File

@@ -0,0 +1,44 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const iface = b.dependency("interface_helper", .{
.target = target,
.optimize = optimize,
});
const exe = b.addExecutable(.{
.name = "zig_webserver",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{},
}),
});
exe.root_module.addImport("interface", iface.module("interface"));
b.installArtifact(exe);
const run_step = b.step("run", "Run the app");
const run_cmd = b.addRunArtifact(exe);
run_step.dependOn(&run_cmd.step);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const exe_tests = b.addTest(.{
.root_module = exe.root_module,
});
const run_exe_tests = b.addRunArtifact(exe_tests);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_exe_tests.step);
}

48
build.zig.zon Normal file
View File

@@ -0,0 +1,48 @@
.{
// This is the default name used by packages depending on this one. For
// example, when a user runs `zig fetch --save <url>`, this field is used
// as the key in the `dependencies` table. Although the user can choose a
// different name, most users will stick with this provided value.
//
// It is redundant to include "zig" in this name because it is already
// within the Zig package namespace.
.name = .zig_webserver,
// This is a [Semantic Version](https://semver.org/).
// In a future version of Zig it will be used for package deduplication.
.version = "0.0.0",
// Together with name, this represents a globally unique package
// identifier. This field is generated by the Zig toolchain when the
// package is first created, and then *never changes*. This allows
// unambiguous detection of one package being an updated version of
// another.
//
// When forking a Zig project, this id should be regenerated (delete the
// field and run `zig build`) if the upstream project is still maintained.
// Otherwise, the fork is *hostile*, attempting to take control over the
// original project's identity. Thus it is recommended to leave the comment
// on the following line intact, so that it shows up in code reviews that
// modify the field.
.fingerprint = 0xf09b7d9dd5d3c2c7, // Changing this has security and trust implications.
// Tracks the earliest Zig version that the package considers to be a
// supported use case.
.minimum_zig_version = "0.16.0",
// This field is optional.
// Each dependency must either provide a `url` and `hash`, or a `path`.
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
// Once all dependencies are fetched, `zig build` no longer requires
// internet connectivity.
.dependencies = .{
.interface_helper = .{
.url = "git+https://git.sirlilpanda.studio/sirlilpanda/zig-interface-helpers#e64b016cfcefe93ede5eddd626fd373454167cbe",
.hash = "interface_helper-0.0.0--QUDPAtEAACFt9ZNHCzvbqGvgQgChObxyLSyA2bHWYK_",
},
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
// For example...
//"LICENSE",
//"README.md",
},
}

331
src/main.zig Normal file
View File

@@ -0,0 +1,331 @@
const std = @import("std");
const log = std.log.scoped(.server);
const iFace = @import("interface");
const LISTEN_ADDR = "127.0.0.1";
const LISTEN_PORT = 8000;
const Route = struct {
const Request = *std.http.Server.Request;
const Self = @This();
userdata: *anyopaque,
vtable: *const Vtable,
pub const Vtable = struct {
matchPath: *const iFace.VtableFn(fn (*anyopaque, []const u8) bool),
GET: *const iFace.VtableFn(fn (*anyopaque, std.Io, Request) anyerror!void),
POST: *const iFace.VtableFn(fn (*anyopaque, std.Io, Request) anyerror!void),
PUT: *const iFace.VtableFn(fn (*anyopaque, std.Io, Request) anyerror!void),
DELETE: *const iFace.VtableFn(fn (*anyopaque, std.Io, Request) anyerror!void),
HEAD: *const iFace.VtableFn(fn (*anyopaque, std.Io, Request) anyerror!void),
CONNECT: *const iFace.VtableFn(fn (*anyopaque, std.Io, Request) anyerror!void),
OPTIONS: *const iFace.VtableFn(fn (*anyopaque, std.Io, Request) anyerror!void),
TRACE: *const iFace.VtableFn(fn (*anyopaque, std.Io, Request) anyerror!void),
PATCH: *const iFace.VtableFn(fn (*anyopaque, std.Io, Request) anyerror!void),
};
pub fn init(route: anytype) Self {
return Self{
.userdata = @ptrCast(route),
.vtable = &Vtable{
.matchPath = iFace.ToVtableFn(@field(@TypeOf(route.*), "matchPath")),
.GET = iFace.ToVtableFn(@field(@TypeOf(route.*), "GET")),
.POST = iFace.ToVtableFn(@field(@TypeOf(route.*), "POST")),
.PUT = iFace.ToVtableFn(@field(@TypeOf(route.*), "PUT")),
.DELETE = iFace.ToVtableFn(@field(@TypeOf(route.*), "DELETE")),
.HEAD = iFace.ToVtableFn(@field(@TypeOf(route.*), "HEAD")),
.CONNECT = iFace.ToVtableFn(@field(@TypeOf(route.*), "CONNECT")),
.OPTIONS = iFace.ToVtableFn(@field(@TypeOf(route.*), "OPTIONS")),
.TRACE = iFace.ToVtableFn(@field(@TypeOf(route.*), "TRACE")),
.PATCH = iFace.ToVtableFn(@field(@TypeOf(route.*), "PATCH")),
},
};
}
pub fn matchPath(self: Self, path: []const u8) bool {
return self.vtable.matchPath(.{ self.userdata, path });
}
pub fn GET(self: Self, io: std.Io, request: Request) anyerror!void {
return self.vtable.GET(.{ self.userdata, io, request });
}
pub fn POST(self: Self, io: std.Io, request: Request) anyerror!void {
return self.vtable.POST(.{ self.userdata, io, request });
}
pub fn PUT(self: Self, io: std.Io, request: Request) anyerror!void {
return self.vtable.PUT(.{ self.userdata, io, request });
}
pub fn DELETE(self: Self, io: std.Io, request: Request) anyerror!void {
return self.vtable.DELETE(.{ self.userdata, io, request });
}
pub fn HEAD(self: Self, io: std.Io, request: Request) anyerror!void {
return self.vtable.HEAD(.{ self.userdata, io, request });
}
pub fn CONNECT(self: Self, io: std.Io, request: Request) anyerror!void {
return self.vtable.CONNECT(.{ self.userdata, io, request });
}
pub fn OPTIONS(self: Self, io: std.Io, request: Request) anyerror!void {
return self.vtable.OPTIONS(.{ self.userdata, io, request });
}
pub fn TRACE(self: Self, io: std.Io, request: Request) anyerror!void {
return self.vtable.TRACE(.{ self.userdata, io, request });
}
pub fn PATCH(self: Self, io: std.Io, request: Request) anyerror!void {
return self.vtable.PATCH(.{ self.userdata, io, request });
}
};
const UsersRoute = struct {
const Self = @This();
const UsersRouteLog = std.log.scoped(.userRoute);
users: std.ArrayList([]const u8),
mutex: std.Io.Mutex, // dont forget the mutex
pub fn init() !Self {
return Self{
.users = .empty,
.mutex = undefined,
};
}
pub fn matchPath(self: Self, path: []const u8) bool {
_ = self;
return std.mem.startsWith(u8, "/users/", path);
}
pub fn GET(self: *Self, io: std.Io, request: *std.http.Server.Request) !void {
_ = self;
_ = io;
UsersRouteLog.info("got GET request", .{});
try request.respond("", .{ .status = .not_found });
return;
}
pub fn PUT(self: Self, io: std.Io, request: *std.http.Server.Request) !void {
_ = self;
_ = io;
UsersRouteLog.info("got PUT request", .{});
try request.respond("", .{ .status = .not_found });
return;
}
pub fn DELETE(self: Self, io: std.Io, request: *std.http.Server.Request) !void {
_ = self;
_ = io;
UsersRouteLog.info("got DELETE request", .{});
try request.respond("", .{ .status = .not_found });
return;
}
pub fn POST(self: Self, io: std.Io, request: *std.http.Server.Request) !void {
_ = self;
_ = io;
UsersRouteLog.info("got POST request", .{});
try request.respond("", .{ .status = .not_found });
return;
}
pub fn HEAD(self: Self, io: std.Io, request: *std.http.Server.Request) !void {
_ = self;
_ = io;
UsersRouteLog.info("got HEAD request", .{});
try request.respond("", .{ .status = .not_found });
return;
}
pub fn CONNECT(self: Self, io: std.Io, request: *std.http.Server.Request) !void {
_ = self;
_ = io;
UsersRouteLog.info("got CONNECT request", .{});
try request.respond("", .{ .status = .not_found });
return;
}
pub fn OPTIONS(self: Self, io: std.Io, request: *std.http.Server.Request) !void {
_ = self;
_ = io;
UsersRouteLog.info("got OPTIONS request", .{});
try request.respond("", .{ .status = .not_found });
return;
}
pub fn TRACE(self: Self, io: std.Io, request: *std.http.Server.Request) !void {
_ = self;
_ = io;
UsersRouteLog.info("got TRACE request", .{});
try request.respond("", .{ .status = .not_found });
return;
}
pub fn PATCH(self: Self, io: std.Io, request: *std.http.Server.Request) !void {
_ = self;
_ = io;
UsersRouteLog.info("got PATCH request", .{});
try request.respond("", .{ .status = .not_found });
return;
}
pub fn ERR(self: Self, io: std.Io, request: *std.http.Server.Request) !void {
_ = self;
_ = io;
try request.respond("", .{ .status = .not_found });
}
};
const HomeRoute = struct {
const Self = @This();
const homeRouteLog = std.log.scoped(.homeRoute);
site_name: []const u8,
pub fn init(name: []const u8) !Self {
return Self{
.site_name = name,
};
}
pub fn matchPath(self: Self, path: []const u8) bool {
_ = self;
return std.mem.eql(u8, "/", path);
}
pub fn GET(self: *Self, io: std.Io, request: *std.http.Server.Request) !void {
_ = self;
_ = io;
homeRouteLog.info("got GET request", .{});
try request.respond("", .{ .status = .not_found });
return;
}
pub fn PUT(self: Self, io: std.Io, request: *std.http.Server.Request) !void {
_ = self;
_ = io;
homeRouteLog.info("got PUT request", .{});
try request.respond("", .{ .status = .not_found });
return;
}
pub fn DELETE(self: Self, io: std.Io, request: *std.http.Server.Request) !void {
_ = self;
_ = io;
homeRouteLog.info("got DELETE request", .{});
try request.respond("", .{ .status = .not_found });
return;
}
pub fn POST(self: Self, io: std.Io, request: *std.http.Server.Request) !void {
_ = self;
_ = io;
homeRouteLog.info("got POST request", .{});
try request.respond("", .{ .status = .not_found });
return;
}
pub fn HEAD(self: Self, io: std.Io, request: *std.http.Server.Request) !void {
_ = self;
_ = io;
homeRouteLog.info("got HEAD request", .{});
try request.respond("", .{ .status = .not_found });
return;
}
pub fn CONNECT(self: Self, io: std.Io, request: *std.http.Server.Request) !void {
_ = self;
_ = io;
homeRouteLog.info("got CONNECT request", .{});
try request.respond("", .{ .status = .not_found });
return;
}
pub fn OPTIONS(self: Self, io: std.Io, request: *std.http.Server.Request) !void {
_ = self;
_ = io;
homeRouteLog.info("got OPTIONS request", .{});
try request.respond("", .{ .status = .not_found });
return;
}
pub fn TRACE(self: Self, io: std.Io, request: *std.http.Server.Request) !void {
_ = self;
_ = io;
homeRouteLog.info("got TRACE request", .{});
try request.respond("", .{ .status = .not_found });
return;
}
pub fn PATCH(self: Self, io: std.Io, request: *std.http.Server.Request) !void {
_ = self;
_ = io;
homeRouteLog.info("got PATCH request", .{});
try request.respond("", .{ .status = .not_found });
return;
}
pub fn ERR(self: Self, io: std.Io, request: *std.http.Server.Request) !void {
_ = self;
_ = io;
try request.respond("", .{ .status = .not_found });
}
};
const Router = struct {
const Self = @This();
// will change this later to be just an array
// with some custom path parsing code
routes: []Route,
pub fn processConnection(self: *Self, io: std.Io, stream: *std.Io.net.Stream) !void {
// Wrap the raw stream in buffered Io.Reader / Io.Writer
errdefer stream.close(io);
var read_buffer: [1024]u8 = undefined;
var write_buffer: [1024]u8 = undefined;
var reader = stream.reader(io, &read_buffer);
var writer = stream.writer(io, &write_buffer);
// HTTP layer: parse the byte stream at HTTP/1.1
var http_server = std.http.Server.init(&reader.interface, &writer.interface);
var req = try http_server.receiveHead();
log.info("method : {s} target : \"{s}\"", .{ @tagName(req.head.method), req.head.target });
for (self.routes) |route| {
if (route.matchPath(req.head.target)) {
switch (req.head.method) {
.GET => try route.GET(io, &req),
.HEAD => try route.HEAD(io, &req),
.POST => try route.POST(io, &req),
.PUT => try route.PUT(io, &req),
.DELETE => try route.DELETE(io, &req),
.CONNECT => try route.CONNECT(io, &req),
.OPTIONS => try route.OPTIONS(io, &req),
.TRACE => try route.TRACE(io, &req),
.PATCH => try route.PATCH(io, &req),
}
log.info("Response sent, closing connection", .{});
return;
}
}
log.info("no path found, closing connection", .{});
try req.respond("", .{ .status = .not_found });
stream.close(io);
}
};
pub fn main(init: std.process.Init) !void {
var home_route = try HomeRoute.init("sir-pandas-site");
var user_route = try UsersRoute.init();
// const home_route_route = home_route.toRoute();
var routes = [_]Route{
Route.init(&home_route),
Route.init(&user_route),
};
var router = Router{ .routes = routes[0..] };
// std.debug.print("route {any}\n", .{home_route_route});
log.info("Listening on http://{s}:{d}", .{ LISTEN_ADDR, LISTEN_PORT });
// set the address and port
const addr = std.Io.net.IpAddress.parseIp4(LISTEN_ADDR, LISTEN_PORT) catch unreachable;
// TCP layer: bind the port and accept the raw streams
var server = try addr.listen(init.io, .{ .reuse_address = true });
defer server.deinit(init.io);
while (true) {
log.info("Waiting for connection...", .{});
var stream = try server.accept(init.io);
log.info("TCP connection established", .{});
_ = try init.io.concurrent(Router.processConnection, .{ &router, init.io, &stream });
}
}