From b45a7df6682b6299dc46cc5cd5443dc110fcb028 Mon Sep 17 00:00:00 2001 From: Rene Schallner Date: Sat, 6 May 2023 04:02:03 +0200 Subject: [PATCH] cookies! --- README.md | 8 ++- build.zig | 1 + build.zig.zon | 6 +- examples/cookies/cookies.zig | 114 +++++++++++++++++++++++++++++++++++ flake.lock | 6 +- src/fio.zig | 44 +++++++++++++- src/zap.zig | 86 ++++++++++++++++++++++++-- 7 files changed, 247 insertions(+), 18 deletions(-) create mode 100644 examples/cookies/cookies.zig diff --git a/README.md b/README.md index ead1026..49d6c21 100644 --- a/README.md +++ b/README.md @@ -38,12 +38,14 @@ Here's what works: - **[endpoint](examples/endpoint/)**: a simple JSON REST API example featuring a `/users` endpoint for PUTting/DELETE-ing/GET-ting/POST-ing and listing users, together with a static HTML and JavaScript frontend to play with. -- **[mustache](examples/mustache/)**: a simple example using +- **[mustache](examples/mustache/mustache.zig)**: a simple example using [mustache](https://mustache.github.io/) templating. -- **[endpoint authentication](examples/endpoint_auth/)**: a simple authenticated +- **[endpoint authentication](examples/endpoint_auth/endpoint_auth.zig)**: a simple authenticated endpoint. Read more about authentication [here](./doc/authentication.md). -- **[http parameters](examples/http_parameters/)**: a simple example sending +- **[http parameters](examples/http_parameters/http_params.zig)**: a simple example sending itself query parameters of all supported types. +- **[cookies](examples/cookies/cookies.zig)**: a simple example sending + itself a cookie and responding with a session cookie. I'll continue wrapping more of facil.io's functionality and adding stuff to zap diff --git a/build.zig b/build.zig index 20fb821..0c7b4db 100644 --- a/build.zig +++ b/build.zig @@ -51,6 +51,7 @@ pub fn build(b: *std.build.Builder) !void { .{ .name = "mustache", .src = "examples/mustache/mustache.zig" }, .{ .name = "endpoint_auth", .src = "examples/endpoint_auth/endpoint_auth.zig" }, .{ .name = "http_params", .src = "examples/http_params/http_params.zig" }, + .{ .name = "cookies", .src = "examples/cookies/cookies.zig" }, }) |excfg| { const ex_name = excfg.name; const ex_src = excfg.src; diff --git a/build.zig.zon b/build.zig.zon index c922f76..841ee35 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,13 +1,9 @@ .{ .name = "zap", - .version = "0.0.14", + .version = "0.0.15", .dependencies = .{ .@"facil.io" = .{ - // temp workaround until zig's fetch is fixed, supporting GH's redirects - // .url = "http://localhost:8000/zap-0.0.7.tar.gz", - - // this is how it should be: .url = "https://github.com/zigzap/facil.io/archive/refs/tags/zap-0.0.7.tar.gz", .hash = "1220d03e0579bbb726efb8224ea289b26227bc421158b45c1b16a60b31bfa400ab33", diff --git a/examples/cookies/cookies.zig b/examples/cookies/cookies.zig new file mode 100644 index 0000000..5eb6f82 --- /dev/null +++ b/examples/cookies/cookies.zig @@ -0,0 +1,114 @@ +const std = @import("std"); +const zap = @import("zap"); + +// We send ourselves a request with a cookie +fn makeRequest(a: std.mem.Allocator, url: []const u8) !void { + const uri = try std.Uri.parse(url); + + var h = std.http.Headers{ .allocator = a }; + defer h.deinit(); + + var http_client: std.http.Client = .{ .allocator = a }; + defer http_client.deinit(); + + var req = try http_client.request(.GET, uri, h, .{}); + defer req.deinit(); + + try req.headers.append("cookie", "ZIG_ZAP=awesome"); + try req.start(); + try req.wait(); +} + +fn makeRequestThread(a: std.mem.Allocator, url: []const u8) !std.Thread { + return try std.Thread.spawn(.{}, makeRequest, .{ a, url }); +} + +// here we go +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{ + .thread_safe = true, + }){}; + var allocator = gpa.allocator(); + + const Handler = struct { + var alloc: std.mem.Allocator = undefined; + + pub fn on_request(r: zap.SimpleRequest) void { + std.debug.print("\n=====================================================\n", .{}); + defer std.debug.print("=====================================================\n\n", .{}); + + r.parseCookies(false); + + var cookie_count = r.getCookiesCount(); + std.log.info("cookie_count: {}", .{cookie_count}); + + // iterate over all cookies as strings + var strCookies = r.cookiesToOwnedStrList(alloc, false) catch unreachable; + defer strCookies.deinit(); + std.debug.print("\n", .{}); + for (strCookies.items) |kv| { + std.log.info("CookieStr `{s}` is `{s}`", .{ kv.key.str, kv.value.str }); + } + + std.debug.print("\n", .{}); + + // // iterate over all cookies + const cookies = r.cookiesToOwnedList(alloc, false) catch unreachable; + defer cookies.deinit(); + for (cookies.items) |kv| { + std.log.info("cookie `{s}` is {any}", .{ kv.key.str, kv.value }); + } + + // let's get cookie "ZIG_ZAP" by name + std.debug.print("\n", .{}); + if (r.getCookieStr("ZIG_ZAP", alloc, false)) |maybe_str| { + if (maybe_str) |*s| { + defer s.deinit(); + + std.log.info("Cookie ZIG_ZAP = {s}", .{s.str}); + } else { + std.log.info("Cookie ZIG_ZAP not found!", .{}); + } + } + // since we provided "false" for duplicating strings in the call + // to getCookieStr(), there won't be an allocation error + else |err| { + std.log.err("cannot check for `ZIG_ZAP` cookie: {any}\n", .{err}); + } + + r.setCookie(.{ + .name = "rene", + .value = "rocksai", + // if we leave .max_age_s = 0 -> session cookie + // .max_age_s = 60, + // + // check out other params: domain, path, secure, http_only + }) catch unreachable; + + r.sendBody("Hello") catch unreachable; + } + }; + + Handler.alloc = allocator; + + // setup listener + var listener = zap.SimpleHttpListener.init( + .{ + .port = 3000, + .on_request = Handler.on_request, + .log = false, + .max_clients = 10, + .max_body_size = 1 * 1024, + }, + ); + zap.enableDebugLog(); + try listener.listen(); + std.log.info("\n\nTerminate with CTRL+C", .{}); + + const thread = try makeRequestThread(allocator, "http://127.0.0.1:3000"); + defer thread.join(); + zap.start(.{ + .threads = 1, + .workers = 0, + }); +} diff --git a/flake.lock b/flake.lock index 814eb82..3d91c81 100644 --- a/flake.lock +++ b/flake.lock @@ -166,11 +166,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1682641397, - "narHash": "sha256-gt/jGCv21Oju3R8t9uzq5c7l3EagYnWhKGlY+uNNtAM=", + "lastModified": 1683288458, + "narHash": "sha256-Avc/4kh6wb5C2C+kDcm8S8fnR1gkDOWW7RPulwaw1Tg=", "owner": "mitchellh", "repo": "zig-overlay", - "rev": "9de49969d69441e2fb50ab891d6fbd0cff0f0b5c", + "rev": "9f9baaee29b3615ef0d7bbe6f8367681ea8091cb", "type": "github" }, "original": { diff --git a/src/fio.zig b/src/fio.zig index 619db9f..6346c85 100644 --- a/src/fio.zig +++ b/src/fio.zig @@ -67,6 +67,47 @@ pub const http_s = extern struct { body: FIOBJ, udata: ?*anyopaque, }; // zig-cache/i/e0c8a6e617497ade13de512cbe191f23/include/http.h:153:12: warning: struct demoted to opaque type - has bitfield + +// typedef struct { +// /** The cookie's name (Symbol). */ +// const char *name; +// /** The cookie's value (leave blank to delete cookie). */ +// const char *value; +// /** The cookie's domain (optional). */ +// const char *domain; +// /** The cookie's path (optional). */ +// const char *path; +// /** The cookie name's size in bytes or a terminating NUL will be assumed.*/ +// size_t name_len; +// /** The cookie value's size in bytes or a terminating NUL will be assumed.*/ +// size_t value_len; +// /** The cookie domain's size in bytes or a terminating NUL will be assumed.*/ +// size_t domain_len; +// /** The cookie path's size in bytes or a terminating NULL will be assumed.*/ +// size_t path_len; +// /** Max Age (how long should the cookie persist), in seconds (0 == session).*/ +// int max_age; +// /** Limit cookie to secure connections.*/ +// unsigned secure : 1; +// /** Limit cookie to HTTP (intended to prevent javascript access/hijacking).*/ +// unsigned http_only : 1; +// } http_cookie_args_s; + +pub const http_cookie_args_s = extern struct { + name: [*c]u8, + value: [*c]u8, + domain: [*c]u8, + path: [*c]u8, + name_len: isize, + value_len: isize, + domain_len: isize, + path_len: isize, + /// in seconds + max_age: c_int, + secure: c_uint, + http_only: c_uint, +}; + pub const struct_fio_str_info_s = extern struct { capa: usize, len: usize, @@ -289,7 +330,8 @@ pub fn fiobj_obj2cstr(o: FIOBJ) callconv(.C) fio_str_info_s { } return fiobj_type_vtable(o).*.to_str.?(o); } -pub const http_cookie_args_s = opaque {}; +// pub const http_cookie_args_s = opaque {}; + pub extern fn http_set_header(h: [*c]http_s, name: FIOBJ, value: FIOBJ) c_int; pub extern fn http_set_header2(h: [*c]http_s, name: fio_str_info_s, value: fio_str_info_s) c_int; pub extern fn http_set_cookie(h: [*c]http_s, http_cookie_args_s) c_int; diff --git a/src/zap.zig b/src/zap.zig index f8ee503..5afcff1 100644 --- a/src/zap.zig +++ b/src/zap.zig @@ -50,6 +50,7 @@ pub const HttpError = error{ HttpSetHeader, HttpParseBody, HttpIterParams, + SetCookie, }; pub const ContentType = enum { @@ -180,15 +181,51 @@ pub const SimpleRequest = struct { /// /// Result is accessible via parametersToOwnedSlice(), parametersToOwnedStrSlice() pub fn parseQuery(self: *const Self) void { - return fio.http_parse_query(self.h); + fio.http_parse_query(self.h); } - /// not implemented. - pub fn parseCookies() !void {} + pub fn parseCookies(self: *const Self, url_encoded: bool) void { + fio.http_parse_cookies(self.h, if (url_encoded) 1 else 0); + } - /// not implemented. - pub fn getCookie(name: []const u8) ?[]const u8 { - _ = name; + // Set a response cookie + pub fn setCookie(self: *const Self, args: CookieArgs) HttpError!void { + var c: fio.http_cookie_args_s = .{ + .name = util.toCharPtr(args.name), + .name_len = @intCast(isize, args.name.len), + .value = util.toCharPtr(args.value), + .value_len = @intCast(isize, args.value.len), + .domain = if (args.domain) |p| util.toCharPtr(p) else null, + .domain_len = if (args.domain) |p| @intCast(isize, p.len) else 0, + .path = if (args.path) |p| util.toCharPtr(p) else null, + .path_len = if (args.path) |p| @intCast(isize, p.len) else 0, + .max_age = args.max_age_s, + .secure = if (args.secure) 1 else 0, + .http_only = if (args.http_only) 1 else 0, + }; + if (fio.http_set_cookie(self.h, c) == -1) { + return error.SetCookie; + } + } + + /// Returns named cookie. Works like getParamStr() + pub fn getCookieStr(self: *const Self, name: []const u8, a: std.mem.Allocator, always_alloc: bool) !?util.FreeOrNot { + if (self.h.*.cookies == 0) return null; + const key = fio.fiobj_str_new(name.ptr, name.len); + defer fio.fiobj_free_wrapped(key); + const value = fio.fiobj_hash_get(self.h.*.cookies, key); + if (value == fio.FIOBJ_INVALID) { + return null; + } + return try util.fio2strAllocOrNot(value, a, always_alloc); + } + + /// Returns the number of parameters after parsing. + /// + /// Parse with parseCookies() + pub fn getCookiesCount(self: *const Self) isize { + if (self.h.*.cookies == 0) return 0; + return fio.fiobj_obj2num(self.h.*.cookies); } /// Returns the number of parameters after parsing. @@ -199,6 +236,32 @@ pub const SimpleRequest = struct { return fio.fiobj_obj2num(self.h.*.params); } + /// Same as parametersToOwnedStrList() but for cookies + pub fn cookiesToOwnedStrList(self: *const Self, a: std.mem.Allocator, always_alloc: bool) anyerror!HttpParamStrKVList { + var params = try std.ArrayList(HttpParamStrKV).initCapacity(a, @intCast(usize, self.getCookiesCount())); + var context: _parametersToOwnedStrSliceContext = .{ + .params = ¶ms, + .allocator = a, + .always_alloc = always_alloc, + }; + const howmany = fio.fiobj_each1(self.h.*.cookies, 0, _each_nextParamStr, &context); + if (howmany != self.getCookiesCount()) { + return error.HttpIterParams; + } + return .{ .items = try params.toOwnedSlice(), .allocator = a }; + } + + /// Same as parametersToOwnedList() but for cookies + pub fn cookiesToOwnedList(self: *const Self, a: std.mem.Allocator, dupe_strings: bool) !HttpParamKVList { + var params = try std.ArrayList(HttpParamKV).initCapacity(a, @intCast(usize, self.getCookiesCount())); + var context: _parametersToOwnedSliceContext = .{ .params = ¶ms, .allocator = a, .dupe_strings = dupe_strings }; + const howmany = fio.fiobj_each1(self.h.*.cookies, 0, _each_nextParam, &context); + if (howmany != self.getCookiesCount()) { + return error.HttpIterParams; + } + return .{ .items = try params.toOwnedSlice(), .allocator = a }; + } + /// Returns the query / body parameters as key/value pairs, as strings. /// Supported param types that will be converted: /// @@ -419,6 +482,17 @@ pub fn Fiobj2HttpParam(o: fio.FIOBJ, a: std.mem.Allocator, dupe_string: bool) !? }; } +pub const CookieArgs = struct { + name: []const u8, + value: []const u8, + domain: ?[]const u8 = null, + path: ?[]const u8 = null, + /// max age in seconds. 0 -> session + max_age_s: c_int = 0, + secure: bool = true, + http_only: bool = true, +}; + pub const HttpRequestFn = *const fn (r: [*c]fio.http_s) callconv(.C) void; pub const SimpleHttpRequestFn = *const fn (SimpleRequest) void;