1
0
Fork 0
mirror of https://github.com/zigzap/zap.git synced 2025-10-20 15:14:08 +00:00

Refactored request, auth, endpoint: - 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

This commit is contained in:
Rene Schallner 2024-01-10 15:05:53 +01:00
parent 7141318caf
commit 61fbbe8b08
12 changed files with 1157 additions and 1129 deletions

View file

@ -4,23 +4,23 @@ Zap supports both Basic and Bearer authentication which are based on HTTP
headers. headers.
For a cookie-based ("session token", not to mistake for "session cookie") 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/). [example](../examples/userpass_session_auth/).
For convenience, Authenticator types exist that can authenticate requests. 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 The following describes the Authenticator types. All of them provide the
`authenticateRequest()` function, which takes a `zap.Request` and returns `authenticateRequest()` function, which takes a `zap.Request` and returns
a bool value whether it could be authenticated or not. a bool value whether it could be authenticated or not.
Further down, we show how to use the Authenticators, and also the Further down, we show how to use the Authenticators, and also the
`AuthenticatingEndpoint`. `Endpoint.Authenticating`.
## Basic Authentication ## 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 - `Lookup`: either a map to look up passwords for users or a set to lookup
base64 encoded tokens (user:pass -> base64-encode = token) base64 encoded tokens (user:pass -> base64-encode = token)
@ -35,11 +35,11 @@ support `contains([]const u8)`.
## Bearer Authentication ## 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 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. 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)`. `contains([]const u8)`.
## Request Authentication ## Request Authentication
@ -56,7 +56,7 @@ const zap = @import("zap");
const allocator = std.heap.page_allocator; const allocator = std.heap.page_allocator;
const token = "hello, world"; 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(); defer auth.deinit();
@ -90,7 +90,7 @@ defer set.deinit();
// insert auth tokens // insert auth tokens
try set.put(token, {}); 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(); defer auth.deinit();
@ -127,7 +127,7 @@ const pass = "opensesame";
try map.put(user, pass); try map.put(user, pass);
// create authenticator // create authenticator
const Authenticator = zap.BasicAuth(Map, .UserPass); const Authenticator = zap.Auth.Basic(Map, .UserPass);
var auth = try Authenticator.init(a, &map, null); var auth = try Authenticator.init(a, &map, null);
defer auth.deinit(); defer auth.deinit();
@ -163,7 +163,7 @@ defer set.deinit();
try set.put(token, {}); try set.put(token, {});
// create authenticator // create authenticator
const Authenticator = zap.BasicAuth(Set, .Token68); const Authenticator = zap.Auth.Basic(Set, .Token68);
var auth = try Authenticator.init(allocator, &set, null); var auth = try Authenticator.init(allocator, &set, null);
defer auth.deinit(); defer auth.deinit();
@ -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 Here, we only show using one of the Authenticator types. See the tests for more
examples. 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 The example below should make clear how to wrap an endpoint into an
`AuthenticatingEndpoint`: `Endpoint.Authenticating`:
```zig ```zig
const std = @import("std"); const std = @import("std");
@ -240,12 +240,12 @@ pub fn main() !void {
}); });
// create authenticator // create authenticator
const Authenticator = zap.BearerAuthSingle; const Authenticator = zap.Auth.BearerSingle;
var authenticator = try Authenticator.init(a, token, null); var authenticator = try Authenticator.init(a, token, null);
defer authenticator.deinit(); defer authenticator.deinit();
// create authenticating endpoint // create authenticating endpoint
const BearerAuthEndpoint = zap.AuthenticatingEndpoint(Authenticator); const BearerAuthEndpoint = zap.Endpoint.Authenticating(Authenticator);
var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator);
try listener.register(auth_ep.endpoint()); try listener.register(auth_ep.endpoint());

View file

