@interopio/gateway-server 0.4.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 (40) hide show
  1. package/changelog.md +94 -0
  2. package/dist/gateway-ent.cjs +305 -0
  3. package/dist/gateway-ent.cjs.map +7 -0
  4. package/dist/gateway-ent.js +277 -0
  5. package/dist/gateway-ent.js.map +7 -0
  6. package/dist/index.cjs +1713 -0
  7. package/dist/index.cjs.map +7 -0
  8. package/dist/index.js +1682 -0
  9. package/dist/index.js.map +7 -0
  10. package/dist/metrics-rest.cjs +21440 -0
  11. package/dist/metrics-rest.cjs.map +7 -0
  12. package/dist/metrics-rest.js +21430 -0
  13. package/dist/metrics-rest.js.map +7 -0
  14. package/gateway-server.d.ts +69 -0
  15. package/package.json +66 -0
  16. package/readme.md +9 -0
  17. package/src/common/compose.ts +40 -0
  18. package/src/gateway/ent/config.ts +174 -0
  19. package/src/gateway/ent/index.ts +18 -0
  20. package/src/gateway/ent/logging.ts +89 -0
  21. package/src/gateway/ent/server.ts +34 -0
  22. package/src/gateway/metrics/rest.ts +20 -0
  23. package/src/gateway/ws/core.ts +90 -0
  24. package/src/index.ts +3 -0
  25. package/src/logger.ts +6 -0
  26. package/src/mesh/connections.ts +101 -0
  27. package/src/mesh/rest-directory/routes.ts +38 -0
  28. package/src/mesh/ws/broker/core.ts +163 -0
  29. package/src/mesh/ws/cluster/core.ts +107 -0
  30. package/src/mesh/ws/relays/core.ts +159 -0
  31. package/src/metrics/routes.ts +86 -0
  32. package/src/server/address.ts +47 -0
  33. package/src/server/cors.ts +311 -0
  34. package/src/server/exchange.ts +379 -0
  35. package/src/server/monitoring.ts +167 -0
  36. package/src/server/types.ts +69 -0
  37. package/src/server/ws-client-verify.ts +79 -0
  38. package/src/server.ts +316 -0
  39. package/src/utils.ts +10 -0
  40. package/types/gateway-ent.d.ts +212 -0
@@ -0,0 +1,47 @@
1
+ import {type NetworkInterfaceInfo, networkInterfaces} from 'node:os';
2
+
3
+ const PORT_RANGE_MATCHER = /^(\d+|(0x[\da-f]+))(-(\d+|(0x[\da-f]+)))?$/i;
4
+ function validPort(port: number) {
5
+ if (port > 0xFFFF) throw new Error(`bad port ${port}`);
6
+ return port;
7
+ }
8
+
9
+ /**
10
+ * parse port range. port can be number or string representing comma separated
11
+ * list of port ranges for e.g. "3434,8380-8385"
12
+ * @param port
13
+ */
14
+ export function* portRange(port: number | string): Generator<number> {
15
+ if (typeof port === 'string') {
16
+ for (const portRange of port.split(',')) {
17
+ const trimmed = portRange.trim();
18
+ const matchResult = PORT_RANGE_MATCHER.exec(trimmed);
19
+ if (matchResult) {
20
+ const start = parseInt(matchResult[1]);
21
+ const end = parseInt(matchResult[4] ?? matchResult[1]);
22
+ for (let i = validPort(start); i < validPort(end) + 1; i++) {
23
+ yield i;
24
+ }
25
+ }
26
+ else {
27
+ throw new Error(`'${portRange}' is not a valid port or range.`);
28
+ }
29
+ }
30
+ } else {
31
+ yield validPort(port);
32
+ }
33
+ }
34
+
35
+ export const localIp = (() => {
36
+ function first<T>(a: T[]): T | undefined {
37
+ return a.length > 0 ? a[0] : undefined;
38
+ }
39
+ const addresses = Object.values(networkInterfaces())
40
+ .flatMap((details?: NetworkInterfaceInfo[]) => {
41
+ return (details ?? []).filter((info) => info.family === 'IPv4');
42
+ }).reduce((acc, info) => {
43
+ acc[info.internal ? 'internal' : 'external'].push(info);
44
+ return acc;
45
+ }, {internal: [] as NetworkInterfaceInfo[], external: [] as NetworkInterfaceInfo[]});
46
+ return (first(addresses.internal) ?? first(addresses.external))?.address;
47
+ })();
@@ -0,0 +1,311 @@
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
+ }