@stuntman/server
Advanced tools
Comparing version 0.1.2 to 0.1.3
/// <reference types="node" /> | ||
import http from 'http'; | ||
import { Express as ExpressServer } from 'express'; | ||
import { NextFunction, Request, Response, Express as ExpressServer } from 'express'; | ||
import type * as Stuntman from '@stuntman/shared'; | ||
@@ -14,2 +14,5 @@ import LRUCache from 'lru-cache'; | ||
server: http.Server | null; | ||
auth: (req: Request, type: 'read' | 'write') => void; | ||
authReadOnly: (req: Request, res: Response, next: NextFunction) => void; | ||
authReadWrite: (req: Request, res: Response, next: NextFunction) => void; | ||
constructor(options: ApiOptions, webGuiOptions?: Stuntman.WebGuiConfig); | ||
@@ -16,0 +19,0 @@ private initWebGui; |
@@ -14,7 +14,11 @@ "use strict"; | ||
const serialize_javascript_1 = __importDefault(require("serialize-javascript")); | ||
const validatiors_1 = require("./validatiors"); | ||
const validators_1 = require("./validators"); | ||
const utils_1 = require("./utils"); | ||
const API_KEY_HEADER = 'x-api-key'; | ||
class API { | ||
constructor(options, webGuiOptions) { | ||
this.server = null; | ||
if (!options.apiKeyReadOnly !== !options.apiKeyReadWrite) { | ||
throw new Error('apiKeyReadOnly and apiKeyReadWrite options need to be set either both or none'); | ||
} | ||
this.options = options; | ||
@@ -25,2 +29,19 @@ this.trafficStore = (0, storage_1.getTrafficStore)(this.options.mockUuid); | ||
this.apiApp.use(express_1.default.text()); | ||
this.auth = (req, type) => { | ||
const hasValidReadKey = req.header(API_KEY_HEADER) === this.options.apiKeyReadOnly; | ||
const hasValidWriteKey = req.header(API_KEY_HEADER) === this.options.apiKeyReadWrite; | ||
const hasValidKey = type === 'read' ? hasValidReadKey || hasValidWriteKey : hasValidWriteKey; | ||
if (!hasValidKey) { | ||
throw new shared_1.AppError({ httpCode: shared_1.HttpCode.UNAUTHORIZED, message: 'unauthorized' }); | ||
} | ||
return; | ||
}; | ||
this.authReadOnly = (req, res, next) => { | ||
this.auth(req, 'read'); | ||
next(); | ||
}; | ||
this.authReadWrite = (req, res, next) => { | ||
this.auth(req, 'write'); | ||
next(); | ||
}; | ||
this.apiApp.use((req, res, next) => { | ||
@@ -30,19 +51,19 @@ requestContext_1.default.bind(req, this.options.mockUuid); | ||
}); | ||
this.apiApp.get('/rule', async (req, res) => { | ||
this.apiApp.get('/rule', this.authReadOnly, async (req, res) => { | ||
res.send((0, shared_1.stringify)(await (0, ruleExecutor_1.getRuleExecutor)(this.options.mockUuid).getRules())); | ||
}); | ||
this.apiApp.get('/rule/:ruleId', async (req, res) => { | ||
this.apiApp.get('/rule/:ruleId', this.authReadOnly, async (req, res) => { | ||
res.send((0, shared_1.stringify)(await (0, ruleExecutor_1.getRuleExecutor)(this.options.mockUuid).getRule(req.params.ruleId))); | ||
}); | ||
this.apiApp.get('/rule/:ruleId/disable', (req, res) => { | ||
this.apiApp.get('/rule/:ruleId/disable', this.authReadWrite, (req, res) => { | ||
(0, ruleExecutor_1.getRuleExecutor)(this.options.mockUuid).disableRule(req.params.ruleId); | ||
res.send(); | ||
}); | ||
this.apiApp.get('/rule/:ruleId/enable', (req, res) => { | ||
this.apiApp.get('/rule/:ruleId/enable', this.authReadWrite, (req, res) => { | ||
(0, ruleExecutor_1.getRuleExecutor)(this.options.mockUuid).enableRule(req.params.ruleId); | ||
res.send(); | ||
}); | ||
this.apiApp.post('/rule', async (req, res) => { | ||
this.apiApp.post('/rule', this.authReadWrite, async (req, res) => { | ||
const deserializedRule = (0, utils_1.deserializeRule)(req.body); | ||
(0, validatiors_1.validateDeserializedRule)(deserializedRule); | ||
(0, validators_1.validateDeserializedRule)(deserializedRule); | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
@@ -53,7 +74,7 @@ // @ts-ignore | ||
}); | ||
this.apiApp.get('/rule/:ruleId/remove', async (req, res) => { | ||
this.apiApp.get('/rule/:ruleId/remove', this.authReadWrite, async (req, res) => { | ||
await (0, ruleExecutor_1.getRuleExecutor)(this.options.mockUuid).removeRule(req.params.ruleId); | ||
res.send(); | ||
}); | ||
this.apiApp.get('/traffic', (req, res) => { | ||
this.apiApp.get('/traffic', this.authReadOnly, (req, res) => { | ||
const serializedTraffic = {}; | ||
@@ -65,3 +86,3 @@ for (const [key, value] of this.trafficStore.entries()) { | ||
}); | ||
this.apiApp.get('/traffic/:ruleIdOrLabel', (req, res) => { | ||
this.apiApp.get('/traffic/:ruleIdOrLabel', this.authReadOnly, (req, res) => { | ||
const serializedTraffic = {}; | ||
@@ -75,3 +96,9 @@ for (const [key, value] of this.trafficStore.entries()) { | ||
}); | ||
this.apiApp.use((error, req, res, _next) => { | ||
if (!(webGuiOptions === null || webGuiOptions === void 0 ? void 0 : webGuiOptions.disabled)) { | ||
this.apiApp.set('views', __dirname + '/webgui'); | ||
this.apiApp.set('view engine', 'pug'); | ||
this.initWebGui(); | ||
} | ||
this.apiApp.all(/.*/, (req, res) => res.status(404).send()); | ||
this.apiApp.use((error, req, res) => { | ||
const ctx = requestContext_1.default.get(req); | ||
@@ -97,10 +124,5 @@ const uuid = (ctx === null || ctx === void 0 ? void 0 : ctx.uuid) || (0, uuid_1.v4)(); | ||
}); | ||
if (!(webGuiOptions === null || webGuiOptions === void 0 ? void 0 : webGuiOptions.disabled)) { | ||
this.apiApp.set('views', __dirname + '/webgui'); | ||
this.apiApp.set('view engine', 'pug'); | ||
this.initWebGui(); | ||
} | ||
} | ||
initWebGui() { | ||
this.apiApp.get('/webgui/rules', async (req, res) => { | ||
this.apiApp.get('/webgui/rules', this.authReadOnly, async (req, res) => { | ||
const rules = {}; | ||
@@ -112,3 +134,3 @@ for (const rule of await (0, ruleExecutor_1.getRuleExecutor)(this.options.mockUuid).getRules()) { | ||
}); | ||
this.apiApp.get('/webgui/traffic', async (req, res) => { | ||
this.apiApp.get('/webgui/traffic', this.authReadOnly, async (req, res) => { | ||
const serializedTraffic = []; | ||
@@ -123,3 +145,3 @@ for (const value of this.trafficStore.values()) { | ||
// TODO make webui way better and safer, nicer formatting, eslint/prettier, blackjack and hookers | ||
this.apiApp.post('/webgui/rules/unsafesave', async (req, res) => { | ||
this.apiApp.post('/webgui/rules/unsafesave', this.authReadWrite, async (req, res) => { | ||
const rule = new Function(req.body)(); | ||
@@ -126,0 +148,0 @@ if (!rule || |
@@ -9,7 +9,7 @@ "use strict"; | ||
const shared_1 = require("@stuntman/shared"); | ||
const validatiors_1 = require("./validatiors"); | ||
const validators_1 = require("./validators"); | ||
// TODO | ||
const deserializeRule = (serializedRule) => { | ||
shared_1.logger.debug(serializedRule, 'attempt to deserialize rule'); | ||
(0, validatiors_1.validateSerializedRuleProperties)(serializedRule); | ||
(0, validators_1.validateSerializedRuleProperties)(serializedRule); | ||
const rule = { | ||
@@ -16,0 +16,0 @@ id: serializedRule.id, |
@@ -24,2 +24,4 @@ /// <reference types="node" /> | ||
constructor(options: Stuntman.ServerConfig); | ||
private proxyRequest; | ||
private requestHandler; | ||
start(): void; | ||
@@ -26,0 +28,0 @@ stop(): void; |
314
dist/mock.js
@@ -88,157 +88,3 @@ "use strict"; | ||
}); | ||
this.mockApp.all(/.*/, async (req, res) => { | ||
var _a, _b, _c, _d, _e; | ||
const ctx = requestContext_1.default.get(req); | ||
const requestUuid = (ctx === null || ctx === void 0 ? void 0 : ctx.uuid) || (0, uuid_1.v4)(); | ||
const timestamp = Date.now(); | ||
const originalHostname = req.headers.host || req.hostname; | ||
const unproxiedHostname = req.hostname.replace(this.MOCK_DOMAIN_REGEX, ''); | ||
const isProxiedHostname = originalHostname !== unproxiedHostname; | ||
const originalRequest = { | ||
id: requestUuid, | ||
timestamp, | ||
url: `${req.protocol}://${req.hostname}${req.originalUrl}`, | ||
method: req.method, | ||
rawHeaders: new shared_1.RawHeaders(...req.rawHeaders), | ||
...((Buffer.isBuffer(req.body) && { body: req.body.toString('utf-8') }) || | ||
(typeof req.body === 'string' && { body: req.body })), | ||
}; | ||
shared_1.logger.debug(originalRequest, 'processing request'); | ||
const logContext = { | ||
requestId: originalRequest.id, | ||
}; | ||
const mockEntry = { | ||
originalRequest, | ||
modifiedRequest: { | ||
...this.unproxyRequest(req), | ||
id: requestUuid, | ||
timestamp, | ||
...(originalRequest.body && { gqlBody: naiveGQLParser(originalRequest.body) }), | ||
}, | ||
}; | ||
if (!isProxiedHostname) { | ||
this.removeProxyPort(mockEntry.modifiedRequest); | ||
} | ||
const matchingRule = await (0, ruleExecutor_1.getRuleExecutor)(this.mockUuid).findMatchingRule(mockEntry.modifiedRequest); | ||
if (matchingRule) { | ||
mockEntry.mockRuleId = matchingRule.id; | ||
mockEntry.labels = matchingRule.labels; | ||
if ((_a = matchingRule.actions) === null || _a === void 0 ? void 0 : _a.mockResponse) { | ||
const staticResponse = typeof matchingRule.actions.mockResponse === 'function' | ||
? matchingRule.actions.mockResponse(mockEntry.modifiedRequest) | ||
: matchingRule.actions.mockResponse; | ||
mockEntry.modifiedResponse = staticResponse; | ||
shared_1.logger.debug({ ...logContext, staticResponse }, 'replying with mocked response'); | ||
if (matchingRule.storeTraffic) { | ||
this.trafficStore.set(requestUuid, mockEntry); | ||
} | ||
if (staticResponse.rawHeaders) { | ||
for (const header of staticResponse.rawHeaders.toHeaderPairs()) { | ||
res.setHeader(header[0], header[1]); | ||
} | ||
} | ||
res.status(staticResponse.status || 200); | ||
res.send(staticResponse.body); | ||
// static response blocks any further processing | ||
return; | ||
} | ||
if ((_b = matchingRule.actions) === null || _b === void 0 ? void 0 : _b.modifyRequest) { | ||
mockEntry.modifiedRequest = (_c = matchingRule.actions) === null || _c === void 0 ? void 0 : _c.modifyRequest(mockEntry.modifiedRequest); | ||
shared_1.logger.debug({ ...logContext, modifiedRequest: mockEntry.modifiedRequest }, 'modified original request'); | ||
} | ||
} | ||
if (this.ipUtils && !isProxiedHostname && !this.ipUtils.isIP(originalHostname)) { | ||
const hostname = originalHostname.split(':')[0]; | ||
try { | ||
const internalIPs = await this.ipUtils.resolveIP(hostname); | ||
if (this.ipUtils.isLocalhostIP(internalIPs) && this.options.mock.externalDns.length) { | ||
const externalIPs = await this.ipUtils.resolveIP(hostname, { useExternalDns: true }); | ||
shared_1.logger.debug({ ...logContext, hostname, externalIPs, internalIPs }, 'switched to external IP'); | ||
mockEntry.modifiedRequest.url = mockEntry.modifiedRequest.url.replace(/^(https?:\/\/)[^:/]+/i, `$1${externalIPs}`); | ||
} | ||
} | ||
catch (error) { | ||
// swallow the exeception, don't think much can be done at this point | ||
shared_1.logger.warn({ ...logContext, error }, `error trying to resolve IP for "${hostname}"`); | ||
} | ||
} | ||
let controller = new AbortController(); | ||
const fetchTimeout = setTimeout(() => { | ||
if (controller) { | ||
controller.abort(`timeout after ${this.options.mock.timeout}`); | ||
} | ||
}, this.options.mock.timeout); | ||
req.on('close', () => { | ||
shared_1.logger.debug(logContext, 'remote client canceled the request'); | ||
clearTimeout(fetchTimeout); | ||
if (controller) { | ||
controller.abort('remote client canceled the request'); | ||
} | ||
}); | ||
let targetResponse; | ||
try { | ||
const requestOptions = { | ||
headers: mockEntry.modifiedRequest.rawHeaders, | ||
body: mockEntry.modifiedRequest.body, | ||
method: mockEntry.modifiedRequest.method.toUpperCase(), | ||
}; | ||
shared_1.logger.debug({ | ||
...logContext, | ||
url: mockEntry.modifiedRequest.url, | ||
...requestOptions, | ||
}, 'outgoing request attempt'); | ||
targetResponse = await (0, undici_1.request)(mockEntry.modifiedRequest.url, requestOptions); | ||
} | ||
catch (error) { | ||
shared_1.logger.error({ ...logContext, error, request: mockEntry.modifiedRequest }, 'error fetching'); | ||
throw error; | ||
} | ||
finally { | ||
controller = null; | ||
clearTimeout(fetchTimeout); | ||
} | ||
const targetResponseBuffer = Buffer.from(await targetResponse.body.arrayBuffer()); | ||
const originalResponse = { | ||
timestamp: Date.now(), | ||
body: targetResponseBuffer.toString('binary'), | ||
status: targetResponse.statusCode, | ||
rawHeaders: shared_1.RawHeaders.fromHeadersRecord(targetResponse.headers), | ||
}; | ||
shared_1.logger.debug({ ...logContext, originalResponse }, 'received response'); | ||
mockEntry.originalResponse = originalResponse; | ||
let modifedResponse = { | ||
...originalResponse, | ||
rawHeaders: new shared_1.RawHeaders(...Array.from(originalResponse.rawHeaders.toHeaderPairs()).flatMap(([key, value]) => { | ||
// TODO this replace may be too aggressive and doesn't handle protocol (won't be necessary with a trusted cert and mock serving http+https) | ||
return [ | ||
key, | ||
isProxiedHostname | ||
? value | ||
: value.replace(new RegExp(`(?:^|\\b)(${unproxiedHostname.replace('.', '\\.')})(?:\\b|$)`, 'igm'), originalHostname), | ||
]; | ||
})), | ||
}; | ||
if ((_d = matchingRule === null || matchingRule === void 0 ? void 0 : matchingRule.actions) === null || _d === void 0 ? void 0 : _d.modifyResponse) { | ||
modifedResponse = (_e = matchingRule === null || matchingRule === void 0 ? void 0 : matchingRule.actions) === null || _e === void 0 ? void 0 : _e.modifyResponse(mockEntry.modifiedRequest, originalResponse); | ||
shared_1.logger.debug({ ...logContext, modifedResponse }, 'modified response'); | ||
} | ||
mockEntry.modifiedResponse = modifedResponse; | ||
if (matchingRule === null || matchingRule === void 0 ? void 0 : matchingRule.storeTraffic) { | ||
this.trafficStore.set(requestUuid, mockEntry); | ||
} | ||
if (modifedResponse.status) { | ||
res.status(modifedResponse.status); | ||
} | ||
if (modifedResponse.rawHeaders) { | ||
for (const header of modifedResponse.rawHeaders.toHeaderPairs()) { | ||
// since fetch decompresses responses we need to get rid of some headers | ||
// TODO maybe could be handled better than just skipping, although express should add these back for new body | ||
if (/^content-(?:length|encoding)$/i.test(header[0])) { | ||
continue; | ||
} | ||
res.setHeader(header[0], isProxiedHostname ? header[1].replace(unproxiedHostname, originalHostname) : header[1]); | ||
} | ||
} | ||
res.end(Buffer.from(modifedResponse.body, 'binary')); | ||
}); | ||
this.mockApp.all(/.*/, this.requestHandler); | ||
this.mockApp.use((error, req, res, _next) => { | ||
@@ -259,2 +105,160 @@ const ctx = requestContext_1.default.get(req); | ||
} | ||
async proxyRequest(req, mockEntry, logContext) { | ||
let controller = new AbortController(); | ||
const fetchTimeout = setTimeout(() => { | ||
if (controller) { | ||
controller.abort(`timeout after ${this.options.mock.timeout}`); | ||
} | ||
}, this.options.mock.timeout); | ||
req.on('close', () => { | ||
shared_1.logger.debug(logContext, 'remote client canceled the request'); | ||
clearTimeout(fetchTimeout); | ||
if (controller) { | ||
controller.abort('remote client canceled the request'); | ||
} | ||
}); | ||
let targetResponse; | ||
try { | ||
const requestOptions = { | ||
headers: mockEntry.modifiedRequest.rawHeaders, | ||
body: mockEntry.modifiedRequest.body, | ||
method: mockEntry.modifiedRequest.method.toUpperCase(), | ||
}; | ||
shared_1.logger.debug({ | ||
...logContext, | ||
url: mockEntry.modifiedRequest.url, | ||
...requestOptions, | ||
}, 'outgoing request attempt'); | ||
targetResponse = await (0, undici_1.request)(mockEntry.modifiedRequest.url, requestOptions); | ||
} | ||
catch (error) { | ||
shared_1.logger.error({ ...logContext, error, request: mockEntry.modifiedRequest }, 'error fetching'); | ||
throw error; | ||
} | ||
finally { | ||
controller = null; | ||
clearTimeout(fetchTimeout); | ||
} | ||
const targetResponseBuffer = Buffer.from(await targetResponse.body.arrayBuffer()); | ||
return { | ||
timestamp: Date.now(), | ||
body: targetResponseBuffer.toString('binary'), | ||
status: targetResponse.statusCode, | ||
rawHeaders: shared_1.RawHeaders.fromHeadersRecord(targetResponse.headers), | ||
}; | ||
} | ||
async requestHandler(req, res) { | ||
var _a, _b, _c, _d, _e; | ||
const ctx = requestContext_1.default.get(req); | ||
const requestUuid = (ctx === null || ctx === void 0 ? void 0 : ctx.uuid) || (0, uuid_1.v4)(); | ||
const timestamp = Date.now(); | ||
const originalHostname = req.headers.host || req.hostname; | ||
const unproxiedHostname = req.hostname.replace(this.MOCK_DOMAIN_REGEX, ''); | ||
const isProxiedHostname = originalHostname !== unproxiedHostname; | ||
const originalRequest = { | ||
id: requestUuid, | ||
timestamp, | ||
url: `${req.protocol}://${req.hostname}${req.originalUrl}`, | ||
method: req.method, | ||
rawHeaders: new shared_1.RawHeaders(...req.rawHeaders), | ||
...((Buffer.isBuffer(req.body) && { body: req.body.toString('utf-8') }) || | ||
(typeof req.body === 'string' && { body: req.body })), | ||
}; | ||
shared_1.logger.debug(originalRequest, 'processing request'); | ||
const logContext = { | ||
requestId: originalRequest.id, | ||
}; | ||
const mockEntry = { | ||
originalRequest, | ||
modifiedRequest: { | ||
...this.unproxyRequest(req), | ||
id: requestUuid, | ||
timestamp, | ||
...(originalRequest.body && { gqlBody: naiveGQLParser(originalRequest.body) }), | ||
}, | ||
}; | ||
if (!isProxiedHostname) { | ||
this.removeProxyPort(mockEntry.modifiedRequest); | ||
} | ||
const matchingRule = await (0, ruleExecutor_1.getRuleExecutor)(this.mockUuid).findMatchingRule(mockEntry.modifiedRequest); | ||
if (matchingRule) { | ||
mockEntry.mockRuleId = matchingRule.id; | ||
mockEntry.labels = matchingRule.labels; | ||
if ((_a = matchingRule.actions) === null || _a === void 0 ? void 0 : _a.mockResponse) { | ||
const staticResponse = typeof matchingRule.actions.mockResponse === 'function' | ||
? matchingRule.actions.mockResponse(mockEntry.modifiedRequest) | ||
: matchingRule.actions.mockResponse; | ||
mockEntry.modifiedResponse = staticResponse; | ||
shared_1.logger.debug({ ...logContext, staticResponse }, 'replying with mocked response'); | ||
if (matchingRule.storeTraffic) { | ||
this.trafficStore.set(requestUuid, mockEntry); | ||
} | ||
if (staticResponse.rawHeaders) { | ||
for (const header of staticResponse.rawHeaders.toHeaderPairs()) { | ||
res.setHeader(header[0], header[1]); | ||
} | ||
} | ||
res.status(staticResponse.status || 200); | ||
res.send(staticResponse.body); | ||
// static response blocks any further processing | ||
return; | ||
} | ||
if ((_b = matchingRule.actions) === null || _b === void 0 ? void 0 : _b.modifyRequest) { | ||
mockEntry.modifiedRequest = (_c = matchingRule.actions) === null || _c === void 0 ? void 0 : _c.modifyRequest(mockEntry.modifiedRequest); | ||
shared_1.logger.debug({ ...logContext, modifiedRequest: mockEntry.modifiedRequest }, 'modified original request'); | ||
} | ||
} | ||
if (this.ipUtils && !isProxiedHostname && !this.ipUtils.isIP(originalHostname)) { | ||
const hostname = originalHostname.split(':')[0]; | ||
try { | ||
const internalIPs = await this.ipUtils.resolveIP(hostname); | ||
if (this.ipUtils.isLocalhostIP(internalIPs) && this.options.mock.externalDns.length) { | ||
const externalIPs = await this.ipUtils.resolveIP(hostname, { useExternalDns: true }); | ||
shared_1.logger.debug({ ...logContext, hostname, externalIPs, internalIPs }, 'switched to external IP'); | ||
mockEntry.modifiedRequest.url = mockEntry.modifiedRequest.url.replace(/^(https?:\/\/)[^:/]+/i, `$1${externalIPs}`); | ||
} | ||
} | ||
catch (error) { | ||
// swallow the exeception, don't think much can be done at this point | ||
shared_1.logger.warn({ ...logContext, error }, `error trying to resolve IP for "${hostname}"`); | ||
} | ||
} | ||
const originalResponse = await this.proxyRequest(req, mockEntry, logContext); | ||
shared_1.logger.debug({ ...logContext, originalResponse }, 'received response'); | ||
mockEntry.originalResponse = originalResponse; | ||
let modifedResponse = { | ||
...originalResponse, | ||
rawHeaders: new shared_1.RawHeaders(...Array.from(originalResponse.rawHeaders.toHeaderPairs()).flatMap(([key, value]) => { | ||
// TODO this replace may be too aggressive and doesn't handle protocol (won't be necessary with a trusted cert and mock serving http+https) | ||
return [ | ||
key, | ||
isProxiedHostname | ||
? value | ||
: value.replace(new RegExp(`(?:^|\\b)(${unproxiedHostname.replace('.', '\\.')})(?:\\b|$)`, 'igm'), originalHostname), | ||
]; | ||
})), | ||
}; | ||
if ((_d = matchingRule === null || matchingRule === void 0 ? void 0 : matchingRule.actions) === null || _d === void 0 ? void 0 : _d.modifyResponse) { | ||
modifedResponse = (_e = matchingRule === null || matchingRule === void 0 ? void 0 : matchingRule.actions) === null || _e === void 0 ? void 0 : _e.modifyResponse(mockEntry.modifiedRequest, originalResponse); | ||
shared_1.logger.debug({ ...logContext, modifedResponse }, 'modified response'); | ||
} | ||
mockEntry.modifiedResponse = modifedResponse; | ||
if (matchingRule === null || matchingRule === void 0 ? void 0 : matchingRule.storeTraffic) { | ||
this.trafficStore.set(requestUuid, mockEntry); | ||
} | ||
if (modifedResponse.status) { | ||
res.status(modifedResponse.status); | ||
} | ||
if (modifedResponse.rawHeaders) { | ||
for (const header of modifedResponse.rawHeaders.toHeaderPairs()) { | ||
// since fetch decompresses responses we need to get rid of some headers | ||
// TODO maybe could be handled better than just skipping, although express should add these back for new body | ||
// if (/^content-(?:length|encoding)$/i.test(header[0])) { | ||
// continue; | ||
// } | ||
res.setHeader(header[0], isProxiedHostname ? header[1].replace(unproxiedHostname, originalHostname) : header[1]); | ||
} | ||
} | ||
res.end(Buffer.from(modifedResponse.body, 'binary')); | ||
} | ||
start() { | ||
@@ -261,0 +265,0 @@ if (this.server) { |
@@ -98,2 +98,3 @@ "use strict"; | ||
if (r.id === (typeof ruleOrId === 'string' ? ruleOrId : ruleOrId.id)) { | ||
r.counter = 0; | ||
r.isEnabled = true; | ||
@@ -143,2 +144,3 @@ shared_1.logger.debug({ ruleId: r.id }, 'rule enabled'); | ||
} | ||
// TODO check if that works | ||
if (matchingRule.disableAfterUse) { | ||
@@ -153,3 +155,3 @@ if (typeof matchingRule.disableAfterUse === 'boolean' || matchingRule.disableAfterUse <= matchingRule.counter) { | ||
shared_1.logger.debug(logContext, 'removing rule for future requests'); | ||
this.removeRule(matchingRule); | ||
await this.removeRule(matchingRule); | ||
} | ||
@@ -184,3 +186,3 @@ } | ||
if (!ruleExecutors[mockUuid]) { | ||
ruleExecutors[mockUuid] = new RuleExecutor(rules_1.DEFAULT_RULES.map((r) => ({ ...r, ttlSeconds: Infinity }))); | ||
ruleExecutors[mockUuid] = new RuleExecutor([...rules_1.DEFAULT_RULES, ...rules_1.CUSTOM_RULES].map((r) => ({ ...r, ttlSeconds: Infinity }))); | ||
} | ||
@@ -187,0 +189,0 @@ return ruleExecutors[mockUuid]; |
import type * as Stuntman from '@stuntman/shared'; | ||
export declare const DEFAULT_RULES: Stuntman.DeployedRule[]; | ||
export declare const CUSTOM_RULES: Stuntman.DeployedRule[]; |
"use strict"; | ||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
var desc = Object.getOwnPropertyDescriptor(m, k); | ||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { | ||
desc = { enumerable: true, get: function() { return m[k]; } }; | ||
} | ||
Object.defineProperty(o, k2, desc); | ||
}) : (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
o[k2] = m[k]; | ||
})); | ||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { | ||
Object.defineProperty(o, "default", { enumerable: true, value: v }); | ||
}) : function(o, v) { | ||
o["default"] = v; | ||
}); | ||
var __importStar = (this && this.__importStar) || function (mod) { | ||
if (mod && mod.__esModule) return mod; | ||
var result = {}; | ||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); | ||
__setModuleDefault(result, mod); | ||
return result; | ||
}; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.DEFAULT_RULES = void 0; | ||
exports.CUSTOM_RULES = exports.DEFAULT_RULES = void 0; | ||
const fs_1 = __importDefault(require("fs")); | ||
const glob_1 = __importDefault(require("glob")); | ||
const tsImport = __importStar(require("ts-import")); | ||
const catchAll_1 = require("./catchAll"); | ||
const echo_1 = require("./echo"); | ||
// TODO add option to load rules additional default rules from some nice configurable folder | ||
const shared_1 = require("@stuntman/shared"); | ||
exports.DEFAULT_RULES = [catchAll_1.catchAllRule, echo_1.echoRule]; | ||
exports.CUSTOM_RULES = []; | ||
const loadAdditionalRules = () => { | ||
if (!shared_1.serverConfig.mock.rulesPath || !fs_1.default.existsSync(shared_1.serverConfig.mock.rulesPath)) { | ||
shared_1.logger.debug({ rulesPath: shared_1.serverConfig.mock.rulesPath }, `additional rules directory not found`); | ||
return; | ||
} | ||
shared_1.logger.debug({ rulesPath: shared_1.serverConfig.mock.rulesPath }, `loading additional rules`); | ||
const filePaths = glob_1.default.sync('*.[tj]s', { absolute: true, cwd: shared_1.serverConfig.mock.rulesPath }); | ||
for (const filePath of filePaths) { | ||
// TODO add .ts rule support | ||
try { | ||
const loadedFile = /\.js$/.test(filePath) ? require(filePath) : tsImport.loadSync(filePath); | ||
// eslint-disable-next-line @typescript-eslint/no-var-requires | ||
const exportedRules = Object.values(loadedFile).filter((rule) => { | ||
if (!rule || !rule.id || typeof rule.matches !== 'function') { | ||
shared_1.logger.error({ filePath, rule }, 'invalid exported rule'); | ||
return false; | ||
} | ||
return true; | ||
}); | ||
exports.CUSTOM_RULES.push(...exportedRules); | ||
} | ||
catch (error) { | ||
shared_1.logger.error({ filePath, error }, 'error importing rule'); | ||
} | ||
} | ||
const ruleIds = [...exports.DEFAULT_RULES, ...exports.CUSTOM_RULES].map((rule) => rule.id); | ||
const duplicatedRuleIds = ruleIds.filter((currentValue, currentIndex) => ruleIds.indexOf(currentValue) !== currentIndex); | ||
if (duplicatedRuleIds.length > 0) { | ||
shared_1.logger.error({ duplicatedRuleIds }, 'duplicated rule ids'); | ||
throw new Error('duplicated rule ids'); | ||
} | ||
}; | ||
loadAdditionalRules(); |
{ | ||
"name": "@stuntman/server", | ||
"version": "0.1.2", | ||
"version": "0.1.3", | ||
"description": "Stuntman - HTTP proxy / mock server with API", | ||
@@ -38,2 +38,3 @@ "main": "dist/index.js", | ||
"express": "5.0.0-beta.1", | ||
"glob": "8.1.0", | ||
"lru-cache": "7.16.0", | ||
@@ -43,2 +44,4 @@ "object-sizeof": "2.6.1", | ||
"serialize-javascript": "6.0.1", | ||
"ts-import": "4.0.0-beta.10", | ||
"typescript": "4.9.5", | ||
"undici": "5.20.0", | ||
@@ -50,2 +53,3 @@ "uuid": "9.0.0" | ||
"@types/express": "4.17.17", | ||
"@types/glob": "8.1.0", | ||
"@types/serialize-javascript": "5.0.2", | ||
@@ -69,3 +73,3 @@ "@types/uuid": "9.0.0", | ||
"lint": "prettier --check . && eslint . --ext ts", | ||
"lint:fix": "prettier --write ./src && eslint ./src --ext ts --fix", | ||
"lint:fix": "prettier --write ./{src,test} && eslint ./{src,test} --ext ts --fix", | ||
"start": "node ./dist/bin/stuntman.js", | ||
@@ -72,0 +76,0 @@ "start:dev": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' ./src/bin/stuntman.ts", |
@@ -7,3 +7,3 @@ # Stuntman | ||
In order to get more familiar with the concept and how to use it please refer to [example app](https://github.com/andrzej-woof/stuntman/tree/master/packages/example#readme) | ||
In order to get more familiar with the concept and how to use it please refer to [example app](https://github.com/andrzej-woof/stuntman/tree/master/apps/example#readme) | ||
@@ -10,0 +10,0 @@ > **_NOTE:_** This project is at a very early stage of developement and as such may often contain breaking changes in upcoming releases before reaching stable version 1.0.0 |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
77235
1299
12
6
2
+ Addedglob@8.1.0
+ Addedts-import@4.0.0-beta.10
+ Addedtypescript@4.9.5
+ Addedbalanced-match@1.0.2(transitive)
+ Addedbrace-expansion@2.0.1(transitive)
+ Addedcomment-parser@1.3.1(transitive)
+ Addedfs.realpath@1.0.0(transitive)
+ Addedglob@8.1.0(transitive)
+ Addedinflight@1.0.6(transitive)
+ Addedminimatch@5.1.6(transitive)
+ Addedonce@1.4.0(transitive)
+ Addedoptions-defaults@2.0.40(transitive)
+ Addedts-import@4.0.0-beta.10(transitive)
+ Addedtslib@2.4.1(transitive)
+ Addedtypescript@4.9.5(transitive)
+ Addedwrappy@1.0.2(transitive)