diff --git a/doc/authentication.md b/doc/authentication.md index 313a827..0df5483 100644 --- a/doc/authentication.md +++ b/doc/authentication.md @@ -4,23 +4,23 @@ Zap supports both Basic and Bearer authentication which are based on HTTP headers. For a cookie-based ("session token", not to mistake for "session cookie") -authentication, see the [UserPassSessionAuth](../src/http_auth.zig#L319) and its +authentication, see the [UserPassSession](../src/http_auth.zig#L319) and its [example](../examples/userpass_session_auth/). For convenience, Authenticator types exist that can authenticate requests. -Zap also provides an `AuthenticatingEndpoint` endpoint-wrapper. Have a look at the [example](../examples/endpoint_auth) and the [tests](../src/tests/test_auth.zig). +Zap also provides an `Endpoint.Authenticating` endpoint-wrapper. Have a look at the [example](../examples/endpoint_auth) and the [tests](../src/tests/test_auth.zig). The following describes the Authenticator types. All of them provide the `authenticateRequest()` function, which takes a `zap.Request` and returns a bool value whether it could be authenticated or not. Further down, we show how to use the Authenticators, and also the -`AuthenticatingEndpoint`. +`Endpoint.Authenticating`. ## Basic Authentication -The `zap.BasicAuth` Authenticator accepts 2 comptime values: +The `zap.Auth.Basic` Authenticator accepts 2 comptime values: - `Lookup`: either a map to look up passwords for users or a set to lookup base64 encoded tokens (user:pass -> base64-encode = token) @@ -35,11 +35,11 @@ support `contains([]const u8)`. ## Bearer Authentication -The `zap.BearerAuthSingle` Authenticator is a convenience-authenticator that +The `zap.Auth.BearerSingle` Authenticator is a convenience-authenticator that takes a single auth token. If all you need is to protect your prototype with a token, this is the one you want to use. -`zap.BearerAuthMulti` accepts a map (`Lookup`) that needs to support +`zap.BearerMulti` accepts a map (`Lookup`) that needs to support `contains([]const u8)`. ## Request Authentication @@ -56,7 +56,7 @@ const zap = @import("zap"); const allocator = std.heap.page_allocator; const token = "hello, world"; -var auth = try zap.BearerAuthSingle.init(allocator, token, null); +var auth = try zap.Auth.BearerSingle.init(allocator, token, null); defer auth.deinit(); @@ -90,7 +90,7 @@ defer set.deinit(); // insert auth tokens try set.put(token, {}); -var auth = try zap.BearerAuthMulti(Set).init(allocator, &set, null); +var auth = try zap.Auth.BearerMulti(Set).init(allocator, &set, null); defer auth.deinit(); @@ -127,7 +127,7 @@ const pass = "opensesame"; try map.put(user, pass); // create authenticator -const Authenticator = zap.BasicAuth(Map, .UserPass); +const Authenticator = zap.Auth.Basic(Map, .UserPass); var auth = try Authenticator.init(a, &map, null); defer auth.deinit(); @@ -163,7 +163,7 @@ defer set.deinit(); try set.put(token, {}); // create authenticator -const Authenticator = zap.BasicAuth(Set, .Token68); +const Authenticator = zap.Auth.Basic(Set, .Token68); var auth = try Authenticator.init(allocator, &set, null); defer auth.deinit(); @@ -182,15 +182,15 @@ fn on_request(r: zap.Request) void { } ``` -## AuthenticatingEndpoint +## Endpoint.Authenticating Here, we only show using one of the Authenticator types. See the tests for more examples. -The `AuthenticatingEndpoint` honors `.unauthorized` in the endpoint settings, where you can pass in a callback to deal with unauthorized requests. If you leave it to `null`, the endpoint will automatically reply with a `401 - Unauthorized` response. +The `Endpoint.Authenticating` honors `.unauthorized` in the endpoint settings, where you can pass in a callback to deal with unauthorized requests. If you leave it to `null`, the endpoint will automatically reply with a `401 - Unauthorized` response. The example below should make clear how to wrap an endpoint into an -`AuthenticatingEndpoint`: +`Endpoint.Authenticating`: ```zig const std = @import("std"); @@ -240,12 +240,12 @@ pub fn main() !void { }); // create authenticator - const Authenticator = zap.BearerAuthSingle; + const Authenticator = zap.Auth.BearerSingle; var authenticator = try Authenticator.init(a, token, null); defer authenticator.deinit(); // create authenticating endpoint - const BearerAuthEndpoint = zap.AuthenticatingEndpoint(Authenticator); + const BearerAuthEndpoint = zap.Endpoint.Authenticating(Authenticator); var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); try listener.register(auth_ep.endpoint()); diff --git a/examples/bindataformpost/bindataformpost.zig b/examples/bindataformpost/bindataformpost.zig index 2578b21..944b92e 100644 --- a/examples/bindataformpost/bindataformpost.zig +++ b/examples/bindataformpost/bindataformpost.zig @@ -31,7 +31,7 @@ const Handler = struct { std.log.info("Param `{s}` in owned list is {any}\n", .{ kv.key.str, v }); switch (v) { // single-file upload - zap.HttpParam.Hash_Binfile => |*file| { + zap.Request.HttpParam.Hash_Binfile => |*file| { const filename = file.filename orelse "(no filename)"; const mimetype = file.mimetype orelse "(no mimetype)"; const data = file.data orelse ""; @@ -41,7 +41,7 @@ const Handler = struct { std.log.debug(" contents: {any}\n", .{data}); }, // multi-file upload - zap.HttpParam.Array_Binfile => |*files| { + zap.Request.HttpParam.Array_Binfile => |*files| { for (files.*.items) |file| { const filename = file.filename orelse "(no filename)"; const mimetype = file.mimetype orelse "(no mimetype)"; diff --git a/examples/endpoint/main.zig b/examples/endpoint/main.zig index a41daa5..31bb487 100644 --- a/examples/endpoint/main.zig +++ b/examples/endpoint/main.zig @@ -21,7 +21,7 @@ pub fn main() !void { // we scope everything that can allocate within this block for leak detection { // setup listener - var listener = zap.EndpointListener.init( + var listener = zap.Endpoint.Listener.init( allocator, .{ .port = 3000, diff --git a/examples/endpoint_auth/endpoint_auth.zig b/examples/endpoint_auth/endpoint_auth.zig index 3c0f19e..4b69d7e 100644 --- a/examples/endpoint_auth/endpoint_auth.zig +++ b/examples/endpoint_auth/endpoint_auth.zig @@ -25,7 +25,7 @@ fn endpoint_http_unauthorized(e: *zap.Endpoint, r: zap.Request) void { pub fn main() !void { // setup listener - var listener = zap.EndpointListener.init( + var listener = zap.Endpoint.Listener.init( a, .{ .port = 3000, @@ -45,12 +45,12 @@ pub fn main() !void { }); // create authenticator - const Authenticator = zap.BearerAuthSingle; + const Authenticator = zap.Auth.BearerSingle; var authenticator = try Authenticator.init(a, token, null); defer authenticator.deinit(); // create authenticating endpoint - const BearerAuthEndpoint = zap.AuthenticatingEndpoint(Authenticator); + const BearerAuthEndpoint = zap.Endpoint.Authenticating(Authenticator); var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); try listener.register(auth_ep.endpoint()); diff --git a/examples/userpass_session_auth/userpass_session_auth.zig b/examples/userpass_session_auth/userpass_session_auth.zig index 0880eb2..40f07cb 100644 --- a/examples/userpass_session_auth/userpass_session_auth.zig +++ b/examples/userpass_session_auth/userpass_session_auth.zig @@ -5,7 +5,7 @@ const Lookup = std.StringHashMap([]const u8); const auth_lock_pw_table = false; // see the source for more info -const Authenticator = zap.UserPassSessionAuth( +const Authenticator = zap.Auth.UserPassSession( Lookup, // we may set this to true if we expect our username -> password map // to change. in that case the authenticator must lock the table for diff --git a/src/endpoint.zig b/src/endpoint.zig index e61c591..4040623 100644 --- a/src/endpoint.zig +++ b/src/endpoint.zig @@ -2,16 +2,18 @@ const std = @import("std"); const zap = @import("zap.zig"); const auth = @import("http_auth.zig"); +const Endpoint = @This(); + // zap types const Request = zap.Request; const ListenerSettings = zap.HttpListenerSettings; -const Listener = zap.HttpListener; +const HttpListener = zap.HttpListener; /// Type of the request function callbacks. pub const RequestFn = *const fn (self: *Endpoint, r: Request) void; /// Settings to initialize an Endpoint -pub const EndpointSettings = struct { +pub const Settings = struct { /// path / slug of the endpoint path: []const u8, /// callback to GET request handler @@ -26,101 +28,57 @@ pub const EndpointSettings = struct { patch: ?RequestFn = null, /// callback to OPTIONS request handler options: ?RequestFn = null, - /// Only applicable to AuthenticatingEndpoint: handler for unauthorized requests + /// Only applicable to Authenticating Endpoint: handler for unauthorized requests unauthorized: ?RequestFn = null, }; -/// The simple Endpoint struct. Create one and pass in your callbacks. Then, -/// pass it to a HttpListener's `register()` function to register with the -/// listener. -/// -/// **NOTE**: A common endpoint pattern for zap is to create your own struct -/// that embeds an Endpoint, provides specific callbacks, and uses -/// `@fieldParentPtr` to get a reference to itself. -/// -/// Example: -/// A simple endpoint listening on the /stop route that shuts down zap. -/// The main thread usually continues at the instructions after the call to zap.start(). -/// -/// ```zig -/// const StopEndpoint = struct { -/// ep: zap.Endpoint = undefined, -/// -/// pub fn init( -/// path: []const u8, -/// ) StopEndpoint { -/// return .{ -/// .ep = zap.Endpoint.init(.{ -/// .path = path, -/// .get = get, -/// }), -/// }; -/// } -/// -/// // access the internal Endpoint -/// pub fn endpoint(self: *StopEndpoint) *zap.Endpoint { -/// return &self.ep; -/// } -/// -/// fn get(e: *zap.Endpoint, r: zap.Request) void { -/// const self: *StopEndpoint = @fieldParentPtr(StopEndpoint, "ep", e); -/// _ = self; -/// _ = r; -/// zap.stop(); -/// } -/// }; -/// ``` -pub const Endpoint = struct { - settings: EndpointSettings, - - const Self = @This(); +settings: Settings, - /// Initialize the endpoint. - /// Set only the callbacks you need. Requests of HTTP methods without a - /// provided callback will be ignored. - pub fn init(s: EndpointSettings) Self { - return .{ - .settings = .{ - .path = s.path, - .get = s.get orelse &nop, - .post = s.post orelse &nop, - .put = s.put orelse &nop, - .delete = s.delete orelse &nop, - .patch = s.patch orelse &nop, - .options = s.options orelse &nop, - .unauthorized = s.unauthorized orelse &nop, - }, - }; - } +/// Initialize the endpoint. +/// Set only the callbacks you need. Requests of HTTP methods without a +/// provided callback will be ignored. +pub fn init(s: Settings) Endpoint { + return .{ + .settings = .{ + .path = s.path, + .get = s.get orelse &nop, + .post = s.post orelse &nop, + .put = s.put orelse &nop, + .delete = s.delete orelse &nop, + .patch = s.patch orelse &nop, + .options = s.options orelse &nop, + .unauthorized = s.unauthorized orelse &nop, + }, + }; +} - // no operation. Dummy handler function for ignoring unset request types. - fn nop(self: *Endpoint, r: Request) void { - _ = self; - _ = r; - } +// no operation. Dummy handler function for ignoring unset request types. +fn nop(self: *Endpoint, r: Request) void { + _ = self; + _ = r; +} - /// The global request handler for this Endpoint, called by the listener. - pub fn onRequest(self: *Endpoint, r: zap.Request) void { - if (r.method) |m| { - if (std.mem.eql(u8, m, "GET")) - return self.settings.get.?(self, r); - if (std.mem.eql(u8, m, "POST")) - return self.settings.post.?(self, r); - if (std.mem.eql(u8, m, "PUT")) - return self.settings.put.?(self, r); - if (std.mem.eql(u8, m, "DELETE")) - return self.settings.delete.?(self, r); - if (std.mem.eql(u8, m, "PATCH")) - return self.settings.patch.?(self, r); - if (std.mem.eql(u8, m, "OPTIONS")) - return self.settings.options.?(self, r); - } +/// The global request handler for this Endpoint, called by the listener. +pub fn onRequest(self: *Endpoint, r: zap.Request) void { + if (r.method) |m| { + if (std.mem.eql(u8, m, "GET")) + return self.settings.get.?(self, r); + if (std.mem.eql(u8, m, "POST")) + return self.settings.post.?(self, r); + if (std.mem.eql(u8, m, "PUT")) + return self.settings.put.?(self, r); + if (std.mem.eql(u8, m, "DELETE")) + return self.settings.delete.?(self, r); + if (std.mem.eql(u8, m, "PATCH")) + return self.settings.patch.?(self, r); + if (std.mem.eql(u8, m, "OPTIONS")) + return self.settings.options.?(self, r); } -}; +} /// Wrap an endpoint with an Authenticator -> new Endpoint of type Endpoint /// is available via the `endpoint()` function. -pub fn AuthenticatingEndpoint(comptime Authenticator: type) type { +pub fn Authenticating(comptime Authenticator: type) type { return struct { authenticator: *Authenticator, ep: *Endpoint, @@ -289,8 +247,8 @@ pub const EndpointListenerError = error{ /// The listener with ednpoint support /// /// NOTE: It switches on path.startsWith -> so use endpoints with distinctly starting names!! -pub const EndpointListener = struct { - listener: Listener, +pub const Listener = struct { + listener: HttpListener, allocator: std.mem.Allocator, const Self = @This(); @@ -314,12 +272,12 @@ pub const EndpointListener = struct { // override the settings with our internal, actul callback function // so that "we" will be called on request - ls.on_request = onRequest; + ls.on_request = Listener.onRequest; // store the settings-provided request callback for later use on_request = l.on_request; return .{ - .listener = Listener.init(ls), + .listener = HttpListener.init(ls), .allocator = a, }; } diff --git a/src/http_auth.zig b/src/http_auth.zig index cafd0ba..e8a7ad2 100644 --- a/src/http_auth.zig +++ b/src/http_auth.zig @@ -61,7 +61,7 @@ pub const AuthResult = enum { /// The authenticator handled the request that didn't pass authentication / /// authorization. /// 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. Handled, }; @@ -79,7 +79,7 @@ pub const AuthResult = enum { /// WWW-Authenticate: Basic realm="this" /// /// 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 { allocator: std.mem.Allocator, realm: ?[]const u8, @@ -219,7 +219,7 @@ pub fn BasicAuth(comptime Lookup: type, comptime kind: BasicAuthStrategy) type { /// Errors: /// HTTP/1.1 401 Unauthorized /// WWW-Authenticate: Bearer realm="example", error="invalid_token", error_description="..." -pub const BearerAuthSingle = struct { +pub const BearerSingle = struct { allocator: std.mem.Allocator, token: []const u8, realm: ?[]const u8, @@ -276,7 +276,7 @@ pub const BearerAuthSingle = struct { /// Errors: /// HTTP/1.1 401 Unauthorized /// 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 { allocator: std.mem.Allocator, lookup: *Lookup, @@ -325,8 +325,8 @@ pub fn BearerAuthMulti(comptime Lookup: type) type { }; } -/// Settings to initialize a UserPassSessionAuth authenticator. -pub const UserPassSessionAuthArgs = struct { +/// Settings to initialize a UserPassSession authenticator. +pub const UserPassSessionArgs = struct { /// username body parameter usernameParam: []const u8, /// password body parameter @@ -341,7 +341,7 @@ pub const UserPassSessionAuthArgs = struct { 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. /// - else: @@ -358,7 +358,7 @@ pub const UserPassSessionAuthArgs = struct { /// mechanisms described above will still kick in. For that reason: please know what /// you're doing. /// -/// See UserPassSessionAuthArgs: +/// See UserPassSessionArgs: /// - username & password param names 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 @@ -376,11 +376,11 @@ pub const UserPassSessionAuthArgs = struct { /// -> 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 /// -> 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 { allocator: std.mem.Allocator, lookup: *Lookup, - settings: UserPassSessionAuthArgs, + settings: UserPassSessionArgs, // TODO: cookie store per user? sessionTokens: SessionTokenMap, @@ -398,7 +398,7 @@ pub fn UserPassSessionAuth(comptime Lookup: type, comptime lockedPwLookups: bool pub fn init( allocator: std.mem.Allocator, lookup: *Lookup, - args: UserPassSessionAuthArgs, + args: UserPassSessionArgs, ) !Self { var ret: Self = .{ .allocator = allocator, @@ -464,7 +464,7 @@ pub fn UserPassSessionAuth(comptime Lookup: type, comptime lockedPwLookups: bool } } } 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 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 }; @@ -503,7 +503,7 @@ pub fn UserPassSessionAuth(comptime Lookup: type, comptime lockedPwLookups: bool } } } 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 @@ -548,12 +548,12 @@ pub fn UserPassSessionAuth(comptime Lookup: type, comptime lockedPwLookups: bool } } } else |err| { - zap.debug("getParamSt() for password failed in UserPassSessionAuth: {any}", .{err}); + zap.debug("getParamSt() for password failed in UserPassSession: {any}", .{err}); return .AuthFailed; } } } 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; @@ -575,7 +575,7 @@ pub fn UserPassSessionAuth(comptime Lookup: type, comptime lockedPwLookups: bool // we need to redirect and return .Handled self.redirect(r) catch |err| { // we just give up - zap.debug("redirect() failed in UserPassSessionAuth: {any}", .{err}); + zap.debug("redirect() failed in UserPassSession: {any}", .{err}); }; return .Handled; }, diff --git a/src/mustache.zig b/src/mustache.zig index 94acfc6..4b00338 100644 --- a/src/mustache.zig +++ b/src/mustache.zig @@ -2,252 +2,249 @@ const std = @import("std"); const fio = @import("fio.zig"); const util = @import("util.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 = struct { - const Self = @This(); - - const struct_mustache_s = opaque {}; - const mustache_s = struct_mustache_s; - const enum_mustache_error_en = c_uint; - const mustache_error_en = enum_mustache_error_en; - - 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_build2(dest: fio.FIOBJ, mustache: ?*mustache_s, data: fio.FIOBJ) fio.FIOBJ; - extern fn fiobj_mustache_free(mustache: ?*mustache_s) void; - - /// Load arguments used when creating a new Mustache instance. - pub const MustacheLoadArgs = struct { - /// Filename. This enables partial templates on filesystem. - filename: ?[]const u8 = null, - - /// String data. Should be used if no filename is specified. - data: ?[]const u8 = null, - }; +const Self = @This(); - /// 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, - }; +const struct_mustache_s = opaque {}; +const mustache_s = struct_mustache_s; +const enum_mustache_error_en = c_uint; +const mustache_error_en = enum_mustache_error_en; - handler: *mustache_s, - - pub const Status = enum(c_int) {}; - 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, - }; +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_build2(dest: fio.FIOBJ, mustache: ?*mustache_s, data: fio.FIOBJ) fio.FIOBJ; +extern fn fiobj_mustache_free(mustache: ?*mustache_s) void; - /// 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; +/// Load arguments used when creating a new Mustache instance. +pub const MustacheLoadArgs = struct { + /// Filename. This enables partial templates on filesystem. + filename: ?[]const u8 = null, - 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, - }; + /// String data. Should be used if no filename is specified. + data: ?[]const u8 = null, +}; - const ret = fiobj_mustache_new(args); - switch (err) { - 0 => return Self{ - .handler = ret.?, - }, - 1 => return Error.MUSTACHE_ERR_TOO_DEEP, - 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; - } +/// 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, +}; - /// 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!Mustache { - return Self.init(.{ .data = data }); - } +/// 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, +}; - /// 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 }); - } +/// 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, + }; - /// Free the data backing a `Mustache` instance. - pub fn deinit(self: *Self) void { - fiobj_mustache_free(self.handler); + const ret = fiobj_mustache_new(args); + switch (err) { + 0 => return Self{ + .handle = ret.?, + }, + 1 => return Error.MUSTACHE_ERR_TOO_DEEP, + 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; +} + +/// 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 }); +} + +/// 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!Self { + return Self.init(.{ .filename = filename }); +} + +/// Free the data backing a `Mustache` instance. +pub fn deinit(self: *Self) void { + fiobj_mustache_free(self.handle); +} + +// 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); } - // 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); - } - }; + /// 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; +/// 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) ++ "'"); } - 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)); - } + 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.*); - }, + } 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)); }, - // 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 => { + // TODO: avoid loops? + return fiobjectify(value.*); }, - else => @compileError("Unable to fiobjectify type '" ++ @typeName(T) ++ "'"), }, - .Array => return fiobjectify(&value), - .Vector => |info| { - const array: [info.len]info.child = value; - return fiobjectify(&array); + // 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) ++ "'"), - } - unreachable; + }, + .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; +} diff --git a/src/request.zig b/src/request.zig new file mode 100644 index 0000000..c6b4fb7 --- /dev/null +++ b/src/request.zig @@ -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 = ¶ms, + .allocator = a, + .always_alloc = always_alloc, + }; + const howmany = fio.fiobj_each1(self.h.*.cookies, 0, _each_nextParamStr, &context); + if (howmany != self.getCookiesCount()) { + return error.HttpIterParams; + } + return .{ .items = try params.toOwnedSlice(), .allocator = a }; +} + +/// Same as parametersToOwnedList() but for cookies +pub fn cookiesToOwnedList(self: *const Self, a: std.mem.Allocator, dupe_strings: bool) !HttpParamKVList { + var params = try std.ArrayList(HttpParamKV).initCapacity(a, @as(usize, @intCast(self.getCookiesCount()))); + var context: _parametersToOwnedSliceContext = .{ .params = ¶ms, .allocator = a, .dupe_strings = dupe_strings }; + const howmany = fio.fiobj_each1(self.h.*.cookies, 0, _each_nextParam, &context); + if (howmany != self.getCookiesCount()) { + return error.HttpIterParams; + } + return .{ .items = try params.toOwnedSlice(), .allocator = a }; +} + +/// Returns the query / body parameters as key/value pairs, as strings. +/// Supported param types that will be converted: +/// +/// - 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 = ¶ms, + .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 = ¶ms, .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); +} diff --git a/src/tests/test_auth.zig b/src/tests/test_auth.zig index d90b1fe..5e5f262 100644 --- a/src/tests/test_auth.zig +++ b/src/tests/test_auth.zig @@ -1,9 +1,8 @@ const std = @import("std"); const zap = @import("zap"); // const Authenticators = @import("http_auth.zig"); -const Authenticators = zap; -const Endpoints = zap; -// const Endpoints = @import("endpoint.zig"); +const Authenticators = zap.Auth; +const Endpoint = zap.Endpoint; const fio = zap; // const fio = @import("fio.zig"); const util = zap; @@ -13,7 +12,7 @@ test "BearerAuthSingle authenticate" { const a = std.testing.allocator; 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(); // invalid auth header @@ -33,7 +32,7 @@ test "BearerAuthMulti authenticate" { 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(); // invalid auth header @@ -54,7 +53,7 @@ test "BasicAuth Token68" { try set.put(token, {}); // create authenticator - const Authenticator = Authenticators.BasicAuth(Set, .Token68); + const Authenticator = Authenticators.Basic(Set, .Token68); var auth = try Authenticator.init(a, &set, null); defer auth.deinit(); @@ -85,7 +84,7 @@ test "BasicAuth UserPass" { const encoded = encoder.encode(&buffer, token); // create authenticator - const Authenticator = Authenticators.BasicAuth(Map, .UserPass); + const Authenticator = Authenticators.Basic(Map, .UserPass); var auth = try Authenticator.init(a, &map, null); defer auth.deinit(); @@ -106,7 +105,7 @@ const HTTP_RESPONSE: []const u8 = ; 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; r.sendBody(HTTP_RESPONSE) catch return; received_response = HTTP_RESPONSE; @@ -114,7 +113,7 @@ fn endpoint_http_get(e: *Endpoints.Endpoint, r: zap.Request) void { zap.stop(); } -fn endpoint_http_unauthorized(e: *Endpoints.Endpoint, r: zap.Request) void { +fn endpoint_http_unauthorized(e: *Endpoint, r: zap.Request) void { _ = e; r.setStatus(.unauthorized); r.sendBody("UNAUTHORIZED ACCESS") catch return; @@ -181,7 +180,7 @@ test "BearerAuthSingle authenticateRequest OK" { const token = "ABCDEFG"; // setup listener - var listener = zap.EndpointListener.init( + var listener = zap.Endpoint.Listener.init( a, .{ .port = 3000, @@ -194,19 +193,19 @@ test "BearerAuthSingle authenticateRequest OK" { defer listener.deinit(); // create mini endpoint - var ep = Endpoints.Endpoint.init(.{ + var ep = Endpoint.init(.{ .path = "/test", .get = endpoint_http_get, .unauthorized = endpoint_http_unauthorized, }); // create authenticator - const Authenticator = Authenticators.BearerAuthSingle; + const Authenticator = Authenticators.BearerSingle; var authenticator = try Authenticator.init(a, token, null); defer authenticator.deinit(); // create authenticating endpoint - const BearerAuthEndpoint = Endpoints.AuthenticatingEndpoint(Authenticator); + const BearerAuthEndpoint = Endpoint.Authenticating(Authenticator); var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); try listener.register(auth_ep.endpoint()); @@ -234,7 +233,7 @@ test "BearerAuthSingle authenticateRequest test-unauthorized" { const token = "ABCDEFG"; // setup listener - var listener = zap.EndpointListener.init( + var listener = zap.Endpoint.Listener.init( a, .{ .port = 3000, @@ -247,7 +246,7 @@ test "BearerAuthSingle authenticateRequest test-unauthorized" { defer listener.deinit(); // create mini endpoint - var ep = Endpoints.Endpoint.init(.{ + var ep = Endpoint.init(.{ .path = "/test", .get = endpoint_http_get, .unauthorized = endpoint_http_unauthorized, @@ -260,12 +259,12 @@ test "BearerAuthSingle authenticateRequest test-unauthorized" { // insert auth tokens try set.put(token, {}); - const Authenticator = Authenticators.BearerAuthMulti(Set); + const Authenticator = Authenticators.BearerMulti(Set); var authenticator = try Authenticator.init(a, &set, null); defer authenticator.deinit(); // create authenticating endpoint - const BearerAuthEndpoint = Endpoints.AuthenticatingEndpoint(Authenticator); + const BearerAuthEndpoint = Endpoint.Authenticating(Authenticator); var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); try listener.register(auth_ep.endpoint()); @@ -291,7 +290,7 @@ test "BearerAuthMulti authenticateRequest OK" { const token = "ABCDEFG"; // setup listener - var listener = zap.EndpointListener.init( + var listener = zap.Endpoint.Listener.init( a, .{ .port = 3000, @@ -304,19 +303,19 @@ test "BearerAuthMulti authenticateRequest OK" { defer listener.deinit(); // create mini endpoint - var ep = Endpoints.Endpoint.init(.{ + var ep = Endpoint.init(.{ .path = "/test", .get = endpoint_http_get, .unauthorized = endpoint_http_unauthorized, }); // create authenticator - const Authenticator = Authenticators.BearerAuthSingle; + const Authenticator = Authenticators.BearerSingle; var authenticator = try Authenticator.init(a, token, null); defer authenticator.deinit(); // create authenticating endpoint - const BearerAuthEndpoint = Endpoints.AuthenticatingEndpoint(Authenticator); + const BearerAuthEndpoint = Endpoint.Authenticating(Authenticator); var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); try listener.register(auth_ep.endpoint()); @@ -342,7 +341,7 @@ test "BearerAuthMulti authenticateRequest test-unauthorized" { const token = "invalid"; // setup listener - var listener = zap.EndpointListener.init( + var listener = zap.Endpoint.Listener.init( a, .{ .port = 3000, @@ -355,19 +354,19 @@ test "BearerAuthMulti authenticateRequest test-unauthorized" { defer listener.deinit(); // create mini endpoint - var ep = Endpoints.Endpoint.init(.{ + var ep = Endpoint.init(.{ .path = "/test", .get = endpoint_http_get, .unauthorized = endpoint_http_unauthorized, }); // create authenticator - const Authenticator = Authenticators.BearerAuthSingle; + const Authenticator = Authenticators.BearerSingle; var authenticator = try Authenticator.init(a, token, null); defer authenticator.deinit(); // create authenticating endpoint - const BearerAuthEndpoint = Endpoints.AuthenticatingEndpoint(Authenticator); + const BearerAuthEndpoint = Endpoint.Authenticating(Authenticator); var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); try listener.register(auth_ep.endpoint()); @@ -393,7 +392,7 @@ test "BasicAuth Token68 authenticateRequest" { const token = "QWxhZGRpbjpvcGVuIHNlc2FtZQ=="; // setup listener - var listener = zap.EndpointListener.init( + var listener = zap.Endpoint.Listener.init( a, .{ .port = 3000, @@ -406,7 +405,7 @@ test "BasicAuth Token68 authenticateRequest" { defer listener.deinit(); // create mini endpoint - var ep = Endpoints.Endpoint.init(.{ + var ep = Endpoint.init(.{ .path = "/test", .get = endpoint_http_get, .unauthorized = endpoint_http_unauthorized, @@ -418,12 +417,12 @@ test "BasicAuth Token68 authenticateRequest" { try set.put(token, {}); // create authenticator - const Authenticator = Authenticators.BasicAuth(Set, .Token68); + const Authenticator = Authenticators.Basic(Set, .Token68); var authenticator = try Authenticator.init(a, &set, null); defer authenticator.deinit(); // create authenticating endpoint - const BearerAuthEndpoint = Endpoints.AuthenticatingEndpoint(Authenticator); + const BearerAuthEndpoint = Endpoint.Authenticating(Authenticator); var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); try listener.register(auth_ep.endpoint()); @@ -449,7 +448,7 @@ test "BasicAuth Token68 authenticateRequest test-unauthorized" { const token = "QWxhZGRpbjpvcGVuIHNlc2FtZQ=="; // setup listener - var listener = zap.EndpointListener.init( + var listener = zap.Endpoint.Listener.init( a, .{ .port = 3000, @@ -462,7 +461,7 @@ test "BasicAuth Token68 authenticateRequest test-unauthorized" { defer listener.deinit(); // create mini endpoint - var ep = Endpoints.Endpoint.init(.{ + var ep = Endpoint.init(.{ .path = "/test", .get = endpoint_http_get, .unauthorized = endpoint_http_unauthorized, @@ -474,12 +473,12 @@ test "BasicAuth Token68 authenticateRequest test-unauthorized" { try set.put(token, {}); // create authenticator - const Authenticator = Authenticators.BasicAuth(Set, .Token68); + const Authenticator = Authenticators.Basic(Set, .Token68); var authenticator = try Authenticator.init(a, &set, null); defer authenticator.deinit(); // create authenticating endpoint - const BearerAuthEndpoint = Endpoints.AuthenticatingEndpoint(Authenticator); + const BearerAuthEndpoint = Endpoint.Authenticating(Authenticator); var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); try listener.register(auth_ep.endpoint()); @@ -504,7 +503,7 @@ test "BasicAuth UserPass authenticateRequest" { const a = std.testing.allocator; // setup listener - var listener = zap.EndpointListener.init( + var listener = zap.Endpoint.Listener.init( a, .{ .port = 3000, @@ -517,7 +516,7 @@ test "BasicAuth UserPass authenticateRequest" { defer listener.deinit(); // create mini endpoint - var ep = Endpoints.Endpoint.init(.{ + var ep = Endpoint.init(.{ .path = "/test", .get = endpoint_http_get, .unauthorized = endpoint_http_unauthorized, @@ -540,12 +539,12 @@ test "BasicAuth UserPass authenticateRequest" { const encoded = encoder.encode(&buffer, token); // create authenticator - const Authenticator = Authenticators.BasicAuth(Map, .UserPass); + const Authenticator = Authenticators.Basic(Map, .UserPass); var authenticator = try Authenticator.init(a, &map, null); defer authenticator.deinit(); // create authenticating endpoint - const BearerAuthEndpoint = Endpoints.AuthenticatingEndpoint(Authenticator); + const BearerAuthEndpoint = Endpoint.Authenticating(Authenticator); var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); try listener.register(auth_ep.endpoint()); @@ -570,7 +569,7 @@ test "BasicAuth UserPass authenticateRequest test-unauthorized" { const a = std.testing.allocator; // setup listener - var listener = zap.EndpointListener.init( + var listener = zap.Endpoint.Listener.init( a, .{ .port = 3000, @@ -583,7 +582,7 @@ test "BasicAuth UserPass authenticateRequest test-unauthorized" { defer listener.deinit(); // create mini endpoint - var ep = Endpoints.Endpoint.init(.{ + var ep = Endpoint.init(.{ .path = "/test", .get = endpoint_http_get, .unauthorized = endpoint_http_unauthorized, @@ -607,12 +606,12 @@ test "BasicAuth UserPass authenticateRequest test-unauthorized" { _ = encoded; // create authenticator - const Authenticator = Authenticators.BasicAuth(Map, .UserPass); + const Authenticator = Authenticators.Basic(Map, .UserPass); var authenticator = try Authenticator.init(a, &map, null); defer authenticator.deinit(); // create authenticating endpoint - const BearerAuthEndpoint = Endpoints.AuthenticatingEndpoint(Authenticator); + const BearerAuthEndpoint = Endpoint.Authenticating(Authenticator); var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); try listener.register(auth_ep.endpoint()); diff --git a/src/tests/test_http_params.zig b/src/tests/test_http_params.zig index c39343f..c5e3387 100644 --- a/src/tests/test_http_params.zig +++ b/src/tests/test_http_params.zig @@ -30,8 +30,8 @@ test "http parameters" { var ran: bool = false; var param_count: isize = 0; - var strParams: ?zap.HttpParamStrKVList = null; - var params: ?zap.HttpParamKVList = null; + var strParams: ?zap.Request.HttpParamStrKVList = null; + var params: ?zap.Request.HttpParamKVList = null; var paramOneStr: ?zap.FreeOrNot = null; pub fn on_request(r: zap.Request) void { diff --git a/src/zap.zig b/src/zap.zig index c258ff4..35f7bf2 100644 --- a/src/zap.zig +++ b/src/zap.zig @@ -9,12 +9,63 @@ pub const fio = @import("fio.zig"); /// Server-Side TLS function wrapper pub const Tls = @import("tls.zig"); -// pub usingnamespace @import("fio.zig"); -pub usingnamespace @import("endpoint.zig"); +/// Endpoint and supporting types. +/// 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("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. /// Contains a special Listener and a Handler struct that support chaining @@ -93,721 +144,6 @@ pub const ContentType = enum { // 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 = ¶ms, - .allocator = a, - .always_alloc = always_alloc, - }; - const howmany = fio.fiobj_each1(self.h.*.cookies, 0, _each_nextParamStr, &context); - if (howmany != self.getCookiesCount()) { - return error.HttpIterParams; - } - return .{ .items = try params.toOwnedSlice(), .allocator = a }; - } - - /// Same as parametersToOwnedList() but for cookies - pub fn cookiesToOwnedList(self: *const Self, a: std.mem.Allocator, dupe_strings: bool) !HttpParamKVList { - var params = try std.ArrayList(HttpParamKV).initCapacity(a, @as(usize, @intCast(self.getCookiesCount()))); - var context: _parametersToOwnedSliceContext = .{ .params = ¶ms, .allocator = a, .dupe_strings = dupe_strings }; - const howmany = fio.fiobj_each1(self.h.*.cookies, 0, _each_nextParam, &context); - if (howmany != self.getCookiesCount()) { - return error.HttpIterParams; - } - return .{ .items = try params.toOwnedSlice(), .allocator = a }; - } - - /// Returns the query / body parameters as key/value pairs, as strings. - /// Supported param types that will be converted: - /// - /// - 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 = ¶ms, - .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 = ¶ms, .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 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, ._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; var user_context: Request.UserContext = .{}; @@ -949,7 +285,7 @@ pub const HttpListener = struct { 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_upgrade = if (self.settings.on_upgrade) |_| Self.theOneAndOnlyUpgradeCallBack else null, .on_response = if (self.settings.on_response) |_| Self.theOneAndOnlyResponseCallBack else null,