srctree

Gregory Mullen parent 8fbc6e4c 03d5bb1d
rewrite and expand the Request API to hold the Request.Data

src/http.zig added: 318, removed: 183, total 135
@@ -1,21 +1,15 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const log = std.log.scoped(.Verse);
const Server = std.http.Server;
 
const Verse = @import("verse.zig");
const Router = @import("router.zig");
const Request = @import("request.zig");
const RequestData = @import("request_data.zig");
//! Verse HTTP server
pub const HTTP = @This();
 
const MAX_HEADER_SIZE = 1 <<| 13;
 
pub const HTTP = @This();
 
alloc: Allocator,
listen_addr: std.net.Address,
router: Router,
max_request_size: usize = 0xffff,
request_buffer: [0xffff]u8 = undefined,
running: bool = true,
alive: bool = false,
 
pub const Options = struct {
host: []const u8 = "127.0.0.1",
@@ -34,45 +28,46 @@ pub fn serve(http: *HTTP) !void {
var srv = try http.listen_addr.listen(.{ .reuse_address = true });
defer srv.deinit();
 
log.warn("HTTP Server listening on port: {any}", .{http.listen_addr.getPort()});
 
const request_buffer: []u8 = try http.alloc.alloc(u8, http.max_request_size);
defer http.alloc.free(request_buffer);
 
while (true) {
var conn = try srv.accept();
defer conn.stream.close();
log.info("HTTP connection from {}", .{conn.address});
var hsrv = std.http.Server.init(conn, request_buffer);
var arena = std.heap.ArenaAllocator.init(http.alloc);
defer arena.deinit();
const a = arena.allocator();
 
var hreq = try hsrv.receiveHead();
var req = try Request.initHttp(a, &hreq);
 
var verse = try buildVerse(a, &req);
 
const callable = http.router.routerfn(&verse, http.router.routefn);
http.router.builderfn(&verse, callable);
log.info("HTTP Server listening on port: {any}", .{http.listen_addr.getPort()});
http.alive = true;
while (http.running) {
try http.once(&srv);
}
unreachable;
}
 
fn buildVerse(a: Allocator, req: *Request) !Verse {
var itr_headers = req.raw.http.iterateHeaders();
pub fn once(http: *HTTP, srv: *net.Server) !void {
var arena = std.heap.ArenaAllocator.init(http.alloc);
defer arena.deinit();
const a = arena.allocator();
 
var conn = try srv.accept();
defer conn.stream.close();
 
log.info("HTTP connection from {}", .{conn.address});
var hsrv = std.http.Server.init(conn, &http.request_buffer);
 
var hreq = try hsrv.receiveHead();
const reqdata = try requestData(a, &hreq);
const req = try Request.initHttp(a, &hreq, reqdata);
 
var verse = try Verse.init(a, &req);
 
const callable = http.router.routerfn(&verse, http.router.routefn);
http.router.builderfn(&verse, callable);
}
 
fn requestData(a: Allocator, req: *std.http.Server.Request) !Request.Data {
var itr_headers = req.iterateHeaders();
while (itr_headers.next()) |header| {
log.debug("http header => {s} -> {s}", .{ header.name, header.value });
log.debug("{}", .{header});
}
log.debug("http target -> {s}", .{req.uri});
var post_data: ?RequestData.PostData = null;
var reqdata: RequestData = undefined;
 
if (req.raw.http.head.content_length) |h_len| {
if (req.head.content_length) |h_len| {
if (h_len > 0) {
const h_type = req.raw.http.head.content_type orelse "text/plain";
var reader = try req.raw.http.reader();
const h_type = req.head.content_type orelse "text/plain";
var reader = try req.reader();
post_data = try RequestData.readBody(a, &reader, h_len, h_type);
log.debug(
"post data \"{s}\" {{{any}}}",
@@ -86,13 +81,51 @@ fn buildVerse(a: Allocator, req: *Request) !Verse {
}
 
var query_data: RequestData.QueryData = undefined;
if (std.mem.indexOf(u8, req.raw.http.head.target, "/")) |i| {
query_data = try RequestData.readQuery(a, req.raw.http.head.target[i..]);
if (std.mem.indexOf(u8, req.head.target, "/")) |i| {
query_data = try RequestData.readQuery(a, req.head.target[i..]);
}
reqdata = RequestData{
 
return RequestData{
.post = post_data,
.query = query_data,
};
 
return Verse.init(a, req, reqdata);
}
 
fn threadFn(server: *HTTP) void {
server.serve() catch |err| {
log.err("Server failed! {}", .{err});
};
}
 
test HTTP {
const a = std.testing.allocator;
var server = try init(a, .{ .port = 9345 }, .{ .routefn = Router.testingRouter });
var thread = try std.Thread.spawn(.{}, threadFn, .{&server});
 
var client = std.http.Client{ .allocator = a };
defer client.deinit();
while (!server.alive) {}
 
var list = std.ArrayList(u8).init(a);
defer list.clearAndFree();
server.running = false;
const fetch = try client.fetch(.{
.response_storage = .{ .dynamic = &list },
.location = .{ .url = "http://localhost:9345/" },
.method = .GET,
});
try std.testing.expectEqual(.ok, fetch.status);
 
thread.join();
}
 
const Verse = @import("verse.zig");
const Router = @import("router.zig");
const Request = @import("request.zig");
const RequestData = @import("request_data.zig");
 
const std = @import("std");
const net = std.net;
const Allocator = std.mem.Allocator;
const log = std.log.scoped(.Verse);
const Server = std.http.Server;
 
src/request.zig added: 318, removed: 183, total 135
@@ -1,28 +1,25 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const indexOf = std.mem.indexOf;
const indexOfScalar = std.mem.indexOfScalar;
const eql = std.mem.eql;
const allocPrint = std.fmt.allocPrint;
 
const Headers = @import("headers.zig");
const Cookies = @import("cookies.zig");
 
const zWSGIRequest = @import("zwsgi.zig").zWSGIRequest;
 
pub const Request = @This();
 
/// TODO this is unstable and likely to be removed
raw: RawReq,
 
/// Default API, still unstable, but unlike to drastically change
headers: Headers,
uri: []const u8,
method: Methods,
cookie_jar: Cookies.Jar,
pub const Data = @import("request_data.zig");
 
/// Unstable API; likely to exist in some form, but might be changed
remote_addr: RemoteAddr,
method: Methods,
uri: []const u8,
host: ?[]const u8,
user_agent: ?UserAgent,
referer: ?[]const u8,
accept: ?Accept,
accept_encoding: Encoding = Encoding.default,
authorization: ?[]const u8,
 
headers: Headers,
/// Default API, still unstable, but unlike to drastically change
cookie_jar: Cookies.Jar,
/// POST or QUERY data
data: Data,
/// TODO this is unstable and likely to be removed
raw: RawReq,
 
pub const RawReq = union(enum) {
zwsgi: *zWSGIRequest,
@@ -34,7 +31,38 @@ const Pair = struct {
val: []const u8,
};
 
pub const Host = []const u8;
pub const RemoteAddr = []const u8;
pub const UserAgent = []const u8;
pub const Accept = []const u8;
pub const Authorization = []const u8;
pub const Referer = []const u8;
 
pub const Encoding = packed struct(usize) {
br: bool,
deflate: bool,
gzip: bool,
zstd: bool,
 
_padding: u60 = 0,
 
pub fn fromStr(str: []const u8) Encoding {
var e = Encoding.default;
inline for (@typeInfo(Encoding).Struct.fields) |f| {
if (indexOf(u8, str, f.name)) |_| {
@field(e, f.name) = if (f.type == bool) true else 0;
}
}
return e;
}
 
pub const default: Encoding = .{
.br = false,
.deflate = false,
.gzip = false,
.zstd = false,
};
};
 
pub const Methods = enum(u8) {
GET = 1,
@@ -56,66 +84,120 @@ pub const Methods = enum(u8) {
}
};
 
pub fn initZWSGI(a: Allocator, zwsgi: *zWSGIRequest) !Request {
var req = Request{
.raw = .{ .zwsgi = zwsgi },
.headers = Headers.init(a),
.method = Methods.GET,
.uri = undefined,
.cookie_jar = undefined,
.remote_addr = undefined,
};
pub fn initZWSGI(a: Allocator, zwsgi: *zWSGIRequest, data: Data) !Request {
var uri: ?[]const u8 = null;
var method: Methods = Methods.GET;
var remote_addr: RemoteAddr = undefined;
var headers = Headers.init(a);
var accept: ?Accept = null;
var host: ?Host = null;
var uagent: ?UserAgent = null;
var referer: ?Referer = null;
var encoding: Encoding = Encoding.default;
var authorization: ?Authorization = null;
 
for (zwsgi.vars) |v| {
try req.addHeader(v.key, v.val);
try headers.add(v.key, v.val);
if (eql(u8, v.key, "PATH_INFO")) {
req.uri = v.val;
uri = v.val;
} else if (eql(u8, v.key, "REQUEST_METHOD")) {
req.method = Methods.fromStr(v.val) catch Methods.GET;
method = Methods.fromStr(v.val) catch Methods.GET;
} else if (eql(u8, v.key, "REMOTE_ADDR")) {
req.remote_addr = v.val;
remote_addr = v.val;
} else if (eqlIgnoreCase("ACCEPT", v.key)) {
accept = v.val;
} else if (eqlIgnoreCase("HOST", v.key)) {
host = v.val;
} else if (eqlIgnoreCase("USER_AGENT", v.key)) {
uagent = v.val;
} else if (eqlIgnoreCase("REFERER", v.key)) {
referer = v.val;
} else if (eqlIgnoreCase("ACCEPT-ENCODING", v.key)) {
encoding = Encoding.fromStr(v.val);
} else if (eqlIgnoreCase("AUTHORIZATION", v.key)) {
authorization = v.val;
}
}
req.cookie_jar = try Cookies.Jar.initFromHeaders(a, &req.headers);
return req;
return .{
.remote_addr = remote_addr,
.host = host,
.user_agent = uagent,
.accept = accept,
.authorization = authorization,
.referer = referer,
.method = Methods.GET,
.uri = uri orelse return error.InvalidRequest,
.headers = headers,
.cookie_jar = try Cookies.Jar.initFromHeaders(a, &headers),
.data = data,
.raw = .{ .zwsgi = zwsgi },
};
}
 
pub fn initHttp(a: Allocator, http: *std.http.Server.Request) !Request {
var req = Request{
.raw = .{ .http = http },
.headers = Headers.init(a),
.uri = http.head.target,
.method = switch (http.head.method) {
.GET => .GET,
.POST => .POST,
.HEAD => .HEAD,
.PUT => .PUT,
.DELETE => .DELETE,
.CONNECT => .CONNECT,
.OPTIONS => .OPTIONS,
.TRACE => .TRACE,
else => @panic("not implemented"),
},
.cookie_jar = undefined,
.remote_addr = undefined,
};
pub fn initHttp(a: Allocator, http: *std.http.Server.Request, data: Data) !Request {
var itr = http.iterateHeaders();
while (itr.next()) |head| {
try req.addHeader(head.name, head.value);
}
req.cookie_jar = try Cookies.Jar.initFromHeaders(a, &req.headers);
var headers = Headers.init(a);
 
var accept: ?Accept = null;
var host: ?Host = null;
var uagent: ?UserAgent = null;
var referer: ?Referer = null;
var encoding: Encoding = Encoding.default;
var authorization: ?Authorization = null;
 
while (itr.next()) |head| {
try headers.add(head.name, head.value);
if (eqlIgnoreCase("accept", head.name)) {
accept = head.value;
} else if (eqlIgnoreCase("host", head.name)) {
host = head.value;
} else if (eqlIgnoreCase("user-agent", head.name)) {
uagent = head.value;
} else if (eqlIgnoreCase("referer", head.name)) {
referer = head.value;
} else if (eqlIgnoreCase("accept-encoding", head.name)) {
encoding = Encoding.fromStr(head.value);
} else if (eqlIgnoreCase("authorization", head.name)) {
authorization = head.value;
}
}
 
var remote_addr: RemoteAddr = undefined;
const ipport = try allocPrint(a, "{}", .{http.server.connection.address});
if (indexOfScalar(u8, ipport, ':')) |i| {
req.remote_addr = ipport[0..i];
try req.addHeader("REMOTE_ADDR", req.remote_addr);
try req.addHeader("REMOTE_PORT", ipport[i + 1 ..]);
remote_addr = ipport[0..i];
try headers.add("REMOTE_ADDR", remote_addr);
try headers.add("REMOTE_PORT", ipport[i + 1 ..]);
} else @panic("invalid address from http server");
 
return req;
return .{
.remote_addr = remote_addr,
.host = host,
.user_agent = uagent,
.accept = accept,
.authorization = authorization,
.referer = referer,
.method = translateStdHttp(http.head.method),
.uri = http.head.target,
.headers = headers,
.cookie_jar = try Cookies.Jar.initFromHeaders(a, &headers),
.data = data,
.raw = .{ .http = http },
};
}
 
pub fn addHeader(self: *Request, name: []const u8, val: []const u8) !void {
try self.headers.add(name, val);
fn translateStdHttp(m: std.http.Method) Methods {
return switch (m) {
.GET => .GET,
.POST => .POST,
.HEAD => .HEAD,
.PUT => .PUT,
.DELETE => .DELETE,
.CONNECT => .CONNECT,
.OPTIONS => .OPTIONS,
.TRACE => .TRACE,
else => @panic("not implemented"),
};
}
 
pub fn getHeader(self: Request, key: []const u8) ?[]const u8 {
@@ -127,3 +209,15 @@ pub fn getHeader(self: Request, key: []const u8) ?[]const u8 {
return null;
}
}
 
const Headers = @import("headers.zig");
const Cookies = @import("cookies.zig");
const zWSGIRequest = @import("zwsgi.zig").zWSGIRequest;
 
const std = @import("std");
const Allocator = std.mem.Allocator;
const indexOf = std.mem.indexOf;
const indexOfScalar = std.mem.indexOfScalar;
const eql = std.mem.eql;
const eqlIgnoreCase = std.ascii.eqlIgnoreCase;
const allocPrint = std.fmt.allocPrint;
 
src/router.zig added: 318, removed: 183, total 135
@@ -313,3 +313,7 @@ fn defaultRouterHtml(vrs: *Verse, routefn: RouteFn) Error!void {
}
return internalServerError;
}
 
pub fn testingRouter(v: *Verse) Error!BuildFn {
return router(v, &root);
}
 
src/server.zig added: 318, removed: 183, total 135
@@ -50,3 +50,7 @@ pub fn serve(srv: *Server) !void {
else => {},
}
}
 
test Server {
std.testing.refAllDecls(@This());
}
 
src/verse.zig added: 318, removed: 183, total 135
@@ -16,7 +16,6 @@ const NetworkError = @import("errors.zig").NetworkError;
 
alloc: Allocator,
request: *const Request,
reqdata: RequestData,
downstream: union(Downstream) {
buffer: std.io.BufferedWriter(ONESHOT_SIZE, Stream.Writer),
zwsgi: Stream,
@@ -80,14 +79,13 @@ pub const RouteData = struct {
}
};
 
pub fn init(a: Allocator, req: *const Request, reqdata: RequestData) !Verse {
pub fn init(a: Allocator, req: *const Request) !Verse {
std.debug.assert(req.uri[0] == '/');
return .{
.alloc = a,
.request = req,
.reqdata = reqdata,
.downstream = switch (req.raw) {
.zwsgi => |z| .{ .zwsgi = z.*.acpt.stream },
.zwsgi => |z| .{ .zwsgi = z.*.conn.stream },
.http => .{ .http = req.raw.http.server.connection.stream },
},
.uri = splitScalar(u8, req.uri[1..], '/'),
 
src/zwsgi.zig added: 318, removed: 183, total 135
@@ -9,7 +9,6 @@ const SA = std.posix.SA;
const Verse = @import("verse.zig");
const Request = @import("request.zig");
const Router = @import("router.zig");
const RequestData = @import("request_data.zig");
 
pub const zWSGI = @This();
 
@@ -87,9 +86,10 @@ pub fn serve(z: *zWSGI) !void {
defer arena.deinit();
const a = arena.allocator();
 
var zreq = try readHeader(a, &acpt);
var request = try Request.initZWSGI(a, &zreq);
var verse = try buildVerse(a, &request);
var zreq = try zWSGIRequest.init(a, &acpt);
const request_data = try requestData(a, &zreq);
const request = try Request.initZWSGI(a, &zreq, request_data);
var verse = try Verse.init(a, &request);
 
defer {
const vars = verse.request.raw.zwsgi.vars;
@@ -128,10 +128,51 @@ fn signalListen(signal: u6) !void {
}
 
pub const zWSGIRequest = struct {
acpt: *net.Server.Connection,
conn: *net.Server.Connection,
header: uProtoHeader = uProtoHeader{},
vars: []uWSGIVar = &[0]uWSGIVar{},
body: ?[]u8 = null,
 
pub fn init(a: Allocator, c: *net.Server.Connection) !zWSGIRequest {
const uwsgi_header = try uProtoHeader.init(c);
 
const buf: []u8 = try a.alloc(u8, uwsgi_header.size);
const read = try c.stream.read(buf);
if (read != uwsgi_header.size) {
std.log.err("unexpected read size {} {}", .{ read, uwsgi_header.size });
}
 
const vars = try readVars(a, buf);
for (vars) |v| {
log.debug("{}", .{v});
}
 
return .{
.conn = c,
.header = uwsgi_header,
.vars = vars,
};
}
fn readVars(a: Allocator, b: []const u8) ![]uWSGIVar {
var list = std.ArrayList(uWSGIVar).init(a);
var buf = b;
while (buf.len > 0) {
const keysize = readU16(buf[0..2]);
buf = buf[2..];
const key = try a.dupe(u8, buf[0..keysize]);
buf = buf[keysize..];
 
const valsize = readU16(buf[0..2]);
buf = buf[2..];
const val = try a.dupe(u8, if (valsize == 0) "" else buf[0..valsize]);
buf = buf[valsize..];
 
try list.append(uWSGIVar{
.key = key,
.val = val,
});
}
return try list.toOwnedSlice();
}
};
 
fn readU16(b: *const [2]u8) u16 {
@@ -145,32 +186,19 @@ test "readu16" {
try std.testing.expectEqual(size, readU16(&buffer));
}
 
fn readVars(a: Allocator, b: []const u8) ![]uWSGIVar {
var list = std.ArrayList(uWSGIVar).init(a);
var buf = b;
while (buf.len > 0) {
const keysize = readU16(buf[0..2]);
buf = buf[2..];
const key = try a.dupe(u8, buf[0..keysize]);
buf = buf[keysize..];
 
const valsize = readU16(buf[0..2]);
buf = buf[2..];
const val = try a.dupe(u8, if (valsize == 0) "" else buf[0..valsize]);
buf = buf[valsize..];
 
try list.append(uWSGIVar{
.key = key,
.val = val,
});
}
return try list.toOwnedSlice();
}
 
const uProtoHeader = packed struct {
mod1: u8 = 0,
size: u16 = 0,
mod2: u8 = 0,
 
pub fn init(c: *net.Server.Connection) !uProtoHeader {
var self: uProtoHeader = .{};
var ptr: [*]u8 = @ptrCast(&self);
if (try c.stream.read(ptr[0..4]) != 4) {
return error.InvalidRead;
}
return self;
}
};
 
const uWSGIVar = struct {
@@ -200,40 +228,16 @@ fn findOr(list: []uWSGIVar, search: []const u8) []const u8 {
return find(list, search) orelse "[missing]";
}
 
fn readHeader(a: Allocator, conn: *net.Server.Connection) !zWSGIRequest {
var uwsgi_header = uProtoHeader{};
var ptr: [*]u8 = @ptrCast(&uwsgi_header);
_ = try conn.stream.read(@alignCast(ptr[0..4]));
fn requestData(a: Allocator, zreq: *zWSGIRequest) !Request.Data {
var post_data: ?Request.Data.PostData = null;
 
const buf: []u8 = try a.alloc(u8, uwsgi_header.size);
const read = try conn.stream.read(buf);
if (read != uwsgi_header.size) {
std.log.err("unexpected read size {} {}", .{ read, uwsgi_header.size });
}
 
const vars = try readVars(a, buf);
for (vars) |v| {
log.debug("{}", .{v});
}
 
return .{
.acpt = conn,
.header = uwsgi_header,
.vars = vars,
};
}
 
fn buildVerse(a: Allocator, req: *Request) !Verse {
var post_data: ?RequestData.PostData = null;
var reqdata: RequestData = undefined;
 
if (find(req.raw.zwsgi.vars, "HTTP_CONTENT_LENGTH")) |h_len| {
const h_type = findOr(req.raw.zwsgi.vars, "HTTP_CONTENT_TYPE");
if (find(zreq.vars, "HTTP_CONTENT_LENGTH")) |h_len| {
const h_type = findOr(zreq.vars, "HTTP_CONTENT_TYPE");
 
const post_size = try std.fmt.parseInt(usize, h_len, 10);
if (post_size > 0) {
var reader = req.raw.zwsgi.acpt.stream.reader().any();
post_data = try RequestData.readBody(a, &reader, post_size, h_type);
var reader = zreq.conn.stream.reader().any();
post_data = try Request.Data.readBody(a, &reader, post_size, h_type);
log.debug(
"post data \"{s}\" {{{any}}}",
.{ post_data.?.rawpost, post_data.?.rawpost },
@@ -245,16 +249,14 @@ fn buildVerse(a: Allocator, req: *Request) !Verse {
}
}
 
var query: RequestData.QueryData = undefined;
if (find(req.raw.zwsgi.vars, "QUERY_STRING")) |qs| {
query = try RequestData.readQuery(a, qs);
var query: Request.Data.QueryData = undefined;
if (find(zreq.vars, "QUERY_STRING")) |qs| {
query = try Request.Data.readQuery(a, qs);
}
reqdata = RequestData{
return .{
.post = post_data,
.query = query,
};
 
return Verse.init(a, req, reqdata);
}
 
test init {