├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon ├── gyro.zzz ├── src ├── req_cookie.zig ├── res_cookie.zig ├── server.zig ├── status.zig ├── types.zig └── zerve.zig └── zig.mod /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache/ 2 | zig-cache/ 3 | zig-out/ 4 | .gyro/ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 flopetautschnig 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zerve 2 | 3 | A simple framework for writing web services in zig. 4 | 5 | * [Create a simple Web App](#create-a-simple-web-app) 6 | * [Types](#types) 7 | * [Route](#route) 8 | * [Handler Functions](#handler-functions) 9 | * [Request](#request) 10 | * [Get Query Params](#get-query-params) 11 | * [Get Header Value by Key](#get-value-of-request-header-by-key) 12 | * [Response](#response) 13 | * [Header](#header) 14 | * [Cookies](#cookies) 15 | * [Read Cookie from Request](#read-cookie-from-request) 16 | * [Add Cookie to Response](#add-cookie-to-response) 17 | * [Method](#method) 18 | * [HTTP-Version](#http-version) 19 | * [Namespaces](#namespaces) 20 | * [Server](#server) 21 | 22 | # Create a simple web app 23 | 24 | ```zig 25 | const zrv = @import("zerve"); // Or set the path to zerve.zig e.g. @import("zerve-main/src/zerve.zig"); 26 | const Request = zrv.Request; 27 | const Response = zrv.Response; 28 | const Server = zrv.Server; 29 | const Route = zrv.Route; 30 | const allocator = std.heap.page_allocator; // Choose any allocator you want! 31 | 32 | fn index(req: *Request) Response { 33 | _=req; 34 | return Response.write("hello!"); 35 | } 36 | 37 | fn about(req: *Request) Response { 38 | _=req; 39 | return Response.write("about site"); 40 | } 41 | 42 | fn writeJson(req: *Request) Response { 43 | _=req; 44 | return Response.json("[1, 2, 3, 4]"); 45 | } 46 | 47 | pub fn main() !void { 48 | const rt = [_]Route{.{"/", index}, .{"/about", about}, .{"/json", writeJson}}; 49 | 50 | try Server.listen("0.0.0.0", 8080, &rt, allocator); // listens to http://localhost:8080 51 | // http://localhost:8080/ "hello!" 52 | // http://localhost:8080/about "about site" 53 | // http://localhost:8080/json "[1, 2, 3, 4]" (JSON-Response) 54 | } 55 | ``` 56 | 57 | # Types 58 | 59 | ## Route 60 | 61 | To write a web service with **zerve** you have to configure one or more Routes. They are being set by creating an Array of `Route`. 62 | 63 | Example: 64 | ```zig 65 | const rt = [_]Route{.{"/hello", helloFunction}, "/about", aboutFunction}; 66 | ``` 67 | You can also set only one path and link it to a handler function, but since `Server.listen()` takes an Array of `Route` as one of it's arguments, 68 | you have do declare it as an Array as well: 69 | ```zig 70 | const rt = [_]Route{.{"/hello", helloFunction}}; 71 | ``` 72 | 73 | ## Handler Functions 74 | 75 | Every Request is handled by a handler function. It has to be of this type: `fn(req: *Request) Response` 76 | 77 | Example: 78 | ```zig 79 | fn hello(req: *Request) Response { 80 | _ = req; 81 | return Response.write("hello"); // `Server` will return a Reponse with body "hello". You will see "hello" on your browser. 82 | } 83 | ``` 84 | 85 | ## Request 86 | 87 | This represents the Request sent by the client. 88 | ```zig 89 | pub const Request = struct { 90 | /// The Request Method, e.g. "GET" 91 | method: Method, 92 | /// HTTP-Version of the Request sent by the client 93 | httpVersion: HTTP_Version, 94 | /// Represents the request headers sent by the client 95 | headers: []const Header, 96 | /// The Request URI 97 | uri: []const u8, 98 | /// Represents the request body sent by the client 99 | body: []const u8, 100 | }; 101 | ``` 102 | 103 | ### Get Query Params 104 | 105 | **zerve** lets you easily extract query params no matter if `Request`method is `GET`or `POST`. 106 | 107 | This can be done by using the `getQuery` method of `Request`. 108 | 109 | Example: 110 | ```zig 111 | fn index(req: Request) Response { 112 | 113 | // Assuming that a query string has been sent by the client containing the requested param, 114 | // e.g. `?user=james` 115 | 116 | const user = req.getQuery("user"); // This will return an optional 117 | 118 | if (user == null) return Response.write("") else return Response.write(user.?); 119 | 120 | } 121 | ``` 122 | 123 | ### Get value of Request header by key 124 | 125 | You can get the header value of any sent header by the client with the `header`method of `Request`. 126 | 127 | Example: 128 | 129 | ```zig 130 | fn index(req: *Request) Response { 131 | 132 | // Get value of the 'Content-Type' header 133 | 134 | const h = req.header("Content-Type"); // This will return an optional 135 | 136 | if (h == null) return Response.write("Header not found!") else return Response.write(h.?); 137 | 138 | } 139 | ``` 140 | 141 | ## Response 142 | 143 | A Response that is sent ny the server. Every handler function has to return a `Response`. 144 | ```zig 145 | pub const Response = struct { 146 | httpVersion: HTTP_Version = HTTP_Version.HTTP1_1, 147 | /// Response status, default is "200 OK" 148 | status: stat.Status = stat.Status.OK, 149 | /// Response eaders sent by the server 150 | headers: []const Header = &[_]Header{.{ .key = "Content-Type", .value = "text/html; charset=utf-8" }}, 151 | /// Response body sent by the server 152 | body: []const u8 = "", 153 | 154 | /// Write a simple response. 155 | pub fn write(s: []const u8) Response 156 | 157 | /// Send a response with json content. 158 | pub fn json(j: []const u8) Response 159 | 160 | /// Send a response with status not found. 161 | pub fn notfound(s: []const u8) Response 162 | 163 | /// Send a response with status forbidden. 164 | pub fn forbidden(s: []u8) Response 165 | }; 166 | ``` 167 | 168 | ## Header 169 | 170 | Every Request or Response has Headers represented by an Array of Headers. Every Header has a key and a value. 171 | ```zig 172 | pub const Header = struct { 173 | key: []const u8, 174 | value: []const u8, 175 | }; 176 | ``` 177 | 178 | ## Cookies 179 | 180 | ### Read Cookie from Request 181 | 182 | To read the Cookie of a request by key, `Request` has a `cookie`-method. 183 | It returns an optional and fetches the value of a `Request.Cookie`. 184 | 185 | Get Request Cookie value by key: 186 | ```zig 187 | fn index(req: *Request) Response { 188 | 189 | // Fetches the cookie value by cookie name. 190 | // The `cookie` method will return an optional and will be `null` 191 | // in case that the cookie does not exist. 192 | 193 | const cookie = if (req.cookie("password")) |password| password else ""; 194 | 195 | return Response.write("cookie-test"); 196 | } 197 | ``` 198 | 199 | ### Add Cookie to Response 200 | 201 | To send a cookie in your `Response` just add a `Response.Cookie` to the `cookies` field. 202 | The `cookies` field is a slice of `Response.Cookie`. 203 | 204 | ```zig 205 | fn index(_: *Request) Response { 206 | 207 | // Define a cookie with name and value. 208 | // It will live for 24 hours, since `maxAge` represents 209 | // lifetime in seconds. 210 | // See all field of the `Response.Cookie` struct below. 211 | 212 | const cookie = Response.Cookie{.name="User", .value="James", .maxAge=60*60*24}; 213 | 214 | var res = Response.write("Set Cookie!"); 215 | // add cookie to the `cookies` field which is a slice of `Response.Cookie` 216 | res.cookies = &[_]Response.Cookie{.{cookie}}; 217 | 218 | return res; 219 | } 220 | ``` 221 | 222 | This are the fields of `Response.Cookie`: 223 | 224 | ```zig 225 | name: []const u8, 226 | value: []const u8, 227 | path: []const u8 = "/", 228 | domain: []const u8 = "", 229 | /// Indicates the number of seconds until the cookie expires. 230 | maxAge: i64 = 0, 231 | secure: bool = true, 232 | httpOnly: bool = true, 233 | sameSite: SameSite = .lax, 234 | ``` 235 | 236 | ## Method 237 | 238 | Represents the http method of a Request or a Response. 239 | ```zig 240 | pub const Method = enum { 241 | GET, 242 | POST, 243 | PUT, 244 | HEAD, 245 | DELETE, 246 | CONNECT, 247 | OPTIONS, 248 | TRACE, 249 | PATCH, 250 | UNKNOWN, 251 | 252 | /// Turns the HTTP_method into a u8-Slice. 253 | pub fn stringify(m: Method) []const u8 {...} 254 | }; 255 | ``` 256 | 257 | ## HTTP-Version 258 | 259 | The HTTP-Version of a Request or a Response. 260 | ```zig 261 | pub const HTTP_Version = enum { 262 | HTTP1_1, 263 | HTTP2, 264 | 265 | /// Parses from `[]u8` 266 | pub fn parse(s: []const u8) HTTP_Version {...} 267 | 268 | /// Stringifies `HTTP_Version` 269 | pub fn stringify(version: HTTP_Version) []const u8 {...} 270 | 271 | }; 272 | ``` 273 | 274 | # Namespaces 275 | 276 | ## Server 277 | 278 | Server is a namespace to configure IP and Port the app will listen to by calling `Server.listen()`, as well as the routing paths (`[]Route`) it shall handle. 279 | You can also choose an allocator that the app will use for dynamic memory allocation. 280 | ```zig 281 | pub fn listen(ip: []const u8, port: u16, rt: []const Route, allocator: std.mem.Allocator) !void {...} 282 | ``` 283 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.Build) void { 4 | const target = b.standardTargetOptions(.{}); 5 | 6 | const optimize = b.standardOptimizeOption(.{}); 7 | 8 | _ = b.addModule("zerve", .{ 9 | .root_source_file = b.path("src/zerve.zig"), 10 | }); 11 | 12 | const lib = b.addStaticLibrary(.{ 13 | .name = "zerve", 14 | .root_source_file = b.path("src/zerve.zig"), 15 | .target = target, 16 | .optimize = optimize, 17 | }); 18 | 19 | b.installArtifact(lib); 20 | 21 | const main_tests = b.addTest(.{ 22 | .root_source_file = b.path("src/zerve.zig"), 23 | .target = target, 24 | .optimize = optimize, 25 | }); 26 | 27 | const test_step = b.step("test", "Run library tests"); 28 | test_step.dependOn(&main_tests.step); 29 | } 30 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = "zerve", 3 | .version = "0.0.23", 4 | .minimum_zig_version = "0.12.0", 5 | .paths = .{ 6 | "build.zig", 7 | "build.zig.zon", 8 | "src", 9 | "LICENSE", 10 | "README.md", 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /gyro.zzz: -------------------------------------------------------------------------------- 1 | pkgs: 2 | zerve: 3 | version: 0.0.23 4 | license: MIT 5 | description: Simple web framework for zig 6 | source_url: "https://github.com/floscodes/zerve" 7 | root: src/zerve.zig 8 | files: 9 | README.md 10 | LICENSE 11 | zig.mod 12 | build.zig 13 | src/*.zig 14 | tags: 15 | http 16 | server 17 | framework 18 | http-server 19 | server-side 20 | -------------------------------------------------------------------------------- /src/req_cookie.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const trim = std.mem.trim; 3 | const split = std.mem.split; 4 | const olderVersion: bool = @import("builtin").zig_version.minor < 11; 5 | 6 | pub const Cookie = struct { 7 | name: []const u8, 8 | value: []const u8, 9 | 10 | pub fn parse(item2: []const u8, allocator: std.mem.Allocator) ![]const Cookie { 11 | var items = split(u8, item2, ";"); 12 | var cookie_buffer = std.ArrayList(Cookie).init(allocator); 13 | var cookie_string = split(u8, items.first(), "="); 14 | const first_cookie = Cookie{ .name = trim(u8, cookie_string.first(), " "), .value = trim(u8, cookie_string.next().?, " ") }; 15 | try cookie_buffer.append(first_cookie); 16 | 17 | while (items.next()) |item| { 18 | cookie_string = split(u8, item, "="); 19 | const name = trim(u8, cookie_string.first(), " "); 20 | const value = if (cookie_string.next()) |v| trim(u8, v, " ") else ""; 21 | const cookie = Cookie{ .name = name, .value = value }; 22 | try cookie_buffer.append(cookie); 23 | } 24 | return if (olderVersion) cookie_buffer.toOwnedSlice() else try cookie_buffer.toOwnedSlice(); 25 | } 26 | }; 27 | 28 | test "Parse Request Cookie(s)" { 29 | const allocator = std.testing.allocator; 30 | const cookie_string = "Test-Cookie=successful; Second-Cookie=also successful"; 31 | const cookie = try Cookie.parse(cookie_string, allocator); 32 | defer allocator.free(cookie); 33 | try std.testing.expect(std.mem.eql(u8, cookie[0].value, "successful")); 34 | try std.testing.expect(std.mem.eql(u8, cookie[0].name, "Test-Cookie")); 35 | try std.testing.expect(std.mem.eql(u8, cookie[1].name, "Second-Cookie")); 36 | try std.testing.expect(std.mem.eql(u8, cookie[1].value, "also successful")); 37 | } 38 | -------------------------------------------------------------------------------- /src/res_cookie.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const types = @import("types.zig"); 3 | 4 | pub const Cookie = struct { 5 | name: []const u8, 6 | value: []const u8, 7 | path: []const u8 = "/", 8 | domain: []const u8 = "", 9 | /// Indicates the number of seconds until the cookie expires. 10 | maxAge: i64 = 0, 11 | secure: bool = true, 12 | httpOnly: bool = true, 13 | sameSite: SameSite = .lax, 14 | 15 | pub fn stringify(self: Cookie, allocator: std.mem.Allocator) ![]const u8 { 16 | const domain = if (std.mem.eql(u8, self.domain, "")) self.domain else try std.fmt.allocPrint(allocator, "Domain={s}; ", .{self.domain}); 17 | defer allocator.free(domain); 18 | const secure = if (self.secure) "Secure; " else ""; 19 | const httpOnly = if (self.httpOnly) "HttpOnly; " else ""; 20 | return try std.fmt.allocPrint(allocator, "Set-Cookie: {s}={s}; Path={s}; {s}Max-Age={}; {s}{s}{s}", .{ self.name, self.value, self.path, domain, self.maxAge, secure, httpOnly, getSameSite(&self) }); 21 | } 22 | }; 23 | 24 | pub const SameSite = enum { 25 | lax, 26 | strict, 27 | none, 28 | }; 29 | 30 | pub fn getSameSite(c: *const Cookie) []const u8 { 31 | switch (c.sameSite) { 32 | .lax => return "SameSite=Lax;", 33 | .strict => return "SameSite=Strict;", 34 | .none => return "SameSite=None;", 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/server.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | pub const io_mode: std.io.Mode = .evented; 3 | const olderVersion: bool = @import("builtin").zig_version.minor < 11; 4 | const eql = std.mem.eql; 5 | 6 | const types = @import("types.zig"); 7 | const Route = types.Route; 8 | const Request = types.Request; 9 | const Response = types.Response; 10 | const Header = types.Header; 11 | const Method = types.Method; 12 | const HTTP_Version = types.HTTP_Version; 13 | 14 | /// Server is a namespace to configure IP and Port the app will listen to, as well as 15 | /// the routing paths (`[]Route`) it shall handle. 16 | /// You can also choose an allocator that the app will use for dynamic memory allocation. 17 | pub const Server = struct { 18 | pub fn listen(ip: []const u8, port: u16, rt: []const Route, allocator: std.mem.Allocator) !void { 19 | 20 | // Init server 21 | const addr = try std.net.Address.parseIp(ip, port); 22 | var server = try addr.listen(.{}); 23 | defer server.deinit(); 24 | 25 | // Handling connections 26 | while (true) { 27 | const conn = if (server.accept()) |conn| conn else |_| continue; 28 | defer conn.stream.close(); 29 | 30 | const client_ip = try std.fmt.allocPrint(allocator, "{}", .{conn.address}); 31 | defer allocator.free(client_ip); 32 | 33 | var buffer = std.ArrayList(u8).init(allocator); 34 | defer buffer.deinit(); 35 | 36 | var byte: [1]u8 = undefined; 37 | var req: Request = undefined; 38 | req.ip = client_ip; 39 | req.body = ""; 40 | // Collect bytes of data from the stream. Then add it 41 | // to the ArrayList. Repeat this until all headers of th request end by detecting 42 | // appearance of "\r\n\r\n". Then read body if one is sent and if required headers exist and 43 | // method is chosen by the client. 44 | var headers_finished = false; 45 | var content_length: usize = 0; 46 | var transfer_encoding_chunked = false; 47 | var header_end: usize = 0; 48 | var header_string: []const u8 = undefined; 49 | while (true) { 50 | // Read Request stream 51 | _ = try conn.stream.read(&byte); 52 | try buffer.appendSlice(&byte); 53 | //check if header is finished 54 | if (!headers_finished) { 55 | if (std.mem.indexOf(u8, buffer.items, "\r\n\r\n")) |header_end_index| { 56 | headers_finished = true; 57 | header_end = header_end_index; 58 | header_string = buffer.items[0..header_end]; 59 | try buildRequestHeadersAndCookies(&req, header_string, allocator); 60 | // Checking Request method and if it is one that can send a body. 61 | // If it is one that must not have a body, exit the loop. 62 | if (req.method == .GET or req.method == .CONNECT or req.method == .HEAD or req.method == .OPTIONS or req.method == .TRACE) break; 63 | 64 | // If Request has a method that can contain a body, check if Content-Length Header or `Transfer-Encoding: chunked` is set. 65 | // `Content-Length` will always be preferred over `Transfer-Encoding`. 66 | // If none og these headers is set, exit loop. A Request body will not be accepted. 67 | if (req.header("Content-Length")) |length| { 68 | content_length = try std.fmt.parseUnsigned(u8, length, 0); 69 | } else if (req.header("Transfer-Encoding")) |value| { 70 | if (!eql(u8, value, "chunked")) break else transfer_encoding_chunked = true; 71 | } else break; 72 | } 73 | } else { 74 | // check how the request body should be read, depending on the relevant header set in the request. 75 | // `Content-Length` will always be preferred over `Transfer-Encoding`. 76 | if (!transfer_encoding_chunked) { 77 | // read body. Check length and add 4 because this is the length of "\r\n\r\n" 78 | if (buffer.items.len - header_end >= content_length + 4) { 79 | req.body = buffer.items[header_end .. header_end + content_length + 4]; 80 | break; 81 | } 82 | } else { 83 | // read body until end sequence of chunked encoding is detected at the end of the stream 84 | if (std.mem.endsWith(u8, buffer.items, "0\r\n\r\n")) { 85 | req.body = buffer.items; 86 | break; 87 | } 88 | } 89 | } 90 | } 91 | defer allocator.free(req.headers); 92 | defer allocator.free(req.cookies); 93 | 94 | // PREPARE FOR BUILDING THE RESPONSE 95 | // if there ist a path set in the uri trim the trailing slash in order to accept it later during the matching check. 96 | if (req.uri.len > 1) req.uri = std.mem.trimRight(u8, req.uri, "/"); 97 | // Declare new URI variable and cut off a possible request string in order to accept it in a GET Request 98 | var uri_parts = std.mem.split(u8, req.uri, "?"); 99 | const uri_string = uri_parts.first(); 100 | 101 | // BUILDING THE RESPONSE 102 | // First initialize a notfound Response that is being changed if a Route path matches with Request URI. 103 | var res = Response.notfound(""); 104 | 105 | // Do the matching check. Iterate over the Routes and change the Response being sent in case of matching. 106 | for (rt) |r| { 107 | var req_path = r[0]; 108 | // Trim a possible trailing slash from Route path in order to accept it during the matching process. 109 | if (req_path.len > 1) req_path = std.mem.trimRight(u8, req_path, "/"); 110 | // Check if there is a match 111 | if (eql(u8, req_path, uri_string)) { 112 | // Change response with handling function in case of match. 113 | res = r[1](&req); 114 | // Exit loop in case of match 115 | break; 116 | } 117 | } 118 | // Stringify the Response. 119 | const response_string = try stringifyResponse(res, allocator); 120 | // Free memory after writing Response and sending it to client. 121 | defer allocator.free(response_string); 122 | // SENDING THE RESPONSE 123 | // Write stringified Response and send it to client. 124 | _ = try conn.stream.write(response_string); 125 | } 126 | } 127 | }; 128 | 129 | // Function that build the Request headers and cookies from stream 130 | fn buildRequestHeadersAndCookies(req: *Request, bytes: []const u8, allocator: std.mem.Allocator) !void { 131 | var header_lines = std.mem.split(u8, bytes, "\r\n"); 132 | var header_buffer = std.ArrayList(Header).init(allocator); 133 | var cookie_buffer = std.ArrayList(Request.Cookie).init(allocator); 134 | 135 | var header_items = std.mem.split(u8, header_lines.first(), " "); 136 | req.method = Method.parse(header_items.first()); 137 | req.uri = if (header_items.next()) |value| value else ""; 138 | 139 | if (header_items.next()) |value| { 140 | req.httpVersion = HTTP_Version.parse(value); 141 | } else { 142 | req.httpVersion = HTTP_Version.HTTP1_1; 143 | } 144 | 145 | while (header_lines.next()) |line| { 146 | var headers = std.mem.split(u8, line, ":"); 147 | const item1 = headers.first(); 148 | // Check if header is a cookie and parse it 149 | if (eql(u8, item1, "Cookie") or eql(u8, item1, "cookie")) { 150 | const item2 = if (headers.next()) |value| value else ""; 151 | const cookies = try Request.Cookie.parse(item2, allocator); 152 | defer allocator.free(cookies); 153 | try cookie_buffer.appendSlice(cookies); 154 | continue; 155 | } 156 | const item2 = if (headers.next()) |value| std.mem.trim(u8, value, " ") else ""; 157 | const header_pair = Header{ .key = item1, .value = item2 }; 158 | try header_buffer.append(header_pair); 159 | } 160 | 161 | req.cookies = if (olderVersion) cookie_buffer.toOwnedSlice() else try cookie_buffer.toOwnedSlice(); 162 | req.headers = if (olderVersion) header_buffer.toOwnedSlice() else try header_buffer.toOwnedSlice(); 163 | } 164 | 165 | // Test the Request build function 166 | test "build a Request" { 167 | const allocator = std.testing.allocator; 168 | const stream = "GET /test HTTP/1.1\r\nHost: localhost\r\nUser-Agent: Testbot\r\nCookie: Test-Cookie=Test\r\n\r\nThis is the test body!"; 169 | var parts = std.mem.split(u8, stream, "\r\n\r\n"); 170 | const client_ip = "127.0.0.1"; 171 | const headers = parts.first(); 172 | const body = parts.next().?; 173 | var req: Request = undefined; 174 | req.body = body; 175 | req.ip = client_ip; 176 | try buildRequestHeadersAndCookies(&req, headers, allocator); 177 | defer allocator.free(req.headers); 178 | defer allocator.free(req.cookies); 179 | try std.testing.expect(req.method == Method.GET); 180 | try std.testing.expect(req.httpVersion == HTTP_Version.HTTP1_1); 181 | try std.testing.expect(std.mem.eql(u8, req.uri, "/test")); 182 | try std.testing.expect(std.mem.eql(u8, req.headers[1].key, "User-Agent")); 183 | try std.testing.expect(std.mem.eql(u8, req.headers[1].value, "Testbot")); 184 | try std.testing.expect(std.mem.eql(u8, req.headers[0].key, "Host")); 185 | try std.testing.expect(std.mem.eql(u8, req.headers[0].value, "localhost")); 186 | try std.testing.expect(std.mem.eql(u8, req.body, "This is the test body!")); 187 | try std.testing.expect(std.mem.eql(u8, req.cookies[0].name, "Test-Cookie")); 188 | try std.testing.expect(std.mem.eql(u8, req.cookies[0].value, "Test")); 189 | } 190 | 191 | // Function that turns Response into a string 192 | fn stringifyResponse(r: Response, allocator: std.mem.Allocator) ![]const u8 { 193 | var res = std.ArrayList(u8).init(allocator); 194 | try res.appendSlice(r.httpVersion.stringify()); 195 | try res.append(' '); 196 | try res.appendSlice(r.status.stringify()); 197 | try res.appendSlice("\r\n"); 198 | // Add headers 199 | for (r.headers) |header| { 200 | try res.appendSlice(header.key); 201 | try res.appendSlice(": "); 202 | try res.appendSlice(header.value); 203 | try res.appendSlice("\r\n"); 204 | } 205 | // Add cookie-headers 206 | for (r.cookies) |cookie| { 207 | const c = try cookie.stringify(allocator); 208 | defer allocator.free(c); 209 | if (!eql(u8, cookie.name, "") and !eql(u8, cookie.value, "")) { 210 | try res.appendSlice(c); 211 | try res.appendSlice("\r\n"); 212 | } 213 | } 214 | try res.appendSlice("\r\n"); 215 | try res.appendSlice(r.body); 216 | 217 | return if (olderVersion) res.toOwnedSlice() else try res.toOwnedSlice(); 218 | } 219 | 220 | test "stringify Response" { 221 | const allocator = std.testing.allocator; 222 | const headers = [_]types.Header{.{ .key = "User-Agent", .value = "Testbot" }}; 223 | const res = Response{ .headers = &headers, .body = "This is the body!" }; 224 | const res_str = try stringifyResponse(res, allocator); 225 | defer allocator.free(res_str); 226 | try std.testing.expect(eql(u8, res_str, "HTTP/1.1 200 OK\r\nUser-Agent: Testbot\r\n\r\nThis is the body!")); 227 | } 228 | -------------------------------------------------------------------------------- /src/status.zig: -------------------------------------------------------------------------------- 1 | /// Representing the HTTP status 2 | pub const Status = enum(u32) { 3 | // INFORMATION RESPONSES 4 | 5 | CONTINUE = 100, 6 | SWITCHING_PROTOCOLS = 101, 7 | PROCESSING = 102, 8 | EARLY_HINTS = 103, 9 | 10 | // SUCCESSFUL RESPONSES 11 | OK = 200, 12 | CREATED = 201, 13 | ACCEPTED = 202, 14 | NON_AUTHORATIVE_INFORMATION = 203, 15 | NO_CONTENT = 204, 16 | RESET_CONTENT = 205, 17 | PARTIAL_CONTENT = 206, 18 | MULTI_STATUS = 207, 19 | ALREADY_REPORTED = 208, 20 | IM_USED = 226, 21 | 22 | // REDIRECTION MESSAGES 23 | MULTIPLE_CHOICES = 300, 24 | MOVED_PERMANENTLY = 301, 25 | FOUND = 302, 26 | SEE_OTHER = 303, 27 | NOT_MODIFIED = 304, 28 | USE_PROXY = 305, 29 | SWITCH_PROXY = 306, 30 | TEMPORARY_REDIRECT = 307, 31 | PERMANENT_REDIRECT = 308, 32 | 33 | // CLIENT ERROR RESPONSES 34 | BAD_REQUEST = 400, 35 | UNAUTHORIZED = 401, 36 | PAYMENT_REQUIRED = 402, 37 | FORBIDDEN = 403, 38 | NOT_FOUND = 404, 39 | METHOD_NOT_ALLOWED = 405, 40 | NOT_ACCEPTABLE = 406, 41 | PROXY_AUTHENTICATION_REQUIRED = 407, 42 | REQUEST_TIMEOUT = 408, 43 | CONFLICT = 409, 44 | GONE = 410, 45 | LENGTH_REQUIRED = 411, 46 | PRECONDITION_FAILED = 412, 47 | PAYLOAD_TOO_LARGE = 213, 48 | URI_TOO_LONG = 414, 49 | UNSUPPORTED_MEDIA_TYPE = 415, 50 | RANGE_NOT_SATISIFIABLE = 416, 51 | EXPECTATION_FAILED = 417, 52 | I_AM_A_TEAPOT = 418, 53 | MISDIRECTED_REQUEST = 421, 54 | UNPROCESSABLE_CONTENT = 422, 55 | LOCKED = 423, 56 | FAILED_DEPENDENCY = 424, 57 | TOO_EARLY = 425, 58 | UPGRADE_REQUIRED = 426, 59 | PRECONDITION_REQUIRED = 428, 60 | TOO_MANY_REQUESTS = 429, 61 | REQUESTS_HEADER_FIELDS_TOO_LARGE = 431, 62 | UNAVAILABLE_FOR_LEGAL_REASONS = 451, 63 | 64 | // SERVER RESPONSES 65 | INTERNAL_SERVER_ERROR = 500, 66 | NOT_IMPLEMETED = 501, 67 | BAD_GATEWAY = 502, 68 | SERVUCE_UNAVAILABLE = 503, 69 | GATEWAY_TIMEOUT = 504, 70 | HTTP_VERSION_NOT_SUPPORTED = 505, 71 | VARIANT_ALSO_NEGOTIATES = 506, 72 | INSUFFICIENT_STORAGE = 507, 73 | LOOP_DETECTED = 508, 74 | NOT_EXTENDED = 510, 75 | NETWORK_AUTHENTICATION_REQUIRED = 511, 76 | 77 | /// Returns a stringified version of a HTTP status. 78 | /// E.g. `Status.OK.stringify()` will be "200 OK". 79 | pub fn stringify(self: Status) []const u8 { 80 | switch (self) { 81 | Status.CONTINUE => return "100 Continue", 82 | Status.SWITCHING_PROTOCOLS => return "101 Switching Protocols", 83 | Status.PROCESSING => return "102 Processing", 84 | Status.EARLY_HINTS => return "103 Early Hints", 85 | Status.OK => return "200 OK", 86 | Status.CREATED => return "201 Created", 87 | Status.ACCEPTED => return "202 Accepted", 88 | Status.NON_AUTHORATIVE_INFORMATION => return "203 Non-Authorative Information", 89 | Status.NO_CONTENT => return "204 No Content", 90 | Status.RESET_CONTENT => return "205 Reset Content", 91 | Status.PARTIAL_CONTENT => return "206 Partial Content", 92 | Status.MULTI_STATUS => return "207 Multi-Status", 93 | Status.ALREADY_REPORTED => return "208 Already Reported", 94 | Status.IM_USED => return "226 IM Used", 95 | Status.MULTIPLE_CHOICES => return "300 Multiple Choices", 96 | Status.MOVED_PERMANENTLY => return "301 Moved Permanently", 97 | Status.FOUND => return "302 Found", 98 | Status.SEE_OTHER => return "303 See Other", 99 | Status.NOT_MODIFIED => return "304 Not Modified", 100 | Status.USE_PROXY => return "305 Use Proxy", 101 | Status.SWITCH_PROXY => return "306 Switch Proxy", 102 | Status.TEMPORARY_REDIRECT => return "307 Temporary Redirect", 103 | Status.PERMANENT_REDIRECT => return "308 Permanent Redirect", 104 | Status.BAD_REQUEST => return "400 Bad Request", 105 | Status.UNAUTHORIZED => return "401 Unauthorized", 106 | Status.PAYMENT_REQUIRED => return "402 Payment Required", 107 | Status.FORBIDDEN => return "403 Forbidden", 108 | Status.NOT_FOUND => return "404 Not Found", 109 | Status.METHOD_NOT_ALLOWED => return "405 Method Not Allowed", 110 | Status.NOT_ACCEPTABLE => return "406 Not Acceptable", 111 | Status.PROXY_AUTHENTICATION_REQUIRED => return "407 Proxy Authentication Required", 112 | Status.REQUEST_TIMEOUT => return "408 Request Timeout", 113 | Status.CONFLICT => return "409 Conflict", 114 | Status.GONE => return "410 Gone", 115 | Status.LENGTH_REQUIRED => return "411 Length Required", 116 | Status.PRECONDITION_FAILED => return "412 Precondition Failed", 117 | Status.PAYLOAD_TOO_LARGE => return "413 Payload Too Large", 118 | Status.URI_TOO_LONG => return "414 URI Too Long", 119 | Status.UNSUPPORTED_MEDIA_TYPE => return "415 Unsupported Media Type", 120 | Status.RANGE_NOT_SATISIFIABLE => return "416 Range Not Satisfiable", 121 | Status.EXPECTATION_FAILED => return "417 Expectation Failed", 122 | Status.I_AM_A_TEAPOT => return "418 I'm a teapot", 123 | Status.MISDIRECTED_REQUEST => return "421 Misdirected Request", 124 | Status.UNPROCESSABLE_CONTENT => return "422 Unprocessable Content", 125 | Status.LOCKED => return "423 Locked", 126 | Status.FAILED_DEPENDENCY => return "424 Failed Dependency", 127 | Status.TOO_EARLY => return "425 Too Early", 128 | Status.UPGRADE_REQUIRED => return "426 Upgrade Required", 129 | Status.PRECONDITION_REQUIRED => return "428 Precondition Required", 130 | Status.TOO_MANY_REQUESTS => return "429 Too Many Requests", 131 | Status.REQUESTS_HEADER_FIELDS_TOO_LARGE => return "431 Request Header Fields Too Large", 132 | Status.UNAVAILABLE_FOR_LEGAL_REASONS => return "451 Unavailable For Legal Reasons", 133 | Status.INTERNAL_SERVER_ERROR => return "500 Internal Server Error", 134 | Status.NOT_IMPLEMETED => return "501 Not Implemented", 135 | Status.BAD_GATEWAY => return "502 Bad Gateway", 136 | Status.SERVUCE_UNAVAILABLE => return "503 Service Unavailable", 137 | Status.GATEWAY_TIMEOUT => return "504 Gateway Timeout", 138 | Status.HTTP_VERSION_NOT_SUPPORTED => return "505 HTTP Version Not Supported", 139 | Status.VARIANT_ALSO_NEGOTIATES => return "506 Variant Also Negotiates", 140 | Status.INSUFFICIENT_STORAGE => return "507 Insufficient Storage", 141 | Status.LOOP_DETECTED => return "508 Loop Detected", 142 | Status.NOT_EXTENDED => return "510 Not Extended", 143 | Status.NETWORK_AUTHENTICATION_REQUIRED => return "511 Network Authentication Required", 144 | } 145 | } 146 | 147 | /// Parses a given u32 code and returns the corresponding `Status`. 148 | /// E.g. `Status.code(200)` will return `Status.OK`. 149 | /// The program will panic if the passed code does not exist. 150 | pub fn code(n: u32) Status { 151 | return @enumFromInt(n); 152 | } 153 | }; 154 | -------------------------------------------------------------------------------- /src/types.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const tuple = std.meta.Tuple; 3 | const allocator = std.heap.page_allocator; 4 | const eql = std.mem.eql; 5 | const stat = @import("./status.zig"); 6 | const rescookie = @import("./res_cookie.zig"); 7 | const reqcookie = @import("./req_cookie.zig"); 8 | 9 | /// Route is a touple that consists of the path and the function that shall handle it. 10 | /// e.g. `const rt = Route{"/home", home};` 11 | /// It it usual that a webapp handles more than one path so you can declare an array of `Route` 12 | /// e.g. `const rt =[_]Route{.{"/index", index}, .{"/home", home}};` 13 | pub const Route = tuple(&.{ []const u8, *const fn (*Request) Response }); 14 | 15 | /// A header of a `Request` or a `Response`. 16 | /// It is usual that more than one is sent, so you can declare an array. 17 | pub const Header = struct { 18 | key: []const u8, 19 | value: []const u8, 20 | }; 21 | 22 | /// The HTTP Version. 23 | pub const HTTP_Version = enum { 24 | HTTP1_1, 25 | HTTP2, 26 | 27 | /// Parses from `[]u8` 28 | pub fn parse(s: []const u8) HTTP_Version { 29 | if (std.mem.containsAtLeast(u8, s, 1, "2")) return HTTP_Version.HTTP2 else return HTTP_Version.HTTP1_1; 30 | } 31 | /// Stringifies `HTTP_Version` 32 | pub fn stringify(version: HTTP_Version) []const u8 { 33 | switch (version) { 34 | HTTP_Version.HTTP1_1 => return "HTTP/1.1", 35 | HTTP_Version.HTTP2 => return "HTTP/2.0", 36 | } 37 | } 38 | }; 39 | 40 | /// Represents the Method of a request or a response. 41 | pub const Method = enum { 42 | GET, 43 | POST, 44 | PUT, 45 | HEAD, 46 | DELETE, 47 | CONNECT, 48 | OPTIONS, 49 | TRACE, 50 | PATCH, 51 | UNKNOWN, 52 | 53 | /// Parses the Method from a string 54 | pub fn parse(value: []const u8) Method { 55 | if (eql(u8, value, "GET") or eql(u8, value, "get")) return Method.GET; 56 | if (eql(u8, value, "POST") or eql(u8, value, "post")) return Method.POST; 57 | if (eql(u8, value, "PUT") or eql(u8, value, "put")) return Method.PUT; 58 | if (eql(u8, value, "HEAD") or eql(u8, value, "head")) return Method.HEAD; 59 | if (eql(u8, value, "DELETE") or eql(u8, value, "delete")) return Method.DELETE; 60 | if (eql(u8, value, "CONNECT") or eql(u8, value, "connect")) return Method.CONNECT; 61 | if (eql(u8, value, "OPTIONS") or eql(u8, value, "options")) return Method.OPTIONS; 62 | if (eql(u8, value, "TRACE") or eql(u8, value, "trace")) return Method.TRACE; 63 | if (eql(u8, value, "PATCH") or eql(u8, value, "patch")) return Method.PATCH; 64 | return Method.UNKNOWN; 65 | } 66 | 67 | /// Turns the HTTP_method into a u8-Slice. 68 | pub fn stringify(m: Method) []const u8 { 69 | switch (m) { 70 | Method.GET => return "GET", 71 | Method.POST => return "POST", 72 | Method.PUT => return "PUT", 73 | Method.PATCH => return "PATCH", 74 | Method.DELETE => return "DELETE", 75 | Method.HEAD => return "HEAD", 76 | Method.CONNECT => return "CONNECT", 77 | Method.OPTIONS => return "OPTIONS", 78 | Method.TRACE => return "TRACE", 79 | Method.UNKNOWN => return "UNKNOWN", 80 | } 81 | } 82 | }; 83 | 84 | /// Represents a standard http-Request sent by the client. 85 | pub const Request = struct { 86 | /// The Request Method, e.g. "GET" 87 | method: Method, 88 | /// HTTP-Version of the Request sent by the client 89 | httpVersion: HTTP_Version, 90 | /// Represents the client's IP-Address. 91 | ip: []const u8, 92 | /// Represents the request headers sent by the client 93 | headers: []const Header, 94 | /// Request Cookies 95 | cookies: []const Cookie, 96 | /// The Request URI 97 | uri: []const u8, 98 | /// Represents the request body sent by the client 99 | body: []const u8, 100 | /// Represents a Request Cookie 101 | pub const Cookie = reqcookie.Cookie; 102 | 103 | /// Get Request Cookie value by Cookie name 104 | pub fn cookie(self: *Request, name: []const u8) ?[]const u8 { 105 | for (self.*.cookies) |c| { 106 | if (eql(u8, name, c.name)) return c.value; 107 | } 108 | return null; 109 | } 110 | 111 | /// Get Header value by Header key 112 | pub fn header(self: *Request, key: []const u8) ?[]const u8 { 113 | for (self.*.headers) |h| { 114 | if (eql(u8, key, h.key)) return h.value; 115 | } 116 | return null; 117 | } 118 | 119 | /// Get query value by query key 120 | pub fn getQuery(self: *Request, key_needle: []const u8) ?[]const u8 { 121 | var query_string: ?[]const u8 = null; 122 | if (self.method == .GET) { 123 | var parts = std.mem.split(u8, self.uri, "?"); 124 | _ = parts.first(); 125 | query_string = parts.next(); 126 | } 127 | if (self.method == .POST) { 128 | query_string = self.body; 129 | } 130 | if (query_string == null) return null; 131 | var pairs = std.mem.split(u8, query_string.?, "&"); 132 | const first_pair = pairs.first(); 133 | var items = std.mem.split(u8, first_pair, "="); 134 | var key = items.first(); 135 | if (eql(u8, key_needle, key)) { 136 | if (items.next()) |value| return value; 137 | } 138 | while (pairs.next()) |pair| { 139 | items = std.mem.split(u8, pair, "="); 140 | key = items.first(); 141 | if (eql(u8, key_needle, key)) { 142 | if (items.next()) |value| return value; 143 | } 144 | } 145 | return null; 146 | } 147 | 148 | test "get Query" { 149 | var req: Request = undefined; 150 | req.uri = "/about/?user=james&password=1234"; // Write query string in uri after '?' 151 | req.method = .GET; 152 | var user = if (req.getQuery("user")) |v| v else ""; 153 | var pwd = if (req.getQuery("password")) |v| v else ""; 154 | var n = req.getQuery("nothing"); // This key does not exist in query string 155 | 156 | try std.testing.expect(eql(u8, user, "james")); 157 | try std.testing.expect(eql(u8, pwd, "1234")); 158 | try std.testing.expect(n == null); 159 | 160 | // Change method an write query string into body 161 | req.body = "user=james&password=1234"; 162 | req.method = .POST; 163 | 164 | user = if (req.getQuery("user")) |v| v else ""; 165 | pwd = if (req.getQuery("password")) |v| v else ""; 166 | n = req.getQuery("nothing"); // This key does not exist in query string 167 | 168 | try std.testing.expect(eql(u8, user, "james")); 169 | try std.testing.expect(eql(u8, pwd, "1234")); 170 | try std.testing.expect(n == null); 171 | } 172 | }; 173 | 174 | /// Represents a standard http-Response sent by the webapp (server). 175 | /// It is the return type of every handling function. 176 | pub const Response = struct { 177 | httpVersion: HTTP_Version = HTTP_Version.HTTP1_1, 178 | /// Response status, default is "200 OK" 179 | status: stat.Status = stat.Status.OK, 180 | /// Response eaders sent by the server 181 | headers: []const Header = &[_]Header{.{ .key = "Content-Type", .value = "text/html; charset=utf-8" }}, 182 | /// Cookies to be sent 183 | cookies: []const Cookie = &[_]Cookie{.{ .name = "", .value = "" }}, 184 | /// Response body sent by the server 185 | body: []const u8 = "", 186 | 187 | /// Write a simple response. 188 | pub fn write(s: []const u8) Response { 189 | return Response{ .body = s }; 190 | } 191 | 192 | /// Send a response with json content. 193 | pub fn json(j: []const u8) Response { 194 | return Response{ .headers = &[_]Header{.{ .key = "Content-Type", .value = "application/json" }}, .body = j }; 195 | } 196 | 197 | /// Send a response with status not found. 198 | pub fn notfound(s: []const u8) Response { 199 | return Response{ .status = stat.Status.NOT_FOUND, .body = s }; 200 | } 201 | 202 | /// Send a response with status forbidden. 203 | pub fn forbidden(s: []u8) Response { 204 | return Response{ .status = stat.Status.FORBIDDEN, .body = s }; 205 | } 206 | /// Represents the Response Cookie. 207 | pub const Cookie = rescookie.Cookie; 208 | }; 209 | 210 | // Run all tests, even the nested ones 211 | test { 212 | std.testing.refAllDecls(@This()); 213 | } 214 | -------------------------------------------------------------------------------- /src/zerve.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const types = @import("types.zig"); 3 | const status = @import("status.zig"); 4 | const server = @import("server.zig"); 5 | 6 | pub const Server = server.Server; 7 | pub const Route = types.Route; 8 | pub const Header = types.Header; 9 | pub const Request = types.Request; 10 | pub const Response = types.Response; 11 | pub const Method = types.Method; 12 | pub const HTTP_Version = types.HTTP_Version; 13 | 14 | test "Test zerve test-app, run server and serve test page" { 15 | // Set route 16 | const rt = [_]types.Route{.{ "/", handlefn }}; 17 | try Server.listen("0.0.0.0", 8080, &rt, std.testing.allocator); 18 | } 19 | // Function for test 20 | fn handlefn(req: *types.Request) types.Response { 21 | const alloc = std.testing.allocator; 22 | // collect headers of Request 23 | var headers = std.ArrayList(u8).init(alloc); 24 | defer headers.deinit(); 25 | for (req.headers) |header| { 26 | headers.appendSlice(header.key) catch {}; 27 | headers.appendSlice(": ") catch {}; 28 | headers.appendSlice(header.value) catch {}; 29 | headers.appendSlice("\n") catch {}; 30 | } 31 | // collect cookies of Request 32 | var cookies = std.ArrayList(u8).init(alloc); 33 | defer cookies.deinit(); 34 | for (req.cookies) |cookie| { 35 | cookies.appendSlice(cookie.name) catch {}; 36 | cookies.appendSlice(" = ") catch {}; 37 | cookies.appendSlice(cookie.value) catch {}; 38 | cookies.appendSlice("\n") catch {}; 39 | } 40 | const res_string = std.fmt.allocPrint(alloc, "

Run Server Test OK!


URI: {s}


Sent headers:


{s}

Sent Cookies:


{s}

Request body:


{s}", .{ req.uri, headers.items, cookies.items, req.body }) catch "Memory error"; 41 | const res = types.Response{ .body = res_string, .cookies = &[_]Response.Cookie{.{ .name = "Test-Cookie", .value = "Test", .maxAge = 60 * 3 }} }; 42 | return res; 43 | } 44 | -------------------------------------------------------------------------------- /zig.mod: -------------------------------------------------------------------------------- 1 | name: zerve 2 | main: src/zerve.zig 3 | license: MIT 4 | description: Simple web framework for zig 5 | min_zig_version: 0.12.0 6 | dependencies: 7 | --------------------------------------------------------------------------------