wallet: throttle all http requests

even from browsers / service workers
This commit is contained in:
Florian Dold 2022-03-08 19:19:29 +01:00
parent d0376d9e68
commit 1d1c847b79
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
6 changed files with 73 additions and 40 deletions

View File

@ -14,20 +14,13 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { Logger } from "./logging.js";
import { getTimestampNow, timestampCmp, timestampDifference } from "./time.js";
/** /**
* Implementation of token bucket throttling. * Implementation of token bucket throttling.
*/ */
/**
* Imports.
*/
import {
getTimestampNow,
timestampDifference,
timestampCmp,
Logger,
URL,
} from "@gnu-taler/taler-util";
const logger = new Logger("RequestThrottler.ts"); const logger = new Logger("RequestThrottler.ts");

View File

@ -30,3 +30,4 @@ export {
secretbox_open, secretbox_open,
crypto_sign_keyPair_fromSeed, crypto_sign_keyPair_fromSeed,
} from "./nacl-fast.js"; } from "./nacl-fast.js";
export { RequestThrottler } from "./RequestThrottler.js";

View File

@ -25,7 +25,7 @@ import {
HttpRequestOptions, HttpRequestOptions,
HttpResponse, HttpResponse,
} from "../util/http.js"; } from "../util/http.js";
import { RequestThrottler } from "../util/RequestThrottler.js"; import { RequestThrottler } from "@gnu-taler/taler-util";
import Axios, { AxiosResponse } from "axios"; import Axios, { AxiosResponse } from "axios";
import { OperationFailedError, makeErrorDetails } from "../errors.js"; import { OperationFailedError, makeErrorDetails } from "../errors.js";
import { Logger, bytesToString } from "@gnu-taler/taler-util"; import { Logger, bytesToString } from "@gnu-taler/taler-util";

View File

@ -21,7 +21,7 @@
"esModuleInterop": true, "esModuleInterop": true,
"importHelpers": true, "importHelpers": true,
"rootDir": "./src", "rootDir": "./src",
"typeRoots": ["./node_modules/@types"], "typeRoots": ["./node_modules/@types"]
}, },
"references": [ "references": [
{ {

View File

@ -24,7 +24,11 @@ import {
HttpResponse, HttpResponse,
Headers, Headers,
} from "@gnu-taler/taler-wallet-core"; } from "@gnu-taler/taler-wallet-core";
import { Logger, TalerErrorCode } from "@gnu-taler/taler-util"; import {
Logger,
RequestThrottler,
TalerErrorCode,
} from "@gnu-taler/taler-util";
const logger = new Logger("browserHttpLib"); const logger = new Logger("browserHttpLib");
@ -33,12 +37,32 @@ const logger = new Logger("browserHttpLib");
* browser's XMLHttpRequest. * browser's XMLHttpRequest.
*/ */
export class BrowserHttpLib implements HttpRequestLibrary { export class BrowserHttpLib implements HttpRequestLibrary {
fetch(url: string, options?: HttpRequestOptions): Promise<HttpResponse> { private throttle = new RequestThrottler();
const method = options?.method ?? "GET"; private throttlingEnabled = true;
fetch(
requestUrl: string,
options?: HttpRequestOptions,
): Promise<HttpResponse> {
const requestMethod = options?.method ?? "GET";
let requestBody = options?.body; let requestBody = options?.body;
if (this.throttlingEnabled && this.throttle.applyThrottle(requestUrl)) {
const parsedUrl = new URL(requestUrl);
throw OperationFailedError.fromCode(
TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
`request to origin ${parsedUrl.origin} was throttled`,
{
requestMethod,
requestUrl,
throttleStats: this.throttle.getThrottleStats(requestUrl),
},
);
}
return new Promise<HttpResponse>((resolve, reject) => { return new Promise<HttpResponse>((resolve, reject) => {
const myRequest = new XMLHttpRequest(); const myRequest = new XMLHttpRequest();
myRequest.open(method, url); myRequest.open(requestMethod, requestUrl);
if (options?.headers) { if (options?.headers) {
for (const headerName in options.headers) { for (const headerName in options.headers) {
myRequest.setRequestHeader(headerName, options.headers[headerName]); myRequest.setRequestHeader(headerName, options.headers[headerName]);
@ -58,7 +82,7 @@ export class BrowserHttpLib implements HttpRequestLibrary {
TalerErrorCode.WALLET_NETWORK_ERROR, TalerErrorCode.WALLET_NETWORK_ERROR,
"Could not make request", "Could not make request",
{ {
requestUrl: url, requestUrl: requestUrl,
}, },
), ),
); );
@ -71,7 +95,7 @@ export class BrowserHttpLib implements HttpRequestLibrary {
TalerErrorCode.WALLET_NETWORK_ERROR, TalerErrorCode.WALLET_NETWORK_ERROR,
"HTTP request failed (status 0, maybe URI scheme was wrong?)", "HTTP request failed (status 0, maybe URI scheme was wrong?)",
{ {
requestUrl: url, requestUrl: requestUrl,
}, },
); );
reject(exc); reject(exc);
@ -92,7 +116,7 @@ export class BrowserHttpLib implements HttpRequestLibrary {
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
"Invalid JSON from HTTP response", "Invalid JSON from HTTP response",
{ {
requestUrl: url, requestUrl: requestUrl,
httpStatusCode: myRequest.status, httpStatusCode: myRequest.status,
}, },
); );
@ -102,7 +126,7 @@ export class BrowserHttpLib implements HttpRequestLibrary {
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
"Invalid JSON from HTTP response", "Invalid JSON from HTTP response",
{ {
requestUrl: url, requestUrl: requestUrl,
httpStatusCode: myRequest.status, httpStatusCode: myRequest.status,
}, },
); );
@ -126,10 +150,10 @@ export class BrowserHttpLib implements HttpRequestLibrary {
headerMap.set(headerName, value); headerMap.set(headerName, value);
}); });
const resp: HttpResponse = { const resp: HttpResponse = {
requestUrl: url, requestUrl: requestUrl,
status: myRequest.status, status: myRequest.status,
headers: headerMap, headers: headerMap,
requestMethod: method, requestMethod: requestMethod,
json: makeJson, json: makeJson,
text: makeText, text: makeText,
bytes: async () => myRequest.response, bytes: async () => myRequest.response,

View File

@ -17,37 +17,55 @@
/** /**
* Imports. * Imports.
*/ */
import { Logger, TalerErrorCode } from "@gnu-taler/taler-util"; import { RequestThrottler, TalerErrorCode } from "@gnu-taler/taler-util";
import { import {
Headers, HttpRequestLibrary, Headers,
HttpRequestLibrary,
HttpRequestOptions, HttpRequestOptions,
HttpResponse, HttpResponse,
OperationFailedError OperationFailedError,
} from "@gnu-taler/taler-wallet-core"; } from "@gnu-taler/taler-wallet-core";
const logger = new Logger("browserHttpLib");
/** /**
* An implementation of the [[HttpRequestLibrary]] using the * An implementation of the [[HttpRequestLibrary]] using the
* browser's XMLHttpRequest. * browser's XMLHttpRequest.
*/ */
export class ServiceWorkerHttpLib implements HttpRequestLibrary { export class ServiceWorkerHttpLib implements HttpRequestLibrary {
async fetch(requestUrl: string, options?: HttpRequestOptions): Promise<HttpResponse> { private throttle = new RequestThrottler();
private throttlingEnabled = true;
async fetch(
requestUrl: string,
options?: HttpRequestOptions,
): Promise<HttpResponse> {
const requestMethod = options?.method ?? "GET"; const requestMethod = options?.method ?? "GET";
const requestBody = options?.body; const requestBody = options?.body;
const requestHeader = options?.headers; const requestHeader = options?.headers;
if (this.throttlingEnabled && this.throttle.applyThrottle(requestUrl)) {
const parsedUrl = new URL(requestUrl);
throw OperationFailedError.fromCode(
TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
`request to origin ${parsedUrl.origin} was throttled`,
{
requestMethod,
requestUrl,
throttleStats: this.throttle.getThrottleStats(requestUrl),
},
);
}
const response = await fetch(requestUrl, { const response = await fetch(requestUrl, {
headers: requestHeader, headers: requestHeader,
body: requestBody, body: requestBody,
method: requestMethod, method: requestMethod,
// timeout: options?.timeout // timeout: options?.timeout
}) });
const headerMap = new Headers(); const headerMap = new Headers();
response.headers.forEach((value, key) => { response.headers.forEach((value, key) => {
headerMap.set(key, value); headerMap.set(key, value);
}) });
return { return {
headers: headerMap, headers: headerMap,
status: response.status, status: response.status,
@ -56,11 +74,9 @@ export class ServiceWorkerHttpLib implements HttpRequestLibrary {
json: makeJsonHandler(response, requestUrl), json: makeJsonHandler(response, requestUrl),
text: makeTextHandler(response, requestUrl), text: makeTextHandler(response, requestUrl),
bytes: async () => (await response.blob()).arrayBuffer(), bytes: async () => (await response.blob()).arrayBuffer(),
} };
} }
get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> { get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
return this.fetch(url, { return this.fetch(url, {
method: "GET", method: "GET",
@ -89,7 +105,7 @@ function makeTextHandler(response: Response, requestUrl: string) {
return async function getJsonFromResponse(): Promise<any> { return async function getJsonFromResponse(): Promise<any> {
let respText; let respText;
try { try {
respText = await response.text() respText = await response.text();
} catch (e) { } catch (e) {
throw OperationFailedError.fromCode( throw OperationFailedError.fromCode(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
@ -100,15 +116,15 @@ function makeTextHandler(response: Response, requestUrl: string) {
}, },
); );
} }
return respText return respText;
} };
} }
function makeJsonHandler(response: Response, requestUrl: string) { function makeJsonHandler(response: Response, requestUrl: string) {
return async function getJsonFromResponse(): Promise<any> { return async function getJsonFromResponse(): Promise<any> {
let responseJson; let responseJson;
try { try {
responseJson = await response.json() responseJson = await response.json();
} catch (e) { } catch (e) {
throw OperationFailedError.fromCode( throw OperationFailedError.fromCode(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
@ -129,7 +145,6 @@ function makeJsonHandler(response: Response, requestUrl: string) {
}, },
); );
} }
return responseJson return responseJson;
} };
} }