diff --git a/demo/src/app/views/websockets.zig b/demo/src/app/views/websockets.zig new file mode 100644 index 0000000..414c068 --- /dev/null +++ b/demo/src/app/views/websockets.zig @@ -0,0 +1,85 @@ +const std = @import("std"); +const jetzig = @import("jetzig"); + +pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View { + _ = data; + return request.render(.ok); +} + +pub fn get(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View { + _ = data; + _ = id; + return request.render(.ok); +} + +pub fn post(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View { + _ = data; + return request.render(.created); +} + +pub fn put(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View { + _ = data; + _ = id; + return request.render(.ok); +} + +pub fn patch(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View { + _ = data; + _ = id; + return request.render(.ok); +} + +pub fn delete(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View { + _ = data; + _ = id; + return request.render(.ok); +} + + +test "index" { + var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); + defer app.deinit(); + + const response = try app.request(.GET, "/websockets", .{}); + try response.expectStatus(.ok); +} + +test "get" { + var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); + defer app.deinit(); + + const response = try app.request(.GET, "/websockets/example-id", .{}); + try response.expectStatus(.ok); +} + +test "post" { + var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); + defer app.deinit(); + + const response = try app.request(.POST, "/websockets", .{}); + try response.expectStatus(.created); +} + +test "put" { + var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); + defer app.deinit(); + + const response = try app.request(.PUT, "/websockets/example-id", .{}); + try response.expectStatus(.ok); +} + +test "patch" { + var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); + defer app.deinit(); + + const response = try app.request(.PATCH, "/websockets/example-id", .{}); + try response.expectStatus(.ok); +} + +test "delete" { + var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); + defer app.deinit(); + + const response = try app.request(.DELETE, "/websockets/example-id", .{}); + try response.expectStatus(.ok); +} diff --git a/demo/src/app/views/websockets/index.zmpl b/demo/src/app/views/websockets/index.zmpl new file mode 100644 index 0000000..e00e9ec --- /dev/null +++ b/demo/src/app/views/websockets/index.zmpl @@ -0,0 +1,17 @@ +@if (context.request) |request| + @if (request.headers.get("host")) |host| + + @end +@end diff --git a/src/jetzig/http.zig b/src/jetzig/http.zig index 5137903..011e1f2 100644 --- a/src/jetzig/http.zig +++ b/src/jetzig/http.zig @@ -13,6 +13,7 @@ pub const Response = @import("http/Response.zig"); pub const Session = @import("http/Session.zig"); pub const Cookies = @import("http/Cookies.zig"); pub const Headers = @import("http/Headers.zig"); +pub const Websocket = @import("http/Websocket.zig"); pub const Query = @import("http/Query.zig"); pub const MultipartQuery = @import("http/MultipartQuery.zig"); pub const File = @import("http/File.zig"); diff --git a/src/jetzig/http/Headers.zig b/src/jetzig/http/Headers.zig index 83978e3..3d719a7 100644 --- a/src/jetzig/http/Headers.zig +++ b/src/jetzig/http/Headers.zig @@ -41,6 +41,12 @@ pub fn get(self: Headers, name: []const u8) ?[]const u8 { return self.httpz_headers.get(lower); } +/// Get the first value for a given header identified by `name`, which is assumed to be lower case. +pub fn getLower(self: Headers, name: []const u8) ?[]const u8 { + std.debug.assert(name.len <= max_bytes_header_name); + return self.httpz_headers.get(name); +} + /// Get all values for a given header identified by `name`. Names are case insensitive. pub fn getAll(self: Headers, name: []const u8) []const []const u8 { var headers = std.ArrayList([]const u8).init(self.allocator); diff --git a/src/jetzig/http/Request.zig b/src/jetzig/http/Request.zig index 4e31d3e..b0a89ac 100644 --- a/src/jetzig/http/Request.zig +++ b/src/jetzig/http/Request.zig @@ -26,6 +26,7 @@ allocator: std.mem.Allocator, path: jetzig.http.Path, method: Method, headers: jetzig.http.Headers, +host: []const u8, server: *jetzig.http.Server, httpz_request: *httpz.Request, httpz_response: *httpz.Response, @@ -146,12 +147,15 @@ pub fn init( const response_data = try allocator.create(jetzig.data.Data); response_data.* = jetzig.data.Data.init(allocator); + const headers = jetzig.http.Headers.init(allocator, httpz_request.headers); + const host = headers.getLower("host") orelse ""; return .{ .allocator = allocator, .path = path, .method = method, - .headers = jetzig.http.Headers.init(allocator, httpz_request.headers), + .headers = headers, + .host = host, .server = server, .response = response, .response_data = response_data, diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig index 9fd9ce1..f783983 100644 --- a/src/jetzig/http/Server.zig +++ b/src/jetzig/http/Server.zig @@ -61,10 +61,12 @@ pub fn deinit(self: *Server) void { self.allocator.free(self.env.bind); } -const Dispatcher = struct { +const HttpzHandler = struct { server: *Server, - pub fn handle(self: Dispatcher, request: *httpz.Request, response: *httpz.Response) void { + pub const WebsocketHandler = jetzig.http.Websocket; + + pub fn handle(self: HttpzHandler, request: *httpz.Request, response: *httpz.Response) void { self.server.processNextRequest(request, response) catch |err| { self.server.errorHandlerFn(request, response, err) catch {}; }; @@ -77,7 +79,7 @@ pub fn listen(self: *Server) !void { const worker_count = jetzig.config.get(u16, "worker_count"); const thread_count: u16 = jetzig.config.get(?u16, "thread_count") orelse @intCast(try std.Thread.getCpuCount()); - var httpz_server = try httpz.Server(Dispatcher).init( + var httpz_server = try httpz.Server(HttpzHandler).init( self.allocator, .{ .port = self.env.port, @@ -96,7 +98,7 @@ pub fn listen(self: *Server) !void { .max_body_size = jetzig.config.get(usize, "max_bytes_request_body"), }, }, - Dispatcher{ .server = self }, + HttpzHandler{ .server = self }, ); defer httpz_server.deinit(); @@ -139,6 +141,11 @@ pub fn processNextRequest( var repo = try self.repo.bindConnect(.{ .allocator = httpz_response.arena }); defer repo.release(); + if (try self.upgradeWebsocket(httpz_request, httpz_response)) { + try self.logger.DEBUG("Websocket upgrade request successful.", .{}); + return; + } + var response = try jetzig.http.Response.init(httpz_response.arena, httpz_response); var request = try jetzig.http.Request.init( httpz_response.arena, @@ -169,6 +176,14 @@ pub fn processNextRequest( try self.logger.logRequest(&request); } +fn upgradeWebsocket(self: *const Server, httpz_request: *httpz.Request, httpz_response: *httpz.Response) !bool { + return try httpz.upgradeWebsocket( + jetzig.http.Websocket, + httpz_request, + httpz_response, + jetzig.http.Websocket.Context{ .allocator = self.allocator }, + ); +} fn maybeMiddlewareRender(request: *jetzig.http.Request, response: *const jetzig.http.Response) !bool { if (request.middleware_rendered) |_| { // Request processing ends when a middleware renders or redirects. diff --git a/src/jetzig/http/Websocket.zig b/src/jetzig/http/Websocket.zig new file mode 100644 index 0000000..fb815d1 --- /dev/null +++ b/src/jetzig/http/Websocket.zig @@ -0,0 +1,24 @@ +const std = @import("std"); + +const httpz = @import("httpz"); + +pub const Context = struct { + allocator: std.mem.Allocator, +}; + +const Websocket = @This(); + +connection: *httpz.websocket.Conn, +allocator: std.mem.Allocator, + +pub fn init(connection: *httpz.websocket.Conn, context: Context) !Websocket { + return .{ + .connection = connection, + .allocator = context.allocator, + }; +} + +pub fn clientMessage(self: *Websocket, data: []const u8) !void { + const message = try std.mem.concat(self.allocator, u8, &.{ "Hello from Jetzig websocket. Your message was: ", data }); + try self.connection.write(message); +}