From: patrick-scho Date: Wed, 5 Mar 2025 18:57:20 +0000 (+0100) Subject: Initial commit X-Git-Url: https://gitweb.ps.run/zighttp/commitdiff_plain/abb5a6c230ae69c59fd7c9a36ca817a6b3711ed2 Initial commit --- abb5a6c230ae69c59fd7c9a36ca817a6b3711ed2 diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..ce15aa9 --- /dev/null +++ b/build.zig @@ -0,0 +1,29 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + + const optimize = b.standardOptimizeOption(.{}); + + const mod = b.addModule("http", .{ + .root_source_file = .{ .src_path = .{ .owner = b, .sub_path = "src/http.zig" } }, + .target = target, + .optimize = optimize, + }); + _ = mod; + + const unit_tests = b.addTest(.{ + .root_source_file = .{ .cwd_relative = "src/lmdb.zig" }, + .target = target, + .optimize = optimize, + }); + + // const test_bin = b.addInstallBinFile(unit_tests.getEmittedBin(), "./lmdb_test"); + + const run_unit_tests = b.addRunArtifact(unit_tests); + + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_unit_tests.step); + test_step.dependOn(&unit_tests.step); + // test_step.dependOn(&test_bin.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..ac5c268 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,9 @@ +.{ + .name = "http", + .version = "0.0.0", + .paths = .{ + "src/http.zig", + "build.zig", + "build.zig.zon", + }, +} diff --git a/src/http.zig b/src/http.zig new file mode 100644 index 0000000..2c47d1d --- /dev/null +++ b/src/http.zig @@ -0,0 +1,374 @@ +const std = @import("std"); +const posix = std.posix; +const linux = std.os.linux; + +pub const Server = struct { + // TODO: factor out + const BACKLOG = 2048; + + listener: posix.socket_t, + efd: i32, + ready_list: [BACKLOG]linux.epoll_event = undefined, + + ready_count: usize = 0, + ready_index: usize = 0, + + pub fn init(name: []const u8, port: u16) !Server { + const address = try std.net.Address.resolveIp(name, port); + + const tpe: u32 = posix.SOCK.STREAM | posix.SOCK.NONBLOCK; + const protocol = posix.IPPROTO.TCP; + const listener = try posix.socket(address.any.family, tpe, protocol); + + try posix.setsockopt(listener, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1))); + try posix.bind(listener, &address.any, address.getOsSockLen()); + try posix.listen(listener, BACKLOG); + + // epoll_create1 takes flags. We aren't using any in these examples + const efd = try posix.epoll_create1(0); + + var event = linux.epoll_event{ .events = linux.EPOLL.IN, .data = .{ .fd = listener } }; + try posix.epoll_ctl(efd, linux.EPOLL.CTL_ADD, listener, &event); + + return .{ + .listener = listener, + .efd = efd, + }; + } + + pub fn deinit(self: Server) void { + posix.close(self.efd); + posix.close(self.listener); + } + + pub fn wait(self: *Server) void { + if (self.ready_index >= self.ready_count) { + self.ready_index = 0; + self.ready_count = posix.epoll_wait(self.efd, &self.ready_list, -1); + } + } + + pub fn next_request(self: *Server, buf: []u8) !?Request { + while (self.ready_index < self.ready_count) { + const ready = self.ready_list[self.ready_index]; + const ready_socket = ready.data.fd; + self.ready_index += 1; + + if (ready_socket == self.listener) { + const client_socket = try posix.accept(self.listener, null, null, posix.SOCK.NONBLOCK); + errdefer posix.close(client_socket); + var event = linux.epoll_event{ .events = linux.EPOLL.IN, .data = .{ .fd = client_socket } }; + try posix.epoll_ctl(self.efd, linux.EPOLL.CTL_ADD, client_socket, &event); + } else { + var closed = false; + var req = Request{ .fd = ready_socket }; + + const read = posix.read(ready_socket, buf) catch 0; + if (read == 0) { + closed = true; + } else { + if (req.parse(buf[0..read])) + return req; + } + + if (closed or ready.events & linux.EPOLL.RDHUP == linux.EPOLL.RDHUP) { + posix.close(ready_socket); + } + } + } + return null; + } +}; + +// pub const Method = enum { GET, POST }; +pub const Method = std.http.Method; + +// pub const Header = struct { +// const NAME_SIZE = 32; +// const VALUE_SIZE = 128; + +// name: std.BoundedArray(u8, NAME_SIZE), +// value: std.BoundedArray(u8, VALUE_SIZE), +// }; +pub const Header = struct { + const Name = std.BoundedArray(u8, 32); + const Value = std.BoundedArray(u8, 128); + + name: Name = Name.init(0) catch unreachable, + value: Value = Value.init(0) catch unreachable, +}; +pub const Status = std.http.Status; + +pub const Request = struct { + fd: posix.fd_t, + + method: Method = undefined, + target: []const u8 = undefined, + version: ?[]const u8 = null, + head: ?[]const u8 = null, + body: ?[]u8 = null, + + pub fn parse(self: *Request, buf: []u8) bool { + var state: u8 = 0; + + var start: u32 = 0; + // var end: u32 = 0; + + var index: u32 = 0; + while (index < buf.len) { + defer index += 1; + + const c = buf[index]; + + switch (state) { + 0 => { + if (c == ' ') { + self.method = @enumFromInt(Method.parse(buf[start..index])); + start = index + 1; + state += 1; + } + }, + 1 => { + if (c == ' ') { + self.target = buf[start..index]; + start = index + 1; + state += 1; + } + }, + 2 => { + if (c == '\r') { + self.version = buf[start..index]; + start = index + 2; + index += 1; + state += 1; + } + }, + 3 => { + if (c == '\r' and (index + 2) < buf.len and buf[index + 2] == '\r') { + self.head = buf[start .. index + 2]; + + if (index + 4 < buf.len) { + self.body = buf[index + 4 .. buf.len]; + } + return true; + } + }, + else => {}, + } + } + + return true; + } + + pub fn get_header1(self: Request, name: []const u8) ?[]const u8 { + const head = self.head orelse return null; + var start: usize = 0; + var matching: usize = 0; + for (0..head.len) |i| { + const c = head[i]; + + if (matching < name.len) { + if (c == name[matching]) { + // if (matching == 0) start = i; + matching += 1; + } else { + start = i; + matching = 0; + } + } else { + if (c == '\r') { + return head[start..i]; + } + } + } + return null; + } + + pub fn get_cookie(self: Request, name: []const u8) ?[]const u8 { + const cookie = self.get_header("Cookie") orelse return null; + var start: usize = 0; + var matching: usize = 0; + for (0..cookie.len) |i| { + const c = cookie[i]; + + if (matching < name.len) { + if (c == name[matching]) { + if (matching == 0) start = i; + matching += 1; + } else { + matching = 0; + } + } else { + if (c == '=') { + if (std.mem.indexOfScalarPos(u8, cookie, i, ';')) |semi_index| { + return cookie[i + 1 .. semi_index]; + } else { + return cookie[i + 1 .. cookie.len]; + } + } else { + matching = 0; + } + } + } + return null; + } + + pub fn parse1(self: *Request, buf: []const u8) bool { + const method_start: usize = 0; + const method_end = std.mem.indexOfScalar(u8, buf, ' ') orelse return false; + self.method = @enumFromInt(Method.parse(buf[method_start..method_end])); + + const target_start = method_end + 1; + const target_end = std.mem.indexOfScalarPos(u8, buf, target_start, ' ') orelse return false; + self.target = buf[target_start..target_end]; + + const version_start = target_end + 1; + const version_end = std.mem.indexOfPos(u8, buf, version_start, "\r\n") orelse buf.len; + self.version = buf[version_start..version_end]; + + if (version_end + 2 >= buf.len) + return true; + const head_start = version_end + 2; + const head_end = std.mem.indexOfPos(u8, buf, head_start, "\r\n\r\n") orelse buf.len; + self.head = buf[head_start..head_end]; + + if (head_end + 4 >= buf.len) + return true; + const body_start = head_end + 4; + const body_end = buf.len; + self.body = buf[body_start..body_end]; + + return true; + } + + pub fn get_header(self: Request, name: []const u8) ?[]const u8 { + const head = self.head orelse return null; + const header_start = std.mem.indexOf(u8, head, name) orelse return null; + const colon_index = std.mem.indexOfPos(u8, head, header_start, ": ") orelse return null; + const header_end = std.mem.indexOfPos(u8, head, colon_index, "\r\n") orelse return null; + return head[colon_index + 2 .. header_end]; + } + + pub fn get_cookie1(self: Request, name: []const u8) ?[]const u8 { + const cookie = self.get_header("Cookie") orelse return null; + const name_index = std.mem.indexOf(u8, cookie, name) orelse return null; + const eql_index = std.mem.indexOfScalarPos(u8, cookie, name_index, '=') orelse return null; + if (std.mem.indexOfScalarPos(u8, cookie, eql_index, ';')) |semi_index| { + return cookie[eql_index + 1 .. semi_index]; + } else { + return cookie[eql_index + 1 .. cookie.len]; + } + } + + pub fn get_value(self: *Request, name: []const u8) ?[]const u8 { + const body = self.body orelse return null; + const name_index = std.mem.indexOf(u8, body, name) orelse return null; + const eql_index = std.mem.indexOfScalarPos(u8, body, name_index, '=') orelse return null; + if (std.mem.indexOfScalarPos(u8, body, name_index, '&')) |amp_index| { + const result = body[eql_index + 1 .. amp_index]; + return result; + } else { + const result = body[eql_index + 1 .. body.len]; + return result; + } + } +}; + +pub const Response = struct { + const ExtraHeadersMax = 16; + const HeaderList = std.BoundedArray(Header, ExtraHeadersMax); + + fd: posix.fd_t, + stream_head: std.io.FixedBufferStream([]u8), + stream_body: std.io.FixedBufferStream([]u8), + status: Status = .ok, + extra_headers: HeaderList = HeaderList.init(0) catch unreachable, + + pub fn init(fd: posix.fd_t, buf_head: []u8, buf_body: []u8) Response { + return .{ + .fd = fd, + .stream_head = std.io.fixedBufferStream(buf_head), + .stream_body = std.io.fixedBufferStream(buf_body), + }; + } + + pub fn redirect(self: *Response, location: []const u8) !void { + self.status = .see_other; + try self.add_header("Location", .{ "{s}", .{location} }); + } + + pub fn add_header(self: *Response, name: []const u8, value: anytype) !void { + const header = try self.extra_headers.addOne(); + try header.name.writer().writeAll(name); + if (@typeInfo(@TypeOf(value)).Struct.fields.len < 2 or @sizeOf(@TypeOf(value[1])) == 0) { + try header.value.writer().writeAll(value[0]); + } else { + try std.fmt.format(header.value.writer(), value[0], value[1]); + } + } + + pub fn has_header(self: Response, name: []const u8) bool { + for (self.extra_headers.constSlice()) |h| { + if (std.mem.eql(u8, h.name.constSlice(), name)) { + return true; + } + } + return false; + } + + pub fn write(self: *Response, comptime fmt: []const u8, args: anytype) !void { + const writer = self.stream_body.writer(); + + if (@sizeOf(@TypeOf(args)) == 0) { + try writer.writeAll(fmt); + } else { + try std.fmt.format(writer, fmt, args); + } + } + + pub fn send(self: *Response) !void { + // TODO: Provisorium + const compress = false; + var compress_buffer = try std.BoundedArray(u8, 1024 * 32).init(0); + + // write head + const writer = self.stream_head.writer(); + + if (compress) { + var cfbs = std.io.fixedBufferStream(self.stream_body.getWritten()); + var compressor = try std.compress.gzip.compressor(compress_buffer.writer(), .{ .level = .default }); + try compressor.compress(cfbs.reader()); + // try compressor.flush(); + try compressor.finish(); + try std.fmt.format(writer, "HTTP/1.1 {} {?s}\r\n" ++ + "Content-Length: {}\r\n" ++ + "Content-Encoding: gzip\r\n", .{ @intFromEnum(self.status), self.status.phrase(), compress_buffer.constSlice().len }); + } else { + try std.fmt.format(writer, "HTTP/1.1 {} {?s}\r\n" ++ + "Content-Length: {}\r\n", .{ @intFromEnum(self.status), self.status.phrase(), self.stream_body.pos }); + } + + for (self.extra_headers.constSlice()) |header| { + try std.fmt.format(writer, "{s}: {s}\r\n", .{ header.name.constSlice(), header.value.constSlice() }); + } + + try std.fmt.format(writer, "\r\n", .{}); + + // write body to head + if (compress) { + try std.fmt.format(writer, "{s}", .{compress_buffer.constSlice()}); + } else { + try std.fmt.format(writer, "{s}", .{self.stream_body.getWritten()}); + } + + // send + const res = self.stream_head.getWritten(); + var written: usize = 0; + while (written < res.len) { + written += posix.write(self.fd, res[written..res.len]) catch |err| { + std.debug.print("posix.write: {}\n", .{err}); + continue; + }; + } + } +};