X-Git-Url: https://gitweb.ps.run/chirp/blobdiff_plain/8ce4f0b76cab1963cd0a8ad55bf5b30b9eae917f..7d44a5163fd261dcb08282e15da9666e00a4f4b0:/src/http.zig?ds=sidebyside diff --git a/src/http.zig b/src/http.zig new file mode 100644 index 0000000..db625d6 --- /dev/null +++ b/src/http.zig @@ -0,0 +1,344 @@ +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: ?[]const u8 = null, + + pub fn parse(self: *Request, buf: []const 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]; + + if (index + 4 < buf.len) { + self.body = buf[index + 4 .. buf.len]; + } + return true; + } + }, + else => {}, + } + } + + return true; + } + + pub fn get_header(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 { + 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_header1(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 header_end = std.mem.indexOfPos(u8, head, header_start, "\r\n") orelse return null; + return head[header_start..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| { + return body[eql_index + 1 .. amp_index]; + } else { + return body[eql_index + 1 .. body.len]; + } + } +}; + +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 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 { + // write head + const writer = self.stream_head.writer(); + + 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 + + try std.fmt.format(writer, "{s}", .{self.stream_body.getWritten()}); + + // send head + + 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; + }; + } + } +};