808 lines
27 KiB
C++
808 lines
27 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;
|
|
|
|
// 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 500: return "Internal Server Error";
|
|
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));
|
|
}
|
|
}
|
|
|
|
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));
|
|
|
|
char buffer[4096];
|
|
int bytes = recv(clientSocket, buffer, sizeof(buffer) - 1, 0);
|
|
|
|
if (bytes > 0) {
|
|
buffer[bytes] = '\0';
|
|
std::istringstream stream(buffer);
|
|
std::string method, fullPath, version;
|
|
|
|
if (stream >> method >> fullPath >> version) {
|
|
size_t qPos = fullPath.find('?');
|
|
std::string path = (qPos != std::string::npos) ? fullPath.substr(0, qPos) : fullPath;
|
|
|
|
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 {
|
|
dlang_newtable(vm);
|
|
|
|
dlang_pushstring(vm, method.c_str());
|
|
dlang_setfield(vm, "method");
|
|
|
|
dlang_pushstring(vm, path.c_str());
|
|
dlang_setfield(vm, "path");
|
|
|
|
dlang_push(vm, matched->functionVal);
|
|
if (!dlang_call(vm, 1))
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
extern "C" {
|
|
DLANG_API void initialize(DlangVM* vm) {
|
|
if (!vm) return;
|
|
|
|
dlang_newtable(vm);
|
|
|
|
dlang_pushcfunction(vm, NativeCreate);
|
|
dlang_setfield(vm, "server");
|
|
|
|
dlang_setglobal(vm, "http");
|
|
}
|
|
} |