diff --git a/build.zig b/build.zig index bc08984..d8a260f 100644 --- a/build.zig +++ b/build.zig @@ -70,6 +70,7 @@ pub fn build(b: *std.Build) !void { .{ .name = "middleware_with_endpoint", .src = "examples/middleware_with_endpoint/middleware_with_endpoint.zig" }, .{ .name = "senderror", .src = "examples/senderror/senderror.zig" }, .{ .name = "bindataformpost", .src = "examples/bindataformpost/bindataformpost.zig" }, + .{ .name = "accept", .src = "examples/accept/accept.zig" }, }) |excfg| { const ex_name = excfg.name; const ex_src = excfg.src; diff --git a/examples/accept/accept.zig b/examples/accept/accept.zig new file mode 100644 index 0000000..477e973 --- /dev/null +++ b/examples/accept/accept.zig @@ -0,0 +1,76 @@ +const std = @import("std"); +const zap = @import("zap"); + +var gpa = std.heap.GeneralPurposeAllocator(.{ + .thread_safe = true, +}){}; + +fn on_request_verbose(r: zap.Request) void { + // use a local buffer for the parsed accept headers + var accept_buffer: [1024]u8 = undefined; + var fba = std.heap.FixedBufferAllocator.init(&accept_buffer); + const accept_allocator = fba.allocator(); + + const content_type: zap.ContentType = content_type: { + var accept_list = r.parseAcceptHeaders(accept_allocator) catch break :content_type .HTML; + defer accept_list.deinit(); + + for (accept_list.items) |accept| { + break :content_type accept.toContentType() orelse continue; + } + break :content_type .HTML; + }; + + r.setContentType(content_type) catch return; + switch (content_type) { + .TEXT => { + r.sendBody("Hello from ZAP!!!") catch return; + }, + .HTML => { + r.sendBody("

Hello from ZAP!!!

") catch return; + }, + .XML => { + r.sendBody( + \\ + \\ + \\ + \\ Hello from ZAP!!! + \\ + \\ + ) catch return; + }, + .JSON => { + var buffer: [128]u8 = undefined; + const json = zap.stringifyBuf(&buffer, .{ .message = "Hello from ZAP!!!" }, .{}) orelse return; + r.sendJson(json) catch return; + }, + .XHTML => { + r.sendBody( + \\ + \\ + \\ + \\

Hello from ZAP!!!

+ \\ + \\ + ) catch return; + }, + } +} + +pub fn main() !void { + var listener = zap.HttpListener.init(.{ + .port = 3000, + .on_request = on_request_verbose, + .log = true, + .max_clients = 100000, + }); + try listener.listen(); + + std.debug.print("Listening on 0.0.0.0:3000\n", .{}); + + // start worker threads + zap.start(.{ + .threads = 2, + .workers = 2, + }); +} diff --git a/src/http.zig b/src/http.zig index 8c25558..6c2ef45 100644 --- a/src/http.zig +++ b/src/http.zig @@ -1,5 +1,4 @@ const std = @import("std"); -const fio = @import("fio.zig"); /// HTTP Status codes according to `rfc9110` /// https://datatracker.ietf.org/doc/html/rfc9110#name-status-codes @@ -149,20 +148,3 @@ pub fn methodToEnum(method: ?[]const u8) Method { return Method.UNKNOWN; } } - -/// Registers a new mimetype to be used for files ending with the given extension. -pub fn mimetypeRegister(file_extension: []const u8, mime_type_str: []const u8) void { - // NOTE: facil.io is expecting a non-const pointer to u8 values, but it does not - // not appear to actually modify the value. Here we do a const cast so - // that it is easy to pass static strings to http_mimetype_register without - // needing to allocate a buffer on the heap. - const extension = @constCast(file_extension); - const mimetype = fio.fiobj_str_new(mime_type_str.ptr, mime_type_str.len); - - fio.http_mimetype_register(extension.ptr, extension.len, mimetype); -} - -/// Clears the Mime-Type registry (it will be empty after this call). -pub fn mimetypeClear() void { - fio.http_mimetype_clear(); -} diff --git a/src/request.zig b/src/request.zig index 24a02f5..a209531 100644 --- a/src/request.zig +++ b/src/request.zig @@ -6,6 +6,8 @@ const fio = @import("fio.zig"); const util = @import("util.zig"); const zap = @import("zap.zig"); +const ContentType = zap.ContentType; + pub const HttpError = error{ HttpSendBody, HttpSetContentType, @@ -16,15 +18,6 @@ pub const HttpError = error{ 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, @@ -584,6 +577,78 @@ pub fn parseCookies(self: *const Self, url_encoded: bool) void { fio.http_parse_cookies(self.h, if (url_encoded) 1 else 0); } +pub const AcceptItem = struct { + raw: []const u8, + type: Fragment, + subtype: Fragment, + q: f64, + + const Fragment = union(enum) { + glob, + value: []const u8, + }; + + pub fn lessThan(_: void, lhs: AcceptItem, rhs: AcceptItem) bool { + return lhs.q < rhs.q; + } + + pub fn toContentType(item: AcceptItem) ?ContentType { + if (ContentType.string_map.get(item.raw)) |common| { + return common; + } + return null; + } +}; + +/// List holding access headers parsed by parseAcceptHeaders() +const AcceptHeaderList = std.ArrayList(AcceptItem); + +/// Parses `Accept:` http header into `list`, ordered from highest q factor to lowest +pub fn parseAcceptHeaders(self: *const Self, allocator: std.mem.Allocator) !AcceptHeaderList { + const accept_str = self.getHeaderCommon(.accept) orelse return error.NoAcceptHeader; + + const comma_count = std.mem.count(u8, accept_str, ","); + + var list = try AcceptHeaderList.initCapacity(allocator, comma_count + 1); + errdefer list.deinit(); + + var tok_iter = std.mem.tokenize(u8, accept_str, ", "); + while (tok_iter.next()) |tok| { + var split_iter = std.mem.split(u8, tok, ";"); + const mimetype_str = split_iter.next().?; + const q_factor = q_factor: { + const q_factor_str = split_iter.next() orelse break :q_factor 1; + var eq_iter = std.mem.split(u8, q_factor_str, "="); + const q = eq_iter.next().?; + if (q[0] != 'q') break :q_factor 1; + const value = eq_iter.next() orelse break :q_factor 1; + const parsed = std.fmt.parseFloat(f64, value) catch break :q_factor 1; + break :q_factor parsed; + }; + + var type_split_iter = std.mem.split(u8, mimetype_str, "/"); + + const mimetype_type_str = type_split_iter.next() orelse continue; + const mimetype_subtype_str = type_split_iter.next() orelse continue; + + const new_item: AcceptItem = .{ + .raw = mimetype_str, + .type = if (std.mem.eql(u8, "*", mimetype_type_str)) .glob else .{ .value = mimetype_type_str }, + .subtype = if (std.mem.eql(u8, "*", mimetype_subtype_str)) .glob else .{ .value = mimetype_subtype_str }, + .q = q_factor, + }; + for (list.items, 1..) |item, i| { + if (AcceptItem.lessThan({}, new_item, item)) { + list.insertAssumeCapacity(i, new_item); + break; + } + } else { + list.appendAssumeCapacity(new_item); + } + } + return list; +} + /// Set a response cookie pub fn setCookie(self: *const Self, args: CookieArgs) HttpError!void { const c: fio.http_cookie_args_s = .{ diff --git a/src/zap.zig b/src/zap.zig index 988e833..a766e70 100644 --- a/src/zap.zig +++ b/src/zap.zig @@ -122,6 +122,23 @@ pub fn startWithLogging(args: fio.fio_start_args) void { fio.fio_start(args); } +/// Registers a new mimetype to be used for files ending with the given extension. +pub fn mimetypeRegister(file_extension: []const u8, mime_type_str: []const u8) void { + // NOTE: facil.io is expecting a non-const pointer to u8 values, but it does not + // not appear to actually modify the value. Here we do a const cast so + // that it is easy to pass static strings to http_mimetype_register without + // needing to allocate a buffer on the heap. + const extension = @constCast(file_extension); + const mimetype = fio.fiobj_str_new(mime_type_str.ptr, mime_type_str.len); + + fio.http_mimetype_register(extension.ptr, extension.len, mimetype); +} + +/// Clears the Mime-Type registry (it will be empty after this call). +pub fn mimetypeClear() void { + fio.http_mimetype_clear(); +} + pub const ListenError = error{ AlreadyListening, ListenError, @@ -142,8 +159,18 @@ pub const HttpError = error{ pub const ContentType = enum { TEXT, HTML, + XML, JSON, + XHTML, // TODO: more content types + + pub const string_map = std.ComptimeStringMap(ContentType, .{ + .{ "text/plain", .TEXT }, + .{ "text/html", .HTML }, + .{ "application/xml", .XML }, + .{ "application/json", .JSON }, + .{ "application/xhtml+xml", .XHTML }, + }); }; /// Used internally: facilio Http request callback function type