diff --git a/lib.d b/lib.d new file mode 100644 index 0000000..acd1243 --- /dev/null +++ b/lib.d @@ -0,0 +1,172 @@ +module lib; + +/* + lost+skunk , 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); \ No newline at end of file diff --git a/lib.o b/lib.o new file mode 100644 index 0000000..059e159 Binary files /dev/null and b/lib.o differ diff --git a/main.d b/main.d new file mode 100644 index 0000000..af63654 --- /dev/null +++ b/main.d @@ -0,0 +1,23 @@ +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; +} \ No newline at end of file diff --git a/main.o b/main.o new file mode 100644 index 0000000..b3b78c7 Binary files /dev/null and b/main.o differ diff --git a/util.d b/util.d new file mode 100644 index 0000000..e9ee022 --- /dev/null +++ b/util.d @@ -0,0 +1,98 @@ +module util; + +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 ""; +} + +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_negoti + +} \ No newline at end of file