/* 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 * as http from "node:http"; import { RequestOptions } from "node:http"; import { TalerError } from "./errors.js"; import { encodeBody, HttpLibArgs } from "./http-common.js"; import { DEFAULT_REQUEST_TIMEOUT_MS, Headers, HttpRequestLibrary, HttpRequestOptions, HttpResponse, } from "./http.js"; import { Logger, RequestThrottler, TalerErrorCode, typedArrayConcat, URL, } from "./index.js"; const logger = new Logger("http-impl.node.ts"); const textDecoder = new TextDecoder(); /** * Implementation of the HTTP request library interface for node. */ export class HttpLibImpl implements HttpRequestLibrary { private throttle = new RequestThrottler(); private throttlingEnabled = true; constructor(args?: HttpLibArgs) { this.throttlingEnabled = args?.enableThrottling ?? false; } /** * Set whether requests should be throttled. */ setThrottling(enabled: boolean): void { this.throttlingEnabled = enabled; } async fetch(url: string, opt?: HttpRequestOptions): Promise { const method = opt?.method?.toUpperCase() ?? "GET"; let body = opt?.body; 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`, ); } let timeoutMs: number | undefined; if (typeof opt?.timeout?.d_ms === "number") { timeoutMs = opt.timeout.d_ms; } else { timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS; } const headers = { ...opt?.headers }; headers["Content-Type"] = "application/json"; let reqBody: ArrayBuffer | undefined; if (opt?.method == "POST") { reqBody = encodeBody(opt.body); } const options: RequestOptions = { protocol: parsedUrl.protocol, port: parsedUrl.port, host: parsedUrl.hostname, method: method, path: parsedUrl.pathname, headers: opt?.headers, }; const chunks: Uint8Array[] = []; return new Promise((resolve, reject) => { const req = http.request(options, (res) => { res.on("data", (d) => { chunks.push(d); }); res.on("end", () => { const headers: Headers = new Headers(); for (const [k, v] of Object.entries(res.headers)) { if (!v) { continue; } if (typeof v === "string") { headers.set(k, v); } else { headers.set(k, v.join(", ")); } } const data = typedArrayConcat(chunks); const resp: HttpResponse = { requestMethod: method, requestUrl: parsedUrl.href, status: res.statusCode || 0, headers, async bytes() { return data; }, json() { const text = textDecoder.decode(data); return JSON.parse(text); }, async text() { const text = textDecoder.decode(data); return text; }, }; resolve(resp); }); res.on("error", (e) => { reject(e); }); }); if (reqBody) { req.write(new Uint8Array(reqBody)); } req.end(); }); } 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, }); } }