Compare commits
7
Commits
7d5a4693f9
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
962f20e56d | ||
|
|
3397609c12 | ||
|
|
45129fd3d1 | ||
|
|
9b2e1692d4 | ||
|
|
fbdf3889b5 | ||
|
|
533190c5c7 | ||
|
|
76e2ad240b |
No files matched your search
@@ -12,3 +12,7 @@ CTestTestfile.cmake
|
||||
_deps
|
||||
CMakeUserPresets.json
|
||||
out/
|
||||
|
||||
# Auth user store (holds password hashes) and its temporary file
|
||||
users.json
|
||||
users.json.tmp
|
||||
@@ -5,7 +5,7 @@ routing, static file serving, custom error pages, a small template engine (`view
|
||||
file uploads (`multipart/form-data`).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **This project was generated with AI** (GitHub Copilot) and has not been reviewed by a
|
||||
> **This project was generated with AI** and has been reviewed by a
|
||||
> human. Treat the code and this documentation as a starting point: read it, test it, and
|
||||
> only use it in production once you understand and trust it. See
|
||||
> [AI-generated code](#ai-generated-code) at the bottom.
|
||||
@@ -33,6 +33,7 @@ file uploads (`multipart/form-data`).
|
||||
- [`server.maxBody(bytes)`](#servermaxbodybytes)
|
||||
- [File uploads](#file-uploads)
|
||||
- [`http.save` / `http.saveTo`](#httpsave--httpsaveto)
|
||||
- [Authentication (`auth.*`)](#authentication-auth)
|
||||
- [`view.Render(path, vars)`](#viewrenderpath-vars)
|
||||
- [Status codes](#status-codes)
|
||||
- [Request and response flow](#request-and-response-flow)
|
||||
@@ -51,6 +52,7 @@ file uploads (`multipart/form-data`).
|
||||
| Request parsing | Query string, url-encoded bodies, JSON bodies and `multipart/form-data` |
|
||||
| File uploads | Uploaded files as tables (`filename`, `contentType`, `size`, raw `content`) plus `http.save` / `http.saveTo` helpers to write them to disk |
|
||||
| Cookies | Parsed `Cookie:` header as a table |
|
||||
| Authentication | HTTP Basic auth for the paths you choose, with PBKDF2-HMAC-SHA256 hashed passwords in `users.json` |
|
||||
| Error pages | Custom handlers or bodies per status code, with a built-in HTML fallback |
|
||||
| Templates | `view.Render("page.html", { ... })` with recursive `@include` and `{{ key }}` interpolation |
|
||||
| Body limit | Configurable request size limit (`server.maxBody`), oversized uploads get `413` |
|
||||
@@ -118,7 +120,7 @@ Load it from a script with `require` (the returned string is a status message su
|
||||
require("dlang.http");
|
||||
```
|
||||
|
||||
Once loaded, the plugin registers two globals: `http` and `view`.
|
||||
Once loaded, the plugin registers three globals: `http`, `view` and `auth`.
|
||||
|
||||
---
|
||||
|
||||
@@ -129,6 +131,10 @@ Once loaded, the plugin registers two globals: `http` and `view`.
|
||||
```dlang
|
||||
require("dlang.http");
|
||||
|
||||
// Basic auth: hash the password into ./users.json and gate /admin/**
|
||||
auth.addUser("admin", "geheim");
|
||||
auth.protectPrefix("/admin");
|
||||
|
||||
var server = http.server();
|
||||
|
||||
// Serve everything in ./public under /assets (e.g. /assets/css/site.css)
|
||||
@@ -217,7 +223,7 @@ func(req, params, cookies, files)
|
||||
|
||||
| Argument | Contents |
|
||||
| --- | --- |
|
||||
| `req` | Table with `method`, `path`, `query` (only when the URL had one), `params`, `cookies`, `files` and `body` (only when the request body was not empty) |
|
||||
| `req` | Table with `method`, `path`, `query` (only when the URL had one), `headers`, `params`, `cookies`, `files`, `body` (only when the request body was not empty) and `user` (only when the request passed an [`auth`](#authentication-auth) gate) |
|
||||
| `params` | All request parameters merged into one table: query string + body |
|
||||
| `cookies` | The parsed `Cookie:` header |
|
||||
| `files` | Uploaded files from a `multipart/form-data` body, keyed by form field name |
|
||||
@@ -233,6 +239,10 @@ func(req, params, cookies, files)
|
||||
|
||||
A key that occurs more than once (for example `?tag=a&tag=b`) becomes a list.
|
||||
|
||||
`req.headers` holds every request header with its name lower-cased, so
|
||||
`req.headers.authorization` or `req.headers.content-type` can be read directly; a header
|
||||
that occurs more than once becomes a list too.
|
||||
|
||||
The four-argument signature is backward compatible: older three-parameter handlers keep
|
||||
working, they simply never see `files`.
|
||||
|
||||
@@ -352,6 +362,95 @@ the same name.
|
||||
`../../etc/passwd`) is stripped and unsafe characters are replaced, so a client can never
|
||||
choose the target directory.
|
||||
|
||||
### Authentication (`auth.*`)
|
||||
|
||||
Optional HTTP Basic auth. You choose which paths are protected; everything else stays open.
|
||||
|
||||
Passwords are **hashed, not encrypted**: the plugin generates a random salt per account and
|
||||
stores a PBKDF2-HMAC-SHA256 hash of the password, so the file never holds anything that can
|
||||
be turned back into the original password.
|
||||
|
||||
```dlang
|
||||
require("dlang.http");
|
||||
|
||||
auth.addUser("admin", "geheim"); // hashes the password and writes ./users.json
|
||||
auth.realm("My site"); // the text shown in the browser's login popup
|
||||
auth.protectPrefix("/admin"); // /admin and everything below need credentials
|
||||
|
||||
var server = http.server();
|
||||
|
||||
server.route("/admin", ["GET"], func(req, params, cookies, files)
|
||||
return "<h1>Welcome back, " + req.user + "</h1>";
|
||||
end);
|
||||
|
||||
server.listen(8080);
|
||||
```
|
||||
|
||||
The first request for a protected path is answered with `401 Unauthorized` plus a
|
||||
`WWW-Authenticate: Basic realm="..."` header. A browser reacts to that header by showing its
|
||||
own username/password dialog; once the visitor fills it in, the request is repeated with an
|
||||
`Authorization: Basic ...` header and the page is served. Inside the handler `req.user` holds
|
||||
the authenticated name.
|
||||
|
||||
The user file lives in the working directory (`users.json` by default) — add it to your
|
||||
`.gitignore`, since it holds password hashes.
|
||||
|
||||
#### The user store
|
||||
|
||||
| Call | Description |
|
||||
| --- | --- |
|
||||
| `auth.file(path)` | Use another user file; returns the path in use. Default: `users.json`. |
|
||||
| `auth.reload()` | Re-read the file from disk (after editing it by hand). `true` on success. |
|
||||
| `auth.addUser(name, password)` | Create the account or replace its password. Writes the file and returns `true`, or `false` when the name/password is rejected or the file cannot be written. |
|
||||
| `auth.removeUser(name)` | Delete the account. `true` when it existed and the file was written. |
|
||||
| `auth.hasUser(name)` | `true` when the account exists. |
|
||||
| `auth.users()` | List of user names. |
|
||||
| `auth.verify(name, password)` | `true` when the password matches — to check credentials inside your own handler. |
|
||||
| `auth.login(req)` | The name from the request's `Authorization` header, or `""`. |
|
||||
| `auth.iterations(count)` | PBKDF2 rounds for new or updated passwords (default `100000`, clamped to `1000`–`1000000`); returns the value in use. |
|
||||
| `auth.realm(name)` | The realm in the login popup (default `Restricted`); returns the value in use. |
|
||||
|
||||
A name may not be empty or contain `:`, whitespace or control characters (it is sent as
|
||||
`name:password`), and the password may not be empty. `addUser` never overwrites a
|
||||
`users.json` it cannot parse — it reports the problem on `stderr` instead, so a hand-edited
|
||||
file with a typo cannot silently wipe your accounts.
|
||||
|
||||
The file looks like this (`salt` and `hash` are hex; `iterations` is stored per account so
|
||||
the cost can be raised later without invalidating existing passwords):
|
||||
|
||||
```json
|
||||
{
|
||||
"algorithm": "pbkdf2-sha256",
|
||||
"users": [
|
||||
{ "username": "admin", "salt": "ca63c036f511d4006c93ad68353677d7",
|
||||
"hash": "d4b7284ab9668ecbb548f8fd8048b44200368b258cd2b87d00807737c9aec472",
|
||||
"iterations": 100000 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Accounts are read lazily, so `auth.addUser(...)` can simply run just before
|
||||
`server.listen(...)`. A password that verified once is remembered in memory (as a hash of the
|
||||
password, never the password itself), so the deliberately slow PBKDF2 work is paid for the
|
||||
first request of a session instead of every request. Editing the file by hand? Call
|
||||
`auth.reload()`.
|
||||
|
||||
#### Protecting paths
|
||||
|
||||
| Call | Description |
|
||||
| --- | --- |
|
||||
| `auth.protect(path)` | Require credentials for exactly this path. |
|
||||
| `auth.protectPrefix(path)` | Require credentials for this path and everything below it. |
|
||||
| `auth.unprotect(path)` | Remove an exact-path protection again. |
|
||||
| `auth.unprotectPrefix(path)` | Remove a prefix protection again. |
|
||||
|
||||
Paths are normalised (`admin/panel/` becomes `/admin/panel`) and `auth.protectPrefix("/")`
|
||||
gates the whole site. The check happens before routing, static files and error pages, so a
|
||||
protected path never leaks whether it exists: `/secret/nope` answers `401` without
|
||||
credentials and `404` with them. The `401` body comes from
|
||||
[`server.error(401, ...)`](#servererrorcode-handler) like any other status code; only the
|
||||
`WWW-Authenticate` header is added by the plugin.
|
||||
|
||||
### `view.Render(path, vars)`
|
||||
|
||||
Renders a small template file and returns the result as a string. `view.render` is an alias.
|
||||
@@ -403,7 +502,9 @@ flowchart TD
|
||||
A[Accept connection] --> B[Read headers + Content-Length body]
|
||||
B --> C{Body within limit?}
|
||||
C -- no --> D[413]
|
||||
C -- yes --> E[Parse query, cookies and body<br/>urlencoded / JSON / multipart]
|
||||
C -- yes --> N{Protected path?}
|
||||
N -- "wrong or missing credentials" --> O[401 + WWW-Authenticate]
|
||||
N -- "not protected, or valid credentials" --> E[Parse query, headers, cookies and body<br/>urlencoded / JSON / multipart]
|
||||
E --> F{Route matches path?}
|
||||
F -- yes, method allowed --> G[Call handler with req, params, cookies, files]
|
||||
F -- yes, other method --> H[405]
|
||||
@@ -427,8 +528,14 @@ flowchart TD
|
||||
- **In-memory bodies.** The whole body (up to `server.maxBody`) is buffered before a handler
|
||||
runs, and static files are read fully into memory before being sent.
|
||||
- **HTTP/1.1 only**, no TLS. Put a reverse proxy in front if you need HTTPS.
|
||||
- **Global server state.** Routes, static mounts, error pages and the socket are
|
||||
process-wide, so one `listen` call serves one server per process.
|
||||
- **Basic auth is only as safe as the transport.** The browser resends the password with
|
||||
every request (base64 encoded, not encrypted), so use it behind HTTPS — or keep it on a
|
||||
trusted network.
|
||||
- **A wrong password costs a full PBKDF2 run.** `auth.iterations` defaults to 100 000, and
|
||||
the server handles one request at a time, so an attacker can keep it busy with bad
|
||||
credentials. Lower the iteration count or add rate limiting if that matters to you.
|
||||
- **Global server state.** Routes, static mounts, error pages, the protected paths and the
|
||||
loaded accounts are process-wide, so one `listen` call serves one server per process.
|
||||
|
||||
---
|
||||
|
||||
@@ -441,7 +548,7 @@ dlang-http/
|
||||
└─ dlang.http/
|
||||
├─ CMakeLists.txt # builds the `dlang.http` shared library
|
||||
├─ entry.cpp # the entire plugin: server, routing, static files,
|
||||
│ # uploads, error pages and the view engine
|
||||
│ # uploads, error pages, Basic auth and the view engine
|
||||
├─ json.hpp # nlohmann/json (used by the VM's table library)
|
||||
└─ dlang/ # upstream dlang VM copy — do not edit
|
||||
├─ vm.hpp, types/, libs/, parser/
|
||||
@@ -460,8 +567,9 @@ This repository and this README were produced with the help of an AI assistant
|
||||
(GitHub Copilot). That means:
|
||||
|
||||
- The code was **generated, then verified against its own test harnesses** (request parsing,
|
||||
multipart uploads, socket-level request handling, `http.save`/`saveTo`), but it has not
|
||||
been reviewed by a human maintainer.
|
||||
multipart uploads, socket-level request handling, `http.save`/`saveTo`, and the auth
|
||||
module including its SHA-256/HMAC/PBKDF2 test vectors), but it has not been reviewed by a
|
||||
human maintainer.
|
||||
- Comments and documentation may describe behaviour that has not been audited in every
|
||||
edge case, and the API is still evolving.
|
||||
- Treat it as a starting point: review the source in `dlang.http/entry.cpp`, test it with
|
||||
|
||||
@@ -200,13 +200,25 @@ void DlangVM::Execute(ByteCodeArray bytecode)
|
||||
auto b = stack.pop();
|
||||
auto a = stack.pop();
|
||||
|
||||
if (a.type != StackValueType::Int || b.type != StackValueType::Int)
|
||||
throw std::runtime_error("LESS_THAN Operation error, not all variables are integer!");
|
||||
if (a.type == StackValueType::Int && b.type == StackValueType::Int)
|
||||
{
|
||||
stack.push(StackValue(StackValueType::Bool,
|
||||
std::get<double>(a.data) ==
|
||||
std::get<double>(b.data)
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
stack.push(StackValue(StackValueType::Bool,
|
||||
std::get<double>(a.data) ==
|
||||
std::get<double>(b.data)
|
||||
));
|
||||
if (a.type == StackValueType::String && b.type == StackValueType::String)
|
||||
{
|
||||
stack.push(StackValue(StackValueType::Bool,
|
||||
std::get<std::string>(a.data) ==
|
||||
std::get<std::string>(b.data)
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
throw std::runtime_error("COMPARE Operation error, in compatible variables!");
|
||||
} break;
|
||||
|
||||
case Opcodes::GTE: {
|
||||
@@ -284,6 +296,8 @@ void DlangVM::Execute(ByteCodeArray bytecode)
|
||||
|
||||
if (a.type == StackValueType::Int && b.type == StackValueType::Int) {
|
||||
result.data = (std::get<double>(a.data) != std::get<double>(b.data));
|
||||
} else if(a.type == StackValueType::String && b.type == StackValueType::String) {
|
||||
result.data = (std::get<std::string>(a.data) != std::get<std::string>(b.data));
|
||||
}
|
||||
|
||||
stack.push(result);
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
inline ModuleManager* pluginManager = nullptr;
|
||||
|
||||
#define DEBUG_MODE 0
|
||||
#define VERSION "v1.0.36"
|
||||
#define VERSION "v1.0.39"
|
||||
|
||||
using ByteCodeArray = std::vector<double>;
|
||||
|
||||
|
||||
+1176
-3
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user