This commit is contained in:
Jarred Sumner
2021-05-20 02:34:42 -07:00
parent 6475442469
commit cee857ac4e
8 changed files with 425 additions and 153 deletions

3
.gitmodules vendored
View File

@@ -1,3 +1,6 @@
# [submodule "src/deps/zig-clap"]
# path = src/deps/zig-clap
# url = https://github.com/Hejsil/zig-clap
[submodule "src/deps/picohttpparser"]
path = src/deps/picohttpparser
url = https://github.com/h2o/picohttpparser/

View File

@@ -1,5 +1,15 @@
const std = @import("std");
pub fn addPicoHTTP(step: *std.build.LibExeObjStep, comptime dir: []const u8) void {
step.addCSourceFile(dir ++ "/picohttpparser/picohttpparser.c", &[_][]const u8{});
step.addIncludeDir(dir ++ "/picohttpparser");
step.addPackage(.{
.name = "picohttp",
.path = dir ++ "/picohttp.zig",
});
}
pub fn build(b: *std.build.Builder) void {
// Standard target options allows the person running `zig build` to choose
// what target to build for. Here we do not override the defaults, which
@@ -97,6 +107,7 @@ pub fn build(b: *std.build.Builder) void {
if (!target.getCpuArch().isWasm()) {
exe.addLibPath("/usr/local/lib");
addPicoHTTP(exe, "src/deps");
}
exe.install();

View File

@@ -283,9 +283,9 @@ pub const Cli = struct {
var log = logger.Log.init(allocator);
var panicker = MainPanicHandler.init(&log);
MainPanicHandler.Singleton = &panicker;
try Server.start(allocator);
const args = try Arguments.parse(alloc.static, stdout, stderr);
var args = try Arguments.parse(alloc.static, stdout, stderr);
try Server.start(allocator, &args);
var result: options.TransformResult = undefined;
switch (args.resolve orelse Api.ResolveMode.dev) {
Api.ResolveMode.disable => {

261
src/deps/picohttp.zig Normal file
View File

@@ -0,0 +1,261 @@
const std = @import("std");
const c = @cImport(@cInclude("picohttpparser.h"));
const ExactSizeMatcher = @import("../exact_size_matcher.zig").ExactSizeMatcher;
const Match = ExactSizeMatcher(2);
const fmt = std.fmt;
const assert = std.debug.assert;
pub fn addTo(step: *std.build.LibExeObjStep, comptime dir: []const u8) void {
step.addCSourceFile(dir ++ "/lib/picohttpparser.c", &[_][]const u8{});
step.addIncludeDir(dir ++ "/lib");
step.addPackage(.{
.name = "picohttp",
.path = dir ++ "/picohttp.zig",
});
}
pub const Header = struct {
name: []const u8,
value: []const u8,
pub fn isMultiline(self: Header) bool {
return @ptrToInt(self.name.ptr) == 0;
}
pub fn format(self: Header, comptime layout: []const u8, opts: fmt.FormatOptions, writer: anytype) !void {
if (self.isMultiline()) {
try fmt.format(writer, "{s}", .{self.value});
} else {
try fmt.format(writer, "{s}: {s}", .{ self.name, self.value });
}
}
comptime {
assert(@sizeOf(Header) == @sizeOf(c.phr_header));
assert(@alignOf(Header) == @alignOf(c.phr_header));
}
};
pub const Request = struct {
method_: []const u8,
method: Method,
path: []const u8,
minor_version: usize,
headers: []const Header,
pub const Method = enum {
GET,
HEAD,
PATCH,
PUT,
POST,
OPTIONS,
CONNECT,
TRACE,
pub fn which(str: []const u8) ?Method {
if (str.len < 3) {
return null;
}
switch (Match.match(str[0..2])) {
Match.case("GE"), Match.case("ge") => {
return .GET;
},
Match.case("HE"), Match.case("he") => {
return .HEAD;
},
Match.case("PA"), Match.case("pa") => {
return .PATCH;
},
Match.case("PO"), Match.case("po") => {
return .POST;
},
Match.case("PU"), Match.case("pu") => {
return .PUT;
},
Match.case("OP"), Match.case("op") => {
return .OPTIONS;
},
Match.case("CO"), Match.case("co") => {
return .CONNECT;
},
Match.case("TR"), Match.case("tr") => {
return .TRACE;
},
else => {
return null;
},
}
}
};
pub fn parse(buf: []const u8, src: []Header) !Request {
var method: []const u8 = undefined;
var path: []const u8 = undefined;
var minor_version: c_int = undefined;
var num_headers: usize = src.len;
const rc = c.phr_parse_request(
buf.ptr,
buf.len,
@ptrCast([*c][*c]const u8, &method.ptr),
&method.len,
@ptrCast([*c][*c]const u8, &path.ptr),
&path.len,
&minor_version,
@ptrCast([*c]c.phr_header, src.ptr),
&num_headers,
0,
);
return switch (rc) {
-1 => error.BadRequest,
-2 => error.ShortRead,
else => |bytes_read| Request{
.method_ = method,
.method = Request.Method.which(method) orelse return error.InvalidMethod,
.path = path,
.minor_version = @intCast(usize, minor_version),
.headers = src[0..num_headers],
},
};
}
};
test "pico_http: parse request" {
const REQ = "GET /wp-content/uploads/2010/03/hello-kitty-darth-vader-pink.jpg HTTP/1.1\r\n" ++
"Host: www.kittyhell.com\r\n" ++
"User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; ja-JP-mac; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 " ++
"Pathtraq/0.9\r\n" ++
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" ++
"Accept-Language: ja,en-us;q=0.7,en;q=0.3\r\n" ++
"Accept-Encoding: gzip,deflate\r\n" ++
"Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7\r\n" ++
"Keep-Alive: 115\r\n" ++
"Connection: keep-alive\r\n" ++
"TestMultiline: Hello world\r\n" ++
" This is a second line in the header!\r\n" ++
"Cookie: wp_ozh_wsa_visits=2; wp_ozh_wsa_visit_lasttime=xxxxxxxxxx; " ++
"__utma=xxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.x; " ++
"__utmz=xxxxxxxxx.xxxxxxxxxx.x.x.utmccn=(referral)|utmcsr=reader.livedoor.com|utmcct=/reader/|utmcmd=referral\r\n" ++
"\r\n";
var headers: [32]Header = undefined;
const req = try Request.parse(REQ, &headers);
std.debug.print("Method: {s}\n", .{req.method});
std.debug.print("Path: {s}\n", .{req.path});
std.debug.print("Minor Version: {}\n", .{req.minor_version});
for (req.headers) |header| {
std.debug.print("{}\n", .{header});
}
}
pub const Response = struct {
minor_version: usize,
status_code: usize,
status: []const u8,
headers: []const Header,
pub fn parse(buf: []const u8, src: []Header) !Response {
var minor_version: c_int = undefined;
var status_code: c_int = undefined;
var status: []const u8 = undefined;
var num_headers: usize = src.len;
const rc = c.phr_parse_response(
buf.ptr,
buf.len,
&minor_version,
&status_code,
@ptrCast([*c][*c]const u8, &status.ptr),
&status.len,
@ptrCast([*c]c.phr_header, src.ptr),
&num_headers,
0,
);
return switch (rc) {
-1 => error.BadResponse,
-2 => error.ShortRead,
else => |bytes_read| Response{
.minor_version = @intCast(usize, minor_version),
.status_code = @intCast(usize, status_code),
.status = status,
.headers = src[0..num_headers],
},
};
}
};
test "pico_http: parse response" {
const RES = "HTTP/1.1 200 OK\r\n" ++
"Date: Mon, 22 Mar 2021 08:15:54 GMT\r\n" ++
"Content-Type: text/html; charset=utf-8\r\n" ++
"Content-Length: 9593\r\n" ++
"Connection: keep-alive\r\n" ++
"Server: gunicorn/19.9.0\r\n" ++
"Access-Control-Allow-Origin: *\r\n" ++
"Access-Control-Allow-Credentials: true\r\n" ++
"\r\n";
var headers: [32]Header = undefined;
const res = try Response.parse(RES, &headers);
std.debug.print("Minor Version: {}\n", .{res.minor_version});
std.debug.print("Status Code: {}\n", .{res.status_code});
std.debug.print("Status: {s}\n", .{res.status});
for (res.headers) |header| {
std.debug.print("{}\n", .{header});
}
}
pub const Headers = struct {
headers: []const Header,
pub fn parse(buf: []const u8, src: []Header) !Headers {
var num_headers: usize = src.len;
const rc = c.phr_parse_headers(
buf.ptr,
buf.len,
@ptrCast([*c]c.phr_header, src.ptr),
@ptrCast([*c]usize, &num_headers),
0,
);
return switch (rc) {
-1 => error.BadHeaders,
-2 => error.ShortRead,
else => |bytes_read| Headers{
.headers = src[0..num_headers],
},
};
}
};
test "pico_http: parse headers" {
const HEADERS = "Date: Mon, 22 Mar 2021 08:15:54 GMT\r\n" ++
"Content-Type: text/html; charset=utf-8\r\n" ++
"Content-Length: 9593\r\n" ++
"Connection: keep-alive\r\n" ++
"Server: gunicorn/19.9.0\r\n" ++
"Access-Control-Allow-Origin: *\r\n" ++
"Access-Control-Allow-Credentials: true\r\n" ++
"\r\n";
var headers: [32]Header = undefined;
const result = try Headers.parse(HEADERS, &headers);
for (result.headers) |header| {
std.debug.print("{}\n", .{header});
}
}

View File

@@ -0,0 +1,46 @@
const std = @import("std");
pub fn ExactSizeMatcher(comptime max_bytes: usize) type {
const T = std.meta.Int(
.unsigned,
max_bytes * 8,
);
return struct {
pub fn match(str: anytype) T {
return hash(str) orelse std.math.maxInt(T);
}
pub fn case(comptime str: []const u8) T {
return hash(str) orelse std.math.maxInt(T);
}
pub fn hash(str: anytype) ?T {
if (str.len > max_bytes) return null;
var tmp = [_]u8{0} ** max_bytes;
std.mem.copy(u8, &tmp, str[0..str.len]);
return std.mem.readIntNative(T, &tmp);
}
pub fn hashUnsafe(str: anytype) T {
var tmp = [_]u8{0} ** max_bytes;
std.mem.copy(u8, &tmp, str[0..str.len]);
return std.mem.readIntNative(T, &tmp);
}
};
}
const eight = ExactSizeMatcher(8);
test "ExactSizeMatcher 5 letter" {
const word = "yield";
expect(eight.match(word) == eight.case("yield"));
expect(eight.match(word) != eight.case("yields"));
}
test "ExactSizeMatcher 4 letter" {
const Four = ExactSizeMatcher(4);
const word = "from";
expect(Four.match(word) == Four.case("from"));
expect(Four.match(word) != Four.case("fro"));
}

View File

@@ -1,119 +1,112 @@
// const c = @import("./c.zig");
const std = @import("std");
usingnamespace @import("global.zig");
const Address = std.net.Address;
const routez = @import("routez");
const Request = routez.Request;
const Response = routez.Response;
const Api = @import("./api/schema.zig").Api;
const tcp = std.x.net.tcp;
const ip = std.x.net.ip;
const IPv4 = std.x.os.IPv4;
const IPv6 = std.x.os.IPv6;
const Socket = std.x.os.Socket;
const os = std.os;
const picohttp = @import("picohttp");
const Header = picohttp.Header;
const Request = picohttp.Request;
const Response = picohttp.Response;
const Headers = picohttp.Headers;
pub const Server = struct {
pub fn start(allocator: *std.mem.Allocator) !void {
var server = routez.Server.init(
allocator,
.{},
.{
routez.all("/", indexHandler),
routez.get("/about", aboutHandler),
routez.get("/about/more", aboutHandler2),
routez.get("/post/{post_num}/?", postHandler),
routez.static("./", "/static"),
routez.all("/counter", counterHandler),
},
);
var addr = Address.parseIp("127.0.0.1", 8080) catch unreachable;
server.listen(addr) catch unreachable;
options: *Api.TransformOptions,
allocator: *std.mem.Allocator,
threadlocal var headers_buf: [100]picohttp.Header = undefined;
fn run(server: *Server) !void {
const listener = try tcp.Listener.init(.ip, os.SOCK_CLOEXEC);
defer listener.deinit();
listener.setReuseAddress(true) catch {};
listener.setReusePort(true) catch {};
listener.setFastOpen(true) catch {};
// try listener.ack(true);
try listener.bind(ip.Address.initIPv4(IPv4.unspecified, 9000));
try listener.listen(128);
// try listener.set(true);
while (true) {
var conn = try listener.accept(os.SOCK_CLOEXEC);
server.handleConnection(&conn);
}
}
pub fn writeStatus(server: *Server, comptime code: u9, conn: *tcp.Connection) !void {
_ = try conn.client.write(std.fmt.comptimePrint("HTTP/1.1 {d}\r\n", .{code}), os.SOCK_CLOEXEC);
}
pub fn sendError(server: *Server, request: *Request, conn: *tcp.Connection, code: u9, msg: string) !void {
try server.writeStatus(code, connection);
conn.deinit();
}
pub fn handleRequest(server: *Server, request: *Request, conn: *tcp.Connection) !void {
try server.writeStatus(200, conn);
conn.deinit();
// switch (request.method) {
// .GET, .HEAD => {},
// else => {},
// }
}
pub fn handleConnection(server: *Server, conn: *tcp.Connection) void {
errdefer conn.deinit();
// https://stackoverflow.com/questions/686217/maximum-on-http-header-values
var req_buf: [std.mem.page_size]u8 = undefined;
var read_size = conn.client.read(&req_buf, os.SOCK_CLOEXEC) catch |err| {
return;
};
var req = picohttp.Request.parse(req_buf[0..read_size], &headers_buf) catch |err| {
Output.printError("ERR: {s}", .{@errorName(err)});
return;
};
server.handleRequest(&req, conn) catch |err| {
Output.printError("FAIL [{s}] - {s}: {s}", .{ @errorName(err), @tagName(req.method), req.path });
conn.deinit();
return;
};
Output.print("[{s}] - {s}", .{ @tagName(req.method), req.path });
}
pub fn start(allocator: *std.mem.Allocator, options: *Api.TransformOptions) !void {
var server = Server{ .options = options, .allocator = allocator };
try server.run();
}
};
fn indexHandler(req: Request, res: Response) !void {
try res.write("hi\n");
}
// fn indexHandler(req: Request, res: Response) !void {
// try res.write("hi\n");
// }
fn aboutHandler(req: Request, res: Response) !void {
try res.write("Hello from about\n");
}
// fn aboutHandler(req: Request, res: Response) !void {
// try res.write("Hello from about\n");
// }
fn aboutHandler2(req: Request, res: Response) !void {
try res.write("Hello from about2\n");
}
// fn aboutHandler2(req: Request, res: Response) !void {
// try res.write("Hello from about2\n");
// }
fn postHandler(req: Request, res: Response, args: *const struct {
post_num: []const u8,
}) !void {
try res.print("Hello from post, post_num is {s}\n", .{args.post_num});
}
// fn postHandler(req: Request, res: Response, args: *const struct {
// post_num: []const u8,
// }) !void {
// try res.print("Hello from post, post_num is {s}\n", .{args.post_num});
// }
var counter = std.atomic.Int(usize).init(0);
fn counterHandler(req: Request, res: Response) !void {
try res.print("Page loaded {d} times\n", .{counter.fetchAdd(1)});
}
// pub const Server = struct {
// pub var server = std.mem.zeroes(c.struct_mg_callbacks);
// pub fn beginRequest(conn: ?*c.struct_mg_connection) callconv(.C) c_int {
// return 0;
// }
// pub fn endRequest(conn: ?*const c.struct_mg_connection, status_code: c_int) callconv(.C) void {}
// pub fn logMessage(conn: ?*const c.struct_mg_connection, msg: [*c]const u8) callconv(.C) c_int {
// return 1;
// }
// pub fn logAccess(conn: ?*const c.struct_mg_connection, msg: [*c]const u8) callconv(.C) c_int {
// return 1;
// }
// // pub fn initSsl(conn: ?*c_void, ?*c_void) callconv(.C) c_int
// // pub fn initSslDomain(conn: [*c]const u8, ?*c_void, ?*c_void) callconv(.C) c_int
// // pub fn externalSslCtx(ctx: [*c]?*c_void, ?*c_void) callconv(.C) c_int
// // pub fn externalSslCtxDomain(ctx: [*c]const u8, [*c]?*c_void, ?*c_void) callconv(.C) c_int
// // pub fn connectionClose(conn: ?*const c.struct_mg_connection) callconv(.C) void
// // pub fn connectionClosed(conn: ?*const c.struct_mg_connection) callconv(.C) void
// // pub fn initLua(conn: ?*const c.struct_mg_connection, ?*c_void, c_uint) callconv(.C) void
// // pub fn exitLua(conn: ?*const c.struct_mg_connection, ?*c_void, c_uint) callconv(.C) void
// pub fn httpError(conn: ?*c.struct_mg_connection, status: c_int, msg: [*c]const u8) callconv(.C) c_int {
// return 0;
// }
// pub fn handleCodeRequest(conn: ?*c.struct_mg_connection, cbdata: ?*c_void) c_int {
// var buf = "helloooo";
// var buf_slice = buf[0.. :0];
// // c.mg_write(conn, &buf_slice, buf_slice.len);
// c.mg_send_http_ok(conn, "text/plain", buf_slice.len);
// return 200;
// }
// pub fn initContext(ctx: *c.struct_mg_context) callconv(.C) void {
// c.mg_set_request_handler(ctx, "/_src/", &handleCodeRequest, null);
// }
// pub fn exitContext(ctx: *c.struct_mg_context) callconv(.C) void {}
// pub fn initThread(ctx: *c.struct_mg_context, thread_type: c_int) callconv(.C) ?*c_void {}
// pub fn exitThread(ctx: *c.struct_mg_context, thread_type: c_int, user_ptr: ?*c_void) callconv(.C) void {}
// // pub fn initConnection(ctx: ?*const c.struct_mg_connection, [*c]?*c_void) callconv(.C) c_int {
// // }
// pub fn start() !void {
// // server.
// server.begin_request = &beginRequest;
// server.end_request = &endRequest;
// server.log_message = &logMessage;
// server.log_access = &logAccess;
// server.http_error = &httpError;
// server.init_context = &initContext;
// server.exit_context = &exitContext;
// server.init_thread = &initThread;
// server.exit_thread = &exitThread;
// const val = c.mg_init_library(c.MG_FEATURES_COMPRESSION);
// // callbacks.log_access
// var opts = [_:null][*c]const u8{
// "listening_ports",
// "4086",
// "request_timeout_ms",
// "10000",
// "error_log_file",
// "error.log",
// "enable_auth_domain_check",
// "no",
// };
// c.mg_start(&server, 0, opts);
// }
// };
// var counter = std.atomic.Int(usize).init(0);
// fn counterHandler(req: Request, res: Response) !void {
// try res.print("Page loaded {d} times\n", .{counter.fetchAdd(1)});
// }

View File

@@ -360,47 +360,4 @@ test "sortDesc" {
std.testing.expectEqualStrings(sorted_join, string_join);
}
pub fn ExactSizeMatcher(comptime max_bytes: usize) type {
const T = std.meta.Int(
.unsigned,
max_bytes * 8,
);
return struct {
pub fn match(str: anytype) T {
return hash(str) orelse std.math.maxInt(T);
}
pub fn case(comptime str: []const u8) T {
return hash(str) orelse std.math.maxInt(T);
}
fn hash(str: anytype) ?T {
if (str.len > max_bytes) return null;
var tmp = [_]u8{0} ** max_bytes;
std.mem.copy(u8, &tmp, str[0..str.len]);
return std.mem.readIntNative(T, &tmp);
}
fn hashUnsafe(str: anytype) T {
var tmp = [_]u8{0} ** max_bytes;
std.mem.copy(u8, &tmp, str[0..str.len]);
return std.mem.readIntNative(T, &tmp);
}
};
}
const eight = ExactSizeMatcher(8);
test "ExactSizeMatcher 5 letter" {
const word = "yield";
expect(eight.match(word) == eight.case("yield"));
expect(eight.match(word) != eight.case("yields"));
}
test "ExactSizeMatcher 4 letter" {
const Four = ExactSizeMatcher(4);
const word = "from";
expect(Four.match(word) == Four.case("from"));
expect(Four.match(word) != Four.case("fro"));
}
pub usingnamespace @import("exact_size_matcher.zig");