mirror of
https://github.com/zigzap/zap.git
synced 2025-10-20 15:14:08 +00:00
1049 lines
38 KiB
Zig
1049 lines
38 KiB
Zig
// zig type definitions for facilio lib
|
|
// or maybe let's just make it zap directly...
|
|
|
|
const std = @import("std");
|
|
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");
|
|
pub usingnamespace @import("util.zig");
|
|
pub usingnamespace @import("http.zig");
|
|
pub usingnamespace @import("mustache.zig");
|
|
pub usingnamespace @import("http_auth.zig");
|
|
|
|
/// Middleware support.
|
|
/// Contains a special Listener and a Handler struct that support chaining
|
|
/// requests handlers, with an optional stop once a handler indicates it
|
|
/// processed the request. Also sports an EndpointHandler for using regular zap
|
|
/// Endpoints as Handlers.
|
|
pub const Middleware = @import("middleware.zig");
|
|
|
|
/// Websocket API
|
|
pub const WebSockets = @import("websockets.zig");
|
|
|
|
pub const Log = @import("log.zig");
|
|
const http = @import("http.zig");
|
|
|
|
const util = @import("util.zig");
|
|
|
|
// TODO: replace with comptime debug logger like in log.zig
|
|
var _debug: bool = false;
|
|
|
|
/// Start the IO reactor
|
|
///
|
|
/// Will start listeners etc.
|
|
pub fn start(args: fio.fio_start_args) void {
|
|
fio.fio_start(args);
|
|
}
|
|
|
|
/// Stop ZAP:
|
|
///
|
|
/// 1. Stop accepting further incoming requests
|
|
/// 2. Wait for all running request handlers to return
|
|
/// 3. return from `zap.start(...)`
|
|
pub fn stop() void {
|
|
fio.fio_stop();
|
|
}
|
|
|
|
/// Extremely simplistic zap debug function.
|
|
/// TODO: re-wwrite logging or use std.log
|
|
pub fn debug(comptime fmt: []const u8, args: anytype) void {
|
|
if (_debug) {
|
|
std.debug.print("[zap] - " ++ fmt, args);
|
|
}
|
|
}
|
|
|
|
/// Enable zap debug logging
|
|
pub fn enableDebugLog() void {
|
|
_debug = true;
|
|
}
|
|
|
|
/// start Zap with debug logging on
|
|
pub fn startWithLogging(args: fio.fio_start_args) void {
|
|
debug = true;
|
|
fio.fio_start(args);
|
|
}
|
|
|
|
pub const ListenError = error{
|
|
AlreadyListening,
|
|
ListenError,
|
|
};
|
|
|
|
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
|
|
};
|
|
|
|
/// 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});
|
|
|
|
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);
|
|
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, 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;
|
|
}
|
|
|
|
/// 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 {
|
|
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);
|
|
}
|
|
};
|
|
|
|
/// 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;
|
|
|
|
/// Zap Http request callback function type.
|
|
pub const HttpRequestFn = *const fn (Request) void;
|
|
|
|
/// websocket connection upgrade callback type
|
|
/// fn(request, targetstring)
|
|
pub const HttpUpgradeFn = *const fn (r: Request, target_protocol: []const u8) void;
|
|
|
|
/// http finish, called when zap finishes. You get your udata back in the
|
|
/// HttpFinishSetting struct.
|
|
pub const HttpFinishSettings = [*c]fio.struct_http_settings_s;
|
|
|
|
/// Http finish callback type
|
|
pub const HttpFinishFn = *const fn (HttpFinishSettings) void;
|
|
|
|
/// Listener settings
|
|
pub const HttpListenerSettings = struct {
|
|
port: usize,
|
|
interface: [*c]const u8 = null,
|
|
on_request: ?HttpRequestFn,
|
|
on_response: ?HttpRequestFn = null,
|
|
on_upgrade: ?HttpUpgradeFn = null,
|
|
on_finish: ?HttpFinishFn = null,
|
|
// provide any pointer in there for "user data". it will be passed pack in
|
|
// on_finish()'s copy of the struct_http_settings_s
|
|
udata: ?*anyopaque = null,
|
|
public_folder: ?[]const u8 = null,
|
|
max_clients: ?isize = null,
|
|
max_body_size: ?usize = null,
|
|
timeout: ?u8 = null,
|
|
log: bool = false,
|
|
ws_timeout: u8 = 40,
|
|
ws_max_msg_size: usize = 262144,
|
|
tls: ?Tls = null,
|
|
};
|
|
|
|
/// Http listener
|
|
pub const HttpListener = struct {
|
|
settings: HttpListenerSettings,
|
|
|
|
const Self = @This();
|
|
var the_one_and_only_listener: ?*HttpListener = null;
|
|
|
|
/// Create a listener
|
|
pub fn init(settings: HttpListenerSettings) Self {
|
|
std.debug.assert(settings.on_request != null);
|
|
return .{
|
|
.settings = settings,
|
|
};
|
|
}
|
|
|
|
// we could make it dynamic by passing a HttpListener via udata
|
|
/// Used internally: the listener's facilio request callback
|
|
pub fn theOneAndOnlyRequestCallBack(r: [*c]fio.http_s) callconv(.C) void {
|
|
if (the_one_and_only_listener) |l| {
|
|
var req: Request = .{
|
|
.path = util.fio2str(r.*.path),
|
|
.query = util.fio2str(r.*.query),
|
|
.body = util.fio2str(r.*.body),
|
|
.method = util.fio2str(r.*.method),
|
|
.h = r,
|
|
._is_finished_request_global = false,
|
|
._user_context = undefined,
|
|
};
|
|
req._is_finished = &req._is_finished_request_global;
|
|
|
|
var user_context: Request.UserContext = .{};
|
|
req._user_context = &user_context;
|
|
|
|
req.markAsFinished(false);
|
|
std.debug.assert(l.settings.on_request != null);
|
|
if (l.settings.on_request) |on_request| {
|
|
// l.settings.on_request.?(req);
|
|
on_request(req);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Used internally: the listener's facilio response callback
|
|
pub fn theOneAndOnlyResponseCallBack(r: [*c]fio.http_s) callconv(.C) void {
|
|
if (the_one_and_only_listener) |l| {
|
|
var req: Request = .{
|
|
.path = util.fio2str(r.*.path),
|
|
.query = util.fio2str(r.*.query),
|
|
.body = util.fio2str(r.*.body),
|
|
.method = util.fio2str(r.*.method),
|
|
.h = r,
|
|
._is_finished_request_global = false,
|
|
._user_context = undefined,
|
|
};
|
|
req._is_finished = &req._is_finished_request_global;
|
|
|
|
var user_context: Request.UserContext = .{};
|
|
req._user_context = &user_context;
|
|
|
|
l.settings.on_response.?(req);
|
|
}
|
|
}
|
|
|
|
/// Used internally: the listener's facilio upgrade callback
|
|
pub fn theOneAndOnlyUpgradeCallBack(r: [*c]fio.http_s, target: [*c]u8, target_len: usize) callconv(.C) void {
|
|
if (the_one_and_only_listener) |l| {
|
|
var req: Request = .{
|
|
.path = util.fio2str(r.*.path),
|
|
.query = util.fio2str(r.*.query),
|
|
.body = util.fio2str(r.*.body),
|
|
.method = util.fio2str(r.*.method),
|
|
.h = r,
|
|
._is_finished_request_global = false,
|
|
._user_context = undefined,
|
|
};
|
|
const zigtarget: []u8 = target[0..target_len];
|
|
req._is_finished = &req._is_finished_request_global;
|
|
|
|
var user_context: Request.UserContext = .{};
|
|
req._user_context = &user_context;
|
|
|
|
l.settings.on_upgrade.?(req, zigtarget);
|
|
}
|
|
}
|
|
|
|
/// Used internally: the listener's facilio finish callback
|
|
pub fn theOneAndOnlyFinishCallBack(s: [*c]fio.struct_http_settings_s) callconv(.C) void {
|
|
if (the_one_and_only_listener) |l| {
|
|
l.settings.on_finish.?(s);
|
|
}
|
|
}
|
|
|
|
/// Start listening
|
|
pub fn listen(self: *Self) !void {
|
|
var pfolder: [*c]const u8 = null;
|
|
var pfolder_len: usize = 0;
|
|
|
|
if (self.settings.public_folder) |pf| {
|
|
debug("HttpListener.listen(): public folder is {s}\n", .{pf});
|
|
pfolder_len = pf.len;
|
|
pfolder = pf.ptr;
|
|
}
|
|
|
|
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,
|
|
.on_finish = if (self.settings.on_finish) |_| Self.theOneAndOnlyFinishCallBack else null,
|
|
.udata = null,
|
|
.public_folder = pfolder,
|
|
.public_folder_length = pfolder_len,
|
|
.max_header_size = 32 * 1024,
|
|
.max_body_size = self.settings.max_body_size orelse 50 * 1024 * 1024,
|
|
// fio provides good default:
|
|
.max_clients = self.settings.max_clients orelse 0,
|
|
.tls = if (self.settings.tls) |tls| tls.fio_tls else null,
|
|
.reserved1 = 0,
|
|
.reserved2 = 0,
|
|
.reserved3 = 0,
|
|
.ws_max_msg_size = 0,
|
|
.timeout = self.settings.timeout orelse 5,
|
|
.ws_timeout = self.settings.ws_timeout,
|
|
.log = if (self.settings.log) 1 else 0,
|
|
.is_client = 0,
|
|
};
|
|
// TODO: BUG: without this print/sleep statement, -Drelease* loop forever
|
|
// in debug2 and debug3 of hello example
|
|
// std.debug.print("X\n", .{});
|
|
// TODO: still happening?
|
|
std.time.sleep(500 * std.time.ns_per_ms);
|
|
|
|
var portbuf: [100]u8 = undefined;
|
|
const printed_port = try std.fmt.bufPrintZ(&portbuf, "{d}", .{self.settings.port});
|
|
|
|
const ret = fio.http_listen(printed_port.ptr, self.settings.interface, x);
|
|
if (ret == -1) {
|
|
return error.ListenError;
|
|
}
|
|
|
|
// set ourselves up to handle requests:
|
|
// TODO: do we mind the race condition?
|
|
// the HttpRequestFn will check if this is null and not process
|
|
// the request if it isn't set. hence, if started under full load, the
|
|
// first request(s) might not be serviced, as long as it takes from
|
|
// fio.http_listen() to here
|
|
Self.the_one_and_only_listener = self;
|
|
}
|
|
};
|
|
|
|
/// lower level listening, if you don't want to use a listener but rather use
|
|
/// the listen() function.
|
|
pub const ListenSettings = struct {
|
|
on_request: ?FioHttpRequestFn = null,
|
|
on_upgrade: ?FioHttpRequestFn = null,
|
|
on_response: ?FioHttpRequestFn = null,
|
|
on_finish: ?FioHttpRequestFn = null,
|
|
public_folder: ?[]const u8 = null,
|
|
max_header_size: usize = 32 * 1024,
|
|
max_body_size: usize = 50 * 1024 * 1024,
|
|
max_clients: isize = 100,
|
|
keepalive_timeout_s: u8 = 5,
|
|
log: bool = false,
|
|
|
|
const Self = @This();
|
|
|
|
/// Create settings with defaults
|
|
pub fn init() Self {
|
|
return .{};
|
|
}
|
|
};
|
|
|
|
/// Low level listen function
|
|
pub fn listen(port: [*c]const u8, interface: [*c]const u8, settings: ListenSettings) ListenError!void {
|
|
var pfolder: [*c]const u8 = null;
|
|
var pfolder_len: usize = 0;
|
|
|
|
if (settings.public_folder) |pf| {
|
|
pfolder_len = pf.len;
|
|
pfolder = pf.ptr;
|
|
}
|
|
const x: fio.http_settings_s = .{
|
|
.on_request = settings.on_request,
|
|
.on_upgrade = settings.on_upgrade,
|
|
.on_response = settings.on_response,
|
|
.on_finish = settings.on_finish,
|
|
.udata = null,
|
|
.public_folder = pfolder,
|
|
.public_folder_length = pfolder_len,
|
|
.max_header_size = settings.max_header_size,
|
|
.max_body_size = settings.max_body_size,
|
|
.max_clients = settings.max_clients,
|
|
.tls = null,
|
|
.reserved1 = 0,
|
|
.reserved2 = 0,
|
|
.reserved3 = 0,
|
|
.ws_max_msg_size = settings.ws_max_msg_size,
|
|
.timeout = settings.keepalive_timeout_s,
|
|
.ws_timeout = 0,
|
|
.log = if (settings.log) 1 else 0,
|
|
.is_client = 0,
|
|
};
|
|
// TODO: BUG: without this print/sleep statement, -Drelease* loop forever
|
|
// in debug2 and debug3 of hello example
|
|
// std.debug.print("X\n", .{});
|
|
// TODO: still happening?
|
|
std.time.sleep(500 * std.time.ns_per_ms);
|
|
|
|
if (fio.http_listen(port, interface, x) == -1) {
|
|
return error.ListenError;
|
|
}
|
|
}
|
|
|
|
/// lower level sendBody
|
|
pub fn sendBody(request: [*c]fio.http_s, body: []const u8) HttpError!void {
|
|
const ret = fio.http_send_body(request, @as(
|
|
*anyopaque,
|
|
@ptrFromInt(@intFromPtr(body.ptr)),
|
|
), body.len);
|
|
debug("sendBody(): ret = {}\n", .{ret});
|
|
if (ret != -1) return error.HttpSendBody;
|
|
}
|