2022-01-16 21:54:48 +01:00
|
|
|
/*
|
|
|
|
This file is part of GNU Taler
|
2022-06-06 17:05:26 +02:00
|
|
|
(C) 2022 Taler Systems S.A.
|
2022-01-16 21:54:48 +01:00
|
|
|
|
|
|
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
|
|
|
terms of the GNU General Public License as published by the Free Software
|
|
|
|
Foundation; either version 3, or (at your option) any later version.
|
|
|
|
|
|
|
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
|
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
|
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License along with
|
|
|
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Imports.
|
|
|
|
*/
|
2022-12-20 17:44:42 +01:00
|
|
|
import {
|
|
|
|
Logger,
|
|
|
|
RequestThrottler,
|
|
|
|
TalerErrorCode,
|
|
|
|
} from "@gnu-taler/taler-util";
|
2022-01-16 21:54:48 +01:00
|
|
|
import {
|
2022-03-08 19:19:29 +01:00
|
|
|
Headers,
|
|
|
|
HttpRequestLibrary,
|
2022-01-16 21:54:48 +01:00
|
|
|
HttpRequestOptions,
|
|
|
|
HttpResponse,
|
2022-03-22 21:16:38 +01:00
|
|
|
TalerError,
|
2022-01-16 21:54:48 +01:00
|
|
|
} from "@gnu-taler/taler-wallet-core";
|
|
|
|
|
|
|
|
/**
|
|
|
|
* An implementation of the [[HttpRequestLibrary]] using the
|
|
|
|
* browser's XMLHttpRequest.
|
|
|
|
*/
|
|
|
|
export class ServiceWorkerHttpLib implements HttpRequestLibrary {
|
2022-03-08 19:19:29 +01:00
|
|
|
private throttle = new RequestThrottler();
|
|
|
|
private throttlingEnabled = true;
|
|
|
|
|
|
|
|
async fetch(
|
|
|
|
requestUrl: string,
|
|
|
|
options?: HttpRequestOptions,
|
|
|
|
): Promise<HttpResponse> {
|
2022-01-16 21:54:48 +01:00
|
|
|
const requestMethod = options?.method ?? "GET";
|
|
|
|
const requestBody = options?.body;
|
|
|
|
const requestHeader = options?.headers;
|
2022-12-20 17:44:42 +01:00
|
|
|
const requestTimeout = options?.timeout ?? { d_ms: 2 * 1000 };
|
2022-01-16 21:54:48 +01:00
|
|
|
|
2022-03-08 19:19:29 +01:00
|
|
|
if (this.throttlingEnabled && this.throttle.applyThrottle(requestUrl)) {
|
|
|
|
const parsedUrl = new URL(requestUrl);
|
2022-03-22 21:16:38 +01:00
|
|
|
throw TalerError.fromDetail(
|
2022-03-08 19:19:29 +01:00
|
|
|
TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
|
|
|
|
{
|
|
|
|
requestMethod,
|
|
|
|
requestUrl,
|
|
|
|
throttleStats: this.throttle.getThrottleStats(requestUrl),
|
|
|
|
},
|
2022-03-22 21:16:38 +01:00
|
|
|
`request to origin ${parsedUrl.origin} was throttled`,
|
2022-03-08 19:19:29 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-11-12 19:18:55 +01:00
|
|
|
let myBody: BodyInit | undefined = undefined;
|
|
|
|
if (requestBody != null) {
|
|
|
|
if (typeof requestBody === "string") {
|
|
|
|
myBody = requestBody;
|
|
|
|
} else if (requestBody instanceof ArrayBuffer) {
|
|
|
|
myBody = requestBody;
|
|
|
|
} else if (ArrayBuffer.isView(requestBody)) {
|
|
|
|
myBody = requestBody;
|
|
|
|
} else if (typeof requestBody === "object") {
|
|
|
|
myBody = JSON.stringify(myBody);
|
|
|
|
} else {
|
|
|
|
throw Error("unsupported request body type");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-20 17:44:42 +01:00
|
|
|
const controller = new AbortController();
|
|
|
|
let timeoutId: any | undefined;
|
|
|
|
if (requestTimeout.d_ms !== "forever") {
|
|
|
|
timeoutId = setTimeout(() => {
|
|
|
|
controller.abort(TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT);
|
|
|
|
}, requestTimeout.d_ms);
|
|
|
|
}
|
2022-01-16 21:54:48 +01:00
|
|
|
|
2022-12-20 17:44:42 +01:00
|
|
|
try {
|
|
|
|
const response = await fetch(requestUrl, {
|
|
|
|
headers: requestHeader,
|
|
|
|
body: myBody,
|
|
|
|
method: requestMethod,
|
|
|
|
signal: controller.signal,
|
|
|
|
});
|
|
|
|
|
|
|
|
if (timeoutId) {
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
}
|
|
|
|
|
|
|
|
const headerMap = new Headers();
|
|
|
|
response.headers.forEach((value, key) => {
|
|
|
|
headerMap.set(key, value);
|
|
|
|
});
|
|
|
|
return {
|
|
|
|
headers: headerMap,
|
|
|
|
status: response.status,
|
|
|
|
requestMethod,
|
|
|
|
requestUrl,
|
|
|
|
json: makeJsonHandler(response, requestUrl),
|
|
|
|
text: makeTextHandler(response, requestUrl),
|
|
|
|
bytes: async () => (await response.blob()).arrayBuffer(),
|
|
|
|
};
|
|
|
|
} catch (e) {
|
|
|
|
if (controller.signal) {
|
|
|
|
throw TalerError.fromDetail(
|
|
|
|
controller.signal.reason,
|
|
|
|
{},
|
|
|
|
`request to ${requestUrl} timed out`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
throw e;
|
|
|
|
}
|
2022-01-16 21:54:48 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
|
|
|
|
return this.fetch(url, {
|
|
|
|
method: "GET",
|
|
|
|
...opt,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-04-23 17:44:29 +02:00
|
|
|
// FIXME: "Content-Type: application/json" goes here,
|
|
|
|
// after Sebastian suggestion.
|
2022-01-16 21:54:48 +01:00
|
|
|
postJson(
|
|
|
|
url: string,
|
|
|
|
body: any,
|
|
|
|
opt?: HttpRequestOptions,
|
|
|
|
): Promise<HttpResponse> {
|
|
|
|
return this.fetch(url, {
|
|
|
|
method: "POST",
|
2022-06-06 05:09:25 +02:00
|
|
|
headers: { "Content-Type": "application/json" },
|
2022-01-16 21:54:48 +01:00
|
|
|
body: JSON.stringify(body),
|
|
|
|
...opt,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
stop(): void {
|
|
|
|
// Nothing to do
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function makeTextHandler(response: Response, requestUrl: string) {
|
|
|
|
return async function getJsonFromResponse(): Promise<any> {
|
|
|
|
let respText;
|
|
|
|
try {
|
2022-03-08 19:19:29 +01:00
|
|
|
respText = await response.text();
|
2022-01-16 21:54:48 +01:00
|
|
|
} catch (e) {
|
2022-03-22 21:16:38 +01:00
|
|
|
throw TalerError.fromDetail(
|
2022-01-16 21:54:48 +01:00
|
|
|
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
|
|
|
|
{
|
|
|
|
requestUrl,
|
|
|
|
httpStatusCode: response.status,
|
|
|
|
},
|
2022-03-22 21:16:38 +01:00
|
|
|
"Invalid JSON from HTTP response",
|
2022-01-16 21:54:48 +01:00
|
|
|
);
|
|
|
|
}
|
2022-03-08 19:19:29 +01:00
|
|
|
return respText;
|
|
|
|
};
|
2022-01-16 21:54:48 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
function makeJsonHandler(response: Response, requestUrl: string) {
|
|
|
|
return async function getJsonFromResponse(): Promise<any> {
|
|
|
|
let responseJson;
|
|
|
|
try {
|
2022-03-08 19:19:29 +01:00
|
|
|
responseJson = await response.json();
|
2022-01-16 21:54:48 +01:00
|
|
|
} catch (e) {
|
2022-03-22 21:16:38 +01:00
|
|
|
throw TalerError.fromDetail(
|
2022-01-16 21:54:48 +01:00
|
|
|
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
|
|
|
|
{
|
|
|
|
requestUrl,
|
|
|
|
httpStatusCode: response.status,
|
|
|
|
},
|
2022-03-22 21:16:38 +01:00
|
|
|
"Invalid JSON from HTTP response",
|
2022-01-16 21:54:48 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
if (responseJson === null || typeof responseJson !== "object") {
|
2022-03-22 21:16:38 +01:00
|
|
|
throw TalerError.fromDetail(
|
2022-01-16 21:54:48 +01:00
|
|
|
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
|
|
|
|
{
|
|
|
|
requestUrl,
|
|
|
|
httpStatusCode: response.status,
|
|
|
|
},
|
2022-03-22 21:16:38 +01:00
|
|
|
"Invalid JSON from HTTP response",
|
2022-01-16 21:54:48 +01:00
|
|
|
);
|
|
|
|
}
|
2022-03-08 19:19:29 +01:00
|
|
|
return responseJson;
|
|
|
|
};
|
2022-01-16 21:54:48 +01:00
|
|
|
}
|