diff --git a/finnow-api/dub.selections.json b/finnow-api/dub.selections.json index 3a3863c..fe2bc65 100644 --- a/finnow-api/dub.selections.json +++ b/finnow-api/dub.selections.json @@ -1,17 +1,14 @@ { "fileVersion": 1, "versions": { - "asdf": { - "repository": "git+https://github.com/libmir/asdf.git", - "version": "8b3352146256f3d850caa41b25351893fb1b773e" - }, + "asdf": "0.8.0", "d2sqlite3": "1.0.0", "dxml": "0.4.5", "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-starter": "1.6.0", - "handy-http-transport": "1.10.0", + "handy-http-transport": "1.10.1", "handy-http-websockets": "1.2.0", "jwt4d": "0.0.2", "mir-algorithm": "3.22.4", diff --git a/finnow-api/source/api_mapping.d b/finnow-api/source/api_mapping.d index 896df21..1fc4ccf 100644 --- a/finnow-api/source/api_mapping.d +++ b/finnow-api/source/api_mapping.d @@ -162,18 +162,24 @@ private class ExceptionHandlingFilter : HttpRequestFilter { * over time. */ private class TokenBucketRateLimitingFilter : HttpRequestFilter { - import std.datetime; + import std.datetime : Duration, Clock, SysTime, seconds; import std.math : floor; import std.algorithm : min; + import std.traits : Unqual; private static struct TokenBucket { + /// The number of tokens in this bucket. uint tokens; + /// The timestamp at which a token was last removed from this bucket. SysTime lastRequest; } - TokenBucket[string] tokenBuckets; - const uint tokensPerSecond; - const uint maxTokens; + /// The internal set of token buckets, mapped to client addresses. + private shared TokenBucket[string] tokenBuckets; + /// 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.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) { import handy_http_transport.helpers : indexOf; string clientAddr = req.clientAddress.toString(); @@ -215,28 +227,51 @@ private class TokenBucketRateLimitingFilter : HttpRequestFilter { 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) { - TokenBucket* bucket = clientAddr in tokenBuckets; + TokenBucket[string] unsharedBuckets = cast(TokenBucket[string]) tokenBuckets; + TokenBucket* bucket = clientAddr in unsharedBuckets; if (bucket is null) { - tokenBuckets[clientAddr] = TokenBucket(maxTokens, now); - bucket = clientAddr in tokenBuckets; + unsharedBuckets[clientAddr] = TokenBucket(maxTokens, now); + bucket = clientAddr in unsharedBuckets; } 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) { Duration timeSinceLastRequest = now - bucket.lastRequest; const tokensAddedSinceLastRequest = floor((timeSinceLastRequest.total!"msecs") * (tokensPerSecond / 1000.0)); 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() { const Duration fillTime = seconds(maxTokens * tokensPerSecond); - foreach (id; tokenBuckets.byKey()) { - TokenBucket bucket = tokenBuckets[id]; + TokenBucket[string] unsharedBuckets = cast(TokenBucket[string]) tokenBuckets; + foreach (id; unsharedBuckets.byKey()) { + TokenBucket bucket = unsharedBuckets[id]; const Duration timeSinceLastRequest = Clock.currTime() - bucket.lastRequest; if (timeSinceLastRequest > fillTime) { - tokenBuckets.remove(id); + unsharedBuckets.remove(id); } } } diff --git a/finnow-api/source/auth/api_public.d b/finnow-api/source/auth/api_public.d index f98d9b4..af1b1be 100644 --- a/finnow-api/source/auth/api_public.d +++ b/finnow-api/source/auth/api_public.d @@ -19,7 +19,7 @@ void postLogin(ref ServerHttpRequest request, ref ServerHttpResponse response) { LoginData data = readJsonBodyAs!LoginData(request); string token = generateTokenForLogin(data.username, data.password); response.writeBodyString(token); - infoF!"Generated token for user: %s"(data.username); + debugF!"Generated token for user: %s"(data.username); } struct UsernameAvailabilityResponse { diff --git a/finnow-api/source/auth/service.d b/finnow-api/source/auth/service.d index 9734212..e0d5c3b 100644 --- a/finnow-api/source/auth/service.d +++ b/finnow-api/source/auth/service.d @@ -49,9 +49,9 @@ string generateTokenForLogin(string username, string password) { throw new HttpStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials."); } 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); - infoF!"Verification result for login: %s"(verificationResult); + debugF!"Verification result for login: %s"(verificationResult); if (verificationResult == VerifyPasswordResult.Failure) { throw new HttpStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials."); }