Case insensitivity and basic AP user
This commit is contained in:
parent
5fbb1b480b
commit
266c18453a
1 changed files with 133 additions and 35 deletions
168
src/main.zig
168
src/main.zig
|
@ -2,10 +2,88 @@ const std = @import("std");
|
|||
|
||||
pub const io_mode = .evented;
|
||||
|
||||
const HeaderMap = std.StringHashMap([]const u8);
|
||||
const Reader = std.net.Stream.Reader;
|
||||
const Writer = std.net.Stream.Writer;
|
||||
|
||||
const case_insensitive_utf8 = struct {
|
||||
const Hash = std.hash.Wyhash;
|
||||
const View = std.unicode.Utf8View;
|
||||
const toLower = std.ascii.toLower;
|
||||
const isAscii = std.ascii.isASCII;
|
||||
const seed = 1;
|
||||
|
||||
pub fn hash(str: []const u8) u64 {
|
||||
// fallback to regular hash on invalid utf8
|
||||
const view = View.init(str) catch return Hash.hash(seed, str);
|
||||
var iter = view.iterator();
|
||||
|
||||
var h = Hash.init(seed);
|
||||
|
||||
var it = iter.nextCodepointSlice();
|
||||
while (it != null) : (it = iter.nextCodepointSlice()) {
|
||||
if (it.?.len == 1 and isAscii(it.?[0])) {
|
||||
const ch = [1]u8{toLower(it.?[0])};
|
||||
h.update(&ch);
|
||||
} else {
|
||||
h.update(it.?);
|
||||
}
|
||||
}
|
||||
|
||||
return h.final();
|
||||
}
|
||||
|
||||
pub fn eql(a: []const u8, b: []const u8) bool {
|
||||
if (a.len != b.len) return false;
|
||||
|
||||
const va = View.init(a) catch return std.mem.eql(u8, a, b);
|
||||
const vb = View.init(b) catch return false;
|
||||
|
||||
var iter_a = va.iterator();
|
||||
var iter_b = vb.iterator();
|
||||
|
||||
var it_a = iter_a.nextCodepointSlice();
|
||||
var it_b = iter_b.nextCodepointSlice();
|
||||
|
||||
while (it_a != null and it_b != null) : ({
|
||||
it_a = iter_a.nextCodepointSlice();
|
||||
it_b = iter_b.nextCodepointSlice();
|
||||
}) {
|
||||
if (it_a.?.len != it_b.?.len) return false;
|
||||
|
||||
if (it_a.?.len == 1) {
|
||||
if (isAscii(it_a.?[0]) and isAscii(it_b.?[0])) {
|
||||
const ch_a = toLower(it_a.?[0]);
|
||||
const ch_b = toLower(it_b.?[0]);
|
||||
|
||||
if (ch_a != ch_b) return false;
|
||||
} else if (it_a.?[0] != it_b.?[0]) return false;
|
||||
} else if (!std.mem.eql(u8, it_a.?, it_b.?)) return false;
|
||||
}
|
||||
|
||||
return it_a == null and it_b == null;
|
||||
}
|
||||
|
||||
pub fn lowerInPlace(str: []u8) void {
|
||||
const view = View.init(str) catch return;
|
||||
|
||||
var iter = view.iterator();
|
||||
var it = iter.nextCodepointSlice();
|
||||
while (it != null) : (it = iter.nextCodepointSlice()) {
|
||||
if (isAscii(it.?[0])) it.?[0] = toLower(it.?[0]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const HeaderMap = std.HashMap([]const u8, []const u8, struct {
|
||||
pub fn eql(_: @This(), a: []const u8, b: []const u8) bool {
|
||||
return case_insensitive_utf8.eql(a, b);
|
||||
}
|
||||
|
||||
pub fn hash(_: @This(), str: []const u8) u64 {
|
||||
return case_insensitive_utf8.hash(str);
|
||||
}
|
||||
}, std.hash_map.default_max_load_percentage);
|
||||
|
||||
fn handleBadRequest(writer: Writer) !void {
|
||||
std.log.info("400 Bad Request", .{});
|
||||
try writer.writeAll("HTTP/1.1 400 Bad Request");
|
||||
|
@ -32,14 +110,6 @@ const Method = enum {
|
|||
//TRACE,
|
||||
};
|
||||
|
||||
fn areStringsEqual(lhs: []const u8, rhs: []const u8) bool {
|
||||
if (lhs.len != rhs.len) return false;
|
||||
for (lhs) |_, i| {
|
||||
if (lhs[i] != rhs[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
fn parseHttpMethod(reader: Reader) !Method {
|
||||
var buf: [8]u8 = undefined;
|
||||
const str = reader.readUntilDelimiter(&buf, ' ') catch |err| switch (err) {
|
||||
|
@ -48,7 +118,7 @@ fn parseHttpMethod(reader: Reader) !Method {
|
|||
};
|
||||
|
||||
inline for (@typeInfo(Method).Enum.fields) |method| {
|
||||
if (areStringsEqual(method.name, str)) {
|
||||
if (std.mem.eql(u8, method.name, str)) {
|
||||
return @intToEnum(Method, method.value);
|
||||
}
|
||||
}
|
||||
|
@ -63,7 +133,7 @@ fn checkProto(reader: Reader) !void {
|
|||
else => return err,
|
||||
};
|
||||
|
||||
if (!areStringsEqual(proto, "HTTP")) {
|
||||
if (!std.mem.eql(u8, proto, "HTTP")) {
|
||||
return error.UnknownProtocol;
|
||||
}
|
||||
|
||||
|
@ -104,7 +174,8 @@ fn parseHeaders(allocator: std.mem.Allocator, reader: Reader) !HeaderMap {
|
|||
|
||||
// TODO: handle multi-line headers
|
||||
const name = extractHeaderName(line) orelse continue;
|
||||
const value = line[name.len + 1 + 1 ..];
|
||||
const value_end = if (line[line.len - 1] == '\r') line.len - 1 else line.len;
|
||||
const value = line[name.len + 1 + 1 .. value_end];
|
||||
|
||||
if (name.len == 0 or value.len == 0) return error.BadRequest;
|
||||
|
||||
|
@ -116,11 +187,6 @@ fn parseHeaders(allocator: std.mem.Allocator, reader: Reader) !HeaderMap {
|
|||
@memcpy(name_alloc.ptr, name.ptr, name.len);
|
||||
@memcpy(value_alloc.ptr, value.ptr, value.len);
|
||||
|
||||
for (name_alloc) |*ch| {
|
||||
//TODO: utf8
|
||||
ch.* = std.ascii.toLower(ch.*);
|
||||
}
|
||||
|
||||
try map.put(name_alloc, value_alloc);
|
||||
}
|
||||
|
||||
|
@ -163,15 +229,15 @@ fn handleHttpRequest(reader: Reader, writer: Writer) anyerror!void {
|
|||
|
||||
const headers = try parseHeaders(allocator, reader);
|
||||
|
||||
const has_body = (headers.get("content-length") orelse headers.get("transfer-encoding")) != null;
|
||||
const has_body = (headers.get("Content-Length") orelse headers.get("Transfer-Encoding")) != null;
|
||||
|
||||
const tfer_encoding = headers.get("transfer-encoding");
|
||||
if (tfer_encoding != null and !areStringsEqual(tfer_encoding.?, "identity")) {
|
||||
const tfer_encoding = headers.get("Transfer-Encoding");
|
||||
if (tfer_encoding != null and !std.mem.eql(u8, tfer_encoding.?, "identity")) {
|
||||
return error.UnsupportedMediaType;
|
||||
}
|
||||
|
||||
const encoding = headers.get("content-encoding");
|
||||
if (encoding != null and !areStringsEqual(encoding.?, "identity")) {
|
||||
const encoding = headers.get("Content-Encoding");
|
||||
if (encoding != null and !std.mem.eql(u8, encoding.?, "identity")) {
|
||||
return error.UnsupportedMediaType;
|
||||
}
|
||||
|
||||
|
@ -224,6 +290,7 @@ const Context = struct {
|
|||
return switch (status) {
|
||||
200 => "OK",
|
||||
204 => "No Content",
|
||||
404 => "Not Found",
|
||||
else => "",
|
||||
};
|
||||
}
|
||||
|
@ -245,7 +312,7 @@ const Context = struct {
|
|||
try self.openInternal(status);
|
||||
if (body.len != 0) {
|
||||
try self.writer.print("Content-Length: {}\r\n", .{body.len});
|
||||
if (self.headers.get("content-type") == null) {
|
||||
if (self.headers.get("Content-Type") == null) {
|
||||
try self.writer.writeAll("Content-Type: application/octet-stream\r\n");
|
||||
}
|
||||
}
|
||||
|
@ -255,6 +322,10 @@ const Context = struct {
|
|||
try self.writer.writeAll(body);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn statusOnly(self: *Response, status: u16) !void {
|
||||
try self.openInternal(status);
|
||||
}
|
||||
};
|
||||
|
||||
request: Request,
|
||||
|
@ -360,7 +431,7 @@ const Route = struct {
|
|||
|
||||
const slice = path[segment_start..index];
|
||||
const match = switch (seg) {
|
||||
.literal => |str| areStringsEqual(slice, str),
|
||||
.literal => |str| case_insensitive_utf8.eql(slice, str),
|
||||
.param => true,
|
||||
};
|
||||
|
||||
|
@ -387,7 +458,7 @@ const Route = struct {
|
|||
|
||||
switch (seg) {
|
||||
.param => |param| {
|
||||
if (areStringsEqual(param, name)) {
|
||||
if (std.mem.eql(u8, param, name)) {
|
||||
return slice.?;
|
||||
}
|
||||
},
|
||||
|
@ -426,26 +497,53 @@ fn routeRequest(ctx: *Context) !void {
|
|||
|
||||
const routes = [_]Route{
|
||||
Route.from(.GET, "/", staticString("Index Page")),
|
||||
Route.from(.GET, "/test", staticString("some test value idfk")),
|
||||
Route.from(.GET, "/objs/:id/get", getObjIdGet),
|
||||
Route.from(.POST, "/form/submit", staticString("form submit accepted")),
|
||||
Route.from(.GET, "/abc", staticString("abc")),
|
||||
Route.from(.GET, "/user/:id", getUser),
|
||||
};
|
||||
|
||||
const this_scheme = "http";
|
||||
const this_host = "localhost:8080";
|
||||
|
||||
const account = .{
|
||||
.id = "abc123",
|
||||
.handle = "testacct",
|
||||
.host = this_host,
|
||||
};
|
||||
|
||||
fn getUser(ctx: *Context) anyerror!void {
|
||||
const id = ctx.request.arg("id");
|
||||
|
||||
const host = ctx.request.headers.get("host");
|
||||
if (host == null) {
|
||||
try ctx.response.statusOnly(400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!std.mem.eql(u8, account.id, id) or
|
||||
!std.mem.eql(u8, account.host, ctx.request.headers.get("host").?))
|
||||
{
|
||||
try ctx.response.statusOnly(404);
|
||||
return;
|
||||
}
|
||||
|
||||
try ctx.response.headers.put("Content-Type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"");
|
||||
|
||||
var writer = try ctx.response.open(200);
|
||||
try writer.writeAll("{\"type\":\"Person\",");
|
||||
try writer.print("\"id\":\"{s}://{s}/user/{s}\",", .{ this_scheme, this_host, id });
|
||||
try writer.print("\"preferredUsername\":\"{s}\"", .{account.handle});
|
||||
try writer.writeAll("}");
|
||||
}
|
||||
|
||||
fn staticString(comptime str: []const u8) Route.Handler {
|
||||
return (struct {
|
||||
fn func(ctx: *Context) anyerror!void {
|
||||
try ctx.response.headers.put("content-type", "text/plain");
|
||||
try ctx.response.headers.put("Content-Type", "text/plain");
|
||||
try ctx.response.write(200, str);
|
||||
}
|
||||
}).func;
|
||||
}
|
||||
|
||||
fn getObjIdGet(ctx: *Context) anyerror!void {
|
||||
try ctx.response.headers.put("content-type", "text/plain");
|
||||
var writer = try ctx.response.open(200);
|
||||
try writer.print("object id {s}", .{ctx.request.arg("id")});
|
||||
}
|
||||
|
||||
pub fn main() anyerror!void {
|
||||
var srv = std.net.StreamServer.init(.{ .reuse_address = true });
|
||||
defer srv.deinit();
|
||||
|
|
Loading…
Reference in a new issue