From 14688ef87e54b5d01eb7ec4ca125aee9724488e3 Mon Sep 17 00:00:00 2001 From: 221V <221v92@gmail.com> Date: Fri, 5 Dec 2025 23:09:57 +0200 Subject: [PATCH 01/26] ws support --- .gitignore | 3 + build.zig | 55 +++++- build.zig.zon | 14 +- examples_ws/README.md | 39 ++++ examples_ws/cert/README.md | 114 +++++++++++ examples_ws/cert/fullchain.pem | 34 ++++ examples_ws/cert/private.crt | 34 ++++ examples_ws/cert/private.csr | 29 +++ examples_ws/cert/private.key | 52 +++++ examples_ws/cert/privkey.pem | 52 +++++ examples_ws/cert/ssl.conf | 35 ++++ examples_ws/example_ws_1.zig | 209 ++++++++++++++++++++ examples_ws/example_ws_2.zig | 233 ++++++++++++++++++++++ examples_ws/example_ws_3.zig | 179 +++++++++++++++++ src/http/context.zig | 3 + src/http/lib.zig | 6 + src/http/request.zig | 12 +- src/http/server.zig | 70 +++++-- src/http/websocket.zig | 198 +++++++++++++++++++ src/lib.zig | 19 ++ src/websocket/pubsub.zig | 298 +++++++++++++++++++++++++++++ src/websocket/websocket.zig | 340 +++++++++++++++++++++++++++++++++ 22 files changed, 2006 insertions(+), 22 deletions(-) create mode 100644 examples_ws/README.md create mode 100644 examples_ws/cert/README.md create mode 100644 examples_ws/cert/fullchain.pem create mode 100644 examples_ws/cert/private.crt create mode 100644 examples_ws/cert/private.csr create mode 100644 examples_ws/cert/private.key create mode 100644 examples_ws/cert/privkey.pem create mode 100644 examples_ws/cert/ssl.conf create mode 100644 examples_ws/example_ws_1.zig create mode 100644 examples_ws/example_ws_2.zig create mode 100644 examples_ws/example_ws_3.zig create mode 100644 src/http/websocket.zig create mode 100644 src/websocket/pubsub.zig create mode 100644 src/websocket/websocket.zig diff --git a/.gitignore b/.gitignore index 2c30843..87bbe59 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ zig-out/ .zig-cache/ perf*.data* heaptrack* +/ex_ws_1 +/ex_ws_2 +/ex_ws_3 diff --git a/build.zig b/build.zig index 037a9eb..b8e916c 100644 --- a/build.zig +++ b/build.zig @@ -2,8 +2,9 @@ const std = @import("std"); pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); - + //const optimize = b.standardOptimizeOption(.{}); // -O Debug + const optimize = std.builtin.OptimizeMode.ReleaseFast; // -O ReleaseFast + const zzz = b.addModule("zzz", .{ .root_source_file = b.path("src/lib.zig"), .target = target, @@ -35,7 +36,55 @@ pub fn build(b: *std.Build) void { if (target.result.os.tag != .windows) { add_example(b, "unix", false, target, optimize, zzz); } - + + + + const example1_ws_step = b.step("ex_ws_1", "Build ws example1"); + const example2_ws_step = b.step("ex_ws_2", "Build ws example2"); + const example3_ws_step = b.step("ex_ws_3", "Build ws example3"); + + const exe_ws1 = b.addExecutable(.{ // without C libs + .name = "ex_ws_1", // exe name + .root_source_file = b.path("examples_ws/example_ws_1.zig"), // b.path("src/test1.zig"), // main file + .target = target, + .optimize = optimize, + }); + const exe_ws2 = b.addExecutable(.{ // without C libs + .name = "ex_ws_2", // exe name + .root_source_file = b.path("examples_ws/example_ws_2.zig"), // main file + .target = target, + .optimize = optimize, + }); + const exe_ws3 = b.addExecutable(.{ // without C libs + .name = "ex_ws_3", // exe name + .root_source_file = b.path("examples_ws/example_ws_3.zig"), // main file + .target = target, + .optimize = optimize, + }); + + exe_ws1.root_module.addImport("zzz", zzz); + exe_ws2.root_module.addImport("zzz", zzz); + exe_ws3.root_module.addImport("zzz", zzz); + + const install_ws1 = b.addInstallBinFile(exe_ws1.getEmittedBin(), "../../ex_ws_1"); // b.addInstallBinFile(exe_ws1.getEmittedBin(), "ex_ws_1"); // -femit-bin=ex_ws_1 // to project root + b.getInstallStep().dependOn(&install_ws1.step); + example1_ws_step.dependOn(&install_ws1.step); + const install_ws2 = b.addInstallBinFile(exe_ws2.getEmittedBin(), "../../ex_ws_2"); // to project root + b.getInstallStep().dependOn(&install_ws2.step); + example2_ws_step.dependOn(&install_ws2.step); + const install_ws3 = b.addInstallBinFile(exe_ws3.getEmittedBin(), "../../ex_ws_3"); // to project root + b.getInstallStep().dependOn(&install_ws3.step); + example3_ws_step.dependOn(&install_ws3.step); + b.default_step = b.getInstallStep(); + //b.installArtifact(exe_test1); // saves to /zig-out/bin/test1 + + const all_ws_examples_step = b.step("examples_ws", "Build all WS examples"); + all_ws_examples_step.dependOn(&install_ws1.step); + all_ws_examples_step.dependOn(&install_ws2.step); + all_ws_examples_step.dependOn(&install_ws3.step); + + + const tests = b.addTest(.{ .name = "tests", .root_source_file = b.path("./src/tests.zig"), diff --git a/build.zig.zon b/build.zig.zon index 2200eb4..ae04fa1 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,15 +2,19 @@ .name = .zzz, .fingerprint = 0xc3273dca261a7ae0, .version = "0.3.0", - .minimum_zig_version = "0.14.0", + .minimum_zig_version = "0.14.1", .dependencies = .{ .tardy = .{ - .url = "git+https://github.com/tardy-org/tardy?ref=v0.3.0#cd454060f3b6006368d53c05ab96cd16c73c34de", - .hash = "tardy-0.3.0-69wrgi7PAwDFhO7m0aXae6N15s2b28VIOrnRrSHHake6", + //.url = "git+https://github.com/tardy-org/tardy?ref=v0.3.0#cd454060f3b6006368d53c05ab96cd16c73c34de", + //.hash = "tardy-0.3.0-69wrgi7PAwDFhO7m0aXae6N15s2b28VIOrnRrSHHake6", + .url = "git+https://github.com/221V/tardy?ref=zig-0.14#d0364f3142cb859474cd23ccc9da1c97d9dc1e05", + .hash = "tardy-0.3.0-zwolxXHNAwA6tH6Xr1qgUIAyHy6o4-hgOSVOlGBt6jjL", }, .secsock = .{ - .url = "git+https://github.com/tardy-org/secsock?ref=v0.1.0#263dcd630e32c7a5c7a0522a8d1fd04e39b75c24", - .hash = "secsock-0.0.0-p0qurf09AQD95s1NQF2MGpBqMmFz7cKZWibsgv_SQBAr", + //.url = "git+https://github.com/tardy-org/secsock?ref=v0.1.0#263dcd630e32c7a5c7a0522a8d1fd04e39b75c24", + //.hash = "secsock-0.0.0-p0qurf09AQD95s1NQF2MGpBqMmFz7cKZWibsgv_SQBAr", + .url = "git+https://github.com/221V/secsock?ref=zig-0.14#625147512dcacbfe5fa35cab2f94332ba76cd563", + .hash = "secsock-0.0.0-p0qurcM-AQCPUYli5tuTIPncDh3bKpHE-yBEkY5Zma1E", }, }, diff --git a/examples_ws/README.md b/examples_ws/README.md new file mode 100644 index 0000000..90f3b8f --- /dev/null +++ b/examples_ws/README.md @@ -0,0 +1,39 @@ +# zzz webserver ws examples + +``` +zig 0.14.1 + +zig build +zig build ex_ws_1 +zig build ex_ws_2 +zig build ex_ws_3 + +# ws example +./ex_ws_1 +// goto http://localhost:3010/ , open browser webmaster console +ws.send(42); +ws.send("hello world"); + +# wss example (with cert) +sudo ./ex_ws_2 +// goto https://test1.ls/ , open browser webmaster console +ws.send(42); +ws.send("hello world"); + + +# ws PubSub example +sudo ./ex_ws_3 +// goto http://localhost:3010/ in one browser(or common window - non-private), open browser webmaster console + +// also open the same http://localhost:3010/ in second browser(or private window), open browser webmaster console +ws.send("general:hello"); +ws.send("general:42"); +ws.send("gen:777"); + +// check received messages in second window, also send there +ws.send("general:hello 2"); +ws.send("general:43"); +ws.send("gen:hello"); +// and check received messages in first window +``` + diff --git a/examples_ws/cert/README.md b/examples_ws/cert/README.md new file mode 100644 index 0000000..f655f2d --- /dev/null +++ b/examples_ws/cert/README.md @@ -0,0 +1,114 @@ +# how to wildcard certs +how to wildcard certs for localhost ssl-http-wss tests -- +tested on (l)ubuntu 22.04 LTS but possible on any linux +``` +sudo vim /etc/hosts + +# add next lines +127.0.0.1 test1.ls +127.0.0.1 en.test1.ls +127.0.0.1 test2.ls +127.0.0.1 subdomain.test2.ls + +# apply changes without reboot (few commands - latest is newest) +sudo /etc/init.d/networking restart +# or +sudo service networking restart +# or +sudo /etc/init.d/network-manager restart +# or +sudo service network-manager restart +# or +sudo systemctl restart NetworkManager.service + +# next lets create certs +mkdir certs +cd certs +vim ssl.conf + +# add and save next lines +[ req ] +default_bits = 4096 +distinguished_name = req_distinguished_name +req_extensions = req_extensions_section + +[ req_distinguished_name ] +countryName = Country Name (2 letter code) +countryName_min = 2 +countryName_max = 2 +countryName_default = UA +stateOrProvinceName = State or Province Name (full name) +stateOrProvinceName_default = Lviv +localityName = Locality Name (eg, city) +localityName_default = Lviv +organizationName = Organization Name (eg, company) +organizationName_default = Test +organizationalUnitName = Organizational Unit Name (eg, section) +organizationalUnitName_default = IT +commonName = Common Name (e.g. server FQDN or YOUR name) +commonName_max = 64 +commonName_default = localhost +emailAddress = Email Address (eg, admin@example.com) +emailAddress_max = 64 +emailAddress_default = info@test.com + +[ req_extensions_section ] +subjectAltName = @subject_alternative_name_section + +[ subject_alternative_name_section ] +DNS.1 = test1.ls +DNS.2 = *.test1.ls +DNS.3 = test2.ls +DNS.4 = *.test2.ls + +# save file in vim and exit = CTRL + I for editing, next Esc, :w! for save and :q! for exit + +# next generate privkey +openssl genrsa -out private.key 4096 + +# generate CSR (Certificate Signing Request) - "Common name" is your project name +openssl req -new -sha256 -out private.csr -key private.key -config ssl.conf + +# check CSR +openssl req -text -noout -in private.csr + +# we will see something like next +... +X509v3 Subject Alternative Name: DNS:test1.ls +... +Signature Algorithm: sha256WithRSAEncryption +... + +# generate cert +openssl x509 -req -sha256 -days 3650 -in private.csr -signkey private.key -out private.crt -extensions req_extensions_section -extfile ssl.conf + +# install cert as trusted in system +sudo mkdir /usr/share/ca-certificates/extra +sudo cp private.crt /usr/share/ca-certificates/extra/private.crt + +sudo dpkg-reconfigure ca-certificates +# or +sudo update-ca-certificates + +# next we can use cert, for example, in nginx +server{ + listen 443 ssl http2; + ssl_certificate /home/user/certs/private.crt; + ssl_certificate_key /home/user/certs/private.key; + ssl_dhparam /home/user/certs/dhparams.pem; + + ... + server_name www.test1.ls; + return 301 https://test1.ls$request_uri; +} + +# or use without nginx +# we can see cert usage in 2nd ws example + +# if we have private.csr, private.crt and private.key +# but needs fullchain.pem and privkey.pem - just copy: + +cp private.key privkey.pem +cp private.crt fullchain.pem +``` + diff --git a/examples_ws/cert/fullchain.pem b/examples_ws/cert/fullchain.pem new file mode 100644 index 0000000..551b11e --- /dev/null +++ b/examples_ws/cert/fullchain.pem @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +ak3F2Cf6v7xZn39/ +-----END CERTIFICATE----- diff --git a/examples_ws/cert/private.crt b/examples_ws/cert/private.crt new file mode 100644 index 0000000..551b11e --- /dev/null +++ b/examples_ws/cert/private.crt @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +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 +ak3F2Cf6v7xZn39/ +-----END CERTIFICATE----- diff --git a/examples_ws/cert/private.csr b/examples_ws/cert/private.csr new file mode 100644 index 0000000..54cd830 --- /dev/null +++ b/examples_ws/cert/private.csr @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIFBjCCAu4CAQAweTELMAkGA1UEBhMCVUExDTALBgNVBAgMBEx2aXYxDTALBgNV +BAcMBEx2aXYxDTALBgNVBAoMBFRlc3QxCzAJBgNVBAsMAklUMRIwEAYDVQQDDAls +b2NhbGhvc3QxHDAaBgkqhkiG9w0BCQEWDWluZm9AdGVzdC5jb20wggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQDAH7CmRJD/q+uBFXUTIlvLUYQ0XkPdGekU +kAhqa1i9y15om4nYNk982fk+6gh0xDHqODD9GwWz2rHrmZPyoqE8NAxPti7tTnU1 +DV/d9X8G1pBiyWiiZ0VJ6tSASn7CfngBQpREfjgLtgcs3/rZ9mzcPBQu9blGz4xZ +690FBhJvS6iS2c8sxvX7H8V3s15+9ffszZxStNeX7UaNLCe96EQUNyt5usiGqR66 +qWshSheLKgLyRRhZ0fJdPIi1aaWSZJJX92PVFOwoKuump4ZNzkfyCxOdIcplcwrr +nyZsFe26DVOfJA93F9zkiyYXtkCtIsGbyA1peJlOSct0P92GuIz7GB5dFwLu6qHz +6lSNs+ph2BsQko87RIP3FUEr5VESXjrYOkgiMyiPYO230y4BuCj9gGgktqjn50GC +OLYBcFXlUCq3n4rnlSiWXm+lr8N5i9teKJWO9qaAo07GKGphO3z8oVhg5oL+WU68 +IT6SFt34RTQ6FnN5AO/SQY6kANgnzEDdppnAHJGJIRx55pCjbTGQ2J0+wlAAHFiR +tRJDnVkjXc6J6HaZMfD4eHCtnftkCwsDQdCAIgGrJxbBGcErXPaFwXR04YrZHmnm +8hATrdUgJxFhf0p0xuDyLyl/dOIO4wWBwf48xM8nI9NdUXLGaJrYbdUnj4hFLSW5 +qQ1aINrYZwIDAQABoEgwRgYJKoZIhvcNAQkOMTkwNzA1BgNVHREELjAsggh0ZXN0 +MS5sc4IKKi50ZXN0MS5sc4IIdGVzdDIubHOCCioudGVzdDIubHMwDQYJKoZIhvcN +AQELBQADggIBAJvJlcfahvKTyXNLd43MfJOkhR5iXmX+/kM6oQZrRrAuwhhUozY9 +BIn74vfSb7fupbuh1aurnW1ad8H9UgVuw2cIZnhyLJ+DAawOT92wp5r/20VY9ZyQ +sfWp3kJ/obst1k024esBw3jvoAiVXDW+9Ukmd9P/9lfZbLxfOWuhzqYc6IPsvtd/ +bQddQhaZ7kYJ7l6QardypcxpB4cRSajF8wZ6avvNruIThF7KZO1OLrPx369tNrQK +Sa3hV811qW4yuTTxlTQjwNNOGuB4I07R96e0Db86TeHixP4AFuoFNArL9J9SvfaL +tMXwE1Fcsgl8+7Uv1UKb5qk2orvrFayXf3Jxj5EG3TTOjwnd9guPVDEO0bXBV4zw +osGSvG0nNe1nePHXLUJm0GBcl4T4bMSIgPUNb+gFIQEkDNgF8m9UDNvss+9oeh6k +e4VtgC3OJt0O7Rd1d5PJkD0hVCaBX9pv/R5+oAQnLbbhusDnMy9EhjQrpzF6fQIg +WJhVU8D+aMz77XMU23U6neY8CQ9b4RaCFRdd6CxSja2S2R2mBD1Ut/lK0NGJ5aML +IEno//E12Z/RlL/YzjQrpWnSbo9SYcZ7xY7OXcoaF6/cBwD0bDkQJm3MwfH1WTJN +6sEq0ugqiLNrrqutFRXvgLnCfYzMAbkFaL81gEycd9RGu1J+vifWkK1M +-----END CERTIFICATE REQUEST----- diff --git a/examples_ws/cert/private.key b/examples_ws/cert/private.key new file mode 100644 index 0000000..38dd91a --- /dev/null +++ b/examples_ws/cert/private.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +cnk/jkfKFYNPl+wF0d25ufaZl96ENw== +-----END PRIVATE KEY----- diff --git a/examples_ws/cert/privkey.pem b/examples_ws/cert/privkey.pem new file mode 100644 index 0000000..38dd91a --- /dev/null +++ b/examples_ws/cert/privkey.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +cnk/jkfKFYNPl+wF0d25ufaZl96ENw== +-----END PRIVATE KEY----- diff --git a/examples_ws/cert/ssl.conf b/examples_ws/cert/ssl.conf new file mode 100644 index 0000000..bb17517 --- /dev/null +++ b/examples_ws/cert/ssl.conf @@ -0,0 +1,35 @@ +[ req ] +default_bits = 4096 +distinguished_name = req_distinguished_name +req_extensions = req_extensions_section + +[ req_distinguished_name ] +countryName = Country Name (2 letter code) +countryName_min = 2 +countryName_max = 2 +countryName_default = UA +stateOrProvinceName = State or Province Name (full name) +stateOrProvinceName_default = Lviv +localityName = Locality Name (eg, city) +localityName_default = Lviv +organizationName = Organization Name (eg, company) +organizationName_default = Test +organizationalUnitName = Organizational Unit Name (eg, section) +organizationalUnitName_default = IT +commonName = Common Name (e.g. server FQDN or YOUR name) +commonName_max = 64 +commonName_default = localhost +emailAddress = Email Address (eg, admin@example.com) +emailAddress_max = 64 +emailAddress_default = info@test.com + +[ req_extensions_section ] +subjectAltName = @subject_alternative_name_section + +[ subject_alternative_name_section ] +DNS.1 = test1.ls +DNS.2 = *.test1.ls +DNS.3 = test2.ls +DNS.4 = *.test2.ls + + diff --git a/examples_ws/example_ws_1.zig b/examples_ws/example_ws_1.zig new file mode 100644 index 0000000..d6c46dc --- /dev/null +++ b/examples_ws/example_ws_1.zig @@ -0,0 +1,209 @@ + +// ws example (without cert - without https-wss) + +const std = @import("std"); + +const zzz = @import("zzz"); + +//const websocket = @import("zzz/websocket/websocket.zig"); +const websocket = zzz.websocket; + +const Socket = zzz.tardy.Socket; + + +const PORT = 3010; +const HOST = "0.0.0.0"; + +//const STACK_SIZE = 10 * 1024 * 1024; // DEBUG +const STACK_SIZE = 64 * 1024; // RELEASE + + +// WebSocket handlers +fn on_ws_connect(conn: websocket.Conn) !void{ + try conn.send("Hello from zzz WebSocket!"); + std.log.info("WebSocket connected", .{}); +} + + +fn on_ws_close(conn: websocket.Conn, code: u16, reason: []const u8) !void{ + _ = conn; + std.log.info("WS closed: code={d}, reason={s}", .{ code, reason }); +} + + +fn on_ws_message(conn: websocket.Conn, data: []const u8) !void{ + std.log.info("WS Payload received: '{s}' (len: {d})", .{data, data.len}); + + //const msg = try std.fmt.allocPrint(conn.runtime.allocator, "Echo: {s}", .{data}); + //defer conn.runtime.allocator.free(msg); + //try conn.send(msg); + + try conn.send("Hello from Server!"); + std.log.info("WS <- {s}", .{data}); +} + + +fn on_ws_disconnect(conn: websocket.Conn) !void{ + _ = conn; + std.log.info("WebSocket disconnected", .{}); +} + + +// HTTP fallback +fn on_request(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond { + const res = ctx.response; + res.status = .OK; + res.mime = zzz.HTTP.Mime.HTML; + res.body = + \\ + \\zzz + WebSocket + \\ + \\

WebSocket Test

+ \\ + \\ + \\ + ; + return .standard; +} + + +// Upgrade handler +fn on_upgrade(req: *const zzz.Request, proto: []const u8) !bool { + if (!std.mem.eql(u8, proto, "websocket")) return false; + + const key = req.headers.get("Sec-WebSocket-Key") orelse return false; + const ext = req.headers.get("Sec-WebSocket-Extensions"); + + var header_buf = std.ArrayList(u8).init(std.heap.page_allocator); + defer header_buf.deinit(); + + //const res = try websocket.upgrade(req.socket, req.runtime, std.heap.page_allocator, key, ext, header_buf.writer() ); + const res = try websocket.upgrade(req.socket, req.runtime, req.runtime.allocator, key, ext, header_buf.writer() ); + + _ = try req.socket.send_all(req.runtime, header_buf.items); + + const ws_handler = websocket.Handler{ + .on_connect = on_ws_connect, + .on_message = on_ws_message, + .on_close = on_ws_close, + .on_disconnect = on_ws_disconnect, + }; + + if (ws_handler.on_connect) |f| try f(res.conn); + try req.runtime.spawn(.{ res.conn, ws_handler, std.heap.page_allocator }, websocket.runLoop, STACK_SIZE); + //try req.runtime.spawn(.{ res.conn, ws_handler, req.runtime.allocator }, websocket.runLoop, STACK_SIZE); + return true; +} + + +fn on_ws_endpoint(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond { + const req = ctx.request; + + const upgrade = req.headers.get("Upgrade"); + if (upgrade == null or !std.mem.eql(u8, upgrade.?, "websocket")) { + ctx.response.status = .@"Bad Request"; + ctx.response.body = "Expected WebSocket Upgrade"; + return .standard; + } + + const key = req.headers.get("Sec-WebSocket-Key") orelse { + ctx.response.status = .@"Bad Request"; + return .standard; + }; + const ext = req.headers.get("Sec-WebSocket-Extensions"); + + var header_buf = std.ArrayList(u8).init(ctx.allocator); + defer header_buf.deinit(); + + const res = try websocket.upgrade(&ctx.socket, ctx.runtime, ctx.allocator, key, ext, header_buf.writer()); + + _ = try ctx.socket.send_all(ctx.runtime, header_buf.items); + + const ws_handler = websocket.Handler{ + .on_connect = on_ws_connect, + .on_message = on_ws_message, + .on_close = on_ws_close, + .on_disconnect = on_ws_disconnect, + }; + + if (ws_handler.on_connect) |f| try f(res.conn); + + std.log.info("Starting WebSocket Loop...", .{}); + + //websocket.runLoop(res.conn, ws_handler, ctx.runtime.allocator) catch |err| { // sync loop + websocket.runLoop(res.conn, ws_handler, std.heap.page_allocator) catch |err| { // sync loop + std.log.err("WebSocket RunLoop Error: {s}", .{@errorName(err)}); + + if (err == error.Closed) { + std.log.info("Socket closed by browser", .{}); + } + + }; + + std.log.info("WebSocket Loop finished", .{}); + return .close; +} + + + +pub fn main() !void{ + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + defer _ = gpa.deinit(); + + const socket = try Socket.init(.{ .tcp = .{ .host = HOST, .port = PORT } }); + defer socket.close_blocking(); + try socket.bind(); + try socket.listen(1024); // max conn count that are waiting in accept queue + + const TardyType = zzz.tardy.Tardy(.auto); + var tardy = try TardyType.init(allocator, .{}); + //var tardy = try TardyType.init(allocator, .{ .threading = .single }); + defer tardy.deinit(); + + try tardy.entry(&socket, struct { + fn entry(rt: *zzz.tardy.Runtime, s: *const Socket) !void { + const config = zzz.ServerConfig{ + .stack_size = STACK_SIZE, + }; + + const home_route = zzz.HTTP.Route.init("/").get({}, on_request); + const ws_route = zzz.HTTP.Route.init("/ws").get({}, on_ws_endpoint); + const layers = &[_]zzz.HTTP.Layer{ + home_route.layer(), + ws_route.layer(), + }; + + const router = try rt.allocator.create(zzz.Router); + router.* = try zzz.Router.init(rt.allocator, layers, .{ + .not_found = on_request, + }); + // no defer router - lifetime per server work + + const provisions = try rt.allocator.create(zzz.tardy.Pool(zzz.Provision)); // use heap, not stack + provisions.* = try zzz.tardy.Pool(zzz.Provision).init(rt.allocator, 1024, .static); // 1024 = pool size + + const byte_count = provisions.items.len * @sizeOf(zzz.Provision); // set zeros - for initialized = false + @memset(@as([*]u8, @ptrCast(provisions.items.ptr))[0..byte_count], 0); + + const connection_count = try rt.allocator.create(usize); // use heap, not stack + connection_count.* = 0; + + const accept_queued = try rt.allocator.create(bool); + accept_queued.* = false; + + try rt.spawn( + .{ rt, config, router, zzz.secsock.SecureSocket.unsecured(s.*), provisions, connection_count, accept_queued }, + zzz.Server.main_frame, + config.stack_size + ); + + } // end fn entry + }.entry); +} + diff --git a/examples_ws/example_ws_2.zig b/examples_ws/example_ws_2.zig new file mode 100644 index 0000000..f082b05 --- /dev/null +++ b/examples_ws/example_ws_2.zig @@ -0,0 +1,233 @@ + +// ws example (wss - with cert, with https) + +const std = @import("std"); + +const zzz = @import("zzz"); + +//const websocket = @import("zzz/websocket/websocket.zig"); +const websocket = zzz.websocket; + +const Socket = zzz.tardy.Socket; + + +//const PORT = 3010; +const PORT = 443; +const HOST = "0.0.0.0"; + +//const STACK_SIZE = 10 * 1024 * 1024; // DEBUG +const STACK_SIZE = 64 * 1024; // RELEASE + +const FULLCHAIN_CERT = "examples_ws/cert/fullchain.pem"; +const PRIVKEY_CERT = "examples_ws/cert/privkey.pem"; + + +const ServerContext = struct { + socket: Socket, + cert_pem: []const u8, + key_pem: []const u8, +}; + + +// WebSocket handlers +fn on_ws_connect(conn: websocket.Conn) !void{ + try conn.send("Hello from zzz WebSocket!"); + std.log.info("WebSocket connected", .{}); +} + + +fn on_ws_close(conn: websocket.Conn, code: u16, reason: []const u8) !void{ + _ = conn; + std.log.info("WS closed: code={d}, reason={s}", .{ code, reason }); +} + + +fn on_ws_message(conn: websocket.Conn, data: []const u8) !void{ + std.log.info("WS Payload received: '{s}' (len: {d})", .{data, data.len}); + try conn.send("Hello from Server!"); + std.log.info("WS <- {s}", .{data}); +} + + +fn on_ws_disconnect(conn: websocket.Conn) !void{ + _ = conn; + std.log.info("WebSocket disconnected", .{}); +} + + +// HTTP fallback +fn on_request(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond { + const res = ctx.response; + res.status = .OK; + res.mime = zzz.HTTP.Mime.HTML; + res.body = + \\ + \\zzz + WebSocket + \\ + \\

WebSocket Test

+ \\ + \\ + \\ + ; + return .standard; +} + + +// Upgrade handler +fn on_upgrade(req: *const zzz.Request, proto: []const u8) !bool { + if (!std.mem.eql(u8, proto, "websocket")) return false; + + const key = req.headers.get("Sec-WebSocket-Key") orelse return false; + const ext = req.headers.get("Sec-WebSocket-Extensions"); + + var header_buf = std.ArrayList(u8).init(std.heap.page_allocator); + defer header_buf.deinit(); + + //const res = try websocket.upgrade(req.socket, req.runtime, std.heap.page_allocator, key, ext, header_buf.writer() ); + const res = try websocket.upgrade(req.socket, req.runtime, req.runtime.allocator, key, ext, header_buf.writer() ); + + _ = try req.socket.send_all(req.runtime, header_buf.items); + + const ws_handler = websocket.Handler{ + .on_connect = on_ws_connect, + .on_message = on_ws_message, + .on_close = on_ws_close, + .on_disconnect = on_ws_disconnect, + }; + + if (ws_handler.on_connect) |f| try f(res.conn); + try req.runtime.spawn(.{ res.conn, ws_handler, std.heap.page_allocator }, websocket.runLoop, STACK_SIZE); + //try req.runtime.spawn(.{ res.conn, ws_handler, req.runtime.allocator }, websocket.runLoop, STACK_SIZE); + return true; +} + + +fn on_ws_endpoint(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond { + const req = ctx.request; + + const upgrade = req.headers.get("Upgrade"); + if (upgrade == null or !std.mem.eql(u8, upgrade.?, "websocket")) { + ctx.response.status = .@"Bad Request"; + ctx.response.body = "Expected WebSocket Upgrade"; + return .standard; + } + + const key = req.headers.get("Sec-WebSocket-Key") orelse { + ctx.response.status = .@"Bad Request"; + return .standard; + }; + const ext = req.headers.get("Sec-WebSocket-Extensions"); + + var header_buf = std.ArrayList(u8).init(ctx.allocator); + defer header_buf.deinit(); + + const res = try websocket.upgrade(&ctx.socket, ctx.runtime, ctx.allocator, key, ext, header_buf.writer()); + + _ = try ctx.socket.send_all(ctx.runtime, header_buf.items); + + const ws_handler = websocket.Handler{ + .on_connect = on_ws_connect, + .on_message = on_ws_message, + .on_close = on_ws_close, + .on_disconnect = on_ws_disconnect, + }; + + if (ws_handler.on_connect) |f| try f(res.conn); + + std.log.info("Starting WebSocket Loop...", .{}); + + //websocket.runLoop(res.conn, ws_handler, ctx.runtime.allocator) catch |err| { // sync loop + websocket.runLoop(res.conn, ws_handler, std.heap.page_allocator) catch |err| { // sync loop + std.log.err("WebSocket RunLoop Error: {s}", .{@errorName(err)}); + + if (err == error.Closed) { + std.log.info("Socket closed by browser", .{}); + } + + }; + + std.log.info("WebSocket Loop finished", .{}); + return .close; +} + + +pub fn main() !void{ + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + defer _ = gpa.deinit(); + + const socket = try Socket.init(.{ .tcp = .{ .host = HOST, .port = PORT } }); + defer socket.close_blocking(); + try socket.bind(); + try socket.listen(1024); // max conn count that are waiting in accept queue + + const cert = try std.fs.cwd().readFileAlloc(allocator, FULLCHAIN_CERT, 1024 * 10); + defer allocator.free(cert); + const key = try std.fs.cwd().readFileAlloc(allocator, PRIVKEY_CERT, 1024 * 10); + defer allocator.free(key); + + const ctx = ServerContext{ + .socket = socket, + .cert_pem = cert, + .key_pem = key, + }; + + const TardyType = zzz.tardy.Tardy(.auto); + var tardy = try TardyType.init(allocator, .{}); + //var tardy = try TardyType.init(allocator, .{ .threading = .single }); + defer tardy.deinit(); + + try tardy.entry(&ctx, struct { + fn entry(rt: *zzz.tardy.Runtime, s_ctx: *const ServerContext) !void { + const bearssl = try rt.allocator.create(zzz.secsock.BearSSL); + bearssl.* = zzz.secsock.BearSSL.init(rt.allocator); + //defer bearssl.deinit(); + + try bearssl.add_cert_chain("CERTIFICATE", s_ctx.cert_pem, "PRIVATE KEY", s_ctx.key_pem); + const secure_socket = try bearssl.to_secure_socket(s_ctx.socket, .server); + //defer secure_socket.deinit(); + + const config = zzz.ServerConfig{ + .stack_size = STACK_SIZE, + }; + + const home_route = zzz.HTTP.Route.init("/").get({}, on_request); + const ws_route = zzz.HTTP.Route.init("/ws").get({}, on_ws_endpoint); + const layers = &[_]zzz.HTTP.Layer{ + home_route.layer(), + ws_route.layer(), + }; + + const router = try rt.allocator.create(zzz.Router); + router.* = try zzz.Router.init(rt.allocator, layers, .{ + .not_found = on_request, + }); + // no defer router - lifetime per server work + + const provisions = try rt.allocator.create(zzz.tardy.Pool(zzz.Provision)); // use heap instead of stack + provisions.* = try zzz.tardy.Pool(zzz.Provision).init(rt.allocator, 1024, .static); // 1024 = pool size + + const byte_count = provisions.items.len * @sizeOf(zzz.Provision); // set zeros -- initialized = false + @memset(@as([*]u8, @ptrCast(provisions.items.ptr))[0..byte_count], 0); + + const connection_count = try rt.allocator.create(usize); // use heap instead stack + connection_count.* = 0; + + const accept_queued = try rt.allocator.create(bool); + accept_queued.* = false; + + try rt.spawn( + .{ rt, config, router, secure_socket, provisions, connection_count, accept_queued }, + zzz.Server.main_frame, + config.stack_size + ); + + } // end fn entry + }.entry); +} + diff --git a/examples_ws/example_ws_3.zig b/examples_ws/example_ws_3.zig new file mode 100644 index 0000000..5437843 --- /dev/null +++ b/examples_ws/example_ws_3.zig @@ -0,0 +1,179 @@ + +// ws with Pub/Sub example + +const std = @import("std"); + +const zzz = @import("zzz"); + +//const websocket = @import("zzz/websocket/websocket.zig"); +const websocket = zzz.websocket; +const PubSub = zzz.PubSub; +const handle_upgrade = zzz.handle_upgrade; +const WsSession = zzz.WsSession; + +const Socket = zzz.tardy.Socket; +const Timer = zzz.tardy.Timer; + + +const PORT = 3010; +//const PORT = 443; +const HOST = "0.0.0.0"; + +//const STACK_SIZE = 10 * 1024 * 1024; // DEBUG +const STACK_SIZE = 16 * 1024; // RELEASE // reader stack // 8-16 kb without ssl usage, 32 kb when use ssl +const WS_WRITER_STACK = 8 * 1024; // stack size for writer task // 8 kb without ssl usage, 32 kb when use ssl + +var global_pubsub: PubSub = undefined; + + +// WebSocket handlers +fn on_ws_connect(session: *WsSession) !void{ + try global_pubsub.subscribe("general", session); + try session.scheduleSend("Welcome! You are joined to 'general'. Send 'room:message' to chat."); + + std.log.info("WebSocket connected and joined 'general' chat", .{}); +} + + +fn on_ws_close(session: *WsSession) void{ + global_pubsub.removeConn(session); + //std.log.info("WS closed: code={d}, reason={s}", .{ code, reason }); + std.log.info("WS closed, client removed from all chats", .{ }); +} + + +fn on_ws_message(session: *WsSession, data: []const u8) !void{ + std.log.info("WS server received message: '{s}' (len: {d})", .{data, data.len}); + + if (std.mem.indexOfScalar(u8, data, ':')) |idx| { + const command_or_chat = data[0..idx]; + const msg_content = data[idx+1..]; + + if (std.mem.eql(u8, command_or_chat, "join")) { // "join:chatname" + try global_pubsub.subscribe(msg_content, session); + try session.scheduleSend("Joined chat"); + return; + } + + if (std.mem.eql(u8, command_or_chat, "leave")) { // "leave:chatname" + global_pubsub.unsubscribe(msg_content, session); + try session.scheduleSend("Left chat"); + return; + } + + //const formatted_msg = try std.fmt.allocPrint(conn.runtime.allocator, "Msg in {s}: {s}", .{command_or_chat, msg_content}); // pub to chatname + //defer conn.runtime.allocator.free(formatted_msg); + const formatted_msg = try std.fmt.allocPrint(session.allocator, "Msg in {s}: {s}", .{command_or_chat, msg_content}); // pub to chatname + defer session.allocator.free(formatted_msg); + + global_pubsub.publish(command_or_chat, formatted_msg, session); // send message everybody (but not to sender) + //global_pubsub.publish("general", "System: Server is shutting down", null); // "system" message without sender conn + + } else { + try session.scheduleSend("Format: 'chatname:message' or 'join:chatname'"); + } + std.log.info("WS <- {s}", .{data}); +} + + +// optional -- not required +fn on_ws_disconnect(session: *WsSession) void{ + global_pubsub.removeConn(session); + std.log.info("WebSocket disconnected, client removed from all chats", .{}); +} + + +// HTTP fallback +fn on_request(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond { + const res = ctx.response; + res.status = .OK; + res.mime = zzz.HTTP.Mime.HTML; + res.body = + \\ + \\zzz + WebSocket + PubSub + \\ + \\

