/*
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 { Logger } from "@gnu-taler/taler-util";
import { TalerError } from "./errors.js";
import { encodeBody, getDefaultHeaders, HttpLibArgs } from "./http-common.js";
import {
Headers,
HttpRequestLibrary,
HttpRequestOptions,
HttpResponse,
} from "./http.js";
import { RequestThrottler, TalerErrorCode, URL } from "./index.js";
import { qjsOs } from "./qtart.js";
const logger = new Logger("http-impl.qtart.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;
private requireTls = false;
constructor(args?: HttpLibArgs) {
this.throttlingEnabled = args?.enableThrottling ?? false;
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 {
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), ...opt?.headers };
let headersList: string[] = [];
for (let headerName of Object.keys(requestHeadersMap)) {
headersList.push(`${headerName}: ${requestHeadersMap[headerName]}`);
}
if (method === "POST") {
data = encodeBody(opt?.body);
}
const res = await qjsOs.fetchHttp(url, {
method,
data,
headers: headersList,
});
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,
};
}
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,
});
}
}