From ae310a207534b33b7321dd2b9f423a73b1969159 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 14 Sep 2026 14:28:15 -0300 Subject: Add reusable HTTP transport, serial gateway, and WASM file browser --- test/http.zig | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 test/http.zig (limited to 'test/http.zig') diff --git a/test/http.zig b/test/http.zig new file mode 100644 index 0000000..d5064e4 --- /dev/null +++ b/test/http.zig @@ -0,0 +1,92 @@ +const std = @import("std"); +const c9 = @import("cloud9"); +const testing = std.testing; +const http = c9.http; + +test "client and server preserve 9P frames at WebSocket length boundaries" { + var bytes: [70000]u8 = undefined; + var data: [66000]u8 = @splat(0xa5); + var receive_buffer: [70000]u8 = undefined; + for ([_]usize{ 0, 114, 115, 65524, 65525 }) |length| { + const frame = try c9.encode(.{ .rread = .{ .data = data[0..length] } }, 7, &bytes); + for ([_]http.Role{ .client, .server }) |role| { + var writer: std.Io.Writer.Allocating = .init(testing.allocator); + defer writer.deinit(); + var empty: std.Io.Reader = .fixed(""); + var sender: http.WebSocket = .{ .input = &empty, .output = &writer.writer, .role = role }; + try sender.send(frame, .binary, if (role == .client) .{ 1, 2, 3, 4 } else null); + var reader: testing.Reader = .init(&.{}, &.{.{ .buffer = writer.written() }}); + reader.artificial_limit = .limited(1); + var receiver: http.WebSocket = .{ .input = &reader.interface, .output = &writer.writer, .role = if (role == .client) .server else .client }; + const message = try receiver.receive(&receive_buffer); + try testing.expectEqual(.binary, message.opcode); + try testing.expectEqualSlices(u8, frame, message.data); + } + } +} + +test "fragmented message survives an interleaved ping" { + // Rflush tag 1 split after its size field. Ping between the fragments. + var reader: std.Io.Reader = .fixed(&.{ 0x02, 4, 7, 0, 0, 0, 0x89, 1, 'x', 0x80, 3, 109, 1, 0 }); + var writer: std.Io.Writer = .fixed(&.{}); + var socket: http.WebSocket = .{ .input = &reader, .output = &writer, .role = .client }; + var buffer: [64]u8 = undefined; + const ping = try socket.receive(&buffer); + try testing.expectEqual(.ping, ping.opcode); + try testing.expectEqualStrings("x", ping.data); + const frame = try socket.receive(&buffer); + const decoded = try c9.decode(frame.data); + try testing.expectEqual(c9.Type.rflush, decoded.msg.msgType()); + try testing.expectEqual(@as(u16, 1), decoded.tag); +} + +test "framing rejects invalid masks, reserved bits, lengths and controls" { + const cases = .{ + .{ &[_]u8{ 0x82, 0x80 }, error.InvalidMask }, + .{ &[_]u8{ 0xc2, 0 }, error.ReservedBits }, + .{ &[_]u8{ 0x82, 126, 0, 7 }, error.NonCanonicalLength }, + .{ &[_]u8{ 0x89, 126, 0, 126 }, error.InvalidControl }, + .{ &[_]u8{ 0x09, 0 }, error.InvalidControl }, + .{ &[_]u8{ 0x80, 0 }, error.UnexpectedContinuation }, + .{ &[_]u8{ 0x81, 0 }, error.ExpectedBinary }, + .{ &[_]u8{ 0x88, 1, 0 }, error.InvalidClose }, + .{ &[_]u8{ 0x88, 2, 3, 237 }, error.InvalidClose }, + .{ &[_]u8{ 0x88, 3, 3, 232, 255 }, error.InvalidClose }, + .{ &[_]u8{ 0x82, 100 }, error.MessageTooLarge }, + }; + inline for (cases) |case| { + var reader: std.Io.Reader = .fixed(case[0]); + var writer: std.Io.Writer = .fixed(&.{}); + var socket: http.WebSocket = .{ .input = &reader, .output = &writer, .role = .client }; + var buffer: [64]u8 = undefined; + try testing.expectError(case[1], socket.receive(&buffer)); + } +} + +test "binary messages must contain exactly one complete 9P frame" { + var reader: std.Io.Reader = .fixed(&.{ 0x82, 7, 8, 0, 0, 0, 109, 1, 0 }); + var writer: std.Io.Writer = .fixed(&.{}); + var socket: http.WebSocket = .{ .input = &reader, .output = &writer, .role = .client }; + var buffer: [64]u8 = undefined; + try testing.expectError(error.Truncated, socket.receive(&buffer)); +} + +test "upgrade validates nonce and uses RFC 6455 accept value" { + var reader: std.Io.Reader = .fixed("GET /9p HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive, Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n"); + var writer: std.Io.Writer.Allocating = .init(testing.allocator); + defer writer.deinit(); + var server: std.http.Server = .init(&reader, &writer.writer); + var request = try server.receiveHead(); + _ = try http.accept(&request); + try testing.expect(std.mem.find(u8, writer.written(), "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=") != null); +} + +test "upgrade rejects invalid nonce before responding" { + inline for (.{ "not-a-nonce", "AAAAAAAAAAAAAAAAAAAAAAAA", "AAAAAAAAAAAAAAAAAAAAAAA=", "!!!!!!!!!!!!!!!!!!!!!!==" }) |nonce| { + var reader: std.Io.Reader = .fixed("GET /9p HTTP/1.1\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: " ++ nonce ++ "\r\n\r\n"); + var writer: std.Io.Writer = .fixed(&.{}); + var server: std.http.Server = .init(&reader, &writer); + var request = try server.receiveHead(); + try testing.expectError(error.InvalidUpgrade, http.accept(&request)); + } +} -- cgit v1.3