168 lines
6.4 KiB
D
168 lines
6.4 KiB
D
|
// 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);
|