/* 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 SPDX-License-Identifier: AGPL3.0-or-later */ /** * Imports. */ import { Headers, HttpRequestLibrary, HttpRequestOptions, HttpResponse, } from "../util/http"; import { RequestThrottler } from "../util/RequestThrottler"; import Axios, { AxiosResponse } from "axios"; import { OperationFailedError, makeErrorDetails } from "../operations/errors"; import { URL } from "../util/url"; import { Logger } from "../util/logging"; import { bytesToString } from "../crypto/talerCrypto"; import { TalerErrorCode } from "@gnu-taler/taler-util"; const logger = new Logger("NodeHttpLib.ts"); /** * Implementation of the HTTP request library interface for node. */ export class NodeHttpLib implements HttpRequestLibrary { private throttle = new RequestThrottler(); private throttlingEnabled = true; /** * Set whether requests should be throttled. */ setThrottling(enabled: boolean): void { this.throttlingEnabled = enabled; } async fetch(url: string, opt?: HttpRequestOptions): Promise { const method = opt?.method ?? "GET"; let body = opt?.body; const parsedUrl = new URL(url); if (this.throttlingEnabled && this.throttle.applyThrottle(url)) { throw OperationFailedError.fromCode( TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED, `request to origin ${parsedUrl.origin} was throttled`, { requestMethod: method, requestUrl: url, throttleStats: this.throttle.getThrottleStats(url), }, ); } let timeout: number | undefined; if (typeof opt?.timeout?.d_ms === "number") { timeout = opt.timeout.d_ms; } let resp: AxiosResponse; try { resp = await Axios({ method, url: url, responseType: "arraybuffer", headers: opt?.headers, validateStatus: () => true, transformResponse: (x) => x, data: body, timeout, }); } catch (e) { throw OperationFailedError.fromCode( TalerErrorCode.WALLET_NETWORK_ERROR, `${e.message}`, { requestUrl: url, requestMethod: method, }, ); } const makeText = async (): Promise => { const respText = new Uint8Array(resp.data); return bytesToString(respText); }; const makeJson = async (): Promise => { let responseJson; const respText = await makeText(); try { responseJson = JSON.parse(respText); } catch (e) { logger.trace(`invalid json: '${resp.data}'`); throw new OperationFailedError( makeErrorDetails( TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, "invalid JSON", { httpStatusCode: resp.status, requestUrl: url, requestMethod: method, }, ), ); } if (responseJson === null || typeof responseJson !== "object") { logger.trace(`invalid json (not an object): '${respText}'`); throw new OperationFailedError( makeErrorDetails( TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, "invalid JSON", { httpStatusCode: resp.status, requestUrl: url, requestMethod: method, }, ), ); } return responseJson; }; const makeBytes = async () => { if (typeof resp.data.byteLength !== "number") { throw Error("expected array buffer"); } const buf = resp.data; return buf; }; const headers = new Headers(); for (const hn of Object.keys(resp.headers)) { headers.set(hn, resp.headers[hn]); } return { requestUrl: url, requestMethod: method, headers, status: resp.status, text: makeText, json: makeJson, bytes: makeBytes, }; } async get(url: string, opt?: HttpRequestOptions): Promise { return this.fetch(url, { method: "GET", ...opt, }); } async postJson( url: string, body: any, opt?: HttpRequestOptions, ): Promise { return this.fetch(url, { method: "POST", body, ...opt, }); } }