@interopio/gateway-server 0.4.0-beta → 0.5.0-beta
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/changelog.md +18 -0
- package/dist/gateway-ent.cjs +25 -2
- package/dist/gateway-ent.cjs.map +2 -2
- package/dist/gateway-ent.js +25 -2
- package/dist/gateway-ent.js.map +2 -2
- package/dist/index.cjs +1182 -109
- package/dist/index.cjs.map +4 -4
- package/dist/index.js +1182 -109
- package/dist/index.js.map +4 -4
- package/dist/metrics/publisher/rest.cjs +60 -0
- package/dist/metrics/publisher/rest.cjs.map +7 -0
- package/dist/metrics/publisher/rest.js +23 -0
- package/dist/metrics/publisher/rest.js.map +7 -0
- package/package.json +12 -12
- package/types/gateway-ent.d.ts +2 -1
- package/dist/metrics-rest.cjs +0 -21440
- package/dist/metrics-rest.cjs.map +0 -7
- package/dist/metrics-rest.js +0 -21430
- package/dist/metrics-rest.js.map +0 -7
- package/src/common/compose.ts +0 -40
- package/src/gateway/ent/config.ts +0 -174
- package/src/gateway/ent/index.ts +0 -18
- package/src/gateway/ent/logging.ts +0 -89
- package/src/gateway/ent/server.ts +0 -34
- package/src/gateway/metrics/rest.ts +0 -20
- package/src/gateway/ws/core.ts +0 -90
- package/src/index.ts +0 -3
- package/src/logger.ts +0 -6
- package/src/mesh/connections.ts +0 -101
- package/src/mesh/rest-directory/routes.ts +0 -38
- package/src/mesh/ws/broker/core.ts +0 -163
- package/src/mesh/ws/cluster/core.ts +0 -107
- package/src/mesh/ws/relays/core.ts +0 -159
- package/src/metrics/routes.ts +0 -86
- package/src/server/address.ts +0 -47
- package/src/server/cors.ts +0 -311
- package/src/server/exchange.ts +0 -379
- package/src/server/monitoring.ts +0 -167
- package/src/server/types.ts +0 -69
- package/src/server/ws-client-verify.ts +0 -79
- package/src/server.ts +0 -316
- package/src/utils.ts +0 -10
package/src/server/cors.ts
DELETED
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
WebExchange,
|
|
3
|
-
HttpRequest,
|
|
4
|
-
ReadonlyHttpHeaders,
|
|
5
|
-
ServerHttpResponse, ServerHttpRequest
|
|
6
|
-
} from './types.ts';
|
|
7
|
-
import getLogger from '../logger.js';
|
|
8
|
-
import {IOGateway} from '@interopio/gateway';
|
|
9
|
-
import {HttpServerResponse} from './exchange.ts';
|
|
10
|
-
|
|
11
|
-
function isSameOrigin(request: HttpRequest<ReadonlyHttpHeaders>) {
|
|
12
|
-
const origin = request.headers.one('origin');
|
|
13
|
-
if (origin === undefined) {
|
|
14
|
-
return true;
|
|
15
|
-
}
|
|
16
|
-
const url = request.URL;
|
|
17
|
-
const actualProtocol = url.protocol;
|
|
18
|
-
const actualHost = url.host;
|
|
19
|
-
|
|
20
|
-
const originUrl = new URL(origin);
|
|
21
|
-
|
|
22
|
-
const originHost = originUrl.host;
|
|
23
|
-
const originProtocol = originUrl.protocol;
|
|
24
|
-
return actualProtocol === originProtocol
|
|
25
|
-
&& actualHost === originHost;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Returns `true` if the request is a valid CORS one by checking `Origin` header presence and ensuring origins differ.
|
|
30
|
-
*/
|
|
31
|
-
export function isCorsRequest(request: HttpRequest<ReadonlyHttpHeaders>): boolean {
|
|
32
|
-
return request.headers.has('origin') && !isSameOrigin(request);
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function isPreFlightRequest(request: HttpRequest): boolean {
|
|
37
|
-
return request.method === 'OPTIONS'
|
|
38
|
-
&& request.headers.has('origin')
|
|
39
|
-
&& request.headers.has('access-control-request-method');
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const VARY_HEADERS: readonly string[] = ['Origin', 'Access-Control-Request-Method', 'Access-Control-Request-Headers'];
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Processes a request given a {@link CorsConfig}.
|
|
46
|
-
*
|
|
47
|
-
* @param exchange the current exchange
|
|
48
|
-
* @param config the CORS configuration to use, possibly `undefined` in which case pre-flight requests are rejected, but all others allowed
|
|
49
|
-
* @returns `false` if the request is rejected, `true` otherwise
|
|
50
|
-
*/
|
|
51
|
-
export function processRequest(exchange: WebExchange, config?: CorsConfig): boolean {
|
|
52
|
-
const {request, response} = exchange;
|
|
53
|
-
const responseHeaders = response.headers;
|
|
54
|
-
|
|
55
|
-
if (!responseHeaders.has('Vary')) {
|
|
56
|
-
responseHeaders.set('Vary', VARY_HEADERS.join(', '));
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
const varyHeaders = responseHeaders.list('Vary');
|
|
60
|
-
for (const header of VARY_HEADERS) {
|
|
61
|
-
if (!varyHeaders.find(h => h === header)) {
|
|
62
|
-
varyHeaders.push(header);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
responseHeaders.set('Vary', varyHeaders.join(', '));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
if (!isCorsRequest(request)) {
|
|
70
|
-
return true;
|
|
71
|
-
}
|
|
72
|
-
} catch (e) {
|
|
73
|
-
if(logger.enabledFor('debug')) {
|
|
74
|
-
logger.debug(`reject: origin is malformed`);
|
|
75
|
-
}
|
|
76
|
-
rejectRequest(response);
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (responseHeaders.has('access-control-allow-origin')) {
|
|
81
|
-
logger.trace(`skip: already contains "Access-Control-Allow-Origin"`);
|
|
82
|
-
return true;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const preFlightRequest = isPreFlightRequest(request);
|
|
86
|
-
|
|
87
|
-
if (config) {
|
|
88
|
-
return handleInternal(exchange, config, preFlightRequest);
|
|
89
|
-
}
|
|
90
|
-
if (preFlightRequest) {
|
|
91
|
-
rejectRequest(response);
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
return true;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export type CorsConfig = {
|
|
98
|
-
origins?: {
|
|
99
|
-
allow?: '*' | IOGateway.Filtering.Matcher[]
|
|
100
|
-
}
|
|
101
|
-
methods?: {
|
|
102
|
-
allow?: '*' | string[]
|
|
103
|
-
}
|
|
104
|
-
headers?: {
|
|
105
|
-
allow?: '*' | string[]
|
|
106
|
-
expose?: string[]
|
|
107
|
-
}
|
|
108
|
-
credentials?: {
|
|
109
|
-
allow?: boolean
|
|
110
|
-
},
|
|
111
|
-
privateNetwork?: {
|
|
112
|
-
allow?: boolean
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export /*testing*/ function validateConfig(config?: CorsConfig): CorsConfig | undefined {
|
|
117
|
-
if (config) {
|
|
118
|
-
|
|
119
|
-
const headers = config.headers;
|
|
120
|
-
if (headers?.allow && headers.allow !== ALL) {
|
|
121
|
-
headers.allow = headers.allow.map(header => header.toLowerCase());
|
|
122
|
-
}
|
|
123
|
-
const origins = config.origins;
|
|
124
|
-
if (origins?.allow && origins.allow !== ALL) {
|
|
125
|
-
origins.allow = origins.allow.map(origin => {
|
|
126
|
-
if (typeof origin === 'string') {
|
|
127
|
-
// exact match
|
|
128
|
-
return origin.toLowerCase();
|
|
129
|
-
}
|
|
130
|
-
return origin;
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
return config;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const handler = (config?: CorsConfig) => {
|
|
138
|
-
validateConfig(config);
|
|
139
|
-
return async (ctx: WebExchange<ServerHttpRequest, HttpServerResponse>, next: () => Promise<void>) => {
|
|
140
|
-
const isValid = processRequest(ctx, config);
|
|
141
|
-
if (!isValid || isPreFlightRequest(ctx.request)) {
|
|
142
|
-
// do we need to call end?
|
|
143
|
-
ctx.response._res.end();
|
|
144
|
-
} else {
|
|
145
|
-
await next();
|
|
146
|
-
}
|
|
147
|
-
};
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
export default (config?: CorsConfig) => [handler(config)];
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const logger = getLogger('cors');
|
|
154
|
-
|
|
155
|
-
function rejectRequest(response: ServerHttpResponse) {
|
|
156
|
-
response.statusCode = 403;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function handleInternal(exchange: WebExchange,
|
|
160
|
-
config: CorsConfig, preFlightRequest: boolean): boolean {
|
|
161
|
-
const {request, response} = exchange;
|
|
162
|
-
const responseHeaders = response.headers;
|
|
163
|
-
|
|
164
|
-
const requestOrigin = request.headers.one('origin');
|
|
165
|
-
const allowOrigin = checkOrigin(config, requestOrigin);
|
|
166
|
-
|
|
167
|
-
if (allowOrigin === undefined) {
|
|
168
|
-
if (logger.enabledFor('debug')) {
|
|
169
|
-
logger.debug(`reject: '${requestOrigin}' origin is not allowed`);
|
|
170
|
-
}
|
|
171
|
-
rejectRequest(response);
|
|
172
|
-
return false;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const requestMethod = getMethodToUse(request, preFlightRequest);
|
|
176
|
-
const allowMethods = checkMethods(config, requestMethod);
|
|
177
|
-
if (allowMethods === undefined) {
|
|
178
|
-
if (logger.enabledFor('debug')) {
|
|
179
|
-
logger.debug(`reject: HTTP '${requestMethod}' is not allowed`);
|
|
180
|
-
}
|
|
181
|
-
rejectRequest(response);
|
|
182
|
-
return false;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const requestHeaders = getHeadersToUse(request, preFlightRequest);
|
|
186
|
-
const allowHeaders = checkHeaders(config, requestHeaders);
|
|
187
|
-
if (preFlightRequest && allowHeaders === undefined) {
|
|
188
|
-
if (logger.enabledFor('debug')) {
|
|
189
|
-
logger.debug(`reject: headers '${requestHeaders}' are not allowed`);
|
|
190
|
-
}
|
|
191
|
-
rejectRequest(response);
|
|
192
|
-
return false;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
responseHeaders.set('access-control-allow-origin', allowOrigin);
|
|
196
|
-
|
|
197
|
-
if (preFlightRequest) {
|
|
198
|
-
responseHeaders.set('access-control-allow-methods', allowMethods.join(','));
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
if (preFlightRequest && allowHeaders !== undefined && allowHeaders.length > 0) {
|
|
202
|
-
responseHeaders.set('access-control-allow-headers', allowHeaders.join(', '));
|
|
203
|
-
}
|
|
204
|
-
const exposeHeaders = config.headers?.expose;
|
|
205
|
-
if (exposeHeaders && exposeHeaders.length > 0) {
|
|
206
|
-
responseHeaders.set('access-control-expose-headers', exposeHeaders.join(', '));
|
|
207
|
-
}
|
|
208
|
-
if (config.credentials?.allow) {
|
|
209
|
-
responseHeaders.set('access-control-allow-credentials', 'true');
|
|
210
|
-
}
|
|
211
|
-
if (config.privateNetwork?.allow && request.headers.one('access-control-request-private-network') === 'true') {
|
|
212
|
-
responseHeaders.set('access-control-allow-private-network', 'true');
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// no max-age support :)
|
|
216
|
-
return true;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const ALL = '*';
|
|
220
|
-
const DEFAULT_METHODS = ['GET', 'HEAD'];
|
|
221
|
-
|
|
222
|
-
function validateAllowCredentials(config: CorsConfig) {
|
|
223
|
-
if (config.credentials?.allow === true && config.origins?.allow === ALL) {
|
|
224
|
-
throw new Error(`when credentials.allow is true origins.allow cannot be "*"`);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function validateAllowPrivateNetwork(config: CorsConfig) {
|
|
229
|
-
if (config.privateNetwork?.allow === true && config.origins?.allow === ALL) {
|
|
230
|
-
throw new Error(`when privateNetwork.allow is true origins.allow cannot be "*"`);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function checkOrigin(config: CorsConfig, origin? :string): string | undefined {
|
|
235
|
-
if (origin) {
|
|
236
|
-
const allowedOrigins = config.origins?.allow;
|
|
237
|
-
if (allowedOrigins) {
|
|
238
|
-
if (allowedOrigins === ALL) {
|
|
239
|
-
validateAllowCredentials(config);
|
|
240
|
-
validateAllowPrivateNetwork(config);
|
|
241
|
-
return ALL;
|
|
242
|
-
}
|
|
243
|
-
const originToCheck = trimTrailingSlash(origin.toLowerCase());
|
|
244
|
-
|
|
245
|
-
for (const allowedOrigin of allowedOrigins) {
|
|
246
|
-
if ((allowedOrigin === ALL) || IOGateway.Filtering.valueMatches(allowedOrigin, originToCheck)) {
|
|
247
|
-
return origin;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function checkMethods(config: CorsConfig, requestMethod?: string): string[] | undefined {
|
|
255
|
-
if (requestMethod) {
|
|
256
|
-
const allowedMethods = config.methods?.allow ?? DEFAULT_METHODS;
|
|
257
|
-
if (allowedMethods === ALL) {
|
|
258
|
-
return [requestMethod];
|
|
259
|
-
}
|
|
260
|
-
if (IOGateway.Filtering.valuesMatch(allowedMethods, requestMethod)) {
|
|
261
|
-
return allowedMethods;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function checkHeaders(config: CorsConfig, requestHeaders?: string[]): string[] | undefined {
|
|
267
|
-
if (requestHeaders === undefined) {
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
if (requestHeaders.length == 0) {
|
|
271
|
-
return [];
|
|
272
|
-
}
|
|
273
|
-
const allowedHeaders = config.headers?.allow;
|
|
274
|
-
if (allowedHeaders === undefined) {
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
const allowAnyHeader = allowedHeaders === ALL;
|
|
278
|
-
const result: string[] = [];
|
|
279
|
-
for (const requestHeader of requestHeaders) {
|
|
280
|
-
const value = requestHeader?.trim();
|
|
281
|
-
if (value) {
|
|
282
|
-
if (allowAnyHeader) {
|
|
283
|
-
result.push(value);
|
|
284
|
-
}
|
|
285
|
-
else {
|
|
286
|
-
for (const allowedHeader of allowedHeaders) {
|
|
287
|
-
if (value.toLowerCase() == allowedHeader) {
|
|
288
|
-
result.push(value);
|
|
289
|
-
break;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
if (result.length > 0) {
|
|
296
|
-
return result;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function trimTrailingSlash(origin: string): string {
|
|
301
|
-
return origin.endsWith('/') ? origin.slice(0, -1) : origin;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function getMethodToUse(request: HttpRequest<ReadonlyHttpHeaders>, isPreFlight: boolean): string | undefined {
|
|
305
|
-
return (isPreFlight ? request.headers.one('access-control-request-method') : request.method);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function getHeadersToUse(request: HttpRequest, isPreFlight: boolean): string[] {
|
|
309
|
-
const headers = request.headers;
|
|
310
|
-
return (isPreFlight ? headers.list('access-control-request-headers') : Array.from(headers.keys()));
|
|
311
|
-
}
|
package/src/server/exchange.ts
DELETED
|
@@ -1,379 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
HeaderValue,
|
|
3
|
-
HeaderValues,
|
|
4
|
-
HttpRequest, HttpResponse,
|
|
5
|
-
MutableHttpHeaders,
|
|
6
|
-
ReadonlyHttpHeaders,
|
|
7
|
-
ServerHttpRequest,
|
|
8
|
-
ServerHttpResponse,
|
|
9
|
-
WebExchange
|
|
10
|
-
} from './types.js';
|
|
11
|
-
import http from 'node:http';
|
|
12
|
-
import http2 from 'node:http2';
|
|
13
|
-
|
|
14
|
-
function requestToProtocol(request: ServerHttpRequest, defaultProtocol: string): string {
|
|
15
|
-
let proto = request.headers.get('x-forwarded-proto');
|
|
16
|
-
|
|
17
|
-
if (Array.isArray(proto)) {
|
|
18
|
-
proto = proto[0];
|
|
19
|
-
}
|
|
20
|
-
if (proto !== undefined) {
|
|
21
|
-
return (proto as string).split(',', 1)[0].trim();
|
|
22
|
-
}
|
|
23
|
-
return defaultProtocol;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function requestToHost(request: ServerHttpRequest, defaultHost?: string): string | undefined {
|
|
27
|
-
let host = request.headers.get('x-forwarded-for');
|
|
28
|
-
if (host === undefined) {
|
|
29
|
-
host = request.headers.get('x-forwarded-host');
|
|
30
|
-
if (Array.isArray(host)) {
|
|
31
|
-
host = host[0];
|
|
32
|
-
}
|
|
33
|
-
if (host) {
|
|
34
|
-
const port = request.headers.one('x-forwarded-port');
|
|
35
|
-
if (port) {
|
|
36
|
-
host = `${host}:${port}`;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
if (host === undefined) {
|
|
40
|
-
host = request.headers.one('host');
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
if (Array.isArray(host)) {
|
|
44
|
-
host = host[0];
|
|
45
|
-
}
|
|
46
|
-
if (host) {
|
|
47
|
-
return (host as string).split(',', 1)[0].trim();
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return defaultHost;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export class HttpServerRequest implements ServerHttpRequest {
|
|
56
|
-
private _body: Promise<Blob> | undefined;
|
|
57
|
-
private _url: URL | undefined;
|
|
58
|
-
private readonly _headers: ReadonlyHttpHeaders;
|
|
59
|
-
constructor(readonly _req: http.IncomingMessage) {
|
|
60
|
-
this._headers = new IncomingMessageHeaders(_req);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
get http2(): boolean {
|
|
64
|
-
return this._req.httpVersionMajor >= 2;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
get headers() {
|
|
68
|
-
return this._headers;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
get path() {
|
|
72
|
-
return this.URL?.pathname;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
get URL() {
|
|
76
|
-
this._url ??= new URL(this._req.url!, `${this.protocol}://${this.host}`);
|
|
77
|
-
return this._url;
|
|
78
|
-
}
|
|
79
|
-
get query() {
|
|
80
|
-
return this.URL?.search;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
get method() {
|
|
84
|
-
return this._req.method;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
get host() {
|
|
88
|
-
let dh: string | undefined = undefined;
|
|
89
|
-
if (this._req.httpVersionMajor >= 2) {
|
|
90
|
-
dh = (this._req?.headers as http2.IncomingHttpHeaders)[':authority'];
|
|
91
|
-
}
|
|
92
|
-
if (dh === undefined) {
|
|
93
|
-
dh = this._req?.socket.remoteAddress;
|
|
94
|
-
}
|
|
95
|
-
return requestToHost(this, dh);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
get protocol() {
|
|
99
|
-
let dp: string | undefined = undefined;
|
|
100
|
-
if (this._req.httpVersionMajor > 2) {
|
|
101
|
-
dp = (this._req.headers as http2.IncomingHttpHeaders)[':scheme'];
|
|
102
|
-
}
|
|
103
|
-
if (dp === undefined) {
|
|
104
|
-
dp = this._req?.socket['encrypted'] ? 'https' : 'http'
|
|
105
|
-
}
|
|
106
|
-
return requestToProtocol(this, dp);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
get socket() {
|
|
110
|
-
return this._req.socket;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
get body() {
|
|
114
|
-
this._body ??= new Promise((resolve, reject) => {
|
|
115
|
-
const chunks: Uint8Array[] = [];
|
|
116
|
-
this._req
|
|
117
|
-
.on('error', (err: Error) => reject(err))
|
|
118
|
-
.on('data', (chunk) => chunks.push(chunk))
|
|
119
|
-
.on('end', () => {
|
|
120
|
-
resolve(new Blob(chunks));
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
return this._body;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
get text() {
|
|
127
|
-
return this.body.then(async (blob) => await blob.text());
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
get json() {
|
|
131
|
-
return this.body.then(async (blob) => {
|
|
132
|
-
const json = JSON.parse(await blob.text());
|
|
133
|
-
return json;
|
|
134
|
-
// try {
|
|
135
|
-
// } catch (e) {
|
|
136
|
-
// reject(e instanceof Error ? e : new Error(`parse failed :${e}`));
|
|
137
|
-
// }
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
class IncomingMessageHeaders implements ReadonlyHttpHeaders {
|
|
143
|
-
constructor(private readonly _msg: http.IncomingMessage) {
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
has(name: string): boolean {
|
|
147
|
-
return this._msg.headers[name] !== undefined;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
get(name: string): string | (readonly string[]) | undefined {
|
|
151
|
-
return this._msg.headers[name];
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
list(name: string): string[] {
|
|
155
|
-
return toList(this._msg.headers[name]);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
one(name: string): string | undefined {
|
|
159
|
-
const value = this._msg.headers[name];
|
|
160
|
-
if (Array.isArray(value)) {
|
|
161
|
-
return value[0];
|
|
162
|
-
}
|
|
163
|
-
return value;
|
|
164
|
-
}
|
|
165
|
-
keys() {
|
|
166
|
-
return Object.keys(this._msg.headers).values();
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
class OutgoingMessageHeaders implements MutableHttpHeaders {
|
|
171
|
-
constructor(private readonly _msg: http.OutgoingMessage) {
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
has(name: string): boolean {
|
|
175
|
-
return this._msg.hasHeader(name);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
keys() {
|
|
179
|
-
return this._msg.getHeaderNames().values();
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
get(name: string): HeaderValues {
|
|
183
|
-
return this._msg.getHeader(name);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
one(name: string): HeaderValue {
|
|
187
|
-
const value = this._msg.getHeader(name);
|
|
188
|
-
if (Array.isArray(value)) {
|
|
189
|
-
return value[0];
|
|
190
|
-
}
|
|
191
|
-
return value;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
set(name: string, value: HeaderValues): this {
|
|
195
|
-
if (!this._msg.headersSent) {
|
|
196
|
-
if (Array.isArray(value)) {
|
|
197
|
-
value = value.map(v => typeof v === 'number' ? String(v) : v);
|
|
198
|
-
} else if (typeof value === 'number') {
|
|
199
|
-
value = String(value);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (value) {
|
|
203
|
-
this._msg.setHeader(name, value);
|
|
204
|
-
}
|
|
205
|
-
else {
|
|
206
|
-
this._msg.removeHeader(name);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
return this;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
add(name: string, value: string | (readonly string[])): this {
|
|
213
|
-
if (!this._msg.headersSent) {
|
|
214
|
-
this._msg.appendHeader(name, value);
|
|
215
|
-
}
|
|
216
|
-
return this;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
list(name: string): string[] {
|
|
220
|
-
const values = this.get(name);
|
|
221
|
-
return toList(values);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
export class HttpServerResponse implements ServerHttpResponse {
|
|
226
|
-
private readonly _headers: MutableHttpHeaders;
|
|
227
|
-
constructor(readonly _res: http.ServerResponse) {
|
|
228
|
-
this._headers = new OutgoingMessageHeaders(_res);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
get statusCode(): number {
|
|
232
|
-
return this._res.statusCode;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
set statusCode(value: number) {
|
|
236
|
-
if (this._res.headersSent) {
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
this._res.statusCode = value;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
get headers() {
|
|
243
|
-
return this._headers;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
export class DefaultWebExchange<Request extends ServerHttpRequest, Response extends ServerHttpResponse> extends WebExchange<Request, Response> {
|
|
248
|
-
constructor(readonly request: Request, readonly response: Response) {
|
|
249
|
-
super();
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function toList(values: number | string | (readonly string[]) | undefined): string[] {
|
|
254
|
-
if (typeof values === 'string') {
|
|
255
|
-
values = [values];
|
|
256
|
-
}
|
|
257
|
-
if (typeof values === 'number') {
|
|
258
|
-
values = [String(values)];
|
|
259
|
-
}
|
|
260
|
-
const list: string[] = [];
|
|
261
|
-
if (values) {
|
|
262
|
-
for (const value of values) {
|
|
263
|
-
if (value) {
|
|
264
|
-
list.push(...parseHeader(value));
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
return list;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function parseHeader(value: string): string[] {
|
|
272
|
-
const list: string[] = [];
|
|
273
|
-
{
|
|
274
|
-
let start = 0;
|
|
275
|
-
let end = 0;
|
|
276
|
-
|
|
277
|
-
for (let i = 0; i < value.length; i++) {
|
|
278
|
-
switch (value.charCodeAt(i)) {
|
|
279
|
-
case 0x20: // space
|
|
280
|
-
if (start === end) {
|
|
281
|
-
start = end = i + 1;
|
|
282
|
-
}
|
|
283
|
-
break;
|
|
284
|
-
case 0x2c: // comma
|
|
285
|
-
list.push(value.slice(start, end));
|
|
286
|
-
start = end = i + 1;
|
|
287
|
-
break;
|
|
288
|
-
default:
|
|
289
|
-
end = end + 1;
|
|
290
|
-
break;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
list.push(value.slice(start, end));
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return list;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
export class MapHttpHeaders extends Map<string, (readonly string[])> implements MutableHttpHeaders {
|
|
300
|
-
|
|
301
|
-
get(name: string) {
|
|
302
|
-
return super.get(name.toLowerCase());
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
one(name: string): string | undefined {
|
|
306
|
-
return this.get(name)?.[0];
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
list(name: string) {
|
|
310
|
-
const values = super.get(name.toLowerCase());
|
|
311
|
-
return toList(values);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
set(name: string, value: number | string | (readonly string[]) | undefined): this {
|
|
315
|
-
if (typeof value === 'number') {
|
|
316
|
-
value = String(value);
|
|
317
|
-
}
|
|
318
|
-
if (typeof value === 'string') {
|
|
319
|
-
value = [value];
|
|
320
|
-
}
|
|
321
|
-
if (value) {
|
|
322
|
-
return super.set(name.toLowerCase(), value);
|
|
323
|
-
}
|
|
324
|
-
else {
|
|
325
|
-
super.delete(name.toLowerCase());
|
|
326
|
-
return this;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
add(name: string, value: string | (readonly string[])) {
|
|
331
|
-
const prev = super.get(name.toLowerCase());
|
|
332
|
-
if (typeof value === 'string') {
|
|
333
|
-
value = [value];
|
|
334
|
-
}
|
|
335
|
-
if (prev) {
|
|
336
|
-
value = prev.concat(value);
|
|
337
|
-
}
|
|
338
|
-
this.set(name, value);
|
|
339
|
-
return this;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
export class MockHttpRequest implements HttpRequest<MutableHttpHeaders> {
|
|
344
|
-
private _body?: Blob;
|
|
345
|
-
readonly headers = new MapHttpHeaders();
|
|
346
|
-
|
|
347
|
-
constructor(private readonly url: URL, method?: string) {
|
|
348
|
-
this.method = method ?? 'GET';
|
|
349
|
-
this.headers.set('Host', url.hostname);
|
|
350
|
-
this.path = url?.pathname ?? '/';
|
|
351
|
-
}
|
|
352
|
-
method: string;
|
|
353
|
-
path: string;
|
|
354
|
-
|
|
355
|
-
get host() {
|
|
356
|
-
return requestToHost(this, this.url.host);
|
|
357
|
-
}
|
|
358
|
-
get protocol() {
|
|
359
|
-
return requestToProtocol(this, this.url.protocol.slice(0, -1));
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
get body(): Promise<Blob> {
|
|
363
|
-
const body = this._body;
|
|
364
|
-
return body ? Promise.resolve(body): Promise.reject(new Error(`no body set`));
|
|
365
|
-
}
|
|
366
|
-
set body(value: Blob) {
|
|
367
|
-
this._body = value;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
get URL() {
|
|
371
|
-
return new URL(this.path, `${this.protocol}://${this.host}`);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
export class MockHttpResponse implements HttpResponse<MutableHttpHeaders> {
|
|
376
|
-
statusCode!: number;
|
|
377
|
-
headers = new MapHttpHeaders();
|
|
378
|
-
|
|
379
|
-
}
|