@ -31,7 +31,7 @@ const Handler = struct {
std.log.info("Param `{s}` in owned list is {any}\n", .{ kv.key.str, v }); std.log.info("Param `{s}` in owned list is {any}\n", .{ kv.key.str, v });
switch (v) { switch (v) {
// single-file upload // single-file upload
zap.HttpParam.Hash_Binfile => |*file| { zap.Request.HttpParam.Hash_Binfile => |*file| {
const filename = file.filename orelse "(no filename)"; const filename = file.filename orelse "(no filename)";
const mimetype = file.mimetype orelse "(no mimetype)"; const mimetype = file.mimetype orelse "(no mimetype)";
const data = file.data orelse ""; const data = file.data orelse "";
@ -41,7 +41,7 @@ const Handler = struct {
std.log.debug(" contents: {any}\n", .{data}); std.log.debug(" contents: {any}\n", .{data});
}, },
// multi-file upload // multi-file upload
zap.HttpParam.Array_Binfile => |*files| { zap.Request.HttpParam.Array_Binfile => |*files| {
for (files.*.items) |file| { for (files.*.items) |file| {
const filename = file.filename orelse "(no filename)"; const filename = file.filename orelse "(no filename)";
const mimetype = file.mimetype orelse "(no mimetype)"; const mimetype = file.mimetype orelse "(no mimetype)";

View file

@ -21,7 +21,7 @@ pub fn main() !void {
// we scope everything that can allocate within this block for leak detection // we scope everything that can allocate within this block for leak detection
{ {
// setup listener // setup listener
var listener = zap.EndpointListener.init( var listener = zap.Endpoint.Listener.init(
allocator, allocator,
.{ .{
.port = 3000, .port = 3000,

View file

@ -25,7 +25,7 @@ fn endpoint_http_unauthorized(e: *zap.Endpoint, r: zap.Request) void {
pub fn main() !void { pub fn main() !void {
// setup listener // setup listener
var listener = zap.EndpointListener.init( var listener = zap.Endpoint.Listener.init(
a, a,
.{ .{
.port = 3000, .port = 3000,
@ -45,12 +45,12 @@ pub fn main() !void {
}); });
// create authenticator // create authenticator
const Authenticator = zap.BearerAuthSingle; const Authenticator = zap.Auth.BearerSingle;
var authenticator = try Authenticator.init(a, token, null); var authenticator = try Authenticator.init(a, token, null);
defer authenticator.deinit(); defer authenticator.deinit();
// create authenticating endpoint // create authenticating endpoint
const BearerAuthEndpoint = zap.AuthenticatingEndpoint(Authenticator); const BearerAuthEndpoint = zap.Endpoint.Authenticating(Authenticator);
var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator);
try listener.register(auth_ep.endpoint()); try listener.register(auth_ep.endpoint());

View file

@ -5,7 +5,7 @@ const Lookup = std.StringHashMap([]const u8);
const auth_lock_pw_table = false; const auth_lock_pw_table = false;
// see the source for more info // see the source for more info
const Authenticator = zap.UserPassSessionAuth( const Authenticator = zap.Auth.UserPassSession(
Lookup, Lookup,
// we may set this to true if we expect our username -> password map // 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 // to change. in that case the authenticator must lock the table for

View file

@ -2,16 +2,18 @@ const std = @import("std");
const zap = @import("zap.zig"); const zap = @import("zap.zig");
const auth = @import("http_auth.zig"); const auth = @import("http_auth.zig");
const Endpoint = @This();
// zap types // zap types
const Request = zap.Request; const Request = zap.Request;
const ListenerSettings = zap.HttpListenerSettings; const ListenerSettings = zap.HttpListenerSettings;
const Listener = zap.HttpListener; const HttpListener = zap.HttpListener;
/// Type of the request function callbacks. /// Type of the request function callbacks.
pub const RequestFn = *const fn (self: *Endpoint, r: Request) void; pub const RequestFn = *const fn (self: *Endpoint, r: Request) void;
/// Settings to initialize an Endpoint /// Settings to initialize an Endpoint
pub const EndpointSettings = struct { pub const Settings = struct {
/// path / slug of the endpoint /// path / slug of the endpoint
path: []const u8, path: []const u8,
/// callback to GET request handler /// callback to GET request handler
@ -26,101 +28,57 @@ pub const EndpointSettings = struct {
patch: ?RequestFn = null, patch: ?RequestFn = null,
/// callback to OPTIONS request handler /// callback to OPTIONS request handler
options: ?RequestFn = null, options: ?RequestFn = null,
/// Only applicable to AuthenticatingEndpoint: handler for unauthorized requests /// Only applicable to Authenticating Endpoint: handler for unauthorized requests
unauthorized: ?RequestFn = null, unauthorized: ?RequestFn = null,
}; };
/// The simple Endpoint struct. Create one and pass in your callbacks. Then, settings: Settings,
/// 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(); /// 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,
},
};
}
/// Initialize the endpoint. // no operation. Dummy handler function for ignoring unset request types.
/// Set only the callbacks you need. Requests of HTTP methods without a fn nop(self: *Endpoint, r: Request) void {
/// provided callback will be ignored. _ = self;
pub fn init(s: EndpointSettings) Self { _ = r;
return .{ }
.settings = .{
.path = s.path, /// The global request handler for this Endpoint, called by the listener.
.get = s.get orelse &nop, pub fn onRequest(self: *Endpoint, r: zap.Request) void {
.post = s.post orelse &nop, if (r.method) |m| {
.put = s.put orelse &nop, if (std.mem.eql(u8, m, "GET"))
.delete = s.delete orelse &nop, return self.settings.get.?(self, r);
.patch = s.patch orelse &nop, if (std.mem.eql(u8, m, "POST"))
.options = s.options orelse &nop, return self.settings.post.?(self, r);
.unauthorized = s.unauthorized orelse &nop, 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);
} }
}
// 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);
}
}
};
/// Wrap an endpoint with an Authenticator -> new Endpoint of type Endpoint /// Wrap an endpoint with an Authenticator -> new Endpoint of type Endpoint
/// is available via the `endpoint()` function. /// is available via the `endpoint()` function.
pub fn AuthenticatingEndpoint(comptime Authenticator: type) type { pub fn Authenticating(comptime Authenticator: type) type {
return struct { return struct {
authenticator: *Authenticator, authenticator: *Authenticator,
ep: *Endpoint, ep: *Endpoint,
@ -289,8 +247,8 @@ pub const EndpointListenerError = error{
/// The listener with ednpoint support /// The listener with ednpoint support
/// ///
/// NOTE: It switches on path.startsWith -> so use endpoints with distinctly starting names!! /// NOTE: It switches on path.startsWith -> so use endpoints with distinctly starting names!!
pub const EndpointListener = struct { pub const Listener = struct {
listener: Listener, listener: HttpListener,
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
const Self = @This(); const Self = @This();
@ -314,12 +272,12 @@ pub const EndpointListener = struct {
// override the settings with our internal, actul callback function // override the settings with our internal, actul callback function
// so that "we" will be called on request // 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 // store the settings-provided request callback for later use
on_request = l.on_request; on_request = l.on_request;
return .{ return .{
.listener = Listener.init(ls), .listener = HttpListener.init(ls),
.allocator = a, .allocator = a,
}; };
} }

View file

@ -61,7 +61,7 @@ pub const AuthResult = enum {
/// The authenticator handled the request that didn't pass authentication / /// The authenticator handled the request that didn't pass authentication /
/// authorization. /// authorization.
/// This is used to implement authenticators that redirect to a login /// This is used to implement authenticators that redirect to a login
/// page. An AuthenticatingEndpoint will not do the default, which is trying /// page. An Authenticating endpoint will not do the default, which is trying
/// to call the `unauthorized` callback if one exists orelse ignore the request. /// to call the `unauthorized` callback if one exists orelse ignore the request.
Handled, Handled,
}; };
@ -79,7 +79,7 @@ pub const AuthResult = enum {
/// WWW-Authenticate: Basic realm="this" /// WWW-Authenticate: Basic realm="this"
/// ///
/// Lookup : any kind of map that implements get([]const u8) -> []const u8 /// Lookup : any kind of map that implements get([]const u8) -> []const u8
pub fn BasicAuth(comptime Lookup: type, comptime kind: BasicAuthStrategy) type { pub fn Basic(comptime Lookup: type, comptime kind: BasicAuthStrategy) type {
return struct { return struct {
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
realm: ?[]const u8, realm: ?[]const u8,
@ -219,7 +219,7 @@ pub fn BasicAuth(comptime Lookup: type, comptime kind: BasicAuthStrategy) type {
/// Errors: /// Errors:
/// HTTP/1.1 401 Unauthorized /// HTTP/1.1 401 Unauthorized
/// WWW-Authenticate: Bearer realm="example", error="invalid_token", error_description="..." /// WWW-Authenticate: Bearer realm="example", error="invalid_token", error_description="..."
pub const BearerAuthSingle = struct { pub const BearerSingle = struct {
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
token: []const u8, token: []const u8,
realm: ?[]const u8, realm: ?[]const u8,
@ -276,7 +276,7 @@ pub const BearerAuthSingle = struct {
/// Errors: /// Errors:
/// HTTP/1.1 401 Unauthorized /// HTTP/1.1 401 Unauthorized
/// WWW-Authenticate: Bearer realm="example", error="invalid_token", error_description="..." /// WWW-Authenticate: Bearer realm="example", error="invalid_token", error_description="..."
pub fn BearerAuthMulti(comptime Lookup: type) type { pub fn BearerMulti(comptime Lookup: type) type {
return struct { return struct {
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
lookup: *Lookup, lookup: *Lookup,
@ -325,8 +325,8 @@ pub fn BearerAuthMulti(comptime Lookup: type) type {
}; };
} }
/// Settings to initialize a UserPassSessionAuth authenticator. /// Settings to initialize a UserPassSession authenticator.
pub const UserPassSessionAuthArgs = struct { pub const UserPassSessionArgs = struct {
/// username body parameter /// username body parameter
usernameParam: []const u8, usernameParam: []const u8,
/// password body parameter /// password body parameter
@ -341,7 +341,7 @@ pub const UserPassSessionAuthArgs = struct {
redirectCode: zap.StatusCode = .found, redirectCode: zap.StatusCode = .found,
}; };
/// UserPassSessionAuth supports the following use case: /// UserPassSession supports the following use case:
/// ///
/// - checks every request: is it going to the login page? -> let the request through. /// - checks every request: is it going to the login page? -> let the request through.
/// - else: /// - else:
@ -358,7 +358,7 @@ pub const UserPassSessionAuthArgs = struct {
/// mechanisms described above will still kick in. For that reason: please know what /// mechanisms described above will still kick in. For that reason: please know what
/// you're doing. /// you're doing.
/// ///
/// See UserPassSessionAuthArgs: /// See UserPassSessionArgs:
/// - username & password param names can be defined by you /// - username & password param names can be defined by you
/// - session cookie name and max-age can be defined by you /// - session cookie name and max-age can be defined by you
/// - login page and redirect code (.302) can be defined by you /// - login page and redirect code (.302) can be defined by you
@ -376,11 +376,11 @@ pub const UserPassSessionAuthArgs = struct {
/// -> another browser program with the page still open would still be able to use /// -> another browser program with the page still open would still be able to use
/// -> the session. Which is kindof OK, but not as cool as erasing the token /// -> the session. Which is kindof OK, but not as cool as erasing the token
/// -> on the server side which immediately block all other browsers as well. /// -> on the server side which immediately block all other browsers as well.
pub fn UserPassSessionAuth(comptime Lookup: type, comptime lockedPwLookups: bool) type { pub fn UserPassSession(comptime Lookup: type, comptime lockedPwLookups: bool) type {
return struct { return struct {
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
lookup: *Lookup, lookup: *Lookup,
settings: UserPassSessionAuthArgs, settings: UserPassSessionArgs,
// TODO: cookie store per user? // TODO: cookie store per user?
sessionTokens: SessionTokenMap, sessionTokens: SessionTokenMap,
@ -398,7 +398,7 @@ pub fn UserPassSessionAuth(comptime Lookup: type, comptime lockedPwLookups: bool
pub fn init( pub fn init(
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
lookup: *Lookup, lookup: *Lookup,
args: UserPassSessionAuthArgs, args: UserPassSessionArgs,
) !Self { ) !Self {
var ret: Self = .{ var ret: Self = .{
.allocator = allocator, .allocator = allocator,
@ -464,7 +464,7 @@ pub fn UserPassSessionAuth(comptime Lookup: type, comptime lockedPwLookups: bool
} }
} }
} else |err| { } else |err| {
zap.debug("unreachable: UserPassSessionAuth.logout: {any}", .{err}); zap.debug("unreachable: UserPassSession.logout: {any}", .{err});
} }
} }
@ -478,7 +478,7 @@ pub fn UserPassSessionAuth(comptime Lookup: type, comptime lockedPwLookups: bool
// parse body // parse body
r.parseBody() catch { r.parseBody() catch {
// zap.debug("warning: parseBody() failed in UserPassSessionAuth: {any}", .{err}); // zap.debug("warning: parseBody() failed in UserPassSession: {any}", .{err});
// this is not an error in case of e.g. gets with querystrings // this is not an error in case of e.g. gets with querystrings
}; };
@ -503,7 +503,7 @@ pub fn UserPassSessionAuth(comptime Lookup: type, comptime lockedPwLookups: bool
} }
} }
} else |err| { } else |err| {
zap.debug("unreachable: could not check for cookie in UserPassSessionAuth: {any}", .{err}); zap.debug("unreachable: could not check for cookie in UserPassSession: {any}", .{err});
} }
// get params of username and password // get params of username and password
@ -548,12 +548,12 @@ pub fn UserPassSessionAuth(comptime Lookup: type, comptime lockedPwLookups: bool
} }
} }
} else |err| { } else |err| {
zap.debug("getParamSt() for password failed in UserPassSessionAuth: {any}", .{err}); zap.debug("getParamSt() for password failed in UserPassSession: {any}", .{err});
return .AuthFailed; return .AuthFailed;
} }
} }
} else |err| { } else |err| {
zap.debug("getParamSt() for user failed in UserPassSessionAuth: {any}", .{err}); zap.debug("getParamSt() for user failed in UserPassSession: {any}", .{err});
return .AuthFailed; return .AuthFailed;
} }
return .AuthFailed; return .AuthFailed;
@ -575,7 +575,7 @@ pub fn UserPassSessionAuth(comptime Lookup: type, comptime lockedPwLookups: bool
// we need to redirect and return .Handled // we need to redirect and return .Handled
self.redirect(r) catch |err| { self.redirect(r) catch |err| {
// we just give up // we just give up
zap.debug("redirect() failed in UserPassSessionAuth: {any}", .{err}); zap.debug("redirect() failed in UserPassSession: {any}", .{err});
}; };
return .Handled; return .Handled;
}, },

View file

@ -2,252 +2,249 @@ const std = @import("std");
const fio = @import("fio.zig"); const fio = @import("fio.zig");
const util = @import("util.zig"); const util = @import("util.zig");
/// A struct to handle Mustache templating. const Self = @This();
///
/// This is a wrapper around fiobj's mustache template handling.
/// See http://facil.io/0.7.x/fiobj_mustache for more information.
pub const Mustache = struct {
const Self = @This();
const struct_mustache_s = opaque {}; const struct_mustache_s = opaque {};
const mustache_s = struct_mustache_s; const mustache_s = struct_mustache_s;
const enum_mustache_error_en = c_uint; const enum_mustache_error_en = c_uint;
const mustache_error_en = enum_mustache_error_en; const mustache_error_en = enum_mustache_error_en;
extern fn fiobj_mustache_new(args: MustacheLoadArgsFio) ?*mustache_s; extern fn fiobj_mustache_new(args: MustacheLoadArgsFio) ?*mustache_s;
extern fn fiobj_mustache_build(mustache: ?*mustache_s, data: fio.FIOBJ) fio.FIOBJ; extern fn fiobj_mustache_build(mustache: ?*mustache_s, data: fio.FIOBJ) fio.FIOBJ;
extern fn fiobj_mustache_build2(dest: fio.FIOBJ, mustache: ?*mustache_s, data: fio.FIOBJ) fio.FIOBJ; extern fn fiobj_mustache_build2(dest: fio.FIOBJ, mustache: ?*mustache_s, data: fio.FIOBJ) fio.FIOBJ;
extern fn fiobj_mustache_free(mustache: ?*mustache_s) void; extern fn fiobj_mustache_free(mustache: ?*mustache_s) void;
/// Load arguments used when creating a new Mustache instance. /// Load arguments used when creating a new Mustache instance.
pub const MustacheLoadArgs = struct { pub const MustacheLoadArgs = struct {
/// Filename. This enables partial templates on filesystem. /// Filename. This enables partial templates on filesystem.
filename: ?[]const u8 = null, filename: ?[]const u8 = null,
/// String data. Should be used if no filename is specified. /// String data. Should be used if no filename is specified.
data: ?[]const u8 = null, data: ?[]const u8 = null,
};
/// Internal struct used for interfacing with fio.
const MustacheLoadArgsFio = extern struct {
filename: [*c]const u8,
filename_len: usize,
data: [*c]const u8,
data_len: usize,
err: [*c]mustache_error_en,
};
/// Handle to the underlying fiobj mustache instance.
handle: *mustache_s,
pub const Error = error{
MUSTACHE_ERR_TOO_DEEP,
MUSTACHE_ERR_CLOSURE_MISMATCH,
MUSTACHE_ERR_FILE_NOT_FOUND,
MUSTACHE_ERR_FILE_TOO_BIG,
MUSTACHE_ERR_FILE_NAME_TOO_LONG,
MUSTACHE_ERR_FILE_NAME_TOO_SHORT,
MUSTACHE_ERR_EMPTY_TEMPLATE,
MUSTACHE_ERR_DELIMITER_TOO_LONG,
MUSTACHE_ERR_NAME_TOO_LONG,
MUSTACHE_ERR_UNKNOWN,
MUSTACHE_ERR_USER_ERROR,
};
/// Create a new `Mustache` instance; `deinit()` should be called to free
/// the object after usage.
pub fn init(load_args: MustacheLoadArgs) Error!Self {
var err: mustache_error_en = undefined;
const args: MustacheLoadArgsFio = .{
.filename = filn: {
if (load_args.filename) |filn| break :filn filn.ptr else break :filn null;
},
.filename_len = filn_len: {
if (load_args.filename) |filn| break :filn_len filn.len else break :filn_len 0;
},
.data = data: {
if (load_args.data) |data| break :data data.ptr else break :data null;
},
.data_len = data_len: {
if (load_args.data) |data| break :data_len data.len else break :data_len 0;
},
.err = &err,
}; };
/// Internal struct used for interfacing with fio. const ret = fiobj_mustache_new(args);
const MustacheLoadArgsFio = extern struct { switch (err) {
filename: [*c]const u8, 0 => return Self{
filename_len: usize, .handle = ret.?,
data: [*c]const u8, },
data_len: usize, 1 => return Error.MUSTACHE_ERR_TOO_DEEP,
err: [*c]mustache_error_en, 2 => return Error.MUSTACHE_ERR_CLOSURE_MISMATCH,
}; 3 => return Error.MUSTACHE_ERR_FILE_NOT_FOUND,
4 => return Error.MUSTACHE_ERR_FILE_TOO_BIG,
5 => return Error.MUSTACHE_ERR_FILE_NAME_TOO_LONG,
6 => return Error.MUSTACHE_ERR_FILE_NAME_TOO_SHORT,
7 => return Error.MUSTACHE_ERR_EMPTY_TEMPLATE,
8 => return Error.MUSTACHE_ERR_DELIMITER_TOO_LONG,
9 => return Error.MUSTACHE_ERR_NAME_TOO_LONG,
10 => return Error.MUSTACHE_ERR_UNKNOWN,
11 => return Error.MUSTACHE_ERR_USER_ERROR,
else => return Error.MUSTACHE_ERR_UNKNOWN,
}
unreachable;
}
handler: *mustache_s, /// Convenience function to create a new `Mustache` instance with in-memory data loaded;
/// `deinit()` should be called to free the object after usage..
pub fn fromData(data: []const u8) Error!Self {
return Self.init(.{ .data = data });
}
pub const Status = enum(c_int) {}; /// Convenience function to create a new `Mustache` instance with file-based data loaded;
pub const Error = error{ /// `deinit()` should be called to free the object after usage..
MUSTACHE_ERR_TOO_DEEP, pub fn fromFile(filename: []const u8) Error!Self {
MUSTACHE_ERR_CLOSURE_MISMATCH, return Self.init(.{ .filename = filename });
MUSTACHE_ERR_FILE_NOT_FOUND, }
MUSTACHE_ERR_FILE_TOO_BIG,
MUSTACHE_ERR_FILE_NAME_TOO_LONG,
MUSTACHE_ERR_FILE_NAME_TOO_SHORT,
MUSTACHE_ERR_EMPTY_TEMPLATE,
MUSTACHE_ERR_DELIMITER_TOO_LONG,
MUSTACHE_ERR_NAME_TOO_LONG,
MUSTACHE_ERR_UNKNOWN,
MUSTACHE_ERR_USER_ERROR,
};
/// Create a new `Mustache` instance; `deinit()` should be called to free /// Free the data backing a `Mustache` instance.
/// the object after usage. pub fn deinit(self: *Self) void {
pub fn init(load_args: MustacheLoadArgs) Error!Self { fiobj_mustache_free(self.handle);
var err: mustache_error_en = undefined; }
const args: MustacheLoadArgsFio = .{ // TODO: implement these - fiobj_mustache.c
.filename = filn: { // pub extern fn fiobj_mustache_build(mustache: ?*mustache_s, data: FIOBJ) FIOBJ;
if (load_args.filename) |filn| break :filn filn.ptr else break :filn null; // pub extern fn fiobj_mustache_build2(dest: FIOBJ, mustache: ?*mustache_s, data: FIOBJ) FIOBJ;
},
.filename_len = filn_len: {
if (load_args.filename) |filn| break :filn_len filn.len else break :filn_len 0;
},
.data = data: {
if (load_args.data) |data| break :data data.ptr else break :data null;
},
.data_len = data_len: {
if (load_args.data) |data| break :data_len data.len else break :data_len 0;
},
.err = &err,
};
const ret = fiobj_mustache_new(args); /// The result from calling `build`.
switch (err) { const MustacheBuildResult = struct {
0 => return Self{ fiobj_result: fio.FIOBJ = 0,
.handler = ret.?,
}, /// Holds the context converted into a fiobj.
1 => return Error.MUSTACHE_ERR_TOO_DEEP, /// This is used in `build`.
2 => return Error.MUSTACHE_ERR_CLOSURE_MISMATCH, fiobj_context: fio.FIOBJ = 0,
3 => return Error.MUSTACHE_ERR_FILE_NOT_FOUND,
4 => return Error.MUSTACHE_ERR_FILE_TOO_BIG, /// Free the data backing a `MustacheBuildResult` instance.
5 => return Error.MUSTACHE_ERR_FILE_NAME_TOO_LONG, pub fn deinit(m: *const MustacheBuildResult) void {
6 => return Error.MUSTACHE_ERR_FILE_NAME_TOO_SHORT, fio.fiobj_free_wrapped(m.fiobj_result);
7 => return Error.MUSTACHE_ERR_EMPTY_TEMPLATE, fio.fiobj_free_wrapped(m.fiobj_context);
8 => return Error.MUSTACHE_ERR_DELIMITER_TOO_LONG,
9 => return Error.MUSTACHE_ERR_NAME_TOO_LONG,
10 => return Error.MUSTACHE_ERR_UNKNOWN,
11 => return Error.MUSTACHE_ERR_USER_ERROR,
else => return Error.MUSTACHE_ERR_UNKNOWN,
}
unreachable;
} }
/// Convenience function to create a new `Mustache` instance with in-memory data loaded; /// Retrieve a string representation of the built template.
/// `deinit()` should be called to free the object after usage.. pub fn str(m: *const MustacheBuildResult) ?[]const u8 {
pub fn fromData(data: []const u8) Error!Mustache { return util.fio2str(m.fiobj_result);
return Self.init(.{ .data = data });
}
/// Convenience function to create a new `Mustache` instance with file-based data loaded;
/// `deinit()` should be called to free the object after usage..
pub fn fromFile(filename: []const u8) Error!Mustache {
return Self.init(.{ .filename = filename });
}
/// Free the data backing a `Mustache` instance.
pub fn deinit(self: *Self) void {
fiobj_mustache_free(self.handler);
}
// TODO: implement these - fiobj_mustache.c
// pub extern fn fiobj_mustache_build(mustache: ?*mustache_s, data: FIOBJ) FIOBJ;
// pub extern fn fiobj_mustache_build2(dest: FIOBJ, mustache: ?*mustache_s, data: FIOBJ) FIOBJ;
/// The result from calling `build`.
const MustacheBuildResult = struct {
fiobj_result: fio.FIOBJ = 0,
/// Holds the context converted into a fiobj.
/// This is used in `build`.
fiobj_context: fio.FIOBJ = 0,
/// Free the data backing a `MustacheBuildResult` instance.
pub fn deinit(m: *const MustacheBuildResult) void {
fio.fiobj_free_wrapped(m.fiobj_result);
fio.fiobj_free_wrapped(m.fiobj_context);
}
/// Retrieve a string representation of the built template.
pub fn str(m: *const MustacheBuildResult) ?[]const u8 {
return util.fio2str(m.fiobj_result);
}
};
/// Build the Mustache template; `deinit()` should be called on the build
/// result to free the data.
/// TODO: This build is slow because it needs to translate to a FIOBJ data
/// object - FIOBJ_T_HASH
pub fn build(self: *Self, data: anytype) MustacheBuildResult {
const T = @TypeOf(data);
if (@typeInfo(T) != .Struct) {
@compileError("No struct: '" ++ @typeName(T) ++ "'");
}
var result: MustacheBuildResult = .{};
result.fiobj_context = fiobjectify(data);
result.fiobj_result = fiobj_mustache_build(self.handler, result.fiobj_context);
return result;
}
pub fn fiobjectify(
value: anytype,
) fio.FIOBJ {
const T = @TypeOf(value);
switch (@typeInfo(T)) {
.Float, .ComptimeFloat => {
return fio.fiobj_float_new(value);
},
.Int, .ComptimeInt => {
return fio.fiobj_num_new_bignum(value);
},
.Bool => {
return if (value) fio.fiobj_true() else fio.fiobj_false();
},
.Null => {
return 0;
},
.Optional => {
if (value) |payload| {
return fiobjectify(payload);
} else {
return fiobjectify(null);
}
},
.Enum => {
return fio.fiobj_num_new_bignum(@intFromEnum(value));
},
.Union => {
const info = @typeInfo(T).Union;
if (info.tag_type) |UnionTagType| {
inline for (info.fields) |u_field| {
if (value == @field(UnionTagType, u_field.name)) {
return fiobjectify(@field(value, u_field.name));
}
}
} else {
@compileError("Unable to fiobjectify untagged union '" ++ @typeName(T) ++ "'");
}
},
.Struct => |S| {
// create a new fio hashmap
const m = fio.fiobj_hash_new();
// std.debug.print("new struct\n", .{});
inline for (S.fields) |Field| {
// don't include void fields
if (Field.type == void) continue;
// std.debug.print(" new field: {s}\n", .{Field.name});
const fname = fio.fiobj_str_new(util.toCharPtr(Field.name), Field.name.len);
// std.debug.print(" fiobj name : {any}\n", .{fname});
const v = @field(value, Field.name);
// std.debug.print(" value: {any}\n", .{v});
const fvalue = fiobjectify(v);
// std.debug.print(" fiobj value: {any}\n", .{fvalue});
_ = fio.fiobj_hash_set(m, fname, fvalue);
fio.fiobj_free_wrapped(fname);
}
return m;
},
.ErrorSet => return fiobjectify(@as([]const u8, @errorName(value))),
.Pointer => |ptr_info| switch (ptr_info.size) {
.One => switch (@typeInfo(ptr_info.child)) {
.Array => {
const Slice = []const std.meta.Elem(ptr_info.child);
return fiobjectify(@as(Slice, value));
},
else => {
// TODO: avoid loops?
return fiobjectify(value.*);
},
},
// TODO: .Many when there is a sentinel (waiting for https://github.com/ziglang/zig/pull/3972)
.Slice => {
// std.debug.print("new slice\n", .{});
if (ptr_info.child == u8 and std.unicode.utf8ValidateSlice(value)) {
return fio.fiobj_str_new(util.toCharPtr(value), value.len);
}
const arr = fio.fiobj_ary_new2(value.len);
for (value) |x| {
const v = fiobjectify(x);
fio.fiobj_ary_push(arr, v);
}
return arr;
},
else => @compileError("Unable to fiobjectify type '" ++ @typeName(T) ++ "'"),
},
.Array => return fiobjectify(&value),
.Vector => |info| {
const array: [info.len]info.child = value;
return fiobjectify(&array);
},
else => @compileError("Unable to fiobjectify type '" ++ @typeName(T) ++ "'"),
}
unreachable;
} }
}; };
/// Build the Mustache template; `deinit()` should be called on the build
/// result to free the data.
// TODO: The build may be slow because it needs to convert zig types to facil.io
// types. However, this needs to be investigated into.
// See `fiobjectify` for more information.
pub fn build(self: *Self, data: anytype) MustacheBuildResult {
const T = @TypeOf(data);
if (@typeInfo(T) != .Struct) {
@compileError("No struct: '" ++ @typeName(T) ++ "'");
}
var result: MustacheBuildResult = .{};
result.fiobj_context = fiobjectify(data);
result.fiobj_result = fiobj_mustache_build(self.handle, result.fiobj_context);
return result;
}
/// Internal function used to convert zig types to facil.io types.
/// Used when providing the context to `fiobj_mustache_build`.
fn fiobjectify(
value: anytype,
) fio.FIOBJ {
const T = @TypeOf(value);
switch (@typeInfo(T)) {
.Float, .ComptimeFloat => {
return fio.fiobj_float_new(value);
},
.Int, .ComptimeInt => {
return fio.fiobj_num_new_bignum(value);
},
.Bool => {
return if (value) fio.fiobj_true() else fio.fiobj_false();
},
.Null => {
return 0;
},
.Optional => {
if (value) |payload| {
return fiobjectify(payload);
} else {
return fiobjectify(null);
}
},
.Enum => {
return fio.fiobj_num_new_bignum(@intFromEnum(value));
},
.Union => {
const info = @typeInfo(T).Union;
if (info.tag_type) |UnionTagType| {
inline for (info.fields) |u_field| {
if (value == @field(UnionTagType, u_field.name)) {
return fiobjectify(@field(value, u_field.name));
}
}
} else {
@compileError("Unable to fiobjectify untagged union '" ++ @typeName(T) ++ "'");
}
},
.Struct => |S| {
// create a new fio hashmap
const m = fio.fiobj_hash_new();
// std.debug.print("new struct\n", .{});
inline for (S.fields) |Field| {
// don't include void fields
if (Field.type == void) continue;
// std.debug.print(" new field: {s}\n", .{Field.name});
const fname = fio.fiobj_str_new(util.toCharPtr(Field.name), Field.name.len);
// std.debug.print(" fiobj name : {any}\n", .{fname});
const v = @field(value, Field.name);
// std.debug.print(" value: {any}\n", .{v});
const fvalue = fiobjectify(v);
// std.debug.print(" fiobj value: {any}\n", .{fvalue});
_ = fio.fiobj_hash_set(m, fname, fvalue);
fio.fiobj_free_wrapped(fname);
}
return m;
},
.ErrorSet => return fiobjectify(@as([]const u8, @errorName(value))),
.Pointer => |ptr_info| switch (ptr_info.size) {
.One => switch (@typeInfo(ptr_info.child)) {
.Array => {
const Slice = []const std.meta.Elem(ptr_info.child);
return fiobjectify(@as(Slice, value));
},
else => {
// TODO: avoid loops?
return fiobjectify(value.*);
},
},
// TODO: .Many when there is a sentinel (waiting for https://github.com/ziglang/zig/pull/3972)
.Slice => {
// std.debug.print("new slice\n", .{});
if (ptr_info.child == u8 and std.unicode.utf8ValidateSlice(value)) {
return fio.fiobj_str_new(util.toCharPtr(value), value.len);
}
const arr = fio.fiobj_ary_new2(value.len);
for (value) |x| {
const v = fiobjectify(x);
fio.fiobj_ary_push(arr, v);
}
return arr;
},
else => @compileError("Unable to fiobjectify type '" ++ @typeName(T) ++ "'"),
},
.Array => return fiobjectify(&value),
.Vector => |info| {
const array: [info.len]info.child = value;
return fiobjectify(&array);
},
else => @compileError("Unable to fiobjectify type '" ++ @typeName(T) ++ "'"),
}
unreachable;
}

738
src/request.zig Normal file
View file

@ -0,0 +1,738 @@
const std = @import("std");
const Log = @import("log.zig");
const http = @import("http.zig");
const fio = @import("fio.zig");
const util = @import("util.zig");
const zap = @import("zap.zig");
pub const HttpError = error{
HttpSendBody,
HttpSetContentType,
HttpSetHeader,
HttpParseBody,
HttpIterParams,
SetCookie,
SendFile,
};
/// Http Content Type enum.
/// Needs some love.
pub const ContentType = enum {
TEXT,
HTML,
JSON,
// TODO: more content types
};
/// Key value pair of strings from HTTP parameters
pub const HttpParamStrKV = struct {
key: util.FreeOrNot,
value: util.FreeOrNot,
pub fn deinit(self: *@This()) void {
self.key.deinit();
self.value.deinit();
}
};
/// List of key value pairs of Http param strings.
pub const HttpParamStrKVList = struct {
items: []HttpParamStrKV,
allocator: std.mem.Allocator,
pub fn deinit(self: *@This()) void {
for (self.items) |*item| {
item.deinit();
}
self.allocator.free(self.items);
}
};
/// List of key value pairs of Http params (might be of different types).
pub const HttpParamKVList = struct {
items: []HttpParamKV,
allocator: std.mem.Allocator,
pub fn deinit(self: *const @This()) void {
for (self.items) |*item| {
item.deinit();
}
self.allocator.free(self.items);
}
};
/// Enum for HttpParam tagged union
pub const HttpParamValueType = enum {
// Null,
Bool,
Int,
Float,
String,
Unsupported,
Hash_Binfile,
Array_Binfile,
};
/// Tagged union holding a typed Http param
pub const HttpParam = union(HttpParamValueType) {
Bool: bool,
Int: isize,
Float: f64,
/// we don't do writable strings here
String: util.FreeOrNot,
/// value will always be null
Unsupported: ?void,
/// we assume hashes are because of file transmissions
Hash_Binfile: HttpParamBinaryFile,
/// value will always be null
Array_Binfile: std.ArrayList(HttpParamBinaryFile),
};
/// Key value pair of one typed Http param
pub const HttpParamKV = struct {
key: util.FreeOrNot,
value: ?HttpParam,
pub fn deinit(self: *@This()) void {
self.key.deinit();
if (self.value) |p| {
switch (p) {
.String => |*s| s.deinit(),
else => {},
}
}
}
};
/// Struct representing an uploaded file.
pub const HttpParamBinaryFile = struct {
/// file contents
data: ?[]const u8 = null,
/// mimetype
mimetype: ?[]const u8 = null,
/// filename
filename: ?[]const u8 = null,
/// format function for printing file upload data
pub fn format(value: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) std.os.WriteError!void {
const d = value.data orelse "\\0";
const m = value.mimetype orelse "null";
const f = value.filename orelse "null";
return writer.print("<{s} ({s}): {any}>", .{ f, m, d });
}
};
fn parseBinfilesFrom(a: std.mem.Allocator, o: fio.FIOBJ) !HttpParam {
const key_name = fio.fiobj_str_new("name", 4);
const key_data = fio.fiobj_str_new("data", 4);
const key_type = fio.fiobj_str_new("type", 4);
defer {
fio.fiobj_free_wrapped(key_name);
fio.fiobj_free_wrapped(key_data);
fio.fiobj_free_wrapped(key_type);
} // files: they should have "data", "type", and "filename" keys
if (fio.fiobj_hash_haskey(o, key_data) == 1 and fio.fiobj_hash_haskey(o, key_type) == 1 and fio.fiobj_hash_haskey(o, key_name) == 1) {
const filename = fio.fiobj_obj2cstr(fio.fiobj_hash_get(o, key_name));
const mimetype = fio.fiobj_obj2cstr(fio.fiobj_hash_get(o, key_type));
const data = fio.fiobj_hash_get(o, key_data);
var data_slice: ?[]const u8 = null;
switch (fio.fiobj_type(data)) {
fio.FIOBJ_T_DATA => {
if (fio.is_invalid(data) == 1) {
data_slice = "(zap: invalid data)";
std.log.warn("WARNING: HTTP param binary file is not a data object\n", .{});
} else {
// the data
const data_len = fio.fiobj_data_len(data);
var data_buf = fio.fiobj_data_read(data, data_len);
if (data_len < 0) {
std.log.warn("WARNING: HTTP param binary file size negative: {d}\n", .{data_len});
std.log.warn("FIOBJ_TYPE of data is: {d}\n", .{fio.fiobj_type(data)});
} else {
if (data_buf.len != data_len) {
std.log.warn("WARNING: HTTP param binary file size mismatch: should {d}, is: {d}\n", .{ data_len, data_buf.len });
}
if (data_buf.len > 0) {
data_slice = data_buf.data[0..data_buf.len];
} else {
std.log.warn("WARNING: HTTP param binary file buffer size negative: {d}\n", .{data_buf.len});
data_slice = "(zap: invalid data: negative BUFFER size)";
}
}
}
},
fio.FIOBJ_T_STRING => {
const fiostr = fio.fiobj_obj2cstr(data);
if (fiostr.len == 0) {
data_slice = "(zap: empty string data)";
std.log.warn("WARNING: HTTP param binary file has empty string object\n", .{});
} else {
data_slice = fiostr.data[0..fiostr.len];
}
},
fio.FIOBJ_T_ARRAY => {
// OK, data is an array
const len = fio.fiobj_ary_count(data);
const fn_ary = fio.fiobj_hash_get(o, key_name);
const mt_ary = fio.fiobj_hash_get(o, key_type);
if (fio.fiobj_ary_count(fn_ary) == len and fio.fiobj_ary_count(mt_ary) == len) {
var i: isize = 0;
var ret = std.ArrayList(HttpParamBinaryFile).init(a);
while (i < len) : (i += 1) {
const file_data_obj = fio.fiobj_ary_entry(data, i);
const file_name_obj = fio.fiobj_ary_entry(fn_ary, i);
const file_mimetype_obj = fio.fiobj_ary_entry(mt_ary, i);
var has_error: bool = false;
if (fio.is_invalid(file_data_obj) == 1) {
std.log.debug("file data invalid in array", .{});
has_error = true;
}
if (fio.is_invalid(file_name_obj) == 1) {
std.log.debug("file name invalid in array", .{});
has_error = true;
}
if (fio.is_invalid(file_mimetype_obj) == 1) {
std.log.debug("file mimetype invalid in array", .{});
has_error = true;
}
if (has_error) {
return error.Invalid;
}
const file_data = fio.fiobj_obj2cstr(file_data_obj);
const file_name = fio.fiobj_obj2cstr(file_name_obj);
const file_mimetype = fio.fiobj_obj2cstr(file_mimetype_obj);
try ret.append(.{
.data = file_data.data[0..file_data.len],
.mimetype = file_mimetype.data[0..file_mimetype.len],
.filename = file_name.data[0..file_name.len],
});
}
return .{ .Array_Binfile = ret };
} else {
return error.ArrayLenMismatch;
}
},
else => {
// don't know what to do
return error.Unsupported;
},
}
return .{ .Hash_Binfile = .{
.filename = filename.data[0..filename.len],
.mimetype = mimetype.data[0..mimetype.len],
.data = data_slice,
} };
} else {
return .{ .Hash_Binfile = .{} };
}
}
/// Parse FIO object into a typed Http param. Supports file uploads.
pub fn Fiobj2HttpParam(a: std.mem.Allocator, o: fio.FIOBJ, dupe_string: bool) !?HttpParam {
return switch (fio.fiobj_type(o)) {
fio.FIOBJ_T_NULL => null,
fio.FIOBJ_T_TRUE => .{ .Bool = true },
fio.FIOBJ_T_FALSE => .{ .Bool = false },
fio.FIOBJ_T_NUMBER => .{ .Int = fio.fiobj_obj2num(o) },
fio.FIOBJ_T_FLOAT => .{ .Float = fio.fiobj_obj2float(o) },
fio.FIOBJ_T_STRING => .{ .String = try util.fio2strAllocOrNot(a, o, dupe_string) },
fio.FIOBJ_T_ARRAY => {
return .{ .Unsupported = null };
},
fio.FIOBJ_T_HASH => {
const file = try parseBinfilesFrom(a, o);
return file;
},
else => .{ .Unsupported = null },
};
}
/// Args for setting a cookie
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,
};
path: ?[]const u8,
query: ?[]const u8,
body: ?[]const u8,
method: ?[]const u8,
h: [*c]fio.http_s,
/// NEVER touch this field!!!!
/// if you absolutely MUST, then you may provide context here
/// via setUserContext and getUserContext
_user_context: *UserContext,
/// NEVER touch this field!!!!
/// use markAsFinished() and isFinished() instead
/// this is a hack: the listener will put a pointer to this into the udata
/// field of `h`. So copies of the Request will all have way to the
/// same instance of this field.
_is_finished_request_global: bool,
/// NEVER touch this field!!!!
/// this is part of the hack.
_is_finished: *bool = undefined,
pub const UserContext = struct {
user_context: ?*anyopaque = null,
};
const Self = @This();
/// mark the current request as finished. Important for middleware-style
/// request handler chaining. Called when sending a body, redirecting, etc.
pub fn markAsFinished(self: *const Self, finished: bool) void {
// we might be a copy
self._is_finished.* = finished;
}
/// tell whether request processing has finished. (e.g. response sent,
/// redirected, ...)
pub fn isFinished(self: *const Self) bool {
// we might be a copy
return self._is_finished.*;
}
/// if you absolutely must, you can set any context on the request here
// (note, this line is linked to from the readme) -- TODO: sync
pub fn setUserContext(self: *const Self, context: *anyopaque) void {
self._user_context.*.user_context = context;
}
/// get the associated user context of the request.
pub fn getUserContext(self: *const Self, comptime Context: type) ?*Context {
if (self._user_context.*.user_context) |ptr| {
return @as(*Context, @ptrCast(@alignCast(ptr)));
} else {
return null;
}
}
/// Tries to send an error stack trace.
pub fn sendError(self: *const Self, err: anyerror, errorcode_num: usize) void {
// TODO: query accept headers
if (self._internal_sendError(err, errorcode_num)) {
return;
} else |_| {
self.sendBody(@errorName(err)) catch return;
}
}
/// Used internally. Probably does not need to be public.
pub fn _internal_sendError(self: *const Self, err: anyerror, errorcode_num: usize) !void {
// TODO: query accept headers
// TODO: let's hope 20k is enough. Maybe just really allocate here
self.h.*.status = errorcode_num;
var buf: [20 * 1024]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);
var string = std.ArrayList(u8).init(fba.allocator());
var writer = string.writer();
try writer.print("ERROR: {any}\n\n", .{err});
const debugInfo = try std.debug.getSelfDebugInfo();
const ttyConfig: std.io.tty.Config = .no_color;
try std.debug.writeCurrentStackTrace(writer, debugInfo, ttyConfig, null);
try self.sendBody(string.items);
}
/// Send body.
pub fn sendBody(self: *const Self, body: []const u8) HttpError!void {
const ret = fio.http_send_body(self.h, @as(
*anyopaque,
@ptrFromInt(@intFromPtr(body.ptr)),
), body.len);
zap.debug("Request.sendBody(): ret = {}\n", .{ret});
if (ret == -1) return error.HttpSendBody;
self.markAsFinished(true);
}
/// Set content type and send json buffer.
pub fn sendJson(self: *const Self, json: []const u8) HttpError!void {
if (self.setContentType(.JSON)) {
if (fio.http_send_body(self.h, @as(
*anyopaque,
@ptrFromInt(@intFromPtr(json.ptr)),
), json.len) != 0) return error.HttpSendBody;
self.markAsFinished(true);
} else |err| return err;
}
/// Set content type.
pub fn setContentType(self: *const Self, c: ContentType) HttpError!void {
const s = switch (c) {
.TEXT => "text/plain",
.JSON => "application/json",
else => "text/html",
};
zap.debug("setting content-type to {s}\n", .{s});
return self.setHeader("content-type", s);
}
/// redirect to path with status code 302 by default
pub fn redirectTo(self: *const Self, path: []const u8, code: ?http.StatusCode) HttpError!void {
self.setStatus(if (code) |status| status else .found);
try self.setHeader("Location", path);
try self.sendBody("moved");
self.markAsFinished(true);
}
/// shows how to use the logger
pub fn setContentTypeWithLogger(
self: *const Self,
c: ContentType,
logger: *const Log,
) HttpError!void {
const s = switch (c) {
.TEXT => "text/plain",
.JSON => "application/json",
else => "text/html",
};
logger.log("setting content-type to {s}\n", .{s});
return self.setHeader("content-type", s);
}
/// Tries to determine the content type by file extension of request path, and sets it.
pub fn setContentTypeFromPath(self: *const Self) !void {
const t = fio.http_mimetype_find2(self.h.*.path);
if (fio.is_invalid(t) == 1) return error.HttpSetContentType;
const ret = fio.fiobj_hash_set(
self.h.*.private_data.out_headers,
fio.HTTP_HEADER_CONTENT_TYPE,
t,
);
if (ret == -1) return error.HttpSetContentType;
}
/// Tries to determine the content type by filename extension, and sets it.
/// If the extension cannot be determined, NoExtensionInFilename error is
/// returned.
pub fn setContentTypeFromFilename(self: *const Self, filename: []const u8) !void {
const ext = std.fs.path.extension(filename);
if (ext.len > 1) {
const e = ext[1..];
const obj = fio.http_mimetype_find(@constCast(e.ptr), e.len);
if (util.fio2str(obj)) |mime_str| {
try self.setHeader("content-type", mime_str);
}
} else {
return error.NoExtensionInFilename;
}
}
/// Returns the header value of given key name. Returned mem is temp.
/// Do not free it.
pub fn getHeader(self: *const Self, name: []const u8) ?[]const u8 {
const hname = fio.fiobj_str_new(util.toCharPtr(name), name.len);
defer fio.fiobj_free_wrapped(hname);
return util.fio2str(fio.fiobj_hash_get(self.h.*.headers, hname));
}
/// Set header.
pub fn setHeader(self: *const Self, name: []const u8, value: []const u8) HttpError!void {
const hname: fio.fio_str_info_s = .{
.data = util.toCharPtr(name),
.len = name.len,
.capa = name.len,
};
zap.debug("setHeader: hname = {s}\n", .{name});
const vname: fio.fio_str_info_s = .{
.data = util.toCharPtr(value),
.len = value.len,
.capa = value.len,
};
zap.debug("setHeader: vname = {s}\n", .{value});
const ret = fio.http_set_header2(self.h, hname, vname);
// FIXME without the following if, we get errors in release builds
// at least we don't have to log unconditionally
if (ret == -1) {
std.debug.print("***************** zap.zig:274\n", .{});
}
zap.debug("setHeader: ret = {}\n", .{ret});
if (ret == 0) return;
return error.HttpSetHeader;
}
/// Set status by numeric value.
pub fn setStatusNumeric(self: *const Self, status: usize) void {
self.h.*.status = status;
}
/// Set status by enum.
pub fn setStatus(self: *const Self, status: http.StatusCode) void {
self.h.*.status = @as(usize, @intCast(@intFromEnum(status)));
}
/// Sends a file if present in the filesystem orelse returns an error.
///
/// - efficiently sends a file using gzip compression
/// - also handles range requests if `Range` or `If-Range` headers are present in the request.
/// - sends the response headers and the specified file (the response's body).
///
/// On success, the `self.h` handle will be consumed and invalid.
/// On error, the handle will still be valid and should be used to send an error response
///
/// Important: sets last-modified and cache-control headers with a max-age value of 1 hour!
/// You can override that by setting those headers yourself, e.g.: setHeader("Cache-Control", "no-cache")
pub fn sendFile(self: *const Self, file_path: []const u8) !void {
if (fio.http_sendfile2(self.h, util.toCharPtr(file_path), file_path.len, null, 0) != 0)
return error.SendFile;
self.markAsFinished(true);
}
/// Attempts to decode the request's body.
/// This should be called BEFORE parseQuery
/// Result is accessible via parametersToOwnedSlice(), parametersToOwnedStrSlice()
///
/// Supported body types:
/// - application/x-www-form-urlencoded
/// - application/json
/// - multipart/form-data
pub fn parseBody(self: *const Self) HttpError!void {
if (fio.http_parse_body(self.h) == -1) return error.HttpParseBody;
}
/// Parses the query part of an HTTP request
/// This should be called AFTER parseBody(), just in case the body is a JSON
/// object that doesn't have a hash map at its root.
///
/// Result is accessible via parametersToOwnedSlice(), parametersToOwnedStrSlice()
pub fn parseQuery(self: *const Self) void {
fio.http_parse_query(self.h);
}
/// Parse received cookie headers
pub fn parseCookies(self: *const Self, url_encoded: bool) void {
fio.http_parse_cookies(self.h, if (url_encoded) 1 else 0);
}
/// Set a response cookie
pub fn setCookie(self: *const Self, args: CookieArgs) HttpError!void {
const c: fio.http_cookie_args_s = .{
.name = util.toCharPtr(args.name),
.name_len = @as(isize, @intCast(args.name.len)),
.value = util.toCharPtr(args.value),
.value_len = @as(isize, @intCast(args.value.len)),
.domain = if (args.domain) |p| util.toCharPtr(p) else null,
.domain_len = if (args.domain) |p| @as(isize, @intCast(p.len)) else 0,
.path = if (args.path) |p| util.toCharPtr(p) else null,
.path_len = if (args.path) |p| @as(isize, @intCast(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,
};
// TODO WAT?
// if we:
// if(fio.http_set_cookie(...) == -1)
// instead of capturing it in `ret` first and then checking it,
// all ReleaseXXX builds return an error!
// TODO: still happening?
const ret = fio.http_set_cookie(self.h, c);
if (ret == -1) {
std.log.err("fio.http_set_cookie returned: {}\n", .{ret});
return error.SetCookie;
}
}
/// Returns named cookie. Works like getParamStr().
pub fn getCookieStr(self: *const Self, a: std.mem.Allocator, name: []const u8, 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(a, value, always_alloc);
}
/// Returns the number of cookies 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.
///
/// Parse with parseBody() and / or parseQuery()
pub fn getParamCount(self: *const Self) isize {
if (self.h.*.params == 0) return 0;
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, @as(usize, @intCast(self.getCookiesCount())));
var context: _parametersToOwnedStrSliceContext = .{
.params = &params,
.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, @as(usize, @intCast(self.getCookiesCount())));
var context: _parametersToOwnedSliceContext = .{ .params = &params, .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:
///
/// - Bool
/// - Int
/// - Float
/// - String
///
/// At the moment, no fio ARRAYs are supported as well as HASH maps.
/// So, for JSON body payloads: parse the body instead.
///
/// Requires parseBody() and/or parseQuery() have been called.
/// Returned list needs to be deinited.
pub fn parametersToOwnedStrList(self: *const Self, a: std.mem.Allocator, always_alloc: bool) anyerror!HttpParamStrKVList {
var params = try std.ArrayList(HttpParamStrKV).initCapacity(a, @as(usize, @intCast(self.getParamCount())));
var context: _parametersToOwnedStrSliceContext = .{
.params = &params,
.allocator = a,
.always_alloc = always_alloc,
};
const howmany = fio.fiobj_each1(self.h.*.params, 0, _each_nextParamStr, &context);
if (howmany != self.getParamCount()) {
return error.HttpIterParams;
}
return .{ .items = try params.toOwnedSlice(), .allocator = a };
}
const _parametersToOwnedStrSliceContext = struct {
allocator: std.mem.Allocator,
params: *std.ArrayList(HttpParamStrKV),
last_error: ?anyerror = null,
always_alloc: bool,
};
fn _each_nextParamStr(fiobj_value: fio.FIOBJ, context: ?*anyopaque) callconv(.C) c_int {
const ctx: *_parametersToOwnedStrSliceContext = @as(*_parametersToOwnedStrSliceContext, @ptrCast(@alignCast(context)));
// this is thread-safe, guaranteed by fio
const fiobj_key: fio.FIOBJ = fio.fiobj_hash_key_in_loop();
ctx.params.append(.{
.key = util.fio2strAllocOrNot(ctx.allocator, fiobj_key, ctx.always_alloc) catch |err| {
ctx.last_error = err;
return -1;
},
.value = util.fio2strAllocOrNot(ctx.allocator, fiobj_value, ctx.always_alloc) catch |err| {
ctx.last_error = err;
return -1;
},
}) catch |err| {
// what to do?
// signal the caller that an error occured by returning -1
// also, set the error
ctx.last_error = err;
return -1;
};
return 0;
}
/// Returns the query / body parameters as key/value pairs
/// Supported param types that will be converted:
///
/// - Bool
/// - Int
/// - Float
/// - String
///
/// At the moment, no fio ARRAYs are supported as well as HASH maps.
/// So, for JSON body payloads: parse the body instead.
///
/// Requires parseBody() and/or parseQuery() have been called.
/// Returned slice needs to be freed.
pub fn parametersToOwnedList(self: *const Self, a: std.mem.Allocator, dupe_strings: bool) !HttpParamKVList {
var params = try std.ArrayList(HttpParamKV).initCapacity(a, @as(usize, @intCast(self.getParamCount())));
var context: _parametersToOwnedSliceContext = .{ .params = &params, .allocator = a, .dupe_strings = dupe_strings };
const howmany = fio.fiobj_each1(self.h.*.params, 0, _each_nextParam, &context);
if (howmany != self.getParamCount()) {
return error.HttpIterParams;
}
return .{ .items = try params.toOwnedSlice(), .allocator = a };
}
const _parametersToOwnedSliceContext = struct {
params: *std.ArrayList(HttpParamKV),
last_error: ?anyerror = null,
allocator: std.mem.Allocator,
dupe_strings: bool,
};
fn _each_nextParam(fiobj_value: fio.FIOBJ, context: ?*anyopaque) callconv(.C) c_int {
const ctx: *_parametersToOwnedSliceContext = @as(*_parametersToOwnedSliceContext, @ptrCast(@alignCast(context)));
// this is thread-safe, guaranteed by fio
const fiobj_key: fio.FIOBJ = fio.fiobj_hash_key_in_loop();
ctx.params.append(.{
.key = util.fio2strAllocOrNot(ctx.allocator, fiobj_key, ctx.dupe_strings) catch |err| {
ctx.last_error = err;
return -1;
},
.value = Fiobj2HttpParam(ctx.allocator, fiobj_value, ctx.dupe_strings) catch |err| {
ctx.last_error = err;
return -1;
},
}) catch |err| {
// what to do?
// signal the caller that an error occured by returning -1
// also, set the error
ctx.last_error = err;
return -1;
};
return 0;
}
/// get named parameter as string
/// Supported param types that will be converted:
///
/// - Bool
/// - Int
/// - Float
/// - String
///
/// At the moment, no fio ARRAYs are supported as well as HASH maps.
/// So, for JSON body payloads: parse the body instead.
///
/// Requires parseBody() and/or parseQuery() have been called.
/// The returned string needs to be deinited with .deinit()
pub fn getParamStr(self: *const Self, a: std.mem.Allocator, name: []const u8, always_alloc: bool) !?util.FreeOrNot {
if (self.h.*.params == 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.*.params, key);
if (value == fio.FIOBJ_INVALID) {
return null;
}
return try util.fio2strAllocOrNot(a, value, always_alloc);
}

View file

@ -1,9 +1,8 @@
const std = @import("std"); const std = @import("std");
const zap = @import("zap"); const zap = @import("zap");
// const Authenticators = @import("http_auth.zig"); // const Authenticators = @import("http_auth.zig");
const Authenticators = zap; const Authenticators = zap.Auth;
const Endpoints = zap; const Endpoint = zap.Endpoint;
// const Endpoints = @import("endpoint.zig");
const fio = zap; const fio = zap;
// const fio = @import("fio.zig"); // const fio = @import("fio.zig");
const util = zap; const util = zap;
@ -13,7 +12,7 @@ test "BearerAuthSingle authenticate" {
const a = std.testing.allocator; const a = std.testing.allocator;
const token = "hello, world"; const token = "hello, world";
var auth = try Authenticators.BearerAuthSingle.init(a, token, null); var auth = try Authenticators.BearerSingle.init(a, token, null);
defer auth.deinit(); defer auth.deinit();
// invalid auth header // invalid auth header
@ -33,7 +32,7 @@ test "BearerAuthMulti authenticate" {
try set.put(token, {}); try set.put(token, {});
var auth = try Authenticators.BearerAuthMulti(Set).init(a, &set, null); var auth = try Authenticators.BearerMulti(Set).init(a, &set, null);
defer auth.deinit(); defer auth.deinit();
// invalid auth header // invalid auth header
@ -54,7 +53,7 @@ test "BasicAuth Token68" {
try set.put(token, {}); try set.put(token, {});
// create authenticator // create authenticator
const Authenticator = Authenticators.BasicAuth(Set, .Token68); const Authenticator = Authenticators.Basic(Set, .Token68);
var auth = try Authenticator.init(a, &set, null); var auth = try Authenticator.init(a, &set, null);
defer auth.deinit(); defer auth.deinit();
@ -85,7 +84,7 @@ test "BasicAuth UserPass" {
const encoded = encoder.encode(&buffer, token); const encoded = encoder.encode(&buffer, token);
// create authenticator // create authenticator
const Authenticator = Authenticators.BasicAuth(Map, .UserPass); const Authenticator = Authenticators.Basic(Map, .UserPass);
var auth = try Authenticator.init(a, &map, null); var auth = try Authenticator.init(a, &map, null);
defer auth.deinit(); defer auth.deinit();
@ -106,7 +105,7 @@ const HTTP_RESPONSE: []const u8 =
; ;
var received_response: []const u8 = "null"; var received_response: []const u8 = "null";
fn endpoint_http_get(e: *Endpoints.Endpoint, r: zap.Request) void { fn endpoint_http_get(e: *Endpoint, r: zap.Request) void {
_ = e; _ = e;
r.sendBody(HTTP_RESPONSE) catch return; r.sendBody(HTTP_RESPONSE) catch return;
received_response = HTTP_RESPONSE; received_response = HTTP_RESPONSE;
@ -114,7 +113,7 @@ fn endpoint_http_get(e: *Endpoints.Endpoint, r: zap.Request) void {
zap.stop(); zap.stop();
} }
fn endpoint_http_unauthorized(e: *Endpoints.Endpoint, r: zap.Request) void { fn endpoint_http_unauthorized(e: *Endpoint, r: zap.Request) void {
_ = e; _ = e;
r.setStatus(.unauthorized); r.setStatus(.unauthorized);
r.sendBody("UNAUTHORIZED ACCESS") catch return; r.sendBody("UNAUTHORIZED ACCESS") catch return;
@ -181,7 +180,7 @@ test "BearerAuthSingle authenticateRequest OK" {
const token = "ABCDEFG"; const token = "ABCDEFG";
// setup listener // setup listener
var listener = zap.EndpointListener.init( var listener = zap.Endpoint.Listener.init(
a, a,
.{ .{
.port = 3000, .port = 3000,
@ -194,19 +193,19 @@ test "BearerAuthSingle authenticateRequest OK" {
defer listener.deinit(); defer listener.deinit();
// create mini endpoint // create mini endpoint
var ep = Endpoints.Endpoint.init(.{ var ep = Endpoint.init(.{
.path = "/test", .path = "/test",
.get = endpoint_http_get, .get = endpoint_http_get,
.unauthorized = endpoint_http_unauthorized, .unauthorized = endpoint_http_unauthorized,
}); });
// create authenticator // create authenticator
const Authenticator = Authenticators.BearerAuthSingle; const Authenticator = Authenticators.BearerSingle;
var authenticator = try Authenticator.init(a, token, null); var authenticator = try Authenticator.init(a, token, null);
defer authenticator.deinit(); defer authenticator.deinit();
// create authenticating endpoint // create authenticating endpoint
const BearerAuthEndpoint = Endpoints.AuthenticatingEndpoint(Authenticator); const BearerAuthEndpoint = Endpoint.Authenticating(Authenticator);
var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator);
try listener.register(auth_ep.endpoint()); try listener.register(auth_ep.endpoint());
@ -234,7 +233,7 @@ test "BearerAuthSingle authenticateRequest test-unauthorized" {
const token = "ABCDEFG"; const token = "ABCDEFG";
// setup listener // setup listener
var listener = zap.EndpointListener.init( var listener = zap.Endpoint.Listener.init(
a, a,
.{ .{
.port = 3000, .port = 3000,
@ -247,7 +246,7 @@ test "BearerAuthSingle authenticateRequest test-unauthorized" {
defer listener.deinit(); defer listener.deinit();
// create mini endpoint // create mini endpoint
var ep = Endpoints.Endpoint.init(.{ var ep = Endpoint.init(.{
.path = "/test", .path = "/test",
.get = endpoint_http_get, .get = endpoint_http_get,
.unauthorized = endpoint_http_unauthorized, .unauthorized = endpoint_http_unauthorized,
@ -260,12 +259,12 @@ test "BearerAuthSingle authenticateRequest test-unauthorized" {
// insert auth tokens // insert auth tokens
try set.put(token, {}); try set.put(token, {});
const Authenticator = Authenticators.BearerAuthMulti(Set); const Authenticator = Authenticators.BearerMulti(Set);
var authenticator = try Authenticator.init(a, &set, null); var authenticator = try Authenticator.init(a, &set, null);
defer authenticator.deinit(); defer authenticator.deinit();
// create authenticating endpoint // create authenticating endpoint
const BearerAuthEndpoint = Endpoints.AuthenticatingEndpoint(Authenticator); const BearerAuthEndpoint = Endpoint.Authenticating(Authenticator);
var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator);
try listener.register(auth_ep.endpoint()); try listener.register(auth_ep.endpoint());
@ -291,7 +290,7 @@ test "BearerAuthMulti authenticateRequest OK" {
const token = "ABCDEFG"; const token = "ABCDEFG";
// setup listener // setup listener
var listener = zap.EndpointListener.init( var listener = zap.Endpoint.Listener.init(
a, a,
.{ .{
.port = 3000, .port = 3000,
@ -304,19 +303,19 @@ test "BearerAuthMulti authenticateRequest OK" {
defer listener.deinit(); defer listener.deinit();
// create mini endpoint // create mini endpoint
var ep = Endpoints.Endpoint.init(.{ var ep = Endpoint.init(.{
.path = "/test", .path = "/test",
.get = endpoint_http_get, .get = endpoint_http_get,
.unauthorized = endpoint_http_unauthorized, .unauthorized = endpoint_http_unauthorized,
}); });
// create authenticator // create authenticator
const Authenticator = Authenticators.BearerAuthSingle; const Authenticator = Authenticators.BearerSingle;
var authenticator = try Authenticator.init(a, token, null); var authenticator = try Authenticator.init(a, token, null);
defer authenticator.deinit(); defer authenticator.deinit();
// create authenticating endpoint // create authenticating endpoint
const BearerAuthEndpoint = Endpoints.AuthenticatingEndpoint(Authenticator); const BearerAuthEndpoint = Endpoint.Authenticating(Authenticator);
var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator);
try listener.register(auth_ep.endpoint()); try listener.register(auth_ep.endpoint());
@ -342,7 +341,7 @@ test "BearerAuthMulti authenticateRequest test-unauthorized" {
const token = "invalid"; const token = "invalid";
// setup listener // setup listener
var listener = zap.EndpointListener.init( var listener = zap.Endpoint.Listener.init(
a, a,
.{ .{
.port = 3000, .port = 3000,
@ -355,19 +354,19 @@ test "BearerAuthMulti authenticateRequest test-unauthorized" {
defer listener.deinit(); defer listener.deinit();
// create mini endpoint // create mini endpoint
var ep = Endpoints.Endpoint.init(.{ var ep = Endpoint.init(.{
.path = "/test", .path = "/test",
.get = endpoint_http_get, .get = endpoint_http_get,
.unauthorized = endpoint_http_unauthorized, .unauthorized = endpoint_http_unauthorized,
}); });
// create authenticator // create authenticator
const Authenticator = Authenticators.BearerAuthSingle; const Authenticator = Authenticators.BearerSingle;
var authenticator = try Authenticator.init(a, token, null); var authenticator = try Authenticator.init(a, token, null);
defer authenticator.deinit(); defer authenticator.deinit();
// create authenticating endpoint // create authenticating endpoint
const BearerAuthEndpoint = Endpoints.AuthenticatingEndpoint(Authenticator); const BearerAuthEndpoint = Endpoint.Authenticating(Authenticator);
var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator);
try listener.register(auth_ep.endpoint()); try listener.register(auth_ep.endpoint());
@ -393,7 +392,7 @@ test "BasicAuth Token68 authenticateRequest" {
const token = "QWxhZGRpbjpvcGVuIHNlc2FtZQ=="; const token = "QWxhZGRpbjpvcGVuIHNlc2FtZQ==";
// setup listener // setup listener
var listener = zap.EndpointListener.init( var listener = zap.Endpoint.Listener.init(
a, a,
.{ .{
.port = 3000, .port = 3000,
@ -406,7 +405,7 @@ test "BasicAuth Token68 authenticateRequest" {
defer listener.deinit(); defer listener.deinit();
// create mini endpoint // create mini endpoint
var ep = Endpoints.Endpoint.init(.{ var ep = Endpoint.init(.{
.path = "/test", .path = "/test",
.get = endpoint_http_get, .get = endpoint_http_get,
.unauthorized = endpoint_http_unauthorized, .unauthorized = endpoint_http_unauthorized,
@ -418,12 +417,12 @@ test "BasicAuth Token68 authenticateRequest" {
try set.put(token, {}); try set.put(token, {});
// create authenticator // create authenticator
const Authenticator = Authenticators.BasicAuth(Set, .Token68); const Authenticator = Authenticators.Basic(Set, .Token68);
var authenticator = try Authenticator.init(a, &set, null); var authenticator = try Authenticator.init(a, &set, null);
defer authenticator.deinit(); defer authenticator.deinit();
// create authenticating endpoint // create authenticating endpoint
const BearerAuthEndpoint = Endpoints.AuthenticatingEndpoint(Authenticator); const BearerAuthEndpoint = Endpoint.Authenticating(Authenticator);
var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator);
try listener.register(auth_ep.endpoint()); try listener.register(auth_ep.endpoint());
@ -449,7 +448,7 @@ test "BasicAuth Token68 authenticateRequest test-unauthorized" {
const token = "QWxhZGRpbjpvcGVuIHNlc2FtZQ=="; const token = "QWxhZGRpbjpvcGVuIHNlc2FtZQ==";
// setup listener // setup listener
var listener = zap.EndpointListener.init( var listener = zap.Endpoint.Listener.init(
a, a,
.{ .{
.port = 3000, .port = 3000,
@ -462,7 +461,7 @@ test "BasicAuth Token68 authenticateRequest test-unauthorized" {
defer listener.deinit(); defer listener.deinit();
// create mini endpoint // create mini endpoint
var ep = Endpoints.Endpoint.init(.{ var ep = Endpoint.init(.{
.path = "/test", .path = "/test",
.get = endpoint_http_get, .get = endpoint_http_get,
.unauthorized = endpoint_http_unauthorized, .unauthorized = endpoint_http_unauthorized,
@ -474,12 +473,12 @@ test "BasicAuth Token68 authenticateRequest test-unauthorized" {
try set.put(token, {}); try set.put(token, {});
// create authenticator // create authenticator
const Authenticator = Authenticators.BasicAuth(Set, .Token68); const Authenticator = Authenticators.Basic(Set, .Token68);
var authenticator = try Authenticator.init(a, &set, null); var authenticator = try Authenticator.init(a, &set, null);
defer authenticator.deinit(); defer authenticator.deinit();
// create authenticating endpoint // create authenticating endpoint
const BearerAuthEndpoint = Endpoints.AuthenticatingEndpoint(Authenticator); const BearerAuthEndpoint = Endpoint.Authenticating(Authenticator);
var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator);
try listener.register(auth_ep.endpoint()); try listener.register(auth_ep.endpoint());
@ -504,7 +503,7 @@ test "BasicAuth UserPass authenticateRequest" {
const a = std.testing.allocator; const a = std.testing.allocator;
// setup listener // setup listener
var listener = zap.EndpointListener.init( var listener = zap.Endpoint.Listener.init(
a, a,
.{ .{
.port = 3000, .port = 3000,
@ -517,7 +516,7 @@ test "BasicAuth UserPass authenticateRequest" {
defer listener.deinit(); defer listener.deinit();
// create mini endpoint // create mini endpoint
var ep = Endpoints.Endpoint.init(.{ var ep = Endpoint.init(.{
.path = "/test", .path = "/test",
.get = endpoint_http_get, .get = endpoint_http_get,
.unauthorized = endpoint_http_unauthorized, .unauthorized = endpoint_http_unauthorized,
@ -540,12 +539,12 @@ test "BasicAuth UserPass authenticateRequest" {
const encoded = encoder.encode(&buffer, token); const encoded = encoder.encode(&buffer, token);
// create authenticator // create authenticator
const Authenticator = Authenticators.BasicAuth(Map, .UserPass); const Authenticator = Authenticators.Basic(Map, .UserPass);
var authenticator = try Authenticator.init(a, &map, null); var authenticator = try Authenticator.init(a, &map, null);
defer authenticator.deinit(); defer authenticator.deinit();
// create authenticating endpoint // create authenticating endpoint
const BearerAuthEndpoint = Endpoints.AuthenticatingEndpoint(Authenticator); const BearerAuthEndpoint = Endpoint.Authenticating(Authenticator);
var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator);
try listener.register(auth_ep.endpoint()); try listener.register(auth_ep.endpoint());
@ -570,7 +569,7 @@ test "BasicAuth UserPass authenticateRequest test-unauthorized" {
const a = std.testing.allocator; const a = std.testing.allocator;
// setup listener // setup listener
var listener = zap.EndpointListener.init( var listener = zap.Endpoint.Listener.init(
a, a,
.{ .{
.port = 3000, .port = 3000,
@ -583,7 +582,7 @@ test "BasicAuth UserPass authenticateRequest test-unauthorized" {
defer listener.deinit(); defer listener.deinit();
// create mini endpoint // create mini endpoint
var ep = Endpoints.Endpoint.init(.{ var ep = Endpoint.init(.{
.path = "/test", .path = "/test",
.get = endpoint_http_get, .get = endpoint_http_get,
.unauthorized = endpoint_http_unauthorized, .unauthorized = endpoint_http_unauthorized,
@ -607,12 +606,12 @@ test "BasicAuth UserPass authenticateRequest test-unauthorized" {
_ = encoded; _ = encoded;
// create authenticator // create authenticator
const Authenticator = Authenticators.BasicAuth(Map, .UserPass); const Authenticator = Authenticators.Basic(Map, .UserPass);
var authenticator = try Authenticator.init(a, &map, null); var authenticator = try Authenticator.init(a, &map, null);
defer authenticator.deinit(); defer authenticator.deinit();
// create authenticating endpoint // create authenticating endpoint
const BearerAuthEndpoint = Endpoints.AuthenticatingEndpoint(Authenticator); const BearerAuthEndpoint = Endpoint.Authenticating(Authenticator);
var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator);
try listener.register(auth_ep.endpoint()); try listener.register(auth_ep.endpoint());

View file

@ -30,8 +30,8 @@ test "http parameters" {
var ran: bool = false; var ran: bool = false;
var param_count: isize = 0; var param_count: isize = 0;
var strParams: ?zap.HttpParamStrKVList = null; var strParams: ?zap.Request.HttpParamStrKVList = null;
var params: ?zap.HttpParamKVList = null; var params: ?zap.Request.HttpParamKVList = null;
var paramOneStr: ?zap.FreeOrNot = null; var paramOneStr: ?zap.FreeOrNot = null;
pub fn on_request(r: zap.Request) void { pub fn on_request(r: zap.Request) void {

View file

@ -9,12 +9,63 @@ pub const fio = @import("fio.zig");
/// Server-Side TLS function wrapper /// Server-Side TLS function wrapper
pub const Tls = @import("tls.zig"); pub const Tls = @import("tls.zig");
// pub usingnamespace @import("fio.zig"); /// Endpoint and supporting types.
pub usingnamespace @import("endpoint.zig"); /// 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 = @import("endpoint.zig");
pub usingnamespace @import("util.zig"); pub usingnamespace @import("util.zig");
pub usingnamespace @import("http.zig"); pub usingnamespace @import("http.zig");
pub usingnamespace @import("mustache.zig");
pub usingnamespace @import("http_auth.zig"); /// A struct to handle Mustache templating.
///
/// This is a wrapper around fiobj's mustache template handling.
/// See http://facil.io/0.7.x/fiobj_mustache for more information.
pub const Mustache = @import("mustache.zig");
/// Authenticators
pub const Auth = @import("http_auth.zig");
/// Http request and supporting types.
pub const Request = @import("request.zig");
/// Middleware support. /// Middleware support.
/// Contains a special Listener and a Handler struct that support chaining /// Contains a special Listener and a Handler struct that support chaining
@ -93,721 +144,6 @@ pub const ContentType = enum {
// TODO: more content types // TODO: more content types
}; };
/// HttpRequest passed to request callback functions.
pub const Request = struct {
path: ?[]const u8,
query: ?[]const u8,
body: ?[]const u8,
method: ?[]const u8,
h: [*c]fio.http_s,
/// NEVER touch this field!!!!
/// if you absolutely MUST, then you may provide context here
/// via setUserContext and getUserContext
_user_context: *UserContext,
/// NEVER touch this field!!!!
/// use markAsFinished() and isFinished() instead
/// this is a hack: the listener will put a pointer to this into the udata
/// field of `h`. So copies of the Request will all have way to the
/// same instance of this field.
_is_finished_request_global: bool,
/// NEVER touch this field!!!!
/// this is part of the hack.
_is_finished: *bool = undefined,
const UserContext = struct {
user_context: ?*anyopaque = null,
};
const Self = @This();
/// mark the current request as finished. Important for middleware-style
/// request handler chaining. Called when sending a body, redirecting, etc.
pub fn markAsFinished(self: *const Self, finished: bool) void {
// we might be a copy
self._is_finished.* = finished;
}
/// tell whether request processing has finished. (e.g. response sent,
/// redirected, ...)
pub fn isFinished(self: *const Self) bool {
// we might be a copy
return self._is_finished.*;
}
/// if you absolutely must, you can set any context on the request here
// (note, this line is linked to from the readme) -- TODO: sync
pub fn setUserContext(self: *const Self, context: *anyopaque) void {
self._user_context.*.user_context = context;
}
/// get the associated user context of the request.
pub fn getUserContext(self: *const Self, comptime Context: type) ?*Context {
if (self._user_context.*.user_context) |ptr| {
return @as(*Context, @ptrCast(@alignCast(ptr)));
} else {
return null;
}
}
/// Tries to send an error stack trace.
pub fn sendError(self: *const Self, err: anyerror, errorcode_num: usize) void {
// TODO: query accept headers
if (self._internal_sendError(err, errorcode_num)) {
return;
} else |_| {
self.sendBody(@errorName(err)) catch return;
}
}
/// Used internally. Probably does not need to be public.
pub fn _internal_sendError(self: *const Self, err: anyerror, errorcode_num: usize) !void {
// TODO: query accept headers
// TODO: let's hope 20k is enough. Maybe just really allocate here
self.h.*.status = errorcode_num;
var buf: [20 * 1024]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);
var string = std.ArrayList(u8).init(fba.allocator());
var writer = string.writer();
try writer.print("ERROR: {any}\n\n", .{err});
var debugInfo = try std.debug.getSelfDebugInfo();
var ttyConfig: std.io.tty.Config = .no_color;
try std.debug.writeCurrentStackTrace(writer, debugInfo, ttyConfig, null);
try self.sendBody(string.items);
}
/// Send body.
pub fn sendBody(self: *const Self, body: []const u8) HttpError!void {
const ret = fio.http_send_body(self.h, @as(
*anyopaque,
@ptrFromInt(@intFromPtr(body.ptr)),
), body.len);
debug("Request.sendBody(): ret = {}\n", .{ret});
if (ret == -1) return error.HttpSendBody;
self.markAsFinished(true);
}
/// Set content type and send json buffer.
pub fn sendJson(self: *const Self, json: []const u8) HttpError!void {
if (self.setContentType(.JSON)) {
if (fio.http_send_body(self.h, @as(
*anyopaque,
@ptrFromInt(@intFromPtr(json.ptr)),
), json.len) != 0) return error.HttpSendBody;
self.markAsFinished(true);
} else |err| return err;
}
/// Set content type.
pub fn setContentType(self: *const Self, c: ContentType) HttpError!void {
const s = switch (c) {
.TEXT => "text/plain",
.JSON => "application/json",
else => "text/html",
};
debug("setting content-type to {s}\n", .{s});
return self.setHeader("content-type", s);
}
/// redirect to path with status code 302 by default
pub fn redirectTo(self: *const Self, path: []const u8, code: ?http.StatusCode) HttpError!void {
self.setStatus(if (code) |status| status else .found);
try self.setHeader("Location", path);
try self.sendBody("moved");
self.markAsFinished(true);
}
/// shows how to use the logger
pub fn setContentTypeWithLogger(
self: *const Self,
c: ContentType,
logger: *const Log,
) HttpError!void {
const s = switch (c) {
.TEXT => "text/plain",
.JSON => "application/json",
else => "text/html",
};
logger.log("setting content-type to {s}\n", .{s});
return self.setHeader("content-type", s);
}
/// Tries to determine the content type by file extension of request path, and sets it.
pub fn setContentTypeFromPath(self: *const Self) !void {
const t = fio.http_mimetype_find2(self.h.*.path);
if (fio.is_invalid(t) == 1) return error.HttpSetContentType;
const ret = fio.fiobj_hash_set(
self.h.*.private_data.out_headers,
fio.HTTP_HEADER_CONTENT_TYPE,
t,
);
if (ret == -1) return error.HttpSetContentType;
}
/// Tries to determine the content type by filename extension, and sets it.
/// If the extension cannot be determined, NoExtensionInFilename error is
/// returned.
pub fn setContentTypeFromFilename(self: *const Self, filename: []const u8) !void {
const ext = std.fs.path.extension(filename);
if (ext.len > 1) {
const e = ext[1..];
const obj = fio.http_mimetype_find(@constCast(e.ptr), e.len);
if (util.fio2str(obj)) |mime_str| {
try self.setHeader("content-type", mime_str);
}
} else {
return error.NoExtensionInFilename;
}
}
/// Returns the header value of given key name. Returned mem is temp.
/// Do not free it.
pub fn getHeader(self: *const Self, name: []const u8) ?[]const u8 {
const hname = fio.fiobj_str_new(util.toCharPtr(name), name.len);
defer fio.fiobj_free_wrapped(hname);
return util.fio2str(fio.fiobj_hash_get(self.h.*.headers, hname));
}
/// Set header.
pub fn setHeader(self: *const Self, name: []const u8, value: []const u8) HttpError!void {
const hname: fio.fio_str_info_s = .{
.data = util.toCharPtr(name),
.len = name.len,
.capa = name.len,
};
debug("setHeader: hname = {s}\n", .{name});
const vname: fio.fio_str_info_s = .{
.data = util.toCharPtr(value),
.len = value.len,
.capa = value.len,
};
debug("setHeader: vname = {s}\n", .{value});
const ret = fio.http_set_header2(self.h, hname, vname);
// FIXME without the following if, we get errors in release builds
// at least we don't have to log unconditionally
if (ret == -1) {
std.debug.print("***************** zap.zig:274\n", .{});
}
debug("setHeader: ret = {}\n", .{ret});
if (ret == 0) return;
return error.HttpSetHeader;
}
/// Set status by numeric value.
pub fn setStatusNumeric(self: *const Self, status: usize) void {
self.h.*.status = status;
}
/// Set status by enum.
pub fn setStatus(self: *const Self, status: http.StatusCode) void {
self.h.*.status = @as(usize, @intCast(@intFromEnum(status)));
}
/// Sends a file if present in the filesystem orelse returns an error.
///
/// - efficiently sends a file using gzip compression
/// - also handles range requests if `Range` or `If-Range` headers are present in the request.
/// - sends the response headers and the specified file (the response's body).
///
/// On success, the `self.h` handle will be consumed and invalid.
/// On error, the handle will still be valid and should be used to send an error response
///
/// Important: sets last-modified and cache-control headers with a max-age value of 1 hour!
/// You can override that by setting those headers yourself, e.g.: setHeader("Cache-Control", "no-cache")
pub fn sendFile(self: *const Self, file_path: []const u8) !void {
if (fio.http_sendfile2(self.h, util.toCharPtr(file_path), file_path.len, null, 0) != 0)
return error.SendFile;
self.markAsFinished(true);
}
/// Attempts to decode the request's body.
/// This should be called BEFORE parseQuery
/// Result is accessible via parametersToOwnedSlice(), parametersToOwnedStrSlice()
///
/// Supported body types:
/// - application/x-www-form-urlencoded
/// - application/json
/// - multipart/form-data
pub fn parseBody(self: *const Self) HttpError!void {
if (fio.http_parse_body(self.h) == -1) return error.HttpParseBody;
}
/// Parses the query part of an HTTP request
/// This should be called AFTER parseBody(), just in case the body is a JSON
/// object that doesn't have a hash map at its root.
///
/// Result is accessible via parametersToOwnedSlice(), parametersToOwnedStrSlice()
pub fn parseQuery(self: *const Self) void {
fio.http_parse_query(self.h);
}
/// Parse received cookie headers
pub fn parseCookies(self: *const Self, url_encoded: bool) void {
fio.http_parse_cookies(self.h, if (url_encoded) 1 else 0);
}
/// 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 = @as(isize, @intCast(args.name.len)),
.value = util.toCharPtr(args.value),
.value_len = @as(isize, @intCast(args.value.len)),
.domain = if (args.domain) |p| util.toCharPtr(p) else null,
.domain_len = if (args.domain) |p| @as(isize, @intCast(p.len)) else 0,
.path = if (args.path) |p| util.toCharPtr(p) else null,
.path_len = if (args.path) |p| @as(isize, @intCast(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,
};
// TODO WAT?
// if we:
// if(fio.http_set_cookie(...) == -1)
// instead of capturing it in `ret` first and then checking it,
// all ReleaseXXX builds return an error!
// TODO: still happening?
const ret = fio.http_set_cookie(self.h, c);
if (ret == -1) {
std.log.err("fio.http_set_cookie returned: {}\n", .{ret});
return error.SetCookie;
}
}
/// Returns named cookie. Works like getParamStr().
pub fn getCookieStr(self: *const Self, a: std.mem.Allocator, name: []const u8, 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(a, value, always_alloc);
}
/// Returns the number of cookies 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.
///
/// Parse with parseBody() and / or parseQuery()
pub fn getParamCount(self: *const Self) isize {
if (self.h.*.params == 0) return 0;
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, @as(usize, @intCast(self.getCookiesCount())));
var context: _parametersToOwnedStrSliceContext = .{
.params = &params,
.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, @as(usize, @intCast(self.getCookiesCount())));
var context: _parametersToOwnedSliceContext = .{ .params = &params, .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:
///
/// - Bool
/// - Int
/// - Float
/// - String
///
/// At the moment, no fio ARRAYs are supported as well as HASH maps.
/// So, for JSON body payloads: parse the body instead.
///
/// Requires parseBody() and/or parseQuery() have been called.
/// Returned list needs to be deinited.
pub fn parametersToOwnedStrList(self: *const Self, a: std.mem.Allocator, always_alloc: bool) anyerror!HttpParamStrKVList {
var params = try std.ArrayList(HttpParamStrKV).initCapacity(a, @as(usize, @intCast(self.getParamCount())));
var context: _parametersToOwnedStrSliceContext = .{
.params = &params,
.allocator = a,
.always_alloc = always_alloc,
};
const howmany = fio.fiobj_each1(self.h.*.params, 0, _each_nextParamStr, &context);
if (howmany != self.getParamCount()) {
return error.HttpIterParams;
}
return .{ .items = try params.toOwnedSlice(), .allocator = a };
}
const _parametersToOwnedStrSliceContext = struct {
allocator: std.mem.Allocator,
params: *std.ArrayList(HttpParamStrKV),
last_error: ?anyerror = null,
always_alloc: bool,
};
fn _each_nextParamStr(fiobj_value: fio.FIOBJ, context: ?*anyopaque) callconv(.C) c_int {
const ctx: *_parametersToOwnedStrSliceContext = @as(*_parametersToOwnedStrSliceContext, @ptrCast(@alignCast(context)));
// this is thread-safe, guaranteed by fio
var fiobj_key: fio.FIOBJ = fio.fiobj_hash_key_in_loop();
ctx.params.append(.{
.key = util.fio2strAllocOrNot(ctx.allocator, fiobj_key, ctx.always_alloc) catch |err| {
ctx.last_error = err;
return -1;
},
.value = util.fio2strAllocOrNot(ctx.allocator, fiobj_value, ctx.always_alloc) catch |err| {
ctx.last_error = err;
return -1;
},
}) catch |err| {
// what to do?
// signal the caller that an error occured by returning -1
// also, set the error
ctx.last_error = err;
return -1;
};
return 0;
}
/// Returns the query / body parameters as key/value pairs
/// Supported param types that will be converted:
///
/// - Bool
/// - Int
/// - Float
/// - String
///
/// At the moment, no fio ARRAYs are supported as well as HASH maps.
/// So, for JSON body payloads: parse the body instead.
///
/// Requires parseBody() and/or parseQuery() have been called.
/// Returned slice needs to be freed.
pub fn parametersToOwnedList(self: *const Self, a: std.mem.Allocator, dupe_strings: bool) !HttpParamKVList {
var params = try std.ArrayList(HttpParamKV).initCapacity(a, @as(usize, @intCast(self.getParamCount())));
var context: _parametersToOwnedSliceContext = .{ .params = &params, .allocator = a, .dupe_strings = dupe_strings };
const howmany = fio.fiobj_each1(self.h.*.params, 0, _each_nextParam, &context);
if (howmany != self.getParamCount()) {
return error.HttpIterParams;
}
return .{ .items = try params.toOwnedSlice(), .allocator = a };
}
const _parametersToOwnedSliceContext = struct {
params: *std.ArrayList(HttpParamKV),
last_error: ?anyerror = null,
allocator: std.mem.Allocator,
dupe_strings: bool,
};
fn _each_nextParam(fiobj_value: fio.FIOBJ, context: ?*anyopaque) callconv(.C) c_int {
const ctx: *_parametersToOwnedSliceContext = @as(*_parametersToOwnedSliceContext, @ptrCast(@alignCast(context)));
// this is thread-safe, guaranteed by fio
var fiobj_key: fio.FIOBJ = fio.fiobj_hash_key_in_loop();
ctx.params.append(.{
.key = util.fio2strAllocOrNot(ctx.allocator, fiobj_key, ctx.dupe_strings) catch |err| {
ctx.last_error = err;
return -1;
},
.value = Fiobj2HttpParam(ctx.allocator, fiobj_value, ctx.dupe_strings) catch |err| {
ctx.last_error = err;
return -1;
},
}) catch |err| {
// what to do?
// signal the caller that an error occured by returning -1
// also, set the error
ctx.last_error = err;
return -1;
};
return 0;
}
/// get named parameter as string
/// Supported param types that will be converted:
///
/// - Bool
/// - Int
/// - Float
/// - String
///
/// At the moment, no fio ARRAYs are supported as well as HASH maps.
/// So, for JSON body payloads: parse the body instead.
///
/// Requires parseBody() and/or parseQuery() have been called.
/// The returned string needs to be deinited with .deinit()
pub fn getParamStr(self: *const Self, a: std.mem.Allocator, name: []const u8, always_alloc: bool) !?util.FreeOrNot {
if (self.h.*.params == 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.*.params, key);
if (value == fio.FIOBJ_INVALID) {
return null;
}
return try util.fio2strAllocOrNot(a, value, always_alloc);
}
};
/// Key value pair of strings from HTTP parameters
pub const HttpParamStrKV = struct {
key: util.FreeOrNot,
value: util.FreeOrNot,
pub fn deinit(self: *@This()) void {
self.key.deinit();
self.value.deinit();
}
};
/// List of key value pairs of Http param strings.
pub const HttpParamStrKVList = struct {
items: []HttpParamStrKV,
allocator: std.mem.Allocator,
pub fn deinit(self: *@This()) void {
for (self.items) |*item| {
item.deinit();
}
self.allocator.free(self.items);
}
};
/// List of key value pairs of Http params (might be of different types).
pub const HttpParamKVList = struct {
items: []HttpParamKV,
allocator: std.mem.Allocator,
pub fn deinit(self: *const @This()) void {
for (self.items) |*item| {
item.deinit();
}
self.allocator.free(self.items);
}
};
/// Enum for HttpParam tagged union
pub const HttpParamValueType = enum {
// Null,
Bool,
Int,
Float,
String,
Unsupported,
Hash_Binfile,
Array_Binfile,
};
/// Tagged union holding a typed Http param
pub const HttpParam = union(HttpParamValueType) {
Bool: bool,
Int: isize,
Float: f64,
/// we don't do writable strings here
String: util.FreeOrNot,
/// value will always be null
Unsupported: ?void,
/// we assume hashes are because of file transmissions
Hash_Binfile: HttpParamBinaryFile,
/// value will always be null
Array_Binfile: std.ArrayList(HttpParamBinaryFile),
};
/// Key value pair of one typed Http param
pub const HttpParamKV = struct {
key: util.FreeOrNot,
value: ?HttpParam,
pub fn deinit(self: *@This()) void {
self.key.deinit();
if (self.value) |p| {
switch (p) {
.String => |*s| s.deinit(),
else => {},
}
}
}
};
/// Struct representing an uploaded file.
pub const HttpParamBinaryFile = struct {
/// file contents
data: ?[]const u8 = null,
/// mimetype
mimetype: ?[]const u8 = null,
/// filename
filename: ?[]const u8 = null,
/// format function for printing file upload data
pub fn format(value: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) std.os.WriteError!void {
const d = value.data orelse "\\0";
const m = value.mimetype orelse "null";
const f = value.filename orelse "null";
return writer.print("<{s} ({s}): {any}>", .{ f, m, d });
}
};
fn parseBinfilesFrom(a: std.mem.Allocator, o: fio.FIOBJ) !HttpParam {
const key_name = fio.fiobj_str_new("name", 4);
const key_data = fio.fiobj_str_new("data", 4);
const key_type = fio.fiobj_str_new("type", 4);
defer {
fio.fiobj_free_wrapped(key_name);
fio.fiobj_free_wrapped(key_data);
fio.fiobj_free_wrapped(key_type);
} // files: they should have "data", "type", and "filename" keys
if (fio.fiobj_hash_haskey(o, key_data) == 1 and fio.fiobj_hash_haskey(o, key_type) == 1 and fio.fiobj_hash_haskey(o, key_name) == 1) {
const filename = fio.fiobj_obj2cstr(fio.fiobj_hash_get(o, key_name));
const mimetype = fio.fiobj_obj2cstr(fio.fiobj_hash_get(o, key_type));
const data = fio.fiobj_hash_get(o, key_data);
var data_slice: ?[]const u8 = null;
switch (fio.fiobj_type(data)) {
fio.FIOBJ_T_DATA => {
if (fio.is_invalid(data) == 1) {
data_slice = "(zap: invalid data)";
std.log.warn("WARNING: HTTP param binary file is not a data object\n", .{});
} else {
// the data
const data_len = fio.fiobj_data_len(data);
var data_buf = fio.fiobj_data_read(data, data_len);
if (data_len < 0) {
std.log.warn("WARNING: HTTP param binary file size negative: {d}\n", .{data_len});
std.log.warn("FIOBJ_TYPE of data is: {d}\n", .{fio.fiobj_type(data)});
} else {
if (data_buf.len != data_len) {
std.log.warn("WARNING: HTTP param binary file size mismatch: should {d}, is: {d}\n", .{ data_len, data_buf.len });
}
if (data_buf.len > 0) {
data_slice = data_buf.data[0..data_buf.len];
} else {
std.log.warn("WARNING: HTTP param binary file buffer size negative: {d}\n", .{data_buf.len});
data_slice = "(zap: invalid data: negative BUFFER size)";
}
}
}
},
fio.FIOBJ_T_STRING => {
const fiostr = fio.fiobj_obj2cstr(data);
if (fiostr.len == 0) {
data_slice = "(zap: empty string data)";
std.log.warn("WARNING: HTTP param binary file has empty string object\n", .{});
} else {
data_slice = fiostr.data[0..fiostr.len];
}
},
fio.FIOBJ_T_ARRAY => {
// OK, data is an array
const len = fio.fiobj_ary_count(data);
const fn_ary = fio.fiobj_hash_get(o, key_name);
const mt_ary = fio.fiobj_hash_get(o, key_type);
if (fio.fiobj_ary_count(fn_ary) == len and fio.fiobj_ary_count(mt_ary) == len) {
var i: isize = 0;
var ret = std.ArrayList(HttpParamBinaryFile).init(a);
while (i < len) : (i += 1) {
const file_data_obj = fio.fiobj_ary_entry(data, i);
const file_name_obj = fio.fiobj_ary_entry(fn_ary, i);
const file_mimetype_obj = fio.fiobj_ary_entry(mt_ary, i);
var has_error: bool = false;
if (fio.is_invalid(file_data_obj) == 1) {
std.log.debug("file data invalid in array", .{});
has_error = true;
}
if (fio.is_invalid(file_name_obj) == 1) {
std.log.debug("file name invalid in array", .{});
has_error = true;
}
if (fio.is_invalid(file_mimetype_obj) == 1) {
std.log.debug("file mimetype invalid in array", .{});
has_error = true;
}
if (has_error) {
return error.Invalid;
}
const file_data = fio.fiobj_obj2cstr(file_data_obj);
const file_name = fio.fiobj_obj2cstr(file_name_obj);
const file_mimetype = fio.fiobj_obj2cstr(file_mimetype_obj);
try ret.append(.{
.data = file_data.data[0..file_data.len],
.mimetype = file_mimetype.data[0..file_mimetype.len],
.filename = file_name.data[0..file_name.len],
});
}
return .{ .Array_Binfile = ret };
} else {
return error.ArrayLenMismatch;
}
},
else => {
// don't know what to do
return error.Unsupported;
},
}
return .{ .Hash_Binfile = .{
.filename = filename.data[0..filename.len],
.mimetype = mimetype.data[0..mimetype.len],
.data = data_slice,
} };
} else {
return .{ .Hash_Binfile = .{} };
}
}
/// Parse FIO object into a typed Http param. Supports file uploads.
pub fn Fiobj2HttpParam(a: std.mem.Allocator, o: fio.FIOBJ, dupe_string: bool) !?HttpParam {
return switch (fio.fiobj_type(o)) {
fio.FIOBJ_T_NULL => null,
fio.FIOBJ_T_TRUE => .{ .Bool = true },
fio.FIOBJ_T_FALSE => .{ .Bool = false },
fio.FIOBJ_T_NUMBER => .{ .Int = fio.fiobj_obj2num(o) },
fio.FIOBJ_T_FLOAT => .{ .Float = fio.fiobj_obj2float(o) },
fio.FIOBJ_T_STRING => .{ .String = try util.fio2strAllocOrNot(a, o, dupe_string) },
fio.FIOBJ_T_ARRAY => {
return .{ .Unsupported = null };
},
fio.FIOBJ_T_HASH => {
const file = try parseBinfilesFrom(a, o);
return file;
},
else => .{ .Unsupported = null },
};
}
/// Args for setting a cookie
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,
};
/// Used internally: facilio Http request callback function type /// Used internally: facilio Http request callback function type
pub const FioHttpRequestFn = *const fn (r: [*c]fio.http_s) callconv(.C) void; pub const FioHttpRequestFn = *const fn (r: [*c]fio.http_s) callconv(.C) void;
@ -921,7 +257,7 @@ pub const HttpListener = struct {
._is_finished_request_global = false, ._is_finished_request_global = false,
._user_context = undefined, ._user_context = undefined,
}; };
var zigtarget: []u8 = target[0..target_len]; const zigtarget: []u8 = target[0..target_len];
req._is_finished = &req._is_finished_request_global; req._is_finished = &req._is_finished_request_global;
var user_context: Request.UserContext = .{}; var user_context: Request.UserContext = .{};
@ -949,7 +285,7 @@ pub const HttpListener = struct {
pfolder = pf.ptr; pfolder = pf.ptr;
} }
var x: fio.http_settings_s = .{ const x: fio.http_settings_s = .{
.on_request = if (self.settings.on_request) |_| Self.theOneAndOnlyRequestCallBack else null, .on_request = if (self.settings.on_request) |_| Self.theOneAndOnlyRequestCallBack else null,
.on_upgrade = if (self.settings.on_upgrade) |_| Self.theOneAndOnlyUpgradeCallBack else null, .on_upgrade = if (self.settings.on_upgrade) |_| Self.theOneAndOnlyUpgradeCallBack else null,
.on_response = if (self.settings.on_response) |_| Self.theOneAndOnlyResponseCallBack else null, .on_response = if (self.settings.on_response) |_| Self.theOneAndOnlyResponseCallBack else null,