wallet: throttle all http requests
even from browsers / service workers
This commit is contained in:
parent
d0376d9e68
commit
1d1c847b79
@ -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");
|
||||||
|
|
@ -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";
|
||||||
|
@ -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";
|
||||||
|
@ -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": [
|
||||||
{
|
{
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user