Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cookies #9

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions src/http/cookie.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
const std = @import("std");

pub const Cookie = struct {
name: []const u8,
value: []const u8,
path: ?[]const u8 = null,
domain: ?[]const u8 = null,
// TODO: Use timstamp type instead?
expires: ?[]const u8 = null,
max_age: ?i64 = null,
secure: bool = false,
http_only: bool = false,
same_site: ?SameSite = null,

pub fn init(name: []const u8, value: []const u8) Cookie {
return .{
.name = name,
.value = value,
};
}

pub const SameSite = enum(u2) {
Strict,
Lax,
None,

pub fn toString(self: SameSite) []const u8 {
return switch (self) {
.Strict => "Strict",
.Lax => "Lax",
.None => "None",
};
}
};
};

pub const CookieMap = struct {
allocator: std.mem.Allocator,
map: std.StringHashMap([]const u8),

pub fn init(allocator: std.mem.Allocator) CookieMap {
return .{
.allocator = allocator,
.map = std.StringHashMap([]const u8).init(allocator),
};
}

pub fn deinit(self: *CookieMap) void {
var iter = self.map.iterator();
while (iter.next()) |entry| {
self.allocator.free(entry.key_ptr.*);
self.allocator.free(entry.value_ptr.*);
}
self.map.deinit();
}

pub fn clear(self: *CookieMap) void {
var iter = self.map.iterator();
while (iter.next()) |entry| {
self.allocator.free(entry.key_ptr.*);
self.allocator.free(entry.value_ptr.*);
}
self.map.clearRetainingCapacity();
}

pub fn get(self: CookieMap, name: []const u8) ?[]const u8 {
return self.map.get(name);
}

pub fn count(self: CookieMap) usize {
return self.map.count();
}

pub fn iterator(self: *const CookieMap) std.StringHashMap([]const u8).Iterator {
return self.map.iterator();
}

// For parsing request cookies (simple key=value pairs)
pub fn parseRequestCookies(self: *CookieMap, cookie_header: []const u8) !void {
self.clear();

var pairs = std.mem.splitSequence(u8, cookie_header, "; ");
while (pairs.next()) |pair| {
var kv = std.mem.splitScalar(u8, pair, '=');
const key = kv.next() orelse continue;
const value = kv.next() orelse continue;

if (kv.next() != null) {
continue;
}

const key_dup = try self.allocator.dupe(u8, key);
errdefer self.allocator.free(key_dup);
const value_dup = try self.allocator.dupe(u8, value);
errdefer self.allocator.free(value_dup);

if (try self.map.fetchPut(key_dup, value_dup)) |existing| {
self.allocator.free(existing.key);
self.allocator.free(existing.value);
}
}
}

pub fn formatSetCookie(cookie: Cookie, allocator: std.mem.Allocator) ![]const u8 {
var list = std.ArrayList(u8).init(allocator);
errdefer list.deinit();

try list.writer().print("{s}={s}", .{ cookie.name, cookie.value });

if (cookie.domain) |domain| {
try list.writer().print("; Domain={s}", .{domain});
}
if (cookie.path) |path| {
try list.writer().print("; Path={s}", .{path});
}
if (cookie.expires) |exp| {
try list.writer().print("; Expires={s}", .{exp});
}
if (cookie.max_age) |age| {
try list.writer().print("; Max-Age={d}", .{age});
}
if (cookie.same_site) |same_site| {
try list.writer().print("; SameSite={s}", .{same_site.toString()});
}
if (cookie.secure) {
try list.writer().writeAll("; Secure");
}
if (cookie.http_only) {
try list.writer().writeAll("; HttpOnly");
}

return list.toOwnedSlice();
}
};

const testing = std.testing;

test "Request Cookie Parsing" {
var cookie_map = CookieMap.init(testing.allocator);
defer cookie_map.deinit();

try cookie_map.parseRequestCookies("sessionId=abc123; java=slop");
try testing.expectEqualStrings("abc123", cookie_map.get("sessionId").?);
try testing.expectEqualStrings("slop", cookie_map.get("java").?);
}

