ALPHA: URL parser

This commit is contained in:
lost+skunk 2025-02-07 01:45:14 +03:00
parent 04f918a27a
commit ca1662c403
6 changed files with 264 additions and 200 deletions

View file

@ -0,0 +1,168 @@
// module source;
/*
lost+skunk <git.macaw.me/skunky>, 2025;
Licensed under WTFPL
*/
import core.stdc.stdio;
import core.sys.linux.epoll;
import core.sys.linux.netinet.tcp;
import core.sys.posix.netinet.in_;
import core.sys.posix.unistd;
import core.sys.posix.sys.socket;
import uselesshttpd.util;
struct Response {
short status = 200;
void[] body;
string[string] headers;
string mimetype = "application/octet-stream";
}
struct Server {
string address;
short port;
private shared int epfd, sock;
void start(MD...)() { // TODO: реализовать "слушальщика" хрюникс сокетов (AF_UNIX)
bool ipv6;
for (short i; i < address.length; ++i)
if (address[i] == ':') {ipv6=true;break;}
if (ipv6) {
sock = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in6 sockt;
sockt.sin6_family = AF_INET6;
sockt.sin6_port = htons(port);
inet_pton(AF_INET6, cast(char*)address, &sockt.sin6_addr);
err(bind(sock, cast(sockaddr*)&sockt, sockt.sizeof), "bind");
} else {
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in sockt;
sockt.sin_family = AF_INET;
sockt.sin_port = htons(port);
sockt.sin_addr.s_addr = inet_addr(cast(char*)address);
err(bind(sock, cast(sockaddr*)&sockt, sockt.sizeof), "bind");
}
setsockopt(sock,
IPPROTO_TCP, TCP_NODELAY | SO_REUSEADDR | SO_REUSEPORT,
cast(void*)(new int), int.sizeof);
err(listen(sock, 512), "listen");
serve!MD;
}
void stop() {
epfd.close();
sock.close();
}
void serve(T...)() {
epfd = epoll_create(MAX_CLIENTS);
epoll_event ev;
epoll_event[MAX_EVENTS] evts;
ev.data.fd = sock;
ev.events = EPOLLIN | EPOLLEXCLUSIVE;
epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ev);
for (;;) {
auto w = epoll_wait(epfd, &evts[0], MAX_EVENTS, -1);
for (int i = 0; i < w; ++i) {
auto fd = evts[i].data.fd;
if (fd == sock) { // TODO: добавить поддержку ipv6
// sockaddr_in addr;
// socklen_t al = sockaddr_in.sizeof;
// ev.data.fd = accept4(sock, cast(sockaddr*)&addr, &al, SOCK_NONBLOCK);
ev.data.fd = accept4(sock, null, null, SOCK_NONBLOCK);
epoll_ctl(epfd, EPOLL_CTL_ADD, ev.data.fd, &ev);
// ip = cast(string)inet_ntoa(addr.sin_addr);
}
// if (ev.data.fd == -1) {
rd:
ubyte[128] buf;
for (;;) { // обработчик запросов
auto rd = recv(fd, cast(void*)buf, buf.sizeof, 0);
if (rd < 1) break;
auto rqst = parseReq(buf);
static foreach (mm; __traits(allMembers, T)) {
static if (__traits(isStaticFunction, __traits(getMember, T, mm))) {
foreach(attr; __traits(getAttributes, __traits(getMember, T, mm))) {
static if (is(typeof(attr) == Location)) {
parseAndValidateURL(rqst.path, &rqst);
if (rqst.path == attr.path) {
Response rsp;
__traits(getMember, T, mm)(&rsp, &rqst);
char[] headers;
foreach(header,content;rsp.headers)
headers ~= "\r\n" ~ header ~ ": " ~ content;
auto response =
"HTTP/1.1 " ~ intToStr(rsp.status) ~ ' ' ~ getStatus(rsp.status)
~ "\r\nContent-length: " ~ intToStr(rsp.body.length)
~ "\r\nContent-Type: " ~ rsp.mimetype
~ headers
~ "\r\n\r\n"
~ rsp.body;
write(fd, cast(void*)response, response.length);
goto rd;
} else continue;
}
}
}
}
static auto resp = "HTTP/1.1 404 Not Found\r\nContent-length: 13\r\n\r\n404 Not Found";
write(fd, cast(void*)resp, resp.length);
}
}
}
}
Request parseReq(ubyte[] body) { // TODO: реализовать парсинг заголовков, оформленных хер пойми как
int prev;
short[] xxx;
Request req;
for (short i; i < body.length; ++i) {
if (body[i] == '\r') {
auto splitted = body[prev..i];
for (short x = 1; x < splitted.length; ++x) {
if (prev == 0) { // для прочего говна (метода, пути и протокола)
if (splitted[x] == ' ') xxx ~= x;
else if (xxx.length == 2) {
req.method = cast(req.Methods)splitted[0..xxx[0]];
req.path = cast(char[])splitted[xxx[0]+1..xxx[1]];
// if (splitted[xxx[1]..$] != " HTTP/1.1")
// throw new Exception("Unsupported HTTP version");
} else continue;
} else if (splitted[x-1] == ':') { // для заголовков
req.headers[cast(string)splitted[0..x-1]] = cast(string)splitted[x+1..$];
break;
}
}
prev = i+2;
if ((prev < body.length && prev + 2 <= body.length) && body[prev] == '\r') {
req.body = body[prev+2..$];
break;
}
}
}
return req;
}
}
private:
enum BACKLOG = 512;
enum MAX_EVENTS = 512;
enum MAX_CLIENTS = 512;
enum MAX_MESSAGE_LEN = 2048;
enum SOCK_NONBLOCK = 0x800;
enum MAX_RESPONSES = 512;
extern (C) int accept4(int, sockaddr*, socklen_t*, int);

