#if defined(_WIN32) || defined(_WIN64) #define DLANG_HTTP_WINDOWS #elif defined(__linux__) #define DLANG_HTTP_LINUX #else #error "dlang.http only supports Windows and Linux." #endif #ifdef DLANG_HTTP_WINDOWS #define NOMINMAX #define WIN32_LEAN_AND_MEAN #endif // NOTE: We intentionally do NOT include here. That umbrella header // pulls in every standard library module (libs/*.hpp) and registers all of the // default globals; the http plugin only needs the VM core plus the helper // macros, which we get from and . #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef DLANG_HTTP_WINDOWS #include #include #if defined(_MSC_VER) #pragma comment(lib, "Ws2_32.lib") #endif using SocketHandle = SOCKET; using SockAddr = SOCKADDR; #define DLANG_INVALID_SOCKET INVALID_SOCKET #define DLANG_SOCKET_ERROR SOCKET_ERROR #define DLANG_CLOSE_SOCKET closesocket #define DLANG_SHUT_WRITE SD_SEND #else #include #include #include #include #include #include #include using SocketHandle = int; using SockAddr = struct sockaddr; #define DLANG_INVALID_SOCKET (-1) #define DLANG_SOCKET_ERROR (-1) #define DLANG_CLOSE_SOCKET close #define DLANG_SHUT_WRITE SHUT_WR #endif #if defined(_WIN32) || defined(_WIN64) #define DLANG_API __declspec(dllexport) #else #define DLANG_API __attribute__((visibility("default"))) #endif struct RouteHandler { std::string path; std::vector methods; StackValue functionVal; }; static SocketHandle g_serverSocket = DLANG_INVALID_SOCKET; static std::vector g_routes; // A static asset mount. When `isDirectory` is true the mount maps a URL // prefix (e.g. "/static") onto a folder on disk; otherwise it maps an exact // URL (e.g. "/style.css") onto a single file. struct StaticMount { std::string url; std::string filePath; bool isDirectory; }; // A user supplied error page override. Either a fixed body/contentType or a // function that receives the request and returns the page. struct ErrorPage { bool isFunction = false; StackValue functionVal; std::string body; std::string contentType = "text/html"; }; struct ErrorResponse { std::string body; std::string contentType; }; static std::vector g_staticMounts; static std::unordered_map g_errorPages; // Largest request body we are willing to buffer. Raised/lowered with // `server.maxBody(bytes)`; anything bigger is answered with 413. static size_t g_maxBodyBytes = 16u * 1024u * 1024u; // Toggle blocking mode on a socket in a cross-platform way. static void SetBlocking(SocketHandle sock, bool blocking) { #ifdef DLANG_HTTP_WINDOWS u_long mode = blocking ? 0 : 1; ioctlsocket(sock, FIONBIO, &mode); #else int flags = fcntl(sock, F_GETFL, 0); if (flags == -1) return; if (blocking) flags &= ~O_NONBLOCK; else flags |= O_NONBLOCK; fcntl(sock, F_SETFL, flags); #endif } // Cross-platform sleep used while waiting for new connections. static void SleepMs(int ms) { std::this_thread::sleep_for(std::chrono::milliseconds(ms)); } // Send the whole buffer, handling partial writes. static void SendAll(SocketHandle sock, const std::string& data) { size_t total = 0; while (total < data.size()) { auto sent = send(sock, data.data() + total, static_cast(data.size() - total), 0); if (sent <= 0) break; total += static_cast(sent); } } static const char* ReasonPhrase(int statusCode) { switch (statusCode) { case 200: return "OK"; case 201: return "Created"; case 204: return "No Content"; case 301: return "Moved Permanently"; case 302: return "Found"; case 400: return "Bad Request"; case 401: return "Unauthorized"; case 403: return "Forbidden"; case 404: return "Not Found"; case 405: return "Method Not Allowed"; case 413: return "Payload Too Large"; case 500: return "Internal Server Error"; case 501: return "Not Implemented"; default: return "OK"; } } // Read a field out of a table value without touching the VM value stack. // // The rewritten VM's `DlangVM::GetField(key)` now POPS the table off the stack // and RETURNS the field value (it no longer pushes the result for you), so // reading several fields in a row through the VM would consume the table after // the first lookup. Reading straight from the DlangTable is simpler and keeps // the stack perfectly balanced. static bool GetTableField(const StackValue& tableValue, const char* key, StackValue& out) { if (tableValue.type != StackValueType::Table) return false; auto table = std::get>(tableValue.data); return table->Get(key, out); } // --------------------------------------------------------------------------- // Static files & error pages // --------------------------------------------------------------------------- static std::string ToLower(std::string value) { std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); return value; } // Maps a file extension to a MIME type so CSS/JS/images are served correctly. static std::string ContentTypeForPath(const std::string& path) { size_t dot = path.find_last_of('.'); if (dot == std::string::npos) return "application/octet-stream"; std::string ext = ToLower(path.substr(dot)); static const std::unordered_map types = { { ".html", "text/html" }, { ".htm", "text/html" }, { ".css", "text/css" }, { ".js", "application/javascript" }, { ".mjs", "application/javascript" }, { ".json", "application/json" }, { ".map", "application/json" }, { ".txt", "text/plain" }, { ".xml", "application/xml" }, { ".svg", "image/svg+xml" }, { ".png", "image/png" }, { ".jpg", "image/jpeg" }, { ".jpeg", "image/jpeg" }, { ".gif", "image/gif" }, { ".webp", "image/webp" }, { ".ico", "image/x-icon" }, { ".bmp", "image/bmp" }, { ".woff", "font/woff" }, { ".woff2","font/woff2" }, { ".ttf", "font/ttf" }, { ".otf", "font/otf" }, { ".eot", "application/vnd.ms-fontobject" }, { ".mp3", "audio/mpeg" }, { ".wav", "audio/wav" }, { ".mp4", "video/mp4" }, { ".webm", "video/webm" }, { ".pdf", "application/pdf" }, { ".wasm", "application/wasm" }, }; auto it = types.find(ext); return it != types.end() ? it->second : "application/octet-stream"; } static std::string UrlDecode(const std::string& input) { std::string output; output.reserve(input.size()); for (size_t i = 0; i < input.size(); ++i) { if (input[i] == '%' && i + 2 < input.size()) { std::string hex = input.substr(i + 1, 2); output += static_cast(std::strtol(hex.c_str(), nullptr, 16)); i += 2; } else if (input[i] == '+') { output += ' '; } else { output += input[i]; } } return output; } // Reject anything that could escape the mounted folder. static bool IsSafePath(const std::string& path) { return path.find("..") == std::string::npos && path.find('\\') == std::string::npos; } static bool ReadFileBinary(const std::string& path, std::string& out) { std::ifstream file(path, std::ios::binary); if (!file.is_open()) return false; std::ostringstream buffer; buffer << file.rdbuf(); out = buffer.str(); return true; } static void SendResponse(SocketHandle sock, int statusCode, const std::string& contentType, const std::string& body) { std::ostringstream resp; resp << "HTTP/1.1 " << statusCode << " " << ReasonPhrase(statusCode) << "\r\n" << "Content-Type: " << contentType << "\r\n" << "Content-Length: " << body.length() << "\r\n" << "Connection: close\r\n\r\n" << body; SendAll(sock, resp.str()); } // Restore the value stack after a (possibly failed) call, so that a throwing // handler cannot corrupt the following request. static void ResetStack(DlangVM* vm, int base) { while (vm->stack.top() > base) vm->stack.pop(); } // Built-in error page used whenever the user did not override the status code. static std::string DefaultErrorBody(int statusCode) { std::string title = ReasonPhrase(statusCode); std::ostringstream html; html << "\n" << "\n\n\n" << "\n" << "" << statusCode << " " << title << "\n" << "\n\n\n

