Files
dlang-http/dlang.http/entry.cpp
T
2026-10-15 09:59:23 +02:00

1803 lines
61 KiB
C++

#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 <dlang.hpp> 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 <vm.hpp> and <helpers.hpp>.
#include <vm.hpp>
#include <helpers.hpp>
#include <iostream>
#include <string>
#include <vector>
#include <sstream>
#include <algorithm>
#include <cstdint>
#include <cctype>
#include <cstdlib>
#include <chrono>
#include <thread>
#include <fstream>
#include <filesystem>
#include <unordered_map>
#ifdef DLANG_HTTP_WINDOWS
#include <winsock2.h>
#include <ws2tcpip.h>
#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 <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <cerrno>
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<std::string> methods;
StackValue functionVal;
};
static SocketHandle g_serverSocket = DLANG_INVALID_SOCKET;
static std::vector<RouteHandler> 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<StaticMount> g_staticMounts;
static std::unordered_map<int, ErrorPage> 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<int>(data.size() - total), 0);
if (sent <= 0) break;
total += static_cast<size_t>(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<std::shared_ptr<DlangTable>>(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<char>(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<std::string, std::string> 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<char>(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 << "<!DOCTYPE html>\n"
<< "<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n"
<< "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n"
<< "<title>" << statusCode << " " << title << "</title>\n"
<< "<style>"
<< "body{margin:0;height:100vh;display:flex;align-items:center;"
<< "justify-content:center;background:#111;color:#eee;"
<< "font-family:system-ui,-apple-system,Segoe UI,sans-serif}"
<< ".box{text-align:center}h1{font-size:5rem;margin:0;line-height:1}"
<< "p{margin:.5rem 0 0;opacity:.6;letter-spacing:.05em;text-transform:uppercase}"
<< "</style>\n</head>\n<body>\n<div class=\"box\"><h1>" << statusCode
<< "</h1><p>" << title << "</p></div>\n</body>\n</html>\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<std::string>(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<std::string>(body.data);
produced = true;
}
StackValue contentType;
if (GetTableField(returned, "contentType", contentType) &&
contentType.type == StackValueType::String) {
result.contentType = std::get<std::string>(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<std::string>(pathVal.data);
std::vector<std::string> methods;
if (methodsVal.type == StackValueType::List) {
auto methodsList = std::get<std::shared_ptr<DlangList>>(methodsVal.data);
for (const auto& item : methodsList->values) {
if (item.type == StackValueType::String)
methods.push_back(std::get<std::string>(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<std::shared_ptr<DlangList>>(last.data)) {
auto list = std::get<std::shared_ptr<DlangList>>(last.data);
for (const auto& entry : list->values) {
if (!std::holds_alternative<std::shared_ptr<DlangList>>(entry.data))
continue;
auto pair = std::get<std::shared_ptr<DlangList>>(entry.data);
if (pair->values.size() < 2)
continue;
const StackValue& urlValue = pair->values[0];
const StackValue& fileValue = pair->values[1];
if (std::holds_alternative<std::string>(urlValue.data) &&
std::holds_alternative<std::string>(fileValue.data)) {
AddStaticMount(std::get<std::string>(urlValue.data),
std::get<std::string>(fileValue.data));
}
}
return;
}
// Map of identifier-key -> filesystem path.
if (std::holds_alternative<std::shared_ptr<DlangTable>>(last.data)) {
auto table = std::get<std::shared_ptr<DlangTable>>(last.data);
for (const auto& [key, value] : table->fields) {
if (!std::holds_alternative<std::string>(value.data))
continue;
std::string url = key.empty() ? "/" : key;
if (url.front() != '/')
url = "/" + url;
AddStaticMount(url, std::get<std::string>(value.data));
}
return;
}
if (!std::holds_alternative<std::string>(last.data))
return;
std::string filePath = std::get<std::string>(last.data);
StackValue first = dlang_pop(vm);
if (!std::holds_alternative<std::string>(first.data))
return;
AddStaticMount(std::get<std::string>(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<double>(codeValue.data))
return;
int statusCode = static_cast<int>(std::get<double>(codeValue.data));
ErrorPage page;
if (std::holds_alternative<std::shared_ptr<DlangFunction>>(handler.data)) {
page.isFunction = true;
page.functionVal = handler;
}
else if (std::holds_alternative<std::string>(handler.data)) {
page.body = std::get<std::string>(handler.data);
}
else if (std::holds_alternative<std::shared_ptr<DlangTable>>(handler.data)) {
auto table = std::get<std::shared_ptr<DlangTable>>(handler.data);
StackValue body;
if (table->Get("body", body) && std::holds_alternative<std::string>(body.data))
page.body = std::get<std::string>(body.data);
StackValue contentType;
if (table->Get("contentType", contentType) && std::holds_alternative<std::string>(contentType.data))
page.contentType = std::get<std::string>(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<std::string>(returned.data);
return;
}
if (returned.type != StackValueType::Table)
return;
StackValue body;
GetTableField(returned, "body", body);
if (body.type == StackValueType::String) {
outBody = std::get<std::string>(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<std::string>(contentType.data);
}
StackValue statusCode;
if (GetTableField(returned, "statusCode", statusCode) &&
statusCode.type == StackValueType::Int) {
outStatusCode = static_cast<int>(std::get<double>(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 <script>).
// ---------------------------------------------------------------------------
static const int kMaxViewIncludeDepth = 32;
static std::string ViewTrim(const std::string& value) {
size_t begin = 0;
size_t end = value.size();
while (begin < end && std::isspace(static_cast<unsigned char>(value[begin])))
++begin;
while (end > begin && std::isspace(static_cast<unsigned char>(value[end - 1])))
--end;
return value.substr(begin, end - begin);
}
// Convert a dlang value into the text that replaces a {{ placeholder }}.
static bool ViewValueToString(DlangVM* vm, const StackValue& value, std::string& out) {
switch (value.type) {
case StackValueType::String:
out = std::get<std::string>(value.data);
return true;
case StackValueType::Int:
out = DoubleToString(std::get<double>(value.data));
return true;
case StackValueType::Bool:
out = std::get<bool>(value.data) ? "true" : "false";
return true;
case StackValueType::NIL:
out.clear();
return true;
case StackValueType::Table:
case StackValueType::List:
return EncodeTableAsJson(vm, value, out);
default:
return false;
}
}
// Look up "user.name" style keys, walking nested tables as we go.
static bool LookupViewVariable(const StackValue& variables,
const std::string& path,
StackValue& out) {
StackValue current = variables;
size_t start = 0;
while (true) {
size_t dot = path.find('.', start);
std::string key = ViewTrim(path.substr(
start, dot == std::string::npos ? std::string::npos : dot - start));
if (key.empty() || current.type != StackValueType::Table)
return false;
StackValue next;
if (!GetTableField(current, key.c_str(), next))
return false;
if (dot == std::string::npos) {
out = next;
return true;
}
current = next;
start = dot + 1;
}
}
// Replace every {{ key }} placeholder with the matching value.
static std::string InterpolateViewVariables(DlangVM* vm,
const std::string& source,
const StackValue& variables) {
std::string out;
out.reserve(source.size());
size_t i = 0;
while (i < source.size()) {
size_t open = source.find("{{", i);
if (open == std::string::npos) {
out += source.substr(i);
break;
}
out += source.substr(i, open - i);
size_t close = source.find("}}", open + 2);
if (close == std::string::npos) {
out += source.substr(open);
break;
}
std::string key = ViewTrim(source.substr(open + 2, close - open - 2));
StackValue value;
std::string text;
if (key.empty() ||
!LookupViewVariable(variables, key, value) ||
!ViewValueToString(vm, value, text)) {
// Unknown (or unrenderable) variable: keep the placeholder intact.
out += source.substr(open, close + 2 - open);
}
else {
out += text;
}
i = close + 2;
}
return out;
}
// Recursively inline every @include("...") directive.
static std::string ExpandViewIncludes(const std::string& source,
const std::filesystem::path& baseDir,
std::vector<std::string>& activeFiles,
int depth) {
const std::string directive = "@include";
std::string out;
size_t i = 0;
while (i < source.size()) {
size_t at = source.find(directive, i);
if (at == std::string::npos) {
out += source.substr(i);
break;
}
out += source.substr(i, at - i);
// Parse: @include ( "path" ) -- whitespace and ' / " quotes allowed.
size_t p = at + directive.size();
while (p < source.size() && std::isspace(static_cast<unsigned char>(source[p])))
++p;
bool parsed = false;
std::string includePath;
if (p < source.size() && source[p] == '(') {
++p;
while (p < source.size() && std::isspace(static_cast<unsigned char>(source[p])))
++p;
if (p < source.size() && (source[p] == '"' || source[p] == '\'')) {
char quote = source[p++];
size_t quoteEnd = source.find(quote, p);
if (quoteEnd != std::string::npos) {
includePath = source.substr(p, quoteEnd - p);
size_t q = quoteEnd + 1;
while (q < source.size() && std::isspace(static_cast<unsigned char>(source[q])))
++q;
if (q < source.size() && source[q] == ')') {
i = q + 1;
parsed = true;
}
}
}
}
if (!parsed) {
// Not a well-formed directive; emit it verbatim and move on.
out += directive;
i = at + directive.size();
continue;
}
if (depth >= kMaxViewIncludeDepth) {
out += "<!-- view: include depth limit reached -->";
continue;
}
std::error_code ec;
std::filesystem::path resolved = baseDir / includePath;
if (!std::filesystem::is_regular_file(resolved, ec) || ec) {
// Fall back to a path relative to the process working directory.
if (std::filesystem::is_regular_file(includePath, ec) && !ec)
resolved = includePath;
else {
out += "<!-- view: include not found: " + includePath + " -->";
continue;
}
}
std::filesystem::path canonical = std::filesystem::weakly_canonical(resolved, ec);
std::string canonicalStr = ec ? resolved.string() : canonical.string();
if (std::find(activeFiles.begin(), activeFiles.end(), canonicalStr) != activeFiles.end()) {
out += "<!-- view: include cycle skipped: " + includePath + " -->";
continue;
}
std::string fileContent;
if (!ReadFileBinary(resolved.string(), fileContent)) {
out += "<!-- view: include not found: " + includePath + " -->";
continue;
}
activeFiles.push_back(canonicalStr);
out += ExpandViewIncludes(fileContent, resolved.parent_path(), activeFiles, depth + 1);
activeFiles.pop_back();
}
return out;
}
void NativeViewRender(DlangVM* vm) {
// view.Render(path, variables)
//
// `GET_FIELD` already consumed the `view` table while resolving the
// `view.Render` callee, so only the two explicit arguments are on the stack
// (the variables table is on top).
StackValue variables = dlang_pop(vm);
StackValue pathValue = dlang_pop(vm);
if (pathValue.type != StackValueType::String) {
std::cerr << "view.Render: template path must be a string\n";
throw std::runtime_error("view.Render: template path must be a string");
}
std::string path = std::get<std::string>(pathValue.data);
std::string source;
if (!ReadFileBinary(path, source)) {
std::cerr << "view.Render: could not open template '" << path << "'\n";
throw std::runtime_error("view.Render: could not open template '" + path + "'");
}
std::error_code ec;
std::vector<std::string> activeFiles;
std::filesystem::path canonical = std::filesystem::weakly_canonical(path, ec);
activeFiles.push_back(ec ? path : canonical.string());
std::string expanded = ExpandViewIncludes(
source, std::filesystem::path(path).parent_path(), activeFiles, 0);
std::string rendered = InterpolateViewVariables(vm, expanded, variables);
dlang_pushstring(vm, rendered.c_str());
}
// ---------------------------------------------------------------------------
// Request parsing
//
// Every route handler is invoked with four arguments:
//
// fn(req, params, cookies, files)
//
// req -> { method, path, query, params, cookies, files, body }
// params -> a single key/value table holding *all* request parameters:
// the query string (GET) plus the request body (POST/PUT/PATCH/
// DELETE). Body values are read as url-encoded form data, as a
// JSON object when Content-Type is application/json, or as the
// plain fields of a multipart/form-data upload. A key that occurs
// more than once is collected into a list.
// cookies -> the parsed `Cookie:` header as a key/value table.
// files -> the uploaded files of a multipart/form-data body, keyed by form
// field name (see the "File uploads" section below).
//
// `req.body` holds the raw request body (handy for uploads sent as
// `application/octet-stream`) and is only set when the body is not empty.
//
// Values coming from the query string / url-encoded body are stored as strings;
// JSON bodies keep their native types (numbers, booleans, nested tables/lists).
// ---------------------------------------------------------------------------
static std::string HttpTrim(const std::string& value) {
size_t begin = 0;
size_t end = value.size();
while (begin < end) {
unsigned char c = static_cast<unsigned char>(value[begin]);
if (!std::isspace(c) && value[begin] != '\r' && value[begin] != '\n')
break;
++begin;
}
while (end > begin) {
unsigned char c = static_cast<unsigned char>(value[end - 1]);
if (!std::isspace(c) && value[end - 1] != '\r' && value[end - 1] != '\n')
break;
--end;
}
return value.substr(begin, end - begin);
}
// Case-insensitive lookup of a single header inside a header block. A request
// block starts with the request line (`GET / HTTP/1.1`), which is not a header,
// while the header block of a multipart part has no such line; hence the
// `skipFirstLine` switch.
static bool FindHeader(const std::string& headers, const std::string& name,
std::string& out, bool skipFirstLine) {
std::string wanted = ToLower(HttpTrim(name));
std::istringstream stream(headers);
std::string line;
bool firstLine = true;
while (std::getline(stream, line)) {
if (firstLine) {
firstLine = false;
if (skipFirstLine) // skip the request line
continue;
}
if (!line.empty() && line.back() == '\r')
line.pop_back();
size_t colon = line.find(':');
if (colon == std::string::npos)
continue;
if (ToLower(HttpTrim(line.substr(0, colon))) == wanted) {
out = HttpTrim(line.substr(colon + 1));
return true;
}
}
return false;
}
static bool GetHeaderValue(const std::string& headers, const std::string& name,
std::string& out) {
return FindHeader(headers, name, out, true);
}
// Insert one key/value pair into a parameter table. When the same key shows up
// more than once the values are collected into a list.
static void AddParamValue(DlangTable& table, const std::string& key,
const std::string& value) {
if (key.empty())
return;
StackValue existing;
if (table.Get(key, existing)) {
if (existing.type == StackValueType::String) {
auto list = std::make_shared<DlangList>();
list->Push(existing);
list->Push(StackValue(StackValueType::String, value));
table.Set(key, StackValue(StackValueType::List, list));
}
else if (existing.type == StackValueType::List) {
auto list = std::get<std::shared_ptr<DlangList>>(existing.data);
list->Push(StackValue(StackValueType::String, value));
}
return;
}
table.Set(key, StackValue(StackValueType::String, value));
}
// Parse an `a=1&b=2` style string (query string or url-encoded body).
static void ParseUrlEncoded(const std::string& data, DlangTable& table) {
size_t start = 0;
while (start <= data.size()) {
size_t amp = data.find('&', start);
std::string pair = data.substr(
start, amp == std::string::npos ? std::string::npos : amp - start);
if (!pair.empty()) {
size_t eq = pair.find('=');
std::string key, value;
if (eq == std::string::npos) {
key = UrlDecode(pair);
}
else {
key = UrlDecode(pair.substr(0, eq));
value = UrlDecode(pair.substr(eq + 1));
}
AddParamValue(table, key, value);
}
if (amp == std::string::npos)
break;
start = amp + 1;
}
}
// Parse the `Cookie:` header ("a=1; b=2") into a table.
static void ParseCookies(const std::string& header, DlangTable& table) {
size_t start = 0;
while (start <= header.size()) {
size_t semi = header.find(';', start);
std::string pair = HttpTrim(header.substr(
start, semi == std::string::npos ? std::string::npos : semi - start));
if (!pair.empty()) {
size_t eq = pair.find('=');
if (eq != std::string::npos) {
std::string key = HttpTrim(pair.substr(0, eq));
std::string value = HttpTrim(pair.substr(eq + 1));
if (!key.empty())
table.Set(key, StackValue(StackValueType::String, value));
}
}
if (semi == std::string::npos)
break;
start = semi + 1;
}
}
// Decode a JSON string into a value using the VM's native `table.json_decode`.
// Returns false when the library/function is unavailable or the text is not
// valid JSON, in which case the body is simply ignored.
static bool DecodeJsonValue(DlangVM* vm, const std::string& text, StackValue& out) {
StackValue jsonLib = vm->globals.Get("table");
if (jsonLib.type != StackValueType::Table)
return false;
StackValue decodeFn;
if (!GetTableField(jsonLib, "json_decode", decodeFn) ||
decodeFn.type != StackValueType::Function)
return false;
int base = vm->stack.top();
bool ok = false;
try {
dlang_pushstring(vm, text.c_str());
dlang_push(vm, decodeFn);
if (dlang_call(vm, 1)) {
out = dlang_pop(vm);
ok = true;
}
}
catch (...) {
ok = false;
}
ResetStack(vm, base);
return ok;
}
// Merge the top-level fields of a JSON object body into the parameter table.
static void MergeJsonBody(DlangVM* vm, const std::string& body, DlangTable& table) {
StackValue decoded;
if (!DecodeJsonValue(vm, body, decoded))
return;
if (decoded.type != StackValueType::Table)
return;
auto tablePtr = std::get<std::shared_ptr<DlangTable>>(decoded.data);
for (const auto& [key, value] : tablePtr->fields)
table.Set(key, value);
}
// ---------------------------------------------------------------------------
// File uploads (multipart/form-data)
//
// A form posted with `enctype="multipart/form-data"` (what a browser sends for
// `<input type="file">`, or what `FormData` produces in fetch/XHR) is split into
// its parts:
//
// * plain fields -> merged into the regular parameter table, exactly like a
// url-encoded body, so `params.name` keeps working,
// * file fields -> collected in a `files` table, keyed by form field name.
// A field name that occurs more than once becomes a list.
//
// Every file entry is a table:
//
// { field = "avatar", filename = "cat.png", contentType = "image/png",
// size = 1234, content = "<raw bytes>" }
//
// `content` holds the bytes exactly as they arrived (binary safe). The table is
// exposed as `req.files` and passed to the route handler as its 4th argument:
//
// fn(req, params, cookies, files)
//
// The `http` table writes an upload to disk (the helpers are not a global of
// their own, so that the `files` parameter above cannot shadow them):
//
// http.save(upload, "out/cat.png") -> true on success
// http.saveTo(upload, "out") -> the written path, or "" on failure
// ---------------------------------------------------------------------------
// Percent-decode a string. Unlike UrlDecode this leaves '+' alone, which matters
// for RFC 5987 encoded filenames (`filename*=UTF-8''cat%20pic.png`).
static std::string PercentDecode(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<char>(std::strtol(hex.c_str(), nullptr, 16));
i += 2;
}
else {
output += input[i];
}
}
return output;
}
// Pull the `boundary=...` parameter (quoted or not) out of a Content-Type.
static bool GetMultipartBoundary(const std::string& contentType, std::string& out) {
size_t at = ToLower(contentType).find("boundary=");
if (at == std::string::npos)
return false;
// The boundary ends at the next ';' (other parameters may follow it).
std::string rest = contentType.substr(at + 9);
size_t end = rest.find(';');
std::string boundary = HttpTrim(end == std::string::npos ? rest : rest.substr(0, end));
if (boundary.size() >= 2 && boundary.front() == '"' && boundary.back() == '"')
boundary = boundary.substr(1, boundary.size() - 2);
if (boundary.empty())
return false;
out = boundary;
return true;
}
// Read one `;`-separated parameter (e.g. `name` / `filename`) out of a header
// value such as `form-data; name="avatar"; filename="cat.png"`.
static bool GetHeaderParameter(const std::string& headerValue, const std::string& name,
std::string& out) {
std::string wanted = ToLower(name);
size_t start = 0;
while (start <= headerValue.size()) {
size_t semi = headerValue.find(';', start);
std::string pair = HttpTrim(headerValue.substr(
start, semi == std::string::npos ? std::string::npos : semi - start));
size_t eq = pair.find('=');
if (eq != std::string::npos && ToLower(HttpTrim(pair.substr(0, eq))) == wanted) {
std::string value = HttpTrim(pair.substr(eq + 1));
if (value.size() >= 2 && value.front() == '"' && value.back() == '"')
value = value.substr(1, value.size() - 2);
out = value;
return true;
}
if (semi == std::string::npos)
break;
start = semi + 1;
}
return false;
}
// Filename of a part, honouring both `filename=` and the RFC 5987
// `filename*=UTF-8''...` form. Empty when the part is a plain form field.
static std::string PartFilename(const std::string& disposition) {
std::string filename;
if (GetHeaderParameter(disposition, "filename", filename))
return filename;
std::string extended;
if (!GetHeaderParameter(disposition, "filename*", extended))
return std::string();
// Shaped as charset'language'percent-encoded-value.
size_t firstQuote = extended.find('\'');
size_t secondQuote = (firstQuote == std::string::npos)
? std::string::npos
: extended.find('\'', firstQuote + 1);
if (secondQuote != std::string::npos)
return PercentDecode(extended.substr(secondQuote + 1));
return PercentDecode(extended);
}
// Build the table describing one uploaded file.
static StackValue MakeFileEntry(const std::string& field,
const std::string& filename,
const std::string& contentType,
const std::string& content) {
auto entry = std::make_shared<DlangTable>();
entry->Set("field", StackValue(StackValueType::String, field));
entry->Set("filename", StackValue(StackValueType::String, filename));
entry->Set("contentType", StackValue(StackValueType::String, contentType));
entry->Set("size", StackValue(StackValueType::Int, static_cast<double>(content.size())));
entry->Set("content", StackValue(StackValueType::String, content));
return StackValue(StackValueType::Table, entry);
}
// Store a file entry. Repeated form field names are collected into a list.
static void AddFileEntry(DlangTable& table, const std::string& key, const StackValue& entry) {
if (key.empty())
return;
StackValue existing;
if (table.Get(key, existing)) {
if (existing.type == StackValueType::Table) {
auto list = std::make_shared<DlangList>();
list->Push(existing);
list->Push(entry);
table.Set(key, StackValue(StackValueType::List, list));
}
else if (existing.type == StackValueType::List) {
std::get<std::shared_ptr<DlangList>>(existing.data)->Push(entry);
}
return;
}
table.Set(key, entry);
}
// Split a `multipart/form-data` body into its parts and fill the parameter and
// file tables.
static void ParseMultipartBody(const std::string& body, const std::string& boundary,
DlangTable& params, DlangTable& files) {
const std::string delimiter = "--" + boundary;
size_t position = body.find(delimiter);
if (position == std::string::npos)
return;
while (position != std::string::npos) {
size_t afterDelimiter = position + delimiter.size();
// "--boundary--" closes the body.
if (body.compare(afterDelimiter, 2, "--") == 0)
break;
size_t cursor = afterDelimiter;
if (body.compare(cursor, 2, "\r\n") == 0)
cursor += 2;
else if (cursor < body.size() && body[cursor] == '\n')
cursor += 1;
// Part headers run until the blank line; the raw bytes follow it.
size_t headerEnd = body.find("\r\n\r\n", cursor);
size_t separatorSize = 4;
if (headerEnd == std::string::npos) {
headerEnd = body.find("\n\n", cursor);
separatorSize = 2;
}
if (headerEnd == std::string::npos)
break;
std::string partHeaders = body.substr(cursor, headerEnd - cursor);
size_t contentStart = headerEnd + separatorSize;
size_t next = body.find(delimiter, contentStart);
size_t contentEnd = (next == std::string::npos) ? body.size() : next;
// The line break in front of the next delimiter is framing, not content.
if (contentEnd >= 2 && body.compare(contentEnd - 2, 2, "\r\n") == 0)
contentEnd -= 2;
else if (contentEnd >= 1 && body[contentEnd - 1] == '\n')
contentEnd -= 1;
if (contentEnd < contentStart)
contentEnd = contentStart;
std::string content = body.substr(contentStart, contentEnd - contentStart);
std::string disposition;
std::string fieldName;
if (FindHeader(partHeaders, "Content-Disposition", disposition, false) &&
GetHeaderParameter(disposition, "name", fieldName)) {
std::string filename = PartFilename(disposition);
if (filename.empty()) {
AddParamValue(params, fieldName, content);
}
else {
std::string partContentType;
if (!FindHeader(partHeaders, "Content-Type", partContentType, false))
partContentType = "application/octet-stream";
AddFileEntry(files, fieldName,
MakeFileEntry(fieldName, filename, partContentType, content));
}
}
position = next;
}
}
// Trim a client supplied filename down to something safe to use on disk.
static std::string SafeUploadFilename(const std::string& filename) {
// Some clients (old IE) send a full path; only trust the last component.
size_t slash = filename.find_last_of("/\\");
std::string name = (slash == std::string::npos) ? filename : filename.substr(slash + 1);
std::string cleaned;
cleaned.reserve(name.size());
for (unsigned char c : name) {
if (c < 0x20 || c == 0x7f || c == '"' || c == '*' || c == ':' ||
c == '<' || c == '>' || c == '?' || c == '|')
cleaned += '_';
else
cleaned += static_cast<char>(c);
}
if (cleaned.empty() || cleaned == "." || cleaned == "..")
return "upload.bin";
return cleaned;
}
static bool WriteFileBinary(const std::string& path, const std::string& data) {
std::error_code ec;
std::filesystem::path target(path);
if (target.has_parent_path() && !target.parent_path().empty()) {
std::filesystem::create_directories(target.parent_path(), ec);
if (ec)
return false;
}
std::ofstream file(path, std::ios::binary | std::ios::trunc);
if (!file.is_open())
return false;
file.write(data.data(), static_cast<std::streamsize>(data.size()));
return file.good();
}
// Pull the bytes (and a suggested filename) out of an upload value. A plain
// string is accepted too, so a raw request body can be written as well.
static bool ExtractUploadContent(const StackValue& value, std::string& outContent,
std::string& outFilename) {
if (value.type == StackValueType::String) {
outContent = std::get<std::string>(value.data);
outFilename = "upload.bin";
return true;
}
if (value.type != StackValueType::Table)
return false;
StackValue content;
if (!GetTableField(value, "content", content) || content.type != StackValueType::String)
return false;
outContent = std::get<std::string>(content.data);
StackValue filename;
if (GetTableField(value, "filename", filename) &&
filename.type == StackValueType::String &&
!std::get<std::string>(filename.data).empty())
outFilename = std::get<std::string>(filename.data);
else
outFilename = "upload.bin";
return true;
}
void NativeFilesSave(DlangVM* vm) {
// http.save(upload, path) -> true when the bytes were written to disk.
StackValue pathValue = dlang_pop(vm);
StackValue uploadValue = dlang_pop(vm);
std::string content, filename;
if (pathValue.type != StackValueType::String ||
!ExtractUploadContent(uploadValue, content, filename)) {
dlang_pushboolean(vm, false);
return;
}
dlang_pushboolean(vm, WriteFileBinary(std::get<std::string>(pathValue.data), content));
}
void NativeFilesSaveTo(DlangVM* vm) {
// http.saveTo(upload, folder) -> the written path, or "" on failure.
StackValue folderValue = dlang_pop(vm);
StackValue uploadValue = dlang_pop(vm);
std::string content, filename;
if (folderValue.type != StackValueType::String ||
!ExtractUploadContent(uploadValue, content, filename)) {
dlang_pushstring(vm, "");
return;
}
std::filesystem::path target =
std::filesystem::path(std::get<std::string>(folderValue.data)) /
SafeUploadFilename(filename);
if (!WriteFileBinary(target.string(), content)) {
dlang_pushstring(vm, "");
return;
}
dlang_pushstring(vm, target.string());
}
// Read a full HTTP request: the header block plus the body described by
// Content-Length. `headerEnd` is set to the offset of the blank line that
// terminates the headers. Returns false when no complete header block arrived.
// `outTooLarge` reports a body bigger than `g_maxBodyBytes`, in which case the
// request is refused before the whole body has been buffered.
static bool ReadRequest(SocketHandle sock, std::string& raw, size_t& headerEnd,
bool& outTooLarge) {
raw.clear();
headerEnd = std::string::npos;
outTooLarge = false;
char buffer[4096];
size_t contentLength = 0;
bool hasContentLength = false;
while (true) {
int bytes = recv(sock, buffer, sizeof(buffer), 0);
if (bytes <= 0)
break;
raw.append(buffer, static_cast<size_t>(bytes));
if (headerEnd == std::string::npos) {
headerEnd = raw.find("\r\n\r\n");
if (headerEnd != std::string::npos) {
std::string lengthValue;
if (GetHeaderValue(raw.substr(0, headerEnd), "Content-Length", lengthValue)) {
hasContentLength = true;
contentLength = static_cast<size_t>(
std::strtoul(lengthValue.c_str(), nullptr, 10));
// An announced oversized body is refused up front instead of
// being buffered in full.
if (contentLength > g_maxBodyBytes) {
outTooLarge = true;
break;
}
}
}
}
if (headerEnd != std::string::npos) {
size_t bodyStart = headerEnd + 4;
if (!hasContentLength || raw.size() >= bodyStart + contentLength)
break;
}
// Defensive cap so a misbehaving client cannot exhaust memory, for
// requests that understate their size as well as endless header blocks.
if (raw.size() > g_maxBodyBytes + 64u * 1024u) {
outTooLarge = true;
break;
}
}
return headerEnd != std::string::npos;
}
void NativeMaxBody(DlangVM* vm) {
// server.maxBody(bytes) -> change the request body limit.
StackValue value = dlang_pop(vm);
if (value.type != StackValueType::Int)
return;
double bytes = std::get<double>(value.data);
if (bytes > 0)
g_maxBodyBytes = static_cast<size_t>(bytes);
}
// Handle one fully read request: parse the request line, build the parameter
// tables and dispatch to a route, a static file or an error page.
static void HandleRequest(DlangVM* vm, SocketHandle clientSocket,
const std::string& headerBlock, const std::string& body,
bool bodyTooLarge) {
std::istringstream stream(headerBlock);
std::string method, fullPath, version;
if (!(stream >> method >> fullPath >> version))
return;
size_t qPos = fullPath.find('?');
std::string path = (qPos != std::string::npos) ? fullPath.substr(0, qPos) : fullPath;
std::string query = (qPos != std::string::npos) ? fullPath.substr(qPos + 1) : std::string();
// An oversized upload is refused before anything else is parsed.
if (bodyTooLarge) {
ErrorResponse err = RenderError(vm, 413, method, path);
SendResponse(clientSocket, 413, err.contentType, err.body);
return;
}
// Every request parameter (query string + body) goes into a single key/value
// table, uploaded files into another, and the Cookie header into a third.
auto paramsTable = std::make_shared<DlangTable>();
auto filesTable = std::make_shared<DlangTable>();
ParseUrlEncoded(query, *paramsTable);
if (!body.empty()) {
std::string contentType;
bool hasContentType = GetHeaderValue(headerBlock, "Content-Type", contentType);
std::string boundary;
bool isMultipart = hasContentType &&
ToLower(contentType).find("multipart/form-data") != std::string::npos &&
GetMultipartBoundary(contentType, boundary);
if (isMultipart)
ParseMultipartBody(body, boundary, *paramsTable, *filesTable);
else if (hasContentType && ToLower(contentType).find("json") != std::string::npos)
MergeJsonBody(vm, body, *paramsTable);
else
ParseUrlEncoded(body, *paramsTable);
}
auto cookiesTable = std::make_shared<DlangTable>();
std::string cookieHeader;
if (GetHeaderValue(headerBlock, "Cookie", cookieHeader))
ParseCookies(cookieHeader, *cookiesTable);
StackValue paramsValue(StackValueType::Table, paramsTable);
StackValue cookiesValue(StackValueType::Table, cookiesTable);
StackValue filesValue(StackValueType::Table, filesTable);
const RouteHandler* matched = nullptr;
bool pathMatched = false;
for (const auto& route : g_routes) {
if (route.path != path)
continue;
pathMatched = true;
auto it = std::find(route.methods.begin(), route.methods.end(), method);
if (it != route.methods.end()) {
matched = &route;
break;
}
}
if (matched) {
int base = vm->stack.top();
try {
// arg 1: req = { method, path, query, params, cookies, files, body }
dlang_newtable(vm);
dlang_pushstring(vm, method.c_str());
dlang_setfield(vm, "method");
dlang_pushstring(vm, path.c_str());
dlang_setfield(vm, "path");
if (!query.empty()) {
dlang_pushstring(vm, query.c_str());
dlang_setfield(vm, "query");
}
if (!body.empty()) {
dlang_pushstring(vm, body);
dlang_setfield(vm, "body");
}
dlang_push(vm, paramsValue);
dlang_setfield(vm, "params");
dlang_push(vm, cookiesValue);
dlang_setfield(vm, "cookies");
dlang_push(vm, filesValue);
dlang_setfield(vm, "files");
// arg 2: params, arg 3: cookies, arg 4: files
dlang_push(vm, paramsValue);
dlang_push(vm, cookiesValue);
dlang_push(vm, filesValue);
dlang_push(vm, matched->functionVal);
if (!dlang_call(vm, 4))
throw std::runtime_error("Route handler could not be called");
// CallFunc leaves the handler's return value on top of the stack;
// inspect it directly instead of using the (now stack-consuming)
// field opcodes.
StackValue returned = dlang_pop(vm);
std::string responseBody;
std::string contentType;
int statusCode = 200;
ExtractResponse(vm, returned, responseBody, contentType, statusCode);
ResetStack(vm, base);
SendResponse(clientSocket, statusCode, contentType, responseBody);
}
catch (...) {
ResetStack(vm, base);
ErrorResponse err = RenderError(vm, 500, method, path);
SendResponse(clientSocket, 500, err.contentType, err.body);
}
}
else {
std::string staticBody, staticType;
if (!pathMatched && TryResolveStatic(path, staticBody, staticType)) {
SendResponse(clientSocket, 200, staticType, staticBody);
}
else {
int errorCode = pathMatched ? 405 : 404;
ErrorResponse err = RenderError(vm, errorCode, method, path);
SendResponse(clientSocket, errorCode, err.contentType, err.body);
}
}
}
void NativeListen(DlangVM* vm) {
int portInt = static_cast<int>(dlang_tonumber(vm));
if (g_serverSocket != DLANG_INVALID_SOCKET) {
DLANG_CLOSE_SOCKET(g_serverSocket);
}
g_serverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (g_serverSocket == DLANG_INVALID_SOCKET) {
dlang_pushboolean(vm, false);
return;
}
SetBlocking(g_serverSocket, false);
sockaddr_in service{};
service.sin_family = AF_INET;
service.sin_addr.s_addr = INADDR_ANY;
service.sin_port = htons(static_cast<uint16_t>(portInt));
if (bind(g_serverSocket, (SockAddr*)&service, sizeof(service)) == DLANG_SOCKET_ERROR ||
listen(g_serverSocket, SOMAXCONN) == DLANG_SOCKET_ERROR) {
DLANG_CLOSE_SOCKET(g_serverSocket);
g_serverSocket = DLANG_INVALID_SOCKET;
dlang_pushboolean(vm, false);
return;
}
while (true) {
SocketHandle clientSocket = accept(g_serverSocket, NULL, NULL);
if (clientSocket != DLANG_INVALID_SOCKET) {
SetBlocking(clientSocket, true);
int flag = 1;
setsockopt(clientSocket, IPPROTO_TCP, TCP_NODELAY,
reinterpret_cast<const char*>(&flag), sizeof(flag));
std::string rawRequest;
size_t headerEnd = std::string::npos;
bool bodyTooLarge = false;
if (ReadRequest(clientSocket, rawRequest, headerEnd, bodyTooLarge)) {
std::string headerBlock = rawRequest.substr(0, headerEnd);
std::string body = (headerEnd + 4 < rawRequest.size())
? rawRequest.substr(headerEnd + 4)
: std::string();
HandleRequest(vm, clientSocket, headerBlock, body, bodyTooLarge);
}
shutdown(clientSocket, DLANG_SHUT_WRITE);
char dummy[256];
while (recv(clientSocket, dummy, sizeof(dummy), 0) > 0) {
}
DLANG_CLOSE_SOCKET(clientSocket);
} else {
SleepMs(1);
}
}
}
void NativeCreate(DlangVM* vm)
{
#ifdef DLANG_HTTP_WINDOWS
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
#endif
dlang_newtable(vm);
dlang_pushcfunction(vm, NativeListen);
dlang_setfield(vm, "listen");
dlang_pushcfunction(vm, NativeRoute);
dlang_setfield(vm, "route");
dlang_pushcfunction(vm, NativeStatic);
dlang_setfield(vm, "static");
dlang_pushcfunction(vm, NativeError);
dlang_setfield(vm, "error");
dlang_pushcfunction(vm, NativeMaxBody);
dlang_setfield(vm, "maxBody");
}
extern "C" {
DLANG_API void initialize(DlangVM* vm) {
if (!vm) return;
dlang_newtable(vm);
dlang_pushcfunction(vm, NativeCreate);
dlang_setfield(vm, "server");
// http.save(upload, path) and http.saveTo(upload, folder) write an upload
// to disk. They hang off the `http` table rather than a separate `files`
// global: a route handler's 4th parameter is conventionally called
// `files` and would shadow a global with that name.
dlang_pushcfunction(vm, NativeFilesSave);
dlang_setfield(vm, "save");
dlang_pushcfunction(vm, NativeFilesSaveTo);
dlang_setfield(vm, "saveTo");
dlang_setglobal(vm, "http");
// view.Render(path, variables) -> rendered template string.
dlang_newtable(vm);
dlang_pushcfunction(vm, NativeViewRender);
dlang_setfield(vm, "Render");
dlang_pushcfunction(vm, NativeViewRender);
dlang_setfield(vm, "render");
dlang_setglobal(vm, "view");
}
}