From 7e2c3b6251aec1386f5da6f2bf633cf01e2aaf87 Mon Sep 17 00:00:00 2001 From: Rene Schallner Date: Sun, 16 Apr 2023 23:42:08 +0200 Subject: [PATCH] more tests, cleanup --- doc/authentication.md | 253 +++++++++++++++++++++++++++++++++ src/http_auth.zig | 6 +- src/http_client_testrunner.zig | 24 ++++ src/test_auth.zig | 133 +++++++++++++++-- src/zap.zig | 1 + 5 files changed, 400 insertions(+), 17 deletions(-) create mode 100644 doc/authentication.md diff --git a/doc/authentication.md b/doc/authentication.md new file mode 100644 index 0000000..2472b24 --- /dev/null +++ b/doc/authentication.md @@ -0,0 +1,253 @@ +# Authentication + +Zap supports both Basic and Bearer authentication. + +For convenience, Authenticator types exist that can authenticate requests. + +Zap also provides an `AuthenticatingEndpoint` endpoint-wrapper. + +Have a look at the tests: [here](../src/test_auth.zig) + +The following describes the Authenticator types. All of them provide the +`authenticateRequest()` function, which takes a `zap.SimpleRequest` and returns +a bool value whether it could be authenticated or not. + +Further down, we show how to use the Authenticators, and also the +`AuthenticatingEndpoint`. + +## Basic Authentication + +The `zap.BasicAuth` Authenticator accepts 2 comptime values: + +- `Lookup`: either a map to look up passwords for users or a set to lookup + base64 encoded tokens (user:pass -> base64-encode = token) +- `kind` : + - `UserPass` : decode the authentication header, split into user and + password, then lookup the password in the provided map and compare it. + - `Token68` : don't bother decoding, the 'lookup' set is filled with + base64-encoded tokens, so a fast lookup is enough. + +Maps passed in as `Lookup` type must support `get([]const u8)`, and sets must +support `contains([]const u8)`. + +## Bearer Authentication + +The `zap.BearerAuthSingle` Authenticator is a convenience-authenticator that +takes a single auth token. If all you need is to protect your prototype with a +token, this is the one you want to use. + +`zap.BearerAuthMulti` accepts a map (`Lookup`) that needs to support +`contains([]const u8)`. + +## Request Authentication + +Here we describe how to authenticate requests from within your `on_request` +callback. + +### Single-Token Bearer Authentication + +```zig +const std = @import("std"); +const zap = @import("zap"); + +const allocator = std.heap.page_allocator; +const token = "hello, world"; + +var auth = try zap.BearerAuthSingle.init(allocator, token, null); +defer auth.deinit(); + + +fn on_request(r: zap.SimpleRequest) void { + if(authenticator.authenticateRequest(r)) { + r.sendBody( + \\ + \\

Hello from ZAP!!!

+ \\ + ) catch return; + } else { + r.setStatus(.unauthorized); + r.sendBody("UNAUTHORIZED") catch return; + } +} +``` + +### Multi-Token Bearer Authentication + +```zig +const std = @import("std"); +const zap = @import("zap"); + +const allocator = std.heap.page_allocator; +const token = "hello, world"; + +const Set = std.StringHashMap(void); +var set = Set.init(allocator); // set +defer set.deinit(); + +// insert auth tokens +try set.put(token, {}); + +var auth = try zap.BearerAuthMulti(Set).init(allocator, &set, null); +defer auth.deinit(); + + +fn on_request(r: zap.SimpleRequest) void { + if(authenticator.authenticateRequest(r)) { + r.sendBody( + \\ + \\

Hello from ZAP!!!

+ \\ + ) catch return; + } else { + r.setStatus(.unauthorized); + r.sendBody("UNAUTHORIZED") catch return; + } +} +``` + +### UserPass Basic Authentication + +```zig +const std = @import("std"); +const zap = @import("zap"); + +const allocator = std.heap.page_allocator; + +// create a set of User -> Pass entries +const Map = std.StringHashMap([]const u8); +var map = Map.init(allocator); +defer map.deinit(); + +// create user / pass entry +const user = "Alladdin"; +const pass = "opensesame"; +try map.put(user, pass); + +// create authenticator +const Authenticator = zap.BasicAuth(Map, .UserPass); +var auth = try Authenticator.init(a, &map, null); +defer auth.deinit(); + + +fn on_request(r: zap.SimpleRequest) void { + if(authenticator.authenticateRequest(r)) { + r.sendBody( + \\ + \\

Hello from ZAP!!!

