├── .gitattributes ├── example ├── static │ └── img │ │ ├── sevenfm-logo.png │ │ └── engine-2682239-1920.jpg ├── templates │ ├── form.html │ ├── stream.html │ ├── cover.html │ └── chat.html └── main.zig ├── .gitignore ├── zig.mod ├── src ├── templates │ ├── not-found.html │ ├── style.css │ └── error.html ├── zhp.zig ├── middleware.zig ├── url.zig ├── response.zig ├── cookies.zig ├── simd.zig ├── status.zig ├── websocket.zig ├── template.zig ├── headers.zig ├── forms.zig ├── mimetypes.zig ├── handlers.zig └── util.zig ├── .github ├── workflows │ └── ci.yml └── FUNDING.yml ├── tests ├── bench.zig ├── tornadoweb.py ├── basic.zig ├── parser.zig ├── raw.zig ├── search.zig └── http-requests.txt ├── LICENSE └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | example/static/** linguist-vendored 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /example/static/img/sevenfm-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frmdstryr/zhp/HEAD/example/static/img/sevenfm-logo.png -------------------------------------------------------------------------------- /example/static/img/engine-2682239-1920.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frmdstryr/zhp/HEAD/example/static/img/engine-2682239-1920.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache/ 2 | zig-out/ 3 | 4 | .kdev4/ 5 | *.kdev4 6 | *.kate-swp 7 | 8 | callgrind.* 9 | 10 | __pycache__/ 11 | 12 | -------------------------------------------------------------------------------- /zig.mod: -------------------------------------------------------------------------------- 1 | id: nqpxq5symwcw6oyphfhn0t5em5x1ds15g8j5ij90q5ampntp 2 | name: zhp 3 | main: src/zhp.zig 4 | license: MIT 5 | description: A HTTP server written in Zig. 6 | dependencies: 7 | -------------------------------------------------------------------------------- /src/templates/not-found.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Not Found 6 | 7 | 8 | 9 |

Not Found

10 |

The requested url does not exist.

11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/templates/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial,x-locale-body,sans-serif; 3 | } 4 | 5 | pre { 6 | background-color: #eee; 7 | border-left: 4px solid #3d7e9a; 8 | color: #333; 9 | font-size: 1rem; 10 | font-family: consolas,monaco,"Andale Mono",monospace; 11 | overflow: auto; 12 | padding: 1em; 13 | } 14 | -------------------------------------------------------------------------------- /src/templates/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Server Error 6 | 7 | 8 | 9 |

Server Error

10 |

Stacktrace

11 |
{% yield stacktrace %}
12 |

Request

13 |
{{request}}
14 | 15 | 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - name: Install dependencies 9 | run: | 10 | sudo snap install zig --classic --edge 11 | sudo docker pull williamyeh/wrk 12 | zig version 13 | - name: Module test 14 | run: zig test -OReleaseSafe src/app.zig 15 | - name: Parser test 16 | run: zig run --pkg-begin zhp src/zhp.zig --pkg-end -OReleaseSafe tests/parser.zig 17 | - name: Build 18 | run: zig build -Drelease-safe=true install 19 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: frmdstryr 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | 14 | -------------------------------------------------------------------------------- /tests/bench.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const server_cmd = [_][]const u8{ 4 | "timeout", "-s", "SIGINT", "15s", "./zig-cache/bin/zhttpd", 5 | }; 6 | 7 | const wrk_cmd = [_][]const u8{ "docker", "run", "--rm", "--net", "host", "williamyeh/wrk", "-t2", "-c10", "-d10s", "--latency", "http://127.0.0.1:9000/" }; 8 | 9 | pub fn main() anyerror!void { 10 | const allocator = std.heap.page_allocator; 11 | var server_process = try std.ChildProcess.init(server_cmd[0..], allocator); 12 | defer server_process.deinit(); 13 | try server_process.spawn(); 14 | 15 | // Wait for it to start 16 | std.time.sleep(1 * std.time.ns_per_s); 17 | 18 | var wrk_process = try std.ChildProcess.init(wrk_cmd[0..], allocator); 19 | defer wrk_process.deinit(); 20 | try wrk_process.spawn(); 21 | 22 | var r = wrk_process.wait(); 23 | r = server_process.wait(); 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 frmdstryr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /tests/tornadoweb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import tornado.httpserver 4 | import tornado.ioloop 5 | import tornado.options 6 | import tornado.web 7 | 8 | from tornado.options import define, options 9 | 10 | define("port", default=8888, help="run on the given port", type=int) 11 | 12 | with open('example/templates/cover.html') as f: 13 | template = f.read() 14 | 15 | 16 | class MainHandler(tornado.web.RequestHandler): 17 | def get(self): 18 | self.write("Hello, world") 19 | 20 | 21 | class TemplateHandler(tornado.web.RequestHandler): 22 | def get(self): 23 | self.write(template) 24 | 25 | 26 | def main(): 27 | tornado.options.parse_command_line() 28 | application = tornado.web.Application([ 29 | (r"/", TemplateHandler), 30 | (r"/hello", MainHandler), 31 | ]) 32 | http_server = tornado.httpserver.HTTPServer(application) 33 | http_server.listen(options.port) 34 | tornado.ioloop.IOLoop.current().start() 35 | 36 | 37 | if __name__ == "__main__": 38 | try: 39 | import uvloop 40 | uvloop.install() 41 | except ImportError as e: 42 | print(e) 43 | main() 44 | -------------------------------------------------------------------------------- /example/templates/form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Form 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 |
15 | Served to you by ZHP 16 |
17 |
18 | {% form %} 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/zhp.zig: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------- // 2 | // Copyright (c) 2019-2020, Jairus Martin. // 3 | // Distributed under the terms of the MIT License. // 4 | // The full license is in the file LICENSE, distributed with this software. // 5 | // -------------------------------------------------------------------------- // 6 | pub const forms = @import("forms.zig"); 7 | pub const middleware = @import("middleware.zig"); 8 | pub const util = @import("util.zig"); 9 | pub const mimetypes = @import("mimetypes.zig"); 10 | pub const datetime = @import("time/datetime.zig"); 11 | pub const handlers = @import("handlers.zig"); 12 | pub const responses = @import("status.zig"); 13 | pub const websocket = @import("websocket.zig"); 14 | pub const url = @import("url.zig"); 15 | pub const template = @import("template.zig"); 16 | 17 | pub const Headers = @import("headers.zig").Headers; 18 | pub const Cookies = @import("cookies.zig").Cookies; 19 | pub const Request = @import("request.zig").Request; 20 | pub const Response = @import("response.zig").Response; 21 | pub const Websocket = websocket.Websocket; 22 | 23 | pub const IOStream = util.IOStream; 24 | pub const app = @import("app.zig"); 25 | pub const Route = app.Route; 26 | pub const Router = app.Router; 27 | pub const ServerRequest = app.ServerRequest; 28 | pub const Application = app.Application; 29 | pub const Middleware = app.Middleware; 30 | -------------------------------------------------------------------------------- /example/templates/stream.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Audio Stream 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 |
15 | © 2020 | Streaming live from Seven FM | Served to you by ZHP 16 |
17 |
18 |
19 | 20 | Seven FM 21 | 22 |
23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/middleware.zig: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------- // 2 | // Copyright (c) 2019-2020, Jairus Martin. // 3 | // Distributed under the terms of the MIT License. // 4 | // The full license is in the file LICENSE, distributed with this software. // 5 | // -------------------------------------------------------------------------- // 6 | const std = @import("std"); 7 | const web = @import("zhp.zig"); 8 | const log = std.log; 9 | const Handler = web.Handler; 10 | const Application = web.Application; 11 | const ServerRequest = web.ServerRequest; 12 | 13 | pub const LoggingMiddleware = struct { 14 | pub fn processResponse(app: *Application, server_request: *ServerRequest) !void { 15 | _ = app; 16 | _ = server_request; 17 | const request = &server_request.request; 18 | const response = &server_request.response; 19 | log.info("{d} {s} {s} ({s}) {d}", .{ response.status.code, @tagName(request.method), request.path, request.client, response.body.items.len }); 20 | } 21 | }; 22 | 23 | pub const SessionMiddleware = struct { 24 | // If you want storage use it statically 25 | 26 | pub fn processRequest(app: *Application, server_request: *ServerRequest) !void { 27 | _ = app; 28 | _ = server_request; 29 | } 30 | 31 | pub fn processResponse(app: *Application, server_request: *ServerRequest) !void { 32 | _ = app; 33 | _ = server_request; 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /tests/basic.zig: -------------------------------------------------------------------------------- 1 | // Run with 2 | // zig run --pkg-begin zhp src/zhp.zig --pkg-end -OReleaseSafe tests/basic.zig 3 | // 4 | const std = @import("std"); 5 | const net = std.net; 6 | const fs = std.fs; 7 | const os = std.os; 8 | const web = @import("zhp"); 9 | 10 | pub const io_mode = .evented; 11 | 12 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 13 | const allocator = gpa.allocator(); 14 | 15 | pub fn main() anyerror!void { 16 | const req_listen_addr = try net.Address.parseIp4("127.0.0.1", 9000); 17 | defer std.debug.assert(!gpa.deinit()); 18 | 19 | var server = net.StreamServer.init(.{}); 20 | defer server.deinit(); 21 | 22 | try server.listen(req_listen_addr); 23 | 24 | std.log.warn("Listening at {}\n", .{server.listen_address}); 25 | 26 | while (true) { 27 | const conn = try server.accept(); 28 | std.log.warn("Connected to {}\n", .{conn.address}); 29 | var frame = async handleConn(conn); 30 | await frame catch |err| { 31 | std.log.warn("Disconnected {}: {}\n", .{ conn.address, err }); 32 | }; 33 | } 34 | } 35 | 36 | pub fn handleConn(conn: net.StreamServer.Connection) !void { 37 | var stream = try web.IOStream.initCapacity(allocator, conn.stream, 0, 4096); 38 | defer stream.deinit(); 39 | var request = try web.Request.initCapacity(allocator, 1024 * 10, 32, 32); 40 | defer request.deinit(); 41 | var cnt: usize = 0; 42 | while (true) { 43 | cnt += 1; 44 | defer request.reset(); 45 | request.parse(&stream, .{}) catch |err| switch (err) { 46 | error.EndOfStream, error.BrokenPipe, error.ConnectionResetByPeer => break, 47 | else => return err, 48 | }; 49 | 50 | try conn.stream.writer().writeAll("HTTP/1.1 200 OK\r\n" ++ 51 | "Content-Length: 15\r\n" ++ 52 | "Connection: keep-alive\r\n" ++ 53 | "Content-Type: text/plain; charset=UTF-8\r\n" ++ 54 | "Server: Example\r\n" ++ 55 | "Date: Wed, 17 Apr 2013 12:00:00 GMT\r\n" ++ 56 | "\r\n" ++ 57 | "Hello, World!\r\n" ++ 58 | "\r\n"); 59 | } 60 | return error.ClosedCleanly; 61 | } 62 | -------------------------------------------------------------------------------- /tests/parser.zig: -------------------------------------------------------------------------------- 1 | // Run with 2 | // zig run --pkg-begin zhp src/zhp.zig --pkg-end -OReleaseSafe tests/parser.zig 3 | // 4 | const std = @import("std"); 5 | const net = std.net; 6 | const fs = std.fs; 7 | const os = std.os; 8 | const web = @import("zhp"); 9 | 10 | pub const io_mode = .evented; 11 | 12 | const Test = struct { 13 | path: []const u8, 14 | count: usize, 15 | }; 16 | 17 | const tests = [_]Test{ 18 | .{ .path = "tests/http-requests.txt", .count = 55 }, 19 | .{ .path = "tests/bigger.txt", .count = 275 }, 20 | }; 21 | 22 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 23 | const allocator = gpa.allocator(); 24 | 25 | pub fn main() !void { 26 | defer std.debug.assert(!gpa.deinit()); 27 | var timer = try std.time.Timer.start(); 28 | for (tests) |t| { 29 | std.log.warn("Parsing {s}...", .{t.path}); 30 | var file = try fs.cwd().openFile(t.path, .{}); 31 | timer.reset(); 32 | var socket = net.Stream{ .handle = file.handle }; // HACK... 33 | const cnt = try parseRequests(socket); 34 | std.log.warn("Done! ({} ns/req)\n", .{timer.read() / t.count}); 35 | try std.testing.expectEqual(t.count, cnt); 36 | } 37 | } 38 | 39 | pub fn parseRequests(socket: net.Stream) !usize { 40 | var stream = try web.IOStream.initCapacity(allocator, socket, 0, 4096); 41 | defer stream.deinit(); 42 | var request = try web.Request.initCapacity(allocator, 1024 * 10, 32, 32); 43 | defer request.deinit(); 44 | var cnt: usize = 0; 45 | var end: usize = 0; 46 | while (true) { 47 | cnt += 1; 48 | defer request.reset(); 49 | request.parse(&stream, .{}) catch |err| switch (err) { 50 | error.EndOfStream, error.BrokenPipe, error.ConnectionResetByPeer => break, 51 | else => { 52 | std.log.warn("Stream {}:\n'{s}'\n", .{ end, stream.in_buffer[0..end] }); 53 | std.log.warn("Error parsing:\n'{s}'\n", .{request.buffer.items[0..stream.readCount()]}); 54 | return err; 55 | }, 56 | }; 57 | //std.log.warn("{}\n", .{request}); 58 | end = stream.readCount(); 59 | } 60 | return cnt; 61 | } 62 | -------------------------------------------------------------------------------- /tests/raw.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const net = std.net; 3 | const fs = std.fs; 4 | const os = std.os; 5 | 6 | pub const io_mode = .evented; 7 | 8 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 9 | const allocator = &gpa.allocator; 10 | 11 | pub fn main() anyerror!void { 12 | const req_listen_addr = try net.Address.parseIp4("127.0.0.1", 9000); 13 | defer std.debug.assert(!gpa.deinit()); 14 | 15 | // Ignore sigpipe 16 | var act = os.Sigaction{ 17 | .handler = .{ .sigaction = os.SIG_IGN }, 18 | .mask = os.empty_sigset, 19 | .flags = 0, 20 | }; 21 | os.sigaction(os.SIGPIPE, &act, null); 22 | 23 | var server = net.StreamServer.init(.{ .reuse_address = true }); 24 | defer server.close(); 25 | defer server.deinit(); 26 | 27 | try server.listen(req_listen_addr); 28 | 29 | std.log.warn("Listening at {}\n", .{server.listen_address}); 30 | 31 | while (true) { 32 | const conn = try server.accept(); 33 | std.log.warn("{}\n", .{conn}); 34 | const frame = try allocator.create(@Frame(serve)); 35 | frame.* = async serve(conn); 36 | // Don't wait! 37 | } 38 | } 39 | 40 | pub fn serve(conn: net.StreamServer.Connection) !void { 41 | defer conn.stream.close(); 42 | handleConn(conn) catch |err| { 43 | std.log.warn("Disconnected {}: {}\n", .{ conn, err }); 44 | }; 45 | } 46 | 47 | pub fn handleConn(conn: net.StreamServer.Connection) !void { 48 | var reader = conn.stream.reader(); 49 | var writer = conn.stream.writer(); 50 | var buf: [64 * 1024]u8 = undefined; 51 | while (true) { 52 | const n = try reader.read(&buf); 53 | var it = std.mem.split(buf[0..n], "\r\n\r\n"); 54 | while (it.next()) |req| { 55 | try writer.writeAll("HTTP/1.1 200 OK\r\n" ++ 56 | "Content-Length: 15\r\n" ++ 57 | "Connection: keep-alive\r\n" ++ 58 | "Content-Type: text/plain; charset=UTF-8\r\n" ++ 59 | "Server: Example\r\n" ++ 60 | "Date: Wed, 17 Apr 2013 12:00:00 GMT\r\n" ++ 61 | "\r\n" ++ 62 | "Hello, World!\r\n" ++ 63 | "\r\n"); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZHP 2 | 3 | [![status](https://github.com/frmdstryr/zhp/actions/workflows/ci.yml/badge.svg)](https://github.com/frmdstryr/zhp/actions) 4 | 5 | A (work in progress) Http server written in [Zig](https://ziglang.org/). 6 | 7 | If you have suggestions on improving the design please feel free to comment! 8 | 9 | ### Features 10 | 11 | - A zero-copy parser and aims to compete with these [parser_benchmarks](https://github.com/rust-bakery/parser_benchmarks/tree/master/http) 12 | while still rejecting nonsense requests. It currently runs around ~1000MB/s. 13 | - Regex url routing thanks to [ctregex](https://github.com/alexnask/ctregex.zig) 14 | - Struct based handlers where the method maps to the function name 15 | - A builtin static file handler, error page handler, and not found page handler 16 | - Middleware support 17 | - Parses forms encoded with `multipart/form-data` 18 | - Streaming responses 19 | - Websockets 20 | 21 | See how it compares in the [http benchmarks](https://gist.github.com/kprotty/3f369f46293a421f09190b829cfb48f7#file-newresults-md) 22 | done by kprotty (now very old). 23 | 24 | It's a work in progress... feel free to contribute! 25 | 26 | 27 | ### Demo 28 | 29 | Try out the demo at [https://zhp.codelv.com](https://zhp.codelv.com). 30 | 31 | > Note: If you try to benchmark the server it'll ban you, please run it locally 32 | > or on your own server to do benchmarks. 33 | 34 | To make and deploy your own app see: 35 | - [demo project](https://github.com/frmdstryr/zhp-demo) 36 | - [zig buildpack](https://github.com/frmdstryr/zig-buildpack) 37 | 38 | 39 | ### Example 40 | 41 | See the `example` folder for a more detailed example. 42 | 43 | ```zig 44 | const std = @import("std"); 45 | const web = @import("zhp"); 46 | 47 | pub const io_mode = .evented; 48 | pub const log_level = .info; 49 | 50 | const MainHandler = struct { 51 | pub fn get(self: *MainHandler, request: *web.Request, response: *web.Response) !void { 52 | _ = self; 53 | _ = request; 54 | try response.headers.put("Content-Type", "text/plain"); 55 | _ = try response.stream.write("Hello, World!"); 56 | } 57 | 58 | }; 59 | 60 | pub const routes = [_]web.Route{ 61 | web.Route.create("home", "/", MainHandler), 62 | }; 63 | 64 | pub const middleware = [_]web.Middleware{ 65 | web.Middleware.create(web.middleware.LoggingMiddleware), 66 | }; 67 | 68 | pub fn main() anyerror!void { 69 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 70 | defer std.debug.assert(!gpa.deinit()); 71 | const allocator = gpa.allocator(); 72 | 73 | var app = web.Application.init(allocator, .{.debug=true}); 74 | defer app.deinit(); 75 | 76 | try app.listen("127.0.0.1", 9000); 77 | try app.start(); 78 | } 79 | 80 | ``` 81 | -------------------------------------------------------------------------------- /src/url.zig: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------- // 2 | // Copyright (c) 2019-2020, Jairus Martin. // 3 | // Distributed under the terms of the MIT License. // 4 | // The full license is in the file LICENSE, distributed with this software. // 5 | // -------------------------------------------------------------------------- // 6 | const std = @import("std"); 7 | const mem = std.mem; 8 | const ascii = std.ascii; 9 | const testing = std.testing; 10 | const assert = std.debug.assert; 11 | 12 | // Percent encode the source data 13 | pub fn encode(dest: []u8, source: []const u8) ![]const u8 { 14 | assert(dest.len >= source.len); 15 | var i: usize = 0; 16 | for (source) |ch| { 17 | if (ascii.isAlNum(ch)) { 18 | dest[i] = ch; 19 | i += 1; 20 | } else if (ch == ' ') { 21 | dest[i] = '+'; 22 | i += 1; 23 | } else { 24 | const end = i + 3; 25 | if (end > dest.len) return error.NoSpaceLeft; 26 | dest[i] = '%'; 27 | _ = try std.fmt.bufPrint(dest[i + 1 .. end], "{X}", .{ch}); 28 | i = end; 29 | } 30 | } 31 | return dest[0..i]; 32 | } 33 | 34 | test "url-encode" { 35 | var buf: [256]u8 = undefined; 36 | const encoded = try encode(&buf, "hOlmDALJCWWdjzfBV4ZxJPmrdCLWB/tq7Z/" ++ 37 | "fp4Q/xXbVPPREuMJMVGzKraTuhhNWxCCwi6yFEZg="); 38 | try testing.expectEqualStrings("hOlmDALJCWWdjzfBV4ZxJPmrdCLWB%2Ftq7Z%2F" ++ 39 | "fp4Q%2FxXbVPPREuMJMVGzKraTuhhNWxCCwi6yFEZg%3D", encoded); 40 | } 41 | 42 | // Percent decode the source data 43 | pub fn decode(dest: []u8, source: []const u8) ![]const u8 { 44 | assert(dest.len >= source.len); 45 | var i: usize = 0; 46 | var j: usize = 0; 47 | while (i < source.len) : (i += 1) { 48 | const ch = source[i]; 49 | switch (ch) { 50 | '%' => { 51 | i += 1; 52 | const end = i + 2; 53 | if (source.len < end) return error.DecodeError; 54 | dest[j] = try std.fmt.parseInt(u8, source[i..end], 16); 55 | i += 1; 56 | }, 57 | '+' => { 58 | dest[j] = ' '; 59 | }, 60 | else => { 61 | dest[j] = ch; 62 | }, 63 | } 64 | j += 1; 65 | } 66 | return dest[0..j]; 67 | } 68 | 69 | test "url-decode" { 70 | var buf: [256]u8 = undefined; 71 | const decoded = try decode(&buf, "hOlmDALJCWWdjzfBV4ZxJPmrdCLWB%2Ftq7Z%2F" ++ 72 | "fp4Q%2FxXbVPPREuMJMVGzKraTuhhNWxCCwi6yFEZg%3D"); 73 | try testing.expectEqualStrings("hOlmDALJCWWdjzfBV4ZxJPmrdCLWB/tq7Z/" ++ 74 | "fp4Q/xXbVPPREuMJMVGzKraTuhhNWxCCwi6yFEZg=", decoded); 75 | } 76 | 77 | // Look for host in a url 78 | // 79 | pub fn findHost(url: []const u8) []const u8 { 80 | var host = url; 81 | if (mem.indexOf(u8, host, "://")) |start| { 82 | host = host[start + 3 ..]; 83 | if (mem.indexOf(u8, host, "/")) |end| { 84 | host = host[0..end]; 85 | } 86 | } 87 | return host; 88 | } 89 | 90 | test "url-find-host" { 91 | const url = "http://localhost:9000"; 92 | try testing.expectEqualStrings("localhost:9000", findHost(url)); 93 | const url2 = "localhost:9000"; 94 | try testing.expectEqualStrings("localhost:9000", findHost(url2)); 95 | } 96 | -------------------------------------------------------------------------------- /src/response.zig: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------- // 2 | // Copyright (c) 2019-2020, Jairus Martin. // 3 | // Distributed under the terms of the MIT License. // 4 | // The full license is in the file LICENSE, distributed with this software. // 5 | // -------------------------------------------------------------------------- // 6 | const std = @import("std"); 7 | const Allocator = std.mem.Allocator; 8 | 9 | const web = @import("zhp.zig"); 10 | const responses = web.responses; 11 | const Status = responses.Status; 12 | const Headers = web.Headers; 13 | const Request = web.Request; 14 | const IOStream = web.IOStream; 15 | const testing = std.testing; 16 | 17 | const Bytes = std.ArrayList(u8); 18 | 19 | pub const Response = struct { 20 | pub const WriteError = error{OutOfMemory}; 21 | pub const Writer = std.io.Writer(*Response, WriteError, Response.writeFn); 22 | 23 | // Allocator for this response 24 | allocator: Allocator = undefined, 25 | headers: Headers, 26 | status: Status = responses.OK, 27 | disconnect_on_finish: bool = false, 28 | chunking_output: bool = false, 29 | 30 | // If this is set, the response will read from the stream 31 | send_stream: bool = false, 32 | 33 | // Set to true if your request handler already sent everything 34 | finished: bool = false, 35 | 36 | stream: Writer = undefined, 37 | 38 | // Buffer for output body, if the response is too big use source_stream 39 | body: Bytes, 40 | 41 | pub fn initCapacity(allocator: Allocator, buffer_size: usize, max_headers: usize) !Response { 42 | return Response{ 43 | .allocator = allocator, 44 | .headers = try Headers.initCapacity(allocator, max_headers), 45 | .body = try Bytes.initCapacity(allocator, buffer_size), 46 | }; 47 | } 48 | 49 | // Must be called before writing 50 | pub fn prepare(self: *Response) void { 51 | self.stream = Writer{ .context = self }; 52 | } 53 | 54 | // Reset the request so it can be reused without reallocating memory 55 | pub fn reset(self: *Response) void { 56 | self.body.items.len = 0; 57 | self.headers.reset(); 58 | self.status = responses.OK; 59 | self.disconnect_on_finish = false; 60 | self.chunking_output = false; 61 | self.send_stream = false; 62 | self.finished = false; 63 | } 64 | 65 | // Write into the body buffer 66 | // TODO: If the body reaches a certain limit it should be written 67 | // into a file instead 68 | pub fn writeFn(self: *Response, bytes: []const u8) WriteError!usize { 69 | try self.body.appendSlice(bytes); 70 | return bytes.len; 71 | } 72 | 73 | /// Redirect to the given location 74 | /// if something was written to the stream already it will be cleared 75 | pub fn redirect(self: *Response, location: []const u8) !void { 76 | try self.headers.put("Location", location); 77 | self.status = web.responses.FOUND; 78 | self.body.items.len = 0; // Erase anything that was written 79 | } 80 | 81 | pub fn deinit(self: *Response) void { 82 | self.headers.deinit(); 83 | self.body.deinit(); 84 | } 85 | }; 86 | 87 | test "response" { 88 | const allocator = std.testing.allocator; 89 | var response = try Response.initCapacity(allocator, 4096, 1096); 90 | response.prepare(); 91 | defer response.deinit(); 92 | _ = try response.stream.write("Hello world!\n"); 93 | try testing.expectEqualSlices(u8, "Hello world!\n", response.body.items); 94 | 95 | _ = try response.stream.print("{s}\n", .{"Testing!"}); 96 | try testing.expectEqualSlices(u8, "Hello world!\nTesting!\n", response.body.items); 97 | try response.headers.put("Content-Type", "Keep-Alive"); 98 | } 99 | 100 | test "response-fns" { 101 | testing.refAllDecls(Response); 102 | } 103 | -------------------------------------------------------------------------------- /src/cookies.zig: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------- // 2 | // Copyright (c) 2020, Jairus Martin. // 3 | // Distributed under the terms of the MIT License. // 4 | // The full license is in the file LICENSE, distributed with this software. // 5 | // -------------------------------------------------------------------------- // 6 | const std = @import("std"); 7 | const simd = @import("simd.zig"); 8 | const Allocator = std.mem.Allocator; 9 | const testing = std.testing; 10 | 11 | pub const Cookies = struct { 12 | pub const Cookie = struct { 13 | key: []const u8, 14 | value: []const u8, 15 | }; 16 | pub const List = std.ArrayList(Cookie); 17 | cookies: List, 18 | parsed: bool = false, 19 | 20 | pub fn init(allocator: Allocator) Cookies { 21 | return Cookies{ .cookies = List.init(allocator) }; 22 | } 23 | 24 | pub fn initCapacity(allocator: Allocator, capacity: usize) !Cookies { 25 | return Cookies{ 26 | .cookies = try List.initCapacity(allocator, capacity), 27 | }; 28 | } 29 | 30 | // Parse up to capacity cookies 31 | pub fn parse(self: *Cookies, header: []const u8) !void { 32 | var it = simd.split(u8, header, "; "); 33 | defer self.parsed = true; 34 | while (self.cookies.items.len < self.cookies.capacity) { 35 | const pair = it.next() orelse break; 36 | // Errors are ignored 37 | const pos = simd.indexOf(u8, pair, "=") orelse continue; 38 | const key = pair[0..pos]; 39 | const end = pos + 1; 40 | if (pair.len > end) { 41 | const value = pair[end..]; 42 | self.cookies.appendAssumeCapacity(Cookie{ .key = key, .value = value }); 43 | } 44 | } 45 | } 46 | 47 | pub fn lookup(self: Cookies, key: []const u8) !usize { 48 | for (self.cookies.items) |cookie, i| { 49 | if (std.mem.eql(u8, cookie.key, key)) return i; 50 | } 51 | return error.KeyError; 52 | } 53 | 54 | pub fn get(self: *Cookies, key: []const u8) ![]const u8 { 55 | const i = try self.lookup(key); 56 | return self.cookies.items[i].value; 57 | } 58 | 59 | pub fn getOptional(self: *Cookies, key: []const u8) ?[]const u8 { 60 | return self.get(key) catch null; 61 | } 62 | 63 | pub fn getDefault(self: *Cookies, key: []const u8, default: []const u8) []const u8 { 64 | return self.get(key) catch default; 65 | } 66 | 67 | pub fn contains(self: *Cookies, key: []const u8) bool { 68 | _ = self.lookup(key) catch { 69 | return false; 70 | }; 71 | return true; 72 | } 73 | 74 | pub fn reset(self: *Cookies) void { 75 | self.cookies.items.len = 0; 76 | self.parsed = false; 77 | } 78 | 79 | pub fn deinit(self: *Cookies) void { 80 | self.cookies.deinit(); 81 | } 82 | }; 83 | 84 | test "cookie-parse-api" { 85 | const header = "azk=ue1-5eb08aeed9a7401c9195cb933eb7c966"; 86 | var cookies = try Cookies.initCapacity(std.testing.allocator, 32); 87 | defer cookies.deinit(); 88 | try cookies.parse(header); 89 | 90 | // Not case sensitive 91 | try testing.expect(cookies.contains("azk")); 92 | try testing.expect(!cookies.contains("AZK")); 93 | 94 | try testing.expectEqualStrings("ue1-5eb08aeed9a7401c9195cb933eb7c966", try cookies.get("azk")); 95 | 96 | try testing.expectEqual(cookies.getOptional("user"), null); 97 | try testing.expectEqualStrings("default", cookies.getDefault("user", "default")); 98 | } 99 | 100 | test "cookie-parse-multiple" { 101 | const header = "S_9994987=6754579095859875029; A4=01fmFvgRnI09SF00000; u2=d1263d39-874b-4a89-86cd-a2ab0860ed4e3Zl040"; 102 | var cookies = try Cookies.initCapacity(std.testing.allocator, 32); 103 | defer cookies.deinit(); 104 | try cookies.parse(header); 105 | 106 | try testing.expectEqualStrings("6754579095859875029", try cookies.get("S_9994987")); 107 | 108 | try testing.expectEqualStrings("01fmFvgRnI09SF00000", try cookies.get("A4")); 109 | 110 | try testing.expectEqualStrings("d1263d39-874b-4a89-86cd-a2ab0860ed4e3Zl040", try cookies.get("u2")); 111 | } 112 | 113 | test "cookie-parse-empty-ignored" { 114 | const header = "S_9994987=6754579095859875029; ; u2=d1263d39-874b-4a89-86cd-a2ab0860ed4e3Zl040"; 115 | var cookies = try Cookies.initCapacity(std.testing.allocator, 32); 116 | defer cookies.deinit(); 117 | try cookies.parse(header); 118 | 119 | try testing.expectEqualStrings("6754579095859875029", try cookies.get("S_9994987")); 120 | 121 | try testing.expectEqualStrings("d1263d39-874b-4a89-86cd-a2ab0860ed4e3Zl040", try cookies.get("u2")); 122 | } 123 | -------------------------------------------------------------------------------- /example/templates/cover.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {s} 7 | 8 | 9 | 10 | 13 | 14 | 15 |
16 | 17 |
18 | 19 | 20 |
21 |
22 | 43 |
44 |
45 | 46 | 47 |
48 |

ZHP

49 |
50 |

51 | A HTTP/1.1 server written in Zig. 52 |

53 |
54 |
55 | TRY IT OUT 56 |
57 |
58 | 59 | 60 |
61 | 62 |

63 | Note: This site is served using ZHP behind an nginx reverse proxy. 64 | Please do not benchmark this site or the server will ban your IP for a day (you can try but have been warned). 65 |

66 |
67 | 68 | © 2020 | Created by CodeLV 69 | | Built with 70 | | Photo by coltsfan 71 | 72 |
73 | 74 |
75 |
76 | 77 |
78 |
79 | 80 |

Pages

81 | 92 |
93 |
94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /example/templates/chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Chat / Webocket Demo 7 | 8 | 9 | 10 | 11 |
12 |
13 | 19 |
20 |
21 | 22 |
23 |
24 |
25 |

Enter a username: 26 | 27 |

28 |
29 |
30 |
31 |
32 | 33 |
34 |
35 |
36 |

37 | 38 | 39 |

40 |

41 |
42 |
43 | 44 |
45 | © 2020 | Websocket demo served to you by ZHP 46 |
47 | 48 | 49 | 50 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /src/simd.zig: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------- // 2 | // Copyright (c) 2020, Jairus Martin. // 3 | // Distributed under the terms of the MIT License. // 4 | // The full license is in the file LICENSE, distributed with this software. // 5 | // -------------------------------------------------------------------------- // 6 | const std = @import("std"); 7 | 8 | pub fn copy(comptime T: type, dest: []T, source: []const T) void { 9 | const n = 32; // TODO: Adjust based on bitSizeOf T 10 | const V = @Vector(n, T); 11 | if (source.len < n) return std.mem.copy(T, dest, source); 12 | var end: usize = n; 13 | while (end < source.len) { 14 | const start = end - n; 15 | const source_chunk: V = source[start..end][0..n].*; 16 | const dest_chunk: *V = &@as(V, dest[start..end][0..n].*); 17 | dest_chunk.* = source_chunk; 18 | end = std.math.min(end + n, source.len); 19 | } 20 | } 21 | 22 | pub fn eql(comptime T: type, a: []const T, b: []const T) bool { 23 | const n = 32; 24 | const V8x32 = @Vector(n, T); 25 | if (a.len != b.len) return false; 26 | if (a.ptr == b.ptr) return true; 27 | if (a.len < n) { 28 | // Too small to fit, fallback to standard eql 29 | for (a) |item, index| { 30 | if (b[index] != item) return false; 31 | } 32 | } else { 33 | var end: usize = n; 34 | while (end < a.len) { 35 | const start = end - n; 36 | const a_chunk: V8x32 = a[start..end][0..n].*; 37 | const b_chunk: V8x32 = b[start..end][0..n].*; 38 | if (!@reduce(.And, a_chunk == b_chunk)) { 39 | return false; 40 | } 41 | end = std.math.min(end + n, a.len); 42 | } 43 | } 44 | return true; 45 | } 46 | 47 | pub fn lastIndexOf(comptime T: type, buf: []const u8, delimiter: []const u8) ?usize { 48 | const n = 32; 49 | const k = delimiter.len; 50 | const V8x32 = @Vector(n, T); 51 | const V1x32 = @Vector(n, u1); 52 | //const Vbx32 = @Vector(n, bool); 53 | const first = @splat(n, delimiter[0]); 54 | const last = @splat(n, delimiter[k - 1]); 55 | 56 | if (buf.len < n) { 57 | return std.mem.lastIndexOfPos(T, buf, 0, delimiter); 58 | } 59 | 60 | var start: usize = buf.len - n; 61 | while (start > 0) { 62 | const end = start + n; 63 | const last_end = std.math.min(end + k - 1, buf.len); 64 | const last_start = last_end - n; 65 | 66 | // Look for the first character in the delimter 67 | const first_chunk: V8x32 = buf[start..end][0..n].*; 68 | const last_chunk: V8x32 = buf[last_start..last_end][0..n].*; 69 | const mask = @bitCast(V1x32, first == first_chunk) & @bitCast(V1x32, last == last_chunk); 70 | if (@reduce(.Or, mask) != 0) { 71 | // TODO: Use __builtin_ctz??? 72 | var i: usize = n; 73 | while (i > 0) { 74 | i -= 1; 75 | if (mask[i] == 1 and eql(T, buf[start + i .. start + i + k], delimiter)) { 76 | return start + i; 77 | } 78 | } 79 | } 80 | start = std.math.max(start - n, 0); 81 | } 82 | return null; // Not found 83 | } 84 | 85 | pub fn indexOf(comptime T: type, buf: []const u8, delimiter: []const u8) ?usize { 86 | return indexOfPos(T, buf, 0, delimiter); 87 | } 88 | 89 | pub fn indexOfPos(comptime T: type, buf: []const u8, start_index: usize, delimiter: []const u8) ?usize { 90 | const n = 32; 91 | const k = delimiter.len; 92 | const V8x32 = @Vector(n, T); 93 | const V1x32 = @Vector(n, u1); 94 | const Vbx32 = @Vector(n, bool); 95 | const first = @splat(n, delimiter[0]); 96 | const last = @splat(n, delimiter[k - 1]); 97 | 98 | var end: usize = start_index + n; 99 | var start: usize = end - n; 100 | while (end < buf.len) { 101 | start = end - n; 102 | const last_end = std.math.min(end + k - 1, buf.len); 103 | const last_start = last_end - n; 104 | 105 | // Look for the first character in the delimter 106 | const first_chunk: V8x32 = buf[start..end][0..n].*; 107 | const last_chunk: V8x32 = buf[last_start..last_end][0..n].*; 108 | const mask = @bitCast(V1x32, first == first_chunk) & @bitCast(V1x32, last == last_chunk); 109 | if (@reduce(.Or, mask) != 0) { 110 | // TODO: Use __builtin_clz??? 111 | for (@as([n]bool, @bitCast(Vbx32, mask))) |match, i| { 112 | if (match and eql(T, buf[start + i .. start + i + k], delimiter)) { 113 | return start + i; 114 | } 115 | } 116 | } 117 | end = std.math.min(end + n, buf.len); 118 | } 119 | if (start < buf.len) return std.mem.indexOfPos(T, buf, start_index, delimiter); 120 | return null; // Not found 121 | } 122 | 123 | pub fn split(comptime T: type, buffer: []const T, delimiter: []const T) SplitIterator(T) { 124 | const Iterator = SplitIterator(T); 125 | return Iterator{ .buffer = buffer, .delimiter = delimiter }; 126 | } 127 | 128 | pub fn SplitIterator(comptime T: type) type { 129 | return struct { 130 | const Self = @This(); 131 | index: ?usize = 0, 132 | buffer: []const T, 133 | delimiter: []const T, 134 | 135 | /// Returns a slice of the next field, or null if splitting is complete. 136 | pub fn next(self: *Self) ?[]const T { 137 | const start = self.index orelse return null; 138 | const end = if (indexOfPos(T, self.buffer, start, self.delimiter)) |delim_start| blk: { 139 | self.index = delim_start + self.delimiter.len; 140 | break :blk delim_start; 141 | } else blk: { 142 | self.index = null; 143 | break :blk self.buffer.len; 144 | }; 145 | return self.buffer[start..end]; 146 | } 147 | 148 | /// Returns a slice of the remaining bytes. Does not affect iterator state. 149 | pub fn rest(self: Self) []const T { 150 | const end = self.buffer.len; 151 | const start = self.index orelse end; 152 | return self.buffer[start..end]; 153 | } 154 | }; 155 | } 156 | -------------------------------------------------------------------------------- /tests/search.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn main() anyerror!void { 4 | const find = "\n\n"; // The \r are already stripped out 5 | const n = 275; 6 | const buf = @embedFile("./bigger.txt"); 7 | //const buf = @embedFile("./http-requests.txt"); 8 | //const n = 55; 9 | //const find = "\r\n\r\n"; // The \r are already stripped out 10 | 11 | var ptrs1: [n][]const u8 = undefined; 12 | var ptrs2: [n][]const u8 = undefined; 13 | 14 | var timer = try std.time.Timer.start(); 15 | var it1 = std.mem.split(buf, find); 16 | var cnt: usize = 0; 17 | 18 | while (it1.next()) |req| { 19 | ptrs1[cnt] = req; 20 | cnt += 1; 21 | } 22 | const t1 = timer.lap(); 23 | std.testing.expectEqual(cnt, n); 24 | 25 | var it2 = split(buf, find); 26 | cnt = 0; 27 | while (it2.next()) |req| { 28 | ptrs2[cnt] = req; 29 | cnt += 1; 30 | } 31 | const t2 = timer.lap(); 32 | 33 | std.testing.expectEqual(cnt, n); 34 | std.testing.expectEqual(ptrs1, ptrs2); 35 | 36 | timer.reset(); 37 | for (ptrs1) |src, i| { 38 | const dst = ptrs2[i]; 39 | std.testing.expect(std.mem.eql(u8, src, dst)); 40 | } 41 | const t3 = timer.lap(); 42 | for (ptrs1) |src, i| { 43 | const dst = ptrs2[i]; 44 | std.testing.expect(eql(u8, src, dst)); 45 | } 46 | const t4 = timer.lap(); 47 | 48 | var dest: [4096]u8 = undefined; 49 | timer.reset(); 50 | for (ptrs1) |src, i| { 51 | std.mem.copy(u8, &dest, src); 52 | } 53 | const t5 = timer.lap(); 54 | 55 | for (ptrs1) |src, i| { 56 | copy(u8, &dest, src); 57 | } 58 | const t6 = timer.lap(); 59 | 60 | std.log.warn("split std: {}ns", .{t1}); 61 | std.log.warn("split SIMD: {}ns", .{t2}); 62 | std.log.warn("split diff: {}", .{@intToFloat(f32, t1) / @intToFloat(f32, t2)}); 63 | 64 | std.log.warn("eql std: {}ns", .{t3}); 65 | std.log.warn("eql SIMD: {}ns", .{t4}); 66 | std.log.warn("eql diff: {}", .{@intToFloat(f32, t3) / @intToFloat(f32, t4)}); 67 | 68 | std.log.warn("copy std: {}ns", .{t5}); 69 | std.log.warn("copy SIMD: {}ns", .{t6}); 70 | std.log.warn("copy diff: {}", .{@intToFloat(f32, t5) / @intToFloat(f32, t6)}); 71 | } 72 | 73 | pub inline fn copy(comptime T: type, dest: []T, source: []const T) void { 74 | const n = 32; // TODO: Adjust based on bitSizeOf T 75 | const V = @Vector(n, T); 76 | if (source.len < n) return std.mem.copy(T, dest, source); 77 | var end: usize = n; 78 | while (end < source.len) { 79 | const start = end - n; 80 | const source_chunk: V = source[start..end][0..n].*; 81 | const dest_chunk = &@as(V, dest[start..end][0..n].*); 82 | dest_chunk.* = source_chunk; 83 | end = std.math.min(end + n, source.len); 84 | } 85 | } 86 | 87 | pub inline fn eql(comptime T: type, a: []const T, b: []const T) bool { 88 | const n = 32; 89 | const V8x32 = @Vector(n, T); 90 | if (a.len != b.len) return false; 91 | if (a.ptr == b.ptr) return true; 92 | if (a.len < n) { 93 | // Too small to fit, fallback to standard eql 94 | for (a) |item, index| { 95 | if (b[index] != item) return false; 96 | } 97 | } else { 98 | var end: usize = n; 99 | while (end < a.len) { 100 | const start = end - n; 101 | const a_chunk: V8x32 = a[start..end][0..n].*; 102 | const b_chunk: V8x32 = b[start..end][0..n].*; 103 | if (!@reduce(.And, a_chunk == b_chunk)) { 104 | return false; 105 | } 106 | end = std.math.min(end + n, a.len); 107 | } 108 | } 109 | return true; 110 | } 111 | 112 | pub fn indexOf(comptime T: type, buf: []const u8, delimiter: []const u8) ?usize { 113 | return indexOfAnyPos(T, buf, 0, delimiter); 114 | } 115 | 116 | pub fn indexOfAnyPos(comptime T: type, buf: []const T, start_index: usize, delimiter: []const T) ?usize { 117 | const n = 32; 118 | const k = delimiter.len; 119 | const V8x32 = @Vector(n, T); 120 | const V1x32 = @Vector(n, u1); 121 | const Vbx32 = @Vector(n, bool); 122 | const first = @splat(n, delimiter[0]); 123 | const last = @splat(n, delimiter[k - 1]); 124 | 125 | if (buf.len < n) { 126 | return std.mem.indexOfAnyPos(T, buf, start_index, delimiter); 127 | } 128 | 129 | var end: usize = start_index + n; 130 | while (end < buf.len) { 131 | const start = end - n; 132 | const last_end = std.math.min(end + k - 1, buf.len); 133 | const last_start = last_end - n; 134 | 135 | // Look for the first character in the delimter 136 | const first_chunk: V8x32 = buf[start..end][0..n].*; 137 | const last_chunk: V8x32 = buf[last_start..last_end][0..n].*; 138 | const mask = @bitCast(V1x32, first == first_chunk) & @bitCast(V1x32, last == last_chunk); 139 | if (@reduce(.Or, mask) != 0) { 140 | for (@as([n]bool, @bitCast(Vbx32, mask))) |match, i| { 141 | if (match and eql(T, buf[start + i .. start + i + k], delimiter)) { 142 | return start + i; 143 | } 144 | } 145 | } 146 | end = std.math.min(end + n, buf.len); 147 | } 148 | return null; // Not found 149 | } 150 | 151 | pub fn split(buffer: []const u8, delimiter: []const u8) SplitIterator { 152 | return SplitIterator{ .buffer = buffer, .delimiter = delimiter }; 153 | } 154 | 155 | pub const SplitIterator = struct { 156 | index: ?usize = 0, 157 | buffer: []const u8, 158 | delimiter: []const u8, 159 | 160 | /// Returns a slice of the next field, or null if splitting is complete. 161 | pub fn next(self: *SplitIterator) ?[]const u8 { 162 | const start = self.index orelse return null; 163 | const end = if (indexOfAnyPos(u8, self.buffer, start, self.delimiter)) |delim_start| blk: { 164 | self.index = delim_start + self.delimiter.len; 165 | break :blk delim_start; 166 | } else blk: { 167 | self.index = null; 168 | break :blk self.buffer.len; 169 | }; 170 | return self.buffer[start..end]; 171 | } 172 | 173 | /// Returns a slice of the remaining bytes. Does not affect iterator state. 174 | pub fn rest(self: SplitIterator) []const u8 { 175 | const end = self.buffer.len; 176 | const start = self.index orelse end; 177 | return self.buffer[start..end]; 178 | } 179 | }; 180 | -------------------------------------------------------------------------------- /src/status.zig: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------- // 2 | // Copyright (c) 2019-2020, Jairus Martin. // 3 | // Distributed under the terms of the MIT License. // 4 | // The full license is in the file LICENSE, distributed with this software. // 5 | // -------------------------------------------------------------------------- // 6 | const std = @import("std"); 7 | const testing = std.testing; 8 | 9 | // Supported, IANA-registered status codes available 10 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status 11 | pub const Status = struct { 12 | code: u16, 13 | phrase: []const u8, 14 | description: []const u8, 15 | 16 | pub fn create(code: u16, phrase: []const u8, desc: []const u8) Status { 17 | return Status{ 18 | .code = code, 19 | .phrase = phrase, 20 | .description = desc, 21 | }; 22 | } 23 | }; 24 | 25 | pub const create = Status.create; 26 | 27 | // Informational 28 | pub const CONTINUE = create(100, "Continue", "Request received, please continue"); 29 | pub const SWITCHING_PROTOCOLS = create(101, "Switching Protocols", "Switching to new protocol; obey Upgrade header"); 30 | pub const PROCESSING = create(102, "Processing", ""); 31 | pub const EARLY_HINTS = create(103, "Early Hints", ""); 32 | 33 | // Success 34 | pub const OK = create(200, "OK", "Request fulfilled, document follows"); 35 | pub const CREATED = create(201, "Created", "Document created, URL follows"); 36 | pub const ACCEPTED = create(202, "Accepted", "Request accepted, processing continues off-line"); 37 | pub const NON_AUTHORITATIVE_INFORMATION = create(203, "Non-Authoritative Information", "Request fulfilled from cache"); 38 | pub const NO_CONTENT = create(204, "No Content", "Request fulfilled, nothing follows"); 39 | pub const RESET_CONTENT = create(205, "Reset Content", "Clear input form for further input"); 40 | pub const PARTIAL_CONTENT = create(206, "Partial Content", "Partial content follows"); 41 | pub const MULTI_STATUS = create(207, "Multi-Status", ""); 42 | pub const ALREADY_REPORTED = create(208, "Already Reported", ""); 43 | pub const IM_USED = create(226, "IM Used", ""); 44 | 45 | // Redirection 46 | pub const MULTIPLE_CHOICES = create(300, "Multiple Choices", "Object has several resources -- see URI list"); 47 | pub const MOVED_PERMANENTLY = create(301, "Moved Permanently", "Object moved permanently -- see URI list"); 48 | pub const FOUND = create(302, "Found", "Object moved temporarily -- see URI list"); 49 | pub const SEE_OTHER = create(303, "See Other", "Object moved -- see Method and URL list"); 50 | pub const NOT_MODIFIED = create(304, "Not Modified", "Document has not changed since given time"); 51 | pub const USE_PROXY = create(305, "Use Proxy", "You must use proxy specified in Location to access this resource"); 52 | pub const TEMPORARY_REDIRECT = create(307, "Temporary Redirect", "Object moved temporarily -- see URI list"); 53 | pub const PERMANENT_REDIRECT = create(308, "Permanent Redirect", "Object moved permanently -- see URI list"); 54 | 55 | // Client error 56 | pub const BAD_REQUEST = create(400, "Bad Request", "Bad request syntax or unsupported method"); 57 | pub const UNAUTHORIZED = create(401, "Unauthorized", "No permission -- see authorization schemes"); 58 | pub const PAYMENT_REQUIRED = create(402, "Payment Required", "No payment -- see charging schemes"); 59 | pub const FORBIDDEN = create(403, "Forbidden", "Request forbidden -- authorization will not help"); 60 | pub const NOT_FOUND = create(404, "Not Found", "Nothing matches the given URI"); 61 | pub const METHOD_NOT_ALLOWED = create(405, "Method Not Allowed", "Specified method is invalid for this resource"); 62 | pub const NOT_ACCEPTABLE = create(406, "Not Acceptable", "URI not available in preferred format"); 63 | pub const PROXY_AUTHENTICATION_REQUIRED = create(407, "Proxy Authentication Required", "You must authenticate with this proxy before proceeding"); 64 | pub const REQUEST_TIMEOUT = create(408, "Request Timeout", "Request timed out; try again later"); 65 | pub const CONFLICT = create(409, "Conflict", "Request conflict"); 66 | pub const GONE = create(410, "Gone", "URI no longer exists and has been permanently removed"); 67 | pub const LENGTH_REQUIRED = create(411, "Length Required", "Client must specify Content-Length"); 68 | pub const PRECONDITION_FAILED = create(412, "Precondition Failed", "Precondition in headers is false"); 69 | pub const REQUEST_ENTITY_TOO_LARGE = create(413, "Request Entity Too Large", "Entity is too large"); 70 | pub const REQUEST_URI_TOO_LONG = create(414, "Request-URI Too Long", "URI is too long"); 71 | pub const UNSUPPORTED_MEDIA_TYPE = create(415, "Unsupported Media Type", "Entity body in unsupported format"); 72 | pub const REQUESTED_RANGE_NOT_SATISFIABLE = create(416, "Requested Range Not Satisfiable", "Cannot satisfy request range"); 73 | pub const EXPECTATION_FAILED = create(417, "Expectation Failed", "Expect condition could not be satisfied"); 74 | pub const MISDIRECTED_REQUEST = create(421, "Misdirected Request", "Server is not able to produce a response"); 75 | pub const UNPROCESSABLE_ENTITY = create(422, "Unprocessable Entity", ""); 76 | pub const LOCKED = create(423, "Locked", ""); 77 | pub const FAILED_DEPENDENCY = create(424, "Failed Dependency", ""); 78 | pub const TOO_EARLY = create(425, "Too Early", ""); 79 | pub const UPGRADE_REQUIRED = create(426, "Upgrade Required", ""); 80 | pub const PRECONDITION_REQUIRED = create(428, "Precondition Required", "The origin server requires the request to be conditional"); 81 | pub const TOO_MANY_REQUESTS = create(429, "Too Many Requests", "The user has sent too many requests in a given amount of time (\"rate limiting\")"); 82 | pub const REQUEST_HEADER_FIELDS_TOO_LARGE = create(431, "Request Header Fields Too Large", "The server is unwilling to process the request because its header fields are too large"); 83 | pub const UNAVAILABLE_FOR_LEGAL_REASONS = create(451, "Unavailable For Legal Reasons", "The server is denying access to the resource as a consequence of a legal demand"); 84 | 85 | // server errors 86 | pub const INTERNAL_SERVER_ERROR = create(500, "Internal Server Error", "Server got itself in trouble"); 87 | pub const NOT_IMPLEMENTED = create(501, "Not Implemented", "Server does not support this operation"); 88 | pub const BAD_GATEWAY = create(502, "Bad Gateway", "Invalid responses from another server/proxy"); 89 | pub const SERVICE_UNAVAILABLE = create(503, "Service Unavailable", "The server cannot process the request due to a high load"); 90 | pub const GATEWAY_TIMEOUT = create(504, "Gateway Timeout", "The gateway server did not receive a timely response"); 91 | pub const HTTP_VERSION_NOT_SUPPORTED = create(505, "HTTP Version Not Supported", "Cannot fulfill request"); 92 | pub const VARIANT_ALSO_NEGOTIATES = create(506, "Variant Also Negotiates", ""); 93 | pub const INSUFFICIENT_STORAGE = create(507, "Insufficient Storage", ""); 94 | pub const LOOP_DETECTED = create(508, "Loop Detected", ""); 95 | pub const NOT_EXTENDED = create(510, "Not Extended", ""); 96 | pub const NETWORK_AUTHENTICATION_REQUIRED = create(511, "Network Authentication Required", "The client needs to authenticate to gain network access"); 97 | 98 | /// Lookup the status for the given code 99 | pub fn get(status_code: u16) ?Status { 100 | return switch (status_code) { 101 | 100 => CONTINUE, 102 | 101 => SWITCHING_PROTOCOLS, 103 | 102 => PROCESSING, 104 | 103 => EARLY_HINTS, 105 | 200 => OK, 106 | 201 => CREATED, 107 | 202 => ACCEPTED, 108 | 203 => NON_AUTHORITATIVE_INFORMATION, 109 | 204 => NO_CONTENT, 110 | 205 => RESET_CONTENT, 111 | 206 => PARTIAL_CONTENT, 112 | 207 => MULTI_STATUS, 113 | 208 => ALREADY_REPORTED, 114 | 226 => IM_USED, 115 | 300 => MULTIPLE_CHOICES, 116 | 301 => MOVED_PERMANENTLY, 117 | 302 => FOUND, 118 | 303 => SEE_OTHER, 119 | 304 => NOT_MODIFIED, 120 | 305 => USE_PROXY, 121 | 307 => TEMPORARY_REDIRECT, 122 | 308 => PERMANENT_REDIRECT, 123 | 400 => BAD_REQUEST, 124 | 401 => UNAUTHORIZED, 125 | 402 => PAYMENT_REQUIRED, 126 | 403 => FORBIDDEN, 127 | 404 => NOT_FOUND, 128 | 405 => METHOD_NOT_ALLOWED, 129 | 406 => NOT_ACCEPTABLE, 130 | 407 => PROXY_AUTHENTICATION_REQUIRED, 131 | 408 => REQUEST_TIMEOUT, 132 | 409 => CONFLICT, 133 | 410 => GONE, 134 | 411 => LENGTH_REQUIRED, 135 | 412 => PRECONDITION_FAILED, 136 | 413 => REQUEST_ENTITY_TOO_LARGE, 137 | 414 => REQUEST_URI_TOO_LONG, 138 | 415 => UNSUPPORTED_MEDIA_TYPE, 139 | 416 => REQUESTED_RANGE_NOT_SATISFIABLE, 140 | 417 => EXPECTATION_FAILED, 141 | 422 => UNPROCESSABLE_ENTITY, 142 | 423 => LOCKED, 143 | 424 => FAILED_DEPENDENCY, 144 | 425 => TOO_EARLY, 145 | 426 => UPGRADE_REQUIRED, 146 | 428 => PRECONDITION_REQUIRED, 147 | 429 => TOO_MANY_REQUESTS, 148 | 431 => REQUEST_HEADER_FIELDS_TOO_LARGE, 149 | 500 => INTERNAL_SERVER_ERROR, 150 | 501 => NOT_IMPLEMENTED, 151 | 502 => BAD_GATEWAY, 152 | 503 => SERVICE_UNAVAILABLE, 153 | 504 => GATEWAY_TIMEOUT, 154 | 505 => HTTP_VERSION_NOT_SUPPORTED, 155 | 506 => VARIANT_ALSO_NEGOTIATES, 156 | 507 => INSUFFICIENT_STORAGE, 157 | 508 => LOOP_DETECTED, 158 | 510 => NOT_EXTENDED, 159 | 511 => NETWORK_AUTHENTICATION_REQUIRED, 160 | else => null, 161 | }; 162 | } 163 | 164 | /// Lookup the status for the given code or create one with the given phrase 165 | pub fn getOrCreate(status_code: u16, phrase: []const u8) Status { 166 | return get(status_code) orelse create(status_code, phrase, ""); 167 | } 168 | 169 | test "Status.create" { 170 | const status = create(200, "OK", "Request fulfilled, document follows"); 171 | try testing.expectEqual(status.code, OK.code); 172 | try testing.expectEqualSlices(u8, status.phrase, OK.phrase); 173 | try testing.expectEqualSlices(u8, status.description, OK.description); 174 | } 175 | 176 | test "Status.get" { 177 | try testing.expectEqual(get(200).?, OK); 178 | try testing.expectEqual(get(600), null); 179 | const status = getOrCreate(600, "Unknown"); 180 | try testing.expectEqual(status.code, 600); 181 | try testing.expectEqualSlices(u8, status.phrase, "Unknown"); 182 | } 183 | -------------------------------------------------------------------------------- /src/websocket.zig: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------- // 2 | // Copyright (c) 2020, Jairus Martin. // 3 | // Distributed under the terms of the MIT License. // 4 | // The full license is in the file LICENSE, distributed with this software. // 5 | // -------------------------------------------------------------------------- // 6 | const std = @import("std"); 7 | const web = @import("zhp.zig"); 8 | const builtin = @import("builtin"); 9 | const log = std.log; 10 | const native_endian = builtin.target.cpu.arch.endian(); 11 | 12 | pub const Opcode = enum(u4) { 13 | Continue = 0x0, 14 | Text = 0x1, 15 | Binary = 0x2, 16 | Res3 = 0x3, 17 | Res4 = 0x4, 18 | Res5 = 0x5, 19 | Res6 = 0x6, 20 | Res7 = 0x7, 21 | Close = 0x8, 22 | Ping = 0x9, 23 | Pong = 0xA, 24 | ResB = 0xB, 25 | ResC = 0xC, 26 | ResD = 0xD, 27 | ResE = 0xE, 28 | ResF = 0xF, 29 | 30 | pub fn isControl(opcode: Opcode) bool { 31 | return @enumToInt(opcode) & 0x8 != 0; 32 | } 33 | }; 34 | 35 | pub const WebsocketHeader = packed struct { 36 | len: u7, 37 | mask: bool, 38 | opcode: Opcode, 39 | rsv3: u1 = 0, 40 | rsv2: u1 = 0, 41 | compressed: bool = false, // rsv1 42 | final: bool = true, 43 | 44 | pub fn packLength(length: usize) u7 { 45 | return switch (length) { 46 | 0...126 => @truncate(u7, length), 47 | 127...0xFFFF => 126, 48 | else => 127, 49 | }; 50 | } 51 | }; 52 | 53 | pub const WebsocketDataFrame = struct { 54 | header: WebsocketHeader, 55 | mask: [4]u8 = undefined, 56 | data: []const u8, 57 | 58 | pub fn isValid(dataframe: WebsocketDataFrame) bool { 59 | // Validate control frame 60 | if (dataframe.header.opcode.isControl()) { 61 | if (!dataframe.header.final) { 62 | return false; // Control frames cannot be fragmented 63 | } 64 | if (dataframe.data.len > 125) { 65 | return false; // Control frame payloads cannot exceed 125 bytes 66 | } 67 | } 68 | 69 | // Validate header len field 70 | const expected = switch (dataframe.data.len) { 71 | 0...126 => dataframe.data.len, 72 | 127...0xFFFF => 126, 73 | else => 127, 74 | }; 75 | return dataframe.header.len == expected; 76 | } 77 | }; 78 | 79 | // Create a buffered writer 80 | // TODO: This will still split packets 81 | pub fn Writer(comptime size: usize, comptime opcode: Opcode) type { 82 | const WriterType = switch (opcode) { 83 | .Text => Websocket.TextFrameWriter, 84 | .Binary => Websocket.BinaryFrameWriter, 85 | else => @compileError("Unsupported writer opcode"), 86 | }; 87 | return std.io.BufferedWriter(size, WriterType); 88 | } 89 | 90 | pub const Websocket = struct { 91 | pub const WriteError = error{ 92 | InvalidMessage, 93 | MessageTooLarge, 94 | EndOfStream, 95 | } || std.fs.File.WriteError; 96 | 97 | request: *web.Request, 98 | response: *web.Response, 99 | io: *web.IOStream, 100 | err: ?anyerror = null, 101 | 102 | // ------------------------------------------------------------------------ 103 | // Stream API 104 | // ------------------------------------------------------------------------ 105 | pub const TextFrameWriter = std.io.Writer(*Websocket, WriteError, Websocket.writeText); 106 | pub const BinaryFrameWriter = std.io.Writer(*Websocket, WriteError, Websocket.writeBinary); 107 | 108 | // A buffered writer that will buffer up to size bytes before writing out 109 | pub fn writer(self: *Websocket, comptime size: usize, comptime opcode: Opcode) Writer(size, opcode) { 110 | const BufferedWriter = Writer(size, opcode); 111 | const frame_writer = switch (opcode) { 112 | .Text => TextFrameWriter{ .context = self }, 113 | .Binary => BinaryFrameWriter{ .context = self }, 114 | else => @compileError("Unsupported writer type"), 115 | }; 116 | return BufferedWriter{ .unbuffered_writer = frame_writer }; 117 | } 118 | 119 | // Close and send the status 120 | pub fn close(self: Websocket, code: u16) !void { 121 | const c = if (native_endian == .Big) code else @byteSwap(u16, code); 122 | const data = @bitCast([2]u8, c); 123 | _ = try self.writeMessage(.Close, &data); 124 | } 125 | 126 | // ------------------------------------------------------------------------ 127 | // Low level API 128 | // ------------------------------------------------------------------------ 129 | 130 | // Flush any buffered data out the underlying stream 131 | pub fn flush(self: *Websocket) !void { 132 | try self.io.flush(); 133 | } 134 | 135 | pub fn writeText(self: *Websocket, data: []const u8) !usize { 136 | return self.writeMessage(.Text, data); 137 | } 138 | 139 | pub fn writeBinary(self: *Websocket, data: []const u8) !usize { 140 | return self.writeMessage(.Binary, data); 141 | } 142 | 143 | // Write a final message packet with the given opcode 144 | pub fn writeMessage(self: Websocket, opcode: Opcode, message: []const u8) !usize { 145 | return self.writeSplitMessage(opcode, true, message); 146 | } 147 | 148 | // Write a message packet with the given opcode and final flag 149 | pub fn writeSplitMessage(self: Websocket, opcode: Opcode, final: bool, message: []const u8) !usize { 150 | return self.writeDataFrame(WebsocketDataFrame{ 151 | .header = WebsocketHeader{ 152 | .final = final, 153 | .opcode = opcode, 154 | .mask = false, // Server to client is not masked 155 | .len = WebsocketHeader.packLength(message.len), 156 | }, 157 | .data = message, 158 | }); 159 | } 160 | 161 | // Write a raw data frame 162 | pub fn writeDataFrame(self: Websocket, dataframe: WebsocketDataFrame) !usize { 163 | const stream = self.io.writer(); 164 | 165 | if (!dataframe.isValid()) return error.InvalidMessage; 166 | 167 | try stream.writeIntBig(u16, @bitCast(u16, dataframe.header)); 168 | 169 | // Write extended length if needed 170 | const n = dataframe.data.len; 171 | switch (n) { 172 | 0...126 => {}, // Included in header 173 | 127...0xFFFF => try stream.writeIntBig(u16, @truncate(u16, n)), 174 | else => try stream.writeIntBig(u64, n), 175 | } 176 | 177 | // TODO: Handle compression 178 | if (dataframe.header.compressed) return error.InvalidMessage; 179 | 180 | if (dataframe.header.mask) { 181 | const mask = &dataframe.mask; 182 | try stream.writeAll(mask); 183 | 184 | // Encode 185 | for (dataframe.data) |c, i| { 186 | try stream.writeByte(c ^ mask[i % 4]); 187 | } 188 | } else { 189 | try stream.writeAll(dataframe.data); 190 | } 191 | 192 | try self.io.flush(); 193 | 194 | return dataframe.data.len; 195 | } 196 | 197 | pub fn readDataFrame(self: Websocket) !WebsocketDataFrame { 198 | // Read and retry if we hit the end of the stream buffer 199 | var start = self.io.readCount(); 200 | while (true) { 201 | return self.readDataFrameInBuffer() catch |err| switch (err) { 202 | error.EndOfBuffer => { 203 | // TODO: This can make the request buffer invalid 204 | const n = try self.io.shiftAndFillBuffer(start); 205 | if (n == 0) return error.EndOfStream; 206 | start = 0; 207 | continue; 208 | }, 209 | else => return err, 210 | }; 211 | } 212 | } 213 | 214 | // Read assuming everything can fit before the stream hits the end of 215 | // it's buffer 216 | pub fn readDataFrameInBuffer(self: Websocket) !WebsocketDataFrame { 217 | const stream = self.io; 218 | 219 | const header = try stream.readType(WebsocketHeader, .Big); 220 | 221 | if (header.rsv2 != 0 or header.rsv3 != 0) { 222 | log.debug("Websocket reserved bits set! {}", .{header}); 223 | return error.InvalidMessage; // Reserved bits are not yet used 224 | } 225 | 226 | if (!header.mask) { 227 | log.debug("Websocket client mask header not set! {}", .{header}); 228 | return error.InvalidMessage; // Expected a client message! 229 | } 230 | 231 | if (header.opcode.isControl() and (header.len >= 126 or !header.final)) { 232 | log.debug("Websocket control message is invalid! {}", .{header}); 233 | return error.InvalidMessage; // Abort, frame is invalid 234 | } 235 | 236 | // Decode length 237 | const length: u64 = switch (header.len) { 238 | 0...125 => header.len, 239 | 126 => try stream.readType(u16, .Big), 240 | 127 => blk: { 241 | const l = try stream.readType(u64, .Big); 242 | // Most significant bit must be 0 243 | if (l >> 63 == 1) { 244 | log.debug("Websocket is out of range!", .{}); 245 | return error.InvalidMessage; 246 | } 247 | break :blk l; 248 | }, 249 | }; 250 | 251 | // TODO: Make configurable 252 | if (length > stream.in_buffer.len) { 253 | try self.close(1009); // Abort 254 | return error.MessageTooLarge; 255 | } else if (length + stream.readCount() > stream.in_buffer.len) { 256 | return error.EndOfBuffer; // Need to retry 257 | } 258 | 259 | const start: usize = if (header.mask) 4 else 0; 260 | const end = start + length; 261 | 262 | // Keep reading until it's filled 263 | while (stream.amountBuffered() < end) { 264 | try stream.fillBuffer(); 265 | } 266 | 267 | const buf = stream.readBuffered(); 268 | defer stream.skipBytes(end); 269 | 270 | const mask: [4]u8 = if (header.mask) buf[0..4].* else undefined; 271 | const data = buf[start..end]; 272 | if (header.mask) { 273 | // Decode data in place 274 | for (data) |c, i| { 275 | data[i] = c ^ mask[i % 4]; 276 | } 277 | } 278 | 279 | return WebsocketDataFrame{ 280 | .header = header, 281 | .mask = mask, 282 | .data = data, 283 | }; 284 | } 285 | }; 286 | -------------------------------------------------------------------------------- /src/template.zig: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------- // 2 | // Copyright (c) 2020, Jairus Martin. // 3 | // Distributed under the terms of the MIT License. // 4 | // The full license is in the file LICENSE, distributed with this software. // 5 | // -------------------------------------------------------------------------- // 6 | const std = @import("std"); 7 | const mem = std.mem; 8 | const simd = @import("simd.zig"); 9 | const testing = std.testing; 10 | 11 | const Section = struct { 12 | pub const Type = enum { 13 | Template, 14 | Variable, 15 | Content, 16 | Yield, 17 | }; 18 | content: []const u8, 19 | type: Type, 20 | start: usize, 21 | end: usize, 22 | 23 | pub fn is(comptime self: Section, comptime name: []const u8) bool { 24 | return self.type == .Yield and mem.eql(u8, self.content, name); 25 | } 26 | 27 | pub fn render(comptime self: Section, context: anytype, stream: anytype) @TypeOf(stream).Error!void { 28 | switch (self.type) { 29 | .Content => { 30 | try stream.writeAll(self.content); 31 | }, 32 | .Variable => { 33 | // TODO: Support arbitrary lookups 34 | const v = 35 | if (comptime mem.eql(u8, self.content, "self")) 36 | context 37 | else if (comptime mem.indexOf(u8, self.content, ".")) |offset| 38 | @field(@field(context, self.content[0..offset]), self.content[offset + 1 ..]) 39 | else 40 | @field(context, self.content); 41 | 42 | // TODO: Escape 43 | const T = @TypeOf(v); 44 | switch (@typeInfo(T)) { 45 | .ComptimeInt, .Int, .ComptimeFloat, .Float => { 46 | // {s} says unknown format string?? 47 | try stream.print("{}", .{v}); 48 | }, 49 | else => { 50 | try stream.print("{s}", .{v}); 51 | }, 52 | } 53 | }, 54 | .Template => { 55 | // TODO: Support sub contexts... 56 | const subtemplate = @embedFile(self.content); 57 | try stream.writeAll(subtemplate); 58 | }, 59 | .Yield => {}, 60 | } 61 | } 62 | }; 63 | 64 | pub fn parse(comptime Context: type, comptime template: []const u8) []const Section { 65 | @setEvalBranchQuota(100000); 66 | _ = Context; 67 | 68 | // Count number of sections this will probably be off 69 | comptime var max_sections: usize = 2; 70 | comptime { 71 | var vars = simd.split(u8, template, "{{"); 72 | while (vars.next()) |i| { 73 | _ = i; 74 | max_sections += 1; 75 | } 76 | var blocks = simd.split(u8, template, "{%"); 77 | while (blocks.next()) |i| { 78 | _ = i; 79 | max_sections += 1; 80 | } 81 | } 82 | 83 | // Now parse each section 84 | comptime var sections: [max_sections]Section = undefined; 85 | comptime var pos: usize = 0; 86 | comptime var index: usize = 0; 87 | while (simd.indexOfPos(u8, template, pos, "{")) |i| { 88 | if (i != pos) { 89 | // Content before 90 | sections[index] = Section{ .content = template[pos..i], .type = .Content, .start = pos, .end = i }; 91 | index += 1; 92 | } 93 | 94 | const remainder = template[i..]; 95 | if (mem.startsWith(u8, remainder, "{{")) { 96 | const start = i + 2; 97 | if (simd.indexOfPos(u8, template, start, "}}")) |end| { 98 | const format = std.mem.trim(u8, template[start..end], " "); 99 | pos = end + 2; 100 | sections[index] = Section{ 101 | .content = format, 102 | .type = .Variable, 103 | .start = i, 104 | .end = pos, 105 | }; 106 | index += 1; 107 | continue; 108 | } 109 | @compileError("Incomplete variable expression"); 110 | } else if (mem.startsWith(u8, remainder, "{%")) { 111 | if (mem.startsWith(u8, remainder, "{% yield ")) { 112 | const start = i + 9; 113 | if (simd.indexOfPos(u8, template, start, "%}")) |end| { 114 | pos = end + 2; 115 | sections[index] = Section{ 116 | .content = mem.trim(u8, template[start..end], " "), 117 | .type = .Yield, 118 | .start = i, 119 | .end = pos, 120 | }; 121 | index += 1; 122 | continue; 123 | } 124 | @compileError("Incomplete yield declaration at " ++ template[i..]); 125 | } else if (mem.startsWith(u8, remainder, "{% include ")) { 126 | const start = i + 12; 127 | if (simd.indexOfPos(u8, template, start, "%}")) |end| { 128 | pos = end + 2; 129 | sections[index] = Section{ 130 | .content = mem.trim(u8, template[start..end], " "), 131 | .type = .Template, 132 | .start = i, 133 | .end = pos, 134 | }; 135 | index += 1; 136 | continue; 137 | } 138 | @compileError("Incomplete include declaration"); 139 | } 140 | @compileError("Incomplete template"); 141 | } else { 142 | pos = i + 1; 143 | } 144 | } 145 | 146 | // Final section 147 | if (pos < template.len) { 148 | sections[index] = Section{ 149 | .content = template[pos..], 150 | .type = .Content, 151 | .start = pos, 152 | .end = template.len, 153 | }; 154 | index += 1; 155 | } 156 | 157 | return sections[0..index]; 158 | } 159 | 160 | /// 161 | /// Generate a template that supports some what "django" like formatting 162 | /// - Use {{ field }} or {{ field.subfield }} for varibles 163 | /// - Use {% include 'path/to/another/template' %} to embed a template 164 | /// - Use {% yield 'blockname' %} to return to your code to manually render stuff 165 | pub fn Template(comptime Context: type, comptime template: []const u8) type { 166 | return struct { 167 | pub const ContextType = Context; 168 | pub const source = template; 169 | pub const sections = parse(Context, template); 170 | 171 | const Self = @This(); 172 | 173 | pub fn dump() void { 174 | std.log.warn("Template (length = {d})\n", .{template.len}); 175 | inline for (sections) |s| { 176 | std.log.warn("{s} (\"{s}\")\n", .{ s, template[s.start..s.end] }); 177 | } 178 | } 179 | 180 | // Render the whole template ignoring any yield statements 181 | pub fn render(context: Context, stream: anytype) @TypeOf(stream).Error!void { 182 | inline for (sections) |s, i| { 183 | _ = i; 184 | try s.render(context, stream); 185 | } 186 | } 187 | }; 188 | } 189 | 190 | pub fn FileTemplate(comptime Context: type, comptime filename: []const u8) type { 191 | return Template(Context, @embedFile(filename)); 192 | } 193 | 194 | fn expectRender(comptime T: type, context: anytype, result: []const u8) !void { 195 | var buf: [4096]u8 = undefined; 196 | var stream = std.io.fixedBufferStream(&buf); 197 | try T.render(context, stream.writer()); 198 | try testing.expectEqualStrings(result, stream.getWritten()); 199 | } 200 | 201 | test "template-variable" { 202 | const Context = struct { 203 | name: []const u8, 204 | }; 205 | const Tmpl = Template(Context, "Hello {{name}}!"); 206 | try expectRender(Tmpl, .{ .name = "World" }, "Hello World!"); 207 | } 208 | // 209 | // test "template-variable-self" { 210 | // const Context = struct { 211 | // name: []const u8, 212 | // }; 213 | // const Tmpl = Template(Context, "{{self}}!"); 214 | // try expectRender(Tmpl, .{.name="World"}, "Context{ .name = World }!"); 215 | // } 216 | 217 | test "template-variable-nested" { 218 | const User = struct { 219 | name: []const u8, 220 | }; 221 | const Context = struct { 222 | user: User, 223 | }; 224 | const Tmpl = Template(Context, "Hello {{user.name}}!"); 225 | try expectRender(Tmpl, .{ .user = User{ .name = "World" } }, "Hello World!"); 226 | } 227 | 228 | test "template-multiple-variables" { 229 | const Context = struct { 230 | name: []const u8, 231 | age: u8, 232 | }; 233 | const Tmpl = Template(Context, "User {{name}} is {{age}}!"); 234 | try expectRender(Tmpl, .{ .name = "Bob", .age = 74 }, "User Bob is 74!"); 235 | } 236 | 237 | test "template-variables-whitespace-is-ignored" { 238 | const Context = struct { 239 | name: []const u8, 240 | age: u8, 241 | }; 242 | const Tmpl = Template(Context, "User {{ name }} is {{ age}}!"); 243 | try expectRender(Tmpl, .{ .name = "Bob", .age = 74 }, "User Bob is 74!"); 244 | } 245 | 246 | test "template-yield" { 247 | var buf: [4096]u8 = undefined; 248 | var stream = std.io.fixedBufferStream(&buf); 249 | var writer = stream.writer(); 250 | const Context = struct { 251 | unused: u8 = 0, 252 | }; 253 | const template = Template(Context, "Before {% yield one %} after"); 254 | const context = Context{}; 255 | inline for (template.sections) |s| { 256 | if (s.is("one")) { 257 | try writer.writeAll("then"); 258 | } else { 259 | try s.render(context, writer); 260 | } 261 | } 262 | try testing.expectEqualStrings("Before then after", stream.getWritten()); 263 | } 264 | 265 | test "template-yield-variables" { 266 | var buf: [4096]u8 = undefined; 267 | var stream = std.io.fixedBufferStream(&buf); 268 | var writer = stream.writer(); 269 | const Context = struct { 270 | what: []const u8 = "base", 271 | }; 272 | const template = Template(Context, "All {% yield name %} {{what}} are belong to {% yield who %}"); 273 | //T.dump(); 274 | const context = Context{}; 275 | 276 | inline for (template.sections) |s| { 277 | if (s.is("name")) { 278 | try writer.writeAll("your"); 279 | } else if (s.is("who")) { 280 | try writer.writeAll("us"); 281 | } else { 282 | try s.render(context, writer); 283 | } 284 | } 285 | try testing.expectEqualStrings("All your base are belong to us", stream.getWritten()); 286 | } 287 | -------------------------------------------------------------------------------- /src/headers.zig: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------- // 2 | // Copyright (c) 2019-2020, Jairus Martin. // 3 | // Distributed under the terms of the MIT License. // 4 | // The full license is in the file LICENSE, distributed with this software. // 5 | // -------------------------------------------------------------------------- // 6 | const std = @import("std"); 7 | const ascii = std.ascii; 8 | const mem = std.mem; 9 | const testing = std.testing; 10 | const Allocator = std.mem.Allocator; 11 | 12 | const util = @import("util.zig"); 13 | const Bytes = util.Bytes; 14 | const IOStream = util.IOStream; 15 | 16 | fn isColonValidateToken(ch: u8) !bool { 17 | if (ch == ':') return true; 18 | if (!util.isTokenChar(ch)) return error.BadRequest; 19 | return false; 20 | } 21 | 22 | fn isControlOrPrint(ch: u8) bool { 23 | return !ascii.isPrint(ch) and util.isCtrlChar(ch); 24 | } 25 | 26 | pub const Headers = struct { 27 | pub const Header = struct { 28 | key: []const u8, 29 | value: []const u8, 30 | }; 31 | pub const HeaderList = std.ArrayList(Header); 32 | headers: HeaderList, 33 | 34 | pub fn init(allocator: Allocator) Headers { 35 | return Headers{ 36 | .headers = HeaderList.init(allocator), 37 | }; 38 | } 39 | 40 | pub fn initCapacity(allocator: Allocator, num: usize) !Headers { 41 | return Headers{ 42 | .headers = try HeaderList.initCapacity(allocator, num), 43 | }; 44 | } 45 | 46 | pub fn deinit(self: *Headers) void { 47 | self.headers.deinit(); 48 | } 49 | 50 | pub fn format( 51 | self: Headers, 52 | comptime fmt: []const u8, 53 | options: std.fmt.FormatOptions, 54 | out_stream: anytype, 55 | ) !void { 56 | _ = fmt; 57 | _ = options; 58 | try std.fmt.format(out_stream, "Headers{{", .{}); 59 | for (self.headers.items) |header| { 60 | try std.fmt.format(out_stream, "\"{s}\": \"{s}\", ", .{ header.key, header.value }); 61 | } 62 | try std.fmt.format(out_stream, "}}", .{}); 63 | } 64 | 65 | // Get the index of the key 66 | pub fn lookup(self: *Headers, key: []const u8) !usize { 67 | for (self.headers.items) |header, i| { 68 | if (ascii.eqlIgnoreCase(header.key, key)) return i; 69 | } 70 | return error.KeyError; 71 | } 72 | 73 | // Get the value for the given key 74 | pub fn get(self: *Headers, key: []const u8) ![]const u8 { 75 | const i = try self.lookup(key); 76 | return self.headers.items[i].value; 77 | } 78 | 79 | pub fn getOptional(self: *Headers, key: []const u8) ?[]const u8 { 80 | return self.get(key) catch null; 81 | } 82 | 83 | pub fn getDefault(self: *Headers, key: []const u8, default: []const u8) []const u8 { 84 | return self.get(key) catch default; 85 | } 86 | 87 | pub fn contains(self: *Headers, key: []const u8) bool { 88 | _ = self.lookup(key) catch { 89 | return false; 90 | }; 91 | return true; 92 | } 93 | 94 | // Check if the header equals the other 95 | pub fn eql(self: *Headers, key: []const u8, other: []const u8) bool { 96 | const v = self.get(key) catch { 97 | return false; 98 | }; 99 | return mem.eql(u8, v, other); 100 | } 101 | 102 | pub fn eqlIgnoreCase(self: *Headers, key: []const u8, other: []const u8) bool { 103 | const v = self.get(key) catch { 104 | return false; 105 | }; 106 | return ascii.eqlIgnoreCase(v, other); 107 | } 108 | 109 | pub fn put(self: *Headers, key: []const u8, value: []const u8) !void { 110 | // If the key already exists under a different name don't add it again 111 | const i = self.lookup(key) catch |err| switch (err) { 112 | error.KeyError => { 113 | try self.headers.append(Header{ .key = key, .value = value }); 114 | return; 115 | }, 116 | else => return err, 117 | }; 118 | self.headers.items[i] = Header{ .key = key, .value = value }; 119 | } 120 | 121 | // Put without checking for duplicates 122 | pub fn append(self: *Headers, key: []const u8, value: []const u8) !void { 123 | return self.headers.append(Header{ .key = key, .value = value }); 124 | } 125 | 126 | pub fn appendAssumeCapacity(self: *Headers, key: []const u8, value: []const u8) void { 127 | return self.headers.appendAssumeCapacity(Header{ .key = key, .value = value }); 128 | } 129 | 130 | pub fn remove(self: *Headers, key: []const u8) !void { 131 | const i = try self.lookup(key); // Throw error 132 | _ = self.headers.swapRemove(i); 133 | } 134 | 135 | pub fn pop(self: *Headers, key: []const u8) ![]const u8 { 136 | const i = try self.lookup(key); // Throw error 137 | return self.headers.swapRemove(i).value; 138 | } 139 | 140 | pub fn popDefault(self: *Headers, key: []const u8, default: []const u8) []const u8 { 141 | return self.pop(key) catch default; 142 | } 143 | 144 | // Reset to an empty header list 145 | pub fn reset(self: *Headers) void { 146 | self.headers.items.len = 0; 147 | } 148 | 149 | /// Assumes the streams current buffer will exist for the lifetime 150 | /// of the headers. 151 | /// Note readbyteFast will not modify the buffer internal buffer 152 | pub fn parse(self: *Headers, buf: *Bytes, stream: *IOStream, max_size: usize) !void { 153 | // Reuse the request buffer for this 154 | var index: usize = undefined; 155 | var key: ?[]u8 = null; 156 | var value: ?[]u8 = null; 157 | 158 | const limit = std.math.min(max_size, stream.amountBuffered()); 159 | const read_limit = limit + stream.readCount(); 160 | var read_all_headers: bool = false; 161 | 162 | while (self.headers.items.len < self.headers.capacity) { 163 | var ch = try stream.readByteSafe(); 164 | defer key = null; 165 | 166 | switch (ch) { 167 | '\r' => { 168 | ch = try stream.readByteSafe(); 169 | if (ch != '\n') return error.BadRequest; 170 | read_all_headers = true; 171 | break; // Empty line, we're done 172 | }, 173 | '\n' => { 174 | read_all_headers = true; 175 | break; // Empty line, we're done 176 | }, 177 | ' ', '\t' => { 178 | // Continuation of multi line header 179 | if (key == null) return error.BadRequest; 180 | }, 181 | ':' => return error.BadRequest, // Empty key 182 | else => { 183 | index = stream.readCount() - 1; 184 | 185 | // Read header name 186 | ch = try stream.readUntilExprValidate(error{BadRequest}, isColonValidateToken, ch, read_limit); 187 | 188 | // Header name 189 | key = buf.items[index .. stream.readCount() - 1]; 190 | 191 | // Strip whitespace 192 | while (stream.readCount() < read_limit) { 193 | ch = stream.readByteUnsafe(); 194 | if (!(ch == ' ' or ch == '\t')) break; 195 | } 196 | }, 197 | } 198 | 199 | // Read value 200 | index = stream.readCount() - 1; 201 | ch = stream.readUntilExpr(isControlOrPrint, ch, read_limit); 202 | 203 | // TODO: Strip trailing spaces and tabs? 204 | value = buf.items[index .. stream.readCount() - 1]; 205 | 206 | // Ignore any remaining non-print characters 207 | ch = stream.readUntilExpr(isControlOrPrint, ch, read_limit); 208 | 209 | if (stream.readCount() >= read_limit) { 210 | if (stream.isEmpty()) return error.EndOfBuffer; 211 | return error.RequestHeaderFieldsTooLarge; 212 | } 213 | 214 | // Check CRLF 215 | if (ch == '\r') { 216 | ch = try stream.readByteSafe(); 217 | } 218 | if (ch != '\n') { 219 | return error.BadRequest; 220 | } 221 | 222 | //std.log.warn("Found header: '{}'='{}'\n", .{key.?, value.?}); 223 | self.appendAssumeCapacity(key.?, value.?); 224 | } 225 | 226 | if (!read_all_headers) { 227 | // If you hit this the capacity needs increased 228 | return error.RequestHeaderFieldsTooLarge; 229 | } 230 | } 231 | 232 | pub fn parseBuffer(self: *Headers, data: []const u8, max_size: usize) !void { 233 | const hack = @bitCast([]u8, data); // HACK: Explicitly violate const 234 | var fba = std.heap.FixedBufferAllocator.init(hack); 235 | fba.end_index = data.len; // Ensure we don't modify the buffer 236 | 237 | // Don't deinit since we don't actually own the data 238 | var buf = Bytes.fromOwnedSlice(fba.allocator(), fba.buffer); 239 | var stream = IOStream.fromBuffer(fba.buffer); 240 | try self.parse(&buf, &stream, max_size); 241 | } 242 | }; 243 | 244 | test "headers-get" { 245 | const allocator = std.testing.allocator; 246 | var headers = try Headers.initCapacity(allocator, 64); 247 | defer headers.deinit(); 248 | try headers.put("Cookie", "Nom;nom;nom"); 249 | try testing.expectError(error.KeyError, headers.get("Accept-Type")); 250 | try testing.expectEqualSlices(u8, try headers.get("cookie"), "Nom;nom;nom"); 251 | try testing.expectEqualSlices(u8, try headers.get("cOOKie"), "Nom;nom;nom"); 252 | try testing.expectEqualSlices(u8, headers.getDefault("User-Agent", "zig"), "zig"); 253 | try testing.expectEqualSlices(u8, headers.getDefault("cookie", "zig"), "Nom;nom;nom"); 254 | } 255 | 256 | test "headers-put" { 257 | const allocator = std.testing.allocator; 258 | var headers = try Headers.initCapacity(allocator, 64); 259 | defer headers.deinit(); 260 | try headers.put("Cookie", "Nom;nom;nom"); 261 | try testing.expectEqualSlices(u8, try headers.get("Cookie"), "Nom;nom;nom"); 262 | try headers.put("COOKie", "ABC"); // Squash even if different 263 | std.log.warn("Cookie is: {s}", .{try headers.get("Cookie")}); 264 | try testing.expectEqualSlices(u8, try headers.get("Cookie"), "ABC"); 265 | } 266 | 267 | test "headers-remove" { 268 | const allocator = std.testing.allocator; 269 | var headers = try Headers.initCapacity(allocator, 64); 270 | defer headers.deinit(); 271 | try headers.put("Cookie", "Nom;nom;nom"); 272 | try testing.expect(headers.contains("Cookie")); 273 | try testing.expect(headers.contains("COOKIE")); 274 | try headers.remove("Cookie"); 275 | try testing.expect(!headers.contains("Cookie")); 276 | } 277 | 278 | test "headers-pop" { 279 | const allocator = std.testing.allocator; 280 | var headers = try Headers.initCapacity(allocator, 64); 281 | defer headers.deinit(); 282 | try testing.expectError(error.KeyError, headers.pop("Cookie")); 283 | try headers.put("Cookie", "Nom;nom;nom"); 284 | try testing.expect(mem.eql(u8, try headers.pop("Cookie"), "Nom;nom;nom")); 285 | try testing.expect(!headers.contains("Cookie")); 286 | try testing.expect(mem.eql(u8, headers.popDefault("Cookie", "Hello"), "Hello")); 287 | } 288 | 289 | test "headers-parse" { 290 | const HEADERS = 291 | \\Host: bs.serving-sys.com 292 | \\User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 293 | \\Accept: image/png,image/*;q=0.8,*/*;q=0.5 294 | \\Accept-Language: en-us,en;q=0.5 295 | \\Accept-Encoding: gzip, deflate 296 | \\Connection: keep-alive 297 | \\Referer: http://static.adzerk.net/reddit/ads.html?sr=-reddit.com&bust2 298 | \\ 299 | \\ 300 | ; 301 | 302 | const allocator = std.testing.allocator; 303 | var headers = try Headers.initCapacity(allocator, 64); 304 | defer headers.deinit(); 305 | try headers.parseBuffer(HEADERS[0..], 1024); 306 | 307 | try testing.expect(mem.eql(u8, try headers.get("Host"), "bs.serving-sys.com")); 308 | try testing.expect(mem.eql(u8, try headers.get("Connection"), "keep-alive")); 309 | } 310 | -------------------------------------------------------------------------------- /src/forms.zig: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------- // 2 | // Copyright (c) 2019-2020, Jairus Martin. // 3 | // Distributed under the terms of the MIT License. // 4 | // The full license is in the file LICENSE, distributed with this software. // 5 | // -------------------------------------------------------------------------- // 6 | const std = @import("std"); 7 | const log = std.log; 8 | const ascii = std.ascii; 9 | const mem = std.mem; 10 | const testing = std.testing; 11 | const Allocator = mem.Allocator; 12 | 13 | const web = @import("zhp.zig"); 14 | const util = web.util; 15 | const Request = web.Request; 16 | const Headers = web.Headers; 17 | 18 | const simd = @import("simd.zig"); 19 | 20 | // Represents a file uploaded via a form. 21 | pub const FileUpload = struct { 22 | filename: []const u8, 23 | content_type: []const u8, 24 | body: []const u8, 25 | }; 26 | 27 | pub const ArgMap = util.StringArrayMap([]const u8); 28 | pub const FileMap = util.StringArrayMap(FileUpload); 29 | const WS = " \t\r\n"; 30 | 31 | pub const Form = struct { 32 | allocator: Allocator, 33 | fields: ArgMap, 34 | files: FileMap, 35 | 36 | pub fn init(allocator: Allocator) Form { 37 | return Form{ 38 | .allocator = allocator, 39 | .fields = ArgMap.init(allocator), 40 | .files = FileMap.init(allocator), 41 | }; 42 | } 43 | 44 | pub fn deinit(self: *Form) void { 45 | self.fields.deinit(); 46 | self.files.deinit(); 47 | } 48 | 49 | pub fn parse(self: *Form, request: *Request) !void { 50 | const content_type = try request.headers.get("Content-Type"); 51 | if (!request.read_finished) { 52 | if (request.stream) |stream| { 53 | try request.readBody(stream); 54 | } 55 | } 56 | if (request.content) |content| { 57 | switch (content.type) { 58 | .TempFile => { 59 | return error.NotImplemented; // TODO: Parsing should use a stream 60 | }, 61 | .Buffer => { 62 | try self.parseMultipart(content_type, content.data.buffer); 63 | }, 64 | } 65 | } 66 | } 67 | 68 | pub fn parseMultipart(self: *Form, content_type: []const u8, data: []const u8) !void { 69 | var iter = mem.split(u8, content_type, ";"); 70 | while (iter.next()) |part| { 71 | const pair = mem.trim(u8, part, WS); 72 | const key = "boundary="; 73 | if (pair.len > key.len and mem.startsWith(u8, pair, key)) { 74 | const boundary = pair[key.len..]; 75 | try self.parseMultipartFormData(boundary, data); 76 | } 77 | } 78 | } 79 | 80 | pub fn parseMultipartFormData(self: *Form, boundary: []const u8, data: []const u8) !void { 81 | var bounds = boundary[0..]; 82 | if (mem.startsWith(u8, boundary, "\"") and mem.endsWith(u8, boundary, "\"")) { 83 | bounds = boundary[1 .. bounds.len - 1]; 84 | } 85 | if (bounds.len > 70) { 86 | return error.MultipartBoundaryTooLong; 87 | } 88 | 89 | var buf: [74]u8 = undefined; 90 | 91 | // Check final boundary 92 | const final_boundary = try std.fmt.bufPrint(&buf, "--{s}--", .{bounds}); 93 | const final_boundary_index = mem.lastIndexOf(u8, data, final_boundary); 94 | if (final_boundary_index == null) { 95 | log.warn("Invalid multipart/form-data: no final boundary", .{}); 96 | return error.MultipartFinalBoundaryMissing; 97 | } 98 | 99 | const separator = try std.fmt.bufPrint(&buf, "--{s}\r\n", .{bounds}); 100 | 101 | var fields = simd.split(u8, data[0..final_boundary_index.?], separator); 102 | 103 | // TODO: Make these default capacities configurable 104 | var headers = try Headers.initCapacity(self.allocator, 8); 105 | defer headers.deinit(); 106 | var disp_params = try Headers.initCapacity(self.allocator, 8); 107 | defer disp_params.deinit(); 108 | 109 | while (fields.next()) |part| { 110 | if (part.len == 0) { 111 | continue; 112 | } 113 | const header_sep = "\r\n\r\n"; 114 | const eoh = mem.lastIndexOf(u8, part, header_sep); 115 | if (eoh == null) { 116 | log.warn("multipart/form-data missing headers: {s}", .{part}); 117 | continue; 118 | } 119 | 120 | const body = part[0 .. eoh.? + header_sep.len]; 121 | // NOTE: Do not free, data is assumed to be owned 122 | // also do not do this after parsing or the it will cause a memory leak 123 | headers.reset(); 124 | try headers.parseBuffer(body, body.len + 1); 125 | 126 | const disp_header = headers.getDefault("Content-Disposition", ""); 127 | disp_params.reset(); // NOTE: Do not free, data is assumed to be owned 128 | const disposition = try parseHeader(self.allocator, disp_header, &disp_params); 129 | 130 | if (!ascii.eqlIgnoreCase(disposition, "form-data")) { 131 | log.warn("Invalid multipart/form-data", .{}); 132 | continue; 133 | } 134 | 135 | var field_name = disp_params.getDefault("name", ""); 136 | if (field_name.len == 0) { 137 | log.warn("multipart/form-data value missing name", .{}); 138 | continue; 139 | } 140 | const field_value = part[body.len..part.len]; 141 | 142 | if (disp_params.contains("filename")) { 143 | const content_type = disp_params.getDefault("Content-Type", "application/octet-stream"); 144 | try self.files.append(field_name, FileUpload{ 145 | .filename = disp_params.getDefault("filename", ""), 146 | .body = field_value, 147 | .content_type = content_type, 148 | }); 149 | } else { 150 | try self.fields.append(field_name, field_value); 151 | } 152 | } 153 | } 154 | }; 155 | 156 | test "simple-form" { 157 | const content_type = "multipart/form-data; boundary=---------------------------389538318911445707002572116565"; 158 | const body = 159 | "-----------------------------389538318911445707002572116565\r\n" ++ 160 | "Content-Disposition: form-data; name=\"name\"\r\n" ++ 161 | "\r\n" ++ 162 | "Your name" ++ 163 | "-----------------------------389538318911445707002572116565\r\n" ++ 164 | "Content-Disposition: form-data; name=\"action\"\r\n" ++ 165 | "\r\n" ++ 166 | "1" ++ 167 | "-----------------------------389538318911445707002572116565--\r\n"; 168 | var form = Form.init(std.testing.allocator); 169 | defer form.deinit(); 170 | try form.parseMultipart(content_type, body); 171 | try testing.expectEqualStrings("Your name", form.fields.get("name").?); 172 | try testing.expectEqualStrings("1", form.fields.get("action").?); 173 | } 174 | 175 | test "simple-file-form" { 176 | const content_type = "multipart/form-data; boundary=1234"; 177 | const body = 178 | "--1234\r\n" ++ 179 | "Content-Disposition: form-data; name=files; filename=ab.txt\r\n" ++ 180 | "\r\n" ++ 181 | "Hello!\n" ++ 182 | "--1234--\r\n"; 183 | var form = Form.init(std.testing.allocator); 184 | defer form.deinit(); 185 | try form.parseMultipart(content_type, body); 186 | const f = form.files.get("files").?; 187 | try testing.expectEqualStrings(f.filename, "ab.txt"); 188 | try testing.expectEqualStrings(f.body, "Hello!\n"); 189 | } 190 | 191 | test "multi-file-form" { 192 | const content_type = "multipart/form-data; boundary=1234"; 193 | const body = 194 | "--1234\r\n" ++ 195 | "Content-Disposition: form-data; name=files; filename=ab.txt\r\n" ++ 196 | "\r\n" ++ 197 | "Hello!\n" ++ 198 | "--1234\r\n" ++ 199 | "Content-Disposition: form-data; name=files; filename=data.json; content-type=application/json\r\n" ++ 200 | "\r\n" ++ 201 | "{\"status\": \"OK\"}\n" ++ 202 | "--1234--\r\n"; 203 | var form = Form.init(std.testing.allocator); 204 | defer form.deinit(); 205 | try form.parseMultipart(content_type, body); 206 | const f = form.files.getArray("files").?; 207 | try testing.expect(f.items.len == 2); 208 | try testing.expectEqualStrings(f.items[0].filename, "ab.txt"); 209 | try testing.expectEqualStrings(f.items[0].body, "Hello!\n"); 210 | try testing.expectEqualStrings(f.items[1].filename, "data.json"); 211 | try testing.expectEqualStrings(f.items[1].content_type, "application/json"); 212 | } 213 | 214 | // Parse a header. 215 | // return the first value and update the params with everything else 216 | fn parseHeader(allocator: Allocator, line: []const u8, params: *Headers) ![]const u8 { 217 | if (line.len == 0) return ""; 218 | var it = mem.split(u8, line, ";"); 219 | 220 | // First part is returned as the main header value 221 | const value = if (it.next()) |p| mem.trim(u8, p, " \r\n") else ""; 222 | 223 | // Now get the rest of the parameters 224 | while (it.next()) |p| { 225 | // Split on = 226 | var i = mem.indexOf(u8, p, "="); 227 | if (i == null) continue; 228 | 229 | const name = mem.trim(u8, p[0..i.?], " \r\n"); 230 | const encoded_value = mem.trim(u8, p[i.? + 1 ..], " \r\n"); 231 | const decoded_value = try collapseRfc2231Value(allocator, encoded_value); 232 | try params.append(name, decoded_value); 233 | } 234 | try decodeRfc2231Params(allocator, params); 235 | return value; 236 | } 237 | 238 | fn collapseRfc2231Value(allocator: Allocator, value: []const u8) ![]const u8 { 239 | _ = allocator; 240 | // TODO: Implement this.. 241 | return mem.trim(u8, value, "\""); 242 | } 243 | 244 | fn decodeRfc2231Params(allocator: Allocator, params: *Headers) !void { 245 | _ = allocator; 246 | _ = params; 247 | // TODO: Implement this.. 248 | } 249 | 250 | test "parse-content-disposition-header" { 251 | const allocator = std.testing.allocator; 252 | const d = " form-data; name=\"fieldName\"; filename=\"filename.jpg\""; 253 | var params = try Headers.initCapacity(allocator, 5); 254 | defer params.deinit(); 255 | var v = try parseHeader(allocator, d, ¶ms); 256 | try testing.expectEqualSlices(u8, "form-data", v); 257 | try testing.expectEqualSlices(u8, "fieldName", try params.get("name")); 258 | try testing.expectEqualSlices(u8, "filename.jpg", try params.get("filename")); 259 | } 260 | 261 | /// Inverse of parseHeader. 262 | /// This always returns a copy so it must be cleaned up! 263 | pub fn encodeHeader(allocator: Allocator, key: []const u8, params: Headers) ![]const u8 { 264 | if (params.headers.items.len == 0) { 265 | return try allocator.dupe(u8, key); 266 | } 267 | 268 | // I'm lazy 269 | var arena = std.heap.ArenaAllocator.init(allocator); 270 | defer arena.deinit(); 271 | 272 | var out = std.ArrayList([]const u8).init(arena.allocator()); 273 | try out.append(key); 274 | 275 | // Sort the parameters just to make it easy to test. 276 | for (params.headers.items) |entry| { 277 | if (entry.value.len == 0) { 278 | try out.append(entry.key); 279 | } else { 280 | // TODO: quote if necessary. 281 | try out.append(try std.fmt.allocPrint(&arena.allocator, "{}={}", .{ entry.key, entry.value })); 282 | } 283 | } 284 | return try mem.join(allocator, "; ", out.items); 285 | } 286 | 287 | test "encode-header" { 288 | const allocator = std.testing.allocator; 289 | var params = Headers.init(allocator); 290 | defer params.deinit(); 291 | 292 | var r = try encodeHeader(allocator, "permessage-deflate", params); 293 | try testing.expectEqualSlices(u8, "permessage-deflate", r); 294 | allocator.free(r); 295 | 296 | try params.append("client_no_context_takeover", ""); 297 | try params.append("client_max_window_bits", "15"); 298 | 299 | r = try encodeHeader(allocator, "permessage-deflate", params); 300 | try testing.expectEqualSlices(u8, r, "permessage-deflate; client_no_context_takeover; client_max_window_bits=15"); 301 | allocator.free(r); 302 | } 303 | -------------------------------------------------------------------------------- /src/mimetypes.zig: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------- // 2 | // Copyright (c) 2019-2020, Jairus Martin. // 3 | // Distributed under the terms of the MIT License. // 4 | // The full license is in the file LICENSE, distributed with this software. // 5 | // -------------------------------------------------------------------------- // 6 | const std = @import("std"); 7 | const builtin = @import("builtin"); 8 | const fs = std.fs; 9 | const mem = std.mem; 10 | const ascii = std.ascii; 11 | const Allocator = mem.Allocator; 12 | const testing = std.testing; 13 | 14 | pub const known_files = &[_][]const u8{ 15 | "/etc/mime.types", 16 | "/etc/httpd/mime.types", // Mac OS X 17 | "/etc/httpd/conf/mime.types", // Apache 18 | "/etc/apache/mime.types", // Apache 1 19 | "/etc/apache2/mime.types", // Apache 2 20 | "/usr/local/etc/httpd/conf/mime.types", 21 | "/usr/local/lib/netscape/mime.types", 22 | "/usr/local/etc/httpd/conf/mime.types", // Apache 1.2 23 | "/usr/local/etc/mime.types", // Apache 1.3 24 | }; 25 | 26 | pub const suffix_map = &[_][2][]const u8{ 27 | .{ ".svgz", ".svg.gz" }, 28 | .{ ".tgz", ".tar.gz" }, 29 | .{ ".taz", ".tar.gz" }, 30 | .{ ".tz", ".tar.gz" }, 31 | .{ ".tbz2", ".tar.bz2" }, 32 | .{ ".txz", ".tar.xz" }, 33 | }; 34 | 35 | pub const encodings_map = &[_][2][]const u8{ 36 | .{ ".gz", "gzip" }, 37 | .{ ".Z", "compress" }, 38 | .{ ".bz2", "bzip2" }, 39 | .{ ".xz", "xz" }, 40 | }; 41 | 42 | // Before adding new types, make sure they are either registered with IANA, 43 | // at http://www.isi.edu/in-notes/iana/assignments/media-types 44 | // or extensions, i.e. using the x- prefix 45 | // If you add to these, please keep them sorted! 46 | pub const extension_map = &[_][2][]const u8{ 47 | .{ ".a", "application/octet-stream" }, 48 | .{ ".ai", "application/postscript" }, 49 | .{ ".aif", "audio/x-aiff" }, 50 | .{ ".aifc", "audio/x-aiff" }, 51 | .{ ".aiff", "audio/x-aiff" }, 52 | .{ ".au", "audio/basic" }, 53 | .{ ".avi", "video/x-msvideo" }, 54 | .{ ".bat", "text/plain" }, 55 | .{ ".bcpio", "application/x-bcpio" }, 56 | .{ ".bin", "application/octet-stream" }, 57 | .{ ".bmp", "image/x-ms-bmp" }, 58 | .{ ".c", "text/plain" }, 59 | .{ ".cdf", "application/x-cdf" }, // Dup 60 | .{ ".cdf", "application/x-netcdf" }, 61 | .{ ".cpio", "application/x-cpio" }, 62 | .{ ".csh", "application/x-csh" }, 63 | .{ ".css", "text/css" }, 64 | .{ ".csv", "text/csv" }, 65 | .{ ".dll", "application/octet-stream" }, 66 | .{ ".doc", "application/msword" }, 67 | .{ ".dot", "application/msword" }, 68 | .{ ".dvi", "application/x-dvi" }, 69 | .{ ".eml", "message/rfc822" }, 70 | .{ ".eps", "application/postscript" }, 71 | .{ ".etx", "text/x-setext" }, 72 | .{ ".exe", "application/octet-stream" }, 73 | .{ ".gif", "image/gif" }, 74 | .{ ".gtar", "application/x-gtar" }, 75 | .{ ".h", "text/plain" }, 76 | .{ ".hdf", "application/x-hdf" }, 77 | .{ ".htm", "text/html" }, 78 | .{ ".html", "text/html" }, 79 | .{ ".ico", "image/vnd.microsoft.icon" }, 80 | .{ ".ief", "image/ief" }, 81 | .{ ".jpe", "image/jpeg" }, 82 | .{ ".jpeg", "image/jpeg" }, 83 | .{ ".jpg", "image/jpeg" }, 84 | .{ ".js", "application/javascript" }, 85 | .{ ".json", "application/json" }, 86 | .{ ".ksh", "text/plain" }, 87 | .{ ".latex", "application/x-latex" }, 88 | .{ ".m1v", "video/mpeg" }, 89 | .{ ".man", "application/x-troff-man" }, 90 | .{ ".me", "application/x-troff-me" }, 91 | .{ ".mht", "message/rfc822" }, 92 | .{ ".mhtml", "message/rfc822" }, 93 | .{ ".mid", "audio/midi" }, 94 | .{ ".midi", "audio/midi" }, 95 | .{ ".mif", "application/x-mif" }, 96 | .{ ".mjs", "application/javascript" }, 97 | .{ ".mov", "video/quicktime" }, 98 | .{ ".movie", "video/x-sgi-movie" }, 99 | .{ ".mp2", "audio/mpeg" }, 100 | .{ ".mp3", "audio/mpeg" }, 101 | .{ ".mp4", "video/mp4" }, 102 | .{ ".mpa", "video/mpeg" }, 103 | .{ ".mpe", "video/mpeg" }, 104 | .{ ".mpeg", "video/mpeg" }, 105 | .{ ".mpg", "video/mpeg" }, 106 | .{ ".ms", "application/x-troff-ms" }, 107 | .{ ".nc", "application/x-netcdf" }, 108 | .{ ".nws", "message/rfc822" }, 109 | .{ ".o", "application/octet-stream" }, 110 | .{ ".obj", "application/octet-stream" }, 111 | .{ ".oda", "application/oda" }, 112 | .{ ".p12", "application/x-pkcs12" }, 113 | .{ ".p7c", "application/pkcs7-mime" }, 114 | .{ ".pbm", "image/x-portable-bitmap" }, 115 | .{ ".pdf", "application/pdf" }, 116 | .{ ".pfx", "application/x-pkcs12" }, 117 | .{ ".pgm", "image/x-portable-graymap" }, 118 | .{ ".pct", "image/pict" }, 119 | .{ ".pic", "image/pict" }, 120 | .{ ".pict", "image/pict" }, 121 | .{ ".pl", "text/plain" }, 122 | .{ ".png", "image/png" }, 123 | .{ ".pnm", "image/x-portable-anymap" }, 124 | .{ ".pot", "application/vnd.ms-powerpoint" }, 125 | .{ ".ppa", "application/vnd.ms-powerpoint" }, 126 | .{ ".ppm", "image/x-portable-pixmap" }, 127 | .{ ".pps", "application/vnd.ms-powerpoint" }, 128 | .{ ".ppt", "application/vnd.ms-powerpoint" }, 129 | .{ ".ps", "application/postscript" }, 130 | .{ ".pwz", "application/vnd.ms-powerpoint" }, 131 | .{ ".py", "text/x-python" }, 132 | .{ ".pyc", "application/x-python-code" }, 133 | .{ ".pyo", "application/x-python-code" }, 134 | .{ ".qt", "video/quicktime" }, 135 | .{ ".ra", "audio/x-pn-realaudio" }, 136 | .{ ".ram", "application/x-pn-realaudio" }, 137 | .{ ".ras", "image/x-cmu-raster" }, 138 | .{ ".rdf", "application/xml" }, 139 | .{ ".rgb", "image/x-rgb" }, 140 | .{ ".roff", "application/x-troff" }, 141 | .{ ".rtf", "application/rtf" }, 142 | .{ ".rtx", "text/richtext" }, 143 | .{ ".sgm", "text/x-sgml" }, 144 | .{ ".sgml", "text/x-sgml" }, 145 | .{ ".sh", "application/x-sh" }, 146 | .{ ".shar", "application/x-shar" }, 147 | .{ ".snd", "audio/basic" }, 148 | .{ ".so", "application/octet-stream" }, 149 | .{ ".src", "application/x-wais-source" }, 150 | .{ ".sv4cpio", "application/x-sv4cpio" }, 151 | .{ ".sv4crc", "application/x-sv4crc" }, 152 | .{ ".svg", "image/svg+xml" }, 153 | .{ ".swf", "application/x-shockwave-flash" }, 154 | .{ ".t", "application/x-troff" }, 155 | .{ ".tar", "application/x-tar" }, 156 | .{ ".tcl", "application/x-tcl" }, 157 | .{ ".tex", "application/x-tex" }, 158 | .{ ".texi", "application/x-texinfo" }, 159 | .{ ".texinfo", "application/x-texinfo" }, 160 | .{ ".tif", "image/tiff" }, 161 | .{ ".tiff", "image/tiff" }, 162 | .{ ".tr", "application/x-troff" }, 163 | .{ ".tsv", "text/tab-separated-values" }, 164 | .{ ".txt", "text/plain" }, 165 | .{ ".ustar", "application/x-ustar" }, 166 | .{ ".vcf", "text/x-vcard" }, 167 | .{ ".wav", "audio/x-wav" }, 168 | .{ ".webm", "video/webm" }, 169 | .{ ".wiz", "application/msword" }, 170 | .{ ".wsdl", "application/xml" }, 171 | .{ ".xbm", "image/x-xbitmap" }, 172 | .{ ".xlb", "application/vnd.ms-excel" }, 173 | .{ ".xls", "application/excel" }, 174 | .{ ".xls", "application/vnd.ms-excel" }, // Dup 175 | .{ ".xml", "text/xml" }, 176 | .{ ".xpdl", "application/xml" }, 177 | .{ ".xpm", "image/x-xpixmap" }, 178 | .{ ".xsl", "application/xml" }, 179 | .{ ".xwd", "image/x-xwindowdump" }, 180 | .{ ".xul", "text/xul" }, 181 | .{ ".zip", "application/zip" }, 182 | }; 183 | 184 | // Whitespace characters 185 | const WS = " \t\r\n"; 186 | 187 | // Replace inplace 188 | fn replace(line: []u8, find: u8, replacement: u8) void { 189 | var i: usize = 0; 190 | while (i < line.len) : (i += 1) { 191 | if (line[i] == find) line[i] = replacement; 192 | } 193 | } 194 | 195 | // Trim that doesn't require a const slice 196 | fn trim(slice: []u8, values: []const u8) []u8 { 197 | var begin: usize = 0; 198 | var end: usize = slice.len; 199 | while (begin < end and mem.indexOfScalar(u8, values, slice[begin]) != null) : (begin += 1) {} 200 | while (end > begin and mem.indexOfScalar(u8, values, slice[end - 1]) != null) : (end -= 1) {} 201 | return slice[begin..end]; 202 | } 203 | 204 | pub const Registry = struct { 205 | const StringMap = std.StringHashMap([]const u8); 206 | const StringArray = std.ArrayList([]const u8); 207 | const StringArrayMap = std.StringHashMap(*StringArray); 208 | 209 | loaded: bool = false, 210 | arena: std.heap.ArenaAllocator, 211 | 212 | // Maps extension type to mime type 213 | type_map: StringMap, 214 | 215 | // Maps mime type to list of extensions 216 | type_map_inv: StringArrayMap, 217 | 218 | pub fn init(allocator: Allocator) Registry { 219 | // Must call load separately to avoid https://github.com/ziglang/zig/issues/2765 220 | return Registry{ 221 | .arena = std.heap.ArenaAllocator.init(allocator), 222 | .type_map = StringMap.init(allocator), 223 | .type_map_inv = StringArrayMap.init(allocator), 224 | }; 225 | } 226 | 227 | // Add a mapping between a type and an extension. 228 | // this copies both and will overwrite any existing entries 229 | pub fn addType(self: *Registry, ext: []const u8, mime_type: []const u8) !void { 230 | // Add '.' if necessary 231 | const allocator = self.arena.allocator(); 232 | const extension = 233 | if (mem.startsWith(u8, ext, ".")) 234 | try allocator.dupe(u8, mem.trim(u8, ext, WS)) 235 | else 236 | try mem.concat(allocator, u8, &[_][]const u8{ ".", mem.trim(u8, ext, WS) }); 237 | return self.addTypeInternal(extension, try allocator.dupe(u8, mem.trim(u8, mime_type, WS))); 238 | } 239 | 240 | // Add a mapping between a type and an extension. 241 | // this assumes the entries added are already owend 242 | fn addTypeInternal(self: *Registry, ext: []const u8, mime_type: []const u8) !void { 243 | // std.log.warn(" adding {}: {} to registry...\n", .{ext, mime_type}); 244 | const allocator = self.arena.allocator(); 245 | _ = try self.type_map.put(ext, mime_type); 246 | 247 | if (self.type_map_inv.getEntry(mime_type)) |entry| { 248 | // Check if it's already there 249 | const type_map = entry.value_ptr.*; 250 | for (type_map.items) |e| { 251 | if (mem.eql(u8, e, ext)) return; // Already there 252 | } 253 | try type_map.append(ext); 254 | } else { 255 | // Create a new list of extensions 256 | const extensions = try allocator.create(StringArray); 257 | extensions.* = StringArray.init(allocator); 258 | _ = try self.type_map_inv.put(mime_type, extensions); 259 | try extensions.append(ext); 260 | } 261 | } 262 | 263 | pub fn load(self: *Registry) !void { 264 | if (self.loaded) return; 265 | self.loaded = true; 266 | // Load defaults 267 | for (extension_map) |entry| { 268 | try self.addType(entry[0], entry[1]); 269 | } 270 | 271 | // Load from system 272 | if (builtin.os.tag == .windows) { 273 | // TODO: Windows 274 | } else { 275 | try self.loadRegistryLinux(); 276 | } 277 | } 278 | 279 | pub fn loadRegistryLinux(self: *Registry) !void { 280 | for (known_files) |path| { 281 | var file = fs.openFileAbsolute(path, .{ .mode = .read_only }) catch continue; 282 | // std.log.warn("Loading {}...\n", .{path}); 283 | try self.loadRegistryFile(file); 284 | } 285 | } 286 | 287 | // Read a single mime.types-format file. 288 | pub fn loadRegistryFile(self: *Registry, file: fs.File) !void { 289 | var stream = &std.io.bufferedReader(file.reader()).reader(); 290 | var buf: [1024]u8 = undefined; 291 | while (true) { 292 | const result = try stream.readUntilDelimiterOrEof(&buf, '\n'); 293 | if (result == null) break; // EOF 294 | var line = trim(result.?, WS); 295 | 296 | // Strip comments 297 | const end = mem.indexOf(u8, line, "#") orelse line.len; 298 | line = line[0..end]; 299 | 300 | // Replace tabs with spaces to normalize so tokenize works 301 | replace(line, '\t', ' '); 302 | 303 | // Empty or no spaces 304 | if (line.len == 0 or mem.indexOf(u8, line, " ") == null) continue; 305 | 306 | var it = mem.tokenize(u8, line, " "); 307 | const mime_type = it.next() orelse continue; 308 | while (it.next()) |ext| { 309 | try self.addType(ext, mime_type); 310 | } 311 | } 312 | } 313 | 314 | // Guess the mime type from the filename 315 | pub fn getTypeFromFilename(self: *Registry, filename: []const u8) ?[]const u8 { 316 | const last_dot = mem.lastIndexOf(u8, filename, "."); 317 | if (last_dot) |i| return self.getTypeFromExtension(filename[i..]); 318 | return null; 319 | } 320 | 321 | // Guess the type of a file based on its URL. 322 | pub fn getTypeFromExtension(self: *Registry, ext: []const u8) ?[]const u8 { 323 | if (self.type_map.getEntry(ext)) |entry| { 324 | return entry.value_ptr.*; 325 | } 326 | return null; 327 | } 328 | 329 | pub fn getExtensionsByType(self: *Registry, mime_type: []const u8) ?*StringArray { 330 | if (self.type_map_inv.getEntry(mime_type)) |entry| { 331 | return entry.value_ptr.*; 332 | } 333 | return null; 334 | } 335 | 336 | pub fn deinit(self: *Registry) void { 337 | // Free type 338 | self.type_map.deinit(); 339 | 340 | // Free the type map 341 | self.type_map_inv.deinit(); 342 | 343 | // And free anything else 344 | self.arena.deinit(); 345 | } 346 | }; 347 | 348 | pub var instance: ?Registry = null; 349 | 350 | test "guess-ext" { 351 | var registry = Registry.init(std.testing.allocator); 352 | defer registry.deinit(); 353 | try registry.load(); 354 | 355 | try testing.expectEqualSlices(u8, "image/png", registry.getTypeFromFilename("an-image.png").?); 356 | try testing.expectEqualSlices(u8, "application/javascript", registry.getTypeFromFilename("wavascript.js").?); 357 | } 358 | 359 | test "guess-ext-from-file" { 360 | var registry = Registry.init(std.testing.allocator); 361 | defer registry.deinit(); 362 | try registry.load(); 363 | 364 | // This ext is not in the list above 365 | try testing.expectEqualSlices(u8, "application/x-7z-compressed", registry.getTypeFromFilename("archive.7z").?); 366 | } 367 | 368 | test "guess-ext-unknown" { 369 | var registry = Registry.init(std.testing.allocator); 370 | defer registry.deinit(); 371 | try registry.load(); 372 | 373 | // This ext is not in the list above 374 | try testing.expect(registry.getTypeFromFilename("notanext") == null); 375 | } 376 | -------------------------------------------------------------------------------- /example/main.zig: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------- // 2 | // Copyright (c) 2019-2020, Jairus Martin. // 3 | // Distributed under the terms of the MIT License. // 4 | // The full license is in the file LICENSE, distributed with this software. // 5 | // -------------------------------------------------------------------------- // 6 | const std = @import("std"); 7 | const web = @import("zhp"); 8 | const Request = web.Request; 9 | const Response = web.Response; 10 | 11 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 12 | 13 | pub const io_mode = .evented; 14 | pub const log_level = .debug; 15 | 16 | /// This handler demonstrates how to send a template resrponse using 17 | /// zig's built-in formatting. 18 | const TemplateHandler = struct { 19 | const template = @embedFile("templates/cover.html"); 20 | 21 | pub fn get(self: *TemplateHandler, req: *Request, resp: *Response) !void { 22 | _ = self; 23 | _ = req; 24 | @setEvalBranchQuota(100000); 25 | try resp.stream.print(template, .{"ZHP"}); 26 | } 27 | }; 28 | 29 | /// This handler demonstrates how to set headers and 30 | /// write to the response stream. The response stream is buffered. 31 | /// in memory until the handler completes. 32 | const HelloHandler = struct { 33 | pub fn get(self: *HelloHandler, req: *Request, resp: *Response) !void { 34 | _ = self; 35 | _ = req; 36 | try resp.headers.append("Content-Type", "text/plain"); 37 | try resp.stream.writeAll("Hello, World!"); 38 | } 39 | }; 40 | 41 | /// This handler demonstrates how to send a streaming response. 42 | /// since ZHP buffers the handler output use `send_stream = true` to tell 43 | /// it to invoke the stream method to complete the response. 44 | const StreamHandler = struct { 45 | const template = @embedFile("templates/stream.html"); 46 | allocator: ?std.mem.Allocator = null, 47 | 48 | pub fn get(self: *StreamHandler, req: *Request, resp: *Response) !void { 49 | if (std.mem.eql(u8, req.path, "/stream/live/")) { 50 | try resp.headers.append("Content-Type", "audio/mpeg"); 51 | try resp.headers.append("Cache-Control", "no-cache"); 52 | 53 | // This tells the framework to invoke stream fn after sending the 54 | // headers 55 | resp.send_stream = true; 56 | self.allocator = resp.allocator; 57 | } else { 58 | try resp.stream.writeAll(template); 59 | } 60 | } 61 | 62 | pub fn stream(self: *StreamHandler, io: *web.IOStream) !usize { 63 | std.log.info("Starting audio stream", .{}); 64 | const n = self.forward(io) catch |err| { 65 | std.log.info("Error streaming: {s}", .{err}); 66 | return 0; 67 | }; 68 | return n; 69 | } 70 | 71 | fn forward(self: *StreamHandler, io: *web.IOStream) !usize { 72 | const writer = io.writer(); 73 | std.debug.assert(self.allocator != null); 74 | const a = self.allocator.?; 75 | // http://streams.sevenfm.nl/live 76 | 77 | std.log.info("Connecting...", .{}); 78 | const conn = try std.net.tcpConnectToHost(a, "streams.sevenfm.nl", 80); 79 | defer conn.close(); 80 | std.log.info("Connected!", .{}); 81 | try conn.writer().writeAll("GET /live HTTP/1.1\r\n" ++ 82 | "Host: streams.sevenfm.nl\r\n" ++ 83 | "Accept: */*\r\n" ++ 84 | "Connection: keep-alive\r\n" ++ 85 | "\r\n"); 86 | 87 | var buf: [4096]u8 = undefined; 88 | var total_sent: usize = 0; 89 | 90 | // On the first response skip their server's headers 91 | // but include the icecast stream headers from their response 92 | const end = try conn.read(buf[0..]); 93 | const offset = if (std.mem.indexOf(u8, buf[0..end], "icy-br:")) |o| o else 0; 94 | try writer.writeAll(buf[offset..end]); 95 | total_sent += end - offset; 96 | 97 | // Now just forward the stream data 98 | while (true) { 99 | const n = try conn.read(buf[0..]); 100 | if (n == 0) { 101 | std.log.info("Stream disconnected", .{}); 102 | break; 103 | } 104 | total_sent += n; 105 | try writer.writeAll(buf[0..n]); 106 | try io.flush(); // Send it out the pipe 107 | } 108 | return total_sent; 109 | } 110 | }; 111 | 112 | /// This handler shows how to read headers and cookies from the request. 113 | /// It also shows another way to write to the response stream. 114 | /// Finally it shows one way to use "static" storage that persists between 115 | /// requests. 116 | const JsonHandler = struct { 117 | // Static storage 118 | var counter = std.atomic.Atomic(usize).init(0); 119 | 120 | pub fn get(self: *JsonHandler, req: *Request, resp: *Response) !void { 121 | _ = self; 122 | try resp.headers.append("Content-Type", "application/json"); 123 | 124 | var jw = std.json.writeStream(resp.stream, 4); 125 | try jw.beginObject(); 126 | for (req.headers.headers.items) |h| { 127 | try jw.objectField(h.key); 128 | try jw.emitString(h.value); 129 | } 130 | 131 | // Cookies aren't parsed by default 132 | // If you know they're parsed (eg by middleware) you can just 133 | // use request.cookies directly 134 | if (try req.readCookies()) |cookies| { 135 | try jw.objectField("Cookie"); 136 | try jw.beginObject(); 137 | for (cookies.cookies.items) |c| { 138 | try jw.objectField(c.key); 139 | try jw.emitString(c.value); 140 | } 141 | try jw.endObject(); 142 | } 143 | 144 | try jw.objectField("Request-Count"); 145 | try jw.emitNumber(counter.fetchAdd(1, .Monotonic)); 146 | 147 | try jw.endObject(); 148 | } 149 | }; 150 | 151 | /// This handler demonstrates how to use url arguments. The 152 | /// `request.args` is the result of ctregex's parsing of the url. 153 | const ApiHandler = struct { 154 | pub fn get(self: *ApiHandler, req: *Request, resp: *Response) !void { 155 | _ = self; 156 | try resp.headers.append("Content-Type", "application/json"); 157 | 158 | var jw = std.json.writeStream(resp.stream, 4); 159 | try jw.beginObject(); 160 | const args = req.args.?; 161 | try jw.objectField(args[0].?); 162 | try jw.emitString(args[1].?); 163 | try jw.endObject(); 164 | } 165 | }; 166 | 167 | /// When an error is returned the framework will return the error handler response 168 | const ErrorTestHandler = struct { 169 | pub fn get(self: *ErrorTestHandler, req: *Request, resp: *Response) !void { 170 | _ = self; 171 | _ = req; 172 | try resp.stream.writeAll("Do some work"); 173 | return error.Ooops; 174 | } 175 | }; 176 | 177 | /// Redirect shortcut 178 | const RedirectHandler = struct { 179 | // Shows how to redirect 180 | pub fn get(self: *RedirectHandler, req: *Request, resp: *Response) !void { 181 | _ = self; 182 | _ = req; 183 | // Redirect to home 184 | try resp.redirect("/"); 185 | } 186 | }; 187 | 188 | /// Work in progress... shows one way to render and post a form. 189 | const FormHandler = struct { 190 | const template = @embedFile("templates/form.html"); 191 | const key = "{% form %}"; 192 | const start = std.mem.indexOf(u8, template, key).?; 193 | const end = start + key.len; 194 | 195 | pub fn get(self: *FormHandler, req: *Request, resp: *Response) !void { 196 | _ = self; 197 | _ = req; 198 | // Split the template on the key 199 | const form = 200 | \\
201 | \\
202 | \\
203 | \\
204 | \\ 205 | \\
206 | ; 207 | try resp.stream.writeAll(template[0..start]); 208 | try resp.stream.writeAll(form); 209 | try resp.stream.writeAll(template[end..]); 210 | } 211 | 212 | pub fn post(self: *FormHandler, req: *Request, resp: *Response) !void { 213 | _ = self; 214 | var content_type = req.headers.getDefault("Content-Type", ""); 215 | if (std.mem.startsWith(u8, content_type, "multipart/form-data")) { 216 | var form = web.forms.Form.init(resp.allocator); 217 | form.parse(req) catch |err| switch (err) { 218 | error.NotImplemented => { 219 | resp.status = web.responses.REQUEST_ENTITY_TOO_LARGE; 220 | try resp.stream.writeAll("TODO: Handle large uploads"); 221 | return; 222 | }, 223 | else => return err, 224 | }; 225 | try resp.stream.writeAll(template[0..start]); 226 | 227 | try resp.stream.print( 228 | \\

Hello: {s}

229 | , .{if (form.fields.get("name")) |name| name else ""}); 230 | 231 | if (form.fields.get("agree")) |f| { 232 | _ = f; 233 | try resp.stream.writeAll("Me too!"); 234 | } else { 235 | try resp.stream.writeAll("Aww sorry!"); 236 | } 237 | try resp.stream.writeAll(template[end..]); 238 | } else { 239 | resp.status = web.responses.BAD_REQUEST; 240 | } 241 | } 242 | }; 243 | 244 | const ChatHandler = struct { 245 | const template = @embedFile("templates/chat.html"); 246 | pub fn get(self: *ChatHandler, req: *Request, resp: *Response) !void { 247 | _ = self; 248 | _ = req; 249 | try resp.stream.writeAll(template); 250 | } 251 | }; 252 | 253 | /// Demonstrates the useage of the websocket protocol 254 | const ChatWebsocketHandler = struct { 255 | var client_id = std.atomic.Atomic(usize).init(0); 256 | var chat_handlers = std.ArrayList(*ChatWebsocketHandler).init(gpa.allocator()); 257 | 258 | websocket: web.Websocket, 259 | stream: ?web.websocket.Writer(1024, .Text) = null, 260 | username: []const u8 = "", 261 | 262 | pub fn selectProtocol(req: *Request, resp: *Response) !void { 263 | _ = req; 264 | try resp.headers.append("Sec-WebSocket-Protocol", "json"); 265 | } 266 | 267 | pub fn connected(self: *ChatWebsocketHandler) !void { 268 | std.log.debug("Websocket connected!", .{}); 269 | 270 | // Initialze the stream 271 | self.stream = self.websocket.writer(1024, .Text); 272 | const stream = &self.stream.?; 273 | 274 | var jw = std.json.writeStream(stream.writer(), 4); 275 | try jw.beginObject(); 276 | try jw.objectField("type"); 277 | try jw.emitString("id"); 278 | try jw.objectField("id"); 279 | try jw.emitNumber(client_id.fetchAdd(1, .Monotonic)); 280 | try jw.objectField("date"); 281 | try jw.emitNumber(std.time.milliTimestamp()); 282 | try jw.endObject(); 283 | try stream.flush(); 284 | 285 | try chat_handlers.append(self); 286 | } 287 | 288 | pub fn onMessage(self: *ChatWebsocketHandler, message: []const u8, binary: bool) !void { 289 | _ = self; 290 | _ = binary; 291 | std.log.debug("Websocket message: {s}", .{message}); 292 | const allocator = self.websocket.response.allocator; 293 | var parser = std.json.Parser.init(allocator, false); 294 | defer parser.deinit(); 295 | var obj = try parser.parse(message); 296 | defer obj.deinit(); 297 | const msg = obj.root.Object; 298 | const t = msg.get("type").?.String; 299 | if (std.mem.eql(u8, t, "message")) { 300 | try self.sendMessage(self.username, msg.get("text").?.String); 301 | } else if (std.mem.eql(u8, t, "username")) { 302 | self.username = try allocator.dupe(u8, msg.get("name").?.String); 303 | try self.sendUserList(); 304 | } 305 | } 306 | 307 | pub fn sendUserList(self: *ChatWebsocketHandler) !void { 308 | _ = self; 309 | const t = std.time.milliTimestamp(); 310 | for (chat_handlers.items) |handler| { 311 | const stream = &handler.stream.?; 312 | var jw = std.json.writeStream(stream.writer(), 4); 313 | try jw.beginObject(); 314 | try jw.objectField("type"); 315 | try jw.emitString("userlist"); 316 | try jw.objectField("users"); 317 | try jw.beginArray(); 318 | for (chat_handlers.items) |obj| { 319 | try jw.arrayElem(); 320 | try jw.emitString(obj.username); 321 | } 322 | try jw.endArray(); 323 | try jw.objectField("date"); 324 | try jw.emitNumber(t); 325 | try jw.endObject(); 326 | try stream.flush(); 327 | } 328 | } 329 | 330 | pub fn sendMessage(self: *ChatWebsocketHandler, name: []const u8, message: []const u8) !void { 331 | _ = self; 332 | const t = std.time.milliTimestamp(); 333 | for (chat_handlers.items) |handler| { 334 | const stream = &handler.stream.?; 335 | var jw = std.json.writeStream(stream.writer(), 4); 336 | try jw.beginObject(); 337 | try jw.objectField("type"); 338 | try jw.emitString("message"); 339 | try jw.objectField("text"); 340 | try jw.emitString(message); 341 | try jw.objectField("name"); 342 | try jw.emitString(name); 343 | try jw.objectField("date"); 344 | try jw.emitNumber(t); 345 | try jw.endObject(); 346 | try stream.flush(); 347 | } 348 | } 349 | 350 | pub fn disconnected(self: *ChatWebsocketHandler) !void { 351 | if (self.websocket.err) |err| { 352 | std.log.debug("Websocket error: {s}", .{err}); 353 | } else { 354 | std.log.debug("Websocket closed!", .{}); 355 | } 356 | 357 | for (chat_handlers.items) |handler, i| { 358 | if (handler == self) { 359 | _ = chat_handlers.swapRemove(i); 360 | break; 361 | } 362 | } 363 | } 364 | }; 365 | 366 | // The routes must be defined in the "root" 367 | pub const routes = [_]web.Route{ 368 | web.Route.create("cover", "/", TemplateHandler), 369 | web.Route.create("hello", "/hello", HelloHandler), 370 | web.Route.create("api", "/api/([a-z]+)/(\\d+)/", ApiHandler), 371 | web.Route.create("json", "/json/", JsonHandler), 372 | web.Route.create("stream", "/stream/", StreamHandler), 373 | web.Route.create("stream-media", "/stream/live/", StreamHandler), 374 | web.Route.create("redirect", "/redirect/", RedirectHandler), 375 | web.Route.create("error", "/500/", ErrorTestHandler), 376 | web.Route.create("form", "/form/", FormHandler), 377 | web.Route.create("chat", "/chat/", ChatHandler), 378 | web.Route.websocket("websocket", "/chat/ws/", ChatWebsocketHandler), 379 | web.Route.static("static", "/static/", "example/static/"), 380 | }; 381 | 382 | pub const middleware = [_]web.Middleware{ 383 | //web.Middleware.create(web.middleware.LoggingMiddleware), 384 | //web.Middleware.create(web.middleware.SessionMiddleware), 385 | }; 386 | 387 | pub fn main() !void { 388 | defer std.debug.assert(!gpa.deinit()); 389 | const allocator = gpa.allocator(); 390 | 391 | var app = web.Application.init(allocator, .{ .debug = true }); 392 | 393 | defer app.deinit(); 394 | try app.listen("127.0.0.1", 9000); 395 | try app.start(); 396 | } 397 | -------------------------------------------------------------------------------- /src/handlers.zig: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------- // 2 | // Copyright (c) 2019-2020, Jairus Martin. // 3 | // Distributed under the terms of the MIT License. // 4 | // The full license is in the file LICENSE, distributed with this software. // 5 | // -------------------------------------------------------------------------- // 6 | const std = @import("std"); 7 | const fs = std.fs; 8 | const mem = std.mem; 9 | const ascii = std.ascii; 10 | const log = std.log; 11 | const web = @import("zhp.zig"); 12 | const responses = web.responses; 13 | const Datetime = web.datetime.Datetime; 14 | 15 | pub var default_stylesheet = @embedFile("templates/style.css"); 16 | 17 | pub const IndexHandler = struct { 18 | pub fn get(self: *IndexHandler, request: *web.Request, response: *web.Response) !void { 19 | _ = self; 20 | _ = request; 21 | try response.stream.writeAll( 22 | \\No routes are defined 23 | \\Please add a list of routes in your main zig file. 24 | ); 25 | } 26 | }; 27 | 28 | pub const ServerErrorHandler = struct { 29 | const TemplateContext = struct { 30 | style: []const u8, 31 | request: *web.Request, 32 | }; 33 | const template = web.template.FileTemplate(TemplateContext, "templates/error.html"); 34 | 35 | server_request: *web.ServerRequest, 36 | 37 | pub fn dispatch(self: *ServerErrorHandler, request: *web.Request, response: *web.Response) anyerror!void { 38 | const app = web.Application.instance.?; 39 | response.status = responses.INTERNAL_SERVER_ERROR; 40 | 41 | // Clear any existing data 42 | try response.body.resize(0); 43 | 44 | if (app.options.debug) { 45 | // Split the template on the key 46 | const context = TemplateContext{ .style = default_stylesheet, .request = request }; 47 | 48 | inline for (template.sections) |part| { 49 | if (part.is("stacktrace")) { 50 | // Dump stack trace 51 | if (self.server_request.err) |err| { 52 | try response.stream.print("error: {s}\n", .{err}); 53 | } 54 | if (@errorReturnTrace()) |trace| { 55 | try std.debug.writeStackTrace(trace.*, &response.stream, response.allocator, try std.debug.getSelfDebugInfo(), .no_color); 56 | } 57 | } else { 58 | try part.render(context, response.stream); 59 | } 60 | } 61 | } else { 62 | if (@errorReturnTrace()) |trace| { 63 | const stderr = std.io.getStdErr().writer(); 64 | const held = std.debug.getStderrMutex(); 65 | held.lock(); 66 | defer held.unlock(); 67 | 68 | try std.debug.writeStackTrace(trace.*, &stderr, response.allocator, try std.debug.getSelfDebugInfo(), std.debug.detectTTYConfig()); 69 | } 70 | 71 | try response.stream.writeAll("

