Skip to content

Instantly share code, notes, and snippets.

@joedavis
Created July 10, 2022 16:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joedavis/7e9fb40b61b5c8a69b8697f79e064204 to your computer and use it in GitHub Desktop.
Save joedavis/7e9fb40b61b5c8a69b8697f79e064204 to your computer and use it in GitHub Desktop.
Barebones HTTP implementation in Zig
const std = @import("std");
pub const Headers = std.StringHashMap([]const u8);
pub const StatusCode = enum(u32) {
@"continue" = 100,
switching_protocols = 101,
processing = 102,
early_hints = 103,
ok = 200,
created = 201,
accepted = 202,
non_authorative_information = 203,
no_content = 204,
reset_content = 205,
partial_content = 206,
multi_status = 207,
already_reported = 208,
im_used = 226,
multiple_choices = 300,
moved_permanently = 301,
found = 302,
see_other = 303,
not_modified = 304,
use_proxy = 305,
unused = 306,
temporary_redirect = 307,
permanent_redirect = 308,
bad_request = 400,
unauthorized = 401,
payment_required = 402,
forbidden = 403,
not_found = 404,
method_not_allowed = 405,
not_acceptable = 406,
proxy_authentication = 407,
request_timeout = 408,
conflict = 409,
gone = 410,
length_required = 411,
precondition_failed = 412,
payload_too_large = 413,
uri_too_long = 414,
unsupported_media_type = 415,
range_not_satisfiable = 416,
expectation_failed = 417,
im_a_teapot = 418,
misdirected_request = 421,
unprocessable_entity = 422,
locked = 423,
failed_dependency = 424,
too_early = 425,
upgrade_required = 426,
precondition_required = 428,
too_many_requests = 429,
request_header_fields_too_large = 431,
unavailable_for_legal_reasons = 451,
internal_server_error = 500,
not_implemented = 501,
bad_gateway = 502,
service_unavailable = 503,
gateway_timeout = 504,
http_version_not_supported = 505,
variant_also_negotiates = 506,
insufficient_storage = 507,
loop_detected = 508,
not_extended = 510,
network_authenticaiton_required = 511,
};
pub const reason_phrases: std.enums.EnumMap(StatusCode, []const u8) = blk: {
@setEvalBranchQuota(10000);
var map = std.enums.EnumMap(StatusCode, []const u8){};
for (@typeInfo(StatusCode).Enum.fields) |enum_field| {
var upcased_name = [_]u8{0} ** (enum_field.name.len);
var new_word: bool = true;
for (enum_field.name) |c, i| {
if (new_word) {
upcased_name[i] = std.ascii.toUpper(c);
new_word = false;
} else if (c == '_') {
upcased_name[i] = ' ';
new_word = true;
} else {
upcased_name[i] = c;
}
}
map.put(@intToEnum(StatusCode, enum_field.value), &upcased_name);
}
break :blk map;
};
pub const Method = enum {
GET,
HEAD,
POST,
PUT,
DELETE,
CONNECT,
OPTIONS,
TRACE,
PATCH,
};
pub const ParseError = error{FailedParse} || std.mem.Allocator.Error;
pub const CRLF = "\r\n";
pub const Request = struct {
method: Method,
path: []const u8,
headers: Headers,
body: []const u8,
fn parseMethod(r: *Request, request_text: []const u8) ParseError![]const u8 {
var first_space = std.mem.indexOfScalar(u8, request_text, ' ') orelse return error.FailedParse;
var method = request_text[0..first_space];
r.method = std.meta.stringToEnum(Method, method) orelse return error.FailedParse;
if (first_space + 1 > request_text.len) {
return error.FailedParse;
}
return request_text[first_space + 1 ..];
}
fn parsePath(r: *Request, request_text: []const u8) ParseError![]const u8 {
const end_of_path = std.mem.indexOfScalar(u8, request_text, ' ') orelse std.mem.indexOf(u8, request_text, CRLF) orelse return error.FailedParse;
r.path = request_text[0..end_of_path];
if (end_of_path + 1 > request_text.len) {
return error.FailedParse;
}
return request_text[end_of_path + 1 ..];
}
fn skipToNextLine(request_text: []const u8) ParseError![]const u8 {
const next_line = std.mem.indexOf(u8, request_text, CRLF) orelse return error.FailedParse;
if (next_line + 2 > request_text.len) {
return error.FailedParse;
}
return request_text[next_line + 2 ..];
}
fn parseHeaders(r: *Request, request_text: []const u8) ParseError![]const u8 {
var remaining = request_text;
while (remaining.len > 0 and remaining[0] != '\r') {
const header_name_end = std.mem.indexOfScalar(u8, remaining, ':') orelse return error.FailedParse;
const header_name = remaining[0..header_name_end];
remaining = remaining[header_name_end + 2 ..];
const header_value_end = std.mem.indexOf(u8, remaining, CRLF) orelse return error.FailedParse;
const header_value = remaining[0..header_value_end];
try r.headers.put(header_name, header_value);
remaining = remaining[header_value_end + 2 ..];
}
return remaining;
}
pub fn parse(allocator: std.mem.Allocator, request_text: []const u8) ParseError!Request {
var r = Request{
.method = undefined,
.path = undefined,
.body = undefined,
.headers = Headers.init(allocator),
};
errdefer r.headers.deinit();
var remaining = try r.parseMethod(request_text);
remaining = try r.parsePath(remaining);
remaining = try skipToNextLine(remaining);
remaining = try r.parseHeaders(remaining);
r.body = try skipToNextLine(remaining);
return r;
}
pub fn deinit(r: *Request) void {
r.headers.deinit();
}
};
pub const Response = struct {
headers: Headers,
status_code: StatusCode,
pub fn init(allocator: std.mem.Allocator) Response {
var self = Response{
.headers = Headers.init(allocator),
.status_code = .ok,
};
return self;
}
pub fn writeHeaders(self: *Response, writer: anytype) !void {
try std.fmt.format(writer, "HTTP/1.1 {} {s}\r\n", .{ @enumToInt(self.status_code), reason_phrases.get(self.status_code) });
var iter = self.headers.iterator();
while (iter.next()) |header| {
try std.fmt.format(writer, "{s}: {s}\r\n", .{ header.key_ptr.*, header.value_ptr.* });
}
try writer.writeAll(CRLF);
}
pub fn deinit(self: *Response) void {
self.headers.deinit();
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment