@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/monitoring.ts
DELETED
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import getLogger from '../logger.js';
|
|
2
|
-
import {getHeapStatistics, writeHeapSnapshot, HeapInfo} from 'node:v8';
|
|
3
|
-
import {PathLike} from 'node:fs';
|
|
4
|
-
import {access, mkdir, rename, unlink} from 'node:fs/promises';
|
|
5
|
-
|
|
6
|
-
const log = getLogger('monitoring');
|
|
7
|
-
|
|
8
|
-
export type Options = typeof DEFAULT_OPTIONS;
|
|
9
|
-
|
|
10
|
-
export type Command = 'run' | 'dump' | 'stop';
|
|
11
|
-
export type Channel = (command?: Command) => Promise<boolean>;
|
|
12
|
-
|
|
13
|
-
const DEFAULT_OPTIONS = {
|
|
14
|
-
memoryLimit: 1024 * 1024 * 1024, // 1GB
|
|
15
|
-
reportInterval: 10 * 60 * 1000, // 10 min
|
|
16
|
-
dumpLocation: '.', // current folder
|
|
17
|
-
maxBackups: 10,
|
|
18
|
-
dumpPrefix: 'Heap'
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function fetchStats(): HeapInfo {
|
|
22
|
-
return getHeapStatistics();
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function dumpHeap(opts: Options) {
|
|
26
|
-
const prefix = opts.dumpPrefix ?? 'Heap';
|
|
27
|
-
const target = `${opts.dumpLocation}/${prefix}.heapsnapshot`;
|
|
28
|
-
if (log.enabledFor('debug')) {
|
|
29
|
-
log.debug(`starting heap dump in ${target}`);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
await fileExists(opts.dumpLocation)
|
|
33
|
-
.catch(async (_) => {
|
|
34
|
-
if (log.enabledFor('debug')) {
|
|
35
|
-
log.debug(`dump location ${opts.dumpLocation} does not exists. Will try to create it`);
|
|
36
|
-
}
|
|
37
|
-
try {
|
|
38
|
-
await mkdir(opts.dumpLocation, {recursive: true});
|
|
39
|
-
log.info(`dump location dir ${opts.dumpLocation} successfully created`);
|
|
40
|
-
} catch (e) {
|
|
41
|
-
log.error(`failed to create dump location ${opts.dumpLocation}`);
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
const dumpFileName = writeHeapSnapshot(target);
|
|
45
|
-
log.info(`heap dumped`);
|
|
46
|
-
try {
|
|
47
|
-
log.debug(`rolling snapshot backups`);
|
|
48
|
-
const lastFileName = `${opts.dumpLocation}/${prefix}.${opts.maxBackups}.heapsnapshot`;
|
|
49
|
-
await fileExists(lastFileName)
|
|
50
|
-
.then(async () => {
|
|
51
|
-
if (log.enabledFor('debug')) {
|
|
52
|
-
log.debug(`deleting ${lastFileName}`);
|
|
53
|
-
}
|
|
54
|
-
try {
|
|
55
|
-
await unlink(lastFileName);
|
|
56
|
-
} catch (e) {
|
|
57
|
-
log.warn(`failed to delete ${lastFileName}`, e);
|
|
58
|
-
}
|
|
59
|
-
})
|
|
60
|
-
.catch(() => {
|
|
61
|
-
/* do nothing*/
|
|
62
|
-
});
|
|
63
|
-
for (let i = opts.maxBackups - 1; i > 0; i--) {
|
|
64
|
-
const currentFileName = `${opts.dumpLocation}/${prefix}.${i}.heapsnapshot`;
|
|
65
|
-
const nextFileName = `${opts.dumpLocation}/${prefix}.${i + 1}.heapsnapshot`;
|
|
66
|
-
await fileExists(currentFileName)
|
|
67
|
-
.then(async () => {
|
|
68
|
-
try {
|
|
69
|
-
await rename(currentFileName, nextFileName);
|
|
70
|
-
} catch (e) {
|
|
71
|
-
log.warn(`failed to rename ${currentFileName} to ${nextFileName}`, e);
|
|
72
|
-
}
|
|
73
|
-
})
|
|
74
|
-
.catch(() => {
|
|
75
|
-
/* do nothing*/
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
const firstFileName = `${opts.dumpLocation}/${prefix}.${1}.heapsnapshot`;
|
|
79
|
-
try {
|
|
80
|
-
await rename(dumpFileName, firstFileName);
|
|
81
|
-
} catch (e) {
|
|
82
|
-
log.warn(`failed to rename ${dumpFileName} to ${firstFileName}`, e);
|
|
83
|
-
}
|
|
84
|
-
log.debug('snapshots rolled');
|
|
85
|
-
} catch (e) {
|
|
86
|
-
log.error('error rolling backups', e);
|
|
87
|
-
throw e;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async function fileExists(path: PathLike): Promise<void> {
|
|
92
|
-
log.trace(`checking file ${path}`);
|
|
93
|
-
await access(path);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async function processStats(stats: HeapInfo, state: {
|
|
97
|
-
memoryLimitExceeded: boolean,
|
|
98
|
-
snapshot?: boolean
|
|
99
|
-
}, opts: Options) {
|
|
100
|
-
if (log.enabledFor('debug')) {
|
|
101
|
-
log.debug(`processing heap stats ${JSON.stringify(stats)}`);
|
|
102
|
-
}
|
|
103
|
-
const limit = Math.min(opts.memoryLimit, (0.95 * stats.heap_size_limit));
|
|
104
|
-
const used = stats.used_heap_size;
|
|
105
|
-
log.info(`heap stats ${JSON.stringify(stats)}`);
|
|
106
|
-
if (used >= limit) {
|
|
107
|
-
log.warn(`used heap ${used} bytes exceeds memory limit ${limit} bytes`);
|
|
108
|
-
if (state.memoryLimitExceeded) {
|
|
109
|
-
delete state.snapshot;
|
|
110
|
-
} else {
|
|
111
|
-
state.memoryLimitExceeded = true;
|
|
112
|
-
state.snapshot = true;
|
|
113
|
-
}
|
|
114
|
-
await dumpHeap(opts);
|
|
115
|
-
} else {
|
|
116
|
-
state.memoryLimitExceeded = false;
|
|
117
|
-
delete state.snapshot;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export function start(opts?: Partial<Options>): Options & { channel: Channel } {
|
|
122
|
-
const merged: Options = {...DEFAULT_OPTIONS, ...opts};
|
|
123
|
-
|
|
124
|
-
let stopped = false;
|
|
125
|
-
const state = {memoryLimitExceeded: false};
|
|
126
|
-
const report = async () => {
|
|
127
|
-
const stats = fetchStats();
|
|
128
|
-
await processStats(stats, state, merged);
|
|
129
|
-
}
|
|
130
|
-
const interval = setInterval(report, merged.reportInterval);
|
|
131
|
-
const channel = async (command?: Command) => {
|
|
132
|
-
if (!stopped) {
|
|
133
|
-
command ??= 'run';
|
|
134
|
-
switch (command) {
|
|
135
|
-
case 'run': {
|
|
136
|
-
await report();
|
|
137
|
-
break;
|
|
138
|
-
}
|
|
139
|
-
case 'dump': {
|
|
140
|
-
await dumpHeap(merged);
|
|
141
|
-
break;
|
|
142
|
-
}
|
|
143
|
-
case 'stop': {
|
|
144
|
-
stopped = true;
|
|
145
|
-
clearInterval(interval);
|
|
146
|
-
log.info('exit memory diagnostic');
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
return stopped;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return {...merged, channel};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
async function run({channel}: { channel: Channel }, command?: Command) {
|
|
159
|
-
if (!await channel(command)) {
|
|
160
|
-
log.warn(`cannot execute command "${command}" already closed`)
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
export async function stop(m: { channel: Channel }) {
|
|
166
|
-
return await run(m, 'stop');
|
|
167
|
-
}
|
package/src/server/types.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
export type Middleware<Request extends ServerHttpRequest = ServerHttpRequest, Response extends ServerHttpResponse = ServerHttpResponse> = ((context: WebExchange<Request, Response>, next: () => Promise<void>) => Promise<void>)[];
|
|
2
|
-
|
|
3
|
-
export abstract class WebExchange<Request extends ServerHttpRequest = ServerHttpRequest, Response extends ServerHttpResponse = ServerHttpResponse> {
|
|
4
|
-
abstract readonly request: Request
|
|
5
|
-
abstract readonly response: Response
|
|
6
|
-
|
|
7
|
-
get method(): string | undefined {
|
|
8
|
-
return this.request.method;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
get path(): string | null | undefined {
|
|
12
|
-
return this.request.path;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
export type ReadonlyHeaderValue = string | undefined;
|
|
16
|
-
export type HeaderValue = number | ReadonlyHeaderValue;
|
|
17
|
-
export type ReadonlyHeaderValues = (readonly string[]) | ReadonlyHeaderValue
|
|
18
|
-
export type HeaderValues = (readonly string[]) | HeaderValue
|
|
19
|
-
|
|
20
|
-
export interface HttpHeaders {
|
|
21
|
-
get(name: string): HeaderValues
|
|
22
|
-
list(name: string): string[]
|
|
23
|
-
one(name: string): HeaderValue
|
|
24
|
-
has(name: string): boolean
|
|
25
|
-
keys(): IteratorObject<string>
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface ReadonlyHttpHeaders extends HttpHeaders {
|
|
29
|
-
get(name: string): ReadonlyHeaderValues
|
|
30
|
-
one(name: string): string | undefined
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
export interface MutableHttpHeaders extends HttpHeaders {
|
|
34
|
-
get(name: string): HeaderValues
|
|
35
|
-
one(name: string): HeaderValue
|
|
36
|
-
set(name: string, value: HeaderValues): this
|
|
37
|
-
add(name: string, value: string | (readonly string[])): this
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface HttpMessage<Headers = HttpHeaders> {
|
|
41
|
-
readonly headers: Headers
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface HttpRequest<Headers = HttpHeaders> extends HttpMessage<Headers> {
|
|
45
|
-
readonly path: string,
|
|
46
|
-
readonly method?: string
|
|
47
|
-
readonly URL: URL
|
|
48
|
-
readonly protocol: string
|
|
49
|
-
/**
|
|
50
|
-
* hostname[:port]
|
|
51
|
-
*/
|
|
52
|
-
readonly host?: string
|
|
53
|
-
readonly body: Promise<Blob>
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface HttpResponse<Headers = HttpHeaders> extends HttpMessage<Headers> {
|
|
57
|
-
readonly statusCode: number
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export type ServerHttpRequest = HttpRequest<ReadonlyHttpHeaders> /* & {
|
|
61
|
-
// readonly text: Promise<string>
|
|
62
|
-
// readonly json: Promise<unknown>
|
|
63
|
-
// readonly socket: http.IncomingMessage['socket']
|
|
64
|
-
// readonly _req: http.IncomingMessage
|
|
65
|
-
}*/
|
|
66
|
-
|
|
67
|
-
export interface ServerHttpResponse extends HttpResponse<MutableHttpHeaders> {
|
|
68
|
-
statusCode: number
|
|
69
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import {IOGateway} from '@interopio/gateway';
|
|
2
|
-
import {GatewayServer} from '../../gateway-server';
|
|
3
|
-
import getLogger from '../logger.js';
|
|
4
|
-
|
|
5
|
-
const log = getLogger('gateway.ws.client-verify');
|
|
6
|
-
|
|
7
|
-
export type ProcessedOriginFilters
|
|
8
|
-
= Required<Omit<GatewayServer.OriginFilters, 'blacklist' | 'whitelist'>>
|
|
9
|
-
//| Required<Omit<GatewayServer.OriginFilters, 'block' | 'allow'>>
|
|
10
|
-
;
|
|
11
|
-
|
|
12
|
-
function acceptsMissing(originFilters: ProcessedOriginFilters): boolean {
|
|
13
|
-
switch (originFilters.missing) {
|
|
14
|
-
case 'allow': // fall-through
|
|
15
|
-
case 'whitelist':
|
|
16
|
-
return true;
|
|
17
|
-
case 'block': // fall-through
|
|
18
|
-
case 'blacklist':
|
|
19
|
-
return false;
|
|
20
|
-
default:
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function tryMatch(originFilters: ProcessedOriginFilters, origin: string): boolean | undefined {
|
|
26
|
-
const block = originFilters.block ?? originFilters['blacklist'];
|
|
27
|
-
const allow = originFilters.allow ?? originFilters['whitelist'];
|
|
28
|
-
if (block.length > 0 && IOGateway.Filtering.valuesMatch(block, origin)) {
|
|
29
|
-
log.warn(`origin ${origin} matches block filter`);
|
|
30
|
-
return false;
|
|
31
|
-
} else if (allow.length > 0 && IOGateway.Filtering.valuesMatch(allow, origin)) {
|
|
32
|
-
if (log.enabledFor('debug')) {
|
|
33
|
-
log.debug(`origin ${origin} matches allow filter`);
|
|
34
|
-
}
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function acceptsNonMatched(originFilters: ProcessedOriginFilters): boolean {
|
|
40
|
-
switch (originFilters.non_matched) {
|
|
41
|
-
case 'allow': // fall-through
|
|
42
|
-
case 'whitelist':
|
|
43
|
-
return true;
|
|
44
|
-
case 'block': // fall-through
|
|
45
|
-
case 'blacklist':
|
|
46
|
-
return false;
|
|
47
|
-
default:
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function acceptsOrigin(origin?: string, originFilters?: ProcessedOriginFilters): boolean {
|
|
53
|
-
if (!originFilters) {
|
|
54
|
-
return true;
|
|
55
|
-
}
|
|
56
|
-
if (!origin) {
|
|
57
|
-
return acceptsMissing(originFilters);
|
|
58
|
-
} else {
|
|
59
|
-
const matchResult: boolean | undefined = tryMatch(originFilters, origin);
|
|
60
|
-
if (matchResult) {
|
|
61
|
-
return matchResult;
|
|
62
|
-
} else {
|
|
63
|
-
return acceptsNonMatched(originFilters);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function regexifyOriginFilters(originFilters?: GatewayServer.OriginFilters): ProcessedOriginFilters | undefined {
|
|
69
|
-
if (originFilters) {
|
|
70
|
-
const block = (originFilters.block ?? originFilters.blacklist ?? []).map(IOGateway.Filtering.regexify);
|
|
71
|
-
const allow = (originFilters.allow ?? originFilters.whitelist ?? []).map(IOGateway.Filtering.regexify);
|
|
72
|
-
return {
|
|
73
|
-
non_matched: originFilters.non_matched ?? 'allow',
|
|
74
|
-
missing: originFilters.missing ?? 'allow',
|
|
75
|
-
allow,
|
|
76
|
-
block,
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
package/src/server.ts
DELETED
|
@@ -1,316 +0,0 @@
|
|
|
1
|
-
import {WebSocketServer} from 'ws';
|
|
2
|
-
import http from 'node:http';
|
|
3
|
-
import https from 'node:https';
|
|
4
|
-
import {SecureContextOptions} from 'node:tls';
|
|
5
|
-
import {AddressInfo} from 'node:net';
|
|
6
|
-
import {readFileSync} from 'node:fs';
|
|
7
|
-
import {AsyncLocalStorage} from 'node:async_hooks';
|
|
8
|
-
import {IOGateway} from '@interopio/gateway';
|
|
9
|
-
import wsGateway from './gateway/ws/core.js';
|
|
10
|
-
import NodeConnections from './mesh/connections.js';
|
|
11
|
-
import restDirectory from './mesh/rest-directory/routes.js';
|
|
12
|
-
import meshBroker from './mesh/ws/broker/core.js';
|
|
13
|
-
import meshRelays from './mesh/ws/relays/core.js';
|
|
14
|
-
import meshCluster from './mesh/ws/cluster/core.js';
|
|
15
|
-
import metrics from './metrics/routes.js';
|
|
16
|
-
import {compose} from './common/compose.js';
|
|
17
|
-
import {WebExchange, Middleware} from './server/types.js';
|
|
18
|
-
import {DefaultWebExchange, HttpServerRequest, HttpServerResponse} from './server/exchange.js';
|
|
19
|
-
import {socketKey} from './utils.js';
|
|
20
|
-
import getLogger from './logger.js';
|
|
21
|
-
import {localIp, portRange} from './server/address.js';
|
|
22
|
-
import * as monitoring from './server/monitoring.js';
|
|
23
|
-
import {acceptsOrigin, ProcessedOriginFilters, regexifyOriginFilters} from './server/ws-client-verify.js';
|
|
24
|
-
import {GatewayServer} from '../gateway-server';
|
|
25
|
-
import cors from './server/cors.ts';
|
|
26
|
-
|
|
27
|
-
const logger = getLogger('app');
|
|
28
|
-
|
|
29
|
-
function secureContextOptions(ssl: GatewayServer.SslConfig): SecureContextOptions {
|
|
30
|
-
const options: SecureContextOptions = {};
|
|
31
|
-
if (ssl.key) options.key = readFileSync(ssl.key);
|
|
32
|
-
if (ssl.cert) options.cert = readFileSync(ssl.cert);
|
|
33
|
-
if (ssl.ca) options.ca = readFileSync(ssl.ca);
|
|
34
|
-
return options;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
type RequestHandler = (req: http.IncomingMessage, res: http.ServerResponse) => void;
|
|
38
|
-
|
|
39
|
-
function createListener(middleware: Middleware<HttpServerRequest, HttpServerResponse>,
|
|
40
|
-
routes: Map<string, RouteInfo>) {
|
|
41
|
-
const storage = new AsyncLocalStorage();
|
|
42
|
-
|
|
43
|
-
const listener = compose<WebExchange<HttpServerRequest, HttpServerResponse>>(
|
|
44
|
-
async ({response}, next) => {
|
|
45
|
-
response.headers.set('server', 'gateway-server');
|
|
46
|
-
await next();
|
|
47
|
-
},
|
|
48
|
-
...cors({
|
|
49
|
-
origins: {allow: [/http:\/\/localhost(:\d+)?/]},
|
|
50
|
-
methods: {allow: ['GET', 'HEAD', 'POST', 'DELETE']},
|
|
51
|
-
headers: {allow: '*'},
|
|
52
|
-
credentials: {allow: true}
|
|
53
|
-
}),
|
|
54
|
-
...middleware,
|
|
55
|
-
async ({request, response}, next) => {
|
|
56
|
-
if (request.method === 'GET' && request.path === '/health') {
|
|
57
|
-
response.statusCode = 200;
|
|
58
|
-
response._res.end(http.STATUS_CODES[200]);
|
|
59
|
-
} else {
|
|
60
|
-
await next();
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
async ({request, response}, next) => {
|
|
64
|
-
if (request.method === 'GET' && request.path === '/') {
|
|
65
|
-
response._res.end(`io.Gateway Server`);
|
|
66
|
-
} else {
|
|
67
|
-
await next();
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
async ({request, response}, _next) => {
|
|
71
|
-
const route = routes.get(request.path);
|
|
72
|
-
if (route) {
|
|
73
|
-
response.statusCode = 426;
|
|
74
|
-
response._res
|
|
75
|
-
.appendHeader('Upgrade', 'websocket')
|
|
76
|
-
.appendHeader('Connection', 'Upgrade')
|
|
77
|
-
.appendHeader('Content-Type', 'text/plain');
|
|
78
|
-
response._res.end(`This service [${request.path}] requires use of the websocket protocol.`);
|
|
79
|
-
} else {
|
|
80
|
-
response.statusCode = 404;
|
|
81
|
-
response._res.end(http.STATUS_CODES[404]);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
return (request: http.IncomingMessage, response: http.ServerResponse) => {
|
|
87
|
-
const exchange = new DefaultWebExchange(new HttpServerRequest(request), new HttpServerResponse(response));
|
|
88
|
-
return storage.run(exchange, async () => {
|
|
89
|
-
if (logger.enabledFor('debug')) {
|
|
90
|
-
const socket = exchange.request._req.socket;
|
|
91
|
-
if (logger.enabledFor('debug')) {
|
|
92
|
-
logger.debug(`received ${exchange.method} request for ${exchange.path} from ${socket.remoteAddress}:${socket.remotePort}`);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return await listener(exchange);
|
|
96
|
-
});
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function promisify<T>(fn: (callback?: (err?: Error) => void) => T): Promise<T> {
|
|
101
|
-
return new Promise<T>((resolve, reject) => {
|
|
102
|
-
const r = fn((err?: Error) => {
|
|
103
|
-
if (err) {
|
|
104
|
-
reject(err);
|
|
105
|
-
} else {
|
|
106
|
-
resolve(r);
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
type RouteInfo = {
|
|
113
|
-
readonly default?: boolean,
|
|
114
|
-
readonly ping?: number,
|
|
115
|
-
readonly maxConnections?: number
|
|
116
|
-
readonly originFilters?: ProcessedOriginFilters
|
|
117
|
-
readonly factory: (server: { endpoint: string, wss: WebSocketServer }) => Promise<{
|
|
118
|
-
info?: string,
|
|
119
|
-
close?: () => Promise<void>
|
|
120
|
-
}>,
|
|
121
|
-
// set later in listening
|
|
122
|
-
wss?: WebSocketServer,
|
|
123
|
-
close?: () => Promise<void>
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function memoryMonitor(config?: GatewayServer.ServerConfig['memory']) {
|
|
127
|
-
if (config) {
|
|
128
|
-
return monitoring.start({
|
|
129
|
-
memoryLimit: config.memory_limit,
|
|
130
|
-
dumpLocation: config.dump_location,
|
|
131
|
-
dumpPrefix: config.dump_prefix,
|
|
132
|
-
reportInterval: config.report_interval,
|
|
133
|
-
maxBackups: config.max_backups
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function regexAwareReplacer<T>(_key: string, value: T): string | T {
|
|
139
|
-
return value instanceof RegExp ? value.toString() : value;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export const Factory = async (options: GatewayServer.ServerConfig): Promise<GatewayServer.Server> => {
|
|
143
|
-
const ssl = options.ssl;
|
|
144
|
-
const createServer = ssl ? (options: http.ServerOptions, handler: RequestHandler) => https.createServer({...options, ...secureContextOptions(ssl)}, handler) : (options: http.ServerOptions, handler: RequestHandler) => http.createServer(options, handler);
|
|
145
|
-
const monitor = memoryMonitor(options.memory);
|
|
146
|
-
const middleware: Middleware<HttpServerRequest, HttpServerResponse> = [];
|
|
147
|
-
const routes: Map<string, RouteInfo> = new Map<string, RouteInfo>();
|
|
148
|
-
const gw = IOGateway.Factory({...options.gateway});
|
|
149
|
-
if (options.gateway) {
|
|
150
|
-
const config = options.gateway;
|
|
151
|
-
routes.set(config.route ?? '/', {
|
|
152
|
-
default: config.route === undefined,
|
|
153
|
-
ping: options.gateway.ping,
|
|
154
|
-
factory: wsGateway.bind(gw),
|
|
155
|
-
maxConnections: config.limits?.max_connections,
|
|
156
|
-
originFilters: regexifyOriginFilters(config.origins)
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
if (options.mesh) {
|
|
160
|
-
const connections = new NodeConnections(options.mesh.timeout ?? 60000);
|
|
161
|
-
middleware.push(...restDirectory(connections));
|
|
162
|
-
const ping = options.mesh.ping ?? 30000;
|
|
163
|
-
routes.set('/broker', {factory: meshBroker, ping: ping});
|
|
164
|
-
routes.set('/cluster', {factory: meshCluster, ping: ping});
|
|
165
|
-
routes.set('/relays', {factory: meshRelays, ping: ping});
|
|
166
|
-
}
|
|
167
|
-
if (options.metrics) {
|
|
168
|
-
middleware.push(...(await metrics(options.metrics)));
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const ports = portRange(options.port ?? 0);
|
|
172
|
-
const host = options.host;
|
|
173
|
-
const serverP: Promise<http.Server> = new Promise((resolve, reject) => {
|
|
174
|
-
const onSocketError = (err: Error) => logger.error(`socket error: ${err}`, err);
|
|
175
|
-
const server = createServer(
|
|
176
|
-
{},
|
|
177
|
-
createListener(middleware, routes));
|
|
178
|
-
|
|
179
|
-
server.on('error', (e: Error) => {
|
|
180
|
-
if (e['code'] === 'EADDRINUSE') {
|
|
181
|
-
logger.debug(`port ${e['port']} already in use on address ${e['address']}`);
|
|
182
|
-
const {value: port} = ports.next();
|
|
183
|
-
if (port) {
|
|
184
|
-
logger.info(`retry starting server on port ${port} and host ${host ?? '<unspecified>'}`);
|
|
185
|
-
server.close();
|
|
186
|
-
server.listen(port, host);
|
|
187
|
-
} else {
|
|
188
|
-
logger.warn(`all configured port(s) ${options.port} are in use. closing...`);
|
|
189
|
-
server.close();
|
|
190
|
-
reject(e);
|
|
191
|
-
}
|
|
192
|
-
} else {
|
|
193
|
-
logger.error(`server error: ${e.message}`, e);
|
|
194
|
-
reject(e);
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
server
|
|
198
|
-
.on('listening', async () => {
|
|
199
|
-
const info = server.address() as AddressInfo;
|
|
200
|
-
for (const [path, route] of routes) {
|
|
201
|
-
try {
|
|
202
|
-
logger.info(`creating ws server for [${path}]. max connections: ${route.maxConnections ?? '<unlimited>'}, origin filters: ${route.originFilters ? JSON.stringify(route.originFilters, regexAwareReplacer) : '<none>'}`);
|
|
203
|
-
const wss = new WebSocketServer({noServer: true});
|
|
204
|
-
const endpoint = `${ssl ? 'wss' : 'ws'}://${localIp}:${info.port}${path}`;
|
|
205
|
-
const handler = await route.factory({endpoint, wss});
|
|
206
|
-
const pingInterval = route.ping;
|
|
207
|
-
if (pingInterval) {
|
|
208
|
-
const pingIntervalId = setInterval(() => {
|
|
209
|
-
for (const client of wss.clients) {
|
|
210
|
-
if (client['connected'] === false) {
|
|
211
|
-
client.terminate();
|
|
212
|
-
}
|
|
213
|
-
client['connected'] = false;
|
|
214
|
-
client.ping();
|
|
215
|
-
}
|
|
216
|
-
}, pingInterval);
|
|
217
|
-
wss.on('close', () => {
|
|
218
|
-
clearInterval(pingIntervalId);
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
route.wss = wss;
|
|
222
|
-
route.close = handler.close?.bind(handler);
|
|
223
|
-
} catch (e) {
|
|
224
|
-
logger.warn(`failed to init route ${path}`, e);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
logger.info(`http server listening on ${info.address}:${info.port}`);
|
|
228
|
-
resolve(server);
|
|
229
|
-
});
|
|
230
|
-
server
|
|
231
|
-
.on('upgrade', (req, socket, head) => {
|
|
232
|
-
socket.addListener('error', onSocketError);
|
|
233
|
-
try {
|
|
234
|
-
const request = new HttpServerRequest(req);
|
|
235
|
-
const path = request.path ?? '/';
|
|
236
|
-
const route = (routes.get(path) ?? Array.from(routes.values()).find(route => {
|
|
237
|
-
if (path === '/' && route.default === true) {
|
|
238
|
-
return true;
|
|
239
|
-
}
|
|
240
|
-
}));
|
|
241
|
-
const host = request.host;
|
|
242
|
-
const info = socketKey(request.socket);
|
|
243
|
-
if (route?.wss) {
|
|
244
|
-
socket.removeListener('error', onSocketError);
|
|
245
|
-
const wss = route.wss;
|
|
246
|
-
if (route.maxConnections !== undefined && wss.clients?.size >= route.maxConnections) {
|
|
247
|
-
logger.warn(`${info} dropping ws connection request from ${host} on ${path}. max connections exceeded.`);
|
|
248
|
-
socket.destroy();
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const origin = request.headers['origin'];
|
|
253
|
-
if (!acceptsOrigin(origin, route.originFilters)) {
|
|
254
|
-
logger.info(`${info} dropping ws connection request from ${host} on ${path}. origin ${origin ?? '<missing>'}`);
|
|
255
|
-
socket.destroy();
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
if (logger.enabledFor('debug')) {
|
|
259
|
-
logger.debug(`${info} accepted new ws connection request from ${host} on ${path}`);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
263
|
-
ws.on('pong', () => ws['connected'] = true);
|
|
264
|
-
ws.on('ping', () => {
|
|
265
|
-
});
|
|
266
|
-
wss.emit('connection', ws, req);
|
|
267
|
-
});
|
|
268
|
-
} else {
|
|
269
|
-
logger.warn(`${info} rejected upgrade request from ${host} on ${path}`);
|
|
270
|
-
socket.destroy();
|
|
271
|
-
}
|
|
272
|
-
} catch (err) {
|
|
273
|
-
logger.error(`upgrade error: ${err}`, err);
|
|
274
|
-
}
|
|
275
|
-
})
|
|
276
|
-
.on('close', async () => {
|
|
277
|
-
logger.info(`http server closed.`);
|
|
278
|
-
});
|
|
279
|
-
try {
|
|
280
|
-
const {value: port} = ports.next();
|
|
281
|
-
server.listen(port, host);
|
|
282
|
-
} catch (e) {
|
|
283
|
-
logger.error(`error starting web socket server`, e);
|
|
284
|
-
reject(e instanceof Error ? e : new Error(`listen failed: ${e}`));
|
|
285
|
-
}
|
|
286
|
-
});
|
|
287
|
-
const server = await serverP;
|
|
288
|
-
return new class implements GatewayServer.Server {
|
|
289
|
-
readonly gateway = gw;
|
|
290
|
-
|
|
291
|
-
async close(): Promise<void> {
|
|
292
|
-
for (const [path, route] of routes) {
|
|
293
|
-
try {
|
|
294
|
-
if (route.close) {
|
|
295
|
-
await route.close();
|
|
296
|
-
}
|
|
297
|
-
logger.info(`stopping ws server for [${path}]. clients: ${route.wss?.clients?.size ?? 0}`);
|
|
298
|
-
route.wss?.clients?.forEach(client => {
|
|
299
|
-
client.terminate();
|
|
300
|
-
});
|
|
301
|
-
route.wss?.close();
|
|
302
|
-
} catch (e) {
|
|
303
|
-
logger.warn(`error closing route ${path}`, e);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
await promisify(cb => {
|
|
307
|
-
server.closeAllConnections();
|
|
308
|
-
server.close(cb);
|
|
309
|
-
});
|
|
310
|
-
if (monitor) {
|
|
311
|
-
await monitoring.stop(monitor);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
}
|
package/src/utils.ts
DELETED