@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.
Files changed (42) hide show
  1. package/changelog.md +18 -0
  2. package/dist/gateway-ent.cjs +25 -2
  3. package/dist/gateway-ent.cjs.map +2 -2
  4. package/dist/gateway-ent.js +25 -2
  5. package/dist/gateway-ent.js.map +2 -2
  6. package/dist/index.cjs +1182 -109
  7. package/dist/index.cjs.map +4 -4
  8. package/dist/index.js +1182 -109
  9. package/dist/index.js.map +4 -4
  10. package/dist/metrics/publisher/rest.cjs +60 -0
  11. package/dist/metrics/publisher/rest.cjs.map +7 -0
  12. package/dist/metrics/publisher/rest.js +23 -0
  13. package/dist/metrics/publisher/rest.js.map +7 -0
  14. package/package.json +12 -12
  15. package/types/gateway-ent.d.ts +2 -1
  16. package/dist/metrics-rest.cjs +0 -21440
  17. package/dist/metrics-rest.cjs.map +0 -7
  18. package/dist/metrics-rest.js +0 -21430
  19. package/dist/metrics-rest.js.map +0 -7
  20. package/src/common/compose.ts +0 -40
  21. package/src/gateway/ent/config.ts +0 -174
  22. package/src/gateway/ent/index.ts +0 -18
  23. package/src/gateway/ent/logging.ts +0 -89
  24. package/src/gateway/ent/server.ts +0 -34
  25. package/src/gateway/metrics/rest.ts +0 -20
  26. package/src/gateway/ws/core.ts +0 -90
  27. package/src/index.ts +0 -3
  28. package/src/logger.ts +0 -6
  29. package/src/mesh/connections.ts +0 -101
  30. package/src/mesh/rest-directory/routes.ts +0 -38
  31. package/src/mesh/ws/broker/core.ts +0 -163
  32. package/src/mesh/ws/cluster/core.ts +0 -107
  33. package/src/mesh/ws/relays/core.ts +0 -159
  34. package/src/metrics/routes.ts +0 -86
  35. package/src/server/address.ts +0 -47
  36. package/src/server/cors.ts +0 -311
  37. package/src/server/exchange.ts +0 -379
  38. package/src/server/monitoring.ts +0 -167
  39. package/src/server/types.ts +0 -69
  40. package/src/server/ws-client-verify.ts +0 -79
  41. package/src/server.ts +0 -316
  42. package/src/utils.ts +0 -10
@@ -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
- }
@@ -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
- }