srctree

Gregory Mullen parent 042dc827 31f30882
rewrite headers so that we can have request cookies

build.zig added: 252, removed: 125, total 127
@@ -35,7 +35,10 @@ pub fn build(b: *std.Build) !void {
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_lib_unit_tests.step);
 
const examples = [_][]const u8{"basic"};
const examples = [_][]const u8{
"basic",
"cookies",
};
for (examples) |example| {
const path = try std.fmt.allocPrint(b.allocator, "examples/{s}.zig", .{example});
const example_exe = b.addExecutable(.{
 
examples/basic.zig added: 252, removed: 125, total 127
@@ -25,5 +25,6 @@ fn route(verse: *Verse) Router.Error!BuildFn {
}
 
fn index(verse: *Verse) Router.Error!void {
try verse.quickStart();
try verse.sendRawSlice("hello world");
}
 
filename was Deleted added: 252, removed: 125, total 127
@@ -0,0 +1,33 @@
const std = @import("std");
const Verse = @import("verse");
const Router = Verse.Router;
const BuildFn = Router.BuildFn;
const print = std.fmt.bufPrint;
 
const routes = [_]Router.Match{
Router.GET("", index),
};
 
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const alloc = gpa.allocator();
 
var server = try Verse.Server.init(alloc, .{ .http = .{ .port = 8081 } }, .{ .routefn = route });
 
server.serve() catch |err| {
std.debug.print("error: {any}", .{err});
std.posix.exit(1);
};
}
 
fn route(verse: *Verse) Router.Error!BuildFn {
return Verse.Router.router(verse, &routes);
}
 
fn index(verse: *Verse) Router.Error!void {
var buffer: [2048]u8 = undefined;
const found = try print(&buffer, "{} cookies found by the server\n", .{verse.request.cookie_jar.cookies.items.len});
try verse.quickStart();
try verse.sendRawSlice(found);
}
 
src/cookies.zig added: 252, removed: 125, total 127
@@ -1,8 +1,14 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const eql = std.mem.eql;
const fmt = std.fmt;
const indexOf = std.mem.indexOf;
const indexOfScalar = std.mem.indexOfScalar;
const splitSequence = std.mem.splitSequence;
const ArrayListUnmanaged = std.ArrayListUnmanaged;
 
const Headers = @import("headers.zig");
 
pub const Attributes = struct {
domain: ?[]const u8 = null,
path: ?[]const u8 = null,
@@ -12,8 +18,6 @@ pub const Attributes = struct {
max_age: ?i64 = null,
expires: ?i64 = null,
same_site: ?SameSite = null,
// Cookie state metadata.
source: enum { server, client, nos } = .nos,
 
pub const SameSite = enum {
strict,
@@ -21,11 +25,6 @@ pub const Attributes = struct {
none,
};
 
/// Warning, not implemented!
pub fn fromHeader(_: []const u8) Attributes {
return .{};
}
 
pub fn format(a: Attributes, comptime _: []const u8, _: fmt.FormatOptions, w: anytype) !void {
if (a.domain) |d|
try w.print("; Domain={s}", .{d});
@@ -51,12 +50,16 @@ pub const Cookie = struct {
name: []const u8,
value: []const u8,
attr: Attributes = .{},
// Cookie state metadata.
source: enum { server, client, nos } = .nos,
 
pub fn fromHeader(str: []const u8) Cookie {
pub fn fromHeader(cookie: []const u8) Cookie {
const split = indexOfScalar(u8, cookie, '=') orelse cookie.len - 1;
return .{
.name = str[0..10],
.value = str[10..20],
.attr = Attributes.fromHeader(str[20..]),
.name = cookie[0..split],
.value = cookie[split + 1 ..],
.source = .client,
.attr = .{},
};
}
 
@@ -89,11 +92,11 @@ test Cookie {
}
 
pub const Jar = struct {
alloc: std.mem.Allocator,
alloc: Allocator,
cookies: ArrayListUnmanaged(Cookie),
 
/// Creates a new jar.
pub fn init(a: std.mem.Allocator) !Jar {
pub fn init(a: Allocator) !Jar {
const cookies = try ArrayListUnmanaged(Cookie).initCapacity(a, 8);
return .{
.alloc = a,
@@ -101,6 +104,24 @@ pub const Jar = struct {
};
}
 
pub fn initFromHeaders(a: Allocator, headers: *Headers) !Jar {
var jar = try init(a);
var itr = headers.iterator();
while (itr.next()) |header| {
// TODO we should probably use ASCII case equal here but it's not
// implemented that way because it might be better to normalize the
// incoming header names at the edge.
if (eql(u8, header.name, "Cookie")) {
var cookies = splitSequence(u8, header.value, "; ");
while (cookies.next()) |cookie| {
try jar.add(Cookie.fromHeader(cookie));
}
}
}
 
return jar;
}
 
pub fn raze(jar: *Jar) void {
jar.cookies.deinit(jar.alloc);
}
 
src/headers.zig added: 252, removed: 125, total 127
@@ -2,57 +2,134 @@ const std = @import("std");
 
const Allocator = std.mem.Allocator;
 
pub const HIndex = std.StringHashMap(Value);
 
const Value = struct {
str: []const u8,
next: ?*Value = null,
};
 
pub const Headers = @This();
 
const Header = struct {
name: []const u8,
value: []const u8,
};
 
const ValueList = struct {
value: []const u8,
next: ?*ValueList = null,
};
 
const HeaderMap = std.StringArrayHashMap(*ValueList);
 
alloc: Allocator,
index: HIndex,
headers: HeaderMap,
 
pub fn init(a: Allocator) Headers {
return .{
.alloc = a,
.index = HIndex.init(a),
.headers = HeaderMap.init(a),
};
}
 
pub fn raze(h: *Headers) void {
h.index.deinit(h.alloc);
h.* = undefined;
const values = h.headers.values();
for (values) |val| {
var next: ?*ValueList = val.*.next;
h.alloc.destroy(val);
while (next != null) {
const destroy = next.?;
next = next.?.next;
h.alloc.destroy(destroy);
}
}
h.headers.deinit();
}
 
/// TODO actually normalize to thing
/// TODO are we gonna normilize comptime?
fn normilize(name: []const u8) !void {
if (name.len == 0) return;
fn normalize(_: []const u8) !void {
comptime unreachable;
}
 
pub fn add(h: *Headers, comptime name: []const u8, value: []const u8) !void {
try normilize(name);
const res = try h.index.getOrPut(name);
if (res.found_existing) {
res.value_ptr.* = Value{
.str = value,
.next = res.value_ptr,
};
pub fn add(h: *Headers, name: []const u8, value: []const u8) !void {
// TODO normalize lower
const gop = try h.headers.getOrPut(name);
if (gop.found_existing) {
var end: *ValueList = gop.value_ptr.*;
while (end.*.next != null) {
end = end.next.?;
}
end.next = try h.alloc.create(ValueList);
end.next.?.value = value;
end.next.?.next = null;
} else {
res.value_ptr.* = Value{
.str = value,
};
gop.value_ptr.* = try h.alloc.create(ValueList);
gop.value_ptr.*.value = value;
gop.value_ptr.*.next = null;
}
}
 
/// Starting an iteration will lock the map pointers, callers must complete the
/// iteration, or manually unlock internal pointers. See also: Iterator.finish();
pub fn iterator(h: *Headers) Iterator {
return Iterator.init(h);
}
 
pub const Iterator = struct {
header: *Headers,
inner: HeaderMap.Iterator,
entry: ?HeaderMap.Entry = null,
current: ?*ValueList = null,
current_name: ?[]const u8 = null,
 
pub fn init(h: *Headers) Iterator {
h.headers.lockPointers();
return .{
.header = h,
.inner = h.headers.iterator(),
};
}
 
pub fn next(i: *Iterator) ?Header {
if (i.current) |current| {
defer i.current = current.next;
return .{
.name = i.current_name.?,
.value = current.value,
};
} else {
i.current_name = null;
i.entry = i.inner.next();
if (i.entry) |entry| {
i.current = entry.value_ptr.*;
i.current_name = entry.key_ptr.*;
} else {
i.header.headers.unlockPointers();
return null;
}
return i.next();
}
}
 
/// Helper
pub fn finish(i: *Iterator) void {
while (i.next()) |_| {}
}
};
 
pub fn format(h: Headers, comptime _: []const u8, _: std.fmt.FormatOptions, out: anytype) !void {
_ = h;
_ = out;
unreachable;
}
 
pub fn clearAndFree(h: *Headers) void {
h.index.clearAndFree(h.alloc);
test Headers {
const a = std.testing.allocator;
var hmap = init(a);
defer hmap.raze();
try hmap.add("first", "1");
try hmap.add("first", "2");
try hmap.add("first", "3");
try hmap.add("second", "4");
 
try std.testing.expectEqual(2, hmap.headers.count());
const first = hmap.headers.get("first");
try std.testing.expectEqualStrings(first.?.value, "1");
try std.testing.expectEqualStrings(first.?.next.?.value, "2");
try std.testing.expectEqualStrings(first.?.next.?.next.?.value, "3");
const second = hmap.headers.get("second");
try std.testing.expectEqualStrings(second.?.value, "4");
}
 
src/http.zig added: 252, removed: 125, total 127
@@ -50,7 +50,7 @@ pub fn serve(http: *HTTP) !void {
const a = arena.allocator();
 
var hreq = try hsrv.receiveHead();
var req = try Request.init(a, &hreq);
var req = try Request.initHttp(a, &hreq);
var ipbuf: [0x20]u8 = undefined;
const ipport = try std.fmt.bufPrint(&ipbuf, "{}", .{conn.address});
if (std.mem.indexOfScalar(u8, ipport, ':')) |i| {
@@ -69,10 +69,10 @@ pub fn serve(http: *HTTP) !void {
fn buildVerse(a: Allocator, req: *Request) !Verse {
var itr_headers = req.raw.http.iterateHeaders();
while (itr_headers.next()) |header| {
log.debug("http header => {s} -> {s}\n", .{ header.name, header.value });
log.debug("http header => {s} -> {s}", .{ header.name, header.value });
log.debug("{}", .{header});
}
log.debug("http target -> {s}\n", .{req.uri});
log.debug("http target -> {s}", .{req.uri});
var post_data: ?RequestData.PostData = null;
var reqdata: RequestData = undefined;
 
 
src/request.zig added: 252, removed: 125, total 127
@@ -3,10 +3,20 @@ const Allocator = std.mem.Allocator;
const indexOf = std.mem.indexOf;
const eql = std.mem.eql;
 
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,
headers: Headers,
uri: []const u8,
method: Methods,
cookie_jar: Cookies.Jar,
 
pub const RawReq = union(enum) {
zwsgi: *zWSGIRequest,
http: *std.http.Server.Request,
@@ -17,8 +27,6 @@ const Pair = struct {
val: []const u8,
};
 
pub const HeaderList = std.ArrayList(Pair);
 
pub const Methods = enum(u8) {
GET = 1,
HEAD = 2,
@@ -39,56 +47,49 @@ pub const Methods = enum(u8) {
}
};
 
/// TODO this is unstable and likely to be removed
raw: RawReq,
headers: HeaderList,
uri: []const u8,
method: Methods,
 
pub fn init(a: Allocator, raw_req: anytype) !Request {
switch (@TypeOf(raw_req)) {
*zWSGIRequest => {
var req = Request{
.raw = .{ .zwsgi = raw_req },
.headers = HeaderList.init(a),
.uri = undefined,
.method = Methods.GET,
};
for (raw_req.vars) |v| {
try req.addHeader(v.key, v.val);
if (std.mem.eql(u8, v.key, "PATH_INFO")) {
req.uri = v.val;
}
if (std.mem.eql(u8, v.key, "REQUEST_METHOD")) {
req.method = Methods.fromStr(v.val) catch Methods.GET;
}
}
return req;
},
*std.http.Server.Request => {
var req = Request{
.raw = .{ .http = raw_req },
.headers = HeaderList.init(a),
.uri = raw_req.head.target,
.method = switch (raw_req.head.method) {
.GET => .GET,
.POST => .POST,
else => @panic("not implemented"),
},
};
var itr = raw_req.iterateHeaders();
while (itr.next()) |head| {
try req.addHeader(head.name, head.value);
}
return req;
},
else => @compileError("rawish of " ++ @typeName(raw_req) ++ " isn't a support request type"),
pub fn initZWSGI(a: Allocator, zwsgi: *zWSGIRequest) !Request {
var req = Request{
.raw = .{ .zwsgi = zwsgi },
.headers = Headers.init(a),
.uri = undefined,
.method = Methods.GET,
.cookie_jar = undefined,
};
for (zwsgi.vars) |v| {
try req.addHeader(v.key, v.val);
if (eql(u8, v.key, "PATH_INFO")) {
req.uri = v.val;
}
if (eql(u8, v.key, "REQUEST_METHOD")) {
req.method = Methods.fromStr(v.val) catch Methods.GET;
}
}
@compileError("unreachable");
req.cookie_jar = try Cookies.Jar.initFromHeaders(a, &req.headers);
return req;
}
 
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,
else => @panic("not implemented"),
},
.cookie_jar = undefined,
};
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);
return req;
}
 
pub fn addHeader(self: *Request, name: []const u8, val: []const u8) !void {
try self.headers.append(.{ .name = name, .val = val });
try self.headers.add(name, val);
}
 
pub fn getHeader(self: Request, key: []const u8) ?[]const u8 {
 
src/response.zig added: 252, removed: 125, total 127
@@ -37,7 +37,7 @@ const Error = error{
 
pub const Writer = std.io.Writer(*Response, Error, write);
 
headers: ?Headers = null,
headers: Headers,
tranfer_mode: TransferMode = .static,
// This is just bad code, but I need to give the sane implementation more thought
http_response: ?std.http.Server.Response = null,
@@ -79,13 +79,10 @@ fn headersInit(res: *Response) !void {
}
 
pub fn headersAdd(res: *Response, comptime name: []const u8, value: []const u8) !void {
if (res.headers) |*headers| {
try headers.add(name, value);
} else return Error.HeadersFinished;
try res.headers.add(name, value);
}
 
pub fn start(res: *Response) !void {
if (res.headers == null) return Error.WrongPhase;
if (res.status == null) res.status = .ok;
switch (res.downstream) {
.http => {
@@ -117,27 +114,22 @@ pub fn sendHeaders(res: *Response) !void {
switch (res.downstream) {
.http => try res.http_response.?.flush(),
.zwsgi, .buffer => {
if (res.headers) |*headers| {
try res.sendHTTPHeader();
var itr = headers.index.iterator();
while (itr.next()) |header| {
var buf: [512]u8 = undefined;
const b = try std.fmt.bufPrint(&buf, "{s}: {s}\r\n", .{
header.key_ptr.*,
header.value_ptr.str,
});
_ = try res.write(b);
}
_ = try res.write("Transfer-Encoding: chunked\r\n");
} else return error.WrongPhase;
try res.sendHTTPHeader();
var itr = res.headers.headers.iterator();
while (itr.next()) |header| {
var buf: [512]u8 = undefined;
const b = try std.fmt.bufPrint(&buf, "{s}: {s}\r\n", .{
header.key_ptr.*,
header.value_ptr.*.value,
});
_ = try res.write(b);
}
_ = try res.write("Transfer-Encoding: chunked\r\n");
},
}
res.headers = null;
}
 
pub fn redirect(res: *Response, loc: []const u8, see_other: bool) !void {
if (res.headers == null) return error.WrongPhase;
 
try res.writeAll("HTTP/1.1 ");
if (see_other) {
try res.writeAll("303 See Other\r\n");
@@ -153,7 +145,6 @@ pub fn redirect(res: *Response, loc: []const u8, see_other: bool) !void {
/// Do not use
/// TODO remove
pub fn send(res: *Response, data: []const u8) !void {
if (res.headers != null) try res.start();
try res.writeAll(data);
return res.finish();
}
 
src/zwsgi.zig added: 252, removed: 125, total 127
@@ -89,7 +89,7 @@ pub fn serve(z: *zWSGI) !void {
const a = arena.allocator();
 
var zreq = try readHeader(a, &acpt);
var request = try Request.init(a, &zreq);
var request = try Request.initZWSGI(a, &zreq);
var verse = try buildVerse(a, &request);
 
defer {