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 }