/* 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 */ /** * Fault injection proxy. * * @author Florian Dold */ /** * Imports */ import * as http from "http"; import { URL } from "url"; import { GlobalTestState, ExchangeService, ExchangeServiceInterface, MerchantServiceInterface, MerchantService, } from "./harness"; export interface FaultProxyConfig { inboundPort: number; targetPort: number; } /** * Fault injection context. Modified by fault injection functions. */ export interface FaultInjectionRequestContext { requestUrl: string; method: string; requestHeaders: Record; requestBody?: Buffer; dropRequest: boolean; } export interface FaultInjectionResponseContext { request: FaultInjectionRequestContext; statusCode: number; responseHeaders: Record; responseBody: Buffer | undefined; dropResponse: boolean; } export interface FaultSpec { modifyRequest?: (ctx: FaultInjectionRequestContext) => Promise; modifyResponse?: (ctx: FaultInjectionResponseContext) => Promise; } 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) { 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 accomodate 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; } }