authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-08-04 15:25:42-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-08-07 00:48:32-07:00
logb9fd0eeca6ba45058ed67fc211bb50a93c971a28
tree987862c47882d09ee6b6ad304c7e6444d809ba0b
parentd36c18274813268fd9edb633a31dd9e94ae11040

add std.http.WebSocket


2 files changed, 245 insertions(+), 0 deletions(-)

lib/std/http.zig+2
...@@ -4,6 +4,7 @@ pub const protocol = @import("http/protocol.zig");...@@ -4,6 +4,7 @@ pub const protocol = @import("http/protocol.zig");
4pub const HeadParser = @import("http/HeadParser.zig");4pub const HeadParser = @import("http/HeadParser.zig");
5pub const ChunkParser = @import("http/ChunkParser.zig");5pub const ChunkParser = @import("http/ChunkParser.zig");
6pub const HeaderIterator = @import("http/HeaderIterator.zig");6pub const HeaderIterator = @import("http/HeaderIterator.zig");
7pub const WebSocket = @import("http/WebSocket.zig");
78
8pub const Version = enum {9pub const Version = enum {
9 @"HTTP/1.0",10 @"HTTP/1.0",
...@@ -318,6 +319,7 @@ test {...@@ -318,6 +319,7 @@ test {
318 _ = Status;319 _ = Status;
319 _ = HeadParser;320 _ = HeadParser;
320 _ = ChunkParser;321 _ = ChunkParser;
322 _ = WebSocket;
321 _ = @import("http/test.zig");323 _ = @import("http/test.zig");
322 }324 }
323}325}
lib/std/http/WebSocket.zig created+243
...@@ -0,0 +1,243 @@
1//! See https://tools.ietf.org/html/rfc6455
2
3const builtin = @import("builtin");
4const std = @import("std");
5const WebSocket = @This();
6const assert = std.debug.assert;
7const native_endian = builtin.cpu.arch.endian();
8
9key: []const u8,
10request: *std.http.Server.Request,
11recv_fifo: std.fifo.LinearFifo(u8, .Slice),
12reader: std.io.AnyReader,
13response: std.http.Server.Response,
14/// Number of bytes that have been peeked but not discarded yet.
15outstanding_len: usize,
16
17pub const InitError = error{WebSocketUpgradeMissingKey} ||
18 std.http.Server.Request.ReaderError;
19
20pub fn init(
21 ws: *WebSocket,
22 request: *std.http.Server.Request,
23 send_buffer: []u8,
24 recv_buffer: []align(4) u8,
25) InitError!bool {
26 var sec_websocket_key: ?[]const u8 = null;
27 var upgrade_websocket: bool = false;
28 var it = request.iterateHeaders();
29 while (it.next()) |header| {
30 if (std.ascii.eqlIgnoreCase(header.name, "sec-websocket-key")) {
31 sec_websocket_key = header.value;
32 } else if (std.ascii.eqlIgnoreCase(header.name, "upgrade")) {
33 if (!std.mem.eql(u8, header.value, "websocket"))
34 return false;
35 upgrade_websocket = true;
36 }
37 }
38 if (!upgrade_websocket)
39 return false;
40
41 const key = sec_websocket_key orelse return error.WebSocketUpgradeMissingKey;
42
43 var sha1 = std.crypto.hash.Sha1.init(.{});
44 sha1.update(key);
45 sha1.update("258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
46 var digest: [std.crypto.hash.Sha1.digest_length]u8 = undefined;
47 sha1.final(&digest);
48 var base64_digest: [28]u8 = undefined;
49 assert(std.base64.standard.Encoder.encode(&base64_digest, &digest).len == base64_digest.len);
50
51 request.head.content_length = std.math.maxInt(u64);
52
53 ws.* = .{
54 .key = key,
55 .recv_fifo = std.fifo.LinearFifo(u8, .Slice).init(recv_buffer),
56 .reader = try request.reader(),
57 .response = request.respondStreaming(.{
58 .send_buffer = send_buffer,
59 .respond_options = .{
60 .status = .switching_protocols,
61 .extra_headers = &.{
62 .{ .name = "upgrade", .value = "websocket" },
63 .{ .name = "connection", .value = "upgrade" },
64 .{ .name = "sec-websocket-accept", .value = &base64_digest },
65 },
66 .transfer_encoding = .none,
67 },
68 }),
69 .request = request,
70 .outstanding_len = 0,
71 };
72 return true;
73}
74
75pub const Header0 = packed struct(u8) {
76 opcode: Opcode,
77 rsv3: u1 = 0,
78 rsv2: u1 = 0,
79 rsv1: u1 = 0,
80 fin: bool,
81};
82
83pub const Header1 = packed struct(u8) {
84 payload_len: enum(u7) {
85 len16 = 126,
86 len64 = 127,
87 _,
88 },
89 mask: bool,
90};
91
92pub const Opcode = enum(u4) {
93 continuation = 0,
94 text = 1,
95 binary = 2,
96 connection_close = 8,
97 ping = 9,
98 /// "A Pong frame MAY be sent unsolicited. This serves as a unidirectional
99 /// heartbeat. A response to an unsolicited Pong frame is not expected."
100 pong = 10,
101 _,
102};
103
104pub const ReadSmallTextMessageError = error{
105 ConnectionClose,
106 UnexpectedOpCode,
107 MessageTooBig,
108 MissingMaskBit,
109} || RecvError;
110
111pub const SmallMessage = struct {
112 /// Can be text, binary, or ping.
113 opcode: Opcode,
114 data: []u8,
115};
116
117/// Reads the next message from the WebSocket stream, failing if the message does not fit
118/// into `recv_buffer`.
119pub fn readSmallMessage(ws: *WebSocket) ReadSmallTextMessageError!SmallMessage {
120 while (true) {
121 const header_bytes = (try recv(ws, 2))[0..2];
122 const h0: Header0 = @bitCast(header_bytes[0]);
123 const h1: Header1 = @bitCast(header_bytes[1]);
124
125 switch (h0.opcode) {
126 .text, .binary, .pong, .ping => {},
127 .connection_close => return error.ConnectionClose,
128 .continuation => return error.UnexpectedOpCode,
129 _ => return error.UnexpectedOpCode,
130 }
131
132 if (!h0.fin) return error.MessageTooBig;
133 if (!h1.mask) return error.MissingMaskBit;
134
135 const len: usize = switch (h1.payload_len) {
136 .len16 => try recvReadInt(ws, u16),
137 .len64 => std.math.cast(usize, try recvReadInt(ws, u64)) orelse return error.MessageTooBig,
138 else => @intFromEnum(h1.payload_len),
139 };
140 if (len > ws.recv_fifo.buf.len) return error.MessageTooBig;
141
142 const mask: u32 = @bitCast((try recv(ws, 4))[0..4].*);
143 const payload = try recv(ws, len);
144
145 // Skip pongs.
146 if (h0.opcode == .pong) continue;
147
148 // The last item may contain a partial word of unused data.
149 const floored_len = (payload.len / 4) * 4;
150 const u32_payload: []align(1) u32 = @alignCast(std.mem.bytesAsSlice(u32, payload[0..floored_len]));
151 for (u32_payload) |*elem| elem.* ^= mask;
152 const mask_bytes = std.mem.asBytes(&mask)[0 .. payload.len - floored_len];
153 for (payload[floored_len..], mask_bytes) |*leftover, m| leftover.* ^= m;
154
155 return .{
156 .opcode = h0.opcode,
157 .data = payload,
158 };
159 }
160}
161
162const RecvError = std.http.Server.Request.ReadError || error{EndOfStream};
163
164fn recv(ws: *WebSocket, len: usize) RecvError![]u8 {
165 ws.recv_fifo.discard(ws.outstanding_len);
166 assert(len <= ws.recv_fifo.buf.len);
167 if (len > ws.recv_fifo.count) {
168 const small_buf = ws.recv_fifo.writableSlice(0);
169 const needed = len - ws.recv_fifo.count;
170 const buf = if (small_buf.len >= needed) small_buf else b: {
171 ws.recv_fifo.realign();
172 break :b ws.recv_fifo.writableSlice(0);
173 };
174 const n = try @as(RecvError!usize, @errorCast(ws.reader.readAtLeast(buf, needed)));
175 if (n < needed) return error.EndOfStream;
176 ws.recv_fifo.update(n);
177 }
178 ws.outstanding_len = len;
179 // TODO: improve the std lib API so this cast isn't necessary.
180 return @constCast(ws.recv_fifo.readableSliceOfLen(len));
181}
182
183fn recvReadInt(ws: *WebSocket, comptime I: type) !I {
184 const unswapped: I = @bitCast((try recv(ws, @sizeOf(I)))[0..@sizeOf(I)].*);
185 return switch (native_endian) {
186 .little => @byteSwap(unswapped),
187 .big => unswapped,
188 };
189}
190
191pub const WriteError = std.http.Server.Response.WriteError;
192
193pub fn writeMessage(ws: *WebSocket, message: []const u8, opcode: Opcode) WriteError!void {
194 const iovecs: [1]std.posix.iovec_const = .{
195 .{ .base = message.ptr, .len = message.len },
196 };
197 return writeMessagev(ws, &iovecs, opcode);
198}
199
200pub fn writeMessagev(ws: *WebSocket, message: []const std.posix.iovec_const, opcode: Opcode) WriteError!void {
201 const total_len = l: {
202 var total_len: u64 = 0;
203 for (message) |iovec| total_len += iovec.len;
204 break :l total_len;
205 };
206
207 var header_buf: [2 + 8]u8 = undefined;
208 header_buf[0] = @bitCast(@as(Header0, .{
209 .opcode = opcode,
210 .fin = true,
211 }));
212 const header = switch (total_len) {
213 0...125 => blk: {
214 header_buf[1] = @bitCast(@as(Header1, .{
215 .payload_len = @enumFromInt(total_len),
216 .mask = false,
217 }));
218 break :blk header_buf[0..2];
219 },
220 126...0xffff => blk: {
221 header_buf[1] = @bitCast(@as(Header1, .{
222 .payload_len = .len16,
223 .mask = false,
224 }));
225 std.mem.writeInt(u16, header_buf[2..4], @intCast(total_len), .big);
226 break :blk header_buf[0..4];
227 },
228 else => blk: {
229 header_buf[1] = @bitCast(@as(Header1, .{
230 .payload_len = .len64,
231 .mask = false,
232 }));
233 std.mem.writeInt(u64, header_buf[2..10], total_len, .big);
234 break :blk header_buf[0..10];
235 },
236 };
237
238 const response = &ws.response;
239 try response.writeAll(header);
240 for (message) |iovec|
241 try response.writeAll(iovec.base[0..iovec.len]);
242 try response.flush();
243}