Skip to content

Commit

Permalink
Refactored request, auth, endpoint:
Browse files Browse the repository at this point in the history
- zap.Request : refactored into its own file, along with supporting
  types and functions (e.g. http params related)
    - added setContentTypeFromFilename thx @hauleth.
- zap.Auth : zap.Auth.Basic, zap.Auth.BearerSingle, ...
- zap.Endpoint : zap.Endpoint, zap.Endpoint.Authenticating
  • Loading branch information
renerocksai committed Jan 10, 2024
1 parent 214cfc5 commit 07c74e7
Show file tree
Hide file tree
Showing 12 changed files with 1,145 additions and 1,120 deletions.
30 changes: 15 additions & 15 deletions doc/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,23 @@ Zap supports both Basic and Bearer authentication which are based on HTTP
headers.

For a cookie-based ("session token", not to mistake for "session cookie")
authentication, see the [UserPassSessionAuth](../src/http_auth.zig#L319) and its
authentication, see the [UserPassSession](../src/http_auth.zig#L319) and its
[example](../examples/userpass_session_auth/).

For convenience, Authenticator types exist that can authenticate requests.

Zap also provides an `AuthenticatingEndpoint` endpoint-wrapper. Have a look at the [example](../examples/endpoint_auth) and the [tests](../src/tests/test_auth.zig).
Zap also provides an `Endpoint.Authenticating` endpoint-wrapper. Have a look at the [example](../examples/endpoint_auth) and the [tests](../src/tests/test_auth.zig).

The following describes the Authenticator types. All of them provide the
`authenticateRequest()` function, which takes a `zap.Request` 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`.
`Endpoint.Authenticating`.

## Basic Authentication

The `zap.BasicAuth` Authenticator accepts 2 comptime values:
The `zap.Auth.Basic` 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)
Expand All @@ -35,11 +35,11 @@ support `contains([]const u8)`.

## Bearer Authentication

The `zap.BearerAuthSingle` Authenticator is a convenience-authenticator that
The `zap.Auth.BearerSingle` 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
`zap.BearerMulti` accepts a map (`Lookup`) that needs to support
`contains([]const u8)`.

## Request Authentication
Expand All @@ -56,7 +56,7 @@ const zap = @import("zap");
const allocator = std.heap.page_allocator;
const token = "hello, world";
var auth = try zap.BearerAuthSingle.init(allocator, token, null);
var auth = try zap.Auth.BearerSingle.init(allocator, token, null);
defer auth.deinit();
Expand Down Expand Up @@ -90,7 +90,7 @@ defer set.deinit();
// insert auth tokens
try set.put(token, {});
var auth = try zap.BearerAuthMulti(Set).init(allocator, &set, null);
var auth = try zap.Auth.BearerMulti(Set).init(allocator, &set, null);
defer auth.deinit();
Expand Down Expand Up @@ -127,7 +127,7 @@ const pass = "opensesame";
try map.put(user, pass);
// create authenticator
const Authenticator = zap.BasicAuth(Map, .UserPass);
const Authenticator = zap.Auth.Basic(Map, .UserPass);
var auth = try Authenticator.init(a, &map, null);
defer auth.deinit();
Expand Down Expand Up @@ -163,7 +163,7 @@ defer set.deinit();
try set.put(token, {});
// create authenticator
const Authenticator = zap.BasicAuth(Set, .Token68);
const Authenticator = zap.Auth.Basic(Set, .Token68);
var auth = try Authenticator.init(allocator, &set, null);
defer auth.deinit();
Expand All @@ -182,15 +182,15 @@ fn on_request(r: zap.Request) void {
}
```

## AuthenticatingEndpoint
## Endpoint.Authenticating

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 `Endpoint.Authenticating` 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`:
`Endpoint.Authenticating`:

```zig
const std = @import("std");
Expand Down Expand Up @@ -240,12 +240,12 @@ pub fn main() !void {
});
// create authenticator
const Authenticator = zap.BearerAuthSingle;
const Authenticator = zap.Auth.BearerSingle;
var authenticator = try Authenticator.init(a, token, null);
defer authenticator.deinit();
// create authenticating endpoint
const BearerAuthEndpoint = zap.AuthenticatingEndpoint(Authenticator);
const BearerAuthEndpoint = zap.Endpoint.Authenticating(Authenticator);
var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator);
try listener.register(auth_ep.endpoint());
Expand Down
4 changes: 2 additions & 2 deletions examples/bindataformpost/bindataformpost.zig
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const Handler = struct {
std.log.info("Param `{s}` in owned list is {any}\n", .{ kv.key.str, v });
switch (v) {
// single-file upload
zap.HttpParam.Hash_Binfile => |*file| {
zap.Request.HttpParam.Hash_Binfile => |*file| {
const filename = file.filename orelse "(no filename)";
const mimetype = file.mimetype orelse "(no mimetype)";
const data = file.data orelse "";
Expand All @@ -42,7 +42,7 @@ const Handler = struct {
std.log.debug(" contents: {any}\n", .{data});
},
// multi-file upload
zap.HttpParam.Array_Binfile => |*files| {
zap.Request.HttpParam.Array_Binfile => |*files| {
for (files.*.items) |file| {
const filename = file.filename orelse "(no filename)";
const mimetype = file.mimetype orelse "(no mimetype)";
Expand Down
2 changes: 1 addition & 1 deletion examples/endpoint/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub fn main() !void {
// we scope everything that can allocate within this block for leak detection
{
// setup listener
var listener = zap.EndpointListener.init(
var listener = zap.Endpoint.Listener.init(
allocator,
.{
.port = 3000,
Expand Down
6 changes: 3 additions & 3 deletions examples/endpoint_auth/endpoint_auth.zig
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ fn endpoint_http_unauthorized(e: *zap.Endpoint, r: zap.Request) void {

pub fn main() !void {
// setup listener
var listener = zap.EndpointListener.init(
var listener = zap.Endpoint.Listener.init(
a,
.{
.port = 3000,
Expand All @@ -45,12 +45,12 @@ pub fn main() !void {
});

// create authenticator
const Authenticator = zap.BearerAuthSingle;
const Authenticator = zap.Auth.BearerSingle;
var authenticator = try Authenticator.init(a, token, null);
defer authenticator.deinit();

// create authenticating endpoint
const BearerAuthEndpoint = zap.AuthenticatingEndpoint(Authenticator);
const BearerAuthEndpoint = zap.Endpoint.Authenticating(Authenticator);
var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator);

try listener.register(auth_ep.endpoint());
Expand Down
2 changes: 1 addition & 1 deletion examples/userpass_session_auth/userpass_session_auth.zig
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const Lookup = std.StringHashMap([]const u8);
const auth_lock_pw_table = false;

// see the source for more info
const Authenticator = zap.UserPassSessionAuth(
const Authenticator = zap.Auth.UserPassSession(
Lookup,
// we may set this to true if we expect our username -> password map
// to change. in that case the authenticator must lock the table for
Expand Down
140 changes: 49 additions & 91 deletions src/endpoint.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@ const std = @import("std");
const zap = @import("zap.zig");
const auth = @import("http_auth.zig");

const Endpoint = @This();

// zap types
const Request = zap.Request;
const ListenerSettings = zap.HttpListenerSettings;
const Listener = zap.HttpListener;
const HttpListener = zap.HttpListener;

/// Type of the request function callbacks.
pub const RequestFn = *const fn (self: *Endpoint, r: Request) void;

/// Settings to initialize an Endpoint
pub const EndpointSettings = struct {
pub const Settings = struct {
/// path / slug of the endpoint
path: []const u8,
/// callback to GET request handler
Expand All @@ -26,101 +28,57 @@ pub const EndpointSettings = struct {
patch: ?RequestFn = null,
/// callback to OPTIONS request handler
options: ?RequestFn = null,
/// Only applicable to AuthenticatingEndpoint: handler for unauthorized requests
/// Only applicable to Authenticating Endpoint: handler for unauthorized requests
unauthorized: ?RequestFn = null,
};

/// The simple Endpoint struct. Create one and pass in your callbacks. Then,
/// pass it to a HttpListener's `register()` function to register with the
/// listener.
///
/// **NOTE**: A common endpoint pattern for zap is to create your own struct
/// that embeds an Endpoint, provides specific callbacks, and uses
/// `@fieldParentPtr` to get a reference to itself.
///
/// Example:
/// A simple endpoint listening on the /stop route that shuts down zap.
/// The main thread usually continues at the instructions after the call to zap.start().
///
/// ```zig
/// const StopEndpoint = struct {
/// ep: zap.Endpoint = undefined,
///
/// pub fn init(
/// path: []const u8,
/// ) StopEndpoint {
/// return .{
/// .ep = zap.Endpoint.init(.{
/// .path = path,
/// .get = get,
/// }),
/// };
/// }
///
/// // access the internal Endpoint
/// pub fn endpoint(self: *StopEndpoint) *zap.Endpoint {
/// return &self.ep;
/// }
///
/// fn get(e: *zap.Endpoint, r: zap.Request) void {
/// const self: *StopEndpoint = @fieldParentPtr(StopEndpoint, "ep", e);
/// _ = self;
/// _ = r;
/// zap.stop();
/// }
/// };
/// ```
pub const Endpoint = struct {
settings: EndpointSettings,

const Self = @This();
settings: Settings,

/// Initialize the endpoint.
/// Set only the callbacks you need. Requests of HTTP methods without a
/// provided callback will be ignored.
pub fn init(s: EndpointSettings) Self {
return .{
.settings = .{
.path = s.path,
.get = s.get orelse &nop,
.post = s.post orelse &nop,
.put = s.put orelse &nop,
.delete = s.delete orelse &nop,
.patch = s.patch orelse &nop,
.options = s.options orelse &nop,
.unauthorized = s.unauthorized orelse &nop,
},
};
}
/// Initialize the endpoint.
/// Set only the callbacks you need. Requests of HTTP methods without a
/// provided callback will be ignored.
pub fn init(s: Settings) Endpoint {
return .{
.settings = .{
.path = s.path,
.get = s.get orelse &nop,
.post = s.post orelse &nop,
.put = s.put orelse &nop,
.delete = s.delete orelse &nop,
.patch = s.patch orelse &nop,
.options = s.options orelse &nop,
.unauthorized = s.unauthorized orelse &nop,
},
};
}

// no operation. Dummy handler function for ignoring unset request types.
fn nop(self: *Endpoint, r: Request) void {
_ = self;
_ = r;
}
// no operation. Dummy handler function for ignoring unset request types.
fn nop(self: *Endpoint, r: Request) void {
_ = self;
_ = r;
}

/// The global request handler for this Endpoint, called by the listener.
pub fn onRequest(self: *Endpoint, r: zap.Request) void {
if (r.method) |m| {
if (std.mem.eql(u8, m, "GET"))
return self.settings.get.?(self, r);
if (std.mem.eql(u8, m, "POST"))
return self.settings.post.?(self, r);
if (std.mem.eql(u8, m, "PUT"))
return self.settings.put.?(self, r);
if (std.mem.eql(u8, m, "DELETE"))
return self.settings.delete.?(self, r);
if (std.mem.eql(u8, m, "PATCH"))
return self.settings.patch.?(self, r);
if (std.mem.eql(u8, m, "OPTIONS"))
return self.settings.options.?(self, r);
}
/// The global request handler for this Endpoint, called by the listener.
pub fn onRequest(self: *Endpoint, r: zap.Request) void {
if (r.method) |m| {
if (std.mem.eql(u8, m, "GET"))
return self.settings.get.?(self, r);
if (std.mem.eql(u8, m, "POST"))
return self.settings.post.?(self, r);
if (std.mem.eql(u8, m, "PUT"))
return self.settings.put.?(self, r);
if (std.mem.eql(u8, m, "DELETE"))
return self.settings.delete.?(self, r);
if (std.mem.eql(u8, m, "PATCH"))
return self.settings.patch.?(self, r);
if (std.mem.eql(u8, m, "OPTIONS"))
return self.settings.options.?(self, r);
}
};
}

/// Wrap an endpoint with an Authenticator -> new Endpoint of type Endpoint
/// is available via the `endpoint()` function.
pub fn AuthenticatingEndpoint(comptime Authenticator: type) type {
pub fn Authenticating(comptime Authenticator: type) type {
return struct {
authenticator: *Authenticator,
ep: *Endpoint,
Expand Down Expand Up @@ -289,8 +247,8 @@ pub const EndpointListenerError = error{
/// The listener with ednpoint support
///
/// NOTE: It switches on path.startsWith -> so use endpoints with distinctly starting names!!
pub const EndpointListener = struct {
listener: Listener,
pub const Listener = struct {
listener: HttpListener,
allocator: std.mem.Allocator,

const Self = @This();
Expand All @@ -314,12 +272,12 @@ pub const EndpointListener = struct {

// override the settings with our internal, actul callback function
// so that "we" will be called on request
ls.on_request = onRequest;
ls.on_request = Listener.onRequest;

// store the settings-provided request callback for later use
on_request = l.on_request;
return .{
.listener = Listener.init(ls),
.listener = HttpListener.init(ls),
.allocator = a,
};
}
Expand Down
Loading

0 comments on commit 07c74e7

Please sign in to comment.