" << statusCode << "

" << title << "

\n\n\n"; return html.str(); } // Trim a mount URL and store it alongside its resolved kind (file or folder). static void AddStaticMount(const std::string& url, const std::string& filePath) { std::string prefix = url.empty() ? "/" : url; if (prefix.size() > 1 && prefix.back() == '/') prefix.pop_back(); std::error_code ec; bool isDirectory = std::filesystem::is_directory(filePath, ec) && !ec; g_staticMounts.push_back({ prefix, filePath, isDirectory }); } // Resolve a request path against the configured static mounts. static bool TryResolveStatic(const std::string& urlPath, std::string& outBody, std::string& outContentType) { std::string decoded = UrlDecode(urlPath); if (!IsSafePath(decoded)) return false; // Exact URL -> file mappings win over folder mounts. for (const auto& mount : g_staticMounts) { if (mount.isDirectory || mount.url != decoded) continue; if (ReadFileBinary(mount.filePath, outBody)) { outContentType = ContentTypeForPath(mount.filePath); return true; } return false; } // Folder mounts: pick the longest matching URL prefix. const StaticMount* best = nullptr; for (const auto& mount : g_staticMounts) { if (!mount.isDirectory) continue; if (decoded.compare(0, mount.url.size(), mount.url) != 0) continue; if (decoded.size() != mount.url.size() && decoded[mount.url.size()] != '/') continue; if (best == nullptr || mount.url.size() > best->url.size()) best = &mount; } if (best == nullptr) return false; std::string rest = decoded.substr(best->url.size()); if (rest.empty() || rest == "/") rest = "/index.html"; if (!IsSafePath(rest)) return false; std::filesystem::path full = std::filesystem::path(best->filePath) / rest.substr(rest.front() == '/' ? 1 : 0); std::error_code ec; if (!std::filesystem::is_regular_file(full, ec) || ec) return false; if (!ReadFileBinary(full.string(), outBody)) return false; outContentType = ContentTypeForPath(full.string()); return true; } // Render an error page, using the user override when present and the built-in // default otherwise. static ErrorResponse RenderError(DlangVM* vm, int statusCode, const std::string& method, const std::string& path) { ErrorResponse result; result.contentType = "text/html"; auto it = g_errorPages.find(statusCode); if (it == g_errorPages.end()) { result.body = DefaultErrorBody(statusCode); return result; } const ErrorPage& page = it->second; if (!page.isFunction) { result.body = page.body.empty() ? DefaultErrorBody(statusCode) : page.body; result.contentType = page.contentType; return result; } int base = vm->stack.top(); bool produced = false; try { dlang_newtable(vm); dlang_pushstring(vm, method.c_str()); dlang_setfield(vm, "method"); dlang_pushstring(vm, path.c_str()); dlang_setfield(vm, "path"); dlang_pushnumber(vm, statusCode); dlang_setfield(vm, "statusCode"); dlang_push(vm, page.functionVal); if (!dlang_call(vm, 1)) throw std::runtime_error("Error handler could not be called"); // CallFunc leaves the handler's return value on top of the stack. StackValue returned = dlang_pop(vm); if (returned.type == StackValueType::String) { result.body = std::get(returned.data); produced = true; } else if (returned.type == StackValueType::Table) { StackValue body; if (GetTableField(returned, "body", body) && body.type == StackValueType::String) { result.body = std::get(body.data); produced = true; } StackValue contentType; if (GetTableField(returned, "contentType", contentType) && contentType.type == StackValueType::String) { result.contentType = std::get(contentType.data); } } } catch (...) { produced = false; } ResetStack(vm, base); if (!produced) result.body = DefaultErrorBody(statusCode); return result; } void NativeRoute(DlangVM* vm) { // server.route(path, methods, fn) // // The rewritten VM's `GET_FIELD` already consumed the `server` table while // resolving the `server.route` callee, so this native only sees the three // explicit arguments (last argument on top of the stack). StackValue funcVal = dlang_pop(vm); StackValue methodsVal = dlang_pop(vm); StackValue pathVal = dlang_pop(vm); if (pathVal.type != StackValueType::String) return; if (funcVal.type != StackValueType::Function) return; std::string path = std::get(pathVal.data); std::vector methods; if (methodsVal.type == StackValueType::List) { auto methodsList = std::get>(methodsVal.data); for (const auto& item : methodsList->values) { if (item.type == StackValueType::String) methods.push_back(std::get(item.data)); } } g_routes.push_back({ path, methods, funcVal }); } void NativeStatic(DlangVM* vm) { // Supported call shapes (the last argument is on top of the stack): // server.static("/assets", "./www") -> prefix -> folder/file // server.static({ assets: "./www", js: "./js" }) -> map of prefix -> path // server.static([["/a.css", "./a.css"], ...]) -> explicit URL -> file // // dlang table keys must be identifiers, so the map form uses keys like // `assets` (a leading slash is added automatically). StackValue last = dlang_pop(vm); // List of [url, file] pairs. if (std::holds_alternative>(last.data)) { auto list = std::get>(last.data); for (const auto& entry : list->values) { if (!std::holds_alternative>(entry.data)) continue; auto pair = std::get>(entry.data); if (pair->values.size() < 2) continue; const StackValue& urlValue = pair->values[0]; const StackValue& fileValue = pair->values[1]; if (std::holds_alternative(urlValue.data) && std::holds_alternative(fileValue.data)) { AddStaticMount(std::get(urlValue.data), std::get(fileValue.data)); } } return; } // Map of identifier-key -> filesystem path. if (std::holds_alternative>(last.data)) { auto table = std::get>(last.data); for (const auto& [key, value] : table->fields) { if (!std::holds_alternative(value.data)) continue; std::string url = key.empty() ? "/" : key; if (url.front() != '/') url = "/" + url; AddStaticMount(url, std::get(value.data)); } return; } if (!std::holds_alternative(last.data)) return; std::string filePath = std::get(last.data); StackValue first = dlang_pop(vm); if (!std::holds_alternative(first.data)) return; AddStaticMount(std::get(first.data), filePath); } void NativeError(DlangVM* vm) { // server.error(statusCode, handler) // handler can be a body string, a function, or a { body, contentType } table. StackValue handler = dlang_pop(vm); StackValue codeValue = dlang_pop(vm); if (!std::holds_alternative(codeValue.data)) return; int statusCode = static_cast(std::get(codeValue.data)); ErrorPage page; if (std::holds_alternative>(handler.data)) { page.isFunction = true; page.functionVal = handler; } else if (std::holds_alternative(handler.data)) { page.body = std::get(handler.data); } else if (std::holds_alternative>(handler.data)) { auto table = std::get>(handler.data); StackValue body; if (table->Get("body", body) && std::holds_alternative(body.data)) page.body = std::get(body.data); StackValue contentType; if (table->Get("contentType", contentType) && std::holds_alternative(contentType.data)) page.contentType = std::get(contentType.data); } else { return; // Unsupported handler; keep the built-in default page. } g_errorPages[statusCode] = page; } // Encode a value as JSON using the VM's native `table.json_encode`. The // function is looked up directly on the `table` library instead of through // `GetField`, which would pop the library off the stack and discard the // result (breaking the call that follows). static bool EncodeTableAsJson(DlangVM* vm, const StackValue& value, std::string& out) { StackValue jsonLib = vm->globals.Get("table"); if (jsonLib.type != StackValueType::Table) return false; StackValue encodeFn; if (!GetTableField(jsonLib, "json_encode", encodeFn) || encodeFn.type != StackValueType::Function) return false; int base = vm->stack.top(); dlang_push(vm, value); // argument 1: value to encode dlang_pushnumber(vm, 0); // argument 2: indent dlang_push(vm, encodeFn); // the function to call if (!dlang_call(vm, 2)) { ResetStack(vm, base); return false; } if (!dlang_isstring(vm)) { ResetStack(vm, base); return false; } out = dlang_tostring(vm); ResetStack(vm, base); return true; } // Turn whatever a route/error handler returned into an HTTP response: a body, // a content type and a status code. The handler may return a plain string or a // table with `body`, `contentType` and `statusCode` fields. static void ExtractResponse(DlangVM* vm, const StackValue& returned, std::string& outBody, std::string& outContentType, int& outStatusCode) { outBody.clear(); outContentType = "text/html"; outStatusCode = 200; if (returned.type == StackValueType::String) { outBody = std::get(returned.data); return; } if (returned.type != StackValueType::Table) return; StackValue body; GetTableField(returned, "body", body); if (body.type == StackValueType::String) { outBody = std::get(body.data); } else if (body.type == StackValueType::Table) { // Tables/objects are serialized to JSON by the VM (table.json_encode). if (EncodeTableAsJson(vm, body, outBody)) outContentType = "application/json"; } StackValue contentType; if (GetTableField(returned, "contentType", contentType) && contentType.type == StackValueType::String) { outContentType = std::get(contentType.data); } StackValue statusCode; if (GetTableField(returned, "statusCode", statusCode) && statusCode.type == StackValueType::Int) { outStatusCode = static_cast(std::get(statusCode.data)); } } // --------------------------------------------------------------------------- // Views: view.Render("page.html", { name: "World" }) // // A tiny text/HTML template engine: // // @include("partial.html") -> inlines another template file. Includes are // recursive and resolved relative to the file // that contains them (falling back to the // process working directory). // {{ key }} -> replaced with the matching entry from the // variables table. Dotted paths ({{ user.name }}) // walk into nested tables. Unknown keys are left // untouched so they are easy to spot. // // Variable values may be strings, numbers, booleans or tables/lists (tables and // lists are serialized to JSON, which is handy for embedding data in