aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitmodules3
-rw-r--r--build.zig11
-rw-r--r--src/cli.zig4
-rw-r--r--src/deps/picohttp.zig261
m---------src/deps/picohttpparser0
-rw-r--r--src/exact_size_matcher.zig46
-rw-r--r--src/http.zig215
-rw-r--r--src/string_immutable.zig45
8 files changed, 428 insertions, 157 deletions
diff --git a/.gitmodules b/.gitmodules
index aca112de6..2dd4085f2 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -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/
diff --git a/build.zig b/build.zig
index 042642932..76e7451f5 100644
--- a/build.zig
+++ b/build.zig
@@ -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();
diff --git a/src/cli.zig b/src/cli.zig
index 2f2cca06b..decfbec6b 100644
--- a/src/cli.zig
+++ b/src/cli.zig
@@ -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 => {
diff --git a/src/deps/picohttp.zig b/src/deps/picohttp.zig
new file mode 100644
index 000000000..407c203a1
--- /dev/null
+++ b/src/deps/picohttp.zig
@@ -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});
+ }
+}
diff --git a/src/deps/picohttpparser b/src/deps/picohttpparser
new file mode 160000
+Subproject 066d2b1e9ab820703db0837a7255d92d30f0c9f
diff --git a/src/exact_size_matcher.zig b/src/exact_size_matcher.zig
new file mode 100644
index 000000000..63950f576
--- /dev/null
+++ b/src/exact_size_matcher.zig
@@ -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"));
+}
diff --git a/src/http.zig b/src/http.zig
index c7ec0c786..608011528 100644
--- a/src/http.zig
+++ b/src/http.zig
@@ -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 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 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);
-// }
-// };
+// 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 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});
+// }
+
+// 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)});
+// }
diff --git a/src/string_immutable.zig b/src/string_immutable.zig
index 663b714ea..ab19f4dc9 100644
--- a/src/string_immutable.zig
+++ b/src/string_immutable.zig
@@ -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");