2023-04-22 17:26:49 +02:00
|
|
|
const std = @import("std");
|
2023-04-29 12:46:41 +02:00
|
|
|
pub const io_mode: std.io.Mode = .evented;
|
2023-05-03 20:49:51 +02:00
|
|
|
const olderVersion: bool = @import("builtin").zig_version.minor < 11;
|
2023-04-22 17:26:49 +02:00
|
|
|
const eql = std.mem.eql;
|
|
|
|
|
|
|
|
const types = @import("types.zig");
|
|
|
|
const Route = types.Route;
|
|
|
|
const Request = types.Request;
|
|
|
|
const Response = types.Response;
|
2023-04-24 22:21:47 +02:00
|
|
|
const Header = types.Header;
|
|
|
|
const Method = types.Method;
|
|
|
|
const HTTP_Version = types.HTTP_Version;
|
2023-04-22 17:26:49 +02:00
|
|
|
|
2023-04-23 13:16:45 +02:00
|
|
|
/// Server is a namespace to configure IP and Port the app will listen to, as well as
|
|
|
|
/// the routing paths (`[]Route`) it shall handle.
|
|
|
|
/// You can also choose an allocator that the app will use for dynamic memory allocation.
|
2023-04-22 17:26:49 +02:00
|
|
|
pub const Server = struct {
|
2023-04-23 13:16:45 +02:00
|
|
|
pub fn listen(ip: []const u8, port: u16, rt: []const Route, allocator: std.mem.Allocator) !void {
|
2023-04-22 17:26:49 +02:00
|
|
|
|
|
|
|
// Init server
|
|
|
|
const server_options: std.net.StreamServer.Options = .{};
|
|
|
|
var server = std.net.StreamServer.init(server_options);
|
|
|
|
defer server.deinit();
|
|
|
|
const addr = try std.net.Address.parseIp(ip, port);
|
|
|
|
|
2023-04-29 12:59:28 +02:00
|
|
|
try server.listen(addr);
|
2023-04-22 17:26:49 +02:00
|
|
|
|
|
|
|
// Handling connections
|
|
|
|
while (true) {
|
|
|
|
const conn = if (server.accept()) |conn| conn else |_| continue;
|
|
|
|
defer conn.stream.close();
|
|
|
|
|
2023-04-29 12:59:28 +02:00
|
|
|
const client_ip = try std.fmt.allocPrint(allocator, "{}", .{conn.address});
|
2023-04-30 12:48:05 +02:00
|
|
|
defer allocator.free(client_ip);
|
2023-04-29 12:46:41 +02:00
|
|
|
|
2023-05-18 20:44:22 +02:00
|
|
|
var buffer = std.ArrayList(u8).init(allocator);
|
|
|
|
defer buffer.deinit();
|
2023-05-18 12:14:18 +02:00
|
|
|
|
2023-05-18 20:44:22 +02:00
|
|
|
var byte: [1]u8 = undefined;
|
2023-05-17 23:20:35 +02:00
|
|
|
var req: Request = undefined;
|
|
|
|
req.ip = client_ip;
|
2023-05-18 20:44:22 +02:00
|
|
|
req.body = "";
|
|
|
|
// Collect bytes of data from the stream. Then add it
|
2023-05-18 12:14:18 +02:00
|
|
|
// to the ArrayList. Repeat this until all headers of th request end by detecting
|
2023-05-18 20:44:22 +02:00
|
|
|
// appearance of "\r\n\r\n". Then read body if one is sent and if required headers exist and
|
|
|
|
// method is chosen by the client.
|
|
|
|
var headers_finished = false;
|
|
|
|
var content_length: usize = 0;
|
2023-05-19 11:39:00 +02:00
|
|
|
var transfer_encoding_chunked = false;
|
2023-05-18 20:44:22 +02:00
|
|
|
var header_end: usize = 0;
|
|
|
|
var header_string: []const u8 = undefined;
|
2023-04-22 17:26:49 +02:00
|
|
|
while (true) {
|
2023-05-18 20:44:22 +02:00
|
|
|
// Read Request stream
|
|
|
|
_ = try conn.stream.read(&byte);
|
|
|
|
try buffer.appendSlice(&byte);
|
|
|
|
//check if header is finished
|
|
|
|
if (!headers_finished) {
|
|
|
|
if (std.mem.indexOf(u8, buffer.items, "\r\n\r\n")) |header_end_index| {
|
|
|
|
headers_finished = true;
|
|
|
|
header_end = header_end_index;
|
|
|
|
header_string = buffer.items[0..header_end];
|
|
|
|
try buildRequestHeadersAndCookies(&req, header_string, allocator);
|
|
|
|
// Checking Request method and if it is one that can send a body.
|
2023-05-18 20:47:08 +02:00
|
|
|
// If it is one that must not have a body, exit the loop.
|
2023-05-18 20:44:22 +02:00
|
|
|
if (req.method == .GET or req.method == .CONNECT or req.method == .HEAD or req.method == .OPTIONS or req.method == .TRACE) break;
|
2023-04-22 17:26:49 +02:00
|
|
|
|
2023-05-19 12:49:20 +02:00
|
|
|
// If Request has a method that can contain a body, check if Content-Length Header or `Transfer-Encoding: chunked` is set.
|
|
|
|
// `Content-Length` will always be preferred over `Transfer-Encoding`.
|
|
|
|
// If none og these headers is set, exit loop. A Request body will not be accepted.
|
2023-05-18 20:44:22 +02:00
|
|
|
if (req.header("Content-Length")) |length| {
|
|
|
|
content_length = try std.fmt.parseUnsigned(u8, length, 0);
|
2023-05-19 11:39:00 +02:00
|
|
|
} else if (req.header("Transfer-Encoding")) |value| {
|
|
|
|
if (!eql(u8, value, "chunked")) break else transfer_encoding_chunked = true;
|
2023-05-18 20:44:22 +02:00
|
|
|
} else break;
|
|
|
|
}
|
|
|
|
} else {
|
2023-05-19 12:49:20 +02:00
|
|
|
// check how the request body should be read, depending on the relevant header set in the request.
|
|
|
|
// `Content-Length` will always be preferred over `Transfer-Encoding`.
|
2023-05-19 11:39:00 +02:00
|
|
|
if (!transfer_encoding_chunked) {
|
|
|
|
// read body. Check length and add 4 because this is the length of "\r\n\r\n"
|
|
|
|
if (buffer.items.len - header_end >= content_length + 4) {
|
|
|
|
req.body = buffer.items[header_end .. header_end + content_length + 4];
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
} else {
|
2023-05-19 12:49:20 +02:00
|
|
|
// read body until end sequence of chunked encoding is detected at the end of the stream
|
2023-05-19 11:39:00 +02:00
|
|
|
if (std.mem.endsWith(u8, buffer.items, "0\r\n\r\n")) {
|
|
|
|
req.body = buffer.items;
|
|
|
|
break;
|
|
|
|
}
|
2023-05-18 12:14:18 +02:00
|
|
|
}
|
|
|
|
}
|
2023-05-18 20:44:22 +02:00
|
|
|
}
|
|
|
|
defer allocator.free(req.headers);
|
|
|
|
defer allocator.free(req.cookies);
|
2023-05-18 12:14:18 +02:00
|
|
|
|
|
|
|
// PREPARE FOR BUILDING THE RESPONSE
|
2023-04-23 13:16:45 +02:00
|
|
|
// if there ist a path set in the uri trim the trailing slash in order to accept it later during the matching check.
|
|
|
|
if (req.uri.len > 1) req.uri = std.mem.trimRight(u8, req.uri, "/");
|
2023-05-19 14:34:56 +02:00
|
|
|
// Declare new URI variable and cut off a possible request string in order to accept it in a GET Request
|
|
|
|
var uri_parts = std.mem.split(u8, req.uri, "?");
|
|
|
|
const uri_string = uri_parts.first();
|
2023-04-23 13:16:45 +02:00
|
|
|
|
2023-05-18 12:14:18 +02:00
|
|
|
// BUILDING THE RESPONSE
|
2023-04-23 13:16:45 +02:00
|
|
|
// First initialize a notfound Response that is being changed if a Route path matches with Request URI.
|
|
|
|
var res = Response.notfound("");
|
|
|
|
|
|
|
|
// Do the matching check. Iterate over the Routes and change the Response being sent in case of matching.
|
2023-04-22 17:26:49 +02:00
|
|
|
for (rt) |r| {
|
2023-04-23 13:16:45 +02:00
|
|
|
var req_path = r[0];
|
|
|
|
// Trim a possible trailing slash from Route path in order to accept it during the matching process.
|
|
|
|
if (req_path.len > 1) req_path = std.mem.trimRight(u8, req_path, "/");
|
2023-04-24 14:48:39 +02:00
|
|
|
// Check if there is a match
|
2023-05-19 14:34:56 +02:00
|
|
|
if (eql(u8, req_path, uri_string)) {
|
2023-04-24 14:48:39 +02:00
|
|
|
// Change response with handling function in case of match.
|
2023-05-02 21:48:18 +02:00
|
|
|
res = r[1](&req);
|
2023-04-24 14:48:39 +02:00
|
|
|
// Exit loop in case of match
|
|
|
|
break;
|
2023-04-22 17:26:49 +02:00
|
|
|
}
|
|
|
|
}
|
2023-04-28 21:53:30 +02:00
|
|
|
// Stringify the Response.
|
2023-04-24 22:21:47 +02:00
|
|
|
const response_string = try stringifyResponse(res, allocator);
|
2023-04-28 21:53:30 +02:00
|
|
|
// Free memory after writing Response and sending it to client.
|
|
|
|
defer allocator.free(response_string);
|
2023-05-19 14:34:56 +02:00
|
|
|
// SENDING THE RESPONSE
|
2023-04-28 21:53:30 +02:00
|
|
|
// Write stringified Response and send it to client.
|
2023-04-24 22:21:47 +02:00
|
|
|
_ = try conn.stream.write(response_string);
|
2023-04-22 17:26:49 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
2023-04-23 13:16:45 +02:00
|
|
|
|
2023-05-17 23:20:35 +02:00
|
|
|
// Function that build the Request headers and cookies from stream
|
|
|
|
fn buildRequestHeadersAndCookies(req: *Request, bytes: []const u8, allocator: std.mem.Allocator) !void {
|
|
|
|
var header_lines = std.mem.split(u8, bytes, "\r\n");
|
2023-04-24 22:21:47 +02:00
|
|
|
var header_buffer = std.ArrayList(Header).init(allocator);
|
2023-05-15 17:33:51 +02:00
|
|
|
var cookie_buffer = std.ArrayList(Request.Cookie).init(allocator);
|
2023-04-24 22:21:47 +02:00
|
|
|
|
|
|
|
var header_items = std.mem.split(u8, header_lines.first(), " ");
|
|
|
|
req.method = Method.parse(header_items.first());
|
|
|
|
req.uri = if (header_items.next()) |value| value else "";
|
|
|
|
|
|
|
|
if (header_items.next()) |value| {
|
|
|
|
req.httpVersion = HTTP_Version.parse(value);
|
|
|
|
} else {
|
|
|
|
req.httpVersion = HTTP_Version.HTTP1_1;
|
|
|
|
}
|
|
|
|
|
|
|
|
while (header_lines.next()) |line| {
|
2023-05-17 21:29:13 +02:00
|
|
|
var headers = std.mem.split(u8, line, ":");
|
2023-04-24 22:21:47 +02:00
|
|
|
const item1 = headers.first();
|
2023-05-15 17:33:51 +02:00
|
|
|
// Check if header is a cookie and parse it
|
|
|
|
if (eql(u8, item1, "Cookie") or eql(u8, item1, "cookie")) {
|
|
|
|
const item2 = if (headers.next()) |value| value else "";
|
|
|
|
const cookies = try Request.Cookie.parse(item2, allocator);
|
|
|
|
defer allocator.free(cookies);
|
|
|
|
try cookie_buffer.appendSlice(cookies);
|
|
|
|
continue;
|
|
|
|
}
|
2023-05-17 21:29:13 +02:00
|
|
|
const item2 = if (headers.next()) |value| std.mem.trim(u8, value, " ") else "";
|
2023-04-24 22:21:47 +02:00
|
|
|
const header_pair = Header{ .key = item1, .value = item2 };
|
|
|
|
try header_buffer.append(header_pair);
|
|
|
|
}
|
2023-05-17 23:20:35 +02:00
|
|
|
|
2023-05-15 17:33:51 +02:00
|
|
|
req.cookies = if (olderVersion) cookie_buffer.toOwnedSlice() else try cookie_buffer.toOwnedSlice();
|
2023-05-03 20:49:51 +02:00
|
|
|
req.headers = if (olderVersion) header_buffer.toOwnedSlice() else try header_buffer.toOwnedSlice();
|
2023-04-24 22:21:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Test the Request build function
|
|
|
|
test "build a Request" {
|
2023-04-28 21:53:30 +02:00
|
|
|
const allocator = std.testing.allocator;
|
2023-05-17 23:20:35 +02:00
|
|
|
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!";
|
|
|
|
var parts = std.mem.split(u8, stream, "\r\n\r\n");
|
2023-04-29 12:59:28 +02:00
|
|
|
const client_ip = "127.0.0.1";
|
2023-05-17 23:20:35 +02:00
|
|
|
const headers = parts.first();
|
|
|
|
const body = parts.next().?;
|
|
|
|
var req: Request = undefined;
|
|
|
|
req.body = body;
|
|
|
|
req.ip = client_ip;
|
|
|
|
try buildRequestHeadersAndCookies(&req, headers, allocator);
|
2023-04-28 21:53:30 +02:00
|
|
|
defer allocator.free(req.headers);
|
2023-05-17 21:29:13 +02:00
|
|
|
defer allocator.free(req.cookies);
|
2023-04-24 22:21:47 +02:00
|
|
|
try std.testing.expect(req.method == Method.GET);
|
|
|
|
try std.testing.expect(req.httpVersion == HTTP_Version.HTTP1_1);
|
|
|
|
try std.testing.expect(std.mem.eql(u8, req.uri, "/test"));
|
|
|
|
try std.testing.expect(std.mem.eql(u8, req.headers[1].key, "User-Agent"));
|
|
|
|
try std.testing.expect(std.mem.eql(u8, req.headers[1].value, "Testbot"));
|
|
|
|
try std.testing.expect(std.mem.eql(u8, req.headers[0].key, "Host"));
|
2023-05-17 21:29:13 +02:00
|
|
|
try std.testing.expect(std.mem.eql(u8, req.headers[0].value, "localhost"));
|
2023-04-24 22:21:47 +02:00
|
|
|
try std.testing.expect(std.mem.eql(u8, req.body, "This is the test body!"));
|
2023-05-17 21:29:13 +02:00
|
|
|
try std.testing.expect(std.mem.eql(u8, req.cookies[0].name, "Test-Cookie"));
|
|
|
|
try std.testing.expect(std.mem.eql(u8, req.cookies[0].value, "Test"));
|
2023-04-24 22:21:47 +02:00
|
|
|
}
|
|
|
|
|
2023-04-23 13:16:45 +02:00
|
|
|
// Function that turns Response into a string
|
2023-04-24 22:21:47 +02:00
|
|
|
fn stringifyResponse(r: Response, allocator: std.mem.Allocator) ![]const u8 {
|
2023-04-23 13:16:45 +02:00
|
|
|
var res = std.ArrayList(u8).init(allocator);
|
|
|
|
try res.appendSlice(r.httpVersion.stringify());
|
|
|
|
try res.append(' ');
|
|
|
|
try res.appendSlice(r.status.stringify());
|
|
|
|
try res.appendSlice("\r\n");
|
2023-05-10 21:19:33 +02:00
|
|
|
// Add headers
|
2023-04-23 13:16:45 +02:00
|
|
|
for (r.headers) |header| {
|
2023-04-28 21:53:30 +02:00
|
|
|
try res.appendSlice(header.key);
|
|
|
|
try res.appendSlice(": ");
|
|
|
|
try res.appendSlice(header.value);
|
2023-05-17 21:29:13 +02:00
|
|
|
try res.appendSlice("\r\n");
|
2023-04-23 13:16:45 +02:00
|
|
|
}
|
2023-05-10 21:19:33 +02:00
|
|
|
// Add cookie-headers
|
|
|
|
for (r.cookies) |cookie| {
|
|
|
|
const c = try cookie.stringify(allocator);
|
|
|
|
defer allocator.free(c);
|
2023-05-17 21:29:13 +02:00
|
|
|
if (!eql(u8, cookie.name, "") and !eql(u8, cookie.value, "")) {
|
|
|
|
try res.appendSlice(c);
|
|
|
|
try res.appendSlice("\r\n");
|
|
|
|
}
|
2023-05-10 21:19:33 +02:00
|
|
|
}
|
2023-05-17 21:29:13 +02:00
|
|
|
try res.appendSlice("\r\n");
|
2023-04-23 13:16:45 +02:00
|
|
|
try res.appendSlice(r.body);
|
|
|
|
|
2023-05-03 20:49:51 +02:00
|
|
|
return if (olderVersion) res.toOwnedSlice() else try res.toOwnedSlice();
|
2023-04-23 13:16:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
test "stringify Response" {
|
2023-04-28 21:53:30 +02:00
|
|
|
const allocator = std.testing.allocator;
|
2023-04-23 13:16:45 +02:00
|
|
|
const headers = [_]types.Header{.{ .key = "User-Agent", .value = "Testbot" }};
|
|
|
|
const res = Response{ .headers = &headers, .body = "This is the body!" };
|
2023-04-28 21:53:30 +02:00
|
|
|
const res_str = try stringifyResponse(res, allocator);
|
|
|
|
defer allocator.free(res_str);
|
2023-05-17 23:20:35 +02:00
|
|
|
try std.testing.expect(eql(u8, res_str, "HTTP/1.1 200 OK\r\nUser-Agent: Testbot\r\n\r\nThis is the body!"));
|
2023-04-23 13:16:45 +02:00
|
|
|
}
|