WebSocket PubSub Test

+ \\ + \\ + \\ + ; + return .standard; +} + + +fn on_ws_endpoint(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond { + return handle_upgrade(ctx, .{ + .on_connect = on_ws_connect, + .on_message = on_ws_message, + .on_close = on_ws_close, + //.on_disconnect = on_ws_disconnect, // optional + }, + WS_WRITER_STACK // stack size for writer task + ); +} + + +pub fn main() !void{ + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + defer _ = gpa.deinit(); + + global_pubsub = PubSub.init(allocator); + defer global_pubsub.deinit(); + + const socket = try Socket.init(.{ .tcp = .{ .host = HOST, .port = PORT } }); + defer socket.close_blocking(); + try socket.bind(); + try socket.listen(1024); // max conn count that are waiting in accept queue + + const TardyType = zzz.tardy.Tardy(.auto); + var tardy = try TardyType.init(allocator, .{}); + //var tardy = try TardyType.init(allocator, .{ .threading = .single }); + defer tardy.deinit(); + + try tardy.entry(&socket, struct { + fn entry(rt: *zzz.tardy.Runtime, s: *const Socket) !void { + const config = zzz.ServerConfig{ + .stack_size = STACK_SIZE, // stack for http requests + }; + + const home_route = zzz.HTTP.Route.init("/").get({}, on_request); + const ws_route = zzz.HTTP.Route.init("/ws").get({}, on_ws_endpoint); + const layers = &[_]zzz.HTTP.Layer{ + home_route.layer(), + ws_route.layer(), + }; + + const router = try rt.allocator.create(zzz.Router); + router.* = try zzz.Router.init(rt.allocator, layers, .{ + .not_found = on_request, + }); + // no defer router - lifetime per server work + + const provisions = try rt.allocator.create(zzz.tardy.Pool(zzz.Provision)); // use heap instead of stack + provisions.* = try zzz.tardy.Pool(zzz.Provision).init(rt.allocator, 1024, .static); + + const byte_count = provisions.items.len * @sizeOf(zzz.Provision); // set zeros -- initialized = false + @memset(@as([*]u8, @ptrCast(provisions.items.ptr))[0..byte_count], 0); + + const connection_count = try rt.allocator.create(usize); // use heap instead stack + connection_count.* = 0; + + const accept_queued = try rt.allocator.create(bool); + accept_queued.* = false; + + try rt.spawn( + .{ rt, config, router, zzz.secsock.SecureSocket.unsecured(s.*), provisions, connection_count, accept_queued }, + zzz.Server.main_frame, + config.stack_size + ); + + } // end fn entry + }.entry); +} + diff --git a/src/http/context.zig b/src/http/context.zig index 60dee37..6074eb1 100644 --- a/src/http/context.zig +++ b/src/http/context.zig @@ -30,4 +30,7 @@ pub const Context = struct { captures: []const Capture, /// Map of the KV Query pairs in the URL queries: *const AnyCaseStringMap, + /// bind WebSocket to Context (for use on_connect etc) + ws_user_data: ?*anyopaque = null, }; + diff --git a/src/http/lib.zig b/src/http/lib.zig index 5b2b3f3..06b0b06 100644 --- a/src/http/lib.zig +++ b/src/http/lib.zig @@ -23,8 +23,13 @@ pub const MiddlewareFn = @import("router/middleware.zig").MiddlewareFn; pub const Next = @import("router/middleware.zig").Next; pub const Middlewares = @import("middlewares/lib.zig"); +pub const Provision = @import("server.zig").Provision; + +pub const Capture = @import("router/routing_trie.zig").Capture; pub const FsDir = @import("router/fs_dir.zig").FsDir; +pub const AnyCaseStringMap = @import("../core/any_case_string_map.zig").AnyCaseStringMap; + pub const Server = @import("server.zig").Server; pub const ServerConfig = @import("server.zig").ServerConfig; @@ -36,3 +41,4 @@ pub const HTTPError = error{ URITooLong, HTTPVersionNotSupported, }; + diff --git a/src/http/request.zig b/src/http/request.zig index c74e1f0..fc2ec30 100644 --- a/src/http/request.zig +++ b/src/http/request.zig @@ -2,6 +2,11 @@ const std = @import("std"); const log = std.log.scoped(.@"zzz/http/request"); const assert = std.debug.assert; +//const SecureSocket = @import("secsock").SecureSocket; +//const Runtime = @import("tardy").Runtime; +const secsock = @import("secsock"); +const tardy = @import("tardy"); + const AnyCaseStringMap = @import("../core/any_case_string_map.zig").AnyCaseStringMap; const CookieMap = @import("cookie.zig").CookieMap; const HTTPError = @import("lib.zig").HTTPError; @@ -15,7 +20,11 @@ pub const Request = struct { headers: AnyCaseStringMap, cookies: CookieMap, body: ?[]const u8 = null, - + //socket: *const SecureSocket = undefined, // for WebSocket on_upgrade + //runtime: *Runtime = undefined, + socket: *const secsock.SecureSocket = undefined, // for WebSocket on_upgrade + runtime: *tardy.Runtime = undefined, + /// This is for constructing a Request. pub fn init(allocator: std.mem.Allocator) Request { const headers = AnyCaseStringMap.init(allocator); @@ -243,3 +252,4 @@ test "Malformed AnyCaseStringMap" { }); try testing.expectError(HTTPError.MalformedRequest, err); } + diff --git a/src/http/server.zig b/src/http/server.zig index d3432db..9f56e54 100644 --- a/src/http/server.zig +++ b/src/http/server.zig @@ -51,6 +51,11 @@ pub const TLSFileOptions = union(enum) { }, }; +pub const TLSConfig = struct { + cert: TLSFileOptions, + key: TLSFileOptions, +}; + /// These are various general configuration /// options that are important for the actual framework. /// @@ -122,6 +127,11 @@ pub const ServerConfig = struct { /// /// Default: 2KB request_uri_bytes_max: u32 = 1024 * 2, + /// Callback for upgrade request (Websocket) + /// return true if upgrade processed + on_upgrade: ?*const fn (*const Request, []const u8) anyerror!bool = null, + /// we need two tls files - cert + key + tls: ?TLSConfig = null, }; pub const Provision = struct { @@ -336,18 +346,50 @@ pub const Server = struct { assert(info.current_length <= info.content_length); }, }, + .handler => { - const found = try router.get_bundle_from_host( + + const request_text = provision.zc_recv_buffer.as_slice(); + var request = Request.init(rt.allocator); + request.parse_headers(request_text, .{ + .request_bytes_max = config.request_bytes_max, + .request_uri_bytes_max = config.request_uri_bytes_max, + }) catch |e| { + log.debug("malformed request| {}", .{e}); + break :http_loop; + }; + + + if (config.on_upgrade) |on_upgrade| { // check is this WebSocket upgrade + request.socket = &secure; + request.runtime = rt; + + const upgrade_header = request.headers.get("Upgrade"); + if (upgrade_header) |upgrade| { + + if (std.mem.eql(u8, upgrade, "websocket")) { + if (try on_upgrade(&request, upgrade)) { + continue :http_loop; + } + } + + } + } + + const found = try router.get_bundle_from_host( // continue as common HTTP if not WebSocket upgrade rt.allocator, - provision.request.uri.?, + //provision.request.uri.?, + request.uri.?, provision.captures, &provision.queries, ); defer rt.allocator.free(found.duped); defer for (found.duped) |dupe| rt.allocator.free(dupe); - + + const h_with_data: HandlerWithData = found.route.get_handler( - provision.request.method.?, + //provision.request.method.?, + request.method.?, ) orelse { provision.response.headers.clearRetainingCapacity(); provision.response.status = .@"Method Not Allowed"; @@ -357,7 +399,8 @@ pub const Server = struct { state = .respond; continue; }; - + + const context: Context = .{ .runtime = rt, .allocator = provision.arena.allocator(), @@ -369,13 +412,14 @@ pub const Server = struct { .captures = found.captures, .queries = found.queries, }; - + var next: Next = .{ .context = &context, .middlewares = h_with_data.middlewares, .handler = h_with_data, }; - + + const next_respond: Respond = next.run() catch |e| blk: { log.warn("rt{d} - \"{s} {s}\" {} ({})", .{ rt.id, @@ -385,8 +429,7 @@ pub const Server = struct { secure.socket.addr, }); - // If in Debug Mode, we will return the error name. In other modes, - // we won't to avoid leaking implemenation details. + // If in Debug Mode, we will return the error name const body = if (comptime builtin.mode == .Debug) @errorName(e) else ""; break :blk try provision.response.apply(.{ @@ -395,10 +438,10 @@ pub const Server = struct { .body = body, }); }; - + + switch (next_respond) { .standard => { - // applies the respond onto the response //try provision.response.apply(respond); state = .respond; }, @@ -410,15 +453,15 @@ pub const Server = struct { log.debug("closing connection, exceeded keepalive max", .{}); break :http_loop; } - keepalive_count += 1; } - + try prepare_new_request(&state, provision, config); }, .close => break :http_loop, } }, + .respond => { const body = provision.response.body orelse ""; const content_length = body.len; @@ -531,3 +574,4 @@ pub const Server = struct { ); } }; + diff --git a/src/http/websocket.zig b/src/http/websocket.zig new file mode 100644 index 0000000..40210ae --- /dev/null +++ b/src/http/websocket.zig @@ -0,0 +1,198 @@ + +const std = @import("std"); + +const zzz = @import("../lib.zig"); + +const websocket = zzz.websocket; +const SecureSocket = zzz.secsock.SecureSocket; +const Runtime = zzz.tardy.Runtime; + +const Context = @import("context.zig").Context; + + +pub const WebSocketHandler = struct { + on_connect: ?*const fn (Conn) anyerror!void = null, + on_message: ?*const fn (Conn, []const u8) anyerror!void = null, + on_disconnect: ?*const fn (Conn) anyerror!void = null, + user_data: ?*anyopaque = null, +}; + +pub const Conn = struct { + socket: *const SecureSocket, + runtime: *Runtime, + user_data: ?*anyopaque, + + pub fn send(self: Conn, data: []const u8) !void { + const frame = websocket.frameText(data); + _ = try self.socket.send_all(self.runtime, frame); + } + + pub fn close(self: Conn) void { + self.socket.close_blocking(); + } +}; + +pub fn upgrade_to_websocket( + ctx: *const Context, + handler: WebSocketHandler, +) !bool { + const req = ctx.request; + const res = ctx.response; + + // handshake + if (!std.mem.eql(u8, req.headers.get("Connection") orelse "", "Upgrade")) + return false; + if (!std.mem.eql(u8, req.headers.get("Upgrade") orelse "", "websocket")) + return false; + if (!std.mem.eql(u8, req.headers.get("Sec-WebSocket-Version") orelse "", "13")) + return false; + + const key = req.headers.get("Sec-WebSocket-Key") orelse return false; + + // Sec-WebSocket-Accept + const accept = try compute_accept(ctx.allocator, key); + + // 101 Switching Protocols + res.clear(); + try res.headers.put("Upgrade", "websocket"); + try res.headers.put("Connection", "Upgrade"); + try res.headers.put("Sec-WebSocket-Accept", accept); + res.status = .@"Switching Protocols"; + + try res.headers_into_writer(ctx.header_buffer.writer(), 0); + _ = try ctx.socket.send_all(ctx.runtime, ctx.header_buffer.items); + + // WebSocket Conn + const conn = Conn{ + .socket = &ctx.socket, + .runtime = ctx.runtime, + .user_data = handler.user_data, + }; + + // on_connect + if (handler.on_connect) |on_connect| { + try on_connect(conn); + } + + // process + try ctx.runtime.spawn(.{ conn, handler }, message_loop, 64 * 1024); + return true; +} + +// inner loop +fn message_loop(conn: Conn, handler: WebSocketHandler) !void { + var buffer: [65536]u8 = undefined; + while (true) { + const n = conn.socket.recv(conn.runtime, &buffer) catch |err| { + if (err == error.Closed) { + if (handler.on_disconnect) |on_disconnect| { + _ = on_disconnect(conn); + } + return; + } + return err; + }; + + // RFC 6455 + var i: usize = 0; + while (i < n) { + const op = buffer[i]; + const fin = (op & 0x80) != 0; + const opcode = op & 0x0F; + i += 1; + + if (i >= n) return error.InvalidWebSocketFrame; + + const mask_flag = (buffer[i] & 0x80) != 0; + const payload_len_raw = buffer[i] & 0x7F; + i += 1; + + if (i >= n) return error.InvalidWebSocketFrame; + + var payload_len: usize = payload_len_raw; + var extra: usize = 0; + if (payload_len_raw == 126) { + if (i + 2 > n) return error.InvalidWebSocketFrame; + payload_len = @as(usize, @bitCast(std.mem.readIntBig(u16, buffer[i..]))); + extra = 2; + } else if (payload_len_raw == 127) { + if (i + 8 > n) return error.InvalidWebSocketFrame; + payload_len = @as(usize, @bitCast(std.mem.readIntBig(u64, buffer[i..]))); + extra = 8; + } + + i += extra; + if (mask_flag) { + if (i + 4 > n) return error.InvalidWebSocketFrame; + const mask = buffer[i..][0..4].*; + i += 4; + if (i + payload_len > n) return error.InvalidWebSocketFrame; + + //for (0..payload_len) |j| { + // buffer[i + j] ^= mask[j % 4]; + //} // use next vector optimising instead this + + var j: usize = 0; + const vec_len = std.simd.suggestVectorLength(u8) orelse 16; + const Vector = @Vector(vec_len, u8); + + var mask_arr: [vec_len]u8 = undefined; + for (0..vec_len) |k| mask_arr[k] = mask[k % 4]; + const mask_vec: Vector = mask_arr; + + while (j + vec_len <= payload_len) { + const chunk: Vector = buffer[i+j..][0..vec_len].*; + const res = chunk ^ mask_vec; + buffer[i+j..][0..vec_len].* = res; + j += vec_len; + } + + while (j < payload_len) : (j += 1) { + buffer[i + j] ^= mask[j % 4]; + } + + + } + + if (i + payload_len > n) return error.InvalidWebSocketFrame; + + switch (opcode) { + 0x1 => { // Text + if (fin and handler.on_message) |on_msg| { + try on_msg(conn, buffer[i .. i + payload_len]); + } + }, + 0x8 => { // Close + conn.close(); + if (handler.on_disconnect) |on_disconnect| { + _ = on_disconnect(conn); + } + return; + }, + 0x9 => { // Ping - reply Pong + const pong = ([2]u8{ 0x8A, @intCast(@min(payload_len, 125)) })[0..]; + _ = try conn.socket.send_all(conn.runtime, pong); + if (payload_len <= 125) { + _ = try conn.socket.send_all(conn.runtime, buffer[i .. i + payload_len]); + } + }, + else => { + // ignore other opcode (binary, pong etc) + }, + } + + i += payload_len; + } + } +} + +// Sec-WebSocket-Accept +fn compute_accept(allocator: std.mem.Allocator, key: []const u8) ![]const u8 { + const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + var hasher = std.crypto.hash.sha1.Sha1.init(); + hasher.update(key); + hasher.update(magic); + const hash = hasher.final(); + return try std.base64.encode(allocator, &hash); +} + diff --git a/src/lib.zig b/src/lib.zig index 96d9dd7..703faf1 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -2,10 +2,29 @@ const std = @import("std"); /// Internally exposed Tardy. pub const tardy = @import("tardy"); +//pub const Pool = tardy.Pool; /// Internally exposed secsock. pub const secsock = @import("secsock"); +//pub const SecureSocket = secsock.SecureSocket; /// HyperText Transfer Protocol. /// Supports: HTTP/1.1 pub const HTTP = @import("http/lib.zig"); +pub const ServerConfig = HTTP.ServerConfig; +pub const Request = HTTP.Request; +//pub const Respond = HTTP.Respond; +pub const Router = HTTP.Router; +pub const Context = HTTP.Context; +pub const Provision = HTTP.Provision; + +pub const TypedStorage = @import("core/typed_storage.zig").TypedStorage; + +pub const Server = @import("http/server.zig").Server; + +/// websocket + PubSub +pub const websocket = @import("websocket/websocket.zig"); +pub const PubSub = @import("websocket/pubsub.zig").PubSub; +pub const WsSession = @import("websocket/pubsub.zig").WsSession; +pub const handle_upgrade = @import("websocket/pubsub.zig").handle_upgrade; + diff --git a/src/websocket/pubsub.zig b/src/websocket/pubsub.zig new file mode 100644 index 0000000..ad4ca85 --- /dev/null +++ b/src/websocket/pubsub.zig @@ -0,0 +1,298 @@ + +// Pub/Sub for WS + +const std = @import("std"); +const zzz = @import("../lib.zig"); // "zzz" // for zzz.Context +const Conn = @import("websocket.zig").Conn; +const SecureSocket = zzz.secsock.SecureSocket; + + +pub const UserWsHandler = struct { + on_connect: ?*const fn (session: *WsSession) anyerror!void = null, + on_message: ?*const fn (session: *WsSession, data: []const u8) anyerror!void = null, + on_close: ?*const fn (session: *WsSession) void = null, + on_disconnect: ?*const fn (session: *WsSession) void = null, +}; + + +pub const WsSession = struct { // for safe multithread use Pub/Sub + conn: Conn, + socket_owned: SecureSocket, // owned copy of socket structure (avoid use-after-free error) + outbox: std.ArrayList([]const u8), // messages queue that need to send to client with conn + mutex: std.Thread.Mutex, + writer_task_id: usize = 0, // task id for send in native thread + active: bool = true, // flag for to stop writer_task on disconnect + allocator: std.mem.Allocator, + writer_done: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), // for avoid race condition + handler: UserWsHandler, // for access callbacks inside session + + pub fn init(allocator: std.mem.Allocator, conn: Conn, handler: UserWsHandler) WsSession { + return .{ + .conn = conn, + .socket_owned = conn.socket.*, + .outbox = std.ArrayList([]const u8).init(allocator), + .mutex = .{}, + .allocator = allocator, + .handler = handler, + }; + } + + pub fn deinit(self: *WsSession) void { + self.mutex.lock(); + defer self.mutex.unlock(); + for (self.outbox.items) |msg| { + self.allocator.free(msg); + } + self.outbox.deinit(); + } + + pub fn scheduleSend(self: *WsSession, data: []const u8) !void { // function for safe call from any thread + self.mutex.lock(); + if (!self.active) { + self.mutex.unlock(); + return error.Closed; + } + + const msg_copy = try self.allocator.dupe(u8, data); // copy msg to heap + try self.outbox.append(msg_copy); + self.mutex.unlock(); + + try self.conn.runtime.trigger(self.writer_task_id); // conn.runtime.trigger is thread-safe // this is task-writer in target thread + } +}; + + +pub const PubSub = struct { + topics: std.StringHashMap(std.ArrayList(*WsSession)), // Topic Name -> List of Connections + lock: std.Thread.RwLock, + allocator: std.mem.Allocator, + + pub fn init(allocator: std.mem.Allocator) PubSub { + return .{ + .topics = std.StringHashMap(std.ArrayList(*WsSession)).init(allocator), + .lock = .{}, + .allocator = allocator, + }; + } + + pub fn deinit(self: *PubSub) void { + self.lock.lock(); + defer self.lock.unlock(); + + var iter = self.topics.iterator(); + while (iter.next()) |entry| { + entry.value_ptr.deinit(); + self.allocator.free(entry.key_ptr.*); + } + self.topics.deinit(); + } + + pub fn subscribe(self: *PubSub, topic: []const u8, session: *WsSession) !void { + self.lock.lock(); + defer self.lock.unlock(); + + const entry = try self.topics.getOrPut(topic); + if (!entry.found_existing) { + entry.key_ptr.* = try self.allocator.dupe(u8, topic); + entry.value_ptr.* = std.ArrayList(*WsSession).init(self.allocator); + } + + for (entry.value_ptr.items) |existing| { // not add doublibgs + if (existing == session) return; + } + + try entry.value_ptr.append(session); + std.log.debug("PubSub: Client subscribed to '{s}'", .{topic}); + } + + pub fn unsubscribe(self: *PubSub, topic: []const u8, session: *WsSession) void { + self.lock.lock(); + defer self.lock.unlock(); + + if (self.topics.getPtr(topic)) |list| { + for (list.items, 0..) |s, i| { + if (s == session) { + _ = list.swapRemove(i); + break; + } + } + } + + //if (list.items.len == 0) { // del group if empty + // list.deinit(); + // if (self.topics.fetchRemove(topic)) |kv| { + // self.allocator.free(kv.key); + // } + //} + } + + + pub fn removeConn(self: *PubSub, session: *WsSession) void { // delete connection/client from all groups - call on_disconnect + self.lock.lock(); + defer self.lock.unlock(); + + var iter = self.topics.iterator(); + while (iter.next()) |entry| { + const list = entry.value_ptr; + var i: usize = 0; + while (i < list.items.len) { + if (list.items[i] == session) { + _ = list.swapRemove(i); + } else { + i += 1; + } + } + } + } + + + pub fn publish(self: *PubSub, topic: []const u8, message: []const u8, sender: ?*WsSession) void { + self.lock.lockShared(); // shared lock for read + defer self.lock.unlockShared(); + + if (self.topics.get(topic)) |list| { + for (list.items) |session| { + if (sender) |s| { // do not send for the sender + if (s == session) continue; + } + + session.scheduleSend(message) catch |e| { // todo repeat send on error (one laggy/dead client not blocks all) + std.log.warn("PubSub send failed: {}", .{e}); + }; + } + } + } +}; + + +// helpers + +fn writer_task(session: *WsSession) !void { // send message in same thread with conn + session.writer_task_id = session.conn.runtime.current_task.?; // save task id for PubSub + defer session.writer_done.store(true, .release); + + while (true) { + session.mutex.lock(); + if (!session.active) { + session.mutex.unlock(); + break; + } + + const batch = try session.outbox.toOwnedSlice(); + session.mutex.unlock(); + + if (batch.len > 0) { + for (batch) |msg| { + session.conn.send(msg) catch |e| { + std.log.debug("WS Writer Error: {s}", .{ @errorName(e) }); + }; + session.allocator.free(msg); + } + session.allocator.free(batch); + } + try session.conn.runtime.scheduler.trigger_await(); + } +} + + +pub fn handle_upgrade(ctx: *const zzz.Context, user_handler: UserWsHandler, stack_size_writer: usize) !zzz.HTTP.Respond { + const req = ctx.request; + + const upgrade = req.headers.get("Upgrade"); + if (upgrade == null or !std.mem.eql(u8, upgrade.?, "websocket")) { + ctx.response.status = .@"Bad Request"; + //ctx.response.body = "Expected WebSocket Upgrade"; + return .standard; + } + + const key = req.headers.get("Sec-WebSocket-Key") orelse return .standard; + const ext = req.headers.get("Sec-WebSocket-Extensions"); + + var header_buf = std.ArrayList(u8).init(ctx.allocator); + defer header_buf.deinit(); + + const res = try zzz.websocket.upgrade(&ctx.socket, ctx.runtime, ctx.allocator, key, ext, header_buf.writer()); + _ = try ctx.socket.send_all(ctx.runtime, header_buf.items); + + const session = try std.heap.page_allocator.create(WsSession); + session.* = WsSession.init(std.heap.page_allocator, res.conn, user_handler); + + session.conn.socket = &session.socket_owned; // use heap instead stack + session.conn.user_data = session; // for not use global maps that locks + + try ctx.runtime.spawn(.{ session }, writer_task, stack_size_writer); + + const internal_handler = zzz.websocket.Handler{ + .on_message = struct { + fn wrapper(c: Conn, d: []const u8) !void { + const s: *WsSession = @ptrCast(@alignCast(c.user_data)); // get session from user_data + if (s.handler.on_message) |f| try f(s, d); + } + }.wrapper, + + .on_close = struct { + fn wrapper(c: Conn, code: u16, reason: []const u8) !void { + _ = code; + _ = reason; + const s: *WsSession = @ptrCast(@alignCast(c.user_data)); + if (s.handler.on_close) |f| f(s); + } + }.wrapper, + + .on_disconnect = struct { + fn wrapper(c: Conn) !void { + const s: *WsSession = @ptrCast(@alignCast(c.user_data)); + if (s.handler.on_close) |f| f(s); + } + }.wrapper, + }; + + + if (user_handler.on_connect) |f| { + f(session) catch |e| { // clean if conn failed + std.log.err("WS on_connect error: {s}", .{ @errorName(e) }); + + session.active = false; + ctx.runtime.trigger(session.writer_task_id) catch {}; + + while(!session.writer_done.load(.acquire)){ + try zzz.tardy.Timer.delay(ctx.runtime, .{ .nanos = 1000 }); + } + + session.deinit(); + std.heap.page_allocator.destroy(session); + + std.log.info("WebSocket Loop finished and memory freed", .{}); + return .close; + }; + } + + //zzz.websocket.runLoop(session.conn, internal_handler, ctx.runtime.allocator) catch |err| { // sync loop + zzz.websocket.runLoop(session.conn, internal_handler, std.heap.page_allocator) catch |err| { // sync loop + if (err != error.Closed){ + std.log.err("WebSocket RunLoop Error: {s}", .{ @errorName(err) }); + } + //if (err == error.Closed) { + // std.log.info("WebSocket closed by browser", .{}); + //} + }; + + //std.log.info("Closing session...", .{}); + + session.mutex.lock(); + session.active = false; + session.mutex.unlock(); + + ctx.runtime.trigger(session.writer_task_id) catch {}; + + while (!session.writer_done.load(.acquire)) { // waiting while writer_task will be done + try zzz.tardy.Timer.delay(ctx.runtime, .{ .nanos = 1_000_000 }); // 1 ms + } + + session.deinit(); + std.heap.page_allocator.destroy(session); + + std.log.info("WebSocket Loop finished and memory freed", .{}); + return .close; +} + diff --git a/src/websocket/websocket.zig b/src/websocket/websocket.zig new file mode 100644 index 0000000..6f926cd --- /dev/null +++ b/src/websocket/websocket.zig @@ -0,0 +1,340 @@ + +// websocket - RFC 6455 + RFC 7692 (permessage-deflate) // todo fix last one + +const std = @import("std"); + +const Allocator = std.mem.Allocator; + +const SecureSocket = @import("secsock").SecureSocket; +const Runtime = @import("tardy").Runtime; + +const compress = std.compress; + + +pub const Conn = struct { + socket: *const SecureSocket, + runtime: *Runtime, + user_data: ?*anyopaque = null, + //allocator: Allocator, // maybe todo - and pass with on_upgrade + //compression: bool, + + pub fn send(self: Conn, data: []const u8) !void { + var buf = std.ArrayList(u8).init(self.runtime.allocator); + defer buf.deinit(); + + //try writeFrameHeader(buf.writer(), .text, data.len, true); + try writeFrameHeader(buf.writer(), .text, data.len, false); + try buf.appendSlice(data); + _ = try self.socket.send_all(self.runtime, buf.items); + } + + pub fn sendBinary(self: Conn, binary_data: []const u8) !void { + var buf = std.ArrayList(u8).init(self.runtime.allocator); + defer buf.deinit(); + + try writeFrameHeader(buf.writer(), .binary, binary_data.len, false); + try buf.appendSlice(binary_data); + _ = try self.socket.send_all(self.runtime, buf.items); + + } + + pub fn close(self: Conn, code: u16, reason: []const u8) !void { + if (reason.len > 123) return error.ReasonTooLong; + + var buf = std.ArrayList(u8).init(self.runtime.allocator); + defer buf.deinit(); + + const payload_len = 2 + reason.len; + try writeFrameHeader(buf.writer(), .close, payload_len, false); // close header + + try buf.writer().writeInt(u16, code, .big); // big-endian + + if (reason.len > 0) { + try buf.appendSlice(reason); + } + + _ = try self.socket.send_all(self.runtime, buf.items); + } + +}; + + +pub const Handler = struct { + on_connect: ?*const fn (Conn) anyerror!void = null, + on_message: ?*const fn (Conn, []const u8) anyerror!void = null, + on_binary: ?*const fn (Conn, []const u8) anyerror!void = null, + on_close: ?*const fn (Conn, u16, []const u8) anyerror!void = null, + on_disconnect: ?*const fn (Conn) anyerror!void = null, +}; + +pub const HandshakeResult = struct { + conn: Conn, + //compression: bool, +}; + +pub fn upgrade( + socket: *const SecureSocket, + runtime: *Runtime, + allocator: Allocator, + sec_websocket_key: []const u8, + sec_websocket_extensions: ?[]const u8, + response_writer: anytype, +) !HandshakeResult { + _ = sec_websocket_extensions; + //var compression = false; + //if (sec_websocket_extensions) |ext| { + // if (std.mem.indexOf(u8, ext, "permessage-deflate") != null) { + // const has_client_no_ctx = std.mem.indexOf(u8, ext, "client_no_context_takeover") != null; + // const has_server_no_ctx = std.mem.indexOf(u8, ext, "server_no_context_takeover") != null; + // if (has_client_no_ctx and has_server_no_ctx) { + // compression = true; + // } + // } + //} + + const accept = try computeAccept(allocator, sec_websocket_key); + try response_writer.writeAll("HTTP/1.1 101 Switching Protocols\r\n"); + try response_writer.writeAll("Upgrade: websocket\r\n"); + try response_writer.writeAll("Connection: Upgrade\r\n"); + try response_writer.writeAll("Sec-WebSocket-Accept: "); + try response_writer.writeAll(accept); + try response_writer.writeAll("\r\n"); + //if (compression) { + // try response_writer.writeAll("Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n"); + //} + try response_writer.writeAll("\r\n"); + + return .{ + .conn = Conn{ + .socket = socket, + .runtime = runtime, + .user_data = null, + //.compression = compression, + }, + //.compression = compression, + }; +} + +pub fn runLoop(conn: Conn, handler: Handler, allocator: Allocator) !void { + const buffer = try allocator.alloc(u8, 65536); + defer allocator.free(buffer); + + var fragment = std.ArrayList(u8).init(allocator); + defer fragment.deinit(); + + while (true) { + const n = conn.socket.recv(conn.runtime, buffer) catch |err| { + if (err == error.Closed) { + if (handler.on_disconnect) |on_disc| try on_disc(conn); + return; + } + return err; + }; + + var i: usize = 0; + while (i < n) { + const byte1 = buffer[i]; + i += 1; + if (i >= n) return error.InvalidFrame; + const byte2 = buffer[i]; + i += 1; + + const fin = (byte1 & 0x80) != 0; + const rsv1 = (byte1 & 0x40) != 0; + const opcode = byte1 & 0x0F; + const has_mask = (byte2 & 0x80) != 0; + var payload_len = @as(usize, byte2 & 0x7F); + + if (payload_len == 126) { + if (i + 2 > n) return error.InvalidFrame; + //payload_len = @as(usize, @bitCast(std.mem.readIntBig(u16, buffer[i..]))); + //payload_len = @as(usize, @bitCast(std.mem.readInt(u16, buffer[i..], .big))); + payload_len = @as(usize, @intCast(buffer[i])) << 8 | + @as(usize, @intCast(buffer[i + 1])); + i += 2; + } else if (payload_len == 127) { + if (i + 8 > n) return error.InvalidFrame; + //payload_len = @as(usize, @bitCast(std.mem.readIntBig(u64, buffer[i..]))); + //payload_len = @as(usize, @bitCast(std.mem.readInt(u64, buffer[i..], .big))); + payload_len = + @as(usize, @intCast(buffer[i + 0])) << 56 | + @as(usize, @intCast(buffer[i + 1])) << 48 | + @as(usize, @intCast(buffer[i + 2])) << 40 | + @as(usize, @intCast(buffer[i + 3])) << 32 | + @as(usize, @intCast(buffer[i + 4])) << 24 | + @as(usize, @intCast(buffer[i + 5])) << 16 | + @as(usize, @intCast(buffer[i + 6])) << 8 | + @as(usize, @intCast(buffer[i + 7])); + i += 8; + } + + var mask: [4]u8 = undefined; + if (has_mask) { + if (i + 4 > n) return error.InvalidFrame; + mask = buffer[i..][0..4].*; + i += 4; + } + if (i + payload_len > n) return error.InvalidFrame; + + const payload_start = i; + const payload_end = i + payload_len; + i = payload_end; + + if (has_mask) { + for (payload_start..payload_end) |j| { + buffer[j] ^= mask[(j - payload_start) % 4]; + } + } + + var payload = buffer[payload_start..payload_end]; + + // Compression (permessage-deflate) // todo fix + if (rsv1) return error.CompressionNotNegotiated; + //var decompressed: ?[]u8 = null; + //if (rsv1) { + // if (!conn.compression) return error.CompressionNotNegotiated; + // const inflated_len = payload.len + 4; + // const inflated = try allocator.alloc(u8, inflated_len); + // errdefer allocator.free(inflated); + // @memcpy(inflated[0..payload.len], payload); + // inflated[payload.len + 0] = 0x00; + // inflated[payload.len + 1] = 0x00; + // inflated[payload.len + 2] = 0xFF; + // inflated[payload.len + 3] = 0xFF; + // const stream = std.io.fixedBufferStream(inflated); + // var inflater = compress.flate.InflateStream.init(stream.reader(), .{}); + // defer inflater.deinit(); + // var out = std.ArrayList(u8).init(allocator); + // errdefer out.deinit(); + // try inflater.reader().readAllArrayList(&out, 1024 * 1024); + // allocator.free(inflated); + // decompressed = try out.toOwnedSlice(); + // payload = decompressed.?; + //} + + var is_text: ?bool = null; + switch (opcode) { + 0x1 => { // Text + if (is_text == null) is_text = true; + + if (fin) { + if (handler.on_message) |on_msg| try on_msg(conn, payload); + //if (decompressed) |d| allocator.free(d); + } else { + try fragment.appendSlice(payload); + //if (decompressed) |d| allocator.free(d); + } + }, + + 0x2 => { // Binary + if (is_text == null) is_text = false; + + if (fin) { + if (handler.on_binary) |on_bin| try on_bin(conn, payload); + //if (decompressed) |d| allocator.free(d); + } else { + try fragment.appendSlice(payload); + //if (decompressed) |d| allocator.free(d); + } + }, + + 0x0 => { // Continuation + try fragment.appendSlice(payload); + if (fin) { + const full = fragment.items; + if (is_text orelse true) { + if (handler.on_message) |f| try f(conn, full); + } else { + if (handler.on_binary) |f| try f(conn, full); + } + is_text = null; + fragment.clearRetainingCapacity(); + } + //if (decompressed) |d| allocator.free(d); + }, + 0x8 => { // Close + var code: u16 = 1000; // Normal closure code by default + var reason_slice: []const u8 = ""; + if (payload.len >= 2) { + code = std.mem.readInt(u16, payload[0..2], .big); + + if (payload.len > 2) { + reason_slice = payload[2..]; + if (!std.unicode.utf8ValidateSlice(reason_slice)) { + //conn.socket.socket.close_blocking(); // that makes tardy + //if (decompressed) |d| allocator.free(d); + if (handler.on_disconnect) |on_disc| try on_disc(conn); + return; + } + } + } + conn.close(code, "") catch {}; // just exit + if (handler.on_close) |on_close| try on_close(conn, code, reason_slice); + if (handler.on_disconnect) |on_disc| try on_disc(conn); + //if (decompressed) |d| allocator.free(d); + return; + }, + 0x9 => { // Ping + var buf = std.ArrayList(u8).init(allocator); + defer buf.deinit(); + try writeFrameHeader(buf.writer(), .pong, payload.len, false); + try buf.appendSlice(payload); + _ = try conn.socket.send_all(conn.runtime, buf.items); + //if (decompressed) |d| allocator.free(d); + }, + 0xA => { // Pong - ignore + //if (decompressed) |d| allocator.free(d); + }, + else => { + //if (decompressed) |d| allocator.free(d); + return error.UnsupportedOpcode; + }, + } + } + } +} + + +// helpers + +fn computeAccept(allocator: Allocator, key: []const u8) ![]const u8 { + const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + var hasher = std.crypto.hash.Sha1.init(.{}); + hasher.update(key); + hasher.update(magic); + var hash: [20]u8 = undefined; + hasher.final(&hash); + var buf: [28]u8 = undefined; + const encoded = std.base64.standard.Encoder.encode(&buf, &hash); + return try allocator.dupe(u8, encoded); +} + +const OpCode = enum(u8) { + continuation = 0x0, + text = 0x1, + binary = 0x2, + close = 0x8, + ping = 0x9, + pong = 0xA, +}; + +fn writeFrameHeader(writer: anytype, opcode: OpCode, payload_len: usize, compressed: bool) !void { + var first_byte: u8 = @intFromEnum(opcode); + first_byte |= 0x80; + + if (compressed) first_byte |= 0x40; + try writer.writeByte(first_byte); + + if (payload_len < 126) { + try writer.writeByte(@intCast(payload_len)); + } else if (payload_len <= 0xFFFF) { + var buf: [2]u8 = undefined; + std.mem.writeInt(u16, &buf, @intCast(payload_len), .big); + try writer.writeAll(&buf); + } else { + var buf: [8]u8 = undefined; + std.mem.writeInt(u64, &buf, payload_len, .big); + try writer.writeAll(&buf); + } +} + From 1701c22a46e586ab639452296fea957212460caa Mon Sep 17 00:00:00 2001 From: 221V <221v92@gmail.com> Date: Sat, 27 Dec 2025 13:29:54 +0200 Subject: [PATCH 02/26] ws file upload example, fix ws frame buffer --- .gitignore | 1 + build.zig | 14 +- examples_ws/README.md | 10 +- examples_ws/bert.zig | 750 +++++++++++++++++++++++++++ examples_ws/example_ws_4.zig | 546 +++++++++++++++++++ examples_ws/static/BigInteger.min.js | 1 + examples_ws/static/bert_ftp.js | 610 ++++++++++++++++++++++ examples_ws/static/form.js | 68 +++ src/websocket/pubsub.zig | 59 ++- src/websocket/websocket.zig | 370 +++++++------ 10 files changed, 2253 insertions(+), 176 deletions(-) create mode 100644 examples_ws/bert.zig create mode 100644 examples_ws/example_ws_4.zig create mode 100755 examples_ws/static/BigInteger.min.js create mode 100644 examples_ws/static/bert_ftp.js create mode 100644 examples_ws/static/form.js diff --git a/.gitignore b/.gitignore index 87bbe59..e304926 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ heaptrack* /ex_ws_1 /ex_ws_2 /ex_ws_3 +/ex_ws_4 diff --git a/build.zig b/build.zig index b8e916c..21b77aa 100644 --- a/build.zig +++ b/build.zig @@ -42,6 +42,7 @@ pub fn build(b: *std.Build) void { const example1_ws_step = b.step("ex_ws_1", "Build ws example1"); const example2_ws_step = b.step("ex_ws_2", "Build ws example2"); const example3_ws_step = b.step("ex_ws_3", "Build ws example3"); + const example4_ws_step = b.step("ex_ws_4", "Build ws example4"); const exe_ws1 = b.addExecutable(.{ // without C libs .name = "ex_ws_1", // exe name @@ -61,10 +62,17 @@ pub fn build(b: *std.Build) void { .target = target, .optimize = optimize, }); + const exe_ws4 = b.addExecutable(.{ // without C libs + .name = "ex_ws_4", // exe name + .root_source_file = b.path("examples_ws/example_ws_4.zig"), // main file + .target = target, + .optimize = optimize, + }); exe_ws1.root_module.addImport("zzz", zzz); exe_ws2.root_module.addImport("zzz", zzz); exe_ws3.root_module.addImport("zzz", zzz); + exe_ws4.root_module.addImport("zzz", zzz); const install_ws1 = b.addInstallBinFile(exe_ws1.getEmittedBin(), "../../ex_ws_1"); // b.addInstallBinFile(exe_ws1.getEmittedBin(), "ex_ws_1"); // -femit-bin=ex_ws_1 // to project root b.getInstallStep().dependOn(&install_ws1.step); @@ -75,6 +83,9 @@ pub fn build(b: *std.Build) void { const install_ws3 = b.addInstallBinFile(exe_ws3.getEmittedBin(), "../../ex_ws_3"); // to project root b.getInstallStep().dependOn(&install_ws3.step); example3_ws_step.dependOn(&install_ws3.step); + const install_ws4 = b.addInstallBinFile(exe_ws4.getEmittedBin(), "../../ex_ws_4"); // to project root + b.getInstallStep().dependOn(&install_ws4.step); + example4_ws_step.dependOn(&install_ws4.step); b.default_step = b.getInstallStep(); //b.installArtifact(exe_test1); // saves to /zig-out/bin/test1 @@ -82,7 +93,7 @@ pub fn build(b: *std.Build) void { all_ws_examples_step.dependOn(&install_ws1.step); all_ws_examples_step.dependOn(&install_ws2.step); all_ws_examples_step.dependOn(&install_ws3.step); - + all_ws_examples_step.dependOn(&install_ws4.step); const tests = b.addTest(.{ @@ -134,3 +145,4 @@ fn add_example( run_step.dependOn(&install_artifact.step); run_step.dependOn(&run_artifact.step); } + diff --git a/examples_ws/README.md b/examples_ws/README.md index 90f3b8f..cf639d3 100644 --- a/examples_ws/README.md +++ b/examples_ws/README.md @@ -7,14 +7,17 @@ zig build zig build ex_ws_1 zig build ex_ws_2 zig build ex_ws_3 +zig build ex_ws_4 # ws example ./ex_ws_1 // goto http://localhost:3010/ , open browser webmaster console ws.send(42); ws.send("hello world"); +// in debug build you can see log of received messages -# wss example (with cert) + +# wss example (with cert) // you need sudo because port 443 used sudo ./ex_ws_2 // goto https://test1.ls/ , open browser webmaster console ws.send(42); @@ -35,5 +38,10 @@ ws.send("general:hello 2"); ws.send("general:43"); ws.send("gen:hello"); // and check received messages in first window + + +# ws file upload example, with queue and pause-resume feature +./ex_ws_4 +// goto http://localhost:3010/ , select files for upload ``` diff --git a/examples_ws/bert.zig b/examples_ws/bert.zig new file mode 100644 index 0000000..130e107 --- /dev/null +++ b/examples_ws/bert.zig @@ -0,0 +1,750 @@ + +// https://github.com/221V/zig_erl_bert for BERT encode-decode + +const std = @import("std"); + +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayList; + +const BigInt = std.math.big.int.Managed; +const Limb = std.math.big.Limb; +const Endian = std.builtin.Endian; + +const assert = std.debug.assert; + + +pub const Value_Tag = enum(u8){ + int, + big_int, + float, + atom, + binary, + tuple, + list, + map, + @"null", +}; + + +const Map_Pair = struct{ + key: Bert_Value, + val: Bert_Value, +}; + +pub const Bert_Value = union(Value_Tag){ + int: i64, + big_int: BigInt, + float: f64, + atom: []const u8, + binary: []const u8, + tuple: []Bert_Value, + list: []Bert_Value, + map: []Map_Pair, + @"null": void, +}; + + +fn readExact(reader: anytype, buf: []u8) !void{ + var offset: usize = 0; + while(offset < buf.len){ + const n = try reader.read(buf[offset..]); + if(n == 0){ return error.EndOfStream; } + offset += n; + } +} + + +pub const Bert = struct{ + const Self = @This(); + allocator: Allocator, + + pub fn init(allocator: Allocator) Self{ + return .{ .allocator = allocator }; + } + + pub fn deinit(_: *Self) void{ } + + // constructors + pub fn int(self: Self, n: i64) Bert_Value{ + _ = self; + return .{ .int = n }; + } + + pub fn bigInt(self: Self, n: BigInt) Bert_Value{ + _ = self; + return .{ .big_int = n }; + } + + pub fn float(self: Self, f: f64) Bert_Value{ + _ = self; + return .{ .float = f }; + } + + pub fn atom(self: Self, s: []const u8) !Bert_Value{ + const copy = try self.allocator.dupe(u8, s); + return .{ .atom = copy }; + } + + pub fn binary(self: Self, data: []const u8) !Bert_Value{ + const copy = try self.allocator.dupe(u8, data); + return .{ .binary = copy }; + } + + pub fn tuple(self: Self, elems: []const Bert_Value) !Bert_Value{ + const copy = try self.allocator.dupe(Bert_Value, elems); + return .{ .tuple = copy }; + } + + pub fn list(self: Self, elems: []const Bert_Value) !Bert_Value{ + const copy = try self.allocator.dupe(Bert_Value, elems); + return .{ .list = copy }; + } + + pub fn map(self: Self, pairs: []const Map_Pair) !Bert_Value{ + const copy = try self.allocator.dupe(Map_Pair, pairs); + return .{ .map = copy }; + } + + pub fn @"null"(self: Self) Bert_Value{ + _ = self; + return .{ .@"null" = {} }; + } + + // encode + pub fn encode(self: Self, term: Bert_Value) ![]u8{ + var buffer = ArrayList(u8).init(self.allocator); + defer buffer.deinit(); + + try buffer.append(131); // VERSION_MAGIC + try encodeValue(self.allocator, &buffer, term); + + return buffer.toOwnedSlice(); + } + + // decode + pub fn decode(self: Self, data: []const u8) !Bert_Value{ + if(data.len == 0 or data[0] != 131){ + return error.InvalidVersion; + } + var stream = std.io.fixedBufferStream(data[1..]); + return decodeValue(self.allocator, stream.reader()); + } +}; + + +// helpers +fn encodeValue(allocator: Allocator, buffer: *ArrayList(u8), value: Bert_Value) !void{ + switch(value){ + .int => |n|{ + if(n >= 0 and n <= 255){ + try buffer.append(97); // SMALL_INTEGER_EXT + try buffer.append( @as(u8, @intCast(n)) ); + }else if(n >= -2_147_483_648 and n <= 2_147_483_647){ + try buffer.append(98); // INTEGER_EXT + const be = @byteSwap( @as(i32, @intCast(n)) ); + try buffer.appendSlice(std.mem.asBytes(&be)); + }else{ + try encodeBigInt(allocator, buffer, n, false); + } + }, + + .big_int => |bi|{ + if( bi.eqlZero() ){ // encode zero as SMALL_BIG_EXT: [110, 1, 0, 0] + try buffer.append(110); + try buffer.append(1); + try buffer.append(0); + try buffer.append(0); + return; + } + + const is_neg = !bi.isPositive(); + var abs_bi = try BigInt.init(allocator); + defer abs_bi.deinit(); + try abs_bi.copy( bi.toConst() ); + if(is_neg){ + abs_bi.negate(); + } + + try encodeBigIntFromAbs(allocator, buffer, &abs_bi, is_neg); + }, + + .float => |f|{ + try buffer.append(70); // NEW_FLOAT_EXT + const bits = @as(u64, @bitCast(f)); + var buf: [8]u8 = undefined; + std.mem.writeInt(u64, &buf, bits, .big); + try buffer.appendSlice(&buf); + }, + + .atom => |s|{ + if(s.len > 65535){ return error.AtomTooLong; } + try buffer.append(118); // ATOM_UTF8_EXT + const len_be = @byteSwap( @as(u16, @intCast( @as(u16, @intCast(s.len)) )) ); + try buffer.appendSlice(std.mem.asBytes(&len_be)); + try buffer.appendSlice(s); + }, + + .binary => |b|{ + if(b.len > std.math.maxInt(u32)){ return error.BinaryTooLong; } + try buffer.append(109); // BINARY_EXT + const len_be = @byteSwap( @as(u32, @intCast(b.len)) ); + try buffer.appendSlice(std.mem.asBytes(&len_be)); + try buffer.appendSlice(b); + }, + + .tuple => |elems|{ + if(elems.len <= 255){ + try buffer.append(104); // SMALL_TUPLE_EXT + try buffer.append( @as(u8, @intCast(elems.len)) ); + }else{ + try buffer.append(105); // LARGE_TUPLE_EXT + const len_be = @byteSwap( @as(u32, @intCast(elems.len)) ); + try buffer.appendSlice(std.mem.asBytes(&len_be)); + } + for(elems) |elem|{ + try encodeValue(allocator, buffer, elem); + } + }, + + .list => |elems|{ + try buffer.append(108); // LIST_EXT + const len_be = @byteSwap( @as(u32, @intCast(elems.len)) ); + try buffer.appendSlice(std.mem.asBytes(&len_be)); + for(elems) |elem|{ + try encodeValue(allocator, buffer, elem); + } + try buffer.append(106); // NIL_EXT + }, + + .map => |pairs|{ + if(pairs.len > std.math.maxInt(u32)){ return error.MapTooLarge; } + try buffer.append(116); // MAP_EXT + const len_be = @byteSwap( @as(u32, @intCast(pairs.len)) ); + try buffer.appendSlice(std.mem.asBytes(&len_be)); + for(pairs) |pair|{ + try encodeValue(allocator, buffer, pair.key); + try encodeValue(allocator, buffer, pair.val); + } + }, + + .@"null" => { + try buffer.append(106); // NIL_EXT + }, + } +} + + +fn encodeBigInt(allocator: Allocator, buffer: *ArrayList(u8), n: i64, is_neg: bool) !void{ + var abs_n: u64 = if(is_neg) @as(u64, @intCast(-n)) else @as(u64, @intCast(n)); + var digits = std.ArrayList(u8).init(allocator); + defer digits.deinit(); + + if(abs_n == 0){ + try digits.append(0); + }else{ + while(abs_n > 0) : (abs_n >>= 8){ + try digits.append( @as(u8, @intCast(abs_n & 0xFF)) ); + } + } + + const len = digits.items.len; + if(len > 255){ return error.BigIntTooLargeForSmallBig; } + + try buffer.append(110); // SMALL_BIG_EXT + try buffer.append( @as(u8, @intCast(len)) ); + try buffer.append(if (is_neg) 1 else 0); + try buffer.appendSlice(digits.items); +} + + +fn encodeBigIntFromAbs(allocator: Allocator, buffer: *ArrayList(u8), abs_bi: *const BigInt, is_neg: bool) !void{ // abs_bi > 0 + var digits = std.ArrayList(u8).init(allocator); + defer digits.deinit(); + + for( abs_bi.limbs[0..abs_bi.len()] ) |limb|{ + var val = limb; + var i: usize = 0; + while(i < @sizeOf(Limb)) : (i += 1){ + try digits.append( @as(u8, @truncate(val)) ); + val >>= 8; + } + } + + while(digits.items.len > 1 and digits.items[digits.items.len - 1] == 0){ + _ = digits.pop(); + } + + const len = digits.items.len; + if(len > 255){ return error.BigIntTooLargeForSmallBig; } + + try buffer.append(110); // SMALL_BIG_EXT + try buffer.append( @as(u8, @intCast(len)) ); + try buffer.append( if(is_neg) 1 else 0 ); + try buffer.appendSlice(digits.items); +} + + +fn decodeValue(allocator: Allocator, reader: anytype) !Bert_Value{ + const tag = try reader.readByte(); + //std.debug.print("tag: {}\n", .{tag}); // debug + switch(tag){ + 97 => { // SMALL_INTEGER_EXT + const b = try reader.readByte(); + return Bert_Value{ .int = b }; + }, + + 98 => { // INTEGER_EXT + var buf: [4]u8 = undefined; + try readExact(reader, &buf); + const n = std.mem.readInt(i32, &buf, .big); + return Bert_Value{ .int = n }; + }, + + 70 => { // NEW_FLOAT_EXT + var buf: [8]u8 = undefined; + try readExact(reader, &buf); + const bits = std.mem.readInt(u64, &buf, .big); + return Bert_Value{ .float = @as(f64, @bitCast(bits)) }; + }, + + 118 => { // ATOM_UTF8_EXT + var len_buf: [2]u8 = undefined; + try readExact(reader, &len_buf); + const len = std.mem.readInt(u16, &len_buf, .big); + const atom = try allocator.alloc(u8, len); + errdefer allocator.free(atom); + try readExact(reader, atom); + return Bert_Value{ .atom = atom }; + }, + + 107 => { // STRING_EXT + var len_buf: [2]u8 = undefined; + try readExact(reader, &len_buf); + const len = std.mem.readInt(u16, &len_buf, .big); + const str = try allocator.alloc(u8, len); + errdefer allocator.free(str); + try readExact(reader, str); + return Bert_Value{ .binary = str }; + }, + + 109 => { // BINARY_EXT + var len_buf: [4]u8 = undefined; + try readExact(reader, &len_buf); + const len = std.mem.readInt(u32, &len_buf, .big); + const bin = try allocator.alloc(u8, len); + errdefer allocator.free(bin); + try readExact(reader, bin); + return Bert_Value{ .binary = bin }; + }, + + 104, 105 => { // SMALL_TUPLE_EXT, LARGE_TUPLE_EXT + const arity: u32 = if(tag == 104) try reader.readByte() else blk: { + var len_buf: [4]u8 = undefined; + try readExact(reader, &len_buf); + break :blk std.mem.readInt(u32, &len_buf, .big); + }; + const elems = try allocator.alloc(Bert_Value, arity); + for(elems) |*elem|{ + elem.* = try decodeValue(allocator, reader); + } + return Bert_Value{ .tuple = elems }; + }, + + 108 => { // LIST_EXT + var len_buf: [4]u8 = undefined; + try readExact(reader, &len_buf); + const len = std.mem.readInt(u32, &len_buf, .big); + const elems = try allocator.alloc(Bert_Value, len); + errdefer allocator.free(elems); + for(elems) |*elem|{ + //for(elems, 0..) |*elem, i|{ + //std.debug.print("lets Decode list element {}: {any}\n", .{i, elem}); // debug + elem.* = try decodeValue(allocator, reader); + //std.debug.print("Decoded list element {}: {any}\n", .{i, elem}); // debug + } + const tail_tag = try reader.readByte(); + if(tail_tag != 106){ return error.ImproperListNotSupported; } + return Bert_Value{ .list = elems }; + }, + + 106 => { // NIL_EXT + return Bert_Value{ .@"null" = {} }; + }, + + 110 => { // SMALL_BIG_EXT + const n = try reader.readByte(); + const sign = try reader.readByte(); + const is_neg = sign != 0; + + if(n == 0){ + return Bert_Value{ .int = 0 }; + } + + if(n <= 8){ + var value: u64 = 0; + var shift: u6 = 0; + for(0..n) |_|{ + const byte = try reader.readByte(); + value |= @as(u64, byte) << @as(u6, shift); + shift += 8; + } + + if(is_neg){ + return Bert_Value{ .int = - @as(i64, @intCast(value)) }; + }else if(value <= @as(u64, @intCast(std.math.maxInt(i64))) ){ + return Bert_Value{ .int = @as(i64, @intCast(value)) }; + } + } + + var bi = try BigInt.init(allocator); + errdefer bi.deinit(); + + var base = try BigInt.initSet(allocator, 256); + defer base.deinit(); + var zero = try BigInt.initSet(allocator, 0); + defer zero.deinit(); + + try bi.set(0); + var power = try BigInt.initSet(allocator, 1); + defer power.deinit(); + + for(0..n) |_|{ + const byte = try reader.readByte(); + var digit = try BigInt.initSet(allocator, byte); + defer digit.deinit(); + var term = try BigInt.init(allocator); + defer term.deinit(); + try term.mul(&digit, &power); + try bi.add(&bi, &term); + var next_power = try BigInt.init(allocator); + defer next_power.deinit(); + try next_power.mul(&power, &base); + power.deinit(); + power = next_power; + } + + if(is_neg){ + bi.negate(); + } + + return Bert_Value{ .big_int = bi }; + }, + + 116 => { // MAP_EXT + var len_buf: [4]u8 = undefined; + try readExact(reader, &len_buf); + const len = std.mem.readInt(u32, &len_buf, .big); + const pairs = try allocator.alloc(Map_Pair, len); + for(pairs) |*pair|{ + pair.key = try decodeValue(allocator, reader); + pair.val = try decodeValue(allocator, reader); + } + return Bert_Value{ .map = pairs }; + }, + + else => return error.UnsupportedTag, + } +} + + +// next for matching - select values from decoded with try, without shitcode (if(..){..}else if(..){..}) + +pub const Bert_Get_Error = error{ + Not_List, + Not_Tuple, + Not_Map, + Not_Atom, + Not_Binary, + Not_Int, + Not_Big_Int, + Not_Float, + Not_Found, + Index_Out_Of_Range, + Value_Out_Of_Range, + Big_Int_Too_Large, // bigger than max u128 = 340282366920938463463374607431768211455 +}; + + +pub fn get_int_as_u8(val: Bert_Value) Bert_Get_Error!u8{ + return switch(val){ + .int => |i|{ + if(i < 0 or i > std.math.maxInt(u8)){ return error.Value_Out_Of_Range; } + return @as(u8, @intCast(i)); + }, + else => error.Not_Int, + }; +} + + +pub fn get_int_as_u16(val: Bert_Value) Bert_Get_Error!u16{ + return switch(val){ + .int => |i|{ + if(i < 0 or i > std.math.maxInt(u16)){ return error.Value_Out_Of_Range; } + return @as(u16, @intCast(i)); + }, + else => error.Not_Int, + }; +} + + +pub fn get_int_as_u32(val: Bert_Value) Bert_Get_Error!u32{ + return switch(val){ + .int => |i|{ + if(i < 0 or i > std.math.maxInt(u32)){ return error.Value_Out_Of_Range; } + return @as(u32, @intCast(i)); + }, + else => error.Not_Int, + }; +} + + +pub fn get_int_as_u64(val: Bert_Value) Bert_Get_Error!u64{ + return switch(val){ + .int => |i|{ + if(i < 0){ return error.Value_Out_Of_Range; } // 0 - 18446744073709551615 + return @as(u64, @intCast(i)); + }, + else => error.Not_Int, + }; +} + + +pub fn get_big_int_as_u128(val: Bert_Value) Bert_Get_Error!u128{ + const bi = switch(val){ + .big_int => |b| b, + else => return error.Not_Big_Int, + }; + + if(!bi.isPositive()){ return error.Value_Out_Of_Range; } // negative value + + const limb_cnt = bi.len(); // limbs count (0..2) // each limb is little‑endian u64, 2 pcs for u128 needs + if(limb_cnt == 0){ return @as(u128, 0); } + if(limb_cnt > 2){ return error.Big_Int_Too_Large; } // bigger than max u128 + + const low = @as(u128, @intCast(bi.limbs[0])); + var result = low; + + if(limb_cnt == 2){ + const high = @as(u128, @intCast(bi.limbs[1])); + result = (high << 64) | low; // shift and concatenate + } + + return result; +} + + +pub fn get_int_as_i8(val: Bert_Value) Bert_Get_Error!i8{ + return switch(val){ + .int => |i|{ + if(i < std.math.minInt(i8) or i > std.math.maxInt(i8)){ return error.Value_Out_Of_Range; } + return @as(i8, @intCast(i)); + }, + else => error.Not_Int, + }; +} + + +pub fn get_int_as_i16(val: Bert_Value) Bert_Get_Error!i16{ + return switch(val){ + .int => |i|{ + if(i < std.math.minInt(i16) or i > std.math.maxInt(i16)){ return error.Value_Out_Of_Range; } + return @as(i16, @intCast(i)); + }, + else => error.Not_Int, + }; +} + + +pub fn get_int_as_i32(val: Bert_Value) Bert_Get_Error!i32{ + return switch(val){ + .int => |i|{ + if(i < std.math.minInt(i32) or i > std.math.maxInt(i32)){ return error.Value_Out_Of_Range; } + return @as(i32, @intCast(i)); + }, + else => error.Not_Int, + }; +} + + +pub fn get_int_as_i64(val: Bert_Value) Bert_Get_Error!i64{ + return switch(val){ + .int => |i| i, // i64 + else => error.Not_Int, + }; +} + + +pub fn get_float_as_f64(val: Bert_Value) Bert_Get_Error!f64{ + return switch(val){ + .float => |f| f, // f64 + else => error.Not_Float, + }; +} + + +pub fn get_atom_as_str(val: Bert_Value) Bert_Get_Error![]const u8{ + return switch(val){ + .atom => |at| at, // []const u8 + else => error.Not_Atom, + }; +} + + +pub fn get_binary_as_str(val: Bert_Value) Bert_Get_Error![]const u8{ + return switch(val){ + .binary => |bin| bin, // []const u8 + else => error.Not_Binary, + }; +} + + +pub fn get_list_elem(val: Bert_Value, idx: u32) Bert_Get_Error!Bert_Value{ // get elem from list + return switch(val){ + .list => |elems|{ + if(idx >= elems.len){ return error.Index_Out; } + return elems[idx]; + }, + else => error.Not_List, + }; +} + + +pub fn get_tuple_elem(val: Bert_Value, idx: u32) Bert_Get_Error!Bert_Value{ // get elem from tuple + return switch(val){ + .tuple => |elems|{ + if(idx >= elems.len){ return error.Index_Out_Of_Range; } + return elems[idx]; + }, + else => error.Not_Tuple, + }; +} + + +pub fn get_list(val: Bert_Value) Bert_Get_Error![]Bert_Value{ // get list + return switch(val){ + .list => |elems| elems, + else => error.Not_List, + }; +} + + +pub fn get_tuple(val: Bert_Value) Bert_Get_Error![]Bert_Value{ // get tuple + return switch(val){ + .tuple => |elems| elems, + else => error.Not_Tuple, + }; +} + + +pub fn map_lookup(val: Bert_Value, key: Bert_Value) Bert_Get_Error!Bert_Value{ + return switch(val){ + .map => |pairs|{ + for(pairs) |pair|{ + if(std.meta.eql(pair.key, key)){ return pair.val; } // compare type and value + } + return error.Not_Found; + }, + else => error.Not_Map, + }; +} + + +// next for pretty print in erlang style + +pub fn format_bert(allocator: Allocator, value: Bert_Value) ![]const u8{ + var out = std.ArrayList(u8).init(allocator); + defer out.deinit(); + + try writeValue(&out, value); + return out.toOwnedSlice(); +} + + +fn writeValue(buf: *std.ArrayList(u8), value: Bert_Value) !void{ + const writer = buf.writer(); + switch(value){ + .int => |i|{ + try writer.print("{}", .{i}); + }, + + .big_int => |bi|{ // BigInt to decimal string + const s = try bi.toString(buf.allocator, 10, .lower); + defer buf.allocator.free(s); + try writer.print("{any}", .{s}); + }, + + .float => |f|{ + try writer.print("{any}", .{f}); + }, + + .atom => |a|{ + const needQuotes = !isSimpleAtom(a); // simple atoms (ascii, digits and _ symbol, begins from ascii symbol) + if(needQuotes) try writer.print("'{s}'", .{a}) else try writer.print("{s}", .{a}); + }, + + .binary => |b|{ + //if(isPrintableAscii(b)){ // as text when ascii, otherwise bytes + try writer.print("<<\"{s}\">>", .{b}); + //}else{ + // try writer.print("<<", .{}); + // for(b, 0..) |byte, i|{ + // if(i != 0){ try writer.print(",", .{}); } + // try writer.print("{}", .{byte}); + // } + // try writer.print(">>", .{}); + //} + }, + + .tuple => |elems|{ + try writer.print("{{", .{}); + for(elems, 0..) |e, i|{ + if(i != 0){ try writer.print(", ", .{}); } + try writeValue(buf, e); + } + try writer.print("}}", .{}); + }, + + .list => |elems|{ + try writer.print("[", .{}); + for(elems, 0..) |e, i|{ + if(i != 0){ try writer.print(", ", .{}); } + try writeValue(buf, e); + } + try writer.print("]", .{}); + }, + + .map => |pairs|{ + try writer.print("#{{", .{}); + for(pairs, 0..) |p, i|{ + if(i != 0){ try writer.print(", ", .{}); } + try writeValue(buf, p.key); + try writer.print(" => ", .{}); + try writeValue(buf, p.val); + } + try writer.print("}}", .{}); + }, + + .@"null" => { // NIL is empty list in BERT + try writer.print("[]", .{}); + }, + } +} + + +fn isSimpleAtom(a: []const u8) bool{ + if(a.len == 0){ return false; } + if(std.ascii.isDigit(a[0])){ return false; } // first symbol can not be digit + for(a) |c|{ + if(!std.ascii.isAlphanumeric(c) and c != '_'){ return false; } + } + return true; +} + + +fn isPrintableAscii(b: []const u8) bool{ + for(b) |c|{ + if(c < 32 or c > 126){ return false; } + } + return true; +} + diff --git a/examples_ws/example_ws_4.zig b/examples_ws/example_ws_4.zig new file mode 100644 index 0000000..72dc732 --- /dev/null +++ b/examples_ws/example_ws_4.zig @@ -0,0 +1,546 @@ +const std = @import("std"); + +const zzz = @import("zzz"); +const Socket = zzz.tardy.Socket; +const File = zzz.tardy.File; +const Dir = zzz.tardy.Dir; +const Timer = zzz.tardy.Timer; // for cleaner + +const websocket = zzz.websocket; +const WsSession = zzz.WsSession; +const handle_upgrade = zzz.handle_upgrade; + +const Bert = @import("bert.zig").Bert; // https://github.com/221V/zig_erl_bert for BERT encode-decode +const Bert_Value = @import("bert.zig").Bert_Value; + + +const PORT = 3010; +const HOST = "0.0.0.0"; + +//const WS_STACK_SIZE = 1024 * 1024; // DEBUG +const WS_STACK_SIZE = 32 * 1024; // RELEASE + + +const ExtensionsLimits = struct { + //ext: []const u8, + exts: []const []const u8, // for set few filetypes with same max size + size: usize, +}; + +const UploadConfig = struct { + save_path_temp: []const u8 = "uploads/temp", + save_path_root: []const u8 = "uploads", + max_size_default: usize = 10 * 1024 * 1024, // default 10Mb + + allow_extensions: ?[]const []const u8 = &.{ ".jpg", ".png", ".gif", ".txt", ".pdf", ".mp3", ".mp4", ".avi" }, + deny_extensions: ?[]const []const u8 = &.{ ".exe", ".sh" }, + + extension_limits: []const ExtensionsLimits = &.{ + .{ .exts = &.{ ".txt" }, .size = 1 * 1024 * 1024 }, // 1Mb max + .{ + .exts = &.{ ".jpg", ".png", ".gif", ".pdf" }, + .size = 5 * 1024 * 1024 // 5Mb max + }, + .{ + .exts = &.{ ".mp3", ".mp4" }, + .size = 50 * 1024 * 1024 // 50Mb max + }, + .{ .exts = &.{ ".avi" }, .size = 2 * 1024 * 1024 * 1024 }, // 2Gb max + }, + + ttl_seconds: i64 = 24 * 60 * 60, // 24 hours for temp files +}; + +const config = UploadConfig{}; + + +// session state for uploads +const UploadState = struct { + file: File, + path: []const u8, + path_done: []const u8, + total_size: usize, + current_size: usize, + last_update: i64, +}; + + +// global or per-session map // use per-session user_data custom struct +const SessionContext = struct { + uploads: std.StringHashMap(UploadState), + allocator: std.mem.Allocator, +}; + + +// helpers + +fn checkLimits(name: []const u8, size: usize) !void { + const raw_ext = std.fs.path.extension(name); + + var ext_buf: [10]u8 = undefined; // 10 chars length for files extension + if (raw_ext.len >= ext_buf.len) return error.ExtensionTooLong; + + const lower_ext = std.ascii.lowerString(&ext_buf, raw_ext); + const ext = std.mem.trim(u8, lower_ext, &[_]u8{ 0, ' ', '\t', '\r', '\n' }); + + std.log.info("CheckLimits: file='{s}', raw_ext='{s}', normalized_ext='{s}', size={d}", .{ name, raw_ext, ext, size }); + + if (config.deny_extensions) |list| { // check blacklist + for (list) |denied| { + if (std.mem.eql(u8, ext, denied)) return error.ExtensionDenied; + } + } + + if (config.allow_extensions) |list| { // check whitelist + var found = false; + for (list) |allowed| { + if (std.mem.eql(u8, ext, allowed)) { found = true; break; } + } + if (!found){ + std.log.err("CheckLimits: Extension '{s}' not found in allow list", .{ext}); + return error.ExtensionNotAllowed; + } + } + + var limit = config.max_size_default; + + outer: for (config.extension_limits) |group| { // lets check file size limit // label :outer for exit both loops + for (group.exts) |group_ext| { + if (std.mem.eql(u8, ext, group_ext)) { + limit = group.size; + //std.log.info("CheckLimits: Matched group for '{s}', setting limit to {d}", .{group_ext, limit}); + break :outer; // limit found, stop search + } + } + } + + //std.log.info("CheckLimits: Checking size {d} vs limit {d}", .{size, limit}); + if (size > limit) { + std.log.err("CheckLimits: FAILED. Size {d} > Limit {d}", .{size, limit}); + return error.FileTooLarge; + } +} + + +fn getSavePath(allocator: std.mem.Allocator, filename: []const u8, total_size: usize) ![]const u8 { + //const ts = std.time.timestamp(); // construct path: root/timestamp_file + //const basename = std.fs.path.basename(filename); + //std.log.info("getSavePath: config.save_path_root = '{s}', basename = '{s}', ts = {d}", .{ config.save_path_root, basename, ts }); + + var hasher = std.hash.Wyhash.init(0); // construct path: root/. + hasher.update(filename); + hasher.update(std.mem.asBytes(&total_size)); + const hash = hasher.final(); + + const raw_ext = std.fs.path.extension(filename); + + var ext_buf: [10]u8 = undefined; // 10 chars length for files extension + //if (raw_ext.len >= ext_buf.len) return error.ExtensionTooLong; + + const ext = if (raw_ext.len < ext_buf.len) + std.ascii.lowerString(&ext_buf, raw_ext) + else + raw_ext; + + //std.log.info("getSavePath: config.save_path_root = '{s}', ext = '{s}', ts = {d}", .{ config.save_path_root, ext, ts }); + std.log.info("getSavePath: config.save_path_root = '{s}', hash = '{x}', ext = {s}", .{ config.save_path_root, hash, ext }); + return std.fmt.allocPrint(allocator, "{s}/{x}{s}", .{config.save_path_root, hash, ext}); // .{config.save_path_root, ts, ext}); // .{config.save_path_root, ts, basename}); +} + + +fn getFileInfo(allocator: std.mem.Allocator, filename: []const u8, total_size: usize, is_temp: bool) !struct{ path: []const u8, id: []const u8 } { + var hasher = std.hash.Wyhash.init(0); + hasher.update(filename); + hasher.update(std.mem.asBytes(&total_size)); + const hash = hasher.final(); + + const ext = std.fs.path.extension(filename); + const id_str = try std.fmt.allocPrint(allocator, "{x}", .{hash}); // id = hex hash + + const root = if (is_temp) config.save_path_temp else config.save_path_root; + const path = try std.fmt.allocPrint(allocator, "{s}/{s}{s}", .{root, id_str, ext}); + + return .{ .path = path, .id = id_str }; +} + + +fn cleanup_task(rt: *zzz.tardy.Runtime) !void { + std.log.info("Cleanup task started", .{}); + while (true) { + try Timer.delay(rt, .{ .seconds = 3600 }); // sleep 1 hour + + std.log.info("Cleanup task: Scanning for stale uploads...", .{}); + var dir = std.fs.cwd().openDir(config.save_path_temp, .{ .iterate = true }) catch continue; + defer dir.close(); + + var iter = dir.iterate(); + const now = std.time.timestamp(); + + while (iter.next() catch null) |entry| { + if (entry.kind == .file) { + const stat = dir.statFile(entry.name) catch continue; + const mtime_sec = @divFloor(stat.mtime, std.time.ns_per_s); // time of last modification, in ns + + if (now - mtime_sec > config.ttl_seconds) { + std.log.info("Cleanup task: Deleting stale file {s} (age: {d}s)", .{entry.name, now - mtime_sec}); + dir.deleteFile(entry.name) catch |e| std.log.err("Cleanup task delete error: {s}", .{ @errorName(e) }); + } + } + } + } +} + + +// WS Handlers + +fn on_ws_connect(session: *WsSession) !void { + std.log.info("Client connected", .{}); + + const ctx = try session.allocator.create(SessionContext); // init session context + ctx.* = .{ + .uploads = std.StringHashMap(UploadState).init(session.allocator), + .allocator = session.allocator, + }; + + session.context = ctx; +} + +fn on_ws_message(session: *WsSession, data: []const u8) !void { + // we expect binary BERT data -- if text, ignore or log + _ = session; + std.log.info("Received Text (ignoring): {s}", .{data}); +} + + +// helper for on_ws_binary file_upload handle +fn handleInitUpload(allocator: std.mem.Allocator, ctx: *SessionContext, name: []const u8, total: usize) !struct { offset: usize, response_id: []const u8 } { + std.log.info("DEBUG: handleInitUpload start for {s}", .{name}); + + checkLimits(name, total) catch |e| { + std.log.err("Limit check failed: {s}", .{ @errorName(e) }); + return e; + }; + + const info_temp = try getFileInfo(ctx.allocator, name, total, true); + const info_done = try getFileInfo(ctx.allocator, name, total, false); // for check maybe already uploaded + + std.log.info("DEBUG: Generated ID: {s}", .{info_temp.id}); + + std.fs.cwd().makePath(config.save_path_root) catch {}; // ensure dir exists + std.fs.cwd().makePath(config.save_path_temp) catch {}; + + if (ctx.uploads.fetchRemove(info_temp.id)) |kv| { // close existing handle if resuming + std.log.info("Resuming active session: closing old handle for {s}", .{name}); + kv.value.file.close_blocking(); + + ctx.allocator.free(kv.key); + ctx.allocator.free(kv.value.path); + ctx.allocator.free(kv.value.path_done); + } + + if (std.fs.cwd().access(info_done.path, .{})) { // already uploaded + std.log.info("File already uploaded: {s}", .{info_done.path}); + return .{ .offset = total, .response_id = info_temp.id }; // success, done + + //ctx.allocator.free(info_temp.path); + //ctx.allocator.free(info_temp.id); + //ctx.allocator.free(info_done.path); + //ctx.allocator.free(info_done.id); + + }else |_| {} // check temp (resume) + + var file_exists = false; + var existing_size: usize = 0; + + //const existing_file_result = std.fs.cwd().openFile(info_temp.path, .{ .mode = .read_write }); // try to open temp file that exists + //if(existing_file_result) |f| { // check if exists for resume + if( std.fs.cwd().openFile(info_temp.path, .{ .mode = .read_write }) ) |f| { // check if exists for resume + const stat = f.stat() catch |e| { + std.log.err("Stat failed: {s}", .{ @errorName(e) }); + f.close(); + return e; + }; + //const stat = try f.stat(); + + if(stat.size < total){ // resume - not uploded yet + file_exists = true; + existing_size = stat.size; + f.close(); + + } else if(stat.size == total){ // already uploaded ok + f.close(); + std.log.info("Temp file complete, returning done", .{}); + return .{ .offset = total, .response_id = info_temp.id }; // temp file done + + //ctx.allocator.free(info_temp.path); + //ctx.allocator.free(info_temp.id); + //ctx.allocator.free(info_done.path); + //ctx.allocator.free(info_done.id); + + } else { // error? rewrite + f.close(); + } + + }else |err| { // file not exists or access error + if (err != error.FileNotFound) { + std.log.err("Failed to check existing file '{s}': {s}", .{ info_temp.path, @errorName(err) }); // std.log.warn("OpenFile warning (not fatal): {s}", .{ @errorName(err) }); + return err; // return err and not rewrite + } + } + + std.log.info("DEBUG: Opening file: {s}... Exists: {}", .{ info_temp.path, file_exists }); + + const file = if (file_exists) // open for writing + try std.fs.cwd().openFile(info_temp.path, .{ .mode = .read_write }) + else + try std.fs.cwd().createFile(info_temp.path, .{}); + + if(file_exists){ // if resuming or new file + try file.seekTo(existing_size); + std.log.info("RESUMING: {s} -> {d}/{d}", .{info_temp.id, existing_size, total}); + }else{ + std.log.info("NEW: {s}", .{info_temp.id}); + } + + // Convert std.fs.File to tardy.File if needed, for non-blocking file I/O + // Better use tardy.File, but for simplicity we use std.fs.File handle wrapped + const tardy_file = zzz.tardy.File.from_std(file); + const id_key = try allocator.dupe(u8, info_temp.id); + + try ctx.uploads.put(id_key, .{ + .file = tardy_file, + .path = info_temp.path, // here write + .path_done = info_done.path, // here move when upload 100% + .total_size = total, + .current_size = if(file_exists) existing_size else 0, + .last_update = std.time.milliTimestamp(), + }); + + //ctx.allocator.free(info_done.id); + std.log.info("DEBUG: Init success, returning", .{}); + return .{ .offset = if(file_exists) existing_size else 0, .response_id = info_temp.id }; +} + +fn on_ws_binary(session: *WsSession, data: []const u8) !void { + var arena = std.heap.ArenaAllocator.init(session.allocator); // temp arena for parsing + defer arena.deinit(); + const temp_alloc = arena.allocator(); + + var b = Bert.init(temp_alloc); + const val = b.decode(data) catch |e| { // for file data Bert_Value must be Tuple{ftp, id, ..., data, status} + std.log.err("BERT Decode Error: {s}", .{ @errorName(e) }); + return; + }; + + switch(val){ + .tuple => |elems| { + if(elems.len != 13){ return; } // not file data // n2o ftp protocol has 13 elements + + const atom_tag = elems[0]; + switch (atom_tag) { + .atom => |s| if (!std.mem.eql(u8, s, "ftp")) return, // check 'ftp' atom + else => return, + } + + const id = try get_binary_str(elems[1]); + const name = try get_binary_str(elems[3]); + const total = try get_int_usize(elems[8]); + const offset = try get_int_usize(elems[9]); + const bin_data = try get_binary_str(elems[11]); + const status = try get_binary_str(elems[12]); + + const ctx_ptr = session.context orelse return; // get upload context + const ctx: *SessionContext = @ptrCast(@alignCast(ctx_ptr)); + + var reply_status: []const u8 = "send"; + var current_offset: usize = 0; + var response_id: []const u8 = id; // client id by default + + if(std.mem.eql(u8, status, "init")){ // start upload + std.log.info("Init upload: {s} ({d} bytes)", .{name, total}); + + if( handleInitUpload(ctx.allocator, ctx, name, total) ) |res| { + current_offset = res.offset; + response_id = res.response_id; + std.log.info("WS: Init OK, offset={d}", .{current_offset}); + } else |err| { + std.log.err("Init failed with exception: {s}", .{ @errorName(err) }); + reply_status = "error"; + } + + + }else if(std.mem.eql(u8, status, "send")){ // chunk received + if (ctx.uploads.getPtr(id)) |state| { // here id must be server id because client has update it after init + if (offset != state.current_size) { // validate offset + current_offset = state.current_size; // client out of sync? tell him where we are + }else{ // write data + _ = try state.file.write_all(session.conn.runtime, bin_data, null); + state.current_size += bin_data.len; + state.last_update = std.time.milliTimestamp(); + current_offset = state.current_size; + + if (state.current_size >= state.total_size) { + std.log.info("Upload complete. Move to: {s}", .{state.path_done}); + state.file.close_blocking(); + + std.fs.cwd().rename(state.path, state.path_done) catch |e| { + std.log.err("Failed to move file: {s}", .{ @errorName(e) }); // maybe todo send err status to client.. but file already uploaded + }; + + if(ctx.uploads.fetchRemove(id)) |kv| { + ctx.allocator.free(kv.key); // id + ctx.allocator.free(kv.value.path); + ctx.allocator.free(kv.value.path_done); + } + + //_ = ctx.uploads.remove(id); + } + } + + }else{ + reply_status = "error"; // upload session not found + } + } + + var reply_tuple_items = [_]Bert_Value{ // encode Reply + b.atom("ftp") catch unreachable, + b.binary(response_id) catch unreachable, + b.binary("") catch unreachable, // sid + b.binary("") catch unreachable, // name (empty for ack) + b.binary("") catch unreachable, + b.binary("") catch unreachable, + b.binary("") catch unreachable, + b.binary("") catch unreachable, + b.int(@intCast(total)), + b.int(@intCast(current_offset)), + b.int(0), // block + b.binary("") catch unreachable, // data + b.binary(reply_status) catch unreachable, // status + }; + + std.log.info("WS: Sending Reply status='{s}' offset={d}", .{reply_status, current_offset}); + const encoded = try b.encode(try b.tuple(&reply_tuple_items)); + + session.scheduleSendBinary(encoded) catch |e| { + std.log.err("WS: scheduleSendBinary FAILED: {s}", .{ @errorName(e) }); + }; + }, + else => {}, + } +} + +// helpers +fn get_binary_str(v: Bert_Value) ![]const u8 { + return switch(v) { .binary => |b| b, else => error.NotBinary }; +} + +fn get_int_usize(v: Bert_Value) !usize { + return switch(v) { + //.int => |i| @intCast(i), + //.big_int => 0, + .int => |i| if (i < 0) error.NegativeValue else @intCast(i), + .big_int => |bi| bi.toConst().toInt(usize) catch error.IntTooLarge, + else => error.NotInt + }; +} + + +fn on_ws_close(session: *WsSession) void { + if (session.context) |ptr| { + session.context = null; // clean context for avoid double free error + + const ctx: *SessionContext = @ptrCast(@alignCast(ptr)); + + var iter = ctx.uploads.iterator(); + while (iter.next()) |entry| { // clean up open files + entry.value_ptr.file.close_blocking(); + } + ctx.uploads.deinit(); + + session.allocator.destroy(ctx); + } +} + + +// HTTP Handler +fn on_request(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond { + const res = ctx.response; + res.status = .OK; + res.mime = zzz.HTTP.Mime.HTML; + // embed the JS client + res.body = + \\ + \\ + \\

zzz + WS File Upload Example

+ \\ + \\
+ \\ + \\ + \\ + \\ + ; + return .standard; +} + +fn on_ws_endpoint(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond { + return handle_upgrade(ctx, .{ + .on_connect = on_ws_connect, + .on_message = on_ws_message, + .on_binary = on_ws_binary, + .on_close = on_ws_close, + }, WS_STACK_SIZE); +} + + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + defer _ = gpa.deinit(); + + const socket = try Socket.init(.{ .tcp = .{ .host = HOST, .port = PORT } }); + defer socket.close_blocking(); + try socket.bind(); + try socket.listen(1024); + + const TardyType = zzz.tardy.Tardy(.auto); + var tardy = try TardyType.init(allocator, .{}); + defer tardy.deinit(); + + try tardy.entry(&socket, struct { + fn entry(rt: *zzz.tardy.Runtime, s: *const Socket) !void { + const server_config = zzz.ServerConfig{ .stack_size = 64 * 1024 }; + + const home_route = zzz.HTTP.Route.init("/").get({}, on_request); + const ws_route = zzz.HTTP.Route.init("/ws").get({}, on_ws_endpoint); + + //const js_route = zzz.HTTP.FsDir.serve("/static", zzz.tardy.Dir.cwd()); // bert_ftp.js + const std_static_dir = try std.fs.cwd().openDir("examples_ws/static", .{ .iterate = true }); + const static_dir = zzz.tardy.Dir.from_std(std_static_dir); + const js_route = zzz.HTTP.FsDir.serve("/static", static_dir); // bert_ftp.js + + const layers = &[_]zzz.HTTP.Layer{ home_route.layer(), ws_route.layer(), js_route }; + + const router = try rt.allocator.create(zzz.Router); + router.* = try zzz.Router.init(rt.allocator, layers, .{ .not_found = on_request }); + + const provisions = try rt.allocator.create(zzz.tardy.Pool(zzz.Provision)); + provisions.* = try zzz.tardy.Pool(zzz.Provision).init(rt.allocator, 1024, .static); + const byte_count = provisions.items.len * @sizeOf(zzz.Provision); + @memset(@as([*]u8, @ptrCast(provisions.items.ptr))[0..byte_count], 0); + + const connection_count = try rt.allocator.create(usize); + connection_count.* = 0; + const accept_queued = try rt.allocator.create(bool); + accept_queued.* = false; + + try rt.spawn( + .{ rt, server_config, router, zzz.secsock.SecureSocket.unsecured(s.*), provisions, connection_count, accept_queued }, + zzz.Server.main_frame, + server_config.stack_size + ); + + //try rt.spawn(.{rt}, cleanup_task, 64 * 1024); + } + }.entry); +} + diff --git a/examples_ws/static/BigInteger.min.js b/examples_ws/static/BigInteger.min.js new file mode 100755 index 0000000..2303a07 --- /dev/null +++ b/examples_ws/static/BigInteger.min.js @@ -0,0 +1 @@ +var bigInt=function(t){"use strict";var e=1e7,r=7,o=9007199254740992,n=v(o),i="0123456789abcdefghijklmnopqrstuvwxyz",u="function"==typeof BigInt;function p(t,e,r,o){return void 0===t?p[0]:void 0!==e&&(10!=+e||r)?_(t,e,r,o):K(t)}function a(t,e){this.value=t,this.sign=e,this.isSmall=!1}function s(t){this.value=t,this.sign=t<0,this.isSmall=!0}function l(t){this.value=t}function f(t){return-o0?Math.floor(t):Math.ceil(t)}function m(t,r){var o,n,i=t.length,u=r.length,p=new Array(i),a=0,s=e;for(n=0;n=s?1:0,p[n]=o-a*s;for(;n0&&p.push(a),p}function d(t,e){return t.length>=e.length?m(t,e):m(e,t)}function b(t,r){var o,n,i=t.length,u=new Array(i),p=e;for(n=0;n0;)u[n++]=r%p,r=Math.floor(r/p);return u}function w(t,r){var o,n,i=t.length,u=r.length,p=new Array(i),a=0,s=e;for(o=0;o0;)u[n++]=a%p,a=Math.floor(a/p);return u}function M(t,e){for(var r=[];e-- >0;)r.push(0);return r.concat(t)}function N(t,r,o){return new a(t=0;--o)i=(u=i*s+t[o])-(n=c(u/r))*r,a[o]=0|n;return[a,0|i]}function B(t,r){var o,n=K(r);if(u)return[new l(t.value/n.value),new l(t.value%n.value)];var i,f=t.value,m=n.value;if(0===m)throw new Error("Cannot divide by zero");if(t.isSmall)return n.isSmall?[new s(c(f/m)),new s(f%m)]:[p[0],t];if(n.isSmall){if(1===m)return[t,p[0]];if(-1==m)return[t.negate(),p[0]];var d=Math.abs(m);if(d=0;n--){for(o=v-1,d[n+f]!==c&&(o=Math.floor((d[n+f]*v+d[n+f-1])/c)),i=0,u=0,a=b.length,p=0;ps&&(i=(i+1)*v),o=Math.ceil(i/u);do{if(A(p=q(r,o),f)<=0)break;o--}while(o);l.push(o),f=w(f,p)}return l.reverse(),[h(l),h(f)]}(f,m))[0];var I=t.sign!==n.sign,M=o[1],N=t.sign;return"number"==typeof i?(I&&(i=-i),i=new s(i)):i=new a(i,I),"number"==typeof M?(N&&(M=-M),M=new s(M)):M=new a(M,N),[i,M]}function A(t,e){if(t.length!==e.length)return t.length>e.length?1:-1;for(var r=t.length-1;r>=0;r--)if(t[r]!==e[r])return t[r]>e[r]?1:-1;return 0}function P(t){var e=t.abs();return!e.isUnit()&&(!!(e.equals(2)||e.equals(3)||e.equals(5))||!(e.isEven()||e.isDivisibleBy(3)||e.isDivisibleBy(5))&&(!!e.lesser(49)||void 0))}function Z(t,e){for(var r,o,n,i=t.prev(),u=i,p=0;u.isEven();)u=u.divide(2),p++;t:for(o=0;o=0?o=w(t,e):(o=w(e,t),r=!r),"number"==typeof(o=h(o))?(r&&(o=-o),new s(o)):new a(o,r)}(r,o,this.sign)},a.prototype.minus=a.prototype.subtract,s.prototype.subtract=function(t){var e=K(t),r=this.value;if(r<0!==e.sign)return this.add(e.negate());var o=e.value;return e.isSmall?new s(r-o):S(o,Math.abs(r),r>=0)},s.prototype.minus=s.prototype.subtract,l.prototype.subtract=function(t){return new l(this.value-K(t).value)},l.prototype.minus=l.prototype.subtract,a.prototype.negate=function(){return new a(this.value,!this.sign)},s.prototype.negate=function(){var t=this.sign,e=new s(-this.value);return e.sign=!t,e},l.prototype.negate=function(){return new l(-this.value)},a.prototype.abs=function(){return new a(this.value,!1)},s.prototype.abs=function(){return new s(Math.abs(this.value))},l.prototype.abs=function(){return new l(this.value>=0?this.value:-this.value)},a.prototype.multiply=function(t){var r,o,n,i=K(t),u=this.value,s=i.value,l=this.sign!==i.sign;if(i.isSmall){if(0===s)return p[0];if(1===s)return this;if(-1===s)return this.negate();if((r=Math.abs(s))0?function t(e,r){var o=Math.max(e.length,r.length);if(o<=30)return I(e,r);o=Math.ceil(o/2);var n=e.slice(o),i=e.slice(0,o),u=r.slice(o),p=r.slice(0,o),a=t(i,p),s=t(n,u),l=t(d(i,n),d(p,u)),f=d(d(a,M(w(w(l,a),s),o)),M(s,2*o));return y(f),f}(u,s):I(u,s),l)},a.prototype.times=a.prototype.multiply,s.prototype._multiplyBySmall=function(t){return f(t.value*this.value)?new s(t.value*this.value):N(Math.abs(t.value),v(Math.abs(this.value)),this.sign!==t.sign)},a.prototype._multiplyBySmall=function(t){return 0===t.value?p[0]:1===t.value?this:-1===t.value?this.negate():N(Math.abs(t.value),this.value,this.sign!==t.sign)},s.prototype.multiply=function(t){return K(t)._multiplyBySmall(this)},s.prototype.times=s.prototype.multiply,l.prototype.multiply=function(t){return new l(this.value*K(t).value)},l.prototype.times=l.prototype.multiply,a.prototype.square=function(){return new a(E(this.value),!1)},s.prototype.square=function(){var t=this.value*this.value;return f(t)?new s(t):new a(E(v(Math.abs(this.value))),!1)},l.prototype.square=function(t){return new l(this.value*this.value)},a.prototype.divmod=function(t){var e=B(this,t);return{quotient:e[0],remainder:e[1]}},l.prototype.divmod=s.prototype.divmod=a.prototype.divmod,a.prototype.divide=function(t){return B(this,t)[0]},l.prototype.over=l.prototype.divide=function(t){return new l(this.value/K(t).value)},s.prototype.over=s.prototype.divide=a.prototype.over=a.prototype.divide,a.prototype.mod=function(t){return B(this,t)[1]},l.prototype.mod=l.prototype.remainder=function(t){return new l(this.value%K(t).value)},s.prototype.remainder=s.prototype.mod=a.prototype.remainder=a.prototype.mod,a.prototype.pow=function(t){var e,r,o,n=K(t),i=this.value,u=n.value;if(0===u)return p[1];if(0===i)return p[0];if(1===i)return p[1];if(-1===i)return n.isEven()?p[1]:p[-1];if(n.sign)return p[0];if(!n.isSmall)throw new Error("The exponent "+n.toString()+" is too large.");if(this.isSmall&&f(e=Math.pow(i,u)))return new s(c(e));for(r=this,o=p[1];!0&u&&(o=o.times(r),--u),0!==u;)u/=2,r=r.square();return o},s.prototype.pow=a.prototype.pow,l.prototype.pow=function(t){var e=K(t),r=this.value,o=e.value,n=BigInt(0),i=BigInt(1),u=BigInt(2);if(o===n)return p[1];if(r===n)return p[0];if(r===i)return p[1];if(r===BigInt(-1))return e.isEven()?p[1]:p[-1];if(e.isNegative())return new l(n);for(var a=this,s=p[1];(o&i)===i&&(s=s.times(a),--o),o!==n;)o/=u,a=a.square();return s},a.prototype.modPow=function(t,e){if(t=K(t),(e=K(e)).isZero())throw new Error("Cannot take modPow with modulus 0");var r=p[1],o=this.mod(e);for(t.isNegative()&&(t=t.multiply(p[-1]),o=o.modInv(e));t.isPositive();){if(o.isZero())return p[0];t.isOdd()&&(r=r.multiply(o).mod(e)),t=t.divide(2),o=o.square().mod(e)}return r},l.prototype.modPow=s.prototype.modPow=a.prototype.modPow,a.prototype.compareAbs=function(t){var e=K(t),r=this.value,o=e.value;return e.isSmall?1:A(r,o)},s.prototype.compareAbs=function(t){var e=K(t),r=Math.abs(this.value),o=e.value;return e.isSmall?r===(o=Math.abs(o))?0:r>o?1:-1:-1},l.prototype.compareAbs=function(t){var e=this.value,r=K(t).value;return(e=e>=0?e:-e)===(r=r>=0?r:-r)?0:e>r?1:-1},a.prototype.compare=function(t){if(t===1/0)return-1;if(t===-1/0)return 1;var e=K(t),r=this.value,o=e.value;return this.sign!==e.sign?e.sign?1:-1:e.isSmall?this.sign?-1:1:A(r,o)*(this.sign?-1:1)},a.prototype.compareTo=a.prototype.compare,s.prototype.compare=function(t){if(t===1/0)return-1;if(t===-1/0)return 1;var e=K(t),r=this.value,o=e.value;return e.isSmall?r==o?0:r>o?1:-1:r<0!==e.sign?r<0?-1:1:r<0?1:-1},s.prototype.compareTo=s.prototype.compare,l.prototype.compare=function(t){if(t===1/0)return-1;if(t===-1/0)return 1;var e=this.value,r=K(t).value;return e===r?0:e>r?1:-1},l.prototype.compareTo=l.prototype.compare,a.prototype.equals=function(t){return 0===this.compare(t)},l.prototype.eq=l.prototype.equals=s.prototype.eq=s.prototype.equals=a.prototype.eq=a.prototype.equals,a.prototype.notEquals=function(t){return 0!==this.compare(t)},l.prototype.neq=l.prototype.notEquals=s.prototype.neq=s.prototype.notEquals=a.prototype.neq=a.prototype.notEquals,a.prototype.greater=function(t){return this.compare(t)>0},l.prototype.gt=l.prototype.greater=s.prototype.gt=s.prototype.greater=a.prototype.gt=a.prototype.greater,a.prototype.lesser=function(t){return this.compare(t)<0},l.prototype.lt=l.prototype.lesser=s.prototype.lt=s.prototype.lesser=a.prototype.lt=a.prototype.lesser,a.prototype.greaterOrEquals=function(t){return this.compare(t)>=0},l.prototype.geq=l.prototype.greaterOrEquals=s.prototype.geq=s.prototype.greaterOrEquals=a.prototype.geq=a.prototype.greaterOrEquals,a.prototype.lesserOrEquals=function(t){return this.compare(t)<=0},l.prototype.leq=l.prototype.lesserOrEquals=s.prototype.leq=s.prototype.lesserOrEquals=a.prototype.leq=a.prototype.lesserOrEquals,a.prototype.isEven=function(){return 0==(1&this.value[0])},s.prototype.isEven=function(){return 0==(1&this.value)},l.prototype.isEven=function(){return(this.value&BigInt(1))===BigInt(0)},a.prototype.isOdd=function(){return 1==(1&this.value[0])},s.prototype.isOdd=function(){return 1==(1&this.value)},l.prototype.isOdd=function(){return(this.value&BigInt(1))===BigInt(1)},a.prototype.isPositive=function(){return!this.sign},s.prototype.isPositive=function(){return this.value>0},l.prototype.isPositive=s.prototype.isPositive,a.prototype.isNegative=function(){return this.sign},s.prototype.isNegative=function(){return this.value<0},l.prototype.isNegative=s.prototype.isNegative,a.prototype.isUnit=function(){return!1},s.prototype.isUnit=function(){return 1===Math.abs(this.value)},l.prototype.isUnit=function(){return this.abs().value===BigInt(1)},a.prototype.isZero=function(){return!1},s.prototype.isZero=function(){return 0===this.value},l.prototype.isZero=function(){return this.value===BigInt(0)},a.prototype.isDivisibleBy=function(t){var e=K(t);return!e.isZero()&&(!!e.isUnit()||(0===e.compareAbs(2)?this.isEven():this.mod(e).isZero()))},l.prototype.isDivisibleBy=s.prototype.isDivisibleBy=a.prototype.isDivisibleBy,a.prototype.isPrime=function(t){var e=P(this);if(void 0!==e)return e;var r=this.abs(),o=r.bitLength();if(o<=64)return Z(r,[2,3,5,7,11,13,17,19,23,29,31,37]);for(var n=Math.log(2)*o.toJSNumber(),i=Math.ceil(!0===t?2*Math.pow(n,2):n),u=[],p=0;p-o?new s(t-1):new a(n,!0)},l.prototype.prev=function(){return new l(this.value-BigInt(1))};for(var x=[1];2*x[x.length-1]<=e;)x.push(2*x[x.length-1]);var J=x.length,L=x[J-1];function U(t){return Math.abs(t)<=e}function T(t,e,r){e=K(e);for(var o=t.isNegative(),n=e.isNegative(),i=o?t.not():t,u=n?e.not():e,p=0,a=0,s=null,l=null,f=[];!i.isZero()||!u.isZero();)p=(s=B(i,L))[1].toJSNumber(),o&&(p=L-1-p),a=(l=B(u,L))[1].toJSNumber(),n&&(a=L-1-a),i=s[0],u=l[0],f.push(r(p,a));for(var v=0!==r(o?1:0,n?1:0)?bigInt(-1):bigInt(0),h=f.length-1;h>=0;h-=1)v=v.multiply(L).add(bigInt(f[h]));return v}a.prototype.shiftLeft=function(t){var e=K(t).toJSNumber();if(!U(e))throw new Error(String(e)+" is too large for shifting.");if(e<0)return this.shiftRight(-e);var r=this;if(r.isZero())return r;for(;e>=J;)r=r.multiply(L),e-=J-1;return r.multiply(x[e])},l.prototype.shiftLeft=s.prototype.shiftLeft=a.prototype.shiftLeft,a.prototype.shiftRight=function(t){var e,r=K(t).toJSNumber();if(!U(r))throw new Error(String(r)+" is too large for shifting.");if(r<0)return this.shiftLeft(-r);for(var o=this;r>=J;){if(o.isZero()||o.isNegative()&&o.isUnit())return o;o=(e=B(o,L))[1].isNegative()?e[0].prev():e[0],r-=J-1}return(e=B(o,x[r]))[1].isNegative()?e[0].prev():e[0]},l.prototype.shiftRight=s.prototype.shiftRight=a.prototype.shiftRight,a.prototype.not=function(){return this.negate().prev()},l.prototype.not=s.prototype.not=a.prototype.not,a.prototype.and=function(t){return T(this,t,function(t,e){return t&e})},l.prototype.and=s.prototype.and=a.prototype.and,a.prototype.or=function(t){return T(this,t,function(t,e){return t|e})},l.prototype.or=s.prototype.or=a.prototype.or,a.prototype.xor=function(t){return T(this,t,function(t,e){return t^e})},l.prototype.xor=s.prototype.xor=a.prototype.xor;var j=1<<30,C=(e&-e)*(e&-e)|j;function D(t){var r=t.value,o="number"==typeof r?r|j:"bigint"==typeof r?r|BigInt(j):r[0]+r[1]*e|C;return o&-o}function z(t,e){return t=K(t),e=K(e),t.greater(e)?t:e}function R(t,e){return t=K(t),e=K(e),t.lesser(e)?t:e}function k(t,e){if(t=K(t).abs(),e=K(e).abs(),t.equals(e))return t;if(t.isZero())return e;if(e.isZero())return t;for(var r,o,n=p[1];t.isEven()&&e.isEven();)r=R(D(t),D(e)),t=t.divide(r),e=e.divide(r),n=n.multiply(r);for(;t.isEven();)t=t.divide(D(t));do{for(;e.isEven();)e=e.divide(D(e));t.greater(e)&&(o=e,e=t,t=o),e=e.subtract(t)}while(!e.isZero());return n.isUnit()?t:t.multiply(n)}a.prototype.bitLength=function(){var t=this;return t.compareTo(bigInt(0))<0&&(t=t.negate().subtract(bigInt(1))),0===t.compareTo(bigInt(0))?bigInt(0):bigInt(function t(e,r){if(r.compareTo(e)<=0){var o=t(e,r.square(r)),n=o.p,i=o.e,u=n.multiply(r);return u.compareTo(e)<=0?{p:u,e:2*i+1}:{p:n,e:2*i}}return{p:bigInt(1),e:0}}(t,bigInt(2)).e).add(bigInt(1))},l.prototype.bitLength=s.prototype.bitLength=a.prototype.bitLength;var _=function(t,e,r,o){r=r||i,t=String(t),o||(t=t.toLowerCase(),r=r.toLowerCase());var n,u=t.length,p=Math.abs(e),a={};for(n=0;n=p)){if("1"===f&&1===p)continue;throw new Error(f+" is not a valid digit in base "+e+".")}}e=K(e);var s=[],l="-"===t[0];for(n=l?1:0;n"!==t[n]&&n=0;o--)n=n.add(t[o].times(i)),i=i.times(e);return r?n.negate():n}function F(t,e){if((e=bigInt(e)).isZero()){if(t.isZero())return{value:[0],isNegative:!1};throw new Error("Cannot convert nonzero numbers to base 0.")}if(e.equals(-1)){if(t.isZero())return{value:[0],isNegative:!1};if(t.isNegative())return{value:[].concat.apply([],Array.apply(null,Array(-t.toJSNumber())).map(Array.prototype.valueOf,[1,0])),isNegative:!1};var r=Array.apply(null,Array(t.toJSNumber()-1)).map(Array.prototype.valueOf,[0,1]);return r.unshift([1]),{value:[].concat.apply([],r),isNegative:!1}}var o=!1;if(t.isNegative()&&e.isPositive()&&(o=!0,t=t.abs()),e.isUnit())return t.isZero()?{value:[0],isNegative:!1}:{value:Array.apply(null,Array(t.toJSNumber())).map(Number.prototype.valueOf,1),isNegative:o};for(var n,i=[],u=t;u.isNegative()||u.compareAbs(e)>=0;){n=u.divmod(e),u=n.quotient;var p=n.remainder;p.isNegative()&&(p=e.minus(p).abs(),u=u.next()),i.push(p.toJSNumber())}return i.push(u.toJSNumber()),{value:i.reverse(),isNegative:o}}function G(t,e,r){var o=F(t,e);return(o.isNegative?"-":"")+o.value.map(function(t){return function(t,e){return t<(e=e||i).length?e[t]:"<"+t+">"}(t,r)}).join("")}function H(t){if(f(+t)){var e=+t;if(e===c(e))return u?new l(BigInt(e)):new s(e);throw new Error("Invalid integer: "+t)}var o="-"===t[0];o&&(t=t.slice(1));var n=t.split(/e/i);if(n.length>2)throw new Error("Invalid integer: "+n.join("e"));if(2===n.length){var i=n[1];if("+"===i[0]&&(i=i.slice(1)),(i=+i)!==c(i)||!f(i))throw new Error("Invalid integer: "+i+" is not a valid exponent.");var p=n[0],v=p.indexOf(".");if(v>=0&&(i-=p.length-v-1,p=p.slice(0,v)+p.slice(v+1)),i<0)throw new Error("Cannot include negative exponent part for integers");t=p+=new Array(i+1).join("0")}if(!/^([0-9][0-9]*)$/.test(t))throw new Error("Invalid integer: "+t);if(u)return new l(BigInt(o?"-"+t:t));for(var h=[],g=t.length,m=r,d=g-m;g>0;)h.push(+t.slice(d,g)),(d-=m)<0&&(d=0),g-=m;return y(h),new a(h,o)}function K(t){return"number"==typeof t?function(t){if(u)return new l(BigInt(t));if(f(t)){if(t!==c(t))throw new Error(t+" is not an integer.");return new s(t)}return H(t.toString())}(t):"string"==typeof t?H(t):"bigint"==typeof t?new l(t):t}a.prototype.toArray=function(t){return F(this,t)},s.prototype.toArray=function(t){return F(this,t)},l.prototype.toArray=function(t){return F(this,t)},a.prototype.toString=function(t,e){if(void 0===t&&(t=10),10!==t)return G(this,t,e);for(var r,o=this.value,n=o.length,i=String(o[--n]);--n>=0;)r=String(o[n]),i+="0000000".slice(r.length)+r;return(this.sign?"-":"")+i},s.prototype.toString=function(t,e){return void 0===t&&(t=10),10!=t?G(this,t,e):String(this.value)},l.prototype.toString=s.prototype.toString,l.prototype.toJSON=a.prototype.toJSON=s.prototype.toJSON=function(){return this.toString()},a.prototype.valueOf=function(){return parseInt(this.toString(),10)},a.prototype.toJSNumber=a.prototype.valueOf,s.prototype.valueOf=function(){return this.value},s.prototype.toJSNumber=s.prototype.valueOf,l.prototype.valueOf=l.prototype.toJSNumber=function(){return parseInt(this.toString(),10)};for(var Q=0;Q<1e3;Q++)p[Q]=K(Q),Q>0&&(p[-Q]=K(-Q));return p.one=p[1],p.zero=p[0],p.minusOne=p[-1],p.max=z,p.min=R,p.gcd=k,p.lcm=function(t,e){return t=K(t).abs(),e=K(e).abs(),t.divide(k(t,e)).multiply(e)},p.isInstance=function(t){return t instanceof a||t instanceof s||t instanceof l},p.randBetween=function(t,r,o){t=K(t),r=K(r);var n=o||Math.random,i=R(t,r),u=z(t,r).subtract(i).add(1);if(u.isSmall)return i.add(Math.floor(n()*u));for(var a=F(u,e).value,s=[],l=!0,f=0;f= 0 && o < 256){ return { t: 97, v: o }; } + if(isInteger && o >= -2147483648 && o <= 2147483647){ return {t: 98, v: o}; } + return {t: 110, v: o}; +} + +// BigInt to BERT, with https://github.com/peterolson/BigInteger.js +function bignum(o){ + if(bigInt.isInstance(o) === false){ return {t: 999, v: [97, 0]}; } // o is not bigInt + if(o.greaterOrEquals(0) && o.lesser(256)){ + // t: 97 + return {t: 999, v: [97, o.toJSNumber() ]}; + } + if(o.greaterOrEquals(-2147483648) && o.lesserOrEquals(2147483647)){ + // t: 98 + return {t: 999, v: [98, o.shiftRight(24).toJSNumber(), o.shiftRight(16).and(255).toJSNumber(), o.shiftRight(8).and(255).toJSNumber(), o.and(255).toJSNumber() ]}; + } + // t: 110 + if(o.isNegative()){ + var sign = 1; + var s = bignum_to_bytes(o.abs()); + }else{ + var sign = 0; + var s = bignum_to_bytes(o); + } + return {t: 999, v: [110, s.length, sign].concat(s) }; +} + +function bin(o){ + return { t: 109, v: o instanceof ArrayBuffer ? new Uint8Array(o) : + o instanceof Uint8Array ? o : utf8_enc(o) }; +} + + +// bert encoder +function enc(o){ return fl([131, ein(o)]); } +function ein(o){ + return Array.isArray(o) ? en_108({ t: 108, v: o }) : + (o.t == 999 ? o.v : eval('en_' + o.t)(o) ); // t: 999 = bigInt, already encoded in bignum func +} +function en_undefined(o){ return [106]; } +function unilen(o){ + return (o.v instanceof ArrayBuffer || o.v instanceof Uint8Array) ? o.v.byteLength : + (new TextEncoder().encode(o.v)).byteLength; +} +function en_70(o){ + var x = Array(8).fill(0).flat(); + write_Float(x, o.v, 0, false, 52, 8); + return [70].concat(x); +} +function en_97(o){ return [97, o.v]; } +function en_98(o){ return [98, o.v >>> 24, (o.v >>> 16) & 255, (o.v >>> 8) & 255, o.v & 255]; } +function en_99(o){ + var obj = o.v.toExponential(20), + match = /([^e]+)(e[+-])(\d+)/.exec(obj), + exponentialPart = match[3].length == 1 ? "0" + match[3] : match[3], + num = Array.from(bin(match[1] + match[2] + exponentialPart).v); + return [o.t].concat(num).concat(Array(31 - num.length).fill(0).flat()); +} +function en_100(o){ return [100, o.v.length >>> 8, o.v.length & 255, ar(o)]; } +function en_104(o){ + var l = o.v.length, + r = []; + for(var i = 0; i < l; i++) r[i] = ein(o.v[i]); + return [104, l, r]; +} +function en_106(o){ return [106]; } +function en_107(o){ return [107, o.v.length >>> 8, o.v.length & 255, ar(o)]; } +function en_108(o){ + var l = o.v.length, + r = []; + for(var i = 0; i < l; i++) r.push(ein(o.v[i])); + return o.v.length == 0 ? [106] : + [108, l >>> 24, (l >>> 16) & 255, (l >>> 8) & 255, l & 255, r, 106]; +} +function en_109(o){ + var l = unilen(o); + return [109, l >>> 24, (l >>> 16) & 255, (l >>> 8) & 255, l & 255, ar(o)]; +} +function en_110(o){ + if(o.v < 0){ + var sign = 1; + var s = int_to_bytes(-o.v); + }else{ + var sign = 0; + var s = int_to_bytes(o.v); + } + return [110, s.length, sign].concat(s); +} +function en_115(o){ return [115, o.v.length, ar(o)]; } +function en_116(o){ + var l = o.v.length, + x = [], + r = []; + for(var i = 0; i < l; i++) r.push([ein(o.v[i].k), ein(o.v[i].v)]); + x = [116, l >>> 24, (l >>> 16) & 255, (l >>> 8) & 255, l & 255]; + return o.v.length == 0 ? x : [x, r]; +} +function en_118(o){ return [118, ar(o).length >>> 8, ar(o).length & 255, ar(o)]; } +function en_119(o){ return [119, ar(o).length, ar(o)]; } + + +// bert decoder +function nop(b){ return []; } +function big(b){ + var sk = b == 1 ? sx.getUint8(ix++) : sx.getInt32((a = ix, ix += 4, a)); + var ret = 0, + sig = sx.getUint8(ix++), + count = sk; + while(count-- > 0){ + ret = 256 * ret + sx.getUint8(ix + count); + } + ix += sk; + return ret * (sig == 0 ? 1 : -1); +} +function int(b){ + return b == 1 ? sx.getUint8(ix++) : sx.getInt32((a = ix, ix += 4, a)); +} +function dec(d){ + sx = new DataView(d); + ix = 0; + if(sx.getUint8(ix++) !== 131) throw ("BERT?"); + return din(); +} +function str(b){ + var dv, + sz = (b == 2 ? sx.getUint16(ix) : (b == 1 ? sx.getUint8(ix) : sx.getUint32(ix))); + ix += b; + var r = sx.buffer.slice(ix, ix += sz); + return utf8_arr(r); +} +function run(b){ + var sz = (b == 1 ? sx.getUint8(ix) : sx.getUint32(ix)), + r = []; + ix += b; + for(var i = 0; i < sz; i++) r.push(din()); + if(b == 4) ix++; + return r; +} +function rut(b){ + var sz = (b == 1 ? sx.getUint8(ix) : sx.getUint32(ix)), + r = []; + ix += b; + for(var i = 0; i < sz; i++) r.push(din()); + din(); + return r; +} +function dic(b){ + var sz = sx.getUint32(ix), + r = []; + ix += 4; + for(var i = 0; i < sz; i++) r.push({k: din(), v: din()}); + return r; +} +function iee(x){ + return read_Float(new Uint8Array(sx.buffer.slice(ix, ix += 8)), 0, false, 52, 8); +} + +function flo(x){ + return parseFloat(utf8_arr(sx.buffer.slice(ix, ix += 31))); +} + +function arr(b){ + var dv, + sz = sx.getUint16(ix); + ix += b; + return new Uint8Array(sx.buffer.slice(ix, ix += sz)); +} + +function ref(cr){ + var d, + adj = sx.getUint8(ix++); + adj += sx.getUint8(ix++); + d = din(); + ix += cr + adj * 4; + return d; +} + +function din(){ + var x, + c = sx.getUint8(ix++); + switch(c){ + case 70: x = [iee, 0]; break; + case 90: x = [ref, 4]; break; + case 97: x = [int, 1]; break; + case 98: x = [int, 4]; break; + case 99: x = [flo, 0]; break; + case 100: x = [str, 2]; break; + case 104: x = [run, 1]; break; + case 105: x = [run, 4]; break; + case 107: x = [arr, 2]; break; + case 108: x = [rut, 4]; break; + case 109: x = [str, 4]; break; + case 110: x = [big, 1]; break; + case 111: x = [big, 4]; break; + case 114: x = [ref, 1]; break; + case 115: x = [str, 1]; break; + case 116: x = [dic, 4]; break; + case 118: x = [str, 2]; break; + case 119: x = [str, 1]; break; + default: x = [nop, 0]; + } return { t: c, v: x[0](x[1]) }; +} + + +// bert helpers +function int_to_bytes(Int){ + if(Int % 1 !== 0) return [0]; + var OriginalInt, + Rem, + s = []; + OriginalInt = Int; + while(Int !== 0){ + Rem = Int % 256; + s.push(Rem); + Int = Math.floor(Int / 256); + } + if(Int > 0){ throw ("Argument out of range: " + OriginalInt); } + return s; +} + +function bignum_to_bytes(big_Int){ + var v, + big_Int, + s = []; + big_Int2 = big_Int; + while(big_Int2.isZero() === false){ + v = big_Int2.divmod(256); + s.push(v.remainder.toJSNumber()); + big_Int2 = v.quotient; + } + if(big_Int2.greater(0)){ throw ("Argument out of range::: " + big_Int.toString() ); } + return s; +} + +function uc(u1, u2){ + if(u1.byteLength == 0) return u2; + if(u2.byteLength == 0) return u1; + var a = new Uint8Array(u1.byteLength + u2.byteLength); + a.set(u1, 0); + a.set(u2, u1.byteLength); + return a; +} +function ar(o){ + return o.v instanceof ArrayBuffer ? new Uint8Array(o.v) : o.v instanceof Uint8Array ? o.v : + Array.isArray(o.v) ? new Uint8Array(o.v) : new Uint8Array(utf8_enc(o.v)); +} +function fl(a){ + return a.reduce(function(f, t){ + return uc(f, t instanceof Uint8Array ? t : + Array.isArray(t) ? fl(t) : new Uint8Array([t])); + }, new Uint8Array()); +} + + +// save and restore ftp files queue +function saveState(){ + var state = ftp.queue.filter(i => i.status !== 'done').map(i => ({ + id: i.id, + uid: i.uid, + name: i.name, + total: i.total, + offset: i.offset, + status: 'paused' + })); + localStorage.setItem('ftp_queue', JSON.stringify(state)); +} + +// todo add cleanState // localStorage.removeItem('ftp_queue'); +function restoreState(){ + var stored = localStorage.getItem('ftp_queue'); + if(stored){ + try{ + var items = JSON.parse(stored); + items.forEach(i => { + if(ftp.item(i.id)) return; // no dup when too laggy internet + + var item = { + id: i.id, + uid: i.uid || i.id, + name: i.name, + status: 'missing_file', // special + status_block_id: ftp.status_block_id || 'ftp-status', + ui_update: (typeof ui_update !== 'undefined') ? ui_update : null, + autostart: false, + offset: i.offset, + block: 32 * 1024, + total: i.total, + file: null // no file -- needs to select + }; + + ftp.queue.push(item); + + if(ui_add){ ui_add(item); } + if(item.ui_update){ item.ui_update(item, "Please re-select file for resume upload"); } + }); + + }catch(e){ console.error("Restore failed: ", e); } + } +} + +// WS FTP Logic +var ftp = { + queue: [], + active: false, + last_save_time: 0, // for save queue - item offset to localStorage + init: function(file, ui_add = false, ui_update = false){ + var id = Math.floor(Math.random() * 1000000).toString(); + var item = { + id: id, + uid: id, + status: 'init', + status_block_id: ftp.status_block_id || 'ftp-status', + ui_update: ui_update, + autostart: ftp.autostart || false, + name: ftp.filename || file.name, + offset: 0, + block: 32 * 1024, // chunk size 32KB + total: file.size, + file: file + }; + ftp.queue.push(item); + saveState(); + if(ui_add){ ui_add(item); } + if(!ftp.active) ftp.start(); + return item.id; + }, + + start: function(id){ + if(ftp.active && !id) return; + var item = id ? ftp.item(id) : ftp.next(); + ftp.active = (item) ? true : false; + if(item){ + if(item.status === 'init'){ + ftp.send(item, new Uint8Array(0)); + }else{ + ftp.read_slice(item); + } + } + }, + + stop: function(id){ // pause + var item = ftp.item(id); + if(item){ + item.autostart = false; + if(ftp.active && ftp.current_id === item.id) ftp.active = false; + if(item.ui_update){ item.ui_update(item, "Paused"); } + saveState(); + } + }, + + resume: function(id){ + var item = ftp.item(id); + if(item){ + + if(!item.file || item.status === 'missing_file'){ // after page reload + console.log("File object lost after reload. Please re-select the file using the input button."); // todo add ui + return; + } // otherwise we have files selected already + + item.autostart = true; + item.status = 'init'; // re-init to check offset on server + if(ftp.current_id === item.id) ftp.active = false; + if(item.ui_update){ item.ui_update(item, "Resuming..."); } + if(!ftp.active) ftp.start(); + } + }, + + send: function(item, data){ + ftp.current_id = item.id; + // Tuple: {ftp, Id, Sid, Name, Meta, Other1, Other2, Other3, Total, Offset, Block, Data, Status} + var msg = tuple(atom('ftp'), bin(item.id), bin(''), bin(item.name), bin(''), bin(''), bin(''), bin(''), + number(item.total), number(item.offset), number(data.byteLength), bin(data), bin(item.status)); + ws_send(enc(msg)); // ws.send(enc(msg)); + }, + + read_slice: function(item){ + if(!item.autostart) return; + var reader = new FileReader(); + reader.onloadend = function(e){ + if(e.target.readyState === FileReader.DONE){ + ftp.send(item, new Uint8Array(e.target.result)); + } + }; + reader.onerror = function(e){ + console.error("FileReader error:", e.target.error); + item.autostart = false; + if(item.ui_update){ item.ui_update(item, "Read error"); } + }; + var end = item.offset + item.block; + if(end > item.total) end = item.total; + reader.readAsArrayBuffer( item.file.slice(item.offset, end) ); + }, + + item: function(id){ return ftp.queue.find(i => i.id === id || i.uid === id); }, + + next: function(){ return ftp.queue.find(next => next && next.autostart && (next.status === 'init' || next.offset < next.total) ); } + //next: function(){ return ftp.queue.find(next => next.offset < next.total); } +}; + + +// main Socket logic +var ws; +var reconnectTimer = null; + +function connect(){ + if(ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return; + + var proto = window.location.protocol === 'https:' ? 'wss://' : 'ws://'; + ws = new WebSocket(proto + window.location.host + "/ws"); + ws.binaryType = "arraybuffer"; + + ws.onopen = function(){ + console.log("Connected"); + if(reconnectTimer) { clearInterval(reconnectTimer); reconnectTimer = null; } + + var activeItem = ftp.queue.find(i => i.autostart && i.file && i.status !== 'done'); // filter for not complete file uploads for resume + if(activeItem){ + console.log("Auto-resuming upload:", activeItem.name); + ftp.active = false; + activeItem.status = 'init'; + ftp.start(activeItem.id); + }else{ + if(ftp.active) ftp.start(); + } + }; + + ws.onclose = function(){ + console.log("Disconnected. Reconnecting..."); + ws = null; + if(!reconnectTimer) reconnectTimer = setInterval(connect, 3000); + }; + + ws.onerror = function(e){ + console.log("WS Error", e); + ws.close(); // trigger onclose logic + }; + + ws.onmessage = function(evt){ + if(evt.data instanceof ArrayBuffer){ + if(evt.data.byteLength === 0) return; // ignore empty + try{ + var msg = dec(evt.data); + //console.log("WS received:", evt.data, msg); + if(msg.t === 104 && msg.v[0].t === 118 && msg.v[0].v === "ftp"){ // check if tuple {ftp, ...} + var v = msg.v; // [atom, id, sid, name, meta, o1, o2, o3, total, offset, block, data, status] + var server_id = utf8_arr(v[1].v); // server returns file_id + var offset = v[9].v; + var status = utf8_arr(v[12].v); + + //console.log("WS FTP Reply -> ID:", server_id, "Status:", status, "Offset:", offset); + + //var item = ftp.item(id); + var item = ftp.item(server_id); // but if server just changed id (this is answer for init) + if(!item && ftp.active && ftp.current_id){ + //console.log("Mapping ID, Current: ", ftp.current_id, " Server: ", server_id); + item = ftp.item(ftp.current_id); + if(item){ + //console.log("Server assigned ID: ", server_id, " to local: ", item.id); + item.id = server_id; // but item.uid not changes :) + } + } + + if(!item){ console.error("Item not found for file_id: ", server_id); return;} + item.offset = offset; + + if(status === "send"){ + item.status = "send"; + item.offset = offset; // 0 for 1st + + var now = Date.now(); + if(!ftp.last_save_time || (now - ftp.last_save_time > 2000)){ + saveState(); + ftp.last_save_time = now; + } + + if(item.ui_update){ try{ item.ui_update(item); }catch(e){ console.error("UI Error ui_update:", e); } } + + if(item.offset < item.total){ + try{ ftp.read_slice(item); }catch(e){ console.error("Error in read_slice:", e); } + }else{ + item.status = 'done'; + saveState(); + + if(item.ui_update){ item.ui_update(item, "Done!", true); } // console.log("File complete!"); + //ftp.queue = ftp.queue.filter(i => i.id !== item.id); + item.autostart = true; + ftp.active = false; + ftp.start(); // next file + } + + }else if(status === "error"){ + console.error("Server returned ERROR status"); + if(item.ui_update){ item.ui_update(item, "Error: Rejected by server"); } + item.autostart = false; + } + } + }catch(e){ console.error(e); } + } + }; +} + +function ws_send(data){ // use ws_send(enc(msg)) instead ws.send(enc(msg)) for got status - was send or not + if(ws && ws.readyState === WebSocket.OPEN){ + ws.send(data); + return true; + }else{ + console.warn("WebSocket not open, cannot send"); // todo queue messages or retry later + return false; + } +} + + +// UTF-8 Support +function utf8_dec(ab){ return (new TextDecoder()).decode(ab); } +function utf8_enc(ab){ return (new TextEncoder("utf-8")).encode(ab); } +function utf8_arr(ab){ + if(!(ab instanceof ArrayBuffer)) ab = new Uint8Array(utf8_enc(ab)).buffer; + return utf8_dec(ab); +} + + +// IEEE754 (Floats) +function read_Float(buffer, offset, isLE, mLen, nBytes) { + var e, m + var eLen = (nBytes * 8) - mLen - 1 + var eMax = (1 << eLen) - 1 + var eBias = eMax >> 1 + var nBits = -7 + var i = isLE ? (nBytes - 1) : 0 + var d = isLE ? -1 : 1 + var s = buffer[offset + i] + i += d + e = s & ((1 << (-nBits)) - 1) + s >>= (-nBits) + nBits += eLen + for (; nBits > 0; e = (e * 256) + buffer[offset + i], i += d, nBits -= 8) {} + m = e & ((1 << (-nBits)) - 1) + e >>= (-nBits) + nBits += mLen + for (; nBits > 0; m = (m * 256) + buffer[offset + i], i += d, nBits -= 8) {} + if (e === 0) { + e = 1 - eBias + } else if (e === eMax) { + return m ? NaN : ((s ? -1 : 1) * Infinity) + } else { + m = m + Math.pow(2, mLen) + e = e - eBias + } + return (s ? -1 : 1) * m * Math.pow(2, e - mLen) +} + +function write_Float(buffer, value, offset, isLE, mLen, nBytes) { + var e, m, c + var eLen = (nBytes * 8) - mLen - 1 + var eMax = (1 << eLen) - 1 + var eBias = eMax >> 1 + var rt = (mLen === 23 ? Math.pow(2, -24) - Math.pow(2, -77) : 0) + var i = isLE ? 0 : (nBytes - 1) + var d = isLE ? 1 : -1 + var s = value < 0 || (value === 0 && 1 / value < 0) ? 1 : 0 + value = Math.abs(value) + if (isNaN(value) || value === Infinity) { + m = isNaN(value) ? 1 : 0 + e = eMax + } else { + e = Math.floor(Math.log(value) / Math.LN2) + if (value * (c = Math.pow(2, -e)) < 1) { + e-- + c *= 2 + } + if (e + eBias >= 1) { + value += rt / c + } else { + value += rt * Math.pow(2, 1 - eBias) + } + if (value * c >= 2) { + e++ + c /= 2 + } + if (e + eBias >= eMax) { + m = 0 + e = eMax + } else if (e + eBias >= 1) { + m = ((value * c) - 1) * Math.pow(2, mLen) + e = e + eBias + } else { + m = value * Math.pow(2, eBias - 1) * Math.pow(2, mLen) + e = 0 + } + } + for (; mLen >= 8; buffer[offset + i] = m & 0xff, i += d, m /= 256, mLen -= 8) {} + e = (e << mLen) | m + eLen += mLen + for (; eLen > 0; buffer[offset + i] = e & 0xff, i += d, e /= 256, eLen -= 8) {} + buffer[offset + i - d] |= s * 128 +} + diff --git a/examples_ws/static/form.js b/examples_ws/static/form.js new file mode 100644 index 0000000..370995d --- /dev/null +++ b/examples_ws/static/form.js @@ -0,0 +1,68 @@ + + +// ui helpers +function ui_add(item){ + var el = document.createElement("div"); + el.id = "ftp-" + item.uid; + el.innerHTML = `
+${item.name} +Waiting... + + + +
`; + document.getElementById(item.status_block_id).appendChild(el); +} + +function ui_update(item, txt, is_done){ + var pg = document.getElementById("ftp-pg-" + item.uid); + var st = document.getElementById("ftp-st-" + item.uid); + if(pg) pg.value = item.offset; + if(st){ + var pct = Math.round((item.offset / item.total) * 100); + st.innerHTML = txt || (pct + "% (" + item.offset + " / " + item.total + " bytes)"); + } + if(is_done){ + var ps = document.getElementById("ftp-ps-" + item.uid); + if(ps) ps.remove(); + var rs = document.getElementById("ftp-rs-" + item.uid); + if(rs) rs.remove(); + } +} + + +// work with form files +function selectFiles(input){ + ftp.autostart = true; + for(var i = 0; i < input.files.length; i++){ + //ftp.init(input.files[i], ui_add, ui_update); + var file = input.files[i]; + + var existing = ftp.queue.find(item => + item.status === 'missing_file' && + item.name === file.name && + item.total === file.size + ); + + if(existing){ // resume + console.log("Restoring file: ", file.name); + existing.file = file; + existing.status = 'init'; + existing.autostart = true; + + saveState(); + ftp.start(existing.id); + }else{ // new file + ftp.init(file, ui_add, ui_update); + } + } + input.value = ''; // clear +} + + +// lets connect ws +window.addEventListener("load", function(){ + connect(); + restoreState(); +}, false); + diff --git a/src/websocket/pubsub.zig b/src/websocket/pubsub.zig index ad4ca85..a3bf10f 100644 --- a/src/websocket/pubsub.zig +++ b/src/websocket/pubsub.zig @@ -1,5 +1,5 @@ -// Pub/Sub for WS +// Pub/Sub, WsSession for WS const std = @import("std"); const zzz = @import("../lib.zig"); // "zzz" // for zzz.Context @@ -10,27 +10,33 @@ const SecureSocket = zzz.secsock.SecureSocket; pub const UserWsHandler = struct { on_connect: ?*const fn (session: *WsSession) anyerror!void = null, on_message: ?*const fn (session: *WsSession, data: []const u8) anyerror!void = null, + on_binary: ?*const fn (session: *WsSession, data: []const u8) anyerror!void = null, on_close: ?*const fn (session: *WsSession) void = null, - on_disconnect: ?*const fn (session: *WsSession) void = null, + on_disconnect: ?*const fn (session: *WsSession) void = null, // optional }; +const QueuedMessage = struct { + data: []const u8, + is_binary: bool, +}; -pub const WsSession = struct { // for safe multithread use Pub/Sub +pub const WsSession = struct { // for safe multithread use Pub/Sub, for save user's state (context) conn: Conn, socket_owned: SecureSocket, // owned copy of socket structure (avoid use-after-free error) - outbox: std.ArrayList([]const u8), // messages queue that need to send to client with conn + outbox: std.ArrayList(QueuedMessage), // messages queue that need to send to client with conn mutex: std.Thread.Mutex, writer_task_id: usize = 0, // task id for send in native thread active: bool = true, // flag for to stop writer_task on disconnect allocator: std.mem.Allocator, writer_done: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), // for avoid race condition handler: UserWsHandler, // for access callbacks inside session + context: ?*anyopaque = null, // save user's state pub fn init(allocator: std.mem.Allocator, conn: Conn, handler: UserWsHandler) WsSession { return .{ .conn = conn, .socket_owned = conn.socket.*, - .outbox = std.ArrayList([]const u8).init(allocator), + .outbox = std.ArrayList(QueuedMessage).init(allocator), .mutex = .{}, .allocator = allocator, .handler = handler, @@ -40,13 +46,21 @@ pub const WsSession = struct { // for safe multithread use Pub/Sub pub fn deinit(self: *WsSession) void { self.mutex.lock(); defer self.mutex.unlock(); - for (self.outbox.items) |msg| { - self.allocator.free(msg); + for (self.outbox.items) |item| { + self.allocator.free(item.data); } self.outbox.deinit(); } - pub fn scheduleSend(self: *WsSession, data: []const u8) !void { // function for safe call from any thread + pub fn scheduleSend(self: *WsSession, data: []const u8) !void { // function for safe call from any thread // send text + return self.scheduleSendInternal(data, false); + } + + pub fn scheduleSendBinary(self: *WsSession, data: []const u8) !void { // send binary + return self.scheduleSendInternal(data, true); + } + + fn scheduleSendInternal(self: *WsSession, data: []const u8, is_binary: bool) !void { self.mutex.lock(); if (!self.active) { self.mutex.unlock(); @@ -54,7 +68,7 @@ pub const WsSession = struct { // for safe multithread use Pub/Sub } const msg_copy = try self.allocator.dupe(u8, data); // copy msg to heap - try self.outbox.append(msg_copy); + try self.outbox.append(.{ .data = msg_copy, .is_binary = is_binary }); self.mutex.unlock(); try self.conn.runtime.trigger(self.writer_task_id); // conn.runtime.trigger is thread-safe // this is task-writer in target thread @@ -182,13 +196,23 @@ fn writer_task(session: *WsSession) !void { // send message in same thread with session.mutex.unlock(); if (batch.len > 0) { - for (batch) |msg| { - session.conn.send(msg) catch |e| { - std.log.debug("WS Writer Error: {s}", .{ @errorName(e) }); - }; - session.allocator.free(msg); + for (batch) |item| { + if (item.is_binary) { + session.conn.sendBinary(item.data) catch |e| { + std.log.debug("WS Binary Write Error: {s}", .{ @errorName(e) }); + }; + + }else{ + session.conn.send(item.data) catch |e| { + std.log.debug("WS Text Write Error: {s}", .{ @errorName(e) }); + }; + } + + session.allocator.free(item.data); } session.allocator.free(batch); + + continue; } try session.conn.runtime.scheduler.trigger_await(); } @@ -230,6 +254,13 @@ pub fn handle_upgrade(ctx: *const zzz.Context, user_handler: UserWsHandler, stac } }.wrapper, + .on_binary = struct { + fn wrapper(c: Conn, d: []const u8) !void { + const s: *WsSession = @ptrCast(@alignCast(c.user_data)); + if (s.handler.on_binary) |f| try f(s, d); + } + }.wrapper, + .on_close = struct { fn wrapper(c: Conn, code: u16, reason: []const u8) !void { _ = code; diff --git a/src/websocket/websocket.zig b/src/websocket/websocket.zig index 6f926cd..2b559e6 100644 --- a/src/websocket/websocket.zig +++ b/src/websocket/websocket.zig @@ -116,181 +116,231 @@ pub fn upgrade( } pub fn runLoop(conn: Conn, handler: Handler, allocator: Allocator) !void { - const buffer = try allocator.alloc(u8, 65536); - defer allocator.free(buffer); + //const buffer = try allocator.alloc(u8, 65536); + //defer allocator.free(buffer); - var fragment = std.ArrayList(u8).init(allocator); + const read_buffer_size = 4096; // buffer for read from socket, 4 kb for one read syscall + const read_buffer = try allocator.alloc(u8, read_buffer_size); + defer allocator.free(read_buffer); + + var stash = std.ArrayList(u8).init(allocator); // buffer for incompleted frames between recv calls + defer stash.deinit(); + + var fragment = std.ArrayList(u8).init(allocator); // for fragmented ws msg - Opcode 0x0 defer fragment.deinit(); + var is_text: ?bool = null; + while (true) { - const n = conn.socket.recv(conn.runtime, buffer) catch |err| { + const n = conn.socket.recv(conn.runtime, read_buffer) catch |err| { if (err == error.Closed) { if (handler.on_disconnect) |on_disc| try on_disc(conn); return; } return err; }; - - var i: usize = 0; - while (i < n) { - const byte1 = buffer[i]; - i += 1; - if (i >= n) return error.InvalidFrame; - const byte2 = buffer[i]; - i += 1; - - const fin = (byte1 & 0x80) != 0; - const rsv1 = (byte1 & 0x40) != 0; - const opcode = byte1 & 0x0F; - const has_mask = (byte2 & 0x80) != 0; - var payload_len = @as(usize, byte2 & 0x7F); - - if (payload_len == 126) { - if (i + 2 > n) return error.InvalidFrame; - //payload_len = @as(usize, @bitCast(std.mem.readIntBig(u16, buffer[i..]))); - //payload_len = @as(usize, @bitCast(std.mem.readInt(u16, buffer[i..], .big))); - payload_len = @as(usize, @intCast(buffer[i])) << 8 | - @as(usize, @intCast(buffer[i + 1])); - i += 2; - } else if (payload_len == 127) { - if (i + 8 > n) return error.InvalidFrame; - //payload_len = @as(usize, @bitCast(std.mem.readIntBig(u64, buffer[i..]))); - //payload_len = @as(usize, @bitCast(std.mem.readInt(u64, buffer[i..], .big))); - payload_len = - @as(usize, @intCast(buffer[i + 0])) << 56 | - @as(usize, @intCast(buffer[i + 1])) << 48 | - @as(usize, @intCast(buffer[i + 2])) << 40 | - @as(usize, @intCast(buffer[i + 3])) << 32 | - @as(usize, @intCast(buffer[i + 4])) << 24 | - @as(usize, @intCast(buffer[i + 5])) << 16 | - @as(usize, @intCast(buffer[i + 6])) << 8 | - @as(usize, @intCast(buffer[i + 7])); - i += 8; - } - - var mask: [4]u8 = undefined; - if (has_mask) { - if (i + 4 > n) return error.InvalidFrame; - mask = buffer[i..][0..4].*; - i += 4; - } - if (i + payload_len > n) return error.InvalidFrame; - - const payload_start = i; - const payload_end = i + payload_len; - i = payload_end; - - if (has_mask) { - for (payload_start..payload_end) |j| { - buffer[j] ^= mask[(j - payload_start) % 4]; - } + + if (n == 0) { // if read 0 bytes - but no error.Closed- this can be disconnect too + if (handler.on_disconnect) |on_disc| try on_disc(conn); + return; + } + + try stash.appendSlice(read_buffer[0..n]); // save readed + + var process_offset: usize = 0; + const data = stash.items; + + + while (true) { // do process collected + if (process_offset + 2 > data.len) break; // must be at least 2 bytes - minimal header + + const start_idx = process_offset; + const byte1 = data[start_idx]; + const byte2 = data[start_idx + 1]; + + const fin = (byte1 & 0x80) != 0; + const rsv1 = (byte1 & 0x40) != 0; + const opcode = byte1 & 0x0F; + const has_mask = (byte2 & 0x80) != 0; + var payload_len = @as(usize, byte2 & 0x7F); + + var header_len: usize = 2; + + if (payload_len == 126) { // check header length + if (start_idx + 4 > data.len) break; // lets wait for more data + //payload_len = std.mem.readInt(u16, data[start_idx+2..][0..2], .big); + payload_len = @as(usize, data[start_idx+2]) << 8 | @as(usize, data[start_idx+3]); + header_len += 2; + } else if (payload_len == 127) { + if (start_idx + 10 > data.len) break; // lets wait for more data + //payload_len = std.mem.readInt(u64, data[start_idx+2..][0..8], .big); + payload_len = + @as(usize, data[start_idx+2]) << 56 | + @as(usize, data[start_idx+3]) << 48 | + @as(usize, data[start_idx+4]) << 40 | + @as(usize, data[start_idx+5]) << 32 | + @as(usize, data[start_idx+6]) << 24 | + @as(usize, data[start_idx+7]) << 16 | + @as(usize, data[start_idx+8]) << 8 | + @as(usize, data[start_idx+9]); + header_len += 8; + } + + var mask: [4]u8 = undefined; + if (has_mask) { + if (start_idx + header_len + 4 > data.len) break; // lets wait for mask + @memcpy(&mask, data[start_idx + header_len..][0..4]); + header_len += 4; + } + + const total_frame_len = header_len + payload_len; // check we have complete message' body + if (start_idx + total_frame_len > data.len) break; // lets wait for rest message' body + // here we got complete frame + + const payload_start = start_idx + header_len; + const payload = data[payload_start .. payload_start + payload_len]; + + if (has_mask) { // unmask + var j: usize = 0; + while (j < payload_len) : (j += 1) { + payload[j] ^= mask[j % 4]; } - - var payload = buffer[payload_start..payload_end]; + } + + // Compression (permessage-deflate) // todo + if (rsv1) return error.CompressionNotNegotiated; + //var decompressed: ?[]u8 = null; + //if (rsv1) { + // if (!conn.compression) return error.CompressionNotNegotiated; + // const inflated_len = payload.len + 4; + // const inflated = try allocator.alloc(u8, inflated_len); + // errdefer allocator.free(inflated); + // @memcpy(inflated[0..payload.len], payload); + // inflated[payload.len + 0] = 0x00; + // inflated[payload.len + 1] = 0x00; + // inflated[payload.len + 2] = 0xFF; + // inflated[payload.len + 3] = 0xFF; + // const stream = std.io.fixedBufferStream(inflated); + // var inflater = compress.flate.InflateStream.init(stream.reader(), .{}); + // defer inflater.deinit(); + // var out = std.ArrayList(u8).init(allocator); + // errdefer out.deinit(); + // try inflater.reader().readAllArrayList(&out, 1024 * 1024); + // allocator.free(inflated); + // decompressed = try out.toOwnedSlice(); + // payload = decompressed.?; + //} + + switch (opcode) { + 0x1 => { // Text + if (is_text != null) return error.InvalidWebSocketFrame; + is_text = true; + + if (fin) { + if (handler.on_message) |on_msg| try on_msg(conn, payload); + is_text = null; // because got complete message + //if (decompressed) |d| allocator.free(d); + } else { + try fragment.appendSlice(payload); + //if (decompressed) |d| allocator.free(d); + } + }, - // Compression (permessage-deflate) // todo fix - if (rsv1) return error.CompressionNotNegotiated; - //var decompressed: ?[]u8 = null; - //if (rsv1) { - // if (!conn.compression) return error.CompressionNotNegotiated; - // const inflated_len = payload.len + 4; - // const inflated = try allocator.alloc(u8, inflated_len); - // errdefer allocator.free(inflated); - // @memcpy(inflated[0..payload.len], payload); - // inflated[payload.len + 0] = 0x00; - // inflated[payload.len + 1] = 0x00; - // inflated[payload.len + 2] = 0xFF; - // inflated[payload.len + 3] = 0xFF; - // const stream = std.io.fixedBufferStream(inflated); - // var inflater = compress.flate.InflateStream.init(stream.reader(), .{}); - // defer inflater.deinit(); - // var out = std.ArrayList(u8).init(allocator); - // errdefer out.deinit(); - // try inflater.reader().readAllArrayList(&out, 1024 * 1024); - // allocator.free(inflated); - // decompressed = try out.toOwnedSlice(); - // payload = decompressed.?; - //} + 0x2 => { // Binary + if (is_text != null) return error.InvalidWebSocketFrame; + is_text = false; + + if (fin) { + if (handler.on_binary) |on_bin| try on_bin(conn, payload); + is_text = null; + //if (decompressed) |d| allocator.free(d); + } else { + try fragment.appendSlice(payload); + //if (decompressed) |d| allocator.free(d); + } + }, - var is_text: ?bool = null; - switch (opcode) { - 0x1 => { // Text - if (is_text == null) is_text = true; - - if (fin) { - if (handler.on_message) |on_msg| try on_msg(conn, payload); - //if (decompressed) |d| allocator.free(d); - } else { - try fragment.appendSlice(payload); - //if (decompressed) |d| allocator.free(d); - } - }, - - 0x2 => { // Binary - if (is_text == null) is_text = false; - - if (fin) { - if (handler.on_binary) |on_bin| try on_bin(conn, payload); - //if (decompressed) |d| allocator.free(d); - } else { - try fragment.appendSlice(payload); - //if (decompressed) |d| allocator.free(d); - } - }, + 0x0 => { // Continuation + if (is_text == null) return error.InvalidWebSocketFrame; // we got message' second part but we do not know first part - was no start frame + + try fragment.appendSlice(payload); + + if (fin) { + const full = fragment.items; + if (is_text.?) { // not null here + if (handler.on_message) |f| try f(conn, full); + } else { + if (handler.on_binary) |f| try f(conn, full); + } + is_text = null; + fragment.clearRetainingCapacity(); + } + //if (decompressed) |d| allocator.free(d); + }, + + 0x8 => { // Close + var code: u16 = 1000; // Normal closure code by default + var reason_slice: []const u8 = ""; + + if (payload.len >= 2) { + code = std.mem.readInt(u16, payload[0..2], .big); - 0x0 => { // Continuation - try fragment.appendSlice(payload); - if (fin) { - const full = fragment.items; - if (is_text orelse true) { - if (handler.on_message) |f| try f(conn, full); - } else { - if (handler.on_binary) |f| try f(conn, full); - } - is_text = null; - fragment.clearRetainingCapacity(); - } - //if (decompressed) |d| allocator.free(d); - }, - 0x8 => { // Close - var code: u16 = 1000; // Normal closure code by default - var reason_slice: []const u8 = ""; - if (payload.len >= 2) { - code = std.mem.readInt(u16, payload[0..2], .big); - - if (payload.len > 2) { - reason_slice = payload[2..]; - if (!std.unicode.utf8ValidateSlice(reason_slice)) { - //conn.socket.socket.close_blocking(); // that makes tardy - //if (decompressed) |d| allocator.free(d); - if (handler.on_disconnect) |on_disc| try on_disc(conn); - return; - } - } - } - conn.close(code, "") catch {}; // just exit - if (handler.on_close) |on_close| try on_close(conn, code, reason_slice); - if (handler.on_disconnect) |on_disc| try on_disc(conn); - //if (decompressed) |d| allocator.free(d); - return; - }, - 0x9 => { // Ping - var buf = std.ArrayList(u8).init(allocator); - defer buf.deinit(); - try writeFrameHeader(buf.writer(), .pong, payload.len, false); - try buf.appendSlice(payload); - _ = try conn.socket.send_all(conn.runtime, buf.items); - //if (decompressed) |d| allocator.free(d); - }, - 0xA => { // Pong - ignore - //if (decompressed) |d| allocator.free(d); - }, - else => { - //if (decompressed) |d| allocator.free(d); - return error.UnsupportedOpcode; - }, - } + if (payload.len > 2) reason_slice = payload[2..]; + } + // if (payload.len > 2) { + //reason_slice = payload[2..]; + //if (!std.unicode.utf8ValidateSlice(reason_slice)) { + ////conn.socket.socket.close_blocking(); // that makes tardy + ////if (decompressed) |d| allocator.free(d); + //if (handler.on_disconnect) |on_disc| try on_disc(conn); + //return; + //} + //} + + conn.close(code, "") catch {}; // just exit + + if (handler.on_close) |on_close| try on_close(conn, code, reason_slice); + if (handler.on_disconnect) |on_disc| try on_disc(conn); + //if (decompressed) |d| allocator.free(d); + return; + }, + + 0x9 => { // Ping + const pong_payload = payload; + + var pong_buf = std.ArrayList(u8).init(allocator); + defer pong_buf.deinit(); + + try writeFrameHeader(pong_buf.writer(), .pong, pong_payload.len, false); + try pong_buf.appendSlice(pong_payload); + + _ = try conn.socket.send_all(conn.runtime, pong_buf.items); + //if (decompressed) |d| allocator.free(d); + }, + + 0xA => { // Pong - ignore + //if (decompressed) |d| allocator.free(d); + }, + + else => { + //if (decompressed) |d| allocator.free(d); + return error.UnsupportedOpcode; + }, + } + + process_offset += total_frame_len; // shift offset + } // while ends - processed collected + + if (process_offset > 0) { // clean frame buffer // maybe todo RingBuffer + const remaining = data.len - process_offset; + if (remaining == 0) { + stash.clearRetainingCapacity(); + } else { + std.mem.copyForwards(u8, stash.items[0..remaining], stash.items[process_offset..]); // mv tail to begin + stash.shrinkRetainingCapacity(remaining); + } } + } } From 667bc20677c5cf25622da2b25cb07c9533f97beb Mon Sep 17 00:00:00 2001 From: 221V <221v92@gmail.com> Date: Sun, 28 Dec 2025 06:20:22 +0200 Subject: [PATCH 03/26] use dynamic heap buffer --- examples_ws/example_ws_1.zig | 4 +- examples_ws/example_ws_2.zig | 4 +- examples_ws/example_ws_3.zig | 2 +- examples_ws/example_ws_4.zig | 4 +- src/http/websocket.zig | 212 ++++++++++++++++++++--------------- 5 files changed, 127 insertions(+), 99 deletions(-) diff --git a/examples_ws/example_ws_1.zig b/examples_ws/example_ws_1.zig index d6c46dc..038f781 100644 --- a/examples_ws/example_ws_1.zig +++ b/examples_ws/example_ws_1.zig @@ -14,8 +14,8 @@ const Socket = zzz.tardy.Socket; const PORT = 3010; const HOST = "0.0.0.0"; -//const STACK_SIZE = 10 * 1024 * 1024; // DEBUG -const STACK_SIZE = 64 * 1024; // RELEASE +//const STACK_SIZE = 1 * 1024 * 1024; // DEBUG = 1mb +const STACK_SIZE = 16 * 1024; // RELEASE = 16kb // WebSocket handlers diff --git a/examples_ws/example_ws_2.zig b/examples_ws/example_ws_2.zig index f082b05..a9fe905 100644 --- a/examples_ws/example_ws_2.zig +++ b/examples_ws/example_ws_2.zig @@ -15,8 +15,8 @@ const Socket = zzz.tardy.Socket; const PORT = 443; const HOST = "0.0.0.0"; -//const STACK_SIZE = 10 * 1024 * 1024; // DEBUG -const STACK_SIZE = 64 * 1024; // RELEASE +//const STACK_SIZE = 1 * 1024 * 1024; // DEBUG = 1mb +const STACK_SIZE = 32 * 1024; // RELEASE = 32kb const FULLCHAIN_CERT = "examples_ws/cert/fullchain.pem"; const PRIVKEY_CERT = "examples_ws/cert/privkey.pem"; diff --git a/examples_ws/example_ws_3.zig b/examples_ws/example_ws_3.zig index 5437843..c683c9c 100644 --- a/examples_ws/example_ws_3.zig +++ b/examples_ws/example_ws_3.zig @@ -19,7 +19,7 @@ const PORT = 3010; //const PORT = 443; const HOST = "0.0.0.0"; -//const STACK_SIZE = 10 * 1024 * 1024; // DEBUG +//const STACK_SIZE = 1 * 1024 * 1024; // DEBUG = 1 mb const STACK_SIZE = 16 * 1024; // RELEASE // reader stack // 8-16 kb without ssl usage, 32 kb when use ssl const WS_WRITER_STACK = 8 * 1024; // stack size for writer task // 8 kb without ssl usage, 32 kb when use ssl diff --git a/examples_ws/example_ws_4.zig b/examples_ws/example_ws_4.zig index 72dc732..d619587 100644 --- a/examples_ws/example_ws_4.zig +++ b/examples_ws/example_ws_4.zig @@ -17,8 +17,8 @@ const Bert_Value = @import("bert.zig").Bert_Value; const PORT = 3010; const HOST = "0.0.0.0"; -//const WS_STACK_SIZE = 1024 * 1024; // DEBUG -const WS_STACK_SIZE = 32 * 1024; // RELEASE +//const WS_STACK_SIZE = 1 * 1024 * 1024; // DEBUG = 1mb +const WS_STACK_SIZE = 16 * 1024; // RELEASE = 16kb const ExtensionsLimits = struct { diff --git a/src/http/websocket.zig b/src/http/websocket.zig index 40210ae..ac04e64 100644 --- a/src/http/websocket.zig +++ b/src/http/websocket.zig @@ -81,107 +81,135 @@ pub fn upgrade_to_websocket( // inner loop fn message_loop(conn: Conn, handler: WebSocketHandler) !void { - var buffer: [65536]u8 = undefined; + const allocator = conn.runtime.allocator; + ////var buffer: [65536]u8 = undefined; //const buffer = try allocator.alloc(u8, 65536); //defer allocator.free(buffer); + + const read_buffer_size = 4096; // 4kb = RAM page, so read by 4kb + const read_buffer = try allocator.alloc(u8, read_buffer_size); + defer allocator.free(read_buffer); + + var stash = std.ArrayList(u8).init(allocator); + defer stash.deinit(); + + var fragment = std.ArrayList(u8).init(allocator); + defer fragment.deinit(); + while (true) { - const n = conn.socket.recv(conn.runtime, &buffer) catch |err| { + const n = conn.socket.recv(conn.runtime, read_buffer) catch |err| { if (err == error.Closed) { - if (handler.on_disconnect) |on_disconnect| { - _ = on_disconnect(conn); - } + if (handler.on_disconnect) |on_disconnect| { try on_disconnect(conn); } //_ = on_disconnect(conn); return; } return err; }; - + + if (n == 0) { + if (handler.on_disconnect) |on_disconnect| { try on_disconnect(conn); } + return; + } + + try stash.appendSlice(read_buffer[0..n]); + + var process_offset: usize = 0; + const data = stash.items; + // RFC 6455 - var i: usize = 0; - while (i < n) { - const op = buffer[i]; - const fin = (op & 0x80) != 0; - const opcode = op & 0x0F; - i += 1; - - if (i >= n) return error.InvalidWebSocketFrame; - - const mask_flag = (buffer[i] & 0x80) != 0; - const payload_len_raw = buffer[i] & 0x7F; - i += 1; - - if (i >= n) return error.InvalidWebSocketFrame; - - var payload_len: usize = payload_len_raw; - var extra: usize = 0; - if (payload_len_raw == 126) { - if (i + 2 > n) return error.InvalidWebSocketFrame; - payload_len = @as(usize, @bitCast(std.mem.readIntBig(u16, buffer[i..]))); - extra = 2; - } else if (payload_len_raw == 127) { - if (i + 8 > n) return error.InvalidWebSocketFrame; - payload_len = @as(usize, @bitCast(std.mem.readIntBig(u64, buffer[i..]))); - extra = 8; - } - - i += extra; - if (mask_flag) { - if (i + 4 > n) return error.InvalidWebSocketFrame; - const mask = buffer[i..][0..4].*; - i += 4; - if (i + payload_len > n) return error.InvalidWebSocketFrame; - - //for (0..payload_len) |j| { - // buffer[i + j] ^= mask[j % 4]; - //} // use next vector optimising instead this - - var j: usize = 0; - const vec_len = std.simd.suggestVectorLength(u8) orelse 16; - const Vector = @Vector(vec_len, u8); - - var mask_arr: [vec_len]u8 = undefined; - for (0..vec_len) |k| mask_arr[k] = mask[k % 4]; - const mask_vec: Vector = mask_arr; - - while (j + vec_len <= payload_len) { - const chunk: Vector = buffer[i+j..][0..vec_len].*; - const res = chunk ^ mask_vec; - buffer[i+j..][0..vec_len].* = res; - j += vec_len; - } - - while (j < payload_len) : (j += 1) { - buffer[i + j] ^= mask[j % 4]; - } - - + while (true) { + if (process_offset + 2 > data.len) break; // minimal header = 2 bytes + + const start_idx = process_offset; + const byte1 = data[start_idx]; + const byte2 = data[start_idx + 1]; + + const fin = (byte1 & 0x80) != 0; + const rsv1 = (byte1 & 0x40) != 0; + const opcode = byte1 & 0x0F; + const has_mask = (byte2 & 0x80) != 0; + var payload_len = @as(usize, byte2 & 0x7F); + + var header_len: usize = 2; + + if (payload_len == 126) { + if (start_idx + 4 > data.len) break; // waiting more bytes + payload_len = @as(usize, data[start_idx + 2]) << 8 | @as(usize, data[start_idx + 3]); + header_len += 2; + + } else if (payload_len == 127) { + if (start_idx + 10 > data.len) break; // waiting more bytes + + payload_len = + @as(usize, data[start_idx+2]) << 56 | + @as(usize, data[start_idx+3]) << 48 | + @as(usize, data[start_idx+4]) << 40 | + @as(usize, data[start_idx+5]) << 32 | + @as(usize, data[start_idx+6]) << 24 | + @as(usize, data[start_idx+7]) << 16 | + @as(usize, data[start_idx+8]) << 8 | + @as(usize, data[start_idx+9]); + header_len += 8; + } + + var mask: [4]u8 = undefined; + if (has_mask) { + if (start_idx + header_len + 4 > data.len) break; + @memcpy(&mask, data[start_idx + header_len..][0..4]); + header_len += 4; + } + + const total_frame_len = header_len + payload_len; + if (start_idx + total_frame_len > data.len) break; // is complete? + + const payload_start = start_idx + header_len; // must be complete + const payload = data[payload_start .. payload_start + payload_len]; + + if (has_mask) { + var j: usize = 0; + while (j < payload_len) : (j += 1) { + payload[j] ^= mask[j % 4]; } - - if (i + payload_len > n) return error.InvalidWebSocketFrame; - - switch (opcode) { - 0x1 => { // Text - if (fin and handler.on_message) |on_msg| { - try on_msg(conn, buffer[i .. i + payload_len]); - } - }, - 0x8 => { // Close - conn.close(); - if (handler.on_disconnect) |on_disconnect| { - _ = on_disconnect(conn); - } - return; - }, - 0x9 => { // Ping - reply Pong - const pong = ([2]u8{ 0x8A, @intCast(@min(payload_len, 125)) })[0..]; - _ = try conn.socket.send_all(conn.runtime, pong); - if (payload_len <= 125) { - _ = try conn.socket.send_all(conn.runtime, buffer[i .. i + payload_len]); - } - }, - else => { - // ignore other opcode (binary, pong etc) - }, + } + + switch (opcode) { + 0x1 => { // Text + if (fin and handler.on_message) |on_msg| { + try on_msg(conn, buffer[i .. i + payload_len]); + } + }, + 0x8 => { // Close + conn.close(); + if (handler.on_disconnect) |on_disconnect| { + _ = on_disconnect(conn); + } + return; + }, + 0x9 => { // Ping - reply Pong + const pong = ([2]u8{ 0x8A, @intCast(@min(payload_len, 125)) })[0..]; + _ = try conn.socket.send_all(conn.runtime, pong); + if (payload_len <= 125) { + _ = try conn.socket.send_all(conn.runtime, buffer[i .. i + payload_len]); + } + }, + else => { + // ignore other opcode (binary, pong etc) + }, + } + + process_offset += total_frame_len; + } + + if (process_offset > 0) { // clean + const remaining = data.len - process_offset; + if (remaining == 0) { // clean buffer + stash.clearRetainingCapacity(); + + if (stash.capacity > 1024 * 1024) { // free RAM when buffer size more than 1mb + stash.shrinkAndFree(0); } - - i += payload_len; + + } else { + std.mem.copyForwards(u8, stash.items[0..remaining], stash.items[process_offset..]); + stash.shrinkRetainingCapacity(remaining); + } } } } From a7e29050e6dce696f228e72dfb0425481ce38359 Mon Sep 17 00:00:00 2001 From: 221V <221v92@gmail.com> Date: Sun, 28 Dec 2025 06:46:27 +0200 Subject: [PATCH 04/26] define stack size with builtin mode --- examples_ws/example_ws_1.zig | 8 ++++++-- examples_ws/example_ws_2.zig | 9 +++++++-- examples_ws/example_ws_3.zig | 9 +++++++-- examples_ws/example_ws_4.zig | 7 +++++-- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/examples_ws/example_ws_1.zig b/examples_ws/example_ws_1.zig index 038f781..e102a8d 100644 --- a/examples_ws/example_ws_1.zig +++ b/examples_ws/example_ws_1.zig @@ -14,8 +14,11 @@ const Socket = zzz.tardy.Socket; const PORT = 3010; const HOST = "0.0.0.0"; -//const STACK_SIZE = 1 * 1024 * 1024; // DEBUG = 1mb -const STACK_SIZE = 16 * 1024; // RELEASE = 16kb + +const STACK_SIZE = if (@import("builtin").mode == .Debug) + 1 * 1024 * 1024 // DEBUG = 1mb +else + 16 * 1024; // RELEASE = 16kb // WebSocket handlers @@ -152,6 +155,7 @@ fn on_ws_endpoint(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond { pub fn main() !void{ + //@compileLog("STACK_SIZE = ", STACK_SIZE); var gpa = std.heap.GeneralPurposeAllocator(.{}){}; const allocator = gpa.allocator(); defer _ = gpa.deinit(); diff --git a/examples_ws/example_ws_2.zig b/examples_ws/example_ws_2.zig index a9fe905..f886169 100644 --- a/examples_ws/example_ws_2.zig +++ b/examples_ws/example_ws_2.zig @@ -15,8 +15,12 @@ const Socket = zzz.tardy.Socket; const PORT = 443; const HOST = "0.0.0.0"; -//const STACK_SIZE = 1 * 1024 * 1024; // DEBUG = 1mb -const STACK_SIZE = 32 * 1024; // RELEASE = 32kb + +const STACK_SIZE = if (@import("builtin").mode == .Debug) + 1 * 1024 * 1024 // DEBUG = 1mb +else + 32 * 1024; // RELEASE = 32kb + const FULLCHAIN_CERT = "examples_ws/cert/fullchain.pem"; const PRIVKEY_CERT = "examples_ws/cert/privkey.pem"; @@ -157,6 +161,7 @@ fn on_ws_endpoint(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond { pub fn main() !void{ + //@compileLog("STACK_SIZE = ", STACK_SIZE); var gpa = std.heap.GeneralPurposeAllocator(.{}){}; const allocator = gpa.allocator(); defer _ = gpa.deinit(); diff --git a/examples_ws/example_ws_3.zig b/examples_ws/example_ws_3.zig index c683c9c..984589c 100644 --- a/examples_ws/example_ws_3.zig +++ b/examples_ws/example_ws_3.zig @@ -19,10 +19,15 @@ const PORT = 3010; //const PORT = 443; const HOST = "0.0.0.0"; -//const STACK_SIZE = 1 * 1024 * 1024; // DEBUG = 1 mb -const STACK_SIZE = 16 * 1024; // RELEASE // reader stack // 8-16 kb without ssl usage, 32 kb when use ssl + +const STACK_SIZE = if (@import("builtin").mode == .Debug) + 1 * 1024 * 1024 // DEBUG = 1mb +else + 16 * 1024; // RELEASE = 16kb // reader stack // 8-16 kb without ssl usage, 32 kb when use ssl + const WS_WRITER_STACK = 8 * 1024; // stack size for writer task // 8 kb without ssl usage, 32 kb when use ssl + var global_pubsub: PubSub = undefined; diff --git a/examples_ws/example_ws_4.zig b/examples_ws/example_ws_4.zig index d619587..10a68d9 100644 --- a/examples_ws/example_ws_4.zig +++ b/examples_ws/example_ws_4.zig @@ -17,8 +17,11 @@ const Bert_Value = @import("bert.zig").Bert_Value; const PORT = 3010; const HOST = "0.0.0.0"; -//const WS_STACK_SIZE = 1 * 1024 * 1024; // DEBUG = 1mb -const WS_STACK_SIZE = 16 * 1024; // RELEASE = 16kb + +const WS_STACK_SIZE = if (@import("builtin").mode == .Debug) + 1 * 1024 * 1024 // DEBUG = 1mb +else + 16 * 1024; // RELEASE = 16kb const ExtensionsLimits = struct { From bb323c4279886ca65050d454cf7d075a12adc165 Mon Sep 17 00:00:00 2001 From: 221V <221v92@gmail.com> Date: Mon, 1 Jun 2026 19:01:14 +0300 Subject: [PATCH 05/26] optimize ws perf (maybe) --- src/websocket/websocket.zig | 109 ++++++++++++++++++++++++++++++------ 1 file changed, 93 insertions(+), 16 deletions(-) diff --git a/src/websocket/websocket.zig b/src/websocket/websocket.zig index 2b559e6..1b7fab7 100644 --- a/src/websocket/websocket.zig +++ b/src/websocket/websocket.zig @@ -18,24 +18,53 @@ pub const Conn = struct { //allocator: Allocator, // maybe todo - and pass with on_upgrade //compression: bool, - pub fn send(self: Conn, data: []const u8) !void { - var buf = std.ArrayList(u8).init(self.runtime.allocator); - defer buf.deinit(); + //pub fn send(self: Conn, data: []const u8) !void { + // var buf = std.ArrayList(u8).init(self.runtime.allocator); + // defer buf.deinit(); //try writeFrameHeader(buf.writer(), .text, data.len, true); - try writeFrameHeader(buf.writer(), .text, data.len, false); - try buf.appendSlice(data); - _ = try self.socket.send_all(self.runtime, buf.items); + // try writeFrameHeader(buf.writer(), .text, data.len, false); + // try buf.appendSlice(data); + // _ = try self.socket.send_all(self.runtime, buf.items); + //} + + //pub fn sendBinary(self: Conn, binary_data: []const u8) !void { + // var buf = std.ArrayList(u8).init(self.runtime.allocator); + // defer buf.deinit(); + + // try writeFrameHeader(buf.writer(), .binary, binary_data.len, false); + // try buf.appendSlice(binary_data); + // _ = try self.socket.send_all(self.runtime, buf.items); + //} + + pub fn send(self: Conn, data: []const u8) !void { + try self.writeFrame(.text, data); } pub fn sendBinary(self: Conn, binary_data: []const u8) !void { - var buf = std.ArrayList(u8).init(self.runtime.allocator); - defer buf.deinit(); - - try writeFrameHeader(buf.writer(), .binary, binary_data.len, false); - try buf.appendSlice(binary_data); - _ = try self.socket.send_all(self.runtime, buf.items); - + try self.writeFrame(.binary, binary_data); + } + + fn writeFrame(self: Conn, opcode: OpCode, data: []const u8) !void { + var header: [10]u8 = undefined; + const f_byte: u8 = @intFromEnum(opcode) | 0x80; + header[0] = f_byte; + + var h_len: usize = 2; + if (data.len < 126) { + header[1] = @intCast(data.len); + } else if (data.len <= 0xFFFF) { + header[1] = 126; + std.mem.writeInt(u16, header[2..4], @intCast(data.len), .big); + h_len = 4; + } else { + header[1] = 127; + std.mem.writeInt(u64, header[2..10], data.len, .big); + h_len = 10; + } + + _ = try self.socket.send_all(self.runtime, header[0..h_len]); + _ = try self.socket.send_all(self.runtime, data); } pub fn close(self: Conn, code: u16, reason: []const u8) !void { @@ -200,13 +229,45 @@ pub fn runLoop(conn: Conn, handler: Handler, allocator: Allocator) !void { const payload_start = start_idx + header_len; const payload = data[payload_start .. payload_start + payload_len]; + //if (has_mask) { // unmask + // var j: usize = 0; + // while (j < payload_len) : (j += 1) { + // payload[j] ^= mask[j % 4]; + // } + //} + if (has_mask) { // unmask var j: usize = 0; - while (j < payload_len) : (j += 1) { + const is_little = @import("builtin").target.cpu.arch.endian() == .little; + + const m64 = if (is_little) blk: { // 64-bits unmasking for little-or-big-endian - compiler knows in comptime + // mask[0] is least-significant byte u32 in little-endian + const m32 = @as(u32, mask[0]) | + (@as(u32, mask[1]) << 8) | + (@as(u32, mask[2]) << 16) | + (@as(u32, mask[3]) << 24); + break :blk (@as(u64, m32) << 32) | m32; + } else blk: { + // mask[0] is most-significant byte u32 in big-endian + const m32 = (@as(u32, mask[0]) << 24) | + (@as(u32, mask[1]) << 16) | + (@as(u32, mask[2]) << 8) | + @as(u32, mask[3]); + break :blk (@as(u64, m32) << 32) | m32; + }; + + while (j + 8 <= payload_len) : (j += 8) { // process every 8 bytes once + const val = std.mem.readInt(u64, payload[j..][0..8], if (is_little) .little else .big); + std.mem.writeInt(u64, payload[j..][0..8], val ^ m64, if (is_little) .little else .big); + } + + while (j < payload_len) : (j += 1) { // rest (< 8 bytes) payload[j] ^= mask[j % 4]; } + } + // Compression (permessage-deflate) // todo if (rsv1) return error.CompressionNotNegotiated; //var decompressed: ?[]u8 = null; @@ -331,15 +392,31 @@ pub fn runLoop(conn: Conn, handler: Handler, allocator: Allocator) !void { process_offset += total_frame_len; // shift offset } // while ends - processed collected + //if (process_offset > 0) { // clean frame buffer // maybe todo RingBuffer + // const remaining = data.len - process_offset; + // if (remaining == 0) { + // stash.clearRetainingCapacity(); + // } else { + // std.mem.copyForwards(u8, stash.items[0..remaining], stash.items[process_offset..]); // mv tail to begin + // stash.shrinkRetainingCapacity(remaining); + // } + //} + + if (process_offset > 0) { // clean frame buffer // maybe todo RingBuffer const remaining = data.len - process_offset; if (remaining == 0) { stash.clearRetainingCapacity(); - } else { - std.mem.copyForwards(u8, stash.items[0..remaining], stash.items[process_offset..]); // mv tail to begin + //} else if (process_offset > 8192 or remaining < process_offset) { // copy if 8kb(+) or remaing too less than offset + // std.mem.copyForwards(u8, stash.items[0..remaining], stash.items[process_offset..]); + // stash.shrinkRetainingCapacity(remaining); + } else { // maybe todo use more big buffer (at this function beginning) without reallocations + //std.mem.copyForwards(u8, stash.items[0..remaining], stash.items[process_offset..]); + @memcpy(stash.items[0..remaining], stash.items[process_offset..]); stash.shrinkRetainingCapacity(remaining); } } + } } From 3dbd7b392907939db1149ed85f4c252003ab229a Mon Sep 17 00:00:00 2001 From: 221V <221v92@gmail.com> Date: Mon, 1 Jun 2026 20:57:17 +0300 Subject: [PATCH 06/26] upd build.zig -- add target examples_http -- Build all HTTP examples (same as exists - build WS examples only) --- build.zig | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/build.zig b/build.zig index 21b77aa..b193b29 100644 --- a/build.zig +++ b/build.zig @@ -25,16 +25,17 @@ pub fn build(b: *std.Build) void { zzz.addImport("secsock", secsock); - add_example(b, "basic", false, target, optimize, zzz); - add_example(b, "cookies", false, target, optimize, zzz); - add_example(b, "form", false, target, optimize, zzz); - add_example(b, "fs", false, target, optimize, zzz); - add_example(b, "middleware", false, target, optimize, zzz); - add_example(b, "sse", false, target, optimize, zzz); - add_example(b, "tls", true, target, optimize, zzz); + const all_http_examples_step = b.step("examples_http", "Build all HTTP examples"); + add_http_example(b, all_http_examples_step, "basic", false, target, optimize, zzz); + add_http_example(b, all_http_examples_step, "cookies", false, target, optimize, zzz); + add_http_example(b, all_http_examples_step, "form", false, target, optimize, zzz); + add_http_example(b, all_http_examples_step, "fs", false, target, optimize, zzz); + add_http_example(b, all_http_examples_step, "middleware", false, target, optimize, zzz); + add_http_example(b, all_http_examples_step, "sse", false, target, optimize, zzz); + add_http_example(b, all_http_examples_step, "tls", true, target, optimize, zzz); if (target.result.os.tag != .windows) { - add_example(b, "unix", false, target, optimize, zzz); + add_http_example(b, all_http_examples_step, "unix", false, target, optimize, zzz); } @@ -110,8 +111,9 @@ pub fn build(b: *std.Build) void { test_step.dependOn(&run_test.step); } -fn add_example( +fn add_http_example( b: *std.Build, + all_http_examples_step: *std.Build.Step, name: []const u8, link_libc: bool, target: std.Build.ResolvedTarget, @@ -135,6 +137,8 @@ fn add_example( const install_artifact = b.addInstallArtifact(example, .{}); b.getInstallStep().dependOn(&install_artifact.step); + all_http_examples_step.dependOn(&install_artifact.step); + const build_step = b.step(b.fmt("{s}", .{name}), b.fmt("Build zzz example ({s})", .{name})); build_step.dependOn(&install_artifact.step); From e43666c5d6ffc9de9f3e17652b205727598e5523 Mon Sep 17 00:00:00 2001 From: 221V <221v92@gmail.com> Date: Tue, 2 Jun 2026 14:05:54 +0300 Subject: [PATCH 07/26] clean ws --- src/http/websocket.zig | 144 ++++------------------------------------- 1 file changed, 13 insertions(+), 131 deletions(-) diff --git a/src/http/websocket.zig b/src/http/websocket.zig index ac04e64..4acc49f 100644 --- a/src/http/websocket.zig +++ b/src/http/websocket.zig @@ -81,137 +81,19 @@ pub fn upgrade_to_websocket( // inner loop fn message_loop(conn: Conn, handler: WebSocketHandler) !void { - const allocator = conn.runtime.allocator; - ////var buffer: [65536]u8 = undefined; //const buffer = try allocator.alloc(u8, 65536); //defer allocator.free(buffer); - - const read_buffer_size = 4096; // 4kb = RAM page, so read by 4kb - const read_buffer = try allocator.alloc(u8, read_buffer_size); - defer allocator.free(read_buffer); - - var stash = std.ArrayList(u8).init(allocator); - defer stash.deinit(); - - var fragment = std.ArrayList(u8).init(allocator); - defer fragment.deinit(); - - while (true) { - const n = conn.socket.recv(conn.runtime, read_buffer) catch |err| { - if (err == error.Closed) { - if (handler.on_disconnect) |on_disconnect| { try on_disconnect(conn); } //_ = on_disconnect(conn); - return; - } - return err; - }; - - if (n == 0) { - if (handler.on_disconnect) |on_disconnect| { try on_disconnect(conn); } - return; - } - - try stash.appendSlice(read_buffer[0..n]); - - var process_offset: usize = 0; - const data = stash.items; - - // RFC 6455 - while (true) { - if (process_offset + 2 > data.len) break; // minimal header = 2 bytes - - const start_idx = process_offset; - const byte1 = data[start_idx]; - const byte2 = data[start_idx + 1]; - - const fin = (byte1 & 0x80) != 0; - const rsv1 = (byte1 & 0x40) != 0; - const opcode = byte1 & 0x0F; - const has_mask = (byte2 & 0x80) != 0; - var payload_len = @as(usize, byte2 & 0x7F); - - var header_len: usize = 2; - - if (payload_len == 126) { - if (start_idx + 4 > data.len) break; // waiting more bytes - payload_len = @as(usize, data[start_idx + 2]) << 8 | @as(usize, data[start_idx + 3]); - header_len += 2; - - } else if (payload_len == 127) { - if (start_idx + 10 > data.len) break; // waiting more bytes - - payload_len = - @as(usize, data[start_idx+2]) << 56 | - @as(usize, data[start_idx+3]) << 48 | - @as(usize, data[start_idx+4]) << 40 | - @as(usize, data[start_idx+5]) << 32 | - @as(usize, data[start_idx+6]) << 24 | - @as(usize, data[start_idx+7]) << 16 | - @as(usize, data[start_idx+8]) << 8 | - @as(usize, data[start_idx+9]); - header_len += 8; - } - - var mask: [4]u8 = undefined; - if (has_mask) { - if (start_idx + header_len + 4 > data.len) break; - @memcpy(&mask, data[start_idx + header_len..][0..4]); - header_len += 4; - } - - const total_frame_len = header_len + payload_len; - if (start_idx + total_frame_len > data.len) break; // is complete? - - const payload_start = start_idx + header_len; // must be complete - const payload = data[payload_start .. payload_start + payload_len]; - - if (has_mask) { - var j: usize = 0; - while (j < payload_len) : (j += 1) { - payload[j] ^= mask[j % 4]; - } - } - - switch (opcode) { - 0x1 => { // Text - if (fin and handler.on_message) |on_msg| { - try on_msg(conn, buffer[i .. i + payload_len]); - } - }, - 0x8 => { // Close - conn.close(); - if (handler.on_disconnect) |on_disconnect| { - _ = on_disconnect(conn); - } - return; - }, - 0x9 => { // Ping - reply Pong - const pong = ([2]u8{ 0x8A, @intCast(@min(payload_len, 125)) })[0..]; - _ = try conn.socket.send_all(conn.runtime, pong); - if (payload_len <= 125) { - _ = try conn.socket.send_all(conn.runtime, buffer[i .. i + payload_len]); - } - }, - else => { - // ignore other opcode (binary, pong etc) - }, - } - - process_offset += total_frame_len; - } - - if (process_offset > 0) { // clean - const remaining = data.len - process_offset; - if (remaining == 0) { // clean buffer - stash.clearRetainingCapacity(); - - if (stash.capacity > 1024 * 1024) { // free RAM when buffer size more than 1mb - stash.shrinkAndFree(0); - } - - } else { - std.mem.copyForwards(u8, stash.items[0..remaining], stash.items[process_offset..]); - stash.shrinkRetainingCapacity(remaining); - } - } - } + try websocket.runLoop( + .{ + .socket = conn.socket, + .runtime = conn.runtime, + .user_data = conn.user_data, + }, + .{ + .on_connect = handler.on_connect, + .on_message = handler.on_message, + .on_disconnect = handler.on_disconnect, + }, + conn.runtime.allocator, + ); } // Sec-WebSocket-Accept From 8ae97071a6446dc39158e1b7a9396af051a595a9 Mon Sep 17 00:00:00 2001 From: 221V <221v92@gmail.com> Date: Wed, 3 Jun 2026 00:44:43 +0300 Subject: [PATCH 08/26] do not parse headers again --- src/http/server.zig | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/http/server.zig b/src/http/server.zig index 9f56e54..81fceb8 100644 --- a/src/http/server.zig +++ b/src/http/server.zig @@ -349,26 +349,25 @@ pub const Server = struct { .handler => { - const request_text = provision.zc_recv_buffer.as_slice(); - var request = Request.init(rt.allocator); - request.parse_headers(request_text, .{ - .request_bytes_max = config.request_bytes_max, - .request_uri_bytes_max = config.request_uri_bytes_max, - }) catch |e| { - log.debug("malformed request| {}", .{e}); - break :http_loop; - }; + //const request_text = provision.zc_recv_buffer.as_slice(); + //var request = Request.init(rt.allocator); + //request.parse_headers(request_text, .{ + // .request_bytes_max = config.request_bytes_max, + // .request_uri_bytes_max = config.request_uri_bytes_max, + //}) catch |e| { + // log.debug("malformed request| {}", .{e}); + // break :http_loop; + //}; + var request = &provision.request; if (config.on_upgrade) |on_upgrade| { // check is this WebSocket upgrade request.socket = &secure; request.runtime = rt; - const upgrade_header = request.headers.get("Upgrade"); - if (upgrade_header) |upgrade| { - + if (request.headers.get("Upgrade")) |upgrade| { if (std.mem.eql(u8, upgrade, "websocket")) { - if (try on_upgrade(&request, upgrade)) { + if (try on_upgrade(request, upgrade)) { continue :http_loop; } } From 5252265fdd023ca58bdcb16c5b04fc29f5a5b555 Mon Sep 17 00:00:00 2001 From: 221V <221v92@gmail.com> Date: Wed, 3 Jun 2026 00:44:56 +0300 Subject: [PATCH 09/26] clean ws --- src/http/websocket.zig | 108 ----------------------------------------- 1 file changed, 108 deletions(-) delete mode 100644 src/http/websocket.zig diff --git a/src/http/websocket.zig b/src/http/websocket.zig deleted file mode 100644 index 4acc49f..0000000 --- a/src/http/websocket.zig +++ /dev/null @@ -1,108 +0,0 @@ - -const std = @import("std"); - -const zzz = @import("../lib.zig"); - -const websocket = zzz.websocket; -const SecureSocket = zzz.secsock.SecureSocket; -const Runtime = zzz.tardy.Runtime; - -const Context = @import("context.zig").Context; - - -pub const WebSocketHandler = struct { - on_connect: ?*const fn (Conn) anyerror!void = null, - on_message: ?*const fn (Conn, []const u8) anyerror!void = null, - on_disconnect: ?*const fn (Conn) anyerror!void = null, - user_data: ?*anyopaque = null, -}; - -pub const Conn = struct { - socket: *const SecureSocket, - runtime: *Runtime, - user_data: ?*anyopaque, - - pub fn send(self: Conn, data: []const u8) !void { - const frame = websocket.frameText(data); - _ = try self.socket.send_all(self.runtime, frame); - } - - pub fn close(self: Conn) void { - self.socket.close_blocking(); - } -}; - -pub fn upgrade_to_websocket( - ctx: *const Context, - handler: WebSocketHandler, -) !bool { - const req = ctx.request; - const res = ctx.response; - - // handshake - if (!std.mem.eql(u8, req.headers.get("Connection") orelse "", "Upgrade")) - return false; - if (!std.mem.eql(u8, req.headers.get("Upgrade") orelse "", "websocket")) - return false; - if (!std.mem.eql(u8, req.headers.get("Sec-WebSocket-Version") orelse "", "13")) - return false; - - const key = req.headers.get("Sec-WebSocket-Key") orelse return false; - - // Sec-WebSocket-Accept - const accept = try compute_accept(ctx.allocator, key); - - // 101 Switching Protocols - res.clear(); - try res.headers.put("Upgrade", "websocket"); - try res.headers.put("Connection", "Upgrade"); - try res.headers.put("Sec-WebSocket-Accept", accept); - res.status = .@"Switching Protocols"; - - try res.headers_into_writer(ctx.header_buffer.writer(), 0); - _ = try ctx.socket.send_all(ctx.runtime, ctx.header_buffer.items); - - // WebSocket Conn - const conn = Conn{ - .socket = &ctx.socket, - .runtime = ctx.runtime, - .user_data = handler.user_data, - }; - - // on_connect - if (handler.on_connect) |on_connect| { - try on_connect(conn); - } - - // process - try ctx.runtime.spawn(.{ conn, handler }, message_loop, 64 * 1024); - return true; -} - -// inner loop -fn message_loop(conn: Conn, handler: WebSocketHandler) !void { - try websocket.runLoop( - .{ - .socket = conn.socket, - .runtime = conn.runtime, - .user_data = conn.user_data, - }, - .{ - .on_connect = handler.on_connect, - .on_message = handler.on_message, - .on_disconnect = handler.on_disconnect, - }, - conn.runtime.allocator, - ); -} - -// Sec-WebSocket-Accept -fn compute_accept(allocator: std.mem.Allocator, key: []const u8) ![]const u8 { - const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - var hasher = std.crypto.hash.sha1.Sha1.init(); - hasher.update(key); - hasher.update(magic); - const hash = hasher.final(); - return try std.base64.encode(allocator, &hash); -} - From 70850958452cefa185c80f4d22815acb16563444 Mon Sep 17 00:00:00 2001 From: 221V <221v92@gmail.com> Date: Wed, 3 Jun 2026 00:52:42 +0300 Subject: [PATCH 10/26] ws - rm temp variable --- src/websocket/websocket.zig | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/websocket/websocket.zig b/src/websocket/websocket.zig index 1b7fab7..f64c436 100644 --- a/src/websocket/websocket.zig +++ b/src/websocket/websocket.zig @@ -47,8 +47,7 @@ pub const Conn = struct { fn writeFrame(self: Conn, opcode: OpCode, data: []const u8) !void { var header: [10]u8 = undefined; - const f_byte: u8 = @intFromEnum(opcode) | 0x80; - header[0] = f_byte; + header[0] = @intFromEnum(opcode) | 0x80; var h_len: usize = 2; if (data.len < 126) { From 685a82effd9bcfc635b13e36fb7cdb68408e2884 Mon Sep 17 00:00:00 2001 From: 221V <221v92@gmail.com> Date: Wed, 3 Jun 2026 01:15:19 +0300 Subject: [PATCH 11/26] rename dir examples -> examples_http ; edit build.zig -- executables to repo root --- build.zig | 5 +++-- {examples => examples_http}/basic/main.zig | 0 {examples => examples_http}/cookies/main.zig | 0 {examples => examples_http}/form/main.zig | 0 {examples => examples_http}/fs/main.zig | 0 {examples => examples_http}/fs/static/index.html | 0 {examples => examples_http}/middleware/main.zig | 0 {examples => examples_http}/sse/index.html | 0 {examples => examples_http}/sse/main.zig | 0 {examples => examples_http}/tls/certs/cert.pem | 0 {examples => examples_http}/tls/certs/key.pem | 0 {examples => examples_http}/tls/embed/pico.min.css | 0 {examples => examples_http}/tls/main.zig | 0 {examples => examples_http}/unix/main.zig | 0 14 files changed, 3 insertions(+), 2 deletions(-) rename {examples => examples_http}/basic/main.zig (100%) rename {examples => examples_http}/cookies/main.zig (100%) rename {examples => examples_http}/form/main.zig (100%) rename {examples => examples_http}/fs/main.zig (100%) rename {examples => examples_http}/fs/static/index.html (100%) rename {examples => examples_http}/middleware/main.zig (100%) rename {examples => examples_http}/sse/index.html (100%) rename {examples => examples_http}/sse/main.zig (100%) rename {examples => examples_http}/tls/certs/cert.pem (100%) rename {examples => examples_http}/tls/certs/key.pem (100%) rename {examples => examples_http}/tls/embed/pico.min.css (100%) rename {examples => examples_http}/tls/main.zig (100%) rename {examples => examples_http}/unix/main.zig (100%) diff --git a/build.zig b/build.zig index b193b29..fa033a5 100644 --- a/build.zig +++ b/build.zig @@ -122,7 +122,7 @@ fn add_http_example( ) void { const example = b.addExecutable(.{ .name = name, - .root_source_file = b.path(b.fmt("./examples/{s}/main.zig", .{name})), + .root_source_file = b.path(b.fmt("./examples_http/{s}/main.zig", .{name})), .target = target, .optimize = optimize, .strip = false, @@ -134,7 +134,8 @@ fn add_http_example( example.root_module.addImport("zzz", zzz_module); - const install_artifact = b.addInstallArtifact(example, .{}); + //const install_artifact = b.addInstallArtifact(example, .{}); + const install_artifact = b.addInstallBinFile(example.getEmittedBin(), b.fmt("../../{s}", .{name})); // to project root b.getInstallStep().dependOn(&install_artifact.step); all_http_examples_step.dependOn(&install_artifact.step); diff --git a/examples/basic/main.zig b/examples_http/basic/main.zig similarity index 100% rename from examples/basic/main.zig rename to examples_http/basic/main.zig diff --git a/examples/cookies/main.zig b/examples_http/cookies/main.zig similarity index 100% rename from examples/cookies/main.zig rename to examples_http/cookies/main.zig diff --git a/examples/form/main.zig b/examples_http/form/main.zig similarity index 100% rename from examples/form/main.zig rename to examples_http/form/main.zig diff --git a/examples/fs/main.zig b/examples_http/fs/main.zig similarity index 100% rename from examples/fs/main.zig rename to examples_http/fs/main.zig diff --git a/examples/fs/static/index.html b/examples_http/fs/static/index.html similarity index 100% rename from examples/fs/static/index.html rename to examples_http/fs/static/index.html diff --git a/examples/middleware/main.zig b/examples_http/middleware/main.zig similarity index 100% rename from examples/middleware/main.zig rename to examples_http/middleware/main.zig diff --git a/examples/sse/index.html b/examples_http/sse/index.html similarity index 100% rename from examples/sse/index.html rename to examples_http/sse/index.html diff --git a/examples/sse/main.zig b/examples_http/sse/main.zig similarity index 100% rename from examples/sse/main.zig rename to examples_http/sse/main.zig diff --git a/examples/tls/certs/cert.pem b/examples_http/tls/certs/cert.pem similarity index 100% rename from examples/tls/certs/cert.pem rename to examples_http/tls/certs/cert.pem diff --git a/examples/tls/certs/key.pem b/examples_http/tls/certs/key.pem similarity index 100% rename from examples/tls/certs/key.pem rename to examples_http/tls/certs/key.pem diff --git a/examples/tls/embed/pico.min.css b/examples_http/tls/embed/pico.min.css similarity index 100% rename from examples/tls/embed/pico.min.css rename to examples_http/tls/embed/pico.min.css diff --git a/examples/tls/main.zig b/examples_http/tls/main.zig similarity index 100% rename from examples/tls/main.zig rename to examples_http/tls/main.zig diff --git a/examples/unix/main.zig b/examples_http/unix/main.zig similarity index 100% rename from examples/unix/main.zig rename to examples_http/unix/main.zig From 73787151e2c1d40dc42787f4c065fbd15616257c Mon Sep 17 00:00:00 2001 From: 221V <221v92@gmail.com> Date: Fri, 5 Jun 2026 01:52:08 +0300 Subject: [PATCH 12/26] fix path - fs example_http --- examples_http/fs/main.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_http/fs/main.zig b/examples_http/fs/main.zig index d566ebb..d423f78 100644 --- a/examples_http/fs/main.zig +++ b/examples_http/fs/main.zig @@ -49,7 +49,7 @@ pub fn main() !void { var t = try Tardy.init(allocator, .{ .threading = .auto }); defer t.deinit(); - const static_dir = Dir.from_std(try std.fs.cwd().openDir("examples/fs/static", .{})); + const static_dir = Dir.from_std(try std.fs.cwd().openDir("examples_http/fs/static", .{})); var router = try Router.init(allocator, &.{ Compression(.{ .gzip = .{} }), From 33d5fa8c06867773ab607af29e6292ef8e0c9de9 Mon Sep 17 00:00:00 2001 From: 221V <221v92@gmail.com> Date: Fri, 5 Jun 2026 01:56:07 +0300 Subject: [PATCH 13/26] upd log path --- examples_http/tls/main.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_http/tls/main.zig b/examples_http/tls/main.zig index d8a07e4..14980d2 100644 --- a/examples_http/tls/main.zig +++ b/examples_http/tls/main.zig @@ -1,5 +1,5 @@ const std = @import("std"); -const log = std.log.scoped(.@"examples/tls"); +const log = std.log.scoped(.@"examples_http/tls"); const zzz = @import("zzz"); const http = zzz.HTTP; From 7d75eca597f0b0006a4fab593122a2810f0f39ac Mon Sep 17 00:00:00 2001 From: 221V <221v92@gmail.com> Date: Fri, 5 Jun 2026 02:36:39 +0300 Subject: [PATCH 14/26] fix json error -- sse http example -- just return POST response --- examples_http/sse/index.html | 5 +++-- examples_http/sse/main.zig | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/examples_http/sse/index.html b/examples_http/sse/index.html index 218bccc..f5b4500 100644 --- a/examples_http/sse/index.html +++ b/examples_http/sse/index.html @@ -52,12 +52,13 @@

Server-Sent Events Example

method: 'POST', }) .then(response => response.json()) // Adjust based on your server response + //.then(response => { console.log(response); return response.json() }) // Adjust based on your server response .then(data => { - console.log('Message sent:', data); // Handle successful response + console.log('Message sent: ', data); // Handle successful response this.reset(); // Clear the form }) .catch(error => { - console.error('Error sending message:', error); // Handle any errors + console.error('Error sending message: ', error); // Handle any errors }); }); diff --git a/examples_http/sse/main.zig b/examples_http/sse/main.zig index 715dfab..1d5d5f2 100644 --- a/examples_http/sse/main.zig +++ b/examples_http/sse/main.zig @@ -28,6 +28,21 @@ fn sse_handler(ctx: *const Context, _: void) !Respond { return .responded; } +fn post_handler(ctx: *const Context, _: void) !Respond { + const body = std.json.stringifyAlloc(ctx.allocator, .{ + .id = 1, + .message = "hello from post handler", + }, .{}) catch unreachable; + defer ctx.allocator.free(body); + + return ctx.response.apply(.{ + .status = .OK, + //.mime = http.Mime.TEXT, + .mime = http.Mime.JSON, + .body = body, + }); +} + pub fn main() !void { const host: []const u8 = "0.0.0.0"; const port: u16 = 9862; @@ -42,6 +57,7 @@ pub fn main() !void { const router = try Router.init(allocator, &.{ Route.init("/").embed_file(.{ .mime = http.Mime.HTML }, @embedFile("./index.html")).layer(), Route.init("/stream").get({}, sse_handler).layer(), + Route.init("/message").post({}, post_handler).layer(), }, .{}); // create socket for tardy From f19eefb7f23060f36cf50f9f825768629ff20e40 Mon Sep 17 00:00:00 2001 From: 221V <221v92@gmail.com> Date: Fri, 5 Jun 2026 02:51:01 +0300 Subject: [PATCH 15/26] fix cyrillic correct display form fields -- form example_http --- examples_http/form/main.zig | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/examples_http/form/main.zig b/examples_http/form/main.zig index 2cb122f..df400cd 100644 --- a/examples_http/form/main.zig +++ b/examples_http/form/main.zig @@ -19,6 +19,10 @@ const Respond = http.Respond; fn base_handler(ctx: *const Context, _: void) !Respond { const body = + \\ + \\ + \\ + \\ \\
\\ \\

@@ -30,7 +34,9 @@ fn base_handler(ctx: *const Context, _: void) !Respond { \\

\\ \\ - \\
+ \\ + \\ + \\ ; return ctx.response.apply(.{ @@ -72,6 +78,9 @@ fn generate_handler(ctx: *const Context, _: void) !Respond { return ctx.response.apply(.{ .status = .OK, .mime = http.Mime.TEXT, + .headers = &.{ + .{ "Content-Type", "text/plain; charset=utf-8" }, + }, .body = body, }); } From 17d35df2faefdd0159e12ac97e554fb2717b3697 Mon Sep 17 00:00:00 2001 From: 221V <221v92@gmail.com> Date: Fri, 5 Jun 2026 04:05:26 +0300 Subject: [PATCH 16/26] make zzz less strict on submit empty form values ; fix form example_http --- examples_http/form/main.zig | 52 +++++++++++++++++++++++--------- src/http/form.zig | 2 +- src/http/router/routing_trie.zig | 2 +- 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/examples_http/form/main.zig b/examples_http/form/main.zig index df400cd..e083bb1 100644 --- a/examples_http/form/main.zig +++ b/examples_http/form/main.zig @@ -47,31 +47,55 @@ fn base_handler(ctx: *const Context, _: void) !Respond { } const UserInfo = struct { - fname: []const u8, - mname: []const u8 = "Middle", - lname: []const u8, - age: u8, - height: f32, - weight: ?[]const u8, + fname: ?[]const u8 = null, + mname: ?[]const u8 = "Middle", + lname: ?[]const u8 = null, + age: ?[]const u8 = null, + height: ?[]const u8 = null, + weight: ?[]const u8 = null, }; fn generate_handler(ctx: *const Context, _: void) !Respond { + //std.debug.print("Handler entered, method: {any}\n", .{ctx.request.method}); const info = switch (ctx.request.method.?) { - .GET => try Query(UserInfo).parse(ctx.allocator, ctx), - .POST => try Form(UserInfo).parse(ctx.allocator, ctx), + .GET => Query(UserInfo).parse(ctx.allocator, ctx) catch |err| { + std.debug.print("Query parse failed: {}\n", .{err}); + return ctx.response.apply(.{ + .status = .@"Bad Request", + .mime = http.Mime.TEXT, + .headers = &.{ .{ "Content-Type", "text/plain; charset=utf-8" } }, + .body = "Invalid or empty query parameters", + }); + }, + .POST => Form(UserInfo).parse(ctx.allocator, ctx) catch |err| { + std.debug.print("Form parse failed: {}\n", .{err}); + return ctx.response.apply(.{ + .status = .@"Bad Request", + .mime = http.Mime.TEXT, + .headers = &.{ .{ "Content-Type", "text/plain; charset=utf-8" } }, + .body = "Invalid or empty form data", + }); + }, else => return error.UnexpectedMethod, }; + const fname = info.fname orelse ""; + const mname = info.mname orelse "Middle"; + const lname = info.lname orelse ""; + const age = if (info.age) |v| std.fmt.parseInt(u8, v, 10) catch 0 else 0; + const height = if (info.height) |v| std.fmt.parseFloat(f32, v) catch 0.0 else 0.0; + const weight = if (info.weight) |w| if (w.len == 0) "none" else w else "none"; + const body = try std.fmt.allocPrint( ctx.allocator, "First: {s} | Middle: {s} | Last: {s} | Age: {d} | Height: {d} | Weight: {s}", .{ - info.fname, - info.mname, - info.lname, - info.age, - info.height, - info.weight orelse "none", + if (fname.len == 0) "(empty)" else fname, + if (mname.len == 0) "Middle" else mname, + if (lname.len == 0) "(empty)" else lname, + age, + height, + weight, }, ); diff --git a/src/http/form.zig b/src/http/form.zig index 6cd887e..a762cf5 100644 --- a/src/http/form.zig +++ b/src/http/form.zig @@ -70,7 +70,7 @@ fn construct_map_from_body(allocator: std.mem.Allocator, m: *AnyCaseStringMap, b while (pairs.next()) |pair| { const field_idx = std.mem.indexOfScalar(u8, pair, '=') orelse return error.MissingSeperator; - if (pair.len < field_idx + 2) return error.MissingValue; + //if (pair.len < field_idx + 2) return error.MissingValue; // this makes problem as cannot get emply values when submit POST request with empty form values const key = pair[0..field_idx]; const value = pair[(field_idx + 1)..]; diff --git a/src/http/router/routing_trie.zig b/src/http/router/routing_trie.zig index c59ec33..dac92bc 100644 --- a/src/http/router/routing_trie.zig +++ b/src/http/router/routing_trie.zig @@ -274,7 +274,7 @@ pub const RoutingTrie = struct { while (query_iter.next()) |chunk| { const field_idx = std.mem.indexOfScalar(u8, chunk, '=') orelse return error.MissingValue; - if (chunk.len < field_idx + 2) return error.MissingValue; + //if (chunk.len < field_idx + 2) return error.MissingValue; // this makes problem as connection_refused when submit GET request with empty form values const key = chunk[0..field_idx]; const value = chunk[(field_idx + 1)..]; From ab9f3c713eef83ae7902a37723239cbd7b5eea5d Mon Sep 17 00:00:00 2001 From: 221V <221v92@gmail.com> Date: Mon, 15 Jun 2026 05:46:36 +0300 Subject: [PATCH 17/26] upd gitignore --- .gitignore | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.gitignore b/.gitignore index e304926..2ae1e6e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,15 @@ zig-out/ .zig-cache/ perf*.data* heaptrack* +/basic +/cookies +/form +/fs +/middleware +/rest +/sse +/tls +/unix /ex_ws_1 /ex_ws_2 /ex_ws_3 From e16e949c2bf0c1d89b362b8c5879d0387f68350e Mon Sep 17 00:00:00 2001 From: 221V <221v92@gmail.com> Date: Mon, 15 Jun 2026 05:47:13 +0300 Subject: [PATCH 18/26] add rest http example --- build.zig | 1 + examples_http/rest/main.zig | 140 ++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 examples_http/rest/main.zig diff --git a/build.zig b/build.zig index fa033a5..5bbec0c 100644 --- a/build.zig +++ b/build.zig @@ -33,6 +33,7 @@ pub fn build(b: *std.Build) void { add_http_example(b, all_http_examples_step, "middleware", false, target, optimize, zzz); add_http_example(b, all_http_examples_step, "sse", false, target, optimize, zzz); add_http_example(b, all_http_examples_step, "tls", true, target, optimize, zzz); + add_http_example(b, all_http_examples_step, "rest", false, target, optimize, zzz); if (target.result.os.tag != .windows) { add_http_example(b, all_http_examples_step, "unix", false, target, optimize, zzz); diff --git a/examples_http/rest/main.zig b/examples_http/rest/main.zig new file mode 100644 index 0000000..a45219b --- /dev/null +++ b/examples_http/rest/main.zig @@ -0,0 +1,140 @@ + +// http rest example - Content Negotiation - return different data types depending on client request + +const std = @import("std"); + +const zzz = @import("zzz"); + +const http = zzz.HTTP; +const Socket = zzz.tardy.Socket; + + +const PORT = 9862; // 8080; +const HOST = "0.0.0.0"; + + +const STACK_SIZE = if (@import("builtin").mode == .Debug) + 1 * 1024 * 1024 // DEBUG = 1mb +else + 32 * 1024; // RELEASE = 32kb + + +const User = struct { + id: u32, + name: []const u8, + email: []const u8, +}; + + +fn on_request(ctx: *const http.Context, _: void) !http.Respond { + if(ctx.request.method.? == .GET){ + return ctx.response.apply(.{ + .status = .OK, + .mime = http.Mime.HTML, + .body = + \\ + \\ + \\ + \\ + \\ + \\ + \\

zzz REST Content Negotiation

+ \\ + \\ + \\
+ \\ + \\ + \\ + }); + } // else POST request + + const user = User{ .id = 1, .name = "Alice", .email = "alice@example.com" }; + const accept = ctx.request.headers.get("accept") orelse "*/*"; + + + if(std.mem.indexOf(u8, accept, "application/json") != null){ // if expect json + const body = try std.json.stringifyAlloc(ctx.allocator, user, .{}); + return ctx.response.apply(.{ + .status = .OK, + .mime = http.Mime.JSON, + .body = body, + }); + } + + + if(std.mem.indexOf(u8, accept, "text/html") != null){ // if expect html + const body = try std.fmt.allocPrint(ctx.allocator, + "

User: {s} has ID: {d}

", + .{ user.name, user.id }); + + return ctx.response.apply(.{ + .status = .OK, + .mime = http.Mime.HTML, + .body = body, + }); + } + + + return ctx.response.apply(.{ // else plain text + .status = .OK, + .mime = http.Mime.TEXT, + .body = try std.fmt.allocPrint(ctx.allocator, "User: {s} (ID: {d})", .{ user.name, user.id }), + }); +} + + + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + defer _ = gpa.deinit(); + + const socket = try Socket.init(.{ .tcp = .{ .host = HOST, .port = PORT } }); + defer socket.close_blocking(); + try socket.bind(); + try socket.listen(1024); // max conn count that are waiting in accept queue + + var tardy = try zzz.tardy.Tardy(.auto).init(allocator, .{}); + defer tardy.deinit(); + + try tardy.entry(&socket, struct { + fn entry(rt: *zzz.tardy.Runtime, s: *const Socket) !void { + const config = zzz.ServerConfig{ .stack_size = STACK_SIZE }; + const router = try rt.allocator.create(zzz.Router); + router.* = try zzz.Router.init(rt.allocator, &.{ + http.Route.init("/").get({}, on_request).post({}, on_request).layer(), + }, .{ .not_found = on_request }); + + const provisions = try rt.allocator.create(zzz.tardy.Pool(zzz.Provision)); // use heap instead of stack + provisions.* = try zzz.tardy.Pool(zzz.Provision).init(rt.allocator, 1024, .static); // 1024 = pool size + @memset(std.mem.sliceAsBytes(provisions.items), 0); // set zeros -- initialized = false + + const conn_count = try rt.allocator.create(usize); + conn_count.* = 0; + const accept_q = try rt.allocator.create(bool); + accept_q.* = false; + + try rt.spawn(.{ rt, config, router, zzz.secsock.SecureSocket.unsecured(s.*), provisions, conn_count, accept_q }, zzz.Server.main_frame, config.stack_size); + } // end fn entry + }.entry); +} + From f0967a4c5ac2094074d7bc08fbf9ca96da8033dc Mon Sep 17 00:00:00 2001 From: 221V <221v92@gmail.com> Date: Wed, 17 Jun 2026 00:10:57 +0300 Subject: [PATCH 19/26] refactoring --- examples_http/rest/main.zig | 40 ++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/examples_http/rest/main.zig b/examples_http/rest/main.zig index a45219b..73f5e3c 100644 --- a/examples_http/rest/main.zig +++ b/examples_http/rest/main.zig @@ -116,25 +116,25 @@ pub fn main() !void { var tardy = try zzz.tardy.Tardy(.auto).init(allocator, .{}); defer tardy.deinit(); - try tardy.entry(&socket, struct { - fn entry(rt: *zzz.tardy.Runtime, s: *const Socket) !void { - const config = zzz.ServerConfig{ .stack_size = STACK_SIZE }; - const router = try rt.allocator.create(zzz.Router); - router.* = try zzz.Router.init(rt.allocator, &.{ - http.Route.init("/").get({}, on_request).post({}, on_request).layer(), - }, .{ .not_found = on_request }); - - const provisions = try rt.allocator.create(zzz.tardy.Pool(zzz.Provision)); // use heap instead of stack - provisions.* = try zzz.tardy.Pool(zzz.Provision).init(rt.allocator, 1024, .static); // 1024 = pool size - @memset(std.mem.sliceAsBytes(provisions.items), 0); // set zeros -- initialized = false - - const conn_count = try rt.allocator.create(usize); - conn_count.* = 0; - const accept_q = try rt.allocator.create(bool); - accept_q.* = false; - - try rt.spawn(.{ rt, config, router, zzz.secsock.SecureSocket.unsecured(s.*), provisions, conn_count, accept_q }, zzz.Server.main_frame, config.stack_size); - } // end fn entry - }.entry); + const config = zzz.ServerConfig{ .stack_size = STACK_SIZE }; + + const router = try zzz.Router.init(allocator, &.{ + http.Route.init("/").get({}, on_request).post({}, on_request).layer(), + }, .{ .not_found = on_request }); + + const Entry_Params = struct { + config: zzz.ServerConfig, + router: *const http.Router, + socket: Socket, + }; + + try tardy.entry( + Entry_Params{ .config = config, .router = &router, .socket = socket }, + struct { + fn entry(rt: *zzz.tardy.Runtime, p: Entry_Params) !void { + var server = zzz.Server.init(p.config); + try server.serve(rt, p.router, .{ .normal = p.socket }); + } // end fn entry + }.entry); } From 9cc78817fb9c072e807961be53d48dd43e376154 Mon Sep 17 00:00:00 2001 From: 221V <221v92@gmail.com> Date: Wed, 17 Jun 2026 00:41:47 +0300 Subject: [PATCH 20/26] refactoring 2 --- examples_http/rest/main.zig | 4 +-- examples_ws/example_ws_1.zig | 63 +++++++++++++----------------------- 2 files changed, 24 insertions(+), 43 deletions(-) diff --git a/examples_http/rest/main.zig b/examples_http/rest/main.zig index 73f5e3c..3d9196f 100644 --- a/examples_http/rest/main.zig +++ b/examples_http/rest/main.zig @@ -116,8 +116,6 @@ pub fn main() !void { var tardy = try zzz.tardy.Tardy(.auto).init(allocator, .{}); defer tardy.deinit(); - const config = zzz.ServerConfig{ .stack_size = STACK_SIZE }; - const router = try zzz.Router.init(allocator, &.{ http.Route.init("/").get({}, on_request).post({}, on_request).layer(), }, .{ .not_found = on_request }); @@ -129,7 +127,7 @@ pub fn main() !void { }; try tardy.entry( - Entry_Params{ .config = config, .router = &router, .socket = socket }, + Entry_Params{ .config = .{ .stack_size = STACK_SIZE }, .router = &router, .socket = socket }, struct { fn entry(rt: *zzz.tardy.Runtime, p: Entry_Params) !void { var server = zzz.Server.init(p.config); diff --git a/examples_ws/example_ws_1.zig b/examples_ws/example_ws_1.zig index e102a8d..e7e50d6 100644 --- a/examples_ws/example_ws_1.zig +++ b/examples_ws/example_ws_1.zig @@ -165,48 +165,31 @@ pub fn main() !void{ try socket.bind(); try socket.listen(1024); // max conn count that are waiting in accept queue - const TardyType = zzz.tardy.Tardy(.auto); - var tardy = try TardyType.init(allocator, .{}); - //var tardy = try TardyType.init(allocator, .{ .threading = .single }); + var tardy = try zzz.tardy.Tardy(.auto).init(allocator, .{}); defer tardy.deinit(); - try tardy.entry(&socket, struct { - fn entry(rt: *zzz.tardy.Runtime, s: *const Socket) !void { - const config = zzz.ServerConfig{ - .stack_size = STACK_SIZE, - }; - - const home_route = zzz.HTTP.Route.init("/").get({}, on_request); - const ws_route = zzz.HTTP.Route.init("/ws").get({}, on_ws_endpoint); - const layers = &[_]zzz.HTTP.Layer{ - home_route.layer(), - ws_route.layer(), - }; - - const router = try rt.allocator.create(zzz.Router); - router.* = try zzz.Router.init(rt.allocator, layers, .{ - .not_found = on_request, - }); - // no defer router - lifetime per server work - - const provisions = try rt.allocator.create(zzz.tardy.Pool(zzz.Provision)); // use heap, not stack - provisions.* = try zzz.tardy.Pool(zzz.Provision).init(rt.allocator, 1024, .static); // 1024 = pool size - - const byte_count = provisions.items.len * @sizeOf(zzz.Provision); // set zeros - for initialized = false - @memset(@as([*]u8, @ptrCast(provisions.items.ptr))[0..byte_count], 0); - - const connection_count = try rt.allocator.create(usize); // use heap, not stack - connection_count.* = 0; - - const accept_queued = try rt.allocator.create(bool); - accept_queued.* = false; - - try rt.spawn( - .{ rt, config, router, zzz.secsock.SecureSocket.unsecured(s.*), provisions, connection_count, accept_queued }, - zzz.Server.main_frame, - config.stack_size - ); - + const home_route = zzz.HTTP.Route.init("/").get({}, on_request); + const ws_route = zzz.HTTP.Route.init("/ws").get({}, on_ws_endpoint); + const layers = &[_]zzz.HTTP.Layer{ + home_route.layer(), + ws_route.layer(), + }; + + const router = try zzz.Router.init(allocator, layers, .{ .not_found = on_request }); + // no defer router - lifetime per server work // defer router.deinit(allocator); + + const Entry_Params = struct { + config: zzz.ServerConfig, + router: *const zzz.Router, + socket: Socket, + }; + + try tardy.entry( + Entry_Params{ .config = .{ .stack_size = STACK_SIZE }, .router = &router, .socket = socket }, + struct { + fn entry(rt: *zzz.tardy.Runtime, p: Entry_Params) !void { + var server = zzz.Server.init(p.config); + try server.serve(rt, p.router, .{ .normal = p.socket }); } // end fn entry }.entry); } From 5f26fb4c2a3b81b846b4b04e9628ab387945ffbe Mon Sep 17 00:00:00 2001 From: 221V <221v92@gmail.com> Date: Wed, 17 Jun 2026 03:51:19 +0300 Subject: [PATCH 21/26] refactoring, trying to fix memleak --- examples_ws/example_ws_1.zig | 28 +++++++---- examples_ws/example_ws_2.zig | 83 ++++++++++++-------------------- src/http/router/routing_trie.zig | 3 +- src/http/server.zig | 23 +++++++-- src/websocket/pubsub.zig | 19 +++++--- src/websocket/websocket.zig | 16 +++--- 6 files changed, 91 insertions(+), 81 deletions(-) diff --git a/examples_ws/example_ws_1.zig b/examples_ws/example_ws_1.zig index e7e50d6..0aa9c57 100644 --- a/examples_ws/example_ws_1.zig +++ b/examples_ws/example_ws_1.zig @@ -82,11 +82,13 @@ fn on_upgrade(req: *const zzz.Request, proto: []const u8) !bool { const key = req.headers.get("Sec-WebSocket-Key") orelse return false; const ext = req.headers.get("Sec-WebSocket-Extensions"); - var header_buf = std.ArrayList(u8).init(std.heap.page_allocator); + //var header_buf = std.ArrayList(u8).init(std.heap.page_allocator); + var header_buf = std.ArrayList(u8).init(req.runtime.allocator); defer header_buf.deinit(); //const res = try websocket.upgrade(req.socket, req.runtime, std.heap.page_allocator, key, ext, header_buf.writer() ); - const res = try websocket.upgrade(req.socket, req.runtime, req.runtime.allocator, key, ext, header_buf.writer() ); + //const res = try websocket.upgrade(req.socket, req.runtime, req.runtime.allocator, key, ext, header_buf.writer() ); + const res = try websocket.upgrade(req.socket, req.runtime, key, ext, header_buf.writer() ); _ = try req.socket.send_all(req.runtime, header_buf.items); @@ -98,8 +100,8 @@ fn on_upgrade(req: *const zzz.Request, proto: []const u8) !bool { }; if (ws_handler.on_connect) |f| try f(res.conn); - try req.runtime.spawn(.{ res.conn, ws_handler, std.heap.page_allocator }, websocket.runLoop, STACK_SIZE); - //try req.runtime.spawn(.{ res.conn, ws_handler, req.runtime.allocator }, websocket.runLoop, STACK_SIZE); + //try req.runtime.spawn(.{ res.conn, ws_handler, std.heap.page_allocator }, websocket.runLoop, STACK_SIZE); + try req.runtime.spawn(.{ res.conn, ws_handler, req.runtime.allocator }, websocket.runLoop, STACK_SIZE); return true; } @@ -123,7 +125,8 @@ fn on_ws_endpoint(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond { var header_buf = std.ArrayList(u8).init(ctx.allocator); defer header_buf.deinit(); - const res = try websocket.upgrade(&ctx.socket, ctx.runtime, ctx.allocator, key, ext, header_buf.writer()); + //const res = try websocket.upgrade(&ctx.socket, ctx.runtime, ctx.allocator, key, ext, header_buf.writer()); + const res = try websocket.upgrade(&ctx.socket, ctx.runtime, key, ext, header_buf.writer()); _ = try ctx.socket.send_all(ctx.runtime, header_buf.items); @@ -138,8 +141,8 @@ fn on_ws_endpoint(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond { std.log.info("Starting WebSocket Loop...", .{}); - //websocket.runLoop(res.conn, ws_handler, ctx.runtime.allocator) catch |err| { // sync loop - websocket.runLoop(res.conn, ws_handler, std.heap.page_allocator) catch |err| { // sync loop + websocket.runLoop(res.conn, ws_handler, ctx.runtime.allocator) catch |err| { // sync loop + //websocket.runLoop(res.conn, ws_handler, std.heap.page_allocator) catch |err| { // sync loop std.log.err("WebSocket RunLoop Error: {s}", .{@errorName(err)}); if (err == error.Closed) { @@ -156,9 +159,16 @@ fn on_ws_endpoint(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond { pub fn main() !void{ //@compileLog("STACK_SIZE = ", STACK_SIZE); - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa = std.heap.GeneralPurposeAllocator(.{ + .thread_safe = true, + .stack_trace_frames = 8, + }){}; const allocator = gpa.allocator(); - defer _ = gpa.deinit(); + //defer _ = gpa.deinit(); + defer { + const status = gpa.deinit(); + if (status == .leak){ std.debug.print("MEMORY LEAK DETECTED!", .{}); }else{ std.debug.print("no mem leak.\n", .{}); } + } const socket = try Socket.init(.{ .tcp = .{ .host = HOST, .port = PORT } }); defer socket.close_blocking(); diff --git a/examples_ws/example_ws_2.zig b/examples_ws/example_ws_2.zig index f886169..1c45d95 100644 --- a/examples_ws/example_ws_2.zig +++ b/examples_ws/example_ws_2.zig @@ -130,7 +130,8 @@ fn on_ws_endpoint(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond { var header_buf = std.ArrayList(u8).init(ctx.allocator); defer header_buf.deinit(); - const res = try websocket.upgrade(&ctx.socket, ctx.runtime, ctx.allocator, key, ext, header_buf.writer()); + //const res = try websocket.upgrade(&ctx.socket, ctx.runtime, ctx.allocator, key, ext, header_buf.writer()); + const res = try websocket.upgrade(&ctx.socket, ctx.runtime, key, ext, header_buf.writer()); _ = try ctx.socket.send_all(ctx.runtime, header_buf.items); @@ -152,7 +153,6 @@ fn on_ws_endpoint(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond { if (err == error.Closed) { std.log.info("Socket closed by browser", .{}); } - }; std.log.info("WebSocket Loop finished", .{}); @@ -171,67 +171,44 @@ pub fn main() !void{ try socket.bind(); try socket.listen(1024); // max conn count that are waiting in accept queue + const home_route = zzz.HTTP.Route.init("/").get({}, on_request); + const ws_route = zzz.HTTP.Route.init("/ws").get({}, on_ws_endpoint); + const layers = &[_]zzz.HTTP.Layer{ + home_route.layer(), + ws_route.layer(), + }; + + const router = try zzz.Router.init(allocator, layers, .{ .not_found = on_request }); + // no defer router - lifetime per server work + + var bearssl = zzz.secsock.BearSSL.init(allocator); + defer bearssl.deinit(); + const cert = try std.fs.cwd().readFileAlloc(allocator, FULLCHAIN_CERT, 1024 * 10); defer allocator.free(cert); const key = try std.fs.cwd().readFileAlloc(allocator, PRIVKEY_CERT, 1024 * 10); defer allocator.free(key); - const ctx = ServerContext{ - .socket = socket, - .cert_pem = cert, - .key_pem = key, - }; + try bearssl.add_cert_chain("CERTIFICATE", cert, "PRIVATE KEY", key); + const secure_socket = try bearssl.to_secure_socket(socket, .server); + defer secure_socket.deinit(); const TardyType = zzz.tardy.Tardy(.auto); var tardy = try TardyType.init(allocator, .{}); - //var tardy = try TardyType.init(allocator, .{ .threading = .single }); defer tardy.deinit(); - try tardy.entry(&ctx, struct { - fn entry(rt: *zzz.tardy.Runtime, s_ctx: *const ServerContext) !void { - const bearssl = try rt.allocator.create(zzz.secsock.BearSSL); - bearssl.* = zzz.secsock.BearSSL.init(rt.allocator); - //defer bearssl.deinit(); - - try bearssl.add_cert_chain("CERTIFICATE", s_ctx.cert_pem, "PRIVATE KEY", s_ctx.key_pem); - const secure_socket = try bearssl.to_secure_socket(s_ctx.socket, .server); - //defer secure_socket.deinit(); - - const config = zzz.ServerConfig{ - .stack_size = STACK_SIZE, - }; - - const home_route = zzz.HTTP.Route.init("/").get({}, on_request); - const ws_route = zzz.HTTP.Route.init("/ws").get({}, on_ws_endpoint); - const layers = &[_]zzz.HTTP.Layer{ - home_route.layer(), - ws_route.layer(), - }; - - const router = try rt.allocator.create(zzz.Router); - router.* = try zzz.Router.init(rt.allocator, layers, .{ - .not_found = on_request, - }); - // no defer router - lifetime per server work - - const provisions = try rt.allocator.create(zzz.tardy.Pool(zzz.Provision)); // use heap instead of stack - provisions.* = try zzz.tardy.Pool(zzz.Provision).init(rt.allocator, 1024, .static); // 1024 = pool size - - const byte_count = provisions.items.len * @sizeOf(zzz.Provision); // set zeros -- initialized = false - @memset(@as([*]u8, @ptrCast(provisions.items.ptr))[0..byte_count], 0); - - const connection_count = try rt.allocator.create(usize); // use heap instead stack - connection_count.* = 0; - - const accept_queued = try rt.allocator.create(bool); - accept_queued.* = false; - - try rt.spawn( - .{ rt, config, router, secure_socket, provisions, connection_count, accept_queued }, - zzz.Server.main_frame, - config.stack_size - ); - + const Entry_Params = struct { + config: zzz.ServerConfig, + router: *const zzz.Router, + socket: zzz.secsock.SecureSocket, + }; + + try tardy.entry( + Entry_Params{ .config = .{ .stack_size = STACK_SIZE }, .router = &router, .socket = secure_socket }, + struct { + fn entry(rt: *zzz.tardy.Runtime, p: Entry_Params) !void { + var server = zzz.Server.init(p.config); + try server.serve(rt, p.router, .{ .secure = p.socket }); } // end fn entry }.entry); } diff --git a/src/http/router/routing_trie.zig b/src/http/router/routing_trie.zig index dac92bc..319fbdc 100644 --- a/src/http/router/routing_trie.zig +++ b/src/http/router/routing_trie.zig @@ -297,7 +297,8 @@ pub const RoutingTrie = struct { .route = current.route orelse return null, .captures = captures[0..capture_idx], .queries = queries, - .duped = try duped.toOwnedSlice(allocator), + //.duped = try duped.toOwnedSlice(allocator), + .duped = if (duped.items.len > 0) try duped.toOwnedSlice(allocator) else &.{}, }; } }; diff --git a/src/http/server.zig b/src/http/server.zig index 81fceb8..71c1761 100644 --- a/src/http/server.zig +++ b/src/http/server.zig @@ -237,6 +237,14 @@ pub const Server = struct { const index = try provisions.borrow(); defer provisions.release(index); const provision = provisions.get_ptr(index); + + defer { + log.debug("DEBUG: Releasing slot {d} and resetting arena", .{index}); + provision.request.clear(); + provision.response.clear(); + provision.storage.clear(); + _ = provision.arena.reset(.retain_capacity); + } // if we are growing, we can handle a newly allocated provision here. // otherwise, it should be initalized. @@ -368,7 +376,9 @@ pub const Server = struct { if (request.headers.get("Upgrade")) |upgrade| { if (std.mem.eql(u8, upgrade, "websocket")) { if (try on_upgrade(request, upgrade)) { - continue :http_loop; + log.debug("DEBUG: Upgrade successful for slot {d}, exiting main_frame", .{index}); + //continue :http_loop; + return; } } @@ -376,14 +386,19 @@ pub const Server = struct { } const found = try router.get_bundle_from_host( // continue as common HTTP if not WebSocket upgrade - rt.allocator, + //rt.allocator, + provision.arena.allocator(), //provision.request.uri.?, request.uri.?, provision.captures, &provision.queries, ); - defer rt.allocator.free(found.duped); - defer for (found.duped) |dupe| rt.allocator.free(dupe); + //defer rt.allocator.free(found.duped); + //defer for (found.duped) |dupe| rt.allocator.free(dupe); + //defer if(found.duped.len > 0){ + // for (found.duped) |item| rt.allocator.free(item); + // rt.allocator.free(found.duped); + //}; const h_with_data: HandlerWithData = found.route.get_handler( diff --git a/src/websocket/pubsub.zig b/src/websocket/pubsub.zig index a3bf10f..db857aa 100644 --- a/src/websocket/pubsub.zig +++ b/src/websocket/pubsub.zig @@ -235,11 +235,14 @@ pub fn handle_upgrade(ctx: *const zzz.Context, user_handler: UserWsHandler, stac var header_buf = std.ArrayList(u8).init(ctx.allocator); defer header_buf.deinit(); - const res = try zzz.websocket.upgrade(&ctx.socket, ctx.runtime, ctx.allocator, key, ext, header_buf.writer()); + //const res = try zzz.websocket.upgrade(&ctx.socket, ctx.runtime, ctx.allocator, key, ext, header_buf.writer()); + const res = try zzz.websocket.upgrade(&ctx.socket, ctx.runtime, key, ext, header_buf.writer()); _ = try ctx.socket.send_all(ctx.runtime, header_buf.items); - const session = try std.heap.page_allocator.create(WsSession); - session.* = WsSession.init(std.heap.page_allocator, res.conn, user_handler); + //const session = try std.heap.page_allocator.create(WsSession); + //session.* = WsSession.init(std.heap.page_allocator, res.conn, user_handler); + const session = try ctx.runtime.allocator.create(WsSession); + session.* = WsSession.init(ctx.runtime.allocator, res.conn, user_handler); session.conn.socket = &session.socket_owned; // use heap instead stack session.conn.user_data = session; // for not use global maps that locks @@ -291,15 +294,16 @@ pub fn handle_upgrade(ctx: *const zzz.Context, user_handler: UserWsHandler, stac } session.deinit(); - std.heap.page_allocator.destroy(session); + //std.heap.page_allocator.destroy(session); + ctx.runtime.allocator.destroy(session); std.log.info("WebSocket Loop finished and memory freed", .{}); return .close; }; } - //zzz.websocket.runLoop(session.conn, internal_handler, ctx.runtime.allocator) catch |err| { // sync loop - zzz.websocket.runLoop(session.conn, internal_handler, std.heap.page_allocator) catch |err| { // sync loop + zzz.websocket.runLoop(session.conn, internal_handler, ctx.runtime.allocator) catch |err| { // sync loop + //zzz.websocket.runLoop(session.conn, internal_handler, std.heap.page_allocator) catch |err| { // sync loop if (err != error.Closed){ std.log.err("WebSocket RunLoop Error: {s}", .{ @errorName(err) }); } @@ -321,7 +325,8 @@ pub fn handle_upgrade(ctx: *const zzz.Context, user_handler: UserWsHandler, stac } session.deinit(); - std.heap.page_allocator.destroy(session); + //std.heap.page_allocator.destroy(session); + ctx.runtime.allocator.destroy(session); std.log.info("WebSocket Loop finished and memory freed", .{}); return .close; diff --git a/src/websocket/websocket.zig b/src/websocket/websocket.zig index f64c436..820b9df 100644 --- a/src/websocket/websocket.zig +++ b/src/websocket/websocket.zig @@ -103,7 +103,7 @@ pub const HandshakeResult = struct { pub fn upgrade( socket: *const SecureSocket, runtime: *Runtime, - allocator: Allocator, + //allocator: Allocator, sec_websocket_key: []const u8, sec_websocket_extensions: ?[]const u8, response_writer: anytype, @@ -120,12 +120,12 @@ pub fn upgrade( // } //} - const accept = try computeAccept(allocator, sec_websocket_key); + const accept = computeAccept(sec_websocket_key); try response_writer.writeAll("HTTP/1.1 101 Switching Protocols\r\n"); try response_writer.writeAll("Upgrade: websocket\r\n"); try response_writer.writeAll("Connection: Upgrade\r\n"); try response_writer.writeAll("Sec-WebSocket-Accept: "); - try response_writer.writeAll(accept); + try response_writer.writeAll(&accept); try response_writer.writeAll("\r\n"); //if (compression) { // try response_writer.writeAll("Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n"); @@ -423,16 +423,18 @@ pub fn runLoop(conn: Conn, handler: Handler, allocator: Allocator) !void { // helpers -fn computeAccept(allocator: Allocator, key: []const u8) ![]const u8 { +fn computeAccept(key: []const u8) [28]u8 { const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; var hasher = std.crypto.hash.Sha1.init(.{}); hasher.update(key); hasher.update(magic); + var hash: [20]u8 = undefined; hasher.final(&hash); - var buf: [28]u8 = undefined; - const encoded = std.base64.standard.Encoder.encode(&buf, &hash); - return try allocator.dupe(u8, encoded); + + var out_buf: [28]u8 = undefined; + _ = std.base64.standard.Encoder.encode(&out_buf, &hash); + return out_buf; } const OpCode = enum(u8) { From dc8adeb93426ebe7b51e15629fc0804bb3765ba8 Mon Sep 17 00:00:00 2001 From: 221V <221v92@gmail.com> Date: Wed, 17 Jun 2026 14:14:56 +0300 Subject: [PATCH 22/26] cannot fix memleaks (( --- build.zig | 4 +- build.zig.zon | 10 +-- examples_ws/example_ws_1.zig | 36 +++++++++-- src/http/server.zig | 117 +++++++++++++++++++++++++---------- 4 files changed, 120 insertions(+), 47 deletions(-) diff --git a/build.zig b/build.zig index 5bbec0c..eb5560a 100644 --- a/build.zig +++ b/build.zig @@ -2,8 +2,8 @@ const std = @import("std"); pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); - //const optimize = b.standardOptimizeOption(.{}); // -O Debug - const optimize = std.builtin.OptimizeMode.ReleaseFast; // -O ReleaseFast + const optimize = b.standardOptimizeOption(.{}); // -O Debug + //const optimize = std.builtin.OptimizeMode.ReleaseFast; // -O ReleaseFast const zzz = b.addModule("zzz", .{ .root_source_file = b.path("src/lib.zig"), diff --git a/build.zig.zon b/build.zig.zon index ae04fa1..7e346d2 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,20 +1,20 @@ .{ .name = .zzz, - .fingerprint = 0xc3273dca261a7ae0, + .fingerprint = 0xc3273dca46e69cd6, .version = "0.3.0", .minimum_zig_version = "0.14.1", .dependencies = .{ .tardy = .{ //.url = "git+https://github.com/tardy-org/tardy?ref=v0.3.0#cd454060f3b6006368d53c05ab96cd16c73c34de", //.hash = "tardy-0.3.0-69wrgi7PAwDFhO7m0aXae6N15s2b28VIOrnRrSHHake6", - .url = "git+https://github.com/221V/tardy?ref=zig-0.14#d0364f3142cb859474cd23ccc9da1c97d9dc1e05", - .hash = "tardy-0.3.0-zwolxXHNAwA6tH6Xr1qgUIAyHy6o4-hgOSVOlGBt6jjL", + .url = "git+https://github.com/221V/tardy?ref=zig-0.14#4e0e33d8c4bc666262ab8b1a11dccb2e6703ff78", + .hash = "tardy-0.3.0-zwolxQTOAwDsVB0HQYuUzUDDi8YzZApgKg7VXzQws9zq", }, .secsock = .{ //.url = "git+https://github.com/tardy-org/secsock?ref=v0.1.0#263dcd630e32c7a5c7a0522a8d1fd04e39b75c24", //.hash = "secsock-0.0.0-p0qurf09AQD95s1NQF2MGpBqMmFz7cKZWibsgv_SQBAr", - .url = "git+https://github.com/221V/secsock?ref=zig-0.14#625147512dcacbfe5fa35cab2f94332ba76cd563", - .hash = "secsock-0.0.0-p0qurcM-AQCPUYli5tuTIPncDh3bKpHE-yBEkY5Zma1E", + .url = "git+https://github.com/221V/secsock?ref=zig-0.14#3ee4405b051cbbb458a8f7ffaec305ea355a4ea2", + .hash = "secsock-0.0.0-p0qurcM-AQBHDulqDh4EfyXGzGEFmEOu06vI2HiajIQJ", }, }, diff --git a/examples_ws/example_ws_1.zig b/examples_ws/example_ws_1.zig index 0aa9c57..5aa6c23 100644 --- a/examples_ws/example_ws_1.zig +++ b/examples_ws/example_ws_1.zig @@ -160,7 +160,7 @@ fn on_ws_endpoint(ctx: *const zzz.Context, _: void) !zzz.HTTP.Respond { pub fn main() !void{ //@compileLog("STACK_SIZE = ", STACK_SIZE); var gpa = std.heap.GeneralPurposeAllocator(.{ - .thread_safe = true, + //.thread_safe = true, .stack_trace_frames = 8, }){}; const allocator = gpa.allocator(); @@ -175,7 +175,9 @@ pub fn main() !void{ try socket.bind(); try socket.listen(1024); // max conn count that are waiting in accept queue - var tardy = try zzz.tardy.Tardy(.auto).init(allocator, .{}); + var tardy = try zzz.tardy.Tardy(.auto).init(allocator, .{ + .threading = .single, // for debug + }); defer tardy.deinit(); const home_route = zzz.HTTP.Route.init("/").get({}, on_request); @@ -188,19 +190,41 @@ pub fn main() !void{ const router = try zzz.Router.init(allocator, layers, .{ .not_found = on_request }); // no defer router - lifetime per server work // defer router.deinit(allocator); + var server = zzz.Server.init(.{ .stack_size = STACK_SIZE }); + const Entry_Params = struct { - config: zzz.ServerConfig, + server: *zzz.Server, + //config: zzz.ServerConfig, router: *const zzz.Router, socket: Socket, }; try tardy.entry( - Entry_Params{ .config = .{ .stack_size = STACK_SIZE }, .router = &router, .socket = socket }, + Entry_Params{ .server = &server, .router = &router, .socket = socket }, + //Entry_Params{ .config = .{ .stack_size = STACK_SIZE }, .router = &router, .socket = socket }, struct { fn entry(rt: *zzz.tardy.Runtime, p: Entry_Params) !void { - var server = zzz.Server.init(p.config); - try server.serve(rt, p.router, .{ .normal = p.socket }); + //var server = zzz.Server.init(p.config); + //try server.serve(rt, p.router, .{ .normal = p.socket }); + try p.server.serve(rt, p.router, .{ .normal = p.socket }); + + //try rt.spawn(.{rt, &server}, struct { + try rt.spawn(.{rt, p.server}, struct { + fn shutdown_timer(runtime: *zzz.tardy.Runtime, server1: *zzz.Server) !void { + try zzz.tardy.Timer.delay(runtime, .{ .seconds = 30 }); + std.log.info("AUTO-SHUTDOWN FOR LEAK CHECK...", .{}); + runtime.stop(); + + _ = server1; + //server1.deinit(); + //std.posix.exit(0); + } + }.shutdown_timer, 16 * 1024); + } // end fn entry }.entry); + + server.deinit(); + std.log.info("Done. GPA report now.", .{}); } diff --git a/src/http/server.zig b/src/http/server.zig index 71c1761..bbad9c7 100644 --- a/src/http/server.zig +++ b/src/http/server.zig @@ -150,14 +150,49 @@ pub const Provision = struct { pub const Server = struct { const Self = @This(); config: ServerConfig, + + provision_pool: ?*Pool(Provision) = null, + connection_count: ?*usize = null, + accept_queued: ?*bool = null, + allocator: ?std.mem.Allocator = null, pub fn init(config: ServerConfig) Self { return Self{ .config = config }; } - pub fn deinit(self: *const Self) void { - if (self.tls_ctx) |tls| { - tls.deinit(); + //pub fn deinit(self: *const Self) void { + pub fn deinit(self: *Self) void { + //if (self.tls_ctx) |tls| { + // tls.deinit(); + //} + const alloc = self.allocator orelse return; + + if (self.provision_pool) |pool| { + for (pool.items) |*provision| { + if (provision.initalized) { + provision.zc_recv_buffer.deinit(); + provision.header_buffer.deinit(); + provision.request.deinit(); + provision.response.deinit(); + provision.queries.deinit(); + provision.storage.deinit(); + provision.arena.deinit(); + alloc.free(provision.captures); + provision.initalized = false; + } + } + pool.deinit(); + alloc.destroy(pool); + self.provision_pool = null; + } + + if (self.connection_count) |ptr| { + alloc.destroy(ptr); + self.connection_count = null; + } + if (self.accept_queued) |ptr| { + alloc.destroy(ptr); + self.accept_queued = null; } } @@ -237,33 +272,37 @@ pub const Server = struct { const index = try provisions.borrow(); defer provisions.release(index); const provision = provisions.get_ptr(index); + log.debug("DEBUG CONN START: Slot={d} ActiveTasks={d}", .{index, rt.scheduler.runnable}); defer { - log.debug("DEBUG: Releasing slot {d} and resetting arena", .{index}); + log.debug("DEBUG CONN END: Slot={d} Releasing resources", .{index}); + //log.debug("DEBUG: Releasing slot {d} and resetting arena", .{index}); provision.request.clear(); provision.response.clear(); provision.storage.clear(); - _ = provision.arena.reset(.retain_capacity); + //_ = provision.arena.reset(.retain_capacity); + _ = provision.arena.reset(.free_all); } // if we are growing, we can handle a newly allocated provision here. // otherwise, it should be initalized. - if (!provision.initalized) { - log.debug("initalizing new provision", .{}); - provision.zc_recv_buffer = ZeroCopy(u8).init(rt.allocator, config.socket_buffer_bytes) catch { - @panic("attempting to allocate more memory than available. (ZeroCopyBuffer)"); - }; - provision.header_buffer = std.ArrayList(u8).init(rt.allocator); - provision.arena = std.heap.ArenaAllocator.init(rt.allocator); - provision.captures = rt.allocator.alloc(Capture, config.capture_count_max) catch { - @panic("attempting to allocate more memory than available. (Captures)"); - }; - provision.queries = AnyCaseStringMap.init(rt.allocator); - provision.storage = TypedStorage.init(rt.allocator); - provision.request = Request.init(rt.allocator); - provision.response = Response.init(rt.allocator); - provision.initalized = true; - } + //if (!provision.initalized) { + //log.debug("initalizing new provision", .{}); + //provision.zc_recv_buffer = ZeroCopy(u8).init(rt.allocator, config.socket_buffer_bytes) catch { + // @panic("attempting to allocate more memory than available. (ZeroCopyBuffer)"); + //}; + //provision.header_buffer = std.ArrayList(u8).init(rt.allocator); + //provision.arena = std.heap.ArenaAllocator.init(rt.allocator); + //provision.captures = rt.allocator.alloc(Capture, config.capture_count_max) catch { + // @panic("attempting to allocate more memory than available. (Captures)"); + //}; + //provision.queries = AnyCaseStringMap.init(rt.allocator); + //provision.storage = TypedStorage.init(rt.allocator); + //provision.request = Request.init(rt.allocator); + //provision.response = Response.init(rt.allocator); + ////provision.initalized = true; // what if here we got double init? first time in serve + //} + assert(provision.initalized); defer prepare_new_request(null, provision, config) catch unreachable; var state: State = .{ .request = .header }; @@ -541,20 +580,27 @@ pub const Server = struct { const count = self.config.connection_count_max orelse 1024; const pooling: PoolKind = if (self.config.connection_count_max == null) .grow else .static; - const provision_pool = try rt.allocator.create(Pool(Provision)); - provision_pool.* = try Pool(Provision).init(rt.allocator, count, pooling); - errdefer rt.allocator.destroy(provision_pool); + //const provision_pool = try rt.allocator.create(Pool(Provision)); + //provision_pool.* = try Pool(Provision).init(rt.allocator, count, pooling); + //errdefer rt.allocator.destroy(provision_pool); + self.provision_pool = try rt.allocator.create(Pool(Provision)); + self.provision_pool.?.* = try Pool(Provision).init(rt.allocator, count, pooling); - const connection_count = try rt.allocator.create(usize); - errdefer rt.allocator.destroy(connection_count); - connection_count.* = 0; + //const connection_count = try rt.allocator.create(usize); + //errdefer rt.allocator.destroy(connection_count); + //connection_count.* = 0; + self.connection_count = try rt.allocator.create(usize); + self.connection_count.?.* = 0; - const accept_queued = try rt.allocator.create(bool); - errdefer rt.allocator.destroy(accept_queued); - accept_queued.* = true; + //const accept_queued = try rt.allocator.create(bool); + //errdefer rt.allocator.destroy(accept_queued); + //accept_queued.* = true; + self.accept_queued = try rt.allocator.create(bool); + self.accept_queued.?.* = true; // initialize first batch of provisions :) - for (provision_pool.items) |*provision| { + //for (provision_pool.items) |*provision| { + for (self.provision_pool.?.items) |*provision| { provision.initalized = true; provision.zc_recv_buffer = ZeroCopy(u8).init( rt.allocator, @@ -579,9 +625,12 @@ pub const Server = struct { self.config, router, secure, - provision_pool, - connection_count, - accept_queued, + //provision_pool, + self.provision_pool.?, + //connection_count, + self.connection_count.?, + //accept_queued, + self.accept_queued.?, }, main_frame, self.config.stack_size, From e0db5242fdf149a650b5b81e434c74aafc36bb12 Mon Sep 17 00:00:00 2001 From: 221V <221v92@gmail.com> Date: Wed, 17 Jun 2026 14:18:33 +0300 Subject: [PATCH 23/26] refactoring example 2 --- examples_ws/example_ws_2.zig | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/examples_ws/example_ws_2.zig b/examples_ws/example_ws_2.zig index 1c45d95..1c25ff8 100644 --- a/examples_ws/example_ws_2.zig +++ b/examples_ws/example_ws_2.zig @@ -197,18 +197,23 @@ pub fn main() !void{ var tardy = try TardyType.init(allocator, .{}); defer tardy.deinit(); + var server = zzz.Server.init(.{ .stack_size = STACK_SIZE }); + const Entry_Params = struct { - config: zzz.ServerConfig, + //config: zzz.ServerConfig, + server: *zzz.Server, router: *const zzz.Router, socket: zzz.secsock.SecureSocket, }; try tardy.entry( - Entry_Params{ .config = .{ .stack_size = STACK_SIZE }, .router = &router, .socket = secure_socket }, + //Entry_Params{ .config = .{ .stack_size = STACK_SIZE }, .router = &router, .socket = secure_socket }, + Entry_Params{ .server = &server, .router = &router, .socket = secure_socket }, struct { fn entry(rt: *zzz.tardy.Runtime, p: Entry_Params) !void { - var server = zzz.Server.init(p.config); - try server.serve(rt, p.router, .{ .secure = p.socket }); + //var server = zzz.Server.init(p.config); + //try server.serve(rt, p.router, .{ .secure = p.socket }); + try p.server.serve(rt, p.router, .{ .secure = p.socket }); } // end fn entry }.entry); } From 9a20b8c09bfa48e0bd2155c8f04f41b3d771c332 Mon Sep 17 00:00:00 2001 From: 221V <221v92@gmail.com> Date: Wed, 17 Jun 2026 14:26:31 +0300 Subject: [PATCH 24/26] refactoring ws example 3 --- examples_ws/example_ws_3.zig | 60 ++++++++++++++---------------------- 1 file changed, 23 insertions(+), 37 deletions(-) diff --git a/examples_ws/example_ws_3.zig b/examples_ws/example_ws_3.zig index 984589c..baddc0a 100644 --- a/examples_ws/example_ws_3.zig +++ b/examples_ws/example_ws_3.zig @@ -141,43 +141,29 @@ pub fn main() !void{ //var tardy = try TardyType.init(allocator, .{ .threading = .single }); defer tardy.deinit(); - try tardy.entry(&socket, struct { - fn entry(rt: *zzz.tardy.Runtime, s: *const Socket) !void { - const config = zzz.ServerConfig{ - .stack_size = STACK_SIZE, // stack for http requests - }; - - const home_route = zzz.HTTP.Route.init("/").get({}, on_request); - const ws_route = zzz.HTTP.Route.init("/ws").get({}, on_ws_endpoint); - const layers = &[_]zzz.HTTP.Layer{ - home_route.layer(), - ws_route.layer(), - }; - - const router = try rt.allocator.create(zzz.Router); - router.* = try zzz.Router.init(rt.allocator, layers, .{ - .not_found = on_request, - }); - // no defer router - lifetime per server work - - const provisions = try rt.allocator.create(zzz.tardy.Pool(zzz.Provision)); // use heap instead of stack - provisions.* = try zzz.tardy.Pool(zzz.Provision).init(rt.allocator, 1024, .static); - - const byte_count = provisions.items.len * @sizeOf(zzz.Provision); // set zeros -- initialized = false - @memset(@as([*]u8, @ptrCast(provisions.items.ptr))[0..byte_count], 0); - - const connection_count = try rt.allocator.create(usize); // use heap instead stack - connection_count.* = 0; - - const accept_queued = try rt.allocator.create(bool); - accept_queued.* = false; - - try rt.spawn( - .{ rt, config, router, zzz.secsock.SecureSocket.unsecured(s.*), provisions, connection_count, accept_queued }, - zzz.Server.main_frame, - config.stack_size - ); - + const home_route = zzz.HTTP.Route.init("/").get({}, on_request); + const ws_route = zzz.HTTP.Route.init("/ws").get({}, on_ws_endpoint); + const layers = &[_]zzz.HTTP.Layer{ + home_route.layer(), + ws_route.layer(), + }; + + const router = try zzz.Router.init(allocator, layers, .{ .not_found = on_request }); + // no defer router - lifetime per server work + + var server = zzz.Server.init(.{ .stack_size = STACK_SIZE }); // stack for http requests + + const Entry_Params = struct { + server: *zzz.Server, + router: *const zzz.Router, + socket: Socket, + }; + + try tardy.entry( + Entry_Params{ .server = &server, .router = &router, .socket = socket }, + struct { + fn entry(rt: *zzz.tardy.Runtime, p: Entry_Params) !void { + try p.server.serve(rt, p.router, .{ .normal = p.socket }); } // end fn entry }.entry); } From de8be00f8bb0a881425bac8e6db53f4242cfb6a2 Mon Sep 17 00:00:00 2001 From: 221V <221v92@gmail.com> Date: Wed, 17 Jun 2026 14:34:14 +0300 Subject: [PATCH 25/26] refactoring ws example 4 --- examples_ws/example_ws_2.zig | 3 +- examples_ws/example_ws_3.zig | 3 +- examples_ws/example_ws_4.zig | 63 ++++++++++++++++-------------------- 3 files changed, 29 insertions(+), 40 deletions(-) diff --git a/examples_ws/example_ws_2.zig b/examples_ws/example_ws_2.zig index 1c25ff8..38864e3 100644 --- a/examples_ws/example_ws_2.zig +++ b/examples_ws/example_ws_2.zig @@ -193,8 +193,7 @@ pub fn main() !void{ const secure_socket = try bearssl.to_secure_socket(socket, .server); defer secure_socket.deinit(); - const TardyType = zzz.tardy.Tardy(.auto); - var tardy = try TardyType.init(allocator, .{}); + var tardy = try zzz.tardy.Tardy(.auto).init(allocator, .{}); defer tardy.deinit(); var server = zzz.Server.init(.{ .stack_size = STACK_SIZE }); diff --git a/examples_ws/example_ws_3.zig b/examples_ws/example_ws_3.zig index baddc0a..d07f14a 100644 --- a/examples_ws/example_ws_3.zig +++ b/examples_ws/example_ws_3.zig @@ -136,8 +136,7 @@ pub fn main() !void{ try socket.bind(); try socket.listen(1024); // max conn count that are waiting in accept queue - const TardyType = zzz.tardy.Tardy(.auto); - var tardy = try TardyType.init(allocator, .{}); + var tardy = try zzz.tardy.Tardy(.auto).init(allocator, .{}); //var tardy = try TardyType.init(allocator, .{ .threading = .single }); defer tardy.deinit(); diff --git a/examples_ws/example_ws_4.zig b/examples_ws/example_ws_4.zig index 10a68d9..6f62d68 100644 --- a/examples_ws/example_ws_4.zig +++ b/examples_ws/example_ws_4.zig @@ -505,44 +505,35 @@ pub fn main() !void { try socket.bind(); try socket.listen(1024); - const TardyType = zzz.tardy.Tardy(.auto); - var tardy = try TardyType.init(allocator, .{}); + const home_route = zzz.HTTP.Route.init("/").get({}, on_request); + const ws_route = zzz.HTTP.Route.init("/ws").get({}, on_ws_endpoint); + + //const js_route = zzz.HTTP.FsDir.serve("/static", zzz.tardy.Dir.cwd()); // bert_ftp.js + const std_static_dir = try std.fs.cwd().openDir("examples_ws/static", .{ .iterate = true }); + const static_dir = zzz.tardy.Dir.from_std(std_static_dir); + const js_route = zzz.HTTP.FsDir.serve("/static", static_dir); // bert_ftp.js + + const layers = &[_]zzz.HTTP.Layer{ home_route.layer(), ws_route.layer(), js_route }; + + const router = try zzz.Router.init(allocator, layers, .{ .not_found = on_request }); + + var tardy = try zzz.tardy.Tardy(.auto).init(allocator, .{}); defer tardy.deinit(); + + var server = zzz.Server.init(.{ .stack_size = WS_STACK_SIZE }); + + const Entry_Params = struct { + server: *zzz.Server, + router: *const zzz.Router, + socket: Socket, + }; + - try tardy.entry(&socket, struct { - fn entry(rt: *zzz.tardy.Runtime, s: *const Socket) !void { - const server_config = zzz.ServerConfig{ .stack_size = 64 * 1024 }; - - const home_route = zzz.HTTP.Route.init("/").get({}, on_request); - const ws_route = zzz.HTTP.Route.init("/ws").get({}, on_ws_endpoint); - - //const js_route = zzz.HTTP.FsDir.serve("/static", zzz.tardy.Dir.cwd()); // bert_ftp.js - const std_static_dir = try std.fs.cwd().openDir("examples_ws/static", .{ .iterate = true }); - const static_dir = zzz.tardy.Dir.from_std(std_static_dir); - const js_route = zzz.HTTP.FsDir.serve("/static", static_dir); // bert_ftp.js - - const layers = &[_]zzz.HTTP.Layer{ home_route.layer(), ws_route.layer(), js_route }; - - const router = try rt.allocator.create(zzz.Router); - router.* = try zzz.Router.init(rt.allocator, layers, .{ .not_found = on_request }); - - const provisions = try rt.allocator.create(zzz.tardy.Pool(zzz.Provision)); - provisions.* = try zzz.tardy.Pool(zzz.Provision).init(rt.allocator, 1024, .static); - const byte_count = provisions.items.len * @sizeOf(zzz.Provision); - @memset(@as([*]u8, @ptrCast(provisions.items.ptr))[0..byte_count], 0); - - const connection_count = try rt.allocator.create(usize); - connection_count.* = 0; - const accept_queued = try rt.allocator.create(bool); - accept_queued.* = false; - - try rt.spawn( - .{ rt, server_config, router, zzz.secsock.SecureSocket.unsecured(s.*), provisions, connection_count, accept_queued }, - zzz.Server.main_frame, - server_config.stack_size - ); - - //try rt.spawn(.{rt}, cleanup_task, 64 * 1024); + try tardy.entry( + Entry_Params{ .server = &server, .router = &router, .socket = socket }, + struct { + fn entry(rt: *zzz.tardy.Runtime, p: Entry_Params) !void { + try p.server.serve(rt, p.router, .{ .normal = p.socket }); } }.entry); } From ec7807647f906c36bad9543fc815e884372668fb Mon Sep 17 00:00:00 2001 From: 221V <221v92@gmail.com> Date: Wed, 17 Jun 2026 14:37:41 +0300 Subject: [PATCH 26/26] refactoring http example rest --- examples_http/rest/main.zig | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/examples_http/rest/main.zig b/examples_http/rest/main.zig index 3d9196f..d4cce54 100644 --- a/examples_http/rest/main.zig +++ b/examples_http/rest/main.zig @@ -120,18 +120,23 @@ pub fn main() !void { http.Route.init("/").get({}, on_request).post({}, on_request).layer(), }, .{ .not_found = on_request }); + var server = zzz.Server.init(.{ .stack_size = STACK_SIZE }); // stack for http requests + const Entry_Params = struct { - config: zzz.ServerConfig, + //config: zzz.ServerConfig, + server: *zzz.Server, router: *const http.Router, socket: Socket, }; try tardy.entry( - Entry_Params{ .config = .{ .stack_size = STACK_SIZE }, .router = &router, .socket = socket }, + //Entry_Params{ .config = .{ .stack_size = STACK_SIZE }, .router = &router, .socket = socket }, + Entry_Params{ .server = &server, .router = &router, .socket = socket }, struct { fn entry(rt: *zzz.tardy.Runtime, p: Entry_Params) !void { - var server = zzz.Server.init(p.config); - try server.serve(rt, p.router, .{ .normal = p.socket }); + //var server = zzz.Server.init(p.config); + //try server.serve(rt, p.router, .{ .normal = p.socket }); + try p.server.serve(rt, p.router, .{ .normal = p.socket }); } // end fn entry }.entry); }