223 lines
6.4 KiB
TypeScript
223 lines
6.4 KiB
TypeScript
|
/*
|
||
|
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,
|
||
|
BankService,
|
||
|
ExchangeServiceInterface,
|
||
|
} 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<string, string | string[] | undefined>;
|
||
|
requestBody?: Buffer;
|
||
|
dropRequest: boolean;
|
||
|
}
|
||
|
|
||
|
export interface FaultInjectionResponseContext {
|
||
|
request: FaultInjectionRequestContext;
|
||
|
statusCode: number;
|
||
|
responseHeaders: Record<string, string | string[] | undefined>;
|
||
|
responseBody: Buffer | undefined;
|
||
|
dropResponse: boolean;
|
||
|
}
|
||
|
|
||
|
export interface FaultSpec {
|
||
|
modifyRequest?: (ctx: FaultInjectionRequestContext) => void;
|
||
|
modifyResponse?: (ctx: FaultInjectionResponseContext) => 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://locahost:${this.faultProxyConfig.inboundPort}${req.url}`;
|
||
|
console.log("request for", new URL(requestUrl));
|
||
|
req.on("data", (chunk) => {
|
||
|
requestChunks.push(chunk);
|
||
|
});
|
||
|
req.on("end", () => {
|
||
|
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) {
|
||
|
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", () => {
|
||
|
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) {
|
||
|
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);
|
||
|
}
|
||
|
|
||
|
clearFault() {
|
||
|
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;
|
||
|
}
|
||
|
}
|