summaryrefslogtreecommitdiff
path: root/packages/taler-harness/src/harness/faultInjection.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-harness/src/harness/faultInjection.ts')
-rw-r--r--packages/taler-harness/src/harness/faultInjection.ts271
1 files changed, 271 insertions, 0 deletions
diff --git a/packages/taler-harness/src/harness/faultInjection.ts b/packages/taler-harness/src/harness/faultInjection.ts
new file mode 100644
index 000000000..f4d7fc4b9
--- /dev/null
+++ b/packages/taler-harness/src/harness/faultInjection.ts
@@ -0,0 +1,271 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 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/>
+ */
+
+/**
+ * Fault injection proxy.
+ *
+ * @author Florian Dold <dold@taler.net>
+ */
+
+/**
+ * Imports
+ */
+import * as http from "http";
+import { URL } from "url";
+import {
+ GlobalTestState,
+ ExchangeService,
+ ExchangeServiceInterface,
+ MerchantServiceInterface,
+ MerchantService,
+} from "../harness/harness.js";
+
+export interface FaultProxyConfig {
+ inboundPort: number;
+ targetPort: number;
+}
+
+/**
+ * Fault injection context. Modified by fault injection functions.
+ */
+export interface FaultInjectionRequestContext {
+ requestUrl: string;
+ method: string;
+ requestHeaders: Record<string, string | string[] | undefined>;
+ requestBody?: Buffer;
+ dropRequest: boolean;
+ // These are only used when the request is dropped
+ substituteResponseBody?: Buffer;
+ substituteResponseStatusCode?: number;
+ substituteResponseHeaders?: Record<string, string | string[] | undefined>;
+}
+
+export interface FaultInjectionResponseContext {
+ request: FaultInjectionRequestContext;
+ statusCode: number;
+ responseHeaders: Record<string, string | string[] | undefined>;
+ responseBody: Buffer | undefined;
+ dropResponse: boolean;
+}
+
+export interface FaultSpec {
+ modifyRequest?: (ctx: FaultInjectionRequestContext) => Promise<void>;
+ modifyResponse?: (ctx: FaultInjectionResponseContext) => Promise<void>;
+}
+
+export class FaultProxy {
+ constructor(
+ private globalTestState: GlobalTestState,
+ private faultProxyConfig: FaultProxyConfig,
+ ) {}
+
+ private currentFaultSpecs: FaultSpec[] = [];
+
+ start() {
+ const server = http.createServer((req, res) => {
+ const requestChunks: Buffer[] = [];
+ const requestUrl = `http://localhost:${this.faultProxyConfig.inboundPort}${req.url}`;
+ console.log("request for", new URL(requestUrl));
+ req.on("data", (chunk) => {
+ requestChunks.push(chunk);
+ });
+ req.on("end", async () => {
+ console.log("end of data");
+ let requestBuffer: Buffer | undefined;
+ if (requestChunks.length > 0) {
+ requestBuffer = Buffer.concat(requestChunks);
+ }
+ console.log("full request body", requestBuffer);
+
+ const faultReqContext: FaultInjectionRequestContext = {
+ dropRequest: false,
+ method: req.method!!,
+ requestHeaders: req.headers,
+ requestUrl,
+ requestBody: requestBuffer,
+ };
+
+ for (const faultSpec of this.currentFaultSpecs) {
+ if (faultSpec.modifyRequest) {
+ await faultSpec.modifyRequest(faultReqContext);
+ }
+ }
+
+ if (faultReqContext.dropRequest) {
+ if (faultReqContext.substituteResponseStatusCode) {
+ const statusCode = faultReqContext.substituteResponseStatusCode;
+ res.writeHead(
+ statusCode,
+ http.STATUS_CODES[statusCode],
+ faultReqContext.substituteResponseHeaders,
+ );
+ res.write(faultReqContext.substituteResponseBody);
+ res.end();
+ } else {
+ res.destroy();
+ }
+ return;
+ }
+
+ const faultedUrl = new URL(faultReqContext.requestUrl);
+
+ const proxyRequest = http.request({
+ method: faultReqContext.method,
+ host: "localhost",
+ port: this.faultProxyConfig.targetPort,
+ path: faultedUrl.pathname + faultedUrl.search,
+ headers: faultReqContext.requestHeaders,
+ });
+
+ console.log(
+ `proxying request to target path '${
+ faultedUrl.pathname + faultedUrl.search
+ }'`,
+ );
+
+ if (faultReqContext.requestBody) {
+ proxyRequest.write(faultReqContext.requestBody);
+ }
+ proxyRequest.end();
+ proxyRequest.on("response", (proxyResp) => {
+ console.log("gotten response from target", proxyResp.statusCode);
+ const respChunks: Buffer[] = [];
+ proxyResp.on("data", (proxyRespData) => {
+ respChunks.push(proxyRespData);
+ });
+ proxyResp.on("end", async () => {
+ console.log("end of target response");
+ let responseBuffer: Buffer | undefined;
+ if (respChunks.length > 0) {
+ responseBuffer = Buffer.concat(respChunks);
+ }
+ const faultRespContext: FaultInjectionResponseContext = {
+ request: faultReqContext,
+ dropResponse: false,
+ responseBody: responseBuffer,
+ responseHeaders: proxyResp.headers,
+ statusCode: proxyResp.statusCode!!,
+ };
+ for (const faultSpec of this.currentFaultSpecs) {
+ const modResponse = faultSpec.modifyResponse;
+ if (modResponse) {
+ await modResponse(faultRespContext);
+ }
+ }
+ if (faultRespContext.dropResponse) {
+ req.destroy();
+ return;
+ }
+ if (faultRespContext.responseBody) {
+ // We must accommodate for potentially changed content length
+ faultRespContext.responseHeaders[
+ "content-length"
+ ] = `${faultRespContext.responseBody.byteLength}`;
+ }
+ console.log("writing response head");
+ res.writeHead(
+ faultRespContext.statusCode,
+ http.STATUS_CODES[faultRespContext.statusCode],
+ faultRespContext.responseHeaders,
+ );
+ if (faultRespContext.responseBody) {
+ res.write(faultRespContext.responseBody);
+ }
+ res.end();
+ });
+ });
+ });
+ });
+
+ server.listen(this.faultProxyConfig.inboundPort);
+ this.globalTestState.servers.push(server);
+ }
+
+ addFault(f: FaultSpec) {
+ this.currentFaultSpecs.push(f);
+ }
+
+ clearAllFaults() {
+ this.currentFaultSpecs = [];
+ }
+}
+
+export class FaultInjectedExchangeService implements ExchangeServiceInterface {
+ baseUrl: string;
+ port: number;
+ faultProxy: FaultProxy;
+
+ get name(): string {
+ return this.innerExchange.name;
+ }
+
+ get masterPub(): string {
+ return this.innerExchange.masterPub;
+ }
+
+ private innerExchange: ExchangeService;
+
+ constructor(
+ t: GlobalTestState,
+ e: ExchangeService,
+ proxyInboundPort: number,
+ ) {
+ this.innerExchange = e;
+ this.faultProxy = new FaultProxy(t, {
+ inboundPort: proxyInboundPort,
+ targetPort: e.port,
+ });
+ this.faultProxy.start();
+
+ const exchangeUrl = new URL(e.baseUrl);
+ exchangeUrl.port = `${proxyInboundPort}`;
+ this.baseUrl = exchangeUrl.href;
+ this.port = proxyInboundPort;
+ }
+}
+
+export class FaultInjectedMerchantService implements MerchantServiceInterface {
+ baseUrl: string;
+ port: number;
+ faultProxy: FaultProxy;
+
+ get name(): string {
+ return this.innerMerchant.name;
+ }
+
+ private innerMerchant: MerchantService;
+ private inboundPort: number;
+
+ constructor(
+ t: GlobalTestState,
+ m: MerchantService,
+ proxyInboundPort: number,
+ ) {
+ this.innerMerchant = m;
+ this.faultProxy = new FaultProxy(t, {
+ inboundPort: proxyInboundPort,
+ targetPort: m.port,
+ });
+ this.faultProxy.start();
+ this.inboundPort = proxyInboundPort;
+ }
+
+ makeInstanceBaseUrl(instanceName?: string | undefined): string {
+ const url = new URL(this.innerMerchant.makeInstanceBaseUrl(instanceName));
+ url.port = `${this.inboundPort}`;
+ return url.href;
+ }
+}