@stuntman/server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/config/default.json +23 -0
- package/config/test.json +26 -0
- package/dist/api/api.d.ts +19 -0
- package/dist/api/api.js +162 -0
- package/dist/api/utils.d.ts +4 -0
- package/dist/api/utils.js +69 -0
- package/dist/api/validatiors.d.ts +3 -0
- package/dist/api/validatiors.js +118 -0
- package/dist/api/webgui/rules.pug +145 -0
- package/dist/api/webgui/style.css +28 -0
- package/dist/api/webgui/traffic.pug +37 -0
- package/dist/bin/stuntman.d.ts +2 -0
- package/dist/bin/stuntman.js +7 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/ipUtils.d.ts +17 -0
- package/dist/ipUtils.js +101 -0
- package/dist/mock.d.ts +27 -0
- package/dist/mock.js +308 -0
- package/dist/requestContext.d.ts +9 -0
- package/dist/requestContext.js +18 -0
- package/dist/ruleExecutor.d.ts +21 -0
- package/dist/ruleExecutor.js +172 -0
- package/dist/rules/catchAll.d.ts +2 -0
- package/dist/rules/catchAll.js +15 -0
- package/dist/rules/echo.d.ts +2 -0
- package/dist/rules/echo.js +15 -0
- package/dist/rules/index.d.ts +2 -0
- package/dist/rules/index.js +7 -0
- package/dist/storage.d.ts +4 -0
- package/dist/storage.js +42 -0
- package/package.json +58 -0
- package/src/api/api.ts +194 -0
- package/src/api/utils.ts +69 -0
- package/src/api/validatiors.ts +123 -0
- package/src/api/webgui/rules.pug +145 -0
- package/src/api/webgui/style.css +28 -0
- package/src/api/webgui/traffic.pug +37 -0
- package/src/bin/stuntman.ts +8 -0
- package/src/index.ts +1 -0
- package/src/ipUtils.ts +83 -0
- package/src/mock.ts +349 -0
- package/src/requestContext.ts +23 -0
- package/src/ruleExecutor.ts +193 -0
- package/src/rules/catchAll.ts +14 -0
- package/src/rules/echo.ts +14 -0
- package/src/rules/index.ts +7 -0
- package/src/storage.ts +39 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { AppError, HttpCode, MAX_RULE_TTL_SECONDS, MIN_RULE_TTL_SECONDS, logger, RawHeaders } from '@stuntman/shared';
|
|
2
|
+
import type * as Stuntman from '@stuntman/shared';
|
|
3
|
+
|
|
4
|
+
export const validateSerializedRuleProperties = (rule: Stuntman.SerializedRule): void => {
|
|
5
|
+
if (!rule) {
|
|
6
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid serialized rule' });
|
|
7
|
+
}
|
|
8
|
+
if (typeof rule.id !== 'string') {
|
|
9
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.id' });
|
|
10
|
+
}
|
|
11
|
+
if (typeof rule.matches !== 'object' || !('remoteFn' in rule.matches) || typeof rule.matches.remoteFn !== 'string') {
|
|
12
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.matches' });
|
|
13
|
+
}
|
|
14
|
+
if (rule.priority && typeof rule.priority !== 'number') {
|
|
15
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.priority' });
|
|
16
|
+
}
|
|
17
|
+
if (typeof rule.actions !== 'undefined') {
|
|
18
|
+
if (typeof rule.actions !== 'object') {
|
|
19
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.actions' });
|
|
20
|
+
}
|
|
21
|
+
if (typeof rule.actions.mockResponse !== 'undefined') {
|
|
22
|
+
if (typeof rule.actions.mockResponse !== 'object') {
|
|
23
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.actions.mockResponse' });
|
|
24
|
+
}
|
|
25
|
+
if ('remoteFn' in rule.actions.mockResponse && typeof rule.actions.mockResponse.remoteFn !== 'string') {
|
|
26
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.actions.mockResponse' });
|
|
27
|
+
} else if ('status' in rule.actions.mockResponse) {
|
|
28
|
+
if (typeof rule.actions.mockResponse.status !== 'number') {
|
|
29
|
+
throw new AppError({
|
|
30
|
+
httpCode: HttpCode.BAD_REQUEST,
|
|
31
|
+
message: 'invalid rule.actions.mockResponse.status',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
if (
|
|
35
|
+
typeof rule.actions.mockResponse.rawHeaders !== 'undefined' &&
|
|
36
|
+
(!Array.isArray(rule.actions.mockResponse.rawHeaders) ||
|
|
37
|
+
rule.actions.mockResponse.rawHeaders.some((header) => typeof header !== 'string'))
|
|
38
|
+
) {
|
|
39
|
+
throw new AppError({
|
|
40
|
+
httpCode: HttpCode.BAD_REQUEST,
|
|
41
|
+
message: 'invalid rule.actions.mockResponse.rawHeaders',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (typeof rule.actions.mockResponse.body !== 'undefined' && typeof rule.actions.mockResponse.body !== 'string') {
|
|
45
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.actions.mockResponse.body' });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (typeof rule.actions.modifyRequest !== 'undefined' && typeof rule.actions.modifyRequest.remoteFn !== 'string') {
|
|
50
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.actions.modifyRequest' });
|
|
51
|
+
}
|
|
52
|
+
if (typeof rule.actions.modifyResponse !== 'undefined' && typeof rule.actions.modifyResponse.remoteFn !== 'string') {
|
|
53
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.actions.modifyResponse' });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (
|
|
57
|
+
typeof rule.disableAfterUse !== 'undefined' &&
|
|
58
|
+
typeof rule.disableAfterUse !== 'boolean' &&
|
|
59
|
+
(typeof rule.disableAfterUse !== 'number' || rule.disableAfterUse < 0)
|
|
60
|
+
) {
|
|
61
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.disableAfterUse' });
|
|
62
|
+
}
|
|
63
|
+
if (
|
|
64
|
+
typeof rule.removeAfterUse !== 'undefined' &&
|
|
65
|
+
typeof rule.removeAfterUse !== 'boolean' &&
|
|
66
|
+
(typeof rule.removeAfterUse !== 'number' || rule.removeAfterUse < 0)
|
|
67
|
+
) {
|
|
68
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.removeAfterUse' });
|
|
69
|
+
}
|
|
70
|
+
if (
|
|
71
|
+
typeof rule.labels !== 'undefined' &&
|
|
72
|
+
(!Array.isArray(rule.labels) || rule.labels.some((label) => typeof label !== 'string'))
|
|
73
|
+
) {
|
|
74
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.labels' });
|
|
75
|
+
}
|
|
76
|
+
if (rule.actions?.mockResponse && rule.actions.modifyResponse) {
|
|
77
|
+
throw new AppError({
|
|
78
|
+
httpCode: HttpCode.BAD_REQUEST,
|
|
79
|
+
message: 'rule.actions.mockResponse and rule.actions.modifyResponse are mutually exclusive',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (!rule.ttlSeconds || rule.ttlSeconds < MIN_RULE_TTL_SECONDS || rule.ttlSeconds > MAX_RULE_TTL_SECONDS) {
|
|
83
|
+
throw new AppError({
|
|
84
|
+
httpCode: HttpCode.BAD_REQUEST,
|
|
85
|
+
message: `rule.ttlSeconds should be within ${MIN_RULE_TTL_SECONDS} and ${MAX_RULE_TTL_SECONDS}`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (typeof rule.isEnabled !== 'undefined' && typeof rule.isEnabled !== 'boolean') {
|
|
89
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.isEnabled' });
|
|
90
|
+
}
|
|
91
|
+
if (typeof rule.storeTraffic !== 'undefined' && typeof rule.storeTraffic !== 'boolean') {
|
|
92
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.storeTraffic' });
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const validateDeserializedRule = (deserializedRule: Stuntman.Rule) => {
|
|
97
|
+
// TODO validate other functions ?
|
|
98
|
+
let matchValidationResult: Stuntman.RuleMatchResult;
|
|
99
|
+
try {
|
|
100
|
+
matchValidationResult = deserializedRule.matches({
|
|
101
|
+
id: 'validation',
|
|
102
|
+
method: 'GET',
|
|
103
|
+
rawHeaders: new RawHeaders(),
|
|
104
|
+
timestamp: Date.now(),
|
|
105
|
+
url: 'http://dummy.invalid/',
|
|
106
|
+
});
|
|
107
|
+
} catch (error: any) {
|
|
108
|
+
logger.error({ ruleId: deserializedRule.id }, error);
|
|
109
|
+
throw new AppError({
|
|
110
|
+
httpCode: HttpCode.UNPROCESSABLE_ENTITY,
|
|
111
|
+
message: 'match function returned invalid value',
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (
|
|
115
|
+
matchValidationResult !== true &&
|
|
116
|
+
matchValidationResult !== false &&
|
|
117
|
+
matchValidationResult.result !== true &&
|
|
118
|
+
matchValidationResult.result !== false
|
|
119
|
+
) {
|
|
120
|
+
logger.error({ ruleId: deserializedRule.id, matchValidationResult }, 'match function retruned invalid value');
|
|
121
|
+
throw new AppError({ httpCode: HttpCode.UNPROCESSABLE_ENTITY, message: 'match function returned invalid value' });
|
|
122
|
+
}
|
|
123
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
doctype html
|
|
2
|
+
html
|
|
3
|
+
head
|
|
4
|
+
title Stuntman - rule editor
|
|
5
|
+
script(src='https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.35.0/min/vs/loader.min.js')
|
|
6
|
+
style
|
|
7
|
+
include style.css
|
|
8
|
+
body(style='color: rgb(204, 204, 204); background-color: rgb(50, 50, 50)')
|
|
9
|
+
button#newRule(type='button', onclick='window.newRule()') New rule
|
|
10
|
+
button#saveRule(type='button', onclick='window.saveRule()', disabled) Save rule
|
|
11
|
+
div(style='width: 100%; overflow: hidden')
|
|
12
|
+
div(style='width: 230px; float: left')
|
|
13
|
+
h3 Rules
|
|
14
|
+
ul#ruleKeys.no-bullets
|
|
15
|
+
each ruleId in ruleKeys
|
|
16
|
+
li
|
|
17
|
+
button.rule(
|
|
18
|
+
type='button',
|
|
19
|
+
onclick='window.setRuleModel(this.getAttribute("data-rule-id"))',
|
|
20
|
+
data-rule-id=ruleId
|
|
21
|
+
)= ruleId
|
|
22
|
+
div(style='margin-left: 240px')
|
|
23
|
+
#container(style='height: 400px')
|
|
24
|
+
script.
|
|
25
|
+
const uuidv4 = () => {
|
|
26
|
+
function getRandomSymbol(symbol) {
|
|
27
|
+
var array;
|
|
28
|
+
|
|
29
|
+
if (symbol === 'y') {
|
|
30
|
+
array = ['8', '9', 'a', 'b'];
|
|
31
|
+
return array[Math.floor(Math.random() * array.length)];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
array = new Uint8Array(1);
|
|
35
|
+
window.crypto.getRandomValues(array);
|
|
36
|
+
return (array[0] % 16).toString(16);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, getRandomSymbol);
|
|
40
|
+
};
|
|
41
|
+
require.config({
|
|
42
|
+
paths: {
|
|
43
|
+
vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.35.0/min/vs',
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
require(['vs/editor/editor.main'], function () {
|
|
47
|
+
monaco.languages.typescript.javascriptDefaults.setEagerModelSync(true);
|
|
48
|
+
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
|
|
49
|
+
noSemanticValidation: false,
|
|
50
|
+
noSyntaxValidation: false,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// compiler options
|
|
54
|
+
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
|
|
55
|
+
target: monaco.languages.typescript.ScriptTarget.ES6,
|
|
56
|
+
allowNonTsExtensions: true,
|
|
57
|
+
noLib: true,
|
|
58
|
+
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
|
59
|
+
module: monaco.languages.typescript.ModuleKind.CommonJS,
|
|
60
|
+
noEmit: true,
|
|
61
|
+
checkJs: true,
|
|
62
|
+
allowJs: true,
|
|
63
|
+
isolatedModules: true,
|
|
64
|
+
typeRoots: ['node_modules/@types'],
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
monaco.languages.typescript.typescriptDefaults.addExtraLib(`!{INDEX_DTS}`, 'file:///node_modules/@types/stuntman/index.d.ts');
|
|
68
|
+
const models = {};
|
|
69
|
+
const rules = eval('(!{rules})');
|
|
70
|
+
for (const ruleId of Object.keys(rules)) {
|
|
71
|
+
models[ruleId] = monaco.editor.createModel("import type * as Stuntman from 'stuntman';\n\nvar STUNTMAN_RULE: Stuntman.Rule = " + rules[ruleId] + ';', 'typescript', `file:///${ruleId}.ts`);
|
|
72
|
+
}
|
|
73
|
+
const editor = monaco.editor.create(document.getElementById('container'), {
|
|
74
|
+
theme: 'vs-dark',
|
|
75
|
+
autoIndent: true,
|
|
76
|
+
formatOnPaste: true,
|
|
77
|
+
formatOnType: true,
|
|
78
|
+
automaticLayout: true,
|
|
79
|
+
autoIndent: true,
|
|
80
|
+
tabSize: 2,
|
|
81
|
+
});
|
|
82
|
+
editor.onDidChangeModel((event) => {
|
|
83
|
+
const isInternal = /^file:\/\/\/internal\/.+/.test(event.newModelUrl);
|
|
84
|
+
if (isInternal) {
|
|
85
|
+
document.getElementById('saveRule').setAttribute('disabled', 'true');
|
|
86
|
+
} else {
|
|
87
|
+
document.getElementById('saveRule').removeAttribute('disabled');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
setTimeout(() => {
|
|
91
|
+
editor.getAction('editor.action.formatDocument').run();
|
|
92
|
+
}, 100);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
window.setRuleModel = (ruleId) => {
|
|
96
|
+
if (history.pushState) {
|
|
97
|
+
const newUrl = `${window.location.protocol}//${window.location.host}${window.location.pathname}?ruleId=${encodeURIComponent(ruleId)}`;
|
|
98
|
+
window.history.pushState({ path: newUrl }, '', newUrl);
|
|
99
|
+
}
|
|
100
|
+
editor.setModel(models[ruleId]);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const urlSearchParams = new URLSearchParams(window.location.search);
|
|
104
|
+
if (urlSearchParams.has('ruleId') && urlSearchParams.get('ruleId') in models) {
|
|
105
|
+
editor.setModel(models[urlSearchParams.get('ruleId')]);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
window.saveRule = () => {
|
|
109
|
+
document.getElementById('saveRule').setAttribute('disabled', 'true');
|
|
110
|
+
const modelUri = editor.getModel().uri;
|
|
111
|
+
const result = monaco.languages.typescript.getTypeScriptWorker();
|
|
112
|
+
result.then((worker) => {
|
|
113
|
+
worker(modelUri).then(function (client) {
|
|
114
|
+
client.getEmitOutput(modelUri.toString()).then((output) => {
|
|
115
|
+
const ruleFunctionText = output.outputFiles[0].text.replace(/^export .+$/im, '');
|
|
116
|
+
const newId = new Function(ruleFunctionText + '\n return STUNTMAN_RULE;')().id;
|
|
117
|
+
fetch('/webgui/rules/unsafesave', {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: { 'content-type': 'text/plain' },
|
|
120
|
+
body: ruleFunctionText + '\n return STUNTMAN_RULE;',
|
|
121
|
+
}).then((response) => {
|
|
122
|
+
if (response.ok) {
|
|
123
|
+
window.location.reload();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
alert('Error when saving rule');
|
|
127
|
+
document.getElementById('saveRule').removeAttribute('disabled');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
window.newRule = () => {
|
|
135
|
+
const ruleId = uuidv4();
|
|
136
|
+
const emptyRule = `import type * as Stuntman from \'stuntman\';\n\nvar STUNTMAN_RULE: Stuntman.Rule = { id: '${ruleId}', matches: (req: Stuntman.Request) => true, ttlSeconds: 600 };`;
|
|
137
|
+
models[ruleId] = monaco.editor.createModel(emptyRule, 'typescript', `file:///${ruleId}.ts`);
|
|
138
|
+
const ruleKeyNode = document.getElementById('ruleKeys').firstChild;
|
|
139
|
+
const ruleKeyNodeClone = ruleKeyNode.cloneNode(true);
|
|
140
|
+
ruleKeyNodeClone.getElementsByTagName('button')[0].setAttribute('data-rule-id', ruleId);
|
|
141
|
+
ruleKeyNodeClone.getElementsByTagName('button')[0].innerText = ruleId;
|
|
142
|
+
document.getElementById('ruleKeys').appendChild(ruleKeyNodeClone);
|
|
143
|
+
window.setRuleModel(ruleId);
|
|
144
|
+
};
|
|
145
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
div#container {
|
|
2
|
+
resize: vertical;
|
|
3
|
+
overflow: auto;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
body {
|
|
7
|
+
font-family: Menlo, Monaco, 'Courier New', monospace;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
button.rule {
|
|
11
|
+
font-family: Menlo, Monaco, 'Courier New', monospace;
|
|
12
|
+
background: none !important;
|
|
13
|
+
border: none;
|
|
14
|
+
padding: 0 !important;
|
|
15
|
+
color: #aaa;
|
|
16
|
+
text-decoration: underline;
|
|
17
|
+
cursor: pointer;
|
|
18
|
+
margin-top: 10px;
|
|
19
|
+
font-size: x-small;
|
|
20
|
+
text-align: left;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
ul.no-bullets {
|
|
24
|
+
list-style-type: none; /* Remove bullets */
|
|
25
|
+
padding: 0; /* Remove padding */
|
|
26
|
+
margin: 0; /* Remove margins */
|
|
27
|
+
text-align: left;
|
|
28
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
doctype html
|
|
2
|
+
html
|
|
3
|
+
head
|
|
4
|
+
title Stuntman - rule editor
|
|
5
|
+
script(src='https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.35.0/min/vs/loader.min.js')
|
|
6
|
+
style
|
|
7
|
+
include style.css
|
|
8
|
+
body(style='color: rgb(204, 204, 204); background-color: rgb(50, 50, 50)')
|
|
9
|
+
div(style='width: 100%; overflow: hidden')
|
|
10
|
+
div(style='width: 200px; float: left')
|
|
11
|
+
h3 Traffic log
|
|
12
|
+
div(style='margin-left: 220px')
|
|
13
|
+
#container(style='height: 800px')
|
|
14
|
+
script.
|
|
15
|
+
require.config({
|
|
16
|
+
paths: {
|
|
17
|
+
vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.35.0/min/vs',
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
require(['vs/editor/editor.main'], function () {
|
|
21
|
+
const traffic = !{ traffic };
|
|
22
|
+
const model = monaco.editor.createModel(JSON.stringify(traffic, null, 2), 'json');
|
|
23
|
+
const editor = monaco.editor.create(document.getElementById('container'), {
|
|
24
|
+
theme: 'vs-dark',
|
|
25
|
+
autoIndent: true,
|
|
26
|
+
formatOnPaste: true,
|
|
27
|
+
formatOnType: true,
|
|
28
|
+
automaticLayout: true,
|
|
29
|
+
readOnly: true,
|
|
30
|
+
});
|
|
31
|
+
editor.onDidChangeModel((event) => {
|
|
32
|
+
setTimeout(() => {
|
|
33
|
+
editor.getAction('editor.action.formatDocument').run();
|
|
34
|
+
}, 100);
|
|
35
|
+
});
|
|
36
|
+
editor.setModel(model);
|
|
37
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Mock as StuntmanMock } from './mock';
|
package/src/ipUtils.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { networkInterfaces } from 'os';
|
|
2
|
+
import dns, { Resolver as DNSResolver } from 'node:dns';
|
|
3
|
+
import { getDnsResolutionCache } from './storage';
|
|
4
|
+
import { logger } from '@stuntman/shared';
|
|
5
|
+
|
|
6
|
+
const localhostIPs: string[] = ['127.0.0.1'];
|
|
7
|
+
const IP_WITH_OPTIONAL_PORT_REGEX = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}(:[0-9]+)?$/i;
|
|
8
|
+
|
|
9
|
+
for (const nets of Object.values(networkInterfaces())) {
|
|
10
|
+
if (!nets) {
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
for (const net of nets) {
|
|
14
|
+
const familyV4Value = typeof net.family === 'string' ? 'IPv4' : 4;
|
|
15
|
+
if (net.family === familyV4Value && !localhostIPs.includes(net.address)) {
|
|
16
|
+
localhostIPs.push(net.address);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class IPUtils {
|
|
22
|
+
protected dnsResolutionCache;
|
|
23
|
+
protected mockUuid: string;
|
|
24
|
+
externalDns: dns.Resolver | null = null;
|
|
25
|
+
|
|
26
|
+
constructor(options: { mockUuid: string; externalDns?: string[] }) {
|
|
27
|
+
this.mockUuid = options.mockUuid;
|
|
28
|
+
if (options.externalDns?.length) {
|
|
29
|
+
this.externalDns = new DNSResolver();
|
|
30
|
+
this.externalDns.setServers(options.externalDns);
|
|
31
|
+
}
|
|
32
|
+
this.dnsResolutionCache = getDnsResolutionCache(this.mockUuid);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
isLocalhostIP(ip: string): boolean {
|
|
36
|
+
return localhostIPs.includes(ip);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private async resolveIPs(hostname: string, options?: { useExternalDns?: true }): Promise<[string, ...string[]]> {
|
|
40
|
+
return new Promise<[string, ...string[]]>((resolve, reject) => {
|
|
41
|
+
const callback = (error: NodeJS.ErrnoException | null, addresses: string | string[]) => {
|
|
42
|
+
if (error) {
|
|
43
|
+
logger.debug({ error, hostname }, 'error resolving hostname');
|
|
44
|
+
reject(error);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (typeof addresses === 'string') {
|
|
48
|
+
logger.debug({ ip: [addresses], hostname }, 'resolved hostname');
|
|
49
|
+
resolve([addresses]);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (!addresses || addresses.length === 0) {
|
|
53
|
+
logger.debug({ hostname }, 'missing IPs');
|
|
54
|
+
reject(new Error('No addresses found'));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
logger.debug({ ip: addresses, hostname }, 'resolved hostname');
|
|
58
|
+
resolve([addresses[0], ...addresses.slice(1)]);
|
|
59
|
+
};
|
|
60
|
+
if (options?.useExternalDns) {
|
|
61
|
+
if (!this.externalDns) {
|
|
62
|
+
reject(new Error('external dns servers not set'));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
this.externalDns.resolve(hostname, callback);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
dns.lookup(hostname, callback);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async resolveIP(hostname: string, options?: { useExternalDns?: true }): Promise<string> {
|
|
73
|
+
const cachedIP = this.dnsResolutionCache.get(hostname);
|
|
74
|
+
if (cachedIP) {
|
|
75
|
+
return cachedIP;
|
|
76
|
+
}
|
|
77
|
+
return (await this.resolveIPs(hostname, options))[0];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
isIP(hostname: string): boolean {
|
|
81
|
+
return IP_WITH_OPTIONAL_PORT_REGEX.test(hostname);
|
|
82
|
+
}
|
|
83
|
+
}
|