ALPHA: URL parser
This commit is contained in:
parent
04f918a27a
commit
ca1662c403
8
.gitignore
vendored
8
.gitignore
vendored
@ -1 +1,7 @@
|
||||
*.o
|
||||
tst
|
||||
*.o
|
||||
*.a
|
||||
*.exe
|
||||
*.o
|
||||
*.obj
|
||||
*.lst
|
6
dub.sdl
Normal file
6
dub.sdl
Normal file
@ -0,0 +1,6 @@
|
||||
name "uselesshttpd"
|
||||
description "Dead simple web server library for lost+skunk's projects, powered by Linux epoll"
|
||||
authors "lost+skunk"
|
||||
copyright "Copyright © 2025, lost+skunk"
|
||||
license "WTFPL"
|
||||
dflags "-i"
|
172
lib.d
172
lib.d
@ -1,172 +0,0 @@
|
||||
module lib;
|
||||
|
||||
/*
|
||||
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 util;
|
||||
|
||||
struct Response {
|
||||
short status = 200;
|
||||
ubyte[] body;
|
||||
string[string] headers;
|
||||
string mimetype = "application/octet-stream";
|
||||
}
|
||||
|
||||
struct Request {
|
||||
enum Methods {
|
||||
GET = "GET",
|
||||
PUT = "PUT",
|
||||
POST = "POST",
|
||||
DELETE = "DELETE",
|
||||
OPTIONS = "OPTIONS",
|
||||
// остальное лень реализовывать, да и не трэба..
|
||||
}
|
||||
|
||||
Methods method;
|
||||
string path, body;
|
||||
string[string] headers;
|
||||
}
|
||||
|
||||
struct Server {
|
||||
string address;
|
||||
short port;
|
||||
|
||||
private shared int epfd, sock;
|
||||
void start(MD...)() { // реализовать прокидывания структуры/класса для роутинга
|
||||
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, cast(void*)(new int), int.sizeof);
|
||||
setsockopt(sock, IPPROTO_TCP, SO_REUSEADDR, cast(void*)(new int), int.sizeof);
|
||||
|
||||
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");
|
||||
err(listen(sock, 512), "listen");
|
||||
serve!MD;
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
epfd.close();
|
||||
sock.close();
|
||||
}
|
||||
|
||||
void serve(T...)() {
|
||||
epfd = epoll_create(MAX_CLIENTS);
|
||||
epoll_event ev;
|
||||
epoll_event[MAX_EVENTS] evts;
|
||||
|
||||
ev.events = EPOLLIN | EPOLLOUT | EPOLLET;
|
||||
ev.data.fd = sock;
|
||||
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) {
|
||||
sockaddr_in addr;
|
||||
socklen_t al = sockaddr_in.sizeof;
|
||||
|
||||
ev.events = EPOLLIN | EPOLLET | EPOLLRDHUP | EPOLLHUP;
|
||||
ev.data.fd = accept4(sock, cast(sockaddr*)&addr, &al, SOCK_NONBLOCK);
|
||||
epoll_ctl(epfd, EPOLL_CTL_ADD, ev.data.fd, &ev);
|
||||
} else if (evts[i].events & EPOLLIN) { rd:
|
||||
ubyte[1024] buf;
|
||||
for (;;) { // обработчик запросов
|
||||
if (read(fd, cast(void*)buf, buf.sizeof) > 0) {
|
||||
auto rqst = parseReq(cast(string)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)) {
|
||||
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 head =
|
||||
"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";
|
||||
|
||||
write(fd, cast(void*)head, head.length);
|
||||
write(fd, cast(void*)rsp.body, rsp.body.length);
|
||||
goto rd;
|
||||
} else continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
static auto resp="HTTP/1.1 404 Not Found\r\nContent-length: 0\r\n\r\n";
|
||||
write(fd, cast(void*)resp, resp.length);
|
||||
} else break;
|
||||
}
|
||||
}
|
||||
if (evts[i].events & (EPOLLRDHUP | EPOLLHUP)) {
|
||||
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, null);
|
||||
close(fd);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Request parseReq(string 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 = splitted[0..xxx[0]];
|
||||
req.path = 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[splitted[0..x-1]] = splitted[x+1..$];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
prev = i+2;
|
||||
if (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);
|
23
main.d
23
main.d
@ -1,23 +0,0 @@
|
||||
module app; // обязательно
|
||||
import lib, util: Location;
|
||||
void main() {
|
||||
auto srv = Server("127.0.0.1", 3003);
|
||||
srv.start!app;
|
||||
// srv.shutdown;
|
||||
}
|
||||
|
||||
@Location("/skunk")
|
||||
void huy(Response* w, Request* r) {
|
||||
w.body = cast(ubyte[])"skunk";
|
||||
return;
|
||||
}
|
||||
|
||||
@Location("/skunks")
|
||||
void z(Response* w, Request* r) {
|
||||
w.headers["X-Powered-By"] = "D";
|
||||
w.headers["Lang"] = "D";
|
||||
w.status = 410;
|
||||
w.mimetype = "text/plain;charset=utf-8";
|
||||
w.body = cast(ubyte[])"скунсы";
|
||||
return;
|
||||
}
|
168
source/uselesshttpd/package.d
Normal file
168
source/uselesshttpd/package.d
Normal 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);
|
@ -1,4 +1,9 @@
|
||||
module util;
|
||||
module uselesshttpd.util;
|
||||
|
||||
/*
|
||||
lost+skunk <git.macaw.me/skunky>, 2025;
|
||||
Licensed under WTFPL
|
||||
*/
|
||||
|
||||
char[] intToStr(T)(T num) {
|
||||
char[] buf;
|
||||
@ -22,7 +27,82 @@ 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 "";
|
||||
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
|
||||
@ -93,6 +173,5 @@ private static enum Statuses: short { // спизженно с https://github.co
|
||||
@("Service Unavailable") service_unavailable = 503,
|
||||
@("Gateway Timeout") gateway_timeout = 504,
|
||||
@("HTTP Version Not Supported") http_version_not_supported = 505,
|
||||
@("Variant Also Negotiates") variant_also_negoti
|
||||
|
||||
@("Variant Also Negotiates") variant_also_negotiates = 506
|
||||
}
|
Loading…
Reference in New Issue
Block a user