diff options
-rw-r--r-- | .gitmodules | 3 | ||||
-rw-r--r-- | build.zig | 11 | ||||
-rw-r--r-- | src/cli.zig | 4 | ||||
-rw-r--r-- | src/deps/picohttp.zig | 261 | ||||
m--------- | src/deps/picohttpparser | 0 | ||||
-rw-r--r-- | src/exact_size_matcher.zig | 46 | ||||
-rw-r--r-- | src/http.zig | 215 | ||||
-rw-r--r-- | src/string_immutable.zig | 45 |
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/ @@ -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"); |