@zimic/interceptor 0.17.0-canary.2 → 0.17.0-canary.3
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/dist/{chunk-3SKHNQLL.mjs → chunk-L75WKVZO.mjs} +508 -60
- package/dist/chunk-L75WKVZO.mjs.map +1 -0
- package/dist/{chunk-TYHJPU5G.js → chunk-PURXNE6R.js} +519 -61
- package/dist/chunk-PURXNE6R.js.map +1 -0
- package/dist/cli.js +141 -17
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +137 -13
- package/dist/cli.mjs.map +1 -1
- package/dist/http.d.ts +14 -0
- package/dist/http.js +449 -269
- package/dist/http.js.map +1 -1
- package/dist/http.mjs +449 -269
- package/dist/http.mjs.map +1 -1
- package/dist/scripts/postinstall.js +6 -6
- package/dist/scripts/postinstall.js.map +1 -1
- package/dist/scripts/postinstall.mjs +5 -5
- package/dist/scripts/postinstall.mjs.map +1 -1
- package/dist/server.d.ts +16 -0
- package/dist/server.js +6 -6
- package/dist/server.mjs +1 -1
- package/package.json +11 -10
- package/src/cli/browser/init.ts +5 -6
- package/src/cli/cli.ts +140 -55
- package/src/cli/server/start.ts +22 -7
- package/src/cli/server/token/create.ts +33 -0
- package/src/cli/server/token/list.ts +23 -0
- package/src/cli/server/token/remove.ts +22 -0
- package/src/http/interceptor/HttpInterceptorClient.ts +49 -27
- package/src/http/interceptor/HttpInterceptorStore.ts +25 -9
- package/src/http/interceptor/LocalHttpInterceptor.ts +6 -3
- package/src/http/interceptor/RemoteHttpInterceptor.ts +9 -4
- package/src/http/interceptor/types/options.ts +15 -0
- package/src/http/interceptorWorker/HttpInterceptorWorker.ts +14 -16
- package/src/http/interceptorWorker/RemoteHttpInterceptorWorker.ts +17 -12
- package/src/http/interceptorWorker/types/options.ts +1 -0
- package/src/http/requestHandler/errors/TimesCheckError.ts +1 -1
- package/src/server/InterceptorServer.ts +52 -8
- package/src/server/constants.ts +1 -1
- package/src/server/errors/InvalidInterceptorTokenError.ts +13 -0
- package/src/server/errors/InvalidInterceptorTokenFileError.ts +13 -0
- package/src/server/errors/InvalidInterceptorTokenValueError.ts +13 -0
- package/src/server/types/options.ts +9 -0
- package/src/server/types/public.ts +9 -0
- package/src/server/types/schema.ts +4 -4
- package/src/server/utils/auth.ts +301 -0
- package/src/server/utils/fetch.ts +3 -1
- package/src/utils/data.ts +13 -0
- package/src/utils/files.ts +14 -0
- package/src/utils/{console.ts → logging.ts} +5 -7
- package/src/utils/webSocket.ts +57 -11
- package/src/webSocket/WebSocketClient.ts +11 -5
- package/src/webSocket/WebSocketHandler.ts +72 -51
- package/src/webSocket/WebSocketServer.ts +25 -4
- package/src/webSocket/constants.ts +2 -0
- package/src/webSocket/errors/UnauthorizedWebSocketConnectionError.ts +11 -0
- package/src/webSocket/types.ts +49 -52
- package/dist/chunk-3SKHNQLL.mjs.map +0 -1
- package/dist/chunk-TYHJPU5G.js.map +0 -1
|
@@ -16,7 +16,6 @@ import validateURLProtocol from '@zimic/utils/url/validateURLProtocol';
|
|
|
16
16
|
import { isServerSide } from '@/utils/environment';
|
|
17
17
|
|
|
18
18
|
import HttpInterceptorWorker from '../interceptorWorker/HttpInterceptorWorker';
|
|
19
|
-
import LocalHttpInterceptorWorker from '../interceptorWorker/LocalHttpInterceptorWorker';
|
|
20
19
|
import HttpRequestHandlerClient, { AnyHttpRequestHandlerClient } from '../requestHandler/HttpRequestHandlerClient';
|
|
21
20
|
import LocalHttpRequestHandler from '../requestHandler/LocalHttpRequestHandler';
|
|
22
21
|
import RemoteHttpRequestHandler from '../requestHandler/RemoteHttpRequestHandler';
|
|
@@ -38,11 +37,13 @@ class HttpInterceptorClient<
|
|
|
38
37
|
Schema extends HttpSchema,
|
|
39
38
|
HandlerConstructor extends HttpRequestHandlerConstructor = HttpRequestHandlerConstructor,
|
|
40
39
|
> {
|
|
41
|
-
private worker: HttpInterceptorWorker;
|
|
42
40
|
private store: HttpInterceptorStore;
|
|
43
|
-
|
|
44
41
|
private _baseURL!: URL;
|
|
45
42
|
|
|
43
|
+
private createWorker: () => HttpInterceptorWorker;
|
|
44
|
+
private deleteWorker: () => void;
|
|
45
|
+
private worker?: HttpInterceptorWorker;
|
|
46
|
+
|
|
46
47
|
requestSaving: HttpInterceptorRequestSaving;
|
|
47
48
|
private numberOfSavedRequests = 0;
|
|
48
49
|
|
|
@@ -67,18 +68,20 @@ class HttpInterceptorClient<
|
|
|
67
68
|
};
|
|
68
69
|
|
|
69
70
|
constructor(options: {
|
|
70
|
-
worker: HttpInterceptorWorker;
|
|
71
71
|
store: HttpInterceptorStore;
|
|
72
72
|
baseURL: URL;
|
|
73
|
+
createWorker: () => HttpInterceptorWorker;
|
|
74
|
+
deleteWorker: () => void;
|
|
73
75
|
requestSaving?: Partial<HttpInterceptorRequestSaving>;
|
|
74
76
|
onUnhandledRequest?: UnhandledRequestStrategy;
|
|
75
77
|
Handler: HandlerConstructor;
|
|
76
78
|
}) {
|
|
77
|
-
this.worker = options.worker;
|
|
78
79
|
this.store = options.store;
|
|
79
|
-
|
|
80
80
|
this.baseURL = options.baseURL;
|
|
81
81
|
|
|
82
|
+
this.createWorker = options.createWorker;
|
|
83
|
+
this.deleteWorker = options.deleteWorker;
|
|
84
|
+
|
|
82
85
|
this.requestSaving = {
|
|
83
86
|
enabled: options.requestSaving?.enabled ?? this.getDefaultRequestSavingEnabled(),
|
|
84
87
|
safeLimit: options.requestSaving?.safeLimit ?? DEFAULT_REQUEST_SAVING_SAFE_LIMIT,
|
|
@@ -108,6 +111,7 @@ class HttpInterceptorClient<
|
|
|
108
111
|
|
|
109
112
|
validateURLProtocol(newBaseURL, SUPPORTED_BASE_URL_PROTOCOLS);
|
|
110
113
|
excludeURLParams(newBaseURL);
|
|
114
|
+
|
|
111
115
|
this._baseURL = newBaseURL;
|
|
112
116
|
}
|
|
113
117
|
|
|
@@ -118,29 +122,48 @@ class HttpInterceptorClient<
|
|
|
118
122
|
return this.baseURL.href;
|
|
119
123
|
}
|
|
120
124
|
|
|
125
|
+
private get workerOrThrow() {
|
|
126
|
+
if (!this.worker) {
|
|
127
|
+
throw new NotRunningHttpInterceptorError();
|
|
128
|
+
}
|
|
129
|
+
return this.worker;
|
|
130
|
+
}
|
|
131
|
+
|
|
121
132
|
get platform() {
|
|
122
|
-
return this.worker
|
|
133
|
+
return this.worker?.platform ?? null;
|
|
123
134
|
}
|
|
124
135
|
|
|
125
136
|
async start() {
|
|
126
|
-
|
|
137
|
+
try {
|
|
138
|
+
this.worker = this.createWorker();
|
|
127
139
|
|
|
128
|
-
|
|
129
|
-
|
|
140
|
+
await this.worker.start();
|
|
141
|
+
this.worker.registerRunningInterceptor(this);
|
|
142
|
+
|
|
143
|
+
this.markAsRunning(true);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
await this.stop();
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
130
148
|
}
|
|
131
149
|
|
|
132
150
|
async stop() {
|
|
133
|
-
this.
|
|
134
|
-
this.worker.unregisterRunningInterceptor(this);
|
|
151
|
+
this.worker?.unregisterRunningInterceptor(this);
|
|
135
152
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
153
|
+
// The number of interceptors will be 0 if the first client could not start due to an error.
|
|
154
|
+
const isLastRunningInterceptor = this.numberOfRunningInterceptors === 0 || this.numberOfRunningInterceptors === 1;
|
|
155
|
+
|
|
156
|
+
if (isLastRunningInterceptor) {
|
|
157
|
+
await this.worker?.stop();
|
|
158
|
+
this.deleteWorker();
|
|
139
159
|
}
|
|
160
|
+
|
|
161
|
+
this.markAsRunning(false);
|
|
162
|
+
this.worker = undefined;
|
|
140
163
|
}
|
|
141
164
|
|
|
142
165
|
private markAsRunning(isRunning: boolean) {
|
|
143
|
-
if (this.
|
|
166
|
+
if (this.workerOrThrow.type === 'local') {
|
|
144
167
|
this.store.markLocalInterceptorAsRunning(this, isRunning);
|
|
145
168
|
} else {
|
|
146
169
|
this.store.markRemoteInterceptorAsRunning(this, isRunning, this.baseURL);
|
|
@@ -148,8 +171,12 @@ class HttpInterceptorClient<
|
|
|
148
171
|
this.isRunning = isRunning;
|
|
149
172
|
}
|
|
150
173
|
|
|
151
|
-
|
|
152
|
-
if (this.
|
|
174
|
+
get numberOfRunningInterceptors() {
|
|
175
|
+
if (!this.isRunning) {
|
|
176
|
+
return 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (this.workerOrThrow.type === 'local') {
|
|
153
180
|
return this.store.numberOfRunningLocalInterceptors;
|
|
154
181
|
} else {
|
|
155
182
|
return this.store.numberOfRunningRemoteInterceptors(this.baseURL);
|
|
@@ -222,7 +249,7 @@ class HttpInterceptorClient<
|
|
|
222
249
|
const url = joinURL(this.baseURLAsString, handler.path);
|
|
223
250
|
const urlRegex = createRegExpFromURL(url);
|
|
224
251
|
|
|
225
|
-
const registrationResult = this.
|
|
252
|
+
const registrationResult = this.workerOrThrow.use(this, handler.method, url, async (context) => {
|
|
226
253
|
const response = await this.handleInterceptedRequest(
|
|
227
254
|
urlRegex,
|
|
228
255
|
handler.method,
|
|
@@ -316,11 +343,9 @@ class HttpInterceptorClient<
|
|
|
316
343
|
}
|
|
317
344
|
|
|
318
345
|
clear(options: { onCommitSuccess?: () => void; onCommitError?: () => void } = {}) {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const clearResults: PossiblePromise<AnyHttpRequestHandlerClient | void>[] = [];
|
|
346
|
+
const clearResults: PossiblePromise<AnyHttpRequestHandlerClient | void>[] = [
|
|
347
|
+
this.workerOrThrow.clearInterceptorHandlers(this),
|
|
348
|
+
];
|
|
324
349
|
|
|
325
350
|
for (const method of HTTP_METHODS) {
|
|
326
351
|
const newClearResults = this.clearMethodHandlers(method);
|
|
@@ -333,9 +358,6 @@ class HttpInterceptorClient<
|
|
|
333
358
|
handlersByPath.clear();
|
|
334
359
|
}
|
|
335
360
|
|
|
336
|
-
const clearResult = this.worker.clearInterceptorHandlers(this);
|
|
337
|
-
clearResults.push(clearResult);
|
|
338
|
-
|
|
339
361
|
if (options.onCommitSuccess) {
|
|
340
362
|
void Promise.all(clearResults).then(options.onCommitSuccess, options.onCommitError);
|
|
341
363
|
}
|
|
@@ -7,6 +7,10 @@ import {
|
|
|
7
7
|
} from '../interceptorWorker/types/options';
|
|
8
8
|
import { AnyHttpInterceptorClient } from './HttpInterceptorClient';
|
|
9
9
|
|
|
10
|
+
interface RemoteWorkerKeyOptions {
|
|
11
|
+
auth: RemoteHttpInterceptorWorkerOptions['auth'];
|
|
12
|
+
}
|
|
13
|
+
|
|
10
14
|
class HttpInterceptorStore {
|
|
11
15
|
private static _localWorker?: LocalHttpInterceptorWorker;
|
|
12
16
|
private static runningLocalInterceptors = new Set<AnyHttpInterceptorClient>();
|
|
@@ -20,8 +24,16 @@ class HttpInterceptorStore {
|
|
|
20
24
|
return this.class._localWorker;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
private getRemoteWorkerKey(baseURL: URL, options: RemoteWorkerKeyOptions) {
|
|
28
|
+
if (!options.auth) {
|
|
29
|
+
return baseURL.origin;
|
|
30
|
+
}
|
|
31
|
+
return `${baseURL.origin}:${options.auth.token}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
remoteWorker(baseURL: URL, options: RemoteWorkerKeyOptions) {
|
|
35
|
+
const remoteWorkerKey = this.getRemoteWorkerKey(baseURL, options);
|
|
36
|
+
return this.class.remoteWorkers.get(remoteWorkerKey);
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
get numberOfRunningLocalInterceptors() {
|
|
@@ -70,23 +82,27 @@ class HttpInterceptorStore {
|
|
|
70
82
|
return createdWorker;
|
|
71
83
|
}
|
|
72
84
|
|
|
85
|
+
deleteLocalWorker() {
|
|
86
|
+
this.class._localWorker = undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
73
89
|
getOrCreateRemoteWorker(workerOptions: Omit<RemoteHttpInterceptorWorkerOptions, 'type'>) {
|
|
74
|
-
const
|
|
90
|
+
const remoteWorkerKey = this.getRemoteWorkerKey(workerOptions.serverURL, { auth: workerOptions.auth });
|
|
91
|
+
const existingWorker = this.class.remoteWorkers.get(remoteWorkerKey);
|
|
92
|
+
|
|
75
93
|
if (existingWorker) {
|
|
76
94
|
return existingWorker;
|
|
77
95
|
}
|
|
78
96
|
|
|
79
97
|
const createdWorker = createHttpInterceptorWorker({ ...workerOptions, type: 'remote' });
|
|
80
|
-
this.class.remoteWorkers.set(
|
|
98
|
+
this.class.remoteWorkers.set(remoteWorkerKey, createdWorker);
|
|
81
99
|
|
|
82
100
|
return createdWorker;
|
|
83
101
|
}
|
|
84
102
|
|
|
85
|
-
|
|
86
|
-
this.
|
|
87
|
-
this.class.
|
|
88
|
-
this.class.remoteWorkers.clear();
|
|
89
|
-
this.class.runningRemoteInterceptors.clear();
|
|
103
|
+
deleteRemoteWorker(baseURL: URL, options: RemoteWorkerKeyOptions) {
|
|
104
|
+
const remoteWorkerKey = this.getRemoteWorkerKey(baseURL, options);
|
|
105
|
+
this.class.remoteWorkers.delete(remoteWorkerKey);
|
|
90
106
|
}
|
|
91
107
|
}
|
|
92
108
|
|
|
@@ -15,12 +15,15 @@ class LocalHttpInterceptor<Schema extends HttpSchema> implements PublicLocalHttp
|
|
|
15
15
|
constructor(options: LocalHttpInterceptorOptions) {
|
|
16
16
|
const baseURL = new URL(options.baseURL);
|
|
17
17
|
|
|
18
|
-
const worker = this.store.getOrCreateLocalWorker({});
|
|
19
|
-
|
|
20
18
|
this.client = new HttpInterceptorClient<Schema, typeof LocalHttpRequestHandler>({
|
|
21
|
-
worker,
|
|
22
19
|
store: this.store,
|
|
23
20
|
baseURL,
|
|
21
|
+
createWorker() {
|
|
22
|
+
return this.store.getOrCreateLocalWorker({});
|
|
23
|
+
},
|
|
24
|
+
deleteWorker() {
|
|
25
|
+
this.store.deleteLocalWorker();
|
|
26
|
+
},
|
|
24
27
|
Handler: LocalHttpRequestHandler,
|
|
25
28
|
onUnhandledRequest: options.onUnhandledRequest,
|
|
26
29
|
requestSaving: options.requestSaving,
|
|
@@ -15,13 +15,18 @@ class RemoteHttpInterceptor<Schema extends HttpSchema> implements PublicRemoteHt
|
|
|
15
15
|
constructor(options: RemoteHttpInterceptorOptions) {
|
|
16
16
|
const baseURL = new URL(options.baseURL);
|
|
17
17
|
|
|
18
|
-
const serverURL = new URL(baseURL.origin);
|
|
19
|
-
const worker = this.store.getOrCreateRemoteWorker({ serverURL });
|
|
20
|
-
|
|
21
18
|
this.client = new HttpInterceptorClient<Schema, typeof RemoteHttpRequestHandler>({
|
|
22
|
-
worker,
|
|
23
19
|
store: this.store,
|
|
24
20
|
baseURL,
|
|
21
|
+
createWorker() {
|
|
22
|
+
return this.store.getOrCreateRemoteWorker({
|
|
23
|
+
serverURL: new URL(baseURL.origin),
|
|
24
|
+
auth: options.auth,
|
|
25
|
+
});
|
|
26
|
+
},
|
|
27
|
+
deleteWorker() {
|
|
28
|
+
this.store.deleteRemoteWorker(baseURL, { auth: options.auth });
|
|
29
|
+
},
|
|
25
30
|
Handler: RemoteHttpRequestHandler,
|
|
26
31
|
onUnhandledRequest: options.onUnhandledRequest,
|
|
27
32
|
requestSaving: options.requestSaving,
|
|
@@ -160,6 +160,21 @@ export interface LocalHttpInterceptorOptions extends SharedHttpInterceptorOption
|
|
|
160
160
|
export interface RemoteHttpInterceptorOptions extends SharedHttpInterceptorOptions {
|
|
161
161
|
type: 'remote';
|
|
162
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Options to authenticate the interceptor when connecting to an interceptor server. This is required if the
|
|
165
|
+
* interceptor server was started with the `--tokens-dir` option.
|
|
166
|
+
*
|
|
167
|
+
* @see {@link https://github.com/zimicjs/zimic/wiki/cli‐zimic‐interceptor‐server#authentication Interceptor server authentication}
|
|
168
|
+
*/
|
|
169
|
+
auth?: {
|
|
170
|
+
/**
|
|
171
|
+
* The authentication token to use.
|
|
172
|
+
*
|
|
173
|
+
* @see {@link https://github.com/zimicjs/zimic/wiki/cli‐zimic‐interceptor‐server#authentication Interceptor server authentication}
|
|
174
|
+
*/
|
|
175
|
+
token: string;
|
|
176
|
+
};
|
|
177
|
+
|
|
163
178
|
/**
|
|
164
179
|
* The strategy to use for unhandled requests. If a request starts with the base URL of the interceptor, but no
|
|
165
180
|
* matching handler exists, this strategy will be used. If a function is provided, it will be called with the
|
|
@@ -16,9 +16,9 @@ import { Default, PossiblePromise } from '@zimic/utils/types';
|
|
|
16
16
|
import color from 'picocolors';
|
|
17
17
|
|
|
18
18
|
import { removeArrayElement } from '@/utils/arrays';
|
|
19
|
-
import { formatValueToLog, logWithPrefix } from '@/utils/console';
|
|
20
19
|
import { isClientSide } from '@/utils/environment';
|
|
21
20
|
import { methodCanHaveResponseBody } from '@/utils/http';
|
|
21
|
+
import { formatValueToLog, logger } from '@/utils/logging';
|
|
22
22
|
|
|
23
23
|
import HttpInterceptorClient, { AnyHttpInterceptorClient } from '../interceptor/HttpInterceptorClient';
|
|
24
24
|
import { HttpInterceptorPlatform, HttpInterceptorType, UnhandledRequestStrategy } from '../interceptor/types/options';
|
|
@@ -35,10 +35,11 @@ import {
|
|
|
35
35
|
import { DEFAULT_UNHANDLED_REQUEST_STRATEGY } from './constants';
|
|
36
36
|
import InvalidFormDataError from './errors/InvalidFormDataError';
|
|
37
37
|
import InvalidJSONError from './errors/InvalidJSONError';
|
|
38
|
+
import { HttpInterceptorWorkerType } from './types/options';
|
|
38
39
|
import { HttpResponseFactory } from './types/requests';
|
|
39
40
|
|
|
40
41
|
abstract class HttpInterceptorWorker {
|
|
41
|
-
abstract
|
|
42
|
+
abstract get type(): HttpInterceptorWorkerType;
|
|
42
43
|
|
|
43
44
|
platform: HttpInterceptorPlatform | null = null;
|
|
44
45
|
isRunning = false;
|
|
@@ -474,20 +475,17 @@ abstract class HttpInterceptorWorker {
|
|
|
474
475
|
formatValueToLog(request.body),
|
|
475
476
|
]);
|
|
476
477
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
`${action === 'bypass' ? '
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
'\n\nLearn more: https://github.com/zimicjs/zimic/wiki/api‐zimic‐interceptor‐http#unhandled-requests',
|
|
489
|
-
],
|
|
490
|
-
{ method: action === 'bypass' ? 'warn' : 'error' },
|
|
478
|
+
logger[action === 'bypass' ? 'warn' : 'error'](
|
|
479
|
+
`${action === 'bypass' ? 'Warning:' : 'Error:'} Request was not handled and was ` +
|
|
480
|
+
`${action === 'bypass' ? color.yellow('bypassed') : color.red('rejected')}.\n\n `,
|
|
481
|
+
`${request.method} ${request.url}`,
|
|
482
|
+
'\n Headers:',
|
|
483
|
+
formattedHeaders,
|
|
484
|
+
'\n Search params:',
|
|
485
|
+
formattedSearchParams,
|
|
486
|
+
'\n Body:',
|
|
487
|
+
formattedBody,
|
|
488
|
+
'\n\nLearn more: https://github.com/zimicjs/zimic/wiki/api‐zimic‐interceptor‐http#unhandled-requests',
|
|
491
489
|
);
|
|
492
490
|
}
|
|
493
491
|
}
|
|
@@ -6,7 +6,7 @@ import { HttpHandlerCommit, InterceptorServerWebSocketSchema } from '@/server/ty
|
|
|
6
6
|
import { importCrypto } from '@/utils/crypto';
|
|
7
7
|
import { isClientSide, isServerSide } from '@/utils/environment';
|
|
8
8
|
import { deserializeRequest, serializeResponse } from '@/utils/fetch';
|
|
9
|
-
import {
|
|
9
|
+
import { WebSocketEventMessage } from '@/webSocket/types';
|
|
10
10
|
import WebSocketClient from '@/webSocket/WebSocketClient';
|
|
11
11
|
|
|
12
12
|
import NotRunningHttpInterceptorError from '../interceptor/errors/NotRunningHttpInterceptorError';
|
|
@@ -26,24 +26,26 @@ interface HttpHandler {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
class RemoteHttpInterceptorWorker extends HttpInterceptorWorker {
|
|
29
|
-
webSocketClient: WebSocketClient<InterceptorServerWebSocketSchema>;
|
|
30
|
-
|
|
31
29
|
private httpHandlers = new Map<HttpHandler['id'], HttpHandler>();
|
|
32
30
|
|
|
31
|
+
webSocketClient: WebSocketClient<InterceptorServerWebSocketSchema>;
|
|
32
|
+
private auth?: RemoteHttpInterceptorWorkerOptions['auth'];
|
|
33
|
+
|
|
33
34
|
constructor(options: RemoteHttpInterceptorWorkerOptions) {
|
|
34
35
|
super();
|
|
35
36
|
|
|
36
|
-
const webSocketServerURL = this.deriveWebSocketServerURL(options.serverURL);
|
|
37
37
|
this.webSocketClient = new WebSocketClient({
|
|
38
|
-
url:
|
|
38
|
+
url: this.getWebSocketServerURL(options.serverURL).toString(),
|
|
39
39
|
});
|
|
40
|
+
|
|
41
|
+
this.auth = options.auth;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
get type() {
|
|
43
45
|
return 'remote' as const;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
private
|
|
48
|
+
private getWebSocketServerURL(serverURL: URL) {
|
|
47
49
|
const webSocketServerURL = new URL(serverURL);
|
|
48
50
|
webSocketServerURL.protocol = serverURL.protocol.replace(/^http(s)?:$/, 'ws$1:');
|
|
49
51
|
return webSocketServerURL;
|
|
@@ -51,7 +53,10 @@ class RemoteHttpInterceptorWorker extends HttpInterceptorWorker {
|
|
|
51
53
|
|
|
52
54
|
async start() {
|
|
53
55
|
await super.sharedStart(async () => {
|
|
54
|
-
await this.webSocketClient.start(
|
|
56
|
+
await this.webSocketClient.start({
|
|
57
|
+
parameters: this.auth ? { token: this.auth.token } : undefined,
|
|
58
|
+
waitForAuthentication: true,
|
|
59
|
+
});
|
|
55
60
|
|
|
56
61
|
this.webSocketClient.onEvent('interceptors/responses/create', this.createResponse);
|
|
57
62
|
this.webSocketClient.onEvent('interceptors/responses/unhandled', this.handleUnhandledServerRequest);
|
|
@@ -62,7 +67,7 @@ class RemoteHttpInterceptorWorker extends HttpInterceptorWorker {
|
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
private createResponse = async (
|
|
65
|
-
message:
|
|
70
|
+
message: WebSocketEventMessage<InterceptorServerWebSocketSchema, 'interceptors/responses/create'>,
|
|
66
71
|
) => {
|
|
67
72
|
const { handlerId, request: serializedRequest } = message.data;
|
|
68
73
|
|
|
@@ -87,7 +92,7 @@ class RemoteHttpInterceptorWorker extends HttpInterceptorWorker {
|
|
|
87
92
|
};
|
|
88
93
|
|
|
89
94
|
private handleUnhandledServerRequest = async (
|
|
90
|
-
message:
|
|
95
|
+
message: WebSocketEventMessage<InterceptorServerWebSocketSchema, 'interceptors/responses/unhandled'>,
|
|
91
96
|
) => {
|
|
92
97
|
const { request: serializedRequest } = message.data;
|
|
93
98
|
const request = deserializeRequest(serializedRequest);
|
|
@@ -156,7 +161,7 @@ class RemoteHttpInterceptorWorker extends HttpInterceptorWorker {
|
|
|
156
161
|
|
|
157
162
|
this.httpHandlers.set(handler.id, handler);
|
|
158
163
|
|
|
159
|
-
await this.webSocketClient.request('interceptors/workers/
|
|
164
|
+
await this.webSocketClient.request('interceptors/workers/commit', {
|
|
160
165
|
id: handler.id,
|
|
161
166
|
url: handler.url,
|
|
162
167
|
method,
|
|
@@ -171,7 +176,7 @@ class RemoteHttpInterceptorWorker extends HttpInterceptorWorker {
|
|
|
171
176
|
this.httpHandlers.clear();
|
|
172
177
|
|
|
173
178
|
if (this.webSocketClient.isRunning) {
|
|
174
|
-
await this.webSocketClient.request('interceptors/workers/
|
|
179
|
+
await this.webSocketClient.request('interceptors/workers/reset', undefined);
|
|
175
180
|
}
|
|
176
181
|
}
|
|
177
182
|
|
|
@@ -193,7 +198,7 @@ class RemoteHttpInterceptorWorker extends HttpInterceptorWorker {
|
|
|
193
198
|
method: handler.method,
|
|
194
199
|
}));
|
|
195
200
|
|
|
196
|
-
await this.webSocketClient.request('interceptors/workers/
|
|
201
|
+
await this.webSocketClient.request('interceptors/workers/reset', groupsToRecommit);
|
|
197
202
|
}
|
|
198
203
|
}
|
|
199
204
|
|
|
@@ -5,6 +5,7 @@ export interface LocalHttpInterceptorWorkerOptions {
|
|
|
5
5
|
export interface RemoteHttpInterceptorWorkerOptions {
|
|
6
6
|
type: 'remote';
|
|
7
7
|
serverURL: URL;
|
|
8
|
+
auth?: { token: string };
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export type HttpInterceptorWorkerOptions = LocalHttpInterceptorWorkerOptions | RemoteHttpInterceptorWorkerOptions;
|
|
@@ -3,7 +3,7 @@ import { Range } from '@zimic/utils/types';
|
|
|
3
3
|
import color from 'picocolors';
|
|
4
4
|
|
|
5
5
|
import { HttpInterceptorRequestSaving } from '@/http/interceptor/types/public';
|
|
6
|
-
import { stringifyValueToLog } from '@/utils/
|
|
6
|
+
import { stringifyValueToLog } from '@/utils/logging';
|
|
7
7
|
|
|
8
8
|
import { UnmatchedHttpInterceptorRequestGroup } from '../types/restrictions';
|
|
9
9
|
import TimesDeclarationPointer from './TimesDeclarationPointer';
|
|
@@ -10,8 +10,8 @@ import { removeArrayIndex } from '@/utils/arrays';
|
|
|
10
10
|
import { deserializeResponse, SerializedHttpRequest, serializeRequest } from '@/utils/fetch';
|
|
11
11
|
import { getHttpServerPort, startHttpServer, stopHttpServer } from '@/utils/http';
|
|
12
12
|
import { WebSocketMessageAbortError } from '@/utils/webSocket';
|
|
13
|
-
import {
|
|
14
|
-
import WebSocketServer from '@/webSocket/WebSocketServer';
|
|
13
|
+
import { WebSocketEventMessage } from '@/webSocket/types';
|
|
14
|
+
import WebSocketServer, { WebSocketServerAuthenticate } from '@/webSocket/WebSocketServer';
|
|
15
15
|
|
|
16
16
|
import {
|
|
17
17
|
DEFAULT_ACCESS_CONTROL_HEADERS,
|
|
@@ -24,6 +24,7 @@ import RunningInterceptorServerError from './errors/RunningInterceptorServerErro
|
|
|
24
24
|
import { InterceptorServerOptions } from './types/options';
|
|
25
25
|
import { InterceptorServer as PublicInterceptorServer } from './types/public';
|
|
26
26
|
import { HttpHandlerCommit, InterceptorServerWebSocketSchema } from './types/schema';
|
|
27
|
+
import { validateInterceptorToken } from './utils/auth';
|
|
27
28
|
import { getFetchAPI } from './utils/fetch';
|
|
28
29
|
|
|
29
30
|
interface HttpHandler {
|
|
@@ -42,6 +43,7 @@ class InterceptorServer implements PublicInterceptorServer {
|
|
|
42
43
|
_hostname: string;
|
|
43
44
|
_port: number | undefined;
|
|
44
45
|
logUnhandledRequests: boolean;
|
|
46
|
+
tokensDirectory?: string;
|
|
45
47
|
|
|
46
48
|
private httpHandlerGroups: {
|
|
47
49
|
[Method in HttpMethod]: HttpHandler[];
|
|
@@ -61,6 +63,7 @@ class InterceptorServer implements PublicInterceptorServer {
|
|
|
61
63
|
this._hostname = options.hostname ?? DEFAULT_HOSTNAME;
|
|
62
64
|
this._port = options.port;
|
|
63
65
|
this.logUnhandledRequests = options.logUnhandledRequests ?? DEFAULT_LOG_UNHANDLED_REQUESTS;
|
|
66
|
+
this.tokensDirectory = options.tokensDirectory;
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
get hostname() {
|
|
@@ -120,10 +123,48 @@ class InterceptorServer implements PublicInterceptorServer {
|
|
|
120
123
|
|
|
121
124
|
this.webSocketServer = new WebSocketServer({
|
|
122
125
|
httpServer: this.httpServer,
|
|
126
|
+
authenticate: this.authenticateWebSocketConnection,
|
|
123
127
|
});
|
|
128
|
+
|
|
124
129
|
this.startWebSocketServer();
|
|
125
130
|
}
|
|
126
131
|
|
|
132
|
+
private authenticateWebSocketConnection: WebSocketServerAuthenticate = async (_socket, request) => {
|
|
133
|
+
if (!this.tokensDirectory) {
|
|
134
|
+
return { isValid: true };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const tokenValue = this.getWebSocketRequestTokenValue(request);
|
|
138
|
+
|
|
139
|
+
if (!tokenValue) {
|
|
140
|
+
return { isValid: false, message: 'An interceptor token is required, but none was provided.' };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
await validateInterceptorToken(tokenValue, { tokensDirectory: this.tokensDirectory });
|
|
145
|
+
return { isValid: true };
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error(error);
|
|
148
|
+
return { isValid: false, message: 'The interceptor token is not valid.' };
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
private getWebSocketRequestTokenValue(request: IncomingMessage) {
|
|
153
|
+
const protocols = request.headers['sec-websocket-protocol'] ?? '';
|
|
154
|
+
const parametersAsString = decodeURIComponent(protocols).split(', ');
|
|
155
|
+
|
|
156
|
+
for (const parameterAsString of parametersAsString) {
|
|
157
|
+
const tokenValueMatch = /^token=(?<tokenValue>.+?)$/.exec(parameterAsString);
|
|
158
|
+
const tokenValue = tokenValueMatch?.groups?.tokenValue;
|
|
159
|
+
|
|
160
|
+
if (tokenValue) {
|
|
161
|
+
return tokenValue;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
127
168
|
private async startHttpServer() {
|
|
128
169
|
await startHttpServer(this.httpServerOrThrow, {
|
|
129
170
|
hostname: this.hostname,
|
|
@@ -136,22 +177,25 @@ class InterceptorServer implements PublicInterceptorServer {
|
|
|
136
177
|
|
|
137
178
|
private startWebSocketServer() {
|
|
138
179
|
this.webSocketServerOrThrow.start();
|
|
139
|
-
|
|
140
|
-
this.webSocketServerOrThrow.onEvent('interceptors/workers/
|
|
180
|
+
|
|
181
|
+
this.webSocketServerOrThrow.onEvent('interceptors/workers/commit', this.commitWorker);
|
|
182
|
+
this.webSocketServerOrThrow.onEvent('interceptors/workers/reset', this.resetWorker);
|
|
141
183
|
}
|
|
142
184
|
|
|
143
185
|
private commitWorker = (
|
|
144
|
-
message:
|
|
186
|
+
message: WebSocketEventMessage<InterceptorServerWebSocketSchema, 'interceptors/workers/commit'>,
|
|
145
187
|
socket: Socket,
|
|
146
188
|
) => {
|
|
147
189
|
const commit = message.data;
|
|
190
|
+
|
|
148
191
|
this.registerHttpHandler(commit, socket);
|
|
149
192
|
this.registerWorkerSocketIfUnknown(socket);
|
|
193
|
+
|
|
150
194
|
return {};
|
|
151
195
|
};
|
|
152
196
|
|
|
153
197
|
private resetWorker = (
|
|
154
|
-
message:
|
|
198
|
+
message: WebSocketEventMessage<InterceptorServerWebSocketSchema, 'interceptors/workers/reset'>,
|
|
155
199
|
socket: Socket,
|
|
156
200
|
) => {
|
|
157
201
|
this.removeHttpHandlersBySocket(socket);
|
|
@@ -226,8 +270,8 @@ class InterceptorServer implements PublicInterceptorServer {
|
|
|
226
270
|
}
|
|
227
271
|
|
|
228
272
|
private async stopWebSocketServer() {
|
|
229
|
-
this.webSocketServerOrThrow.offEvent('interceptors/workers/
|
|
230
|
-
this.webSocketServerOrThrow.offEvent('interceptors/workers/
|
|
273
|
+
this.webSocketServerOrThrow.offEvent('interceptors/workers/commit', this.commitWorker);
|
|
274
|
+
this.webSocketServerOrThrow.offEvent('interceptors/workers/reset', this.resetWorker);
|
|
231
275
|
|
|
232
276
|
await this.webSocketServerOrThrow.stop();
|
|
233
277
|
|
package/src/server/constants.ts
CHANGED
|
@@ -22,7 +22,7 @@ export const DEFAULT_ACCESS_CONTROL_HEADERS = Object.freeze({
|
|
|
22
22
|
'access-control-allow-methods': ALLOWED_ACCESS_CONTROL_HTTP_METHODS,
|
|
23
23
|
'access-control-allow-headers': '*',
|
|
24
24
|
'access-control-expose-headers': '*',
|
|
25
|
-
'access-control-max-age': process.env.
|
|
25
|
+
'access-control-max-age': process.env.INTERCEPTOR_SERVER_ACCESS_CONTROL_MAX_AGE,
|
|
26
26
|
}) satisfies AccessControlHeaders;
|
|
27
27
|
|
|
28
28
|
/** The default status code for the preflight request. */
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error thrown when an interceptor token is invalid.
|
|
3
|
+
*
|
|
4
|
+
* @see {@link https://github.com/zimicjs/zimic/wiki/cli‐zimic‐interceptor‐server#authentication Interceptor server authentication}
|
|
5
|
+
*/
|
|
6
|
+
class InvalidInterceptorTokenError extends Error {
|
|
7
|
+
constructor(tokenId: string) {
|
|
8
|
+
super(`Invalid interceptor token: ${tokenId}`);
|
|
9
|
+
this.name = 'InvalidInterceptorTokenError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default InvalidInterceptorTokenError;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error thrown when an interceptor token file is invalid.
|
|
3
|
+
*
|
|
4
|
+
* @see {@link https://github.com/zimicjs/zimic/wiki/cli‐zimic‐interceptor‐server#authentication Interceptor server authentication}
|
|
5
|
+
*/
|
|
6
|
+
class InvalidInterceptorTokenFileError extends Error {
|
|
7
|
+
constructor(tokenFilePath: string, validationErrorMessage: string) {
|
|
8
|
+
super(`Invalid interceptor token file ${tokenFilePath}: ${validationErrorMessage}`);
|
|
9
|
+
this.name = 'InvalidInterceptorTokenFileError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default InvalidInterceptorTokenFileError;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error thrown when an interceptor token value is invalid.
|
|
3
|
+
*
|
|
4
|
+
* @see {@link https://github.com/zimicjs/zimic/wiki/cli‐zimic‐interceptor‐server#authentication Interceptor server authentication}
|
|
5
|
+
*/
|
|
6
|
+
class InvalidInterceptorTokenValueError extends Error {
|
|
7
|
+
constructor(tokenValue: string) {
|
|
8
|
+
super(`Invalid interceptor token value: ${tokenValue}`);
|
|
9
|
+
this.name = 'InvalidInterceptorTokenValueError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default InvalidInterceptorTokenValueError;
|
|
@@ -21,4 +21,13 @@ export interface InterceptorServerOptions {
|
|
|
21
21
|
* @default true
|
|
22
22
|
*/
|
|
23
23
|
logUnhandledRequests?: boolean;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The directory where the authorized interceptor authentication tokens are saved. If provided, only remote
|
|
27
|
+
* interceptors bearing a valid token will be accepted. This option is essential if you are exposing your interceptor
|
|
28
|
+
* server publicly. For local development and testing, though, `--tokens-dir` is optional.
|
|
29
|
+
*
|
|
30
|
+
* @default undefined
|
|
31
|
+
*/
|
|
32
|
+
tokensDirectory?: string;
|
|
24
33
|
}
|