177
source/uselesshttpd/util.d Normal file
View file

@ -0,0 +1,177 @@
module uselesshttpd.util;
/*
lost+skunk <git.macaw.me/skunky>, 2025;
Licensed under WTFPL
*/
char[] intToStr(T)(T num) {
char[] buf;
for(short i; num > 0; ++i) {
buf = (num % 10 + '0')~buf;
num /= 10;
}
return buf;
}
void err(int e, string msg) {
if (e != 0)
throw new Exception("Something went wrong: failed to "~msg~'.');
}
// UDA
struct Location { string path; }
// enum method;
string getStatus(short status) {
static foreach(mmbr; __traits(allMembers, Statuses))
if (__traits(getMember, Statuses, mmbr) == status)
return __traits(getAttributes, __traits(getMember, Statuses, mmbr))[0];
return "WTF";
}
// FIXME: memory leak
void append(char[]* src, char symb) @nogc {
import core.memory: pureMalloc, pureFree;
auto arr =
cast (char[])
pureMalloc(src.length + 1)
[0..src.length + 1];
arr[$-1..$] = symb;
arr[0..$-1] = *src;
*src = arr;
arr = null;
pureFree(arr.ptr);
}
void parseAndValidateURL(char[] url, Request* rqst) {
if (url.length > 2048) throw new Exception("Too long URL");
rqst.path = null;
bool notArgumentPart;
scope (exit) append(&rqst.args, '&');
for (short i; i < url.length; ++i) {
switch (url[i]) {
case '?':
if (notArgumentPart) goto default;
notArgumentPart = true;
break;
case '/':
if (url.length > i+1 && url[i+1] != '/') append(&rqst.path, '/');
break;
case '=', '&':
if (notArgumentPart && url[i-1] != url[i]) append(&rqst.args, url[i]);
break;
case 'A': .. case 'Z':
case 'a': .. case 'z':
case '0': .. case '9':
case '-', '_', '.', '~', '!', '$', '\'', '(', ')', '*', '+', ',', ';', '@', '[', ']', '|', '%':
if (notArgumentPart) append(&rqst.args, url[i]);
else append(&rqst.path, url[i]);
break;
default: throw new Exception("Malformed URL");
}
}
}
struct Request {
enum Methods {
GET = "GET",
PUT = "PUT",
POST = "POST",
DELETE = "DELETE",
OPTIONS = "OPTIONS",
// остальное лень реализовывать, да и не трэба..
}
Methods method;
void[] body;
string[string] headers;
char[] path;
char[] args;
char[] getArgument(string arg) @nogc nothrow {
short split, prev;
for (short i; i < args.length; ++i) {
if (args[i] == '=') split = i;
else if (args[i] == '&') {
if (arg == args[prev..split]) return args[split+1..i];
prev = ++i;
}
}
return null;
}
}
private static enum Statuses: short { // спизженно с https://github.com/zigzap/zap/blob/675c65b509d48c21a8d1fa4c5ec53fc407643a3b/src/http.zig#L6
// Information responses
@("Continue") continuee = 100,
@("Switching Protocols") switching_protocols = 101,
@("Processing") processing = 102, // (WebDAV)
@("Early Hints") early_hints = 103,
// Successful responses
@("OK") ok = 200,
@("Created") created = 201,
@("Accepted") accepted = 202,
@("Non-Authoritative Information") non_authoritative_information = 203,
@("No Content") no_content = 204,
@("Reset Content") reset_content = 205,
@("Partial Content") partial_content = 206,
@("Multi-Status") multi_status = 207, // (WebDAV)
@("Already Reported") already_reported = 208, // (WebDAV)
@("IM Used") im_used = 226, // (HTTP Delta encoding)
// Redirection messages
@("Multiple Choices") multiple_choices = 300,
@("Moved Permanently") moved_permanently = 301,
@("Found") found = 302,
@("See Other") see_other = 303,
@("Not Modified") not_modified = 304,
@("Use Proxy") use_proxy = 305,
@("Unused") unused = 306,
@("Temporary Redirect") temporary_redirect = 307,
@("Permanent Redirect") permanent_redirect = 308,
// Client error responses
@("Bad Request") bad_request = 400,
@("Unauthorized") unauthorized = 401,
@("Payment Required") payment_required = 402,
@("Forbidden") forbidden = 403,
@("Not Found") not_found = 404,
@("Method Not Allowed") method_not_allowed = 405,
@("Not Acceptable") not_acceptable = 406,
@("Proxy Authentication Required") proxy_authentication_required = 407,
@("Request Timeout") request_timeout = 408,
@("Conflict") conflict = 409,
@("Gone") gone = 410,
@("Length Required") length_required = 411,
@("Precondition Failed") precondition_failed = 412,
@("Payload Too Large") payload_too_large = 413,
@("URI Too Long") uri_too_long = 414,
@("Unsupported Media Type") unsupported_media_type = 415,
@("Range Not Satisfiable") range_not_satisfiable = 416,
@("Expectation Failed") expectation_failed = 417,
@("I'm a teapot") im_a_teapot = 418,
@("Misdirected Request") misdirected_request = 421,
@("Unprocessable Content") unprocessable_content = 422, // (WebDAV)
@("Locked") locked = 423, // (WebDAV)
@("Failed Dependency") failed_dependency = 424, // (WebDAV)
@("Too Early") too_early = 425,
@("Upgrade Required") upgrade_required = 426,
@("Precondition Required") precondition_required = 428,
@("Too Many Requests") too_many_requests = 429,
@("Request Header Fields Too Large") request_header_fields_too_large = 431,
@("Unavailable For Legal Reasons") unavailable_for_legal_reasons = 451,
// Server error responses
@("Internal Server Error") internal_server_error = 500,
@("Not Implemented") not_implemented = 501,
@("Bad Gateway") bad_gateway = 502,
@("Service Unavailable") service_unavailable = 503,
@("Gateway Timeout") gateway_timeout = 504,
@("HTTP Version Not Supported") http_version_not_supported = 505,
@("Variant Also Negotiates") variant_also_negotiates = 506
}