wallet-core/packages/taler-util/src/http-impl.node.ts

214 lines
5.7 KiB
TypeScript
Raw Normal View History

2023-02-15 23:32:42 +01:00
/*
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 * as http from "node:http";
2023-02-16 13:54:19 +01:00
import * as https from "node:https";
2023-02-15 23:32:42 +01:00
import { RequestOptions } from "node:http";
import { TalerError } from "./errors.js";
import { encodeBody, getDefaultHeaders, HttpLibArgs } from "./http-common.js";
2023-02-15 23:32:42 +01:00
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;
private allowHttp = false;
2023-02-15 23:32:42 +01:00
constructor(args?: HttpLibArgs) {
this.throttlingEnabled = args?.enableThrottling ?? false;
this.allowHttp = args?.allowHttp ?? false;
2023-02-15 23:32:42 +01:00
}
/**
* 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?.toUpperCase() ?? "GET";
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.allowHttp && 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}`,
);
}
2023-02-15 23:32:42 +01:00
let timeoutMs: number | undefined;
if (typeof opt?.timeout?.d_ms === "number") {
timeoutMs = opt.timeout.d_ms;
} else {
timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS;
}
const requestHeadersMap = { ...getDefaultHeaders(method), ...opt?.headers };
2023-02-15 23:32:42 +01:00
let reqBody: ArrayBuffer | undefined;
if (opt?.method == "POST") {
reqBody = encodeBody(opt.body);
}
2023-02-16 13:54:19 +01:00
let path = parsedUrl.pathname;
if (parsedUrl.search != null) {
path += parsedUrl.search;
}
let protocol: string;
if (parsedUrl.protocol === "https:") {
protocol = "https:";
} else if (parsedUrl.protocol === "http:") {
protocol = "http:";
} else {
throw Error(`unsupported protocol (${parsedUrl.protocol})`);
}
2023-02-15 23:32:42 +01:00
const options: RequestOptions = {
2023-02-16 13:54:19 +01:00
protocol,
2023-02-15 23:32:42 +01:00
port: parsedUrl.port,
2023-02-16 12:28:56 +01:00
host: parsedUrl.hostname,
2023-02-15 23:32:42 +01:00
method: method,
2023-02-16 13:54:19 +01:00
path,
headers: requestHeadersMap,
2023-02-15 23:32:42 +01:00
};
const chunks: Uint8Array[] = [];
return new Promise((resolve, reject) => {
2023-02-16 13:54:19 +01:00
const handler = (res: http.IncomingMessage) => {
2023-02-15 23:32:42 +01:00
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);
});
2023-02-16 13:54:19 +01:00
};
let req: http.ClientRequest;
if (options.protocol === "http:") {
req = http.request(options, handler);
} else if (options.protocol === "https:") {
req = https.request(options, handler);
} else {
throw new Error(`unsupported protocol ${options.protocol}`);
}
2023-02-15 23:32:42 +01:00
req.on("error", (e: Error) => {
reject(e);
});
2023-02-15 23:32:42 +01:00
if (reqBody) {
2023-02-16 12:28:56 +01:00
req.write(new Uint8Array(reqBody));
2023-02-15 23:32:42 +01:00
}
req.end();
});
}
async get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
return this.fetch(url, {
method: "GET",
...opt,
});
}
async postJson(
url: string,
body: any,
opt?: HttpRequestOptions,
): Promise<HttpResponse> {
return this.fetch(url, {
method: "POST",
body,
...opt,
});
}
}