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)); } }