diff options
Diffstat (limited to 'packages/taler-util/src/http-impl.qtart.ts')
-rw-r--r-- | packages/taler-util/src/http-impl.qtart.ts | 211 |
1 files changed, 211 insertions, 0 deletions
diff --git a/packages/taler-util/src/http-impl.qtart.ts b/packages/taler-util/src/http-impl.qtart.ts new file mode 100644 index 000000000..b4e4ebbe7 --- /dev/null +++ b/packages/taler-util/src/http-impl.qtart.ts @@ -0,0 +1,211 @@ +/* + This file is part of GNU Taler + (C) 2019 Taler Systems S.A. + + 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/> + + SPDX-License-Identifier: AGPL3.0-or-later +*/ + +/** + * Imports. + */ +import { Logger, openPromise } from "@gnu-taler/taler-util"; +import { TalerError } from "./errors.js"; +import { HttpLibArgs, encodeBody, getDefaultHeaders } from "./http-common.js"; +import { + Headers, + HttpRequestLibrary, + HttpRequestOptions, + HttpResponse, +} from "./http.js"; +import { RequestThrottler, TalerErrorCode, URL } from "./index.js"; +import { QjsHttpResp, qjsOs } from "./qtart.js"; + +const logger = new Logger("http-impl.qtart.ts"); + +const textDecoder = new TextDecoder(); + +export class RequestTimeoutError extends Error { + public constructor() { + super("Request timed out"); + Object.setPrototypeOf(this, RequestTimeoutError.prototype); + } +} + +export class RequestCancelledError extends Error { + public constructor() { + super("Request cancelled"); + Object.setPrototypeOf(this, RequestCancelledError.prototype); + } +} + +/** + * Implementation of the HTTP request library interface for node. + */ +export class HttpLibImpl implements HttpRequestLibrary { + private throttle = new RequestThrottler(); + private throttlingEnabled = true; + private requireTls = false; + + constructor(args?: HttpLibArgs) { + this.throttlingEnabled = args?.enableThrottling ?? true; + this.requireTls = args?.requireTls ?? false; + } + + /** + * Set whether requests should be throttled. + */ + setThrottling(enabled: boolean): void { + this.throttlingEnabled = enabled; + } + + async fetch(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> { + const method = (opt?.method ?? "GET").toUpperCase(); + + logger.trace(`Requesting ${method} ${url}`); + + const parsedUrl = new URL(url); + if (this.throttlingEnabled && this.throttle.applyThrottle(url)) { + throw TalerError.fromDetail( + TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED, + { + requestMethod: method, + requestUrl: url, + throttleStats: this.throttle.getThrottleStats(url), + }, + `request to origin ${parsedUrl.origin} was throttled`, + ); + } + if (this.requireTls && parsedUrl.protocol !== "https:") { + throw TalerError.fromDetail( + TalerErrorCode.WALLET_NETWORK_ERROR, + { + requestMethod: method, + requestUrl: url, + }, + `request to ${parsedUrl.origin} is not possible with protocol ${parsedUrl.protocol}`, + ); + } + + let data: ArrayBuffer | undefined = undefined; + const requestHeadersMap = getDefaultHeaders(method); + if (opt?.headers) { + Object.entries(opt?.headers).forEach(([key, value]) => { + if (value === undefined) return; + requestHeadersMap[key] = value + }) + } + let headersList: string[] = []; + for (let headerName of Object.keys(requestHeadersMap)) { + headersList.push(`${headerName}: ${requestHeadersMap[headerName]}`); + } + if (method === "POST") { + data = encodeBody(opt?.body); + } + + const cancelPromCap = openPromise<QjsHttpResp>(); + + // Just like WHATWG fetch(), the qjs http client doesn't + // really support cancellation, so cancellation here just + // means that the result is ignored! + const fetchProm = qjsOs.fetchHttp(url, { + method, + data, + headers: headersList, + }); + + let timeoutHandle: any = undefined; + let cancelCancelledHandler: (() => void) | undefined = undefined; + + if (opt?.timeout && opt.timeout.d_ms !== "forever") { + timeoutHandle = setTimeout(() => { + cancelPromCap.reject(new RequestTimeoutError()); + }, opt.timeout.d_ms); + } + + if (opt?.cancellationToken) { + cancelCancelledHandler = opt.cancellationToken.onCancelled(() => { + cancelPromCap.reject(new RequestCancelledError()); + }); + } + + let res: QjsHttpResp; + try { + res = await Promise.race([fetchProm, cancelPromCap.promise]); + } catch (e) { + if (e instanceof RequestCancelledError) { + throw TalerError.fromDetail( + TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR, + { + requestUrl: url, + requestMethod: method, + httpStatusCode: 0, + }, + `Request cancelled`, + ); + } + if (e instanceof RequestTimeoutError) { + throw TalerError.fromDetail( + TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR, + { + requestUrl: url, + requestMethod: method, + httpStatusCode: 0, + }, + `Request timed out`, + ); + } + throw e; + } + + if (timeoutHandle != null) { + clearTimeout(timeoutHandle); + } + + if (cancelCancelledHandler != null) { + cancelCancelledHandler(); + } + + const headers: Headers = new Headers(); + + if (res.headers) { + for (const headerStr of res.headers) { + const splitPos = headerStr.indexOf(":"); + if (splitPos < 0) { + continue; + } + const headerName = headerStr.slice(0, splitPos).trim().toLowerCase(); + const headerValue = headerStr.slice(splitPos + 1).trim(); + headers.set(headerName, headerValue); + } + } + + return { + requestMethod: method, + headers, + async bytes() { + return res.data; + }, + json() { + const text = textDecoder.decode(res.data); + return JSON.parse(text); + }, + async text() { + const text = textDecoder.decode(res.data); + return text; + }, + requestUrl: url, + status: res.status, + }; + } +} |