+ \\ + ) catch return; + } else { + r.setStatus(.unauthorized); + r.sendBody("UNAUTHORIZED") catch return; + } +} +``` + + +### Token68 Basic Authentication + +```zig +const std = @import("std"); +const zap = @import("zap"); + +const allocator = std.heap.page_allocator; +const token = "QWxhZGRpbjpvcGVuIHNlc2FtZQ=="; + +// create a set of Token68 entries +const Set = std.StringHashMap(void); +var set = Set.init(allocator); // set +defer set.deinit(); +try set.put(token, {}); + +// create authenticator +const Authenticator = zap.BasicAuth(Set, .Token68); +var auth = try Authenticator.init(allocator, &set, null); +defer auth.deinit(); + + +fn on_request(r: zap.SimpleRequest) void { + if(authenticator.authenticateRequest(r)) { + r.sendBody( + \\ + \\

Hello from ZAP!!!

+ \\ + ) catch return; + } else { + r.setStatus(.unauthorized); + r.sendBody("UNAUTHORIZED") catch return; + } +} +``` + +## AuthenticatingEndpoint + +Here, we only show using one of the Authenticator types. See the tests for more +examples. + +The `AuthenticatingEndpoint` honors `.unauthorized` in the endpoint settings, where you can pass in a callback to deal with unauthorized requests. If you leave it to `null`, the endpoint will automatically reply with a `401 - Unauthorized` response. + +The example below should make clear how to wrap an endpoint into an +`AuthenticatingEndpoint`: + +```zig +const std = @import("std"); +const zap = @import("zap"); + +const a = std.heap.page_allocator; +const token = "ABCDEFG"; + +// authenticated requests go here +fn endpoint_http_get(e: *zap.SimpleEndpoint, r: zap.SimpleRequest) void { + _ = e; + r.sendBody(HTTP_RESPONSE) catch return; + received_response = HTTP_RESPONSE; + zap.fio_stop(); +} + +// just for fun, we also catch the unauthorized callback +fn endpoint_http_unauthorized(e: *zap.SimpleEndpoint, r: zap.SimpleRequest) void { + _ = e; + r.setStatus(.unauthorized); + r.sendBody("UNAUTHORIZED ACCESS") catch return; + std.debug.print("\nunauthorized\n", .{}); + received_response = "UNAUTHORIZED"; + zap.fio_stop(); +} + + +// setup listener +var listener = zap.SimpleEndpointListener.init( + a, + .{ + .port = 3000, + .on_request = null, + .log = false, + .max_clients = 10, + .max_body_size = 1 * 1024, + }, +); +defer listener.deinit(); + +// create mini endpoint +var ep = zap.SimpleEndpoint.init(.{ + .path = "/test", + .get = endpoint_http_get, + .unauthorized = endpoint_http_unauthorized, +}); + +// create authenticator +const Authenticator = zap.BearerAuthSingle; +var authenticator = try Authenticator.init(a, token, null); +defer authenticator.deinit(); + +// create authenticating endpoint +const BearerAuthEndpoint = zap.AuthenticatingEndpoint(Authenticator); +var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); + +try listener.addEndpoint(auth_ep.getEndpoint()); + +listener.listen() catch {}; +std.debug.print("Listening on 0.0.0.0:3000\n", .{}); +``` + + diff --git a/src/http_auth.zig b/src/http_auth.zig index 6a384de..7590329 100644 --- a/src/http_auth.zig +++ b/src/http_auth.zig @@ -204,10 +204,10 @@ pub const BearerAuthSingle = struct { /// Errors: /// HTTP/1.1 401 Unauthorized /// WWW-Authenticate: Bearer realm="example", error="invalid_token", error_description="..." -pub fn BearerAuthMulti(comptime T: type) type { +pub fn BearerAuthMulti(comptime Lookup: type) type { return struct { allocator: std.mem.Allocator, - lookup: *T, + lookup: *Lookup, realm: ?[]const u8, const Self = @This(); @@ -215,7 +215,7 @@ pub fn BearerAuthMulti(comptime T: type) type { /// Creates a BasicAuth. `lookup` must implement `.get([]const u8) -> []const u8` /// to look up tokens /// if realm is provided (not null), a copy is taken -> call deinit() to clean up - pub fn init(allocator: std.mem.Allocator, lookup: *T, realm: ?[]const u8) !Self { + pub fn init(allocator: std.mem.Allocator, lookup: *Lookup, realm: ?[]const u8) !Self { return .{ .allocator = allocator, .lookup = lookup, diff --git a/src/http_client_testrunner.zig b/src/http_client_testrunner.zig index 20b91ab..b2790d3 100644 --- a/src/http_client_testrunner.zig +++ b/src/http_client_testrunner.zig @@ -2,6 +2,8 @@ const std = @import("std"); pub fn main() !void { const a = std.heap.page_allocator; + + // Bearer Single var p = std.ChildProcess.init(&.{ "./zig-out/bin/http_client", "http://127.0.0.1:3000/test", @@ -22,6 +24,28 @@ pub fn main() !void { std.time.sleep(3 * std.time.ns_per_s); + // Bearer Multi + p = std.ChildProcess.init(&.{ + "./zig-out/bin/http_client", + "http://127.0.0.1:3000/test", + "Bearer", + "ABCDEFG", + }, a); + _ = try p.spawnAndWait(); + + std.time.sleep(3 * std.time.ns_per_s); + + p = std.ChildProcess.init(&.{ + "./zig-out/bin/http_client", + "http://127.0.0.1:3000/test", + "Bearer", + "invalid", + }, a); + _ = try p.spawnAndWait(); + + std.time.sleep(3 * std.time.ns_per_s); + + // Basic p = std.ChildProcess.init(&.{ "./zig-out/bin/http_client", "http://127.0.0.1:3000/test", diff --git a/src/test_auth.zig b/src/test_auth.zig index 0e97fac..6c2091b 100644 --- a/src/test_auth.zig +++ b/src/test_auth.zig @@ -23,12 +23,13 @@ test "BearerAuthMulti authenticate" { const a = std.testing.allocator; const token = "hello, world"; - var map = std.StringHashMap(void).init(a); // set - defer map.deinit(); + const Set = std.StringHashMap(void); + var set = Set.init(a); // set + defer set.deinit(); - try map.put(token, {}); + try set.put(token, {}); - var auth = try Authenticators.BearerAuthMulti(@TypeOf(map)).init(a, &map, null); + var auth = try Authenticators.BearerAuthMulti(Set).init(a, &set, null); defer auth.deinit(); // invalid auth header @@ -65,7 +66,7 @@ test "BasicAuth UserPass" { // create a set of User -> Pass entries const Map = std.StringHashMap([]const u8); - var map = Map.init(a); // set + var map = Map.init(a); defer map.deinit(); // create user / pass entry @@ -287,6 +288,61 @@ test "BearerAuthSingle authenticateRequest test-unauthorized" { .unauthorized = endpoint_http_unauthorized, }); + const Set = std.StringHashMap(void); + var set = Set.init(a); // set + defer set.deinit(); + + // insert auth tokens + try set.put(token, {}); + + const Authenticator = Authenticators.BearerAuthMulti(Set); + var authenticator = try Authenticator.init(a, &set, null); + defer authenticator.deinit(); + + // create authenticating endpoint + const BearerAuthEndpoint = Endpoints.AuthenticatingEndpoint(Authenticator); + var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); + + try listener.addEndpoint(auth_ep.getEndpoint()); + + listener.listen() catch {}; + std.debug.print("Listening on 0.0.0.0:3000\n", .{}); + std.debug.print("Waiting for the following:\n", .{}); + std.debug.print("./zig-out/bin/http_client http://127.0.0.1:3000/test Bearer invalid", .{}); + + // start worker threads + zap.start(.{ + .threads = 1, + .workers = 0, + }); + + try std.testing.expectEqualStrings("UNAUTHORIZED", received_response); +} + +test "BearerAuthMulti authenticateRequest OK" { + const a = std.testing.allocator; + const token = "ABCDEFG"; + + // setup listener + var listener = zap.SimpleEndpointListener.init( + a, + .{ + .port = 3000, + .on_request = null, + .log = false, + .max_clients = 10, + .max_body_size = 1 * 1024, + }, + ); + defer listener.deinit(); + + // create mini endpoint + var ep = Endpoints.SimpleEndpoint.init(.{ + .path = "/test", + .get = endpoint_http_get, + .unauthorized = endpoint_http_unauthorized, + }); + // create authenticator const Authenticator = Authenticators.BearerAuthSingle; var authenticator = try Authenticator.init(a, token, null); @@ -300,8 +356,8 @@ test "BearerAuthSingle authenticateRequest test-unauthorized" { listener.listen() catch {}; std.debug.print("Listening on 0.0.0.0:3000\n", .{}); - std.debug.print("Please run the following:\n", .{}); - std.debug.print("./zig-out/bin/http_client http://127.0.0.1:3000/test Bearer invalid", .{}); + std.debug.print("Waiting for the following:\n", .{}); + std.debug.print("./zig-out/bin/http_client_runner http://127.0.0.1:3000/test Bearer invalid\n", .{}); // start worker threads zap.start(.{ @@ -309,7 +365,56 @@ test "BearerAuthSingle authenticateRequest test-unauthorized" { .workers = 0, }); - try std.testing.expectEqualStrings("UNAUTHORIZED", received_response); + try std.testing.expectEqualStrings(HTTP_RESPONSE, received_response); +} + +test "BearerAuthMulti authenticateRequest test-unauthorized" { + const a = std.testing.allocator; + const token = "invalid"; + + // setup listener + var listener = zap.SimpleEndpointListener.init( + a, + .{ + .port = 3000, + .on_request = null, + .log = false, + .max_clients = 10, + .max_body_size = 1 * 1024, + }, + ); + defer listener.deinit(); + + // create mini endpoint + var ep = Endpoints.SimpleEndpoint.init(.{ + .path = "/test", + .get = endpoint_http_get, + .unauthorized = endpoint_http_unauthorized, + }); + + // create authenticator + const Authenticator = Authenticators.BearerAuthSingle; + var authenticator = try Authenticator.init(a, token, null); + defer authenticator.deinit(); + + // create authenticating endpoint + const BearerAuthEndpoint = Endpoints.AuthenticatingEndpoint(Authenticator); + var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); + + try listener.addEndpoint(auth_ep.getEndpoint()); + + listener.listen() catch {}; + std.debug.print("Listening on 0.0.0.0:3000\n", .{}); + std.debug.print("Waiting for the following:\n", .{}); + std.debug.print("./zig-out/bin/http_client_runner http://127.0.0.1:3000/test Bearer invalid\n", .{}); + + // start worker threads + zap.start(.{ + .threads = 1, + .workers = 0, + }); + + try std.testing.expectEqualStrings(HTTP_RESPONSE, received_response); } test "BasicAuth Token68 authenticateRequest" { @@ -354,7 +459,7 @@ test "BasicAuth Token68 authenticateRequest" { listener.listen() catch {}; std.debug.print("Listening on 0.0.0.0:3000\n", .{}); - std.debug.print("Please run the following:\n", .{}); + std.debug.print("Waiting for the following:\n", .{}); std.debug.print("./zig-out/bin/http_client http://127.0.0.1:3000/test Basic " ++ token, .{}); // start worker threads @@ -408,7 +513,7 @@ test "BasicAuth Token68 authenticateRequest test-unauthorized" { listener.listen() catch {}; std.debug.print("Listening on 0.0.0.0:3000\n", .{}); - std.debug.print("Please run the following:\n", .{}); + std.debug.print("Waiting for the following:\n", .{}); std.debug.print("./zig-out/bin/http_client http://127.0.0.1:3000/test Basic " ++ "invalid", .{}); // start worker threads @@ -445,7 +550,7 @@ test "BasicAuth UserPass authenticateRequest" { // create a set of User -> Pass entries const Map = std.StringHashMap([]const u8); - var map = Map.init(a); // set + var map = Map.init(a); defer map.deinit(); // create user / pass entry @@ -473,7 +578,7 @@ test "BasicAuth UserPass authenticateRequest" { listener.listen() catch {}; std.debug.print("Listening on 0.0.0.0:3000\n", .{}); - std.debug.print("Please run the following:\n", .{}); + std.debug.print("Waiting for the following:\n", .{}); std.debug.print("./zig-out/bin/http_client http://127.0.0.1:3000/test Basic {s}\n", .{encoded}); // start worker threads @@ -510,7 +615,7 @@ test "BasicAuth UserPass authenticateRequest test-unauthorized" { // create a set of User -> Pass entries const Map = std.StringHashMap([]const u8); - var map = Map.init(a); // set + var map = Map.init(a); defer map.deinit(); // create user / pass entry @@ -538,7 +643,7 @@ test "BasicAuth UserPass authenticateRequest test-unauthorized" { listener.listen() catch {}; std.debug.print("Listening on 0.0.0.0:3000\n", .{}); - std.debug.print("Please run the following:\n", .{}); + std.debug.print("Waiting for the following:\n", .{}); std.debug.print("./zig-out/bin/http_client http://127.0.0.1:3000/test Basic {s}-invalid\n", .{encoded}); // start worker threads diff --git a/src/zap.zig b/src/zap.zig index 01a46a0..9284b1b 100644 --- a/src/zap.zig +++ b/src/zap.zig @@ -9,6 +9,7 @@ pub usingnamespace @import("endpoint.zig"); pub usingnamespace @import("util.zig"); pub usingnamespace @import("http.zig"); pub usingnamespace @import("mustache.zig"); +pub usingnamespace @import("http_auth.zig"); pub const Log = @import("log.zig");