lib/http/src/properties/websocket.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const hypothesis = @import("hypothesis");
3 const http = @import("http");
4
5 const Frame = http.Frame;
6 const FrameError = http.FrameError;
7 const Opcode = http.Opcode;
8
9 const Encoding = enum {
10 shortest,
11 extended16,
12 extended64,
13 };
14
15 fn drawMask(data: *hypothesis.ConjectureData) !?[4]u8 {
16 if (!try data.drawBoolean()) return null;
17 const bytes = try data.drawBytes(4, 4);
18 return bytes[0..4].*;
19 }
20
21 fn rawFrameBytes(
22 output: []u8,
23 fin: bool,
24 opcode_value: u8,
25 mask: ?[4]u8,
26 payload: []const u8,
27 encoding: Encoding,
28 ) ![]u8 {
29 const length_bytes: usize = switch (encoding) {
30 .shortest => 0,
31 .extended16 => 2,
32 .extended64 => 8,
33 };
34 const header_bytes = 2 + length_bytes + @as(usize, if (mask == null) 0 else 4);
35 const total_bytes = try std.math.add(usize, header_bytes, payload.len);
36 if (output.len < total_bytes) return error.OutputTooSmall;
37 const bytes = output[0..total_bytes];
38 bytes[0] = @as(u8, if (fin) 0x80 else 0) | opcode_value;
39 const mask_bit: u8 = if (mask != null) 0x80 else 0;
40 var index: usize = 1;
41 switch (encoding) {
42 .shortest => {
43 bytes[index] = mask_bit | @as(u8, @intCast(payload.len));
44 index += 1;
45 },
46 .extended16 => {
47 bytes[index] = mask_bit | 126;
48 index += 1;
49 std.mem.writeInt(u16, bytes[index..][0..2], @intCast(payload.len), .big);
50 index += 2;
51 },
52 .extended64 => {
53 bytes[index] = mask_bit | 127;
54 index += 1;
55 std.mem.writeInt(u64, bytes[index..][0..8], @intCast(payload.len), .big);
56 index += 8;
57 },
58 }
59
60 if (mask) |value| {
61 @memcpy(bytes[index..][0..value.len], &value);
62 index += value.len;
63 for (payload, bytes[index..], 0..) |source, *target, payload_index| {
64 target.* = source ^ value[payload_index % value.len];
65 }
66 } else {
67 @memcpy(bytes[index..], payload);
68 }
69 return bytes;
70 }
71
72 fn expectFrameParseError(
73 expected: FrameError,
74 data: []u8,
75 ) !void {
76 const result = Frame.parse(data, data.len);
77 if (result) |_| {
78 return error.MalformedFrameAccepted;
79 } else |err| {
80 try std.testing.expectEqual(expected, err);
81 }
82 }
83
84 fn expectFrameEqual(expected: Frame, actual: Frame) !void {
85 try std.testing.expectEqual(expected.fin, actual.fin);
86 try std.testing.expectEqual(expected.opcode, actual.opcode);
87 try std.testing.expectEqual(expected.mask != null, actual.mask != null);
88 if (expected.mask) |expected_mask| {
89 try std.testing.expectEqual(expected_mask, actual.mask.?);
90 }
91 try std.testing.expectEqualSlices(u8, expected.payload, actual.payload);
92 }
93
94 const RoundtripProperty = struct {
95 const opcodes = [_]Opcode{
96 .continuation,
97 .text,
98 .binary,
99 .close,
100 .ping,
101 .pong,
102 };
103
104 fn drawOpcode(data: *hypothesis.ConjectureData) !Opcode {
105 const index: usize = @intCast(try data.drawInteger(0, opcodes.len - 1, 0));
106 return opcodes[index];
107 }
108
109 fn drawPayloadLength(data: *hypothesis.ConjectureData, opcode: Opcode) !usize {
110 if (opcode.isControl()) {
111 return @intCast(try data.drawInteger(0, 125, 0));
112 }
113
114 return switch (try data.drawInteger(0, 6, 0)) {
115 0 => @intCast(try data.drawInteger(0, 125, 0)),
116 1 => 126,
117 2 => @intCast(try data.drawInteger(127, 512, 126)),
118 3 => 65535,
119 4 => @intCast(try data.drawInteger(65536, 66000, 65536)),
120 5 => 0,
121 else => @intCast(try data.drawInteger(1024, 4096, 1024)),
122 };
123 }
124
125 pub fn property(data: *hypothesis.ConjectureData, allocator: std.mem.Allocator) !void {
126 const opcode = try drawOpcode(data);
127 const payload_len = try drawPayloadLength(data, opcode);
128 const payload = try data.drawBytes(payload_len, payload_len);
129 const frame = Frame{
130 .fin = if (opcode.isControl()) true else try data.drawBoolean(),
131 .opcode = opcode,
132 .mask = try drawMask(data),
133 .payload = payload,
134 };
135
136 const serialized_length = try frame.serializedLength(payload_len);
137 const serialized = try allocator.alloc(u8, serialized_length);
138 defer allocator.free(serialized);
139 _ = try frame.serializeInto(serialized, payload_len);
140
141 const parsed = try Frame.parse(serialized, payload_len);
142
143 try std.testing.expectEqual(serialized.len, parsed.consumed);
144 try expectFrameEqual(frame, parsed.frame);
145
146 _ = try frame.serializeInto(serialized, payload_len);
147 const server_result = Frame.parseServer(serialized, payload_len);
148 if (frame.mask) |_| {
149 const parsed_server = try server_result;
150 try expectFrameEqual(frame, parsed_server.frame);
151 try std.testing.expectEqual(serialized.len, parsed_server.consumed);
152 } else if (server_result) |_| {
153 return error.UnmaskedFrameAccepted;
154 } else |err| {
155 try std.testing.expectEqual(FrameError.MaskRequired, err);
156 }
157 }
158 };
159
160 test "pbt: generated websocket frames match serializer" {
161 var settings = hypothesis.Settings.dev().withSeed(0x4854_5450_5753_4652);
162 settings.max_examples = 128;
163 try hypothesis.checkNamed(
164 RoundtripProperty,
165 "http-websocket-frame-roundtrip",
166 settings.withDatabase("zig-out/hypothesis-failures/http-websocket-frame"),
167 );
168 }
169
170 const RejectionProperty = struct {
171 const reserved_opcodes = [_]u8{ 0x3, 0x4, 0x5, 0x6, 0x7, 0xB, 0xC, 0xD, 0xE, 0xF };
172 const control_opcodes = [_]u8{ 0x8, 0x9, 0xA };
173
174 fn drawReservedOpcode(data: *hypothesis.ConjectureData) !u8 {
175 const index: usize = @intCast(try data.drawInteger(0, reserved_opcodes.len - 1, 0));
176 return reserved_opcodes[index];
177 }
178
179 fn drawControlOpcode(data: *hypothesis.ConjectureData) !u8 {
180 const index: usize = @intCast(try data.drawInteger(0, control_opcodes.len - 1, 0));
181 return control_opcodes[index];
182 }
183
184 pub fn property(data: *hypothesis.ConjectureData, _: std.mem.Allocator) !void {
185 const case = try data.drawInteger(0, 3, 0);
186 switch (case) {
187 0 => {
188 const payload = try data.drawBytes(0, 16);
189 var raw_bytes: [32]u8 = undefined;
190 const raw = try rawFrameBytes(
191 &raw_bytes,
192 true,
193 try drawReservedOpcode(data),
194 try drawMask(data),
195 payload,
196 .shortest,
197 );
198 try expectFrameParseError(FrameError.InvalidOpcode, raw);
199 },
200 1 => {
201 const payload = try data.drawBytes(0, 16);
202 var raw_bytes: [32]u8 = undefined;
203 const raw = try rawFrameBytes(
204 &raw_bytes,
205 false,
206 try drawControlOpcode(data),
207 try drawMask(data),
208 payload,
209 .shortest,
210 );
211 try expectFrameParseError(FrameError.InvalidFrame, raw);
212 },
213 2 => {
214 const payload_len: usize = @intCast(try data.drawInteger(126, 130, 126));
215 const payload = try data.drawBytes(payload_len, payload_len);
216 var raw_bytes: [144]u8 = undefined;
217 const raw = try rawFrameBytes(
218 &raw_bytes,
219 true,
220 try drawControlOpcode(data),
221 try drawMask(data),
222 payload,
223 .extended16,
224 );
225 try expectFrameParseError(FrameError.InvalidFrame, raw);
226 },
227 else => {
228 const payload = try data.drawBytes(0, 125);
229 var raw_bytes: [144]u8 = undefined;
230 const raw = try rawFrameBytes(
231 &raw_bytes,
232 true,
233 0x2,
234 try drawMask(data),
235 payload,
236 if (try data.drawBoolean()) .extended16 else .extended64,
237 );
238 try expectFrameParseError(FrameError.InvalidFrame, raw);
239 },
240 }
241 }
242 };
243
244 test "pbt: generated malformed websocket frames reject" {
245 var settings = hypothesis.Settings.dev().withSeed(0x4854_5450_5753_bad);
246 settings.max_examples = 128;
247 try hypothesis.checkNamed(
248 RejectionProperty,
249 "http-websocket-frame-rejection",
250 settings.withDatabase("zig-out/hypothesis-failures/http-websocket-invalid-frame"),
251 );
252 }