Upgrade dependencies, hide verbose logging.

This commit is contained in:
andrewlalis 2026-01-15 07:54:47 -05:00
parent a142a847da
commit 669fecf441
4 changed files with 51 additions and 19 deletions

View File

@ -1,17 +1,14 @@
{ {
"fileVersion": 1, "fileVersion": 1,
"versions": { "versions": {
"asdf": { "asdf": "0.8.0",
"repository": "git+https://github.com/libmir/asdf.git",
"version": "8b3352146256f3d850caa41b25351893fb1b773e"
},
"d2sqlite3": "1.0.0", "d2sqlite3": "1.0.0",
"dxml": "0.4.5", "dxml": "0.4.5",
"handy-http-data": "1.3.0", "handy-http-data": "1.3.0",
"handy-http-handlers": "1.2.0", "handy-http-handlers": "1.3.0",
"handy-http-primitives": "1.8.1", "handy-http-primitives": "1.8.1",
"handy-http-starter": "1.6.0", "handy-http-starter": "1.6.0",
"handy-http-transport": "1.10.0", "handy-http-transport": "1.10.1",
"handy-http-websockets": "1.2.0", "handy-http-websockets": "1.2.0",
"jwt4d": "0.0.2", "jwt4d": "0.0.2",
"mir-algorithm": "3.22.4", "mir-algorithm": "3.22.4",

View File

@ -162,18 +162,24 @@ private class ExceptionHandlingFilter : HttpRequestFilter {
* over time. * over time.
*/ */
private class TokenBucketRateLimitingFilter : HttpRequestFilter { private class TokenBucketRateLimitingFilter : HttpRequestFilter {
import std.datetime; import std.datetime : Duration, Clock, SysTime, seconds;
import std.math : floor; import std.math : floor;
import std.algorithm : min; import std.algorithm : min;
import std.traits : Unqual;
private static struct TokenBucket { private static struct TokenBucket {
/// The number of tokens in this bucket.
uint tokens; uint tokens;
/// The timestamp at which a token was last removed from this bucket.
SysTime lastRequest; SysTime lastRequest;
} }
TokenBucket[string] tokenBuckets; /// The internal set of token buckets, mapped to client addresses.
const uint tokensPerSecond; private shared TokenBucket[string] tokenBuckets;
const uint maxTokens; /// The number of tokens that are added to each bucket, per second.
private const uint tokensPerSecond;
/// The maximum number of tokens that each bucket can hold.
private const uint maxTokens;
this(uint tokensPerSecond, uint maxTokens) { this(uint tokensPerSecond, uint maxTokens) {
this.tokensPerSecond = tokensPerSecond; this.tokensPerSecond = tokensPerSecond;
@ -205,6 +211,12 @@ private class TokenBucketRateLimitingFilter : HttpRequestFilter {
} }
} }
/**
* Gets a string identifying the client who made the request.
* Params:
* req = The request.
* Returns: A string uniquely identifying the client.
*/
private string getClientId(in ServerHttpRequest req) { private string getClientId(in ServerHttpRequest req) {
import handy_http_transport.helpers : indexOf; import handy_http_transport.helpers : indexOf;
string clientAddr = req.clientAddress.toString(); string clientAddr = req.clientAddress.toString();
@ -215,28 +227,51 @@ private class TokenBucketRateLimitingFilter : HttpRequestFilter {
return clientAddr; return clientAddr;
} }
/**
* Gets a pointer to a token bucket for a given client, creating it first
* if it doesn't exist yet.
* Params:
* clientAddr = The client's address.
* now = The current timestamp, used to initialize new buckets.
* Returns: A pointer to the token bucket in this filter's internal mapping.
*/
private TokenBucket* getOrCreateBucket(string clientAddr, SysTime now) { private TokenBucket* getOrCreateBucket(string clientAddr, SysTime now) {
TokenBucket* bucket = clientAddr in tokenBuckets; TokenBucket[string] unsharedBuckets = cast(TokenBucket[string]) tokenBuckets;
TokenBucket* bucket = clientAddr in unsharedBuckets;
if (bucket is null) { if (bucket is null) {
tokenBuckets[clientAddr] = TokenBucket(maxTokens, now); unsharedBuckets[clientAddr] = TokenBucket(maxTokens, now);
bucket = clientAddr in tokenBuckets; bucket = clientAddr in unsharedBuckets;
} }
return bucket; return bucket;
} }
/**
* Increments the number of tokens in a client's bucket based on how much
* time has elapsed since the last time they made a request. This filter
* has a defined `tokensPerSecond`, as well as a `maxTokens`, so we know
* how long it takes for a bucket to fill up.
* Params:
* bucket = The bucket to fill with tokens.
* now = The current timestamp.
*/
private void incrementTokensForElapsedTime(TokenBucket* bucket, SysTime now) { private void incrementTokensForElapsedTime(TokenBucket* bucket, SysTime now) {
Duration timeSinceLastRequest = now - bucket.lastRequest; Duration timeSinceLastRequest = now - bucket.lastRequest;
const tokensAddedSinceLastRequest = floor((timeSinceLastRequest.total!"msecs") * (tokensPerSecond / 1000.0)); const tokensAddedSinceLastRequest = floor((timeSinceLastRequest.total!"msecs") * (tokensPerSecond / 1000.0));
bucket.tokens = cast(uint) min(bucket.tokens + tokensAddedSinceLastRequest, maxTokens); bucket.tokens = cast(uint) min(bucket.tokens + tokensAddedSinceLastRequest, maxTokens);
} }
/**
* Removes any token buckets that haven't had a request in a while, and
* thus are full. This keeps our memory footprint smaller.
*/
private void clearOldBuckets() { private void clearOldBuckets() {
const Duration fillTime = seconds(maxTokens * tokensPerSecond); const Duration fillTime = seconds(maxTokens * tokensPerSecond);
foreach (id; tokenBuckets.byKey()) { TokenBucket[string] unsharedBuckets = cast(TokenBucket[string]) tokenBuckets;
TokenBucket bucket = tokenBuckets[id]; foreach (id; unsharedBuckets.byKey()) {
TokenBucket bucket = unsharedBuckets[id];
const Duration timeSinceLastRequest = Clock.currTime() - bucket.lastRequest; const Duration timeSinceLastRequest = Clock.currTime() - bucket.lastRequest;
if (timeSinceLastRequest > fillTime) { if (timeSinceLastRequest > fillTime) {
tokenBuckets.remove(id); unsharedBuckets.remove(id);
} }
} }
} }

View File

@ -19,7 +19,7 @@ void postLogin(ref ServerHttpRequest request, ref ServerHttpResponse response) {
LoginData data = readJsonBodyAs!LoginData(request); LoginData data = readJsonBodyAs!LoginData(request);
string token = generateTokenForLogin(data.username, data.password); string token = generateTokenForLogin(data.username, data.password);
response.writeBodyString(token); response.writeBodyString(token);
infoF!"Generated token for user: %s"(data.username); debugF!"Generated token for user: %s"(data.username);
} }
struct UsernameAvailabilityResponse { struct UsernameAvailabilityResponse {

View File

@ -49,9 +49,9 @@ string generateTokenForLogin(string username, string password) {
throw new HttpStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials."); throw new HttpStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials.");
} }
User user = optionalUser.value; User user = optionalUser.value;
infoF!"Verifying password for login attempt for user %s."(user.username); debugF!"Verifying password for login attempt for user %s."(user.username);
auto verificationResult = verifyPassword(password, HashedPassword(user.passwordHash), PASSWORD_HASH_PEPPER); auto verificationResult = verifyPassword(password, HashedPassword(user.passwordHash), PASSWORD_HASH_PEPPER);
infoF!"Verification result for login: %s"(verificationResult); debugF!"Verification result for login: %s"(verificationResult);
if (verificationResult == VerifyPasswordResult.Failure) { if (verificationResult == VerifyPasswordResult.Failure) {
throw new HttpStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials."); throw new HttpStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials.");
} }