test "Response Cookie Formatting" {
const cookie = Cookie{
.name = "session",
.value = "abc123",
.path = "/",
.domain = "example.com",
.secure = true,
.http_only = true,
.same_site = .Strict,
.max_age = 3600,
};

const formatted = try CookieMap.formatSetCookie(cookie, testing.allocator);
defer testing.allocator.free(formatted);

try testing.expectEqualStrings(
"session=abc123; Domain=example.com; Path=/; Max-Age=3600; SameSite=Strict; Secure; HttpOnly",
formatted,
);
}
2 changes: 2 additions & 0 deletions src/http/lib.zig
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub const Cookie = @import("cookie.zig").Cookie;
pub const CookieMap = @import("cookie.zig").CookieMap;
pub const Status = @import("status.zig").Status;
pub const Method = @import("method.zig").Method;
pub const Request = @import("request.zig").Request;
Expand Down
10 changes: 10 additions & 0 deletions src/http/request.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const std = @import("std");
const log = std.log.scoped(.@"zzz/http/request");
const assert = std.debug.assert;

const CookieMap = @import("cookie.zig").CookieMap;
const Headers = @import("lib.zig").Headers;
const HTTPError = @import("lib.zig").HTTPError;
const Method = @import("lib.zig").Method;
Expand All @@ -21,6 +22,7 @@ pub const Request = struct {
version: std.http.Version,
headers: Headers,
body: []const u8,
cookies: CookieMap,

/// This is for constructing a Request.
pub fn init(allocator: std.mem.Allocator, options: RequestOptions) !Request {
Expand All @@ -36,6 +38,7 @@ pub const Request = struct {
.uri = undefined,
.version = undefined,
.body = undefined,
.cookies = CookieMap.init(allocator),
};
}

Expand Down Expand Up @@ -91,6 +94,13 @@ pub const Request = struct {
try self.headers.add(key, value);
}
}

if (self.headers.get("Cookie")) |cookie_header| {
self.cookies.parseRequestCookies(cookie_header) catch |err| {
log.warn("failed to parse cookies: {}", .{err});
return HTTPError.MalformedRequest;
};
}
}

pub fn set_method(self: *Request, method: Method) void {
Expand Down
23 changes: 21 additions & 2 deletions src/http/response.zig
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const std = @import("std");
const assert = std.debug.assert;

const Cookie = @import("cookie.zig").Cookie;
const CookieMap = @import("cookie.zig").CookieMap;
const Headers = @import("lib.zig").Headers;
const Status = @import("lib.zig").Status;
const Mime = @import("lib.zig").Mime;
Expand All @@ -23,6 +25,7 @@ pub const Response = struct {
body: ?[]const u8 = null,
headers: Headers,
cached_date: CachedDate,
cookies: std.ArrayList(Cookie),

pub fn init(allocator: std.mem.Allocator, options: ResponseOptions) !Response {
return Response{
Expand All @@ -33,11 +36,13 @@ pub const Response = struct {
.index = 0,
.ts = 0,
},
.cookies = std.ArrayList(Cookie).init(allocator),
};
}

pub fn deinit(self: *Response) void {
self.headers.deinit();
self.cookies.deinit();
self.allocator.free(self.cached_date.buffer);
}

Expand All @@ -49,6 +54,12 @@ pub const Response = struct {
self.mime = mime;
}

pub fn set_cookie(self: *Response, cookie: Cookie) void {
self.cookies.append(cookie) catch {
return;
};
}

pub fn set_body(self: *Response, body: []const u8) void {
self.body = body;
}
Expand All @@ -57,6 +68,7 @@ pub const Response = struct {
self.status = null;
self.mime = null;
self.body = null;
self.cookies.clearAndFree();
}

const ResponseSetOptions = struct {
Expand Down Expand Up @@ -96,8 +108,7 @@ pub const Response = struct {
} else {
return error.MissingStatus;
}

try writer.writeAll("\r\n");
try writer.writeAll(" ");

// Standard Headers.

Expand Down Expand Up @@ -145,6 +156,14 @@ pub const Response = struct {
try writer.writeAll("\r\n");
}

// Set cookies
for (self.cookies.items) |cookie| {
const cookie_str = try CookieMap.formatSetCookie(cookie, self.allocator);
defer self.allocator.free(cookie_str);

try writer.print("Set-Cookie: {s}\r\n", .{cookie_str});
}

try writer.writeAll("Content-Length: ");
try std.fmt.formatInt(content_length, 10, .lower, .{}, writer);
try writer.writeAll("\r\n");
Expand Down