srctree

Gregory Mullen parent 178c8ee5 2c06156c
add websocket prototype

build.zig added: 154, removed: 17, total 137
@@ -47,13 +47,8 @@ pub fn build(b: *std.Build) !void {
test_step.dependOn(&run_lib_unit_tests.step);
 
const examples = [_][]const u8{
"basic",
"cookies",
"template",
"endpoint",
"auth-cookie",
"request-userdata",
"api",
"basic", "cookies", "template", "endpoint", "auth-cookie",
"request-userdata", "api", "websocket",
};
inline for (examples) |example| {
const example_exe = b.addExecutable(.{
 
filename was Deleted added: 154, removed: 17, total 137
@@ -0,0 +1,76 @@
//! Basic Websocket
 
/// The verse endpoint API is explained in examples/endpoint.zig and a basic
/// understanding of Verse.Endpoint is assumed here.
const Root = struct {
pub const verse_name = .root;
pub const verse_routes = .{
verse.Router.WEBSOCKET("socket", socket),
};
 
pub fn index(frame: *verse.Frame) verse.Router.Error!void {
var buffer: [0xffffff]u8 = undefined;
const page = try print(&buffer, page_html, .{"page never generated"});
 
try frame.sendHTML(.ok, page);
}
 
fn socket(frame: *verse.Frame) verse.Router.Error!void {
const ws = frame.acceptWebsocket() catch unreachable;
 
for (0..10) |i| {
var buffer: [0xff]u8 = undefined;
ws.send(print(&buffer, "Iteration {}\n", .{i}) catch unreachable) catch unreachable;
std.time.sleep(1_000_000_000);
}
std.debug.print("Socket Example Done\n", .{});
}
 
const page_html =
\\<!DOCTYPE html>
\\<html>
\\<head>
\\ <title>Websocket Example</title>
\\ <style>
\\ html {{ color-scheme: light dark; }}
\\ body {{ width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; }}
\\ </style>
\\ <script>
\\ function retry() {{
\\ socket = new WebSocket("ws://localhost:8088/socket");
\\ socket.onmessage = (event) => {{
\\ console.log(event.data);
\\ }};
\\ }}
\\ retry();
\\</script>
\\</head>
\\<body>
\\<h1>Title</h1>
\\<p>
\\ {s}
\\ <button onclick="retry()"> Connect</button>
\\</p>
\\</body>
\\</html>
\\
;
};
 
pub fn main() !void {
const Endpoints = verse.Endpoints(.{
Root,
});
var endpoints = Endpoints.init(std.heap.page_allocator);
 
endpoints.serve(.{
.mode = .{ .http = .{ .port = 8088 } },
}) catch |err| {
std.log.err("Unable to serve endpoints! err: [{}]", .{err});
@panic("endpoint error");
};
}
 
const std = @import("std");
const verse = @import("verse");
const print = std.fmt.bufPrint;
 
src/frame.zig added: 154, removed: 17, total 137
@@ -180,6 +180,35 @@ pub fn redirect(vrs: *Frame, loc: []const u8, comptime scode: std.http.Status) N
};
}
 
pub fn acceptWebsocket(frame: *Frame) !Websocket {
frame.status = .switching_protocols;
frame.content_type = null;
 
const key = if (frame.request.headers.getCustom("Sec-WebSocket-Key")) |key|
key.value_list.value
else
return error.InvalidWebsocketRequest;
 
var sha1 = std.crypto.hash.Sha1.init(.{});
sha1.update(key);
sha1.update("258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
var digest: [std.crypto.hash.Sha1.digest_length]u8 = undefined;
sha1.final(&digest);
var base64_digest: [28]u8 = undefined;
_ = std.base64.standard.Encoder.encode(&base64_digest, &digest);
 
frame.headersAdd("Upgrade", "websocket") catch unreachable;
frame.headersAdd("Connection", "Upgrade") catch unreachable;
frame.headersAdd("Sec-WebSocket-Accept", base64_digest[0..]) catch unreachable;
frame.sendHeaders() catch |err| switch (err) {
error.BrokenPipe => |e| return e,
else => return error.IOWriteFailure,
};
try frame.sendRawSlice("\r\n");
 
return Websocket{ .frame = frame };
}
 
pub fn init(a: Allocator, req: *const Request, auth: Auth.Provider) !Frame {
return .{
.alloc = a,
@@ -334,6 +363,7 @@ fn flush(vrs: Frame) !void {
fn HTTPHeader(vrs: *Frame) [:0]const u8 {
if (vrs.status == null) vrs.status = .ok;
return switch (vrs.status.?) {
.switching_protocols => "HTTP/1.1 101 Switching Protocols\r\n",
.ok => "HTTP/1.1 200 OK\r\n",
.created => "HTTP/1.1 201 Created\r\n",
.no_content => "HTTP/1.1 204 No Content\r\n",
@@ -388,6 +418,7 @@ const Auth = @import("auth.zig");
const Cookies = @import("cookies.zig");
const ContentType = @import("content-type.zig");
const ResponseData = @import("response-data.zig");
const Websocket = @import("websocket.zig");
 
const Error = @import("errors.zig").Error;
const NetworkError = @import("errors.zig").NetworkError;
 
src/http.zig added: 154, removed: 17, total 137
@@ -64,7 +64,6 @@ fn requestData(a: Allocator, req: *std.http.Server.Request) !Request.Data {
var itr_headers = req.iterateHeaders();
while (itr_headers.next()) |header| {
log.debug("http header => {s} -> {s}", .{ header.name, header.value });
log.debug("{}", .{header});
}
var post_data: ?RequestData.PostData = null;
 
 
src/request.zig added: 154, removed: 17, total 137
@@ -64,7 +64,7 @@ pub const Encoding = packed struct(usize) {
};
};
 
pub const Methods = enum(u8) {
pub const Methods = enum(u9) {
GET = 1,
HEAD = 2,
POST = 4,
@@ -73,6 +73,7 @@ pub const Methods = enum(u8) {
CONNECT = 32,
OPTIONS = 64,
TRACE = 128,
WEBSOCKET = 256,
 
pub fn fromStr(s: []const u8) !Methods {
inline for (std.meta.fields(Methods)) |field| {
@@ -87,7 +88,7 @@ pub const Methods = enum(u8) {
fn initCommon(
a: Allocator,
remote_addr: RemoteAddr,
method: Methods,
_method: Methods,
uri: []const u8,
host: ?Host,
agent: ?UserAgent,
@@ -100,6 +101,12 @@ fn initCommon(
data: Data,
raw: RawReq,
) !Request {
var method = _method;
if (headers.getCustom("Upgrade")) |val| {
std.debug.print("Upgrade: {s}\n", .{val.value_list.value});
method = Methods.WEBSOCKET;
}
 
return .{
.accept = accept,
.accept_encoding = accept_encoding,
 
src/router.zig added: 154, removed: 17, total 137
@@ -50,6 +50,7 @@ pub const Match = struct {
POST: ?Target = null,
PUT: ?Target = null,
TRACE: ?Target = null,
WEBSOCKET: ?Target = null,
};
 
pub fn target(comptime self: Match, comptime req: Request.Methods) ?Target {
@@ -62,6 +63,7 @@ pub const Match = struct {
.POST => self.methods.POST,
.PUT => self.methods.PUT,
.TRACE => self.methods.TRACE,
.WEBSOCKET => self.methods.WEBSOCKET,
};
}
};
@@ -111,6 +113,7 @@ pub fn ROUTE(comptime name: []const u8, comptime match: anytype) Match {
.POST = target,
.PUT = target,
.TRACE = target,
.WEBSOCKET = target,
},
},
};
@@ -199,6 +202,16 @@ pub fn DELETE(comptime name: []const u8, comptime match: BuildFn) Match {
};
}
 
pub fn WEBSOCKET(comptime name: []const u8, comptime match: BuildFn) Match {
return .{
.name = name,
.methods = .{
// TODO .GET?
.WEBSOCKET = buildTarget(match),
},
};
}
 
/// Static file helper that will auto route to the provided directory.
/// Verse normally expects to sit behind an rproxy, that can route requests for
/// static resources without calling Verse. But Verse does have some support for
 
filename was Deleted added: 154, removed: 17, total 137
@@ -0,0 +1,16 @@
frame: *Frame,
 
const Websocket = @This();
 
pub fn send(ws: Websocket, msg: []const u8) !void {
_ = switch (ws.frame.downstream) {
.zwsgi, .http => |stream| try stream.writev(&[2]std.posix.iovec_const{
.{ .base = (&[2]u8{ 0b10000000 | 0x01, @intCast(msg.len & 0x7f) }).ptr, .len = 2 },
.{ .base = msg.ptr, .len = msg.len },
}),
else => {},
};
}
 
const std = @import("std");
const Frame = @import("frame.zig");