Server Error

"); 72 | } 73 | } 74 | }; 75 | 76 | pub const NotFoundHandler = struct { 77 | const template = @embedFile("templates/not-found.html"); 78 | pub fn dispatch(self: *NotFoundHandler, request: *web.Request, response: *web.Response) !void { 79 | _ = self; 80 | _ = request; 81 | response.status = responses.NOT_FOUND; 82 | try response.stream.print(template, .{default_stylesheet}); 83 | } 84 | }; 85 | 86 | pub fn StaticFileHandler(comptime static_url: []const u8, comptime static_root: []const u8) type { 87 | if (!fs.path.isAbsolute(static_url)) { 88 | @compileError("The static url must be absolute"); 89 | } 90 | // TODO: Should the root be checked if it exists? 91 | return struct { 92 | const Self = @This(); 93 | //handler: web.RequestHandler, 94 | file: ?std.fs.File = null, 95 | start: usize = 0, 96 | end: usize = 0, 97 | server_request: *web.ServerRequest, 98 | 99 | pub fn get(self: *Self, request: *web.Request, response: *web.Response) !void { 100 | const allocator = response.allocator; 101 | const mimetypes = &web.mimetypes.instance.?; 102 | 103 | // Determine path relative to the url root 104 | const rel_path = try fs.path.relative(allocator, static_url, request.path); 105 | 106 | // Cannot be outside the root folder 107 | if (rel_path.len == 0 or rel_path[0] == '.') { 108 | return self.renderNotFound(request, response); 109 | } 110 | 111 | const full_path = try fs.path.join(allocator, &[_][]const u8{ static_root, rel_path }); 112 | 113 | const file = fs.cwd().openFile(full_path, .{ .mode = .read_only }) catch |err| { 114 | // TODO: Handle debug page 115 | log.warn("Static file error: {}", .{err}); 116 | return self.renderNotFound(request, response); 117 | }; 118 | errdefer file.close(); 119 | 120 | // Get file info 121 | const stat = try file.stat(); 122 | var modified = Datetime.fromModifiedTime(stat.mtime); 123 | 124 | // If the file was not modified, return 304 125 | if (self.checkNotModified(request, modified)) { 126 | response.status = web.responses.NOT_MODIFIED; 127 | file.close(); 128 | return; 129 | } 130 | 131 | try response.headers.append("Accept-Ranges", "bytes"); 132 | 133 | // Set etag header 134 | if (self.getETagHeader()) |etag| { 135 | try response.headers.append("ETag", etag); 136 | } 137 | 138 | // Set last modified time for caching purposes 139 | // NOTE: The modified result doesn't need freed since the response handles that 140 | var buf = try response.allocator.alloc(u8, 32); 141 | try response.headers.append("Last-Modified", try modified.formatHttpBuf(buf)); 142 | 143 | // TODO: cache control 144 | 145 | self.end = stat.size; 146 | var size: usize = stat.size; 147 | 148 | if (request.headers.getOptional("Range")) |range_header| { 149 | // As per RFC 2616 14.16, if an invalid Range header is specified, 150 | // the request will be treated as if the header didn't exist. 151 | // response.status = responses.PARTIAL_CONTENT; 152 | if (range_header.len > 8 and mem.startsWith(u8, range_header, "bytes=")) { 153 | var it = mem.split(u8, range_header[6..], ","); 154 | 155 | // Only support the first range 156 | const range = mem.trim(u8, it.next().?, " "); 157 | var tokens = mem.split(u8, range, "-"); 158 | var range_end: ?[]const u8 = null; 159 | 160 | if (range[0] == '-') { 161 | range_end = tokens.next().?; // First one never fails 162 | } else { 163 | const range_start = tokens.next().?; // First one never fails 164 | self.start = std.fmt.parseInt(usize, range_start, 10) catch 0; 165 | range_end = tokens.next(); 166 | } 167 | 168 | if (range_end) |value| { 169 | const end = std.fmt.parseInt(usize, value, 10) catch 0; 170 | if (end > self.start) { 171 | // Clients sometimes blindly use a large range to limit their 172 | // download size; cap the endpoint at the actual file size. 173 | self.end = std.math.min(end, size); 174 | } 175 | } 176 | 177 | if (self.start >= size or self.end <= self.start) { 178 | // A byte-range-spec is invalid if the last-byte-pos value is present 179 | // and less than the first-byte-pos. 180 | // https://tools.ietf.org/html/rfc7233#section-2.1 181 | response.status = web.responses.REQUESTED_RANGE_NOT_SATISFIABLE; 182 | try response.headers.append("Content-Type", "text/plain"); 183 | try response.headers.append("Content-Range", try std.fmt.allocPrint(allocator, "bytes */{}", .{size})); 184 | file.close(); 185 | return; 186 | } 187 | 188 | // Determine the actual size 189 | size = self.end - self.start; 190 | 191 | if (size != stat.size) { 192 | // If it's not the full file se it as a partial response 193 | response.status = web.responses.PARTIAL_CONTENT; 194 | try response.headers.append("Content-Range", try std.fmt.allocPrint(allocator, "bytes {}-{}/{}", .{ self.start, self.end, size })); 195 | } 196 | } 197 | } 198 | 199 | // Try to get the content type 200 | const content_type = mimetypes.getTypeFromFilename(full_path) orelse "application/octet-stream"; 201 | try response.headers.append("Content-Type", content_type); 202 | try response.headers.append("Content-Length", try std.fmt.allocPrint(allocator, "{}", .{size})); 203 | self.file = file; 204 | response.send_stream = true; 205 | } 206 | 207 | // Return true if not modified and a 304 can be returned 208 | pub fn checkNotModified(self: *Self, request: *web.Request, mtime: Datetime) bool { 209 | // If client sent If-None-Match, use it, ignore If-Modified-Since 210 | // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag 211 | if (request.headers.getOptional("If-None-Match")) |etag| { 212 | return self.checkETagHeader(etag); 213 | } 214 | 215 | // Check if the file was modified since the header 216 | // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since 217 | const v = request.headers.getDefault("If-Modified-Since", ""); 218 | const since = Datetime.parseModifiedSince(v) catch return false; 219 | return since.gte(mtime); 220 | } 221 | 222 | // Get a hash of the file 223 | pub fn getETagHeader(self: *Self) ?[]const u8 { 224 | _ = self; 225 | // TODO: This 226 | return null; 227 | } 228 | 229 | pub fn checkETagHeader(self: *Self, etag: []const u8) bool { 230 | // TODO: Support other formats 231 | if (self.getETagHeader()) |tag| { 232 | return mem.eql(u8, tag, etag); 233 | } 234 | return false; 235 | } 236 | 237 | // Stream the file 238 | pub fn stream(self: *Self, io: *web.IOStream) !usize { 239 | std.debug.assert(self.end > self.start); 240 | const total_wrote = self.end - self.start; 241 | var bytes_left: usize = total_wrote; 242 | if (self.file) |file| { 243 | defer file.close(); 244 | 245 | // Jump to requested range 246 | if (self.start > 0) { 247 | try file.seekTo(self.start); 248 | } 249 | 250 | // Send it 251 | var reader = file.reader(); 252 | try io.flush(); 253 | while (bytes_left > 0) { 254 | // Read into buffer 255 | const end = std.math.min(bytes_left, io.out_buffer.len); 256 | const n = try reader.read(io.out_buffer[0..end]); 257 | if (n == 0) break; // Unexpected EOF 258 | bytes_left -= n; 259 | try io.flushBuffered(n); 260 | } 261 | } 262 | return total_wrote - bytes_left; 263 | } 264 | 265 | pub fn renderNotFound(self: *Self, request: *web.Request, response: *web.Response) !void { 266 | _ = self; 267 | var handler = NotFoundHandler{}; 268 | try handler.dispatch(request, response); 269 | } 270 | }; 271 | } 272 | 273 | /// Handles a websocket connection 274 | /// See https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers 275 | pub fn WebsocketHandler(comptime Protocol: type) type { 276 | return struct { 277 | const Self = @This(); 278 | 279 | // The app will allocate this in the response's allocator buffer 280 | accept_key: [28]u8 = undefined, 281 | server_request: *web.ServerRequest, 282 | 283 | pub fn get(self: *Self, request: *web.Request, response: *web.Response) !void { 284 | return self.doHandshake(request, response) catch |err| switch (err) { 285 | error.BadRequest => self.respondError(web.responses.BAD_REQUEST), 286 | error.Forbidden => self.respondError(web.responses.FORBIDDEN), 287 | error.UpgradeRequired => self.respondError(web.responses.UPGRADE_REQUIRED), 288 | else => err, 289 | }; 290 | } 291 | 292 | fn respondError(self: *Self, status: web.responses.Status) void { 293 | self.server_request.request.read_finished = true; // Skip reading the body 294 | self.server_request.response.disconnect_on_finish = true; 295 | self.server_request.response.status = status; 296 | } 297 | 298 | fn doHandshake(self: *Self, request: *web.Request, response: *web.Response) !void { 299 | // Check upgrade headers 300 | try self.checkUpgradeHeaders(request); 301 | 302 | // Make sure this is not a cross origin request 303 | if (!self.checkOrigin(request)) { 304 | return error.Forbidden; // Cross origin websockets are forbidden 305 | } 306 | 307 | // Check websocket version 308 | const version = try self.getWebsocketVersion(request); 309 | switch (version) { 310 | 7, 8, 13 => {}, 311 | else => { 312 | // Unsupported version 313 | // Set header to indicate to the client which versions are supported 314 | try response.headers.append("Sec-WebSocket-Version", "7, 8, 13"); 315 | return error.UpgradeRequired; 316 | }, 317 | } 318 | 319 | // Create the accept key 320 | const key = try self.getWebsocketAcceptKey(request); 321 | 322 | // At this point the connection is valid so switch to stream mode 323 | try response.headers.append("Connection", "Upgrade"); 324 | try response.headers.append("Upgrade", "websocket"); 325 | try response.headers.append("Sec-WebSocket-Accept", key); 326 | 327 | // Optionally select a subprotocol 328 | // The function should set the Sec-WebSocket-Protocol 329 | // or return BadRequest 330 | if (@hasDecl(Protocol, "selectProtocol")) { 331 | try Protocol.selectProtocol(request, response); 332 | } 333 | 334 | response.send_stream = true; 335 | response.status = web.responses.SWITCHING_PROTOCOLS; 336 | } 337 | 338 | fn checkUpgradeHeaders(self: *Self, request: *web.Request) !void { 339 | _ = self; 340 | if (!request.headers.eqlIgnoreCase("Upgrade", "websocket")) { 341 | log.debug("Cannot only upgrade to 'websocket'", .{}); 342 | return error.BadRequest; // Can only upgrade to websocket 343 | } 344 | 345 | // Some proxies/load balancers will mess with the connection header 346 | // and browsers also send multiple values here 347 | const header = request.headers.getDefault("Connection", ""); 348 | var it = std.mem.split(u8, header, ","); 349 | while (it.next()) |part| { 350 | const conn = std.mem.trim(u8, part, " "); 351 | if (ascii.eqlIgnoreCase(conn, "upgrade")) { 352 | return; 353 | } 354 | } 355 | // If we didn't find it, give an error 356 | log.debug("Connection must be 'upgrade'", .{}); 357 | return error.BadRequest; // Connection must be upgrade 358 | } 359 | 360 | /// As a safety measure make sure the origin header matches the host header 361 | fn checkOrigin(self: *Self, request: *web.Request) bool { 362 | _ = self; 363 | if (@hasDecl(Protocol, "checkOrigin")) { 364 | return Protocol.checkOrigin(request); 365 | } else { 366 | // Version 13 uses "Origin", others use "Sec-Websocket-Origin" 367 | var origin = web.url.findHost(if (request.headers.getOptional("Origin")) |o| o else request.headers.getDefault("Sec-Websocket-Origin", "")); 368 | 369 | const host = request.headers.getDefault("Host", ""); 370 | if (origin.len == 0 or host.len == 0 or !ascii.eqlIgnoreCase(origin, host)) { 371 | log.debug("Cross origin websockets are not allowed ('{s}' != '{s}')", .{ origin, host }); 372 | return false; 373 | } 374 | return true; 375 | } 376 | } 377 | 378 | fn getWebsocketVersion(self: *Self, request: *web.Request) !u8 { 379 | _ = self; 380 | const v = request.headers.getDefault("Sec-WebSocket-Version", ""); 381 | return std.fmt.parseInt(u8, v, 10) catch error.BadRequest; 382 | } 383 | 384 | fn getWebsocketAcceptKey(self: *Self, request: *web.Request) ![]const u8 { 385 | const key = request.headers.getDefault("Sec-WebSocket-Key", ""); 386 | if (key.len < 8) { 387 | // TODO: Must it be a certain length? 388 | log.debug("Insufficent websocket key length", .{}); 389 | return error.BadRequest; 390 | } 391 | 392 | var hash = std.crypto.hash.Sha1.init(.{}); 393 | var out: [20]u8 = undefined; 394 | hash.update(key); 395 | hash.update("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); 396 | hash.final(&out); 397 | 398 | // Encode it 399 | return std.base64.standard.Encoder.encode(&self.accept_key, &out); 400 | } 401 | 402 | pub fn stream(self: *Self, io: *web.IOStream) !usize { 403 | const request = &self.server_request.request; 404 | const response = &self.server_request.response; 405 | 406 | // Always close 407 | defer io.close(); 408 | 409 | // Flush the request 410 | try io.flush(); 411 | 412 | // Delegate handler 413 | var protocol = Protocol{ 414 | .websocket = web.Websocket{ 415 | .request = request, 416 | .response = response, 417 | .io = io, 418 | }, 419 | }; 420 | try protocol.connected(); 421 | self.processStream(&protocol) catch |err| { 422 | protocol.websocket.err = err; 423 | }; 424 | try protocol.disconnected(); 425 | return 0; 426 | } 427 | 428 | fn processStream(self: *Self, protocol: *Protocol) !void { 429 | _ = self; 430 | const ws = &protocol.websocket; 431 | while (true) { 432 | const dataframe = try ws.readDataFrame(); 433 | if (@hasDecl(Protocol, "onDataFrame")) { 434 | // Let the user handle it 435 | try protocol.onDataFrame(dataframe); 436 | } else { 437 | switch (dataframe.header.opcode) { 438 | .Text => try protocol.onMessage(dataframe.data, false), 439 | .Binary => try protocol.onMessage(dataframe.data, true), 440 | .Ping => { 441 | _ = try ws.writeMessage(.Pong, ""); 442 | }, 443 | .Pong => { 444 | _ = try ws.writeMessage(.Ping, ""); 445 | }, 446 | .Close => { 447 | try ws.close(1000); 448 | break; // Client requsted close 449 | }, 450 | else => return error.UnexpectedOpcode, 451 | } 452 | } 453 | } 454 | } 455 | }; 456 | } 457 | -------------------------------------------------------------------------------- /tests/http-requests.txt: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Host: www.reddit.com 3 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 4 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 5 | Accept-Language: en-us,en;q=0.5 6 | Accept-Encoding: gzip, deflate 7 | Connection: keep-alive 8 | 9 | GET /reddit.v_EZwRzV-Ns.css HTTP/1.1 10 | Host: www.redditstatic.com 11 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 12 | Accept: text/css,*/*;q=0.1 13 | Accept-Language: en-us,en;q=0.5 14 | Accept-Encoding: gzip, deflate 15 | Connection: keep-alive 16 | Referer: http://www.reddit.com/ 17 | 18 | GET /reddit-init.en-us.O1zuMqOOQvY.js HTTP/1.1 19 | Host: www.redditstatic.com 20 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 21 | Accept: */* 22 | Accept-Language: en-us,en;q=0.5 23 | Accept-Encoding: gzip, deflate 24 | Connection: keep-alive 25 | Referer: http://www.reddit.com/ 26 | 27 | GET /reddit.en-us.31yAfSoTsfo.js HTTP/1.1 28 | Host: www.redditstatic.com 29 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 30 | Accept: */* 31 | Accept-Language: en-us,en;q=0.5 32 | Accept-Encoding: gzip, deflate 33 | Connection: keep-alive 34 | Referer: http://www.reddit.com/ 35 | 36 | GET /kill.png HTTP/1.1 37 | Host: www.redditstatic.com 38 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 39 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 40 | Accept-Language: en-us,en;q=0.5 41 | Accept-Encoding: gzip, deflate 42 | Connection: keep-alive 43 | Referer: http://www.reddit.com/ 44 | 45 | GET /icon.png HTTP/1.1 46 | Host: www.redditstatic.com 47 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 48 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 49 | Accept-Language: en-us,en;q=0.5 50 | Accept-Encoding: gzip, deflate 51 | Connection: keep-alive 52 | 53 | GET /favicon.ico HTTP/1.1 54 | Host: www.redditstatic.com 55 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 56 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 57 | Accept-Language: en-us,en;q=0.5 58 | Accept-Encoding: gzip, deflate 59 | Connection: keep-alive 60 | 61 | GET /AMZM4CWd6zstSC8y.jpg HTTP/1.1 62 | Host: b.thumbs.redditmedia.com 63 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 64 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 65 | Accept-Language: en-us,en;q=0.5 66 | Accept-Encoding: gzip, deflate 67 | Connection: keep-alive 68 | Referer: http://www.reddit.com/ 69 | 70 | GET /jz1d5Nm0w97-YyNm.jpg HTTP/1.1 71 | Host: b.thumbs.redditmedia.com 72 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 73 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 74 | Accept-Language: en-us,en;q=0.5 75 | Accept-Encoding: gzip, deflate 76 | Connection: keep-alive 77 | Referer: http://www.reddit.com/ 78 | 79 | GET /aWGO99I6yOcNUKXB.jpg HTTP/1.1 80 | Host: a.thumbs.redditmedia.com 81 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 82 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 83 | Accept-Language: en-us,en;q=0.5 84 | Accept-Encoding: gzip, deflate 85 | Connection: keep-alive 86 | Referer: http://www.reddit.com/ 87 | 88 | GET /rZ_rD5TjrJM0E9Aj.css HTTP/1.1 89 | Host: e.thumbs.redditmedia.com 90 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 91 | Accept: text/css,*/*;q=0.1 92 | Accept-Language: en-us,en;q=0.5 93 | Accept-Encoding: gzip, deflate 94 | Connection: keep-alive 95 | Referer: http://www.reddit.com/ 96 | 97 | GET /tmsPwagFzyTvrGRx.jpg HTTP/1.1 98 | Host: a.thumbs.redditmedia.com 99 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 100 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 101 | Accept-Language: en-us,en;q=0.5 102 | Accept-Encoding: gzip, deflate 103 | Connection: keep-alive 104 | Referer: http://www.reddit.com/ 105 | 106 | GET /KYgUaLvXCK3TCEJx.jpg HTTP/1.1 107 | Host: a.thumbs.redditmedia.com 108 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 109 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 110 | Accept-Language: en-us,en;q=0.5 111 | Accept-Encoding: gzip, deflate 112 | Connection: keep-alive 113 | Referer: http://www.reddit.com/ 114 | 115 | GET /81pzxT5x2ozuEaxX.jpg HTTP/1.1 116 | Host: e.thumbs.redditmedia.com 117 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 118 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 119 | Accept-Language: en-us,en;q=0.5 120 | Accept-Encoding: gzip, deflate 121 | Connection: keep-alive 122 | Referer: http://www.reddit.com/ 123 | 124 | GET /MFqCUiUVPO5V8t6x.jpg HTTP/1.1 125 | Host: a.thumbs.redditmedia.com 126 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 127 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 128 | Accept-Language: en-us,en;q=0.5 129 | Accept-Encoding: gzip, deflate 130 | Connection: keep-alive 131 | Referer: http://www.reddit.com/ 132 | 133 | GET /TFpYTiAO5aEowokv.jpg HTTP/1.1 134 | Host: e.thumbs.redditmedia.com 135 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 136 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 137 | Accept-Language: en-us,en;q=0.5 138 | Accept-Encoding: gzip, deflate 139 | Connection: keep-alive 140 | Referer: http://www.reddit.com/ 141 | 142 | GET /eMWMpmm9APNeNqcF.jpg HTTP/1.1 143 | Host: e.thumbs.redditmedia.com 144 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 145 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 146 | Accept-Language: en-us,en;q=0.5 147 | Accept-Encoding: gzip, deflate 148 | Connection: keep-alive 149 | Referer: http://www.reddit.com/ 150 | 151 | GET /S-IpsJrOKuaK9GZ8.jpg HTTP/1.1 152 | Host: c.thumbs.redditmedia.com 153 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 154 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 155 | Accept-Language: en-us,en;q=0.5 156 | Accept-Encoding: gzip, deflate 157 | Connection: keep-alive 158 | Referer: http://www.reddit.com/ 159 | 160 | GET /3V6dj9PDsNnheDXn.jpg HTTP/1.1 161 | Host: c.thumbs.redditmedia.com 162 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 163 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 164 | Accept-Language: en-us,en;q=0.5 165 | Accept-Encoding: gzip, deflate 166 | Connection: keep-alive 167 | Referer: http://www.reddit.com/ 168 | 169 | GET /wQ3-VmNXhv8sg4SJ.jpg HTTP/1.1 170 | Host: c.thumbs.redditmedia.com 171 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 172 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 173 | Accept-Language: en-us,en;q=0.5 174 | Accept-Encoding: gzip, deflate 175 | Connection: keep-alive 176 | Referer: http://www.reddit.com/ 177 | 178 | GET /ixd1C1njpczEWC22.jpg HTTP/1.1 179 | Host: c.thumbs.redditmedia.com 180 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 181 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 182 | Accept-Language: en-us,en;q=0.5 183 | Accept-Encoding: gzip, deflate 184 | Connection: keep-alive 185 | Referer: http://www.reddit.com/ 186 | 187 | GET /nGsQj15VyOHMwmq8.jpg HTTP/1.1 188 | Host: c.thumbs.redditmedia.com 189 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 190 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 191 | Accept-Language: en-us,en;q=0.5 192 | Accept-Encoding: gzip, deflate 193 | Connection: keep-alive 194 | Referer: http://www.reddit.com/ 195 | 196 | GET /zT4yQmDxQLbIxK1b.jpg HTTP/1.1 197 | Host: c.thumbs.redditmedia.com 198 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 199 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 200 | Accept-Language: en-us,en;q=0.5 201 | Accept-Encoding: gzip, deflate 202 | Connection: keep-alive 203 | Referer: http://www.reddit.com/ 204 | 205 | GET /L5e1HcZLv1iu4nrG.jpg HTTP/1.1 206 | Host: f.thumbs.redditmedia.com 207 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 208 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 209 | Accept-Language: en-us,en;q=0.5 210 | Accept-Encoding: gzip, deflate 211 | Connection: keep-alive 212 | Referer: http://www.reddit.com/ 213 | 214 | GET /WJFFPxD8X4JO_lIG.jpg HTTP/1.1 215 | Host: f.thumbs.redditmedia.com 216 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 217 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 218 | Accept-Language: en-us,en;q=0.5 219 | Accept-Encoding: gzip, deflate 220 | Connection: keep-alive 221 | Referer: http://www.reddit.com/ 222 | 223 | GET /hVMVTDdjuY3bQox5.jpg HTTP/1.1 224 | Host: f.thumbs.redditmedia.com 225 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 226 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 227 | Accept-Language: en-us,en;q=0.5 228 | Accept-Encoding: gzip, deflate 229 | Connection: keep-alive 230 | Referer: http://www.reddit.com/ 231 | 232 | GET /rnWf8CjBcyPQs5y_.jpg HTTP/1.1 233 | Host: f.thumbs.redditmedia.com 234 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 235 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 236 | Accept-Language: en-us,en;q=0.5 237 | Accept-Encoding: gzip, deflate 238 | Connection: keep-alive 239 | Referer: http://www.reddit.com/ 240 | 241 | GET /gZJL1jNylKbGV4d-.jpg HTTP/1.1 242 | Host: d.thumbs.redditmedia.com 243 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 244 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 245 | Accept-Language: en-us,en;q=0.5 246 | Accept-Encoding: gzip, deflate 247 | Connection: keep-alive 248 | Referer: http://www.reddit.com/ 249 | 250 | GET /aNd2zNRLXiMnKUFh.jpg HTTP/1.1 251 | Host: c.thumbs.redditmedia.com 252 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 253 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 254 | Accept-Language: en-us,en;q=0.5 255 | Accept-Encoding: gzip, deflate 256 | Connection: keep-alive 257 | Referer: http://www.reddit.com/ 258 | 259 | GET /droparrowgray.gif HTTP/1.1 260 | Host: www.redditstatic.com 261 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 262 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 263 | Accept-Language: en-us,en;q=0.5 264 | Accept-Encoding: gzip, deflate 265 | Connection: keep-alive 266 | Referer: http://www.redditstatic.com/reddit.v_EZwRzV-Ns.css 267 | 268 | GET /sprite-reddit.an0Lnf61Ap4.png HTTP/1.1 269 | Host: www.redditstatic.com 270 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 271 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 272 | Accept-Language: en-us,en;q=0.5 273 | Accept-Encoding: gzip, deflate 274 | Connection: keep-alive 275 | Referer: http://www.redditstatic.com/reddit.v_EZwRzV-Ns.css 276 | 277 | GET /ga.js HTTP/1.1 278 | Host: www.google-analytics.com 279 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 280 | Accept: */* 281 | Accept-Language: en-us,en;q=0.5 282 | Accept-Encoding: gzip, deflate 283 | Connection: keep-alive 284 | Referer: http://www.reddit.com/ 285 | If-Modified-Since: Tue, 29 Oct 2013 19:33:51 GMT 286 | 287 | GET /reddit/ads.html?sr=-reddit.com&bust2 HTTP/1.1 288 | Host: static.adzerk.net 289 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 290 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 291 | Accept-Language: en-us,en;q=0.5 292 | Accept-Encoding: gzip, deflate 293 | Connection: keep-alive 294 | Referer: http://www.reddit.com/ 295 | 296 | GET /pixel/of_destiny.png?v=hOlmDALJCWWdjzfBV4ZxJPmrdCLWB%2Ftq7Z%2Ffp4Q%2FxXbVPPREuMJMVGzKraTuhhNWxCCwi6yFEZg%3D&r=783333388 HTTP/1.1 297 | Host: pixel.redditmedia.com 298 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 299 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 300 | Accept-Language: en-us,en;q=0.5 301 | Accept-Encoding: gzip, deflate 302 | Connection: keep-alive 303 | Referer: http://www.reddit.com/ 304 | 305 | GET /UNcO-h_QcS9PD-Gn.jpg HTTP/1.1 306 | Host: c.thumbs.redditmedia.com 307 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 308 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 309 | Accept-Language: en-us,en;q=0.5 310 | Accept-Encoding: gzip, deflate 311 | Connection: keep-alive 312 | Referer: http://e.thumbs.redditmedia.com/rZ_rD5TjrJM0E9Aj.css 313 | 314 | GET /welcome-lines.png HTTP/1.1 315 | Host: www.redditstatic.com 316 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 317 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 318 | Accept-Language: en-us,en;q=0.5 319 | Accept-Encoding: gzip, deflate 320 | Connection: keep-alive 321 | Referer: http://www.redditstatic.com/reddit.v_EZwRzV-Ns.css 322 | 323 | GET /welcome-upvote.png HTTP/1.1 324 | Host: www.redditstatic.com 325 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 326 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 327 | Accept-Language: en-us,en;q=0.5 328 | Accept-Encoding: gzip, deflate 329 | Connection: keep-alive 330 | Referer: http://www.redditstatic.com/reddit.v_EZwRzV-Ns.css 331 | 332 | GET /__utm.gif?utmwv=5.5.1&utms=1&utmn=720496082&utmhn=www.reddit.com&utme=8(site*srpath*usertype*uitype)9(%20reddit.com*%20reddit.com-GET_listing*guest*web)11(3!2)&utmcs=UTF-8&utmsr=2560x1600&utmvp=1288x792&utmsc=24-bit&utmul=en-us&utmje=1&utmfl=13.0%20r0&utmdt=reddit%3A%20the%20front%20page%20of%20the%20internet&utmhid=2129416330&utmr=-&utmp=%2F&utmht=1400862512705&utmac=UA-12131688-1&utmcc=__utma%3D55650728.585571751.1400862513.1400862513.1400862513.1%3B%2B__utmz%3D55650728.1400862513.1.1.utmcsr%3D(direct)%7Cutmccn%3D(direct)%7Cutmcmd%3D(none)%3B&utmu=qR~ HTTP/1.1 333 | Host: www.google-analytics.com 334 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 335 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 336 | Accept-Language: en-us,en;q=0.5 337 | Accept-Encoding: gzip, deflate 338 | Connection: keep-alive 339 | Referer: http://www.reddit.com/ 340 | 341 | GET /ImnpOQhbXUPkwceN.png HTTP/1.1 342 | Host: a.thumbs.redditmedia.com 343 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 344 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 345 | Accept-Language: en-us,en;q=0.5 346 | Accept-Encoding: gzip, deflate 347 | Connection: keep-alive 348 | Referer: http://www.reddit.com/ 349 | 350 | GET /ajax/libs/jquery/1.7.1/jquery.min.js HTTP/1.1 351 | Host: ajax.googleapis.com 352 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 353 | Accept: */* 354 | Accept-Language: en-us,en;q=0.5 355 | Accept-Encoding: gzip, deflate 356 | Connection: keep-alive 357 | Referer: http://static.adzerk.net/reddit/ads.html?sr=-reddit.com&bust2 358 | 359 | GET /__utm.gif?utmwv=5.5.1&utms=2&utmn=1493472678&utmhn=www.reddit.com&utmt=event&utme=5(AdBlock*enabled*false)(0)8(site*srpath*usertype*uitype)9(%20reddit.com*%20reddit.com-GET_listing*guest*web)11(3!2)&utmcs=UTF-8&utmsr=2560x1600&utmvp=1288x792&utmsc=24-bit&utmul=en-us&utmje=1&utmfl=13.0%20r0&utmdt=reddit%3A%20the%20front%20page%20of%20the%20internet&utmhid=2129416330&utmr=-&utmp=%2F&utmht=1400862512708&utmac=UA-12131688-1&utmni=1&utmcc=__utma%3D55650728.585571751.1400862513.1400862513.1400862513.1%3B%2B__utmz%3D55650728.1400862513.1.1.utmcsr%3D(direct)%7Cutmccn%3D(direct)%7Cutmcmd%3D(none)%3B&utmu=6R~ HTTP/1.1 360 | Host: www.google-analytics.com 361 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 362 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 363 | Accept-Language: en-us,en;q=0.5 364 | Accept-Encoding: gzip, deflate 365 | Connection: keep-alive 366 | Referer: http://www.reddit.com/ 367 | 368 | GET /ados.js?q=43 HTTP/1.1 369 | Host: secure.adzerk.net 370 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 371 | Accept: */* 372 | Accept-Language: en-us,en;q=0.5 373 | Accept-Encoding: gzip, deflate 374 | Connection: keep-alive 375 | Referer: http://static.adzerk.net/reddit/ads.html?sr=-reddit.com&bust2 376 | 377 | GET /fetch-trackers?callback=jQuery111005268222517967478_1400862512407&ids%5B%5D=t3_25jzeq-t8_k2ii&_=1400862512408 HTTP/1.1 378 | Host: tracker.redditmedia.com 379 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 380 | Accept: */* 381 | Accept-Language: en-us,en;q=0.5 382 | Accept-Encoding: gzip, deflate 383 | Connection: keep-alive 384 | Referer: http://www.reddit.com/ 385 | 386 | GET /ados?t=1400862512892&request={%22Placements%22:[{%22A%22:5146,%22S%22:24950,%22D%22:%22main%22,%22AT%22:5},{%22A%22:5146,%22S%22:24950,%22D%22:%22sponsorship%22,%22AT%22:8}],%22Keywords%22:%22-reddit.com%22,%22Referrer%22:%22http%3A%2F%2Fwww.reddit.com%2F%22,%22IsAsync%22:true,%22WriteResults%22:true} HTTP/1.1 387 | Host: engine.adzerk.net 388 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 389 | Accept: */* 390 | Accept-Language: en-us,en;q=0.5 391 | Accept-Encoding: gzip, deflate 392 | Connection: keep-alive 393 | Referer: http://static.adzerk.net/reddit/ads.html?sr=-reddit.com&bust2 394 | 395 | GET /pixel/of_doom.png?id=t3_25jzeq-t8_k2ii&hash=da31d967485cdbd459ce1e9a5dde279fef7fc381&r=1738649500 HTTP/1.1 396 | Host: pixel.redditmedia.com 397 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 398 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 399 | Accept-Language: en-us,en;q=0.5 400 | Accept-Encoding: gzip, deflate 401 | Connection: keep-alive 402 | Referer: http://www.reddit.com/ 403 | 404 | GET /Extensions/adFeedback.js HTTP/1.1 405 | Host: static.adzrk.net 406 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 407 | Accept: */* 408 | Accept-Language: en-us,en;q=0.5 409 | Accept-Encoding: gzip, deflate 410 | Connection: keep-alive 411 | Referer: http://static.adzerk.net/reddit/ads.html?sr=-reddit.com&bust2 412 | 413 | GET /Extensions/adFeedback.css HTTP/1.1 414 | Host: static.adzrk.net 415 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 416 | Accept: text/css,*/*;q=0.1 417 | Accept-Language: en-us,en;q=0.5 418 | Accept-Encoding: gzip, deflate 419 | Connection: keep-alive 420 | Referer: http://static.adzerk.net/reddit/ads.html?sr=-reddit.com&bust2 421 | 422 | GET /reddit/ads-load.html?bust2 HTTP/1.1 423 | Host: static.adzerk.net 424 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 425 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 426 | Accept-Language: en-us,en;q=0.5 427 | Accept-Encoding: gzip, deflate 428 | Connection: keep-alive 429 | Referer: http://www.reddit.com/ 430 | 431 | GET /Advertisers/a774d7d6148046efa89403a8db635a81.jpg HTTP/1.1 432 | Host: static.adzerk.net 433 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 434 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 435 | Accept-Language: en-us,en;q=0.5 436 | Accept-Encoding: gzip, deflate 437 | Connection: keep-alive 438 | Referer: http://static.adzerk.net/reddit/ads.html?sr=-reddit.com&bust2 439 | 440 | GET /i.gif?e=eyJhdiI6NjIzNTcsImF0Ijo1LCJjbSI6MTE2MzUxLCJjaCI6Nzk4NCwiY3IiOjMzNzAxNSwiZGkiOiI4NmI2Y2UzYWM5NDM0MjhkOTk2ZTg4MjYwZDE5ZTE1YyIsImRtIjoxLCJmYyI6NDE2MTI4LCJmbCI6MjEwNDY0LCJrdyI6Ii1yZWRkaXQuY29tIiwibWsiOiItcmVkZGl0LmNvbSIsIm53Ijo1MTQ2LCJwYyI6MCwicHIiOjIwMzYyLCJydCI6MSwicmYiOiJodHRwOi8vd3d3LnJlZGRpdC5jb20vIiwic3QiOjI0OTUwLCJ1ayI6InVlMS01ZWIwOGFlZWQ5YTc0MDFjOTE5NWNiOTMzZWI3Yzk2NiIsInRzIjoxNDAwODYyNTkzNjQ1fQ&s=lwlbFf2Uywt7zVBFRj_qXXu7msY HTTP/1.1 441 | Host: engine.adzerk.net 442 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 443 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 444 | Accept-Language: en-us,en;q=0.5 445 | Accept-Encoding: gzip, deflate 446 | Connection: keep-alive 447 | Referer: http://static.adzerk.net/reddit/ads.html?sr=-reddit.com&bust2 448 | Cookie: azk=ue1-5eb08aeed9a7401c9195cb933eb7c966 449 | 450 | GET /BurstingPipe/adServer.bs?cn=tf&c=19&mc=imp&pli=9994987&PluID=0&ord=1400862593644&rtu=-1 HTTP/1.1 451 | Host: bs.serving-sys.com 452 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 453 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 454 | Accept-Language: en-us,en;q=0.5 455 | Accept-Encoding: gzip, deflate 456 | Connection: keep-alive 457 | Referer: http://static.adzerk.net/reddit/ads.html?sr=-reddit.com&bust2 458 | 459 | GET /Advertisers/63cfd0044ffd49c0a71a6626f7a1d8f0.jpg HTTP/1.1 460 | Host: static.adzerk.net 461 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 462 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 463 | Accept-Language: en-us,en;q=0.5 464 | Accept-Encoding: gzip, deflate 465 | Connection: keep-alive 466 | Referer: http://static.adzerk.net/reddit/ads-load.html?bust2 467 | 468 | GET /BurstingPipe/adServer.bs?cn=tf&c=19&mc=imp&pli=9962555&PluID=0&ord=1400862593645&rtu=-1 HTTP/1.1 469 | Host: bs.serving-sys.com 470 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 471 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 472 | Accept-Language: en-us,en;q=0.5 473 | Accept-Encoding: gzip, deflate 474 | Connection: keep-alive 475 | Referer: http://static.adzerk.net/reddit/ads-load.html?bust2 476 | Cookie: S_9994987=6754579095859875029; A4=01fmFvgRnI09SF00000; u2=d1263d39-874b-4a89-86cd-a2ab0860ed4e3Zl040 477 | 478 | GET /i.gif?e=eyJhdiI6NjIzNTcsImF0Ijo4LCJjbSI6MTE2MzUxLCJjaCI6Nzk4NCwiY3IiOjMzNzAxOCwiZGkiOiI3OTdlZjU3OWQ5NjE0ODdiODYyMGMyMGJkOTE4YzNiMSIsImRtIjoxLCJmYyI6NDE2MTMxLCJmbCI6MjEwNDY0LCJrdyI6Ii1yZWRkaXQuY29tIiwibWsiOiItcmVkZGl0LmNvbSIsIm53Ijo1MTQ2LCJwYyI6MCwicHIiOjIwMzYyLCJydCI6MSwicmYiOiJodHRwOi8vd3d3LnJlZGRpdC5jb20vIiwic3QiOjI0OTUwLCJ1ayI6InVlMS01ZWIwOGFlZWQ5YTc0MDFjOTE5NWNiOTMzZWI3Yzk2NiIsInRzIjoxNDAwODYyNTkzNjQ2fQ&s=OjzxzXAgQksbdQOHNm-bjZcnZPA HTTP/1.1 479 | Host: engine.adzerk.net 480 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:15.0) Gecko/20100101 Firefox/15.0.1 481 | Accept: image/png,image/*;q=0.8,*/*;q=0.5 482 | Accept-Language: en-us,en;q=0.5 483 | Accept-Encoding: gzip, deflate 484 | Connection: keep-alive 485 | Referer: http://static.adzerk.net/reddit/ads-load.html?bust2 486 | Cookie: azk=ue1-5eb08aeed9a7401c9195cb933eb7c966 487 | 488 | GET /subscribe?host_int=1042356184&ns_map=571794054_374233948806,464381511_13349283399&user_id=245722467&nid=1399334269710011966&ts=1400862514 HTTP/1.1 489 | Host: notify8.dropbox.com 490 | Accept-Encoding: identity 491 | Connection: keep-alive 492 | X-Dropbox-Locale: en_US 493 | User-Agent: DropboxDesktopClient/2.7.54 (Macintosh; 10.8; ('i32',); en_US) 494 | -------------------------------------------------------------------------------- /src/util.zig: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------- // 2 | // Copyright (c) 2019-2020, Jairus Martin. // 3 | // Distributed under the terms of the MIT License. // 4 | // The full license is in the file LICENSE, distributed with this software. // 5 | // -------------------------------------------------------------------------- // 6 | const std = @import("std"); 7 | const builtin = @import("builtin"); 8 | const mem = std.mem; 9 | const math = std.math; 10 | const testing = std.testing; 11 | const Allocator = std.mem.Allocator; 12 | const Stream = std.net.Stream; 13 | const assert = std.debug.assert; 14 | 15 | pub const Bytes = std.ArrayList(u8); 16 | pub const native_endian = builtin.target.cpu.arch.endian(); 17 | 18 | pub inline fn isCtrlChar(ch: u8) bool { 19 | return (ch < @as(u8, 40) and ch != '\t') or ch == @as(u8, 177); 20 | } 21 | 22 | test "is-control-char" { 23 | try testing.expect(isCtrlChar('A') == false); 24 | try testing.expect(isCtrlChar('\t') == false); 25 | try testing.expect(isCtrlChar('\r') == true); 26 | } 27 | 28 | const token_map = [_]u1{ 29 | // 0, 1, 2, 3, 4, 5, 6, 7 ,8, 9,10,11,12,13,14,15 30 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 31 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32 | 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 33 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 34 | 35 | 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 36 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 37 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 38 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 39 | 40 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 44 | 45 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 46 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 49 | }; 50 | 51 | pub inline fn isTokenChar(ch: u8) bool { 52 | return token_map[ch] == 1; 53 | } 54 | 55 | pub const IOStream = struct { 56 | pub const invalid_stream = Stream{ .handle = 0 }; 57 | pub const Error = Stream.WriteError; 58 | pub const ReadError = Stream.ReadError; 59 | const Self = @This(); 60 | 61 | allocator: ?Allocator = null, 62 | in_buffer: []u8 = undefined, 63 | out_buffer: []u8 = undefined, 64 | _in_start_index: usize = 0, 65 | _in_end_index: usize = 0, 66 | _in_count: usize = 0, 67 | _out_count: usize = 0, 68 | _out_index: usize = 0, 69 | closed: bool = false, 70 | owns_in_buffer: bool = true, 71 | unbuffered: bool = false, 72 | in_stream: Stream, 73 | out_stream: Stream, 74 | 75 | // ------------------------------------------------------------------------ 76 | // Constructors 77 | // ------------------------------------------------------------------------ 78 | pub fn init(stream: Stream) IOStream { 79 | return IOStream{ 80 | .in_stream = stream, 81 | .out_stream = stream, 82 | .in_buffer = &[_]u8{}, 83 | .out_buffer = &[_]u8{}, 84 | }; 85 | } 86 | 87 | pub fn initCapacity(allocator: Allocator, stream: ?Stream, in_capacity: usize, out_capacity: usize) !IOStream { 88 | return IOStream{ 89 | .allocator = allocator, 90 | .in_stream = if (stream) |s| s else invalid_stream, 91 | .out_stream = if (stream) |s| s else invalid_stream, 92 | .in_buffer = try allocator.alloc(u8, in_capacity), 93 | .out_buffer = try allocator.alloc(u8, out_capacity), 94 | .owns_in_buffer = in_capacity == 0, 95 | ._in_start_index = in_capacity, 96 | ._in_end_index = in_capacity, 97 | }; 98 | } 99 | 100 | // Used to read only from a fixed buffer 101 | // the buffer must exist for the lifetime of the stream (or until swapped) 102 | pub fn fromBuffer(in_buffer: []u8) IOStream { 103 | return IOStream{ 104 | .in_stream = invalid_stream, 105 | .out_stream = invalid_stream, 106 | .in_buffer = in_buffer, 107 | .owns_in_buffer = false, 108 | ._in_start_index = 0, 109 | ._in_end_index = in_buffer.len, 110 | }; 111 | } 112 | 113 | // ------------------------------------------------------------------------ 114 | // Testing utilities 115 | // ------------------------------------------------------------------------ 116 | pub fn initTest(allocator: Allocator, in_buffer: []const u8) !IOStream { 117 | return IOStream{ 118 | .allocator = allocator, 119 | .in_stream = invalid_stream, 120 | .out_stream = invalid_stream, 121 | .in_buffer = try allocator.dupe(u8, in_buffer), 122 | .owns_in_buffer = in_buffer.len > 0, 123 | ._in_start_index = 0, 124 | ._in_end_index = in_buffer.len, 125 | }; 126 | } 127 | 128 | // Load into the in buffer for testing purposes 129 | pub fn load(self: *Self, allocator: Allocator, in_buffer: []const u8) !void { 130 | self.in_buffer = try allocator.dupe(u8, in_buffer); 131 | self._in_start_index = 0; 132 | self._in_end_index = in_buffer.len; 133 | } 134 | 135 | // ------------------------------------------------------------------------ 136 | // Custom Stream API 137 | // ------------------------------------------------------------------------ 138 | pub fn reset(self: *Self) void { 139 | self._in_start_index = 0; 140 | self._in_count = 0; 141 | self._out_count = 0; 142 | self.closed = false; 143 | self.unbuffered = false; 144 | } 145 | 146 | // Reset the the initial state without reallocating 147 | pub fn reinit(self: *Self, stream: Stream) void { 148 | self.close(); // Close old files 149 | self.in_stream = stream; 150 | self.out_stream = stream; 151 | self._in_start_index = self.in_buffer.len; 152 | self._in_end_index = self.in_buffer.len; 153 | self._in_count = 0; 154 | self._out_index = 0; 155 | self._out_count = 0; 156 | self.closed = false; 157 | self.unbuffered = false; 158 | } 159 | 160 | // Swap the current buffer with a new buffer copying any unread bytes 161 | // into the new buffer 162 | pub fn swapBuffer(self: *Self, buffer: []u8) void { 163 | //const left = self.amountBuffered(); 164 | // Reset counter 165 | self._in_count = 0; 166 | 167 | // No swap needed 168 | if (buffer.ptr == self.in_buffer.ptr) return; 169 | 170 | // So we know not to free the in buf at deinit 171 | self.owns_in_buffer = false; 172 | self.unbuffered = false; 173 | 174 | // Copy what is left 175 | const remaining = self.readBuffered(); 176 | if (remaining.len > 0) { 177 | std.mem.copy(u8, buffer, remaining); 178 | self.in_buffer = buffer; // Set it right away 179 | self._in_start_index = 0; 180 | self._in_end_index = remaining.len; 181 | } else { 182 | self.in_buffer = buffer; // Set it right away 183 | self._in_start_index = buffer.len; 184 | self._in_end_index = buffer.len; 185 | } 186 | } 187 | 188 | // Switch between buffered and unbuffered reads 189 | pub fn readUnbuffered(self: *Self, unbuffered: bool) void { 190 | self.unbuffered = unbuffered; 191 | } 192 | 193 | // TODO: Inline is broken 194 | pub fn shiftAndFillBuffer(self: *Self, start: usize) !usize { 195 | self.unbuffered = true; 196 | defer self.unbuffered = false; 197 | 198 | // Move buffer to beginning 199 | const end = self.readCount(); 200 | const remaining = self.in_buffer[start..end]; 201 | std.mem.copyBackwards(u8, self.in_buffer, remaining); 202 | 203 | // Try to read more 204 | if (remaining.len >= self.in_buffer.len) { 205 | return error.EndOfBuffer; 206 | } 207 | const n = try self.reader().read(self.in_buffer[remaining.len..]); 208 | self._in_start_index = 0; 209 | self._in_end_index = remaining.len + n; 210 | return n; 211 | } 212 | 213 | // ------------------------------------------------------------------------ 214 | // Reader 215 | // ------------------------------------------------------------------------ 216 | pub const Reader = std.io.Reader(*IOStream, Stream.ReadError, IOStream.readFn); 217 | 218 | pub fn reader(self: *Self) Reader { 219 | return Reader{ .context = self }; 220 | } 221 | 222 | // Return the amount of bytes waiting in the input buffer 223 | pub inline fn amountBuffered(self: *Self) usize { 224 | return self._in_end_index - self._in_start_index; 225 | } 226 | 227 | pub inline fn isEmpty(self: *Self) bool { 228 | return self._in_end_index == self._in_start_index; 229 | } 230 | 231 | pub inline fn readCount(self: *Self) usize { 232 | //return self._in_count + self._in_start_index; 233 | return self._in_start_index; 234 | } 235 | 236 | pub inline fn consumeBuffered(self: *Self, size: usize) usize { 237 | const n = math.min(size, self.amountBuffered()); 238 | self._in_start_index += n; 239 | return n; 240 | } 241 | 242 | pub inline fn skipBytes(self: *Self, n: usize) void { 243 | self._in_start_index += n; 244 | } 245 | 246 | pub inline fn readBuffered(self: *Self) []u8 { 247 | return self.in_buffer[self._in_start_index..self._in_end_index]; 248 | } 249 | 250 | // Read any generic type from a stream as long as it is 251 | // a multiple of 8 bytes. This does a an endianness conversion if needed 252 | pub fn readType(self: *Self, comptime T: type, comptime endian: std.builtin.Endian) !T { 253 | const n = @sizeOf(T); 254 | const I = switch (n) { 255 | 1 => u8, 256 | 2 => u16, 257 | 4 => u32, 258 | 8 => u64, 259 | 16 => u128, 260 | else => @compileError("Not implemented"), 261 | }; 262 | while (self.amountBuffered() < n) { 263 | try self.fillBuffer(); 264 | } 265 | const d = @bitCast(I, self.readBuffered()[0..n].*); 266 | const r = if (endian != native_endian) @byteSwap(I, d) else d; 267 | self.skipBytes(n); 268 | return @bitCast(T, r); 269 | } 270 | 271 | pub fn readFn(self: *Self, dest: []u8) !usize { 272 | //const self = @fieldParentPtr(BufferedReader, "stream", in_stream); 273 | if (self.unbuffered) return try self.in_stream.read(dest); 274 | 275 | // Hot path for one byte reads 276 | if (dest.len == 1 and self._in_end_index > self._in_start_index) { 277 | dest[0] = self.in_buffer[self._in_start_index]; 278 | self._in_start_index += 1; 279 | return 1; 280 | } 281 | 282 | var dest_index: usize = 0; 283 | while (true) { 284 | const dest_space = dest.len - dest_index; 285 | if (dest_space == 0) { 286 | return dest_index; 287 | } 288 | const amt_buffered = self.amountBuffered(); 289 | if (amt_buffered == 0) { 290 | assert(self._in_end_index <= self.in_buffer.len); 291 | // Make sure the last read actually gave us some data 292 | if (self._in_end_index == 0) { 293 | // reading from the unbuffered stream returned nothing 294 | // so we have nothing left to read. 295 | return dest_index; 296 | } 297 | // we can read more data from the unbuffered stream 298 | if (dest_space < self.in_buffer.len) { 299 | self._in_start_index = 0; 300 | self._in_end_index = try self.in_stream.read(self.in_buffer[0..]); 301 | //self._in_count += self._in_end_index; 302 | 303 | // Shortcut 304 | if (self._in_end_index >= dest_space) { 305 | mem.copy(u8, dest[dest_index..], self.in_buffer[0..dest_space]); 306 | self._in_start_index = dest_space; 307 | return dest.len; 308 | } 309 | } else { 310 | // asking for so much data that buffering is actually less efficient. 311 | // forward the request directly to the unbuffered stream 312 | const amt_read = try self.in_stream.read(dest[dest_index..]); 313 | //self._in_count += amt_read; 314 | return dest_index + amt_read; 315 | } 316 | } 317 | 318 | const copy_amount = math.min(dest_space, amt_buffered); 319 | const copy_end_index = self._in_start_index + copy_amount; 320 | mem.copy(u8, dest[dest_index..], self.in_buffer[self._in_start_index..copy_end_index]); 321 | self._in_start_index = copy_end_index; 322 | dest_index += copy_amount; 323 | } 324 | } 325 | 326 | // TODO: Inline is broken 327 | pub fn fillBuffer(self: *Self) !void { 328 | const n = try self.readFn(self.in_buffer); 329 | if (n == 0) return error.EndOfStream; 330 | self._in_start_index = 0; 331 | self._in_end_index = n; 332 | } 333 | 334 | /// Reads 1 byte from the stream or returns `error.EndOfStream`. 335 | pub fn readByte(self: *Self) !u8 { 336 | if (self._in_end_index == self._in_start_index) { 337 | // Do a direct read into the input buffer 338 | self._in_end_index = try self.readFn(self.in_buffer[0..self.in_buffer.len]); 339 | self._in_start_index = 0; 340 | if (self._in_end_index < 1) return error.EndOfStream; 341 | } 342 | const c = self.in_buffer[self._in_start_index]; 343 | self._in_start_index += 1; 344 | //self._in_count += 1; 345 | return c; 346 | } 347 | 348 | pub inline fn readByteSafe(self: *Self) !u8 { 349 | if (self._in_end_index == self._in_start_index) { 350 | return error.EndOfBuffer; 351 | } 352 | return self.readByteUnsafe(); 353 | } 354 | 355 | pub inline fn readByteUnsafe(self: *Self) u8 { 356 | const c = self.in_buffer[self._in_start_index]; 357 | self._in_start_index += 1; 358 | return c; 359 | } 360 | 361 | pub inline fn lastByte(self: *Self) u8 { 362 | return self.in_buffer[self._in_start_index]; 363 | } 364 | 365 | // Read up to limit bytes from the stream buffer until the expression 366 | // returns true or the limit is hit. The initial value is checked first. 367 | pub fn readUntilExpr(self: *Self, comptime expr: fn (ch: u8) bool, initial: u8, limit: usize) u8 { 368 | var found = false; 369 | var ch: u8 = initial; 370 | while (!found and self.readCount() + 8 < limit) { 371 | inline for ("01234567") |_| { 372 | if (expr(ch)) { 373 | found = true; 374 | break; 375 | } 376 | ch = self.readByteUnsafe(); 377 | } 378 | } 379 | if (!found) { 380 | while (self.readCount() < limit) { 381 | if (expr(ch)) { 382 | break; 383 | } 384 | ch = self.readByteUnsafe(); 385 | } 386 | } 387 | return ch; 388 | } 389 | 390 | // Read up to limit bytes from the stream buffer until the expression 391 | // returns true or the limit is hit. The initial value is checked first. 392 | // If the expression returns an error abort. 393 | pub fn readUntilExprValidate(self: *Self, comptime ErrorType: type, comptime expr: fn (ch: u8) ErrorType!bool, initial: u8, limit: usize) !u8 { 394 | var found = false; 395 | var ch: u8 = initial; 396 | while (!found and self.readCount() + 8 < limit) { 397 | inline for ("01234567") |_| { 398 | if (try expr(ch)) { 399 | found = true; 400 | break; 401 | } 402 | ch = self.readByteUnsafe(); 403 | } 404 | } 405 | if (!found) { 406 | while (self.readCount() < limit) { 407 | if (try expr(ch)) { 408 | break; 409 | } 410 | ch = self.readByteUnsafe(); 411 | } 412 | } 413 | return ch; 414 | } 415 | 416 | // ------------------------------------------------------------------------ 417 | // OutStream 418 | // ------------------------------------------------------------------------ 419 | pub const Writer = std.io.Writer(*IOStream, Stream.WriteError, IOStream.writeFn); 420 | 421 | pub fn writer(self: *Self) Writer { 422 | return Writer{ .context = self }; 423 | } 424 | 425 | fn writeFn(self: *Self, bytes: []const u8) !usize { 426 | if (bytes.len == 1) { 427 | self.out_buffer[self._out_index] = bytes[0]; 428 | self._out_index += 1; 429 | if (self._out_index == self.out_buffer.len) { 430 | try self.flush(); 431 | } 432 | return @as(usize, 1); 433 | } else if (bytes.len >= self.out_buffer.len) { 434 | try self.flush(); 435 | return self.out_stream.write(bytes); 436 | } 437 | var src_index: usize = 0; 438 | 439 | while (src_index < bytes.len) { 440 | const dest_space_left = self.out_buffer.len - self._out_index; 441 | const copy_amt = math.min(dest_space_left, bytes.len - src_index); 442 | mem.copy(u8, self.out_buffer[self._out_index..], bytes[src_index .. src_index + copy_amt]); 443 | self._out_index += copy_amt; 444 | assert(self._out_index <= self.out_buffer.len); 445 | if (self._out_index == self.out_buffer.len) { 446 | try self.flush(); 447 | } 448 | src_index += copy_amt; 449 | } 450 | return src_index; 451 | } 452 | 453 | pub fn flush(self: *Self) !void { 454 | try self.out_stream.writer().writeAll(self.out_buffer[0..self._out_index]); 455 | self._out_index = 0; 456 | } 457 | 458 | // Flush 'size' bytes from the start of the buffer out the stream 459 | pub fn flushBuffered(self: *Self, size: usize) !void { 460 | self._out_index = std.math.min(size, self.out_buffer.len); 461 | try self.flush(); 462 | } 463 | 464 | // Read directly into the output buffer then flush it out 465 | pub fn writeFromReader(self: *Self, in_stream: anytype) !usize { 466 | var total_wrote: usize = 0; 467 | if (self._out_index != 0) { 468 | total_wrote += self._out_index; 469 | try self.flush(); 470 | } 471 | 472 | while (true) { 473 | self._out_index = try in_stream.read(self.out_buffer); 474 | if (self._out_index == 0) break; 475 | 476 | total_wrote += self._out_index; 477 | try self.flush(); 478 | } 479 | return total_wrote; 480 | } 481 | 482 | // ------------------------------------------------------------------------ 483 | // Cleanup 484 | // ------------------------------------------------------------------------ 485 | pub fn close(self: *Self) void { 486 | if (self.closed) return; 487 | self.closed = true; 488 | // TODO: Doesn't need closed? 489 | // const in_stream = &self.in_stream; 490 | // const out_stream = &self.out_stream ; 491 | // if (in_stream.handle != 0) in_stream.close(); 492 | // std.log.warn("Close in={} out={}\n", .{in_stream, out_stream}); 493 | // if (in_stream.handle != out_stream.handle and out_stream.handle != 0) { 494 | // out_stream.close(); 495 | // } 496 | } 497 | 498 | pub fn deinit(self: *Self) void { 499 | if (!self.closed) self.close(); 500 | if (self.allocator) |allocator| { 501 | 502 | // If the buffer was swapped assume that it is no longer owned 503 | if (self.owns_in_buffer) { 504 | allocator.free(self.in_buffer); 505 | } 506 | allocator.free(self.out_buffer); 507 | } 508 | } 509 | }; 510 | 511 | const DummyHeldLock = struct { 512 | mutex: *std.Thread.Mutex, 513 | pub fn release(self: DummyHeldLock) void { 514 | self.mutex.unlock(); 515 | } 516 | }; 517 | 518 | // The event based lock doesn't work without evented io 519 | pub const Lock = if (std.io.is_async) std.event.Lock else std.Thread.Mutex; 520 | pub const HeldLock = if (std.io.is_async) std.event.Lock.Held else DummyHeldLock; 521 | 522 | pub fn ObjectPool(comptime T: type) type { 523 | return struct { 524 | const Self = @This(); 525 | pub const ObjectList = std.ArrayList(*T); 526 | 527 | allocator: Allocator, 528 | // Stores all created objects 529 | objects: ObjectList, 530 | 531 | // Stores objects that have been released 532 | free_objects: ObjectList, 533 | 534 | // Lock to use if using threads 535 | mutex: Lock = Lock{}, 536 | 537 | pub fn init(allocator: Allocator) Self { 538 | return Self{ 539 | .allocator = allocator, 540 | .objects = ObjectList.init(allocator), 541 | .free_objects = ObjectList.init(allocator), 542 | }; 543 | } 544 | 545 | // Get an object released back into the pool 546 | pub fn get(self: *Self) ?*T { 547 | if (self.free_objects.items.len == 0) return null; 548 | return self.free_objects.swapRemove(0); // Pull the oldest 549 | } 550 | 551 | // Create an object and allocate space for it in the pool 552 | pub fn create(self: *Self) !*T { 553 | const obj = try self.allocator.create(T); 554 | try self.objects.append(obj); 555 | try self.free_objects.ensureTotalCapacity(self.objects.items.len); 556 | return obj; 557 | } 558 | 559 | // Return a object back to the pool, this assumes it was created 560 | // using create (which ensures capacity to return this quickly). 561 | pub fn release(self: *Self, object: *T) void { 562 | return self.free_objects.appendAssumeCapacity(object); 563 | } 564 | 565 | pub fn deinit(self: *Self) void { 566 | while (self.objects.popOrNull()) |obj| { 567 | self.allocator.destroy(obj); 568 | } 569 | self.objects.deinit(); 570 | self.free_objects.deinit(); 571 | } 572 | 573 | pub fn acquire(self: *Self) HeldLock { 574 | if (std.io.is_async) { 575 | return self.mutex.acquire(); 576 | } else { 577 | self.mutex.lock(); 578 | return DummyHeldLock{ .mutex = &self.mutex }; 579 | } 580 | } 581 | }; 582 | } 583 | 584 | test "object-pool" { 585 | const Point = struct { 586 | x: u8, 587 | y: u8, 588 | }; 589 | var pool = ObjectPool(Point).init(std.testing.allocator); 590 | defer pool.deinit(); 591 | 592 | // Pool is empty 593 | try testing.expect(pool.get() == null); 594 | 595 | // Create 596 | var test_point = Point{ .x = 10, .y = 3 }; 597 | const pt = try pool.create(); 598 | pt.* = test_point; 599 | 600 | // Pool is still empty 601 | try testing.expect(pool.get() == null); 602 | 603 | // Relase 604 | pool.release(pt); 605 | 606 | // Should get the same thing back 607 | try testing.expectEqual(pool.get().?.*, test_point); 608 | } 609 | 610 | // An unmanaged map of arrays 611 | pub fn StringArrayMap(comptime T: type) type { 612 | return struct { 613 | const Self = @This(); 614 | pub const Array = std.ArrayList(T); 615 | pub const Map = std.StringHashMap(*Array); 616 | allocator: Allocator, 617 | storage: Map, 618 | 619 | pub fn init(allocator: Allocator) Self { 620 | return Self{ 621 | .allocator = allocator, 622 | .storage = Map.init(allocator), 623 | }; 624 | } 625 | 626 | pub fn deinit(self: *Self) void { 627 | // Deinit each array 628 | var it = self.storage.iterator(); 629 | while (it.next()) |entry| { 630 | const array = entry.value_ptr.*; 631 | array.deinit(); 632 | self.allocator.destroy(array); 633 | } 634 | self.storage.deinit(); 635 | } 636 | 637 | pub fn reset(self: *Self) void { 638 | // Deinit each array 639 | var it = self.storage.iterator(); 640 | while (it.pop()) |entry| { 641 | const array = entry.value_ptr.*; 642 | array.deinit(); 643 | self.allocator.destroy(array); 644 | } 645 | } 646 | 647 | pub fn append(self: *Self, name: []const u8, arg: T) !void { 648 | if (!self.storage.contains(name)) { 649 | const ptr = try self.allocator.create(Array); 650 | ptr.* = Array.init(self.allocator); 651 | _ = try self.storage.put(name, ptr); 652 | } 653 | var array = self.getArray(name).?; 654 | try array.append(arg); 655 | } 656 | 657 | // Return entire set 658 | pub fn getArray(self: *Self, name: []const u8) ?*Array { 659 | if (self.storage.getEntry(name)) |entry| { 660 | return entry.value_ptr.*; 661 | } 662 | return null; 663 | } 664 | 665 | // Return first field 666 | pub fn get(self: *Self, name: []const u8) ?T { 667 | if (self.getArray(name)) |array| { 668 | return if (array.items.len > 0) array.items[0] else null; 669 | } 670 | return null; 671 | } 672 | }; 673 | } 674 | 675 | test "string-array-map" { 676 | const Map = StringArrayMap([]const u8); 677 | var map = Map.init(std.testing.allocator); 678 | defer map.deinit(); 679 | try map.append("query", "a"); 680 | try map.append("query", "b"); 681 | try map.append("query", "c"); 682 | const query = map.getArray("query").?; 683 | try testing.expect(query.items.len == 3); 684 | try testing.expect(mem.eql(u8, query.items[0], "a")); 685 | } 686 | --------------------------------------------------------------------------------