@mug-lab/cookie-auth-proxy 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/README.md +642 -0
- package/SECURITY.md +9 -0
- package/config.json +43 -0
- package/cookie-auth-proxy.config.example.json +43 -0
- package/overrides/README.md +4 -0
- package/package.json +35 -0
- package/src/admin/admin.css +508 -0
- package/src/admin/editors.js +600 -0
- package/src/admin/events.js +325 -0
- package/src/admin/files.js +113 -0
- package/src/admin/renderers.js +513 -0
- package/src/admin/state.js +325 -0
- package/src/admin.html +178 -0
- package/src/config/index.js +372 -0
- package/src/cookie-auth-proxy.schema.json +233 -0
- package/src/diagnostics.js +154 -0
- package/src/init.js +201 -0
- package/src/logger.js +18 -0
- package/src/proxy.js +158 -0
- package/src/regex.js +101 -0
- package/src/runtime/index.js +671 -0
- package/src/sensitive.js +107 -0
- package/src/server/admin-server.js +562 -0
- package/src/server/proxy-server.js +671 -0
- package/src/server/rewrite.js +228 -0
- package/stubs/README.md +8 -0
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const { compileSafeRegex } = require("../regex");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* プロキシプロセスの実行時ルール状態を扱うモジュール。
|
|
7
|
+
*
|
|
8
|
+
* API履歴、Stub、JSON Override、条件マッチ、Import/Export正規化など、
|
|
9
|
+
* ローカルデバッグ中だけ変化するデータをここに集約する。
|
|
10
|
+
* ネットワーク転送と管理APIルーティングはserver配下に分けている。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 設定ファイル上のルールと起動時Importフォルダから、メモリ上の実行時状態を作る。
|
|
15
|
+
*
|
|
16
|
+
* @param {object} config マージ済みプロキシ設定。
|
|
17
|
+
* @returns {object} config.runtimeへ保持する可変の実行時状態。
|
|
18
|
+
*/
|
|
19
|
+
function createRuntimeState(config) {
|
|
20
|
+
const startupStubs = readStartupStubImports(config);
|
|
21
|
+
const configuredStubs = Array.isArray(config.stubs) ? config.stubs : [];
|
|
22
|
+
const startupJsonOverrides = readStartupJsonOverrideImports(config);
|
|
23
|
+
const configuredJsonOverrides = Array.isArray(config.jsonOverrides)
|
|
24
|
+
? config.jsonOverrides
|
|
25
|
+
: [];
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
history: [],
|
|
29
|
+
nextHistoryId: 1,
|
|
30
|
+
stubsEnabled: config.stubsEnabled === true,
|
|
31
|
+
stubs: [...configuredStubs, ...startupStubs]
|
|
32
|
+
.map(normalizeStub)
|
|
33
|
+
.filter(Boolean),
|
|
34
|
+
jsonOverridesEnabled: config.jsonOverridesEnabled === true,
|
|
35
|
+
jsonOverrides: [...configuredJsonOverrides, ...startupJsonOverrides]
|
|
36
|
+
.map(normalizeJsonOverride)
|
|
37
|
+
.filter(Boolean),
|
|
38
|
+
nextStubId: 1,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 起動時Stubインポートフォルダからルールを読み込む。
|
|
44
|
+
*/
|
|
45
|
+
function readStartupStubImports(config) {
|
|
46
|
+
return readStartupRuleImports({
|
|
47
|
+
config,
|
|
48
|
+
directory: config.startupStubImportDir,
|
|
49
|
+
label: "stub",
|
|
50
|
+
arrayName: "stubs",
|
|
51
|
+
aliases: ["stubs"],
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 起動時Overrideインポートフォルダからルールを読み込む。
|
|
57
|
+
*/
|
|
58
|
+
function readStartupJsonOverrideImports(config) {
|
|
59
|
+
return readStartupRuleImports({
|
|
60
|
+
config,
|
|
61
|
+
directory: config.startupOverrideImportDir,
|
|
62
|
+
label: "JSON override",
|
|
63
|
+
arrayName: "jsonOverrides",
|
|
64
|
+
aliases: ["jsonOverrides", "overrides"],
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 起動時Importフォルダからルールファイルを読み込む。
|
|
70
|
+
* ファイル名順で処理するため、010-base.jsonのようなprefixで優先順位を制御できる。
|
|
71
|
+
*
|
|
72
|
+
* @param {object} options Importオプション。
|
|
73
|
+
* @param {object} options.config マージ済みプロキシ設定。
|
|
74
|
+
* @param {string} options.directory process cwdから見たImportフォルダパス。
|
|
75
|
+
* @param {string} options.label ログ表示用のルール種別。
|
|
76
|
+
* @param {string} options.arrayName オブジェクト形式ファイルで期待する配列名。
|
|
77
|
+
* @param {string[]} options.aliases 受け付ける配列プロパティ名。
|
|
78
|
+
* @returns {object[]} runtime用IDを除いたImport済みルール。
|
|
79
|
+
*/
|
|
80
|
+
function readStartupRuleImports({
|
|
81
|
+
config,
|
|
82
|
+
directory,
|
|
83
|
+
label,
|
|
84
|
+
arrayName,
|
|
85
|
+
aliases,
|
|
86
|
+
}) {
|
|
87
|
+
if (!directory) return [];
|
|
88
|
+
|
|
89
|
+
const importDir = path.resolve(process.cwd(), directory);
|
|
90
|
+
let entries;
|
|
91
|
+
try {
|
|
92
|
+
entries = fs.readdirSync(importDir, { withFileTypes: true });
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (error.code !== "ENOENT") {
|
|
95
|
+
runtimeLog(
|
|
96
|
+
config,
|
|
97
|
+
"error",
|
|
98
|
+
`Startup ${label} import directory is not readable: ${importDir}`,
|
|
99
|
+
error.message,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const files = entries
|
|
106
|
+
.filter(
|
|
107
|
+
(entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".json"),
|
|
108
|
+
)
|
|
109
|
+
.map((entry) => path.join(importDir, entry.name))
|
|
110
|
+
.sort((a, b) => a.localeCompare(b));
|
|
111
|
+
|
|
112
|
+
const rules = [];
|
|
113
|
+
for (const filePath of files) {
|
|
114
|
+
try {
|
|
115
|
+
const body = readJsonFile(filePath);
|
|
116
|
+
const imported = Array.isArray(body)
|
|
117
|
+
? body
|
|
118
|
+
: aliases.map((alias) => body[alias]).find(Array.isArray);
|
|
119
|
+
if (!Array.isArray(imported)) {
|
|
120
|
+
runtimeLog(
|
|
121
|
+
config,
|
|
122
|
+
"error",
|
|
123
|
+
`Skipped startup ${label} import without ${arrayName} array: ${filePath}`,
|
|
124
|
+
);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
rules.push(...imported.map((rule) => ({ ...rule, id: undefined })));
|
|
129
|
+
runtimeLog(
|
|
130
|
+
config,
|
|
131
|
+
"info",
|
|
132
|
+
`Loaded startup ${label}s: ${filePath} (${imported.length})`,
|
|
133
|
+
);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
runtimeLog(
|
|
136
|
+
config,
|
|
137
|
+
"error",
|
|
138
|
+
`Failed to load startup ${label}s: ${filePath}`,
|
|
139
|
+
error.message,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return rules;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* BOMを除去してJSONファイルを読み込む。
|
|
149
|
+
*/
|
|
150
|
+
function readJsonFile(filePath) {
|
|
151
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8").replace(/^\uFEFF/, ""));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 入力されたStub風オブジェクトを内部ルール形式へ正規化する。
|
|
156
|
+
*
|
|
157
|
+
* @param {object} stub ユーザー入力またはImportされたStub定義。
|
|
158
|
+
* @returns {object|null} 正規化済みStub。不正な入力ならnull。
|
|
159
|
+
*/
|
|
160
|
+
function normalizeStub(stub) {
|
|
161
|
+
if (!stub || typeof stub !== "object") return null;
|
|
162
|
+
|
|
163
|
+
const method = normalizeMethod(stub.method, "stub.method");
|
|
164
|
+
const pathPattern = String(stub.pathPattern || stub.path || "^/api(?:/|$)");
|
|
165
|
+
const queryParams = isPlainObject(stub.queryParams) ? stub.queryParams : {};
|
|
166
|
+
const requestBody = isPlainObject(stub.requestBody) ? stub.requestBody : {};
|
|
167
|
+
validateRulePatterns("stub", {
|
|
168
|
+
pathPattern,
|
|
169
|
+
queryParams,
|
|
170
|
+
requestBody,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const bodyEncoding = normalizeBodyEncoding(stub);
|
|
174
|
+
const headers =
|
|
175
|
+
stub.headers && typeof stub.headers === "object"
|
|
176
|
+
? stub.headers
|
|
177
|
+
: { "content-type": "application/json; charset=utf-8" };
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
id: createId(),
|
|
181
|
+
name: String(stub.name || "Untitled stub"),
|
|
182
|
+
enabled: stub.enabled !== false,
|
|
183
|
+
method,
|
|
184
|
+
pathPattern,
|
|
185
|
+
queryParams,
|
|
186
|
+
requestBody,
|
|
187
|
+
status: Number(stub.status || 200),
|
|
188
|
+
headers,
|
|
189
|
+
bodyEncoding,
|
|
190
|
+
body:
|
|
191
|
+
bodyEncoding === "base64"
|
|
192
|
+
? ""
|
|
193
|
+
: normalizeTextStubBody(stub.body, bodyEncoding),
|
|
194
|
+
bodyBase64:
|
|
195
|
+
bodyEncoding === "base64"
|
|
196
|
+
? String(
|
|
197
|
+
stub.bodyBase64 === undefined ? stub.body || "" : stub.bodyBase64,
|
|
198
|
+
)
|
|
199
|
+
: "",
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Stub本文をHTTPレスポンス本文として扱う文字列へ正規化する。
|
|
205
|
+
* JSON Body Typeの本文は起動時Import/API更新時にも検証する。
|
|
206
|
+
*/
|
|
207
|
+
function normalizeTextStubBody(body, bodyEncoding) {
|
|
208
|
+
const text =
|
|
209
|
+
body === undefined
|
|
210
|
+
? "{}"
|
|
211
|
+
: typeof body === "string"
|
|
212
|
+
? body
|
|
213
|
+
: JSON.stringify(body, null, 2);
|
|
214
|
+
|
|
215
|
+
if (bodyEncoding !== "json") return text;
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
return JSON.stringify(JSON.parse(text), null, 2);
|
|
219
|
+
} catch (parseError) {
|
|
220
|
+
const error = new Error(
|
|
221
|
+
`stub.body: invalid JSON body (${parseError.message})`,
|
|
222
|
+
);
|
|
223
|
+
error.statusCode = 400;
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Stub本文種別を内部表現へ正規化する。
|
|
230
|
+
*/
|
|
231
|
+
function normalizeBodyEncoding(stub) {
|
|
232
|
+
if (stub.bodyEncoding === "base64") return "base64";
|
|
233
|
+
if (stub.bodyEncoding === "text") return "text";
|
|
234
|
+
if (stub.bodyEncoding === "json") return "json";
|
|
235
|
+
if (stub.bodyBase64) return "base64";
|
|
236
|
+
return "json";
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 内部Stubを共有しやすいExportファイル形式へ変換する。
|
|
241
|
+
* runtime専用IDは除外し、JSONテキスト本文は素のJSON値として出力する。
|
|
242
|
+
*
|
|
243
|
+
* @param {object} stub 内部形式のStub。
|
|
244
|
+
* @returns {object} Export可能なStub。
|
|
245
|
+
*/
|
|
246
|
+
function exportStub(stub) {
|
|
247
|
+
const { id, ...exported } = stub;
|
|
248
|
+
if (exported.bodyEncoding !== "json") return exported;
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const body = JSON.parse(exported.body);
|
|
252
|
+
if (isPlainObject(body) || Array.isArray(body)) {
|
|
253
|
+
return { ...exported, body };
|
|
254
|
+
}
|
|
255
|
+
} catch {
|
|
256
|
+
// Non-JSON text bodies stay as strings in the export file.
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return exported;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* 内部IDを除外したOverrideのExport用オブジェクトを作る。
|
|
264
|
+
*/
|
|
265
|
+
function exportJsonOverride(override) {
|
|
266
|
+
const { id, ...exported } = override;
|
|
267
|
+
return exported;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 画面操作中のルール識別に使う短いランダムIDを作る。
|
|
272
|
+
*/
|
|
273
|
+
function createId() {
|
|
274
|
+
return Math.random().toString(36).slice(2, 10);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* 入力されたJSON Override風オブジェクトを内部ルール形式へ正規化する。
|
|
279
|
+
*
|
|
280
|
+
* @param {object} override ユーザー入力またはImportされたOverride定義。
|
|
281
|
+
* @returns {object|null} 正規化済みOverride。不正な入力ならnull。
|
|
282
|
+
*/
|
|
283
|
+
function normalizeJsonOverride(override) {
|
|
284
|
+
if (!override || typeof override !== "object") return null;
|
|
285
|
+
|
|
286
|
+
const method = normalizeMethod(override.method, "JSON override.method");
|
|
287
|
+
const pathPattern = String(
|
|
288
|
+
override.pathPattern || override.path || "^/api(?:/|$)",
|
|
289
|
+
);
|
|
290
|
+
const queryParams = isPlainObject(override.queryParams)
|
|
291
|
+
? override.queryParams
|
|
292
|
+
: {};
|
|
293
|
+
const requestBody = isPlainObject(override.requestBody)
|
|
294
|
+
? override.requestBody
|
|
295
|
+
: {};
|
|
296
|
+
validateRulePatterns("JSON override", {
|
|
297
|
+
pathPattern,
|
|
298
|
+
queryParams,
|
|
299
|
+
requestBody,
|
|
300
|
+
});
|
|
301
|
+
const value = override.value === undefined ? {} : override.value;
|
|
302
|
+
if (!isPlainObject(value)) {
|
|
303
|
+
const error = new Error("JSON override.value: object JSON is required.");
|
|
304
|
+
error.statusCode = 400;
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
id: createId(),
|
|
310
|
+
name: String(override.name || "Untitled override"),
|
|
311
|
+
enabled: override.enabled !== false,
|
|
312
|
+
method,
|
|
313
|
+
pathPattern,
|
|
314
|
+
queryParams,
|
|
315
|
+
requestBody,
|
|
316
|
+
value,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* HTTPメソッドを許可リストで検証して正規化する。
|
|
322
|
+
*/
|
|
323
|
+
function normalizeMethod(method, label) {
|
|
324
|
+
const normalized = String(method || "GET").toUpperCase();
|
|
325
|
+
const allowed = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "*"];
|
|
326
|
+
if (!allowed.includes(normalized)) {
|
|
327
|
+
const error = new Error(`${label}: unsupported HTTP method.`);
|
|
328
|
+
error.statusCode = 400;
|
|
329
|
+
throw error;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return normalized;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* リクエスト条件に一致する最初の有効なStubを探す。
|
|
337
|
+
*/
|
|
338
|
+
function findStub(config, method, requestMatch) {
|
|
339
|
+
if (!config.runtime.stubsEnabled) return null;
|
|
340
|
+
|
|
341
|
+
return config.runtime.stubs.find((stub) => {
|
|
342
|
+
if (!stub.enabled) return false;
|
|
343
|
+
if (stub.method !== "*" && stub.method !== method.toUpperCase())
|
|
344
|
+
return false;
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
return (
|
|
348
|
+
matchesPathPattern(stub.pathPattern, requestMatch.pathname) &&
|
|
349
|
+
matchesObjectSubset(requestMatch.queryParams, stub.queryParams) &&
|
|
350
|
+
matchesObjectSubset(requestMatch.requestBody, stub.requestBody)
|
|
351
|
+
);
|
|
352
|
+
} catch {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* リクエスト条件に一致する有効なJSON Overrideを優先順で抽出する。
|
|
360
|
+
*/
|
|
361
|
+
function findJsonOverrides(config, method, requestMatch) {
|
|
362
|
+
if (!config.runtime.jsonOverridesEnabled) return [];
|
|
363
|
+
|
|
364
|
+
return config.runtime.jsonOverrides.filter((override) => {
|
|
365
|
+
if (!override.enabled) return false;
|
|
366
|
+
if (override.method !== "*" && override.method !== method.toUpperCase())
|
|
367
|
+
return false;
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
return (
|
|
371
|
+
matchesPathPattern(override.pathPattern, requestMatch.pathname) &&
|
|
372
|
+
matchesObjectSubset(requestMatch.queryParams, override.queryParams) &&
|
|
373
|
+
matchesObjectSubset(requestMatch.requestBody, override.requestBody)
|
|
374
|
+
);
|
|
375
|
+
} catch {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* リクエストパスがルールのパス正規表現に一致するか判定する。
|
|
383
|
+
*/
|
|
384
|
+
function matchesPathPattern(pathPattern, pathname) {
|
|
385
|
+
return compileSafeRegex(pathPattern, "pathPattern").test(pathname);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* 期待オブジェクトの指定項目が実データに一致するか判定する。
|
|
390
|
+
*/
|
|
391
|
+
function matchesObjectSubset(actual, expected) {
|
|
392
|
+
if (!isPlainObject(expected) || Object.keys(expected).length === 0)
|
|
393
|
+
return true;
|
|
394
|
+
if (!isPlainObject(actual)) return false;
|
|
395
|
+
|
|
396
|
+
return Object.entries(expected).every(([key, expectedValue]) =>
|
|
397
|
+
matchesValue(actual[key], expectedValue),
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* ルール値を正規表現または文字列として実値と比較する。
|
|
403
|
+
*/
|
|
404
|
+
function matchesValue(actualValue, expectedValue) {
|
|
405
|
+
if (isPlainObject(expectedValue)) {
|
|
406
|
+
return matchesObjectSubset(actualValue, expectedValue);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (Array.isArray(expectedValue)) {
|
|
410
|
+
const actualValues = Array.isArray(actualValue)
|
|
411
|
+
? actualValue
|
|
412
|
+
: [actualValue];
|
|
413
|
+
return expectedValue.every((value) =>
|
|
414
|
+
actualValues.some((actual) => matchesValue(actual, value)),
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (expectedValue === null)
|
|
419
|
+
return actualValue === null || actualValue === undefined;
|
|
420
|
+
if (actualValue === undefined) return false;
|
|
421
|
+
const actualValues = Array.isArray(actualValue) ? actualValue : [actualValue];
|
|
422
|
+
try {
|
|
423
|
+
const pattern = compileSafeRegex(String(expectedValue), "value pattern");
|
|
424
|
+
return actualValues.some((actual) => pattern.test(String(actual)));
|
|
425
|
+
} catch {
|
|
426
|
+
return actualValues.some(
|
|
427
|
+
(actual) => String(actual) === String(expectedValue),
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Stub/Overrideの条件正規表現を登録前に検査する。
|
|
434
|
+
*/
|
|
435
|
+
function validateRulePatterns(label, rule) {
|
|
436
|
+
compileSafeRegex(rule.pathPattern, `${label}.pathPattern`);
|
|
437
|
+
validateRegexObject(`${label}.queryParams`, rule.queryParams);
|
|
438
|
+
validateRegexObject(`${label}.requestBody`, rule.requestBody);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* オブジェクト内の正規表現条件を再帰的に検査する。
|
|
443
|
+
*/
|
|
444
|
+
function validateRegexObject(label, value) {
|
|
445
|
+
if (!isPlainObject(value)) return;
|
|
446
|
+
|
|
447
|
+
Object.entries(value).forEach(([key, entry]) => {
|
|
448
|
+
const entryLabel = `${label}.${key}`;
|
|
449
|
+
if (Array.isArray(entry)) {
|
|
450
|
+
entry.forEach((item, index) => validateRegexValue(`${entryLabel}[${index}]`, item));
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
validateRegexValue(entryLabel, entry);
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* 条件値として使われる正規表現文字列を検査する。
|
|
460
|
+
*/
|
|
461
|
+
function validateRegexValue(label, value) {
|
|
462
|
+
if (value === null || value === undefined) return;
|
|
463
|
+
if (Array.isArray(value)) {
|
|
464
|
+
value.forEach((item, index) => validateRegexValue(`${label}[${index}]`, item));
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
compileSafeRegex(String(value), label);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* API通信履歴へ1件追加し、上限件数を超えた古い履歴を破棄する。
|
|
473
|
+
*/
|
|
474
|
+
function addHistory(config, entry) {
|
|
475
|
+
const item = {
|
|
476
|
+
id: config.runtime.nextHistoryId,
|
|
477
|
+
at: new Date().toISOString(),
|
|
478
|
+
...entry,
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
config.runtime.nextHistoryId += 1;
|
|
482
|
+
config.runtime.history.push(item);
|
|
483
|
+
if (config.runtime.history.length > config.historyLimit) {
|
|
484
|
+
config.runtime.history.splice(
|
|
485
|
+
0,
|
|
486
|
+
config.runtime.history.length - config.historyLimit,
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* trimBodyを実行する。
|
|
493
|
+
*/
|
|
494
|
+
function trimBody(value) {
|
|
495
|
+
return Buffer.isBuffer(value) ? value.toString("utf8") : String(value || "");
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Stub本文をレスポンス送信用Bufferへ変換する。
|
|
500
|
+
*/
|
|
501
|
+
function stubBodyBuffer(stub) {
|
|
502
|
+
return stub.bodyEncoding === "base64"
|
|
503
|
+
? Buffer.from(stub.bodyBase64 || "", "base64")
|
|
504
|
+
: Buffer.from(stub.body || "", "utf8");
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* レスポンスBufferを履歴保存用の本文情報へ変換する。
|
|
509
|
+
*/
|
|
510
|
+
function historyBodyFromBuffer(buffer, encoding, maxBytes = Infinity) {
|
|
511
|
+
const truncated = Number.isFinite(maxBytes) && buffer.length > maxBytes;
|
|
512
|
+
const storedBuffer = truncated ? buffer.subarray(buffer.length - maxBytes) : buffer;
|
|
513
|
+
return historyBodyFromStoredBuffer(storedBuffer, buffer.length, encoding);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* すでに上限内へ切り詰めたレスポンスBufferを履歴保存用の本文情報へ変換する。
|
|
518
|
+
*/
|
|
519
|
+
function historyBodyFromStoredBuffer(storedBuffer, totalBytes, encoding) {
|
|
520
|
+
const truncated = storedBuffer.length < totalBytes;
|
|
521
|
+
|
|
522
|
+
if (encoding === "base64") {
|
|
523
|
+
return {
|
|
524
|
+
responseBodyEncoding: "base64",
|
|
525
|
+
responseBody: "",
|
|
526
|
+
responseBodyBase64: storedBuffer.toString("base64"),
|
|
527
|
+
responseBodySize: totalBytes,
|
|
528
|
+
responseBodyStoredSize: storedBuffer.length,
|
|
529
|
+
responseBodyTruncated: truncated,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return {
|
|
534
|
+
responseBodyEncoding: "text",
|
|
535
|
+
responseBody: trimBody(storedBuffer),
|
|
536
|
+
responseBodyBase64: "",
|
|
537
|
+
responseBodySize: totalBytes,
|
|
538
|
+
responseBodyStoredSize: storedBuffer.length,
|
|
539
|
+
responseBodyTruncated: truncated,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* 一致したJSON Override値をAPIのJSONレスポンス本文へ適用する。
|
|
545
|
+
*
|
|
546
|
+
* @param {Buffer} buffer レスポンス本文。
|
|
547
|
+
* @param {object[]} overrides 優先順に並んだ一致済みOverrideルール。
|
|
548
|
+
* @returns {{body: Buffer, appliedOverrideIds: string[]}} 書き換え後本文と適用ルールID。
|
|
549
|
+
*/
|
|
550
|
+
function applyJsonOverrides(buffer, overrides) {
|
|
551
|
+
if (overrides.length === 0) {
|
|
552
|
+
return { body: buffer, appliedOverrideIds: [] };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
let parsed;
|
|
556
|
+
try {
|
|
557
|
+
parsed = JSON.parse(buffer.toString("utf8"));
|
|
558
|
+
} catch {
|
|
559
|
+
return { body: buffer, appliedOverrideIds: [] };
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (!isPlainObject(parsed)) {
|
|
563
|
+
return { body: buffer, appliedOverrideIds: [] };
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
for (const override of overrides) {
|
|
567
|
+
deepMerge(parsed, override.value);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return {
|
|
571
|
+
body: Buffer.from(JSON.stringify(parsed), "utf8"),
|
|
572
|
+
appliedOverrideIds: overrides.map((override) => override.id),
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Override値を既存レスポンスJSONへ再帰的にマージする。
|
|
578
|
+
*/
|
|
579
|
+
function deepMerge(target, source) {
|
|
580
|
+
if (!isPlainObject(source)) return target;
|
|
581
|
+
|
|
582
|
+
for (const [key, value] of Object.entries(source)) {
|
|
583
|
+
if (isPlainObject(value) && isPlainObject(target[key])) {
|
|
584
|
+
deepMerge(target[key], value);
|
|
585
|
+
} else {
|
|
586
|
+
target[key] = value;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return target;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* 配列ではない通常オブジェクトか判定する。
|
|
595
|
+
*/
|
|
596
|
+
function isPlainObject(value) {
|
|
597
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* URLSearchParamsを重複キー対応のプレーンオブジェクトへ変換する。
|
|
602
|
+
*/
|
|
603
|
+
function queryParamsObject(searchParams) {
|
|
604
|
+
const params = {};
|
|
605
|
+
for (const [key, value] of searchParams.entries()) {
|
|
606
|
+
if (Object.prototype.hasOwnProperty.call(params, key)) {
|
|
607
|
+
params[key] = Array.isArray(params[key])
|
|
608
|
+
? [...params[key], value]
|
|
609
|
+
: [params[key], value];
|
|
610
|
+
} else {
|
|
611
|
+
params[key] = value;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return params;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* JSONまたはフォーム形式のリクエストBodyを条件判定用オブジェクトへ変換する。
|
|
619
|
+
*/
|
|
620
|
+
function parseRequestBody(headers, body) {
|
|
621
|
+
if (!body || body.length === 0) return {};
|
|
622
|
+
|
|
623
|
+
const contentType = String(headers["content-type"] || "").toLowerCase();
|
|
624
|
+
const text = body.toString("utf8");
|
|
625
|
+
|
|
626
|
+
if (
|
|
627
|
+
contentType.includes("application/json") ||
|
|
628
|
+
/^[\s\r\n]*[\[{]/.test(text)
|
|
629
|
+
) {
|
|
630
|
+
try {
|
|
631
|
+
const parsed = JSON.parse(text);
|
|
632
|
+
return isPlainObject(parsed) ? parsed : {};
|
|
633
|
+
} catch {
|
|
634
|
+
return {};
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
639
|
+
return queryParamsObject(new URLSearchParams(text));
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return {};
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* runtimeモジュール内の起動時読み込みログを出力する。
|
|
647
|
+
*/
|
|
648
|
+
function runtimeLog(config, level, ...parts) {
|
|
649
|
+
const rank = { silent: 0, error: 1, info: 2, debug: 3 };
|
|
650
|
+
if ((rank[config.logLevel] || rank.info) < rank[level]) return;
|
|
651
|
+
|
|
652
|
+
const stream = level === "error" ? console.error : console.log;
|
|
653
|
+
stream(`[${new Date().toISOString()}]`, ...parts);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
module.exports = {
|
|
657
|
+
addHistory,
|
|
658
|
+
applyJsonOverrides,
|
|
659
|
+
createRuntimeState,
|
|
660
|
+
exportJsonOverride,
|
|
661
|
+
exportStub,
|
|
662
|
+
findJsonOverrides,
|
|
663
|
+
findStub,
|
|
664
|
+
historyBodyFromBuffer,
|
|
665
|
+
historyBodyFromStoredBuffer,
|
|
666
|
+
normalizeJsonOverride,
|
|
667
|
+
normalizeStub,
|
|
668
|
+
parseRequestBody,
|
|
669
|
+
queryParamsObject,
|
|
670
|
+
stubBodyBuffer,
|
|
671
|
+
};
|