1
0
mirror of https://github.com/bitwarden/jslib synced 2025-12-23 19:53:55 +00:00

Throttle calls to HIBP api (#25)

Randomly failing to check by passwords, I'm pretty sure its because ~2000 connections are made at the same time.
This commit is contained in:
Fred Cox
2019-02-02 17:17:44 +02:00
committed by Kyle Spearrin
parent ff0e166755
commit db37a831e4
3 changed files with 169 additions and 0 deletions

57
src/misc/throttle.ts Normal file
View File

@@ -0,0 +1,57 @@
/**
* Use as a Decorator on async functions, it will limit how many times the function can be
* in-flight at a time.
*
* Calls beyond the limit will be queued, and run when one of the active calls finishes
*/
export function throttle(limit: number, throttleKey: (args: any[]) => string) {
return <T>(target: any, propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<(...args: any[]) => Promise<T>>) => {
const originalMethod: () => Promise<T> = descriptor.value;
const allThrottles = new Map<any, Map<string, Array<() => void>>>();
const getThrottles = (obj: any) => {
let throttles = allThrottles.get(obj);
if (throttles != null) {
return throttles;
}
throttles = new Map<string, Array<() => void>>();
allThrottles.set(obj, throttles);
return throttles;
};
return {
value: function(...args: any[]) {
const throttles = getThrottles(this);
const argsThrottleKey = throttleKey(args);
let queue = throttles.get(argsThrottleKey);
if (!queue) {
queue = [];
throttles.set(argsThrottleKey, queue);
}
return new Promise<T>((resolve, reject) => {
const exec = () => {
originalMethod.apply(this, args)
.finally(() => {
queue.splice(queue.indexOf(exec), 1);
if (queue.length >= limit) {
queue[limit - 1]();
} else if (queue.length === 0) {
throttles.delete(argsThrottleKey);
if (throttles.size === 0) {
allThrottles.delete(this);
}
}
})
.then(resolve, reject);
};
queue.push(exec);
if (queue.length <= limit) {
exec();
}
});
},
};
};
}

View File

@@ -2,6 +2,7 @@ import { ApiService } from '../abstractions/api.service';
import { AuditService as AuditServiceAbstraction } from '../abstractions/audit.service';
import { CryptoFunctionService } from '../abstractions/cryptoFunction.service';
import { throttle } from '../misc/throttle';
import { Utils } from '../misc/utils';
import { BreachAccountResponse } from '../models/response/breachAccountResponse';
@@ -12,6 +13,7 @@ const PwnedPasswordsApi = 'https://api.pwnedpasswords.com/range/';
export class AuditService implements AuditServiceAbstraction {
constructor(private cryptoFunctionService: CryptoFunctionService, private apiService: ApiService) { }
@throttle(100, () => 'passwordLeaked')
async passwordLeaked(password: string): Promise<number> {
const hashBytes = await this.cryptoFunctionService.hash(password, 'sha1');
const hash = Utils.fromBufferToHex(hashBytes).toUpperCase();