mwc-proxy 0.0.1

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.
@@ -0,0 +1,203 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const crypto_1 = require("crypto");
7
+ const prismarine_auth_1 = require("prismarine-auth");
8
+ const yggdrasil_1 = __importDefault(require("yggdrasil"));
9
+ const debug_1 = __importDefault(require("debug"));
10
+ const metrics_1 = require("./metrics");
11
+ const yggdrasilServer = yggdrasil_1.default.server({ host: 'https://sessionserver.mojang.com' });
12
+ class FakeFileCache {
13
+ cacheName;
14
+ sharedCache;
15
+ cache;
16
+ constructor(cacheName, initial, sharedCache) {
17
+ this.cacheName = cacheName;
18
+ this.sharedCache = sharedCache;
19
+ this.cache = initial;
20
+ }
21
+ async loadInitialValue() {
22
+ return {};
23
+ }
24
+ async getCached() {
25
+ if (this.cache === undefined) {
26
+ this.cache = await this.loadInitialValue();
27
+ }
28
+ return this.cache;
29
+ }
30
+ async setCached(cached) {
31
+ this.cache = cached;
32
+ this.sharedCache[this.cacheName] = cached;
33
+ }
34
+ async setCachedPartial(cached) {
35
+ await this.setCached({
36
+ ...this.cache,
37
+ ...cached,
38
+ });
39
+ }
40
+ }
41
+ exports.default = (app, authTokensEndpoint = '/', sessionServerProxy = '/session', authLog = (str) => { }, urlRoot = '', options = {}) => {
42
+ const authActiveConnections = new Map();
43
+ const maxPerIp = options.authMaxConcurrentPerIp ?? 2;
44
+ const maxTotal = options.authMaxConcurrentTotal ?? 5;
45
+ if (options.authToggleDebugEndpoint) {
46
+ const endpoint = typeof options.authToggleDebugEndpoint === 'string' ? `${urlRoot}/${options.authToggleDebugEndpoint}` : `${urlRoot}/toggle-debug`;
47
+ app.get(endpoint, (req, res) => {
48
+ const SCOPES = ['prismarine-auth'];
49
+ if (debug_1.default.enabled(SCOPES.join(','))) {
50
+ debug_1.default.disable();
51
+ return res.send('Debug disabled');
52
+ }
53
+ debug_1.default.enable(SCOPES.join(','));
54
+ return res.send('Debug enabled');
55
+ });
56
+ }
57
+ app.use((req, res, next) => {
58
+ if (req.path === authTokensEndpoint && req.method === 'POST') {
59
+ const { ip } = req;
60
+ const currentCount = authActiveConnections.get(ip) || 0;
61
+ if (currentCount >= maxPerIp) {
62
+ res.status(429).send('Too many requests from this IP, please slow down.');
63
+ return;
64
+ }
65
+ const totalConnections = [...authActiveConnections.values()].reduce((acc, val) => acc + val, 0);
66
+ if (totalConnections >= maxTotal) {
67
+ res.status(429).send('Server is busy with other requests, please try again later.');
68
+ return;
69
+ }
70
+ authActiveConnections.set(ip, currentCount + 1);
71
+ res.on('close', () => {
72
+ // console.log('close auth req')
73
+ const currentCount = authActiveConnections.get(ip) || 0;
74
+ authActiveConnections.set(ip, currentCount - 1);
75
+ });
76
+ next();
77
+ }
78
+ next();
79
+ });
80
+ app.post(authTokensEndpoint, async (req, res) => {
81
+ const sendString = (string) => {
82
+ res.write(`${string}\n\n`);
83
+ //@ts-expect-error
84
+ res.flush();
85
+ };
86
+ try {
87
+ const cachesAndOptions = req.body;
88
+ authLog(`Starting auth request: ${cachesAndOptions.connectingServer} ${cachesAndOptions.connectingServerVersion} ${req.ip} ${req.headers.origin}`);
89
+ // from minecraft-protocol, for easier testing
90
+ const bannedServers = ['mc.hypixel.net', 'hypixel.net'];
91
+ if (cachesAndOptions.connectingServer && bannedServers.includes(cachesAndOptions.connectingServer)) {
92
+ res.status(403).send({
93
+ error: 'Server is not supported yet. If you interested, please let us know in the discord.',
94
+ });
95
+ return;
96
+ }
97
+ if (!cachesAndOptions.flow) {
98
+ cachesAndOptions.authTitle = prismarine_auth_1.Titles.MinecraftNintendoSwitch;
99
+ cachesAndOptions.deviceType = 'Nintendo';
100
+ cachesAndOptions.flow = 'live';
101
+ }
102
+ const newCache = {};
103
+ // Set headers for SSE
104
+ res.setHeader('Content-Type', 'text/event-stream');
105
+ res.setHeader('Cache-Control', 'no-cache');
106
+ res.setHeader('Connection', 'keep-alive');
107
+ const authflow = new prismarine_auth_1.Authflow('',
108
+ //@ts-expect-error
109
+ ({ cacheName, username }) => new FakeFileCache(cacheName, cachesAndOptions[cacheName], newCache), {
110
+ flow: cachesAndOptions.flow,
111
+ msalConfig: cachesAndOptions.msalConfig,
112
+ authTitle: cachesAndOptions.authTitle,
113
+ deviceType: cachesAndOptions.deviceType,
114
+ deviceVersion: cachesAndOptions.deviceVersion,
115
+ }, data => {
116
+ sendString(`${JSON.stringify(data)}`);
117
+ setTimeout(() => {
118
+ res.end();
119
+ }, data.expires_in * 1000);
120
+ });
121
+ // Send an event to the client
122
+ sendString(`data: ${JSON.stringify({ message: 'Hello from the authentication server! Starting your request now.' })}`);
123
+ // Keep the connection open by sending a comment every 5 seconds
124
+ const interval = setInterval(() => {
125
+ sendString(`: ${new Date().toISOString()}`);
126
+ }, 5000);
127
+ res.on('close', () => {
128
+ res.end();
129
+ clearInterval(interval);
130
+ //@ts-expect-error
131
+ const { msa } = authflow;
132
+ if (msa) {
133
+ msa.polling = false;
134
+ }
135
+ });
136
+ const tokenData = await authflow.getMinecraftJavaToken({
137
+ fetchCertificates: true,
138
+ fetchProfile: true,
139
+ ...cachesAndOptions.getJavaTokenOptions,
140
+ });
141
+ transformAllKeysDeep(tokenData);
142
+ sendString(`${JSON.stringify({ newCache })}`);
143
+ sendString(`${JSON.stringify(tokenData)}`);
144
+ // profileKeys.public.export
145
+ sendString('data: {"message": "Request complete. Bye!"}');
146
+ authLog(`Auth request complete: ${Object.keys(tokenData).join(', ')}; ${Object.keys(newCache).join(', ')}`);
147
+ metrics_1.updateMetrics.recordAuthRequest(true);
148
+ res.end();
149
+ }
150
+ catch (e) {
151
+ try {
152
+ console.error(e);
153
+ sendString(`${JSON.stringify({ error: e.message })}`);
154
+ res.status(500).send({
155
+ error: e.message,
156
+ });
157
+ metrics_1.updateMetrics.recordAuthRequest(false);
158
+ res.end();
159
+ }
160
+ catch { }
161
+ }
162
+ });
163
+ app.post(sessionServerProxy, async (req, res) => {
164
+ try {
165
+ authLog(`Join request received`);
166
+ yggdrasilServer.join(req.body.accessToken, req.body.selectedProfile, req.body.serverId, new Uint8Array(req.body.sharedSecret.data), new Uint8Array(req.body.publicKey.data), err => {
167
+ if (err) {
168
+ console.error(err);
169
+ return res.status(403).send({
170
+ error: err.message,
171
+ });
172
+ }
173
+ return res.status(204).send();
174
+ });
175
+ }
176
+ catch (err) {
177
+ console.error(err);
178
+ try {
179
+ res.status(500).send({
180
+ error: err.message,
181
+ });
182
+ }
183
+ catch { }
184
+ }
185
+ });
186
+ };
187
+ const transformAllKeysDeep = obj => {
188
+ if (typeof obj !== 'object' || obj === null)
189
+ return obj;
190
+ const keys = Object.keys(obj);
191
+ const result = {};
192
+ for (const key of keys) {
193
+ const value = obj[key];
194
+ if (value instanceof crypto_1.KeyObject) {
195
+ const isPrivate = value.type === 'private';
196
+ result[key] = value.export({
197
+ format: 'der',
198
+ type: isPrivate ? 'pkcs8' : 'spki',
199
+ });
200
+ }
201
+ transformAllKeysDeep(value);
202
+ }
203
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * minecraft-web-proxy — library entry point
3
+ *
4
+ * Import from here when using this package as a library (npm) or when building
5
+ * a custom server (like src-mwc). Zero side-effects: nothing starts, nothing
6
+ * listens. See src/app.ts for the standalone CLI/Docker entry.
7
+ */
8
+ export { createProxyMiddleware, currentState, parseSocksProxy } from './api';
9
+ export type { ProxyMiddlewareOptions } from './api';
10
+ export { SignalClient } from './signal-client';
11
+ export type { SignalClientOptions } from './signal-client';
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,qBAAqB,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,OAAO,CAAA;AAC5E,YAAY,EAAE,sBAAsB,EAAE,MAAM,OAAO,CAAA;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,YAAY,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ /**
3
+ * minecraft-web-proxy — library entry point
4
+ *
5
+ * Import from here when using this package as a library (npm) or when building
6
+ * a custom server (like src-mwc). Zero side-effects: nothing starts, nothing
7
+ * listens. See src/app.ts for the standalone CLI/Docker entry.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.SignalClient = exports.parseSocksProxy = exports.currentState = exports.createProxyMiddleware = void 0;
11
+ var api_1 = require("./api");
12
+ Object.defineProperty(exports, "createProxyMiddleware", { enumerable: true, get: function () { return api_1.createProxyMiddleware; } });
13
+ Object.defineProperty(exports, "currentState", { enumerable: true, get: function () { return api_1.currentState; } });
14
+ Object.defineProperty(exports, "parseSocksProxy", { enumerable: true, get: function () { return api_1.parseSocksProxy; } });
15
+ var signal_client_1 = require("./signal-client");
16
+ Object.defineProperty(exports, "SignalClient", { enumerable: true, get: function () { return signal_client_1.SignalClient; } });
@@ -0,0 +1,18 @@
1
+ import promBundle from 'express-prom-bundle';
2
+ export declare const systemMetrics: {
3
+ cpuUsage: any;
4
+ memoryUsage: any;
5
+ processMemory: any;
6
+ };
7
+ export declare const activeConnectionsGauge: any;
8
+ export declare const totalConnectionsCounter: any;
9
+ export declare const authRequestsCounter: any;
10
+ export declare const connectionDurationGauge: any;
11
+ export declare const metricsMiddleware: promBundle.Middleware;
12
+ export declare const updateMetrics: {
13
+ addConnection(): void;
14
+ removeConnection(): void;
15
+ recordAuthRequest(success: boolean): void;
16
+ updateConnectionDuration(token: string, duration: number, host: string, ip: string): void;
17
+ };
18
+ //# sourceMappingURL=metrics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../src/metrics.ts"],"names":[],"mappings":"AACA,OAAO,UAAU,MAAM,qBAAqB,CAAA;AAsC5C,eAAO,MAAM,aAAa;;;;CAoDzB,CAAA;AAGD,eAAO,MAAM,sBAAsB,KAIjC,CAAA;AAEF,eAAO,MAAM,uBAAuB,KAIlC,CAAA;AAEF,eAAO,MAAM,mBAAmB,KAK9B,CAAA;AAEF,eAAO,MAAM,uBAAuB,KAKlC,CAAA;AAGF,eAAO,MAAM,iBAAiB,uBAQ5B,CAAA;AAGF,eAAO,MAAM,aAAa;;;+BAgBK,OAAO;oCAOF,MAAM,YAAY,MAAM,QAAQ,MAAM,MAAM,MAAM;CAOrF,CAAA"}
@@ -0,0 +1,174 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.updateMetrics = exports.metricsMiddleware = exports.connectionDurationGauge = exports.authRequestsCounter = exports.totalConnectionsCounter = exports.activeConnectionsGauge = exports.systemMetrics = void 0;
7
+ const os_1 = __importDefault(require("os"));
8
+ const express_prom_bundle_1 = __importDefault(require("express-prom-bundle"));
9
+ const prom_client_1 = require("prom-client");
10
+ // Create a new registry
11
+ const register = new prom_client_1.Registry();
12
+ // Enable collection of default metrics with custom prefix and error handling
13
+ try {
14
+ (0, prom_client_1.collectDefaultMetrics)({
15
+ register,
16
+ prefix: 'minecraft_proxy_',
17
+ gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5],
18
+ });
19
+ }
20
+ catch (error) {
21
+ console.error('Error collecting default metrics:', error);
22
+ }
23
+ // Safely create and manage metrics
24
+ const createMetric = (MetricClass, config) => {
25
+ try {
26
+ return new MetricClass(config);
27
+ }
28
+ catch (error) {
29
+ console.error(`Error creating metric ${config.name}:`, error);
30
+ // Return a dummy metric that won't crash on operations
31
+ return {
32
+ inc() { },
33
+ dec() { },
34
+ set() { },
35
+ labels: () => ({
36
+ inc() { },
37
+ dec() { },
38
+ set() { },
39
+ }),
40
+ };
41
+ }
42
+ };
43
+ // System metrics with error handling
44
+ exports.systemMetrics = {
45
+ cpuUsage: createMetric(prom_client_1.Gauge, {
46
+ name: 'minecraft_proxy_cpu_usage_percentage',
47
+ help: 'CPU usage percentage',
48
+ registers: [register],
49
+ collect() {
50
+ try {
51
+ const cpus = os_1.default.cpus();
52
+ const totalCpuTime = cpus.reduce((acc, cpu) => acc + Object.values(cpu.times).reduce((sum, time) => sum + time, 0), 0);
53
+ const idleTime = cpus.reduce((acc, cpu) => acc + cpu.times.idle, 0);
54
+ const usagePercentage = ((totalCpuTime - idleTime) / totalCpuTime) * 100;
55
+ this.set(usagePercentage);
56
+ }
57
+ catch (error) {
58
+ console.error('Error collecting CPU metrics:', error);
59
+ }
60
+ }
61
+ }),
62
+ memoryUsage: createMetric(prom_client_1.Gauge, {
63
+ name: 'minecraft_proxy_memory_usage_bytes',
64
+ help: 'Memory usage in bytes',
65
+ registers: [register],
66
+ labelNames: ['type'],
67
+ collect() {
68
+ try {
69
+ const used = os_1.default.totalmem() - os_1.default.freemem();
70
+ this.set({ type: 'total' }, os_1.default.totalmem());
71
+ this.set({ type: 'free' }, os_1.default.freemem());
72
+ this.set({ type: 'used' }, used);
73
+ }
74
+ catch (error) {
75
+ console.error('Error collecting memory metrics:', error);
76
+ }
77
+ }
78
+ }),
79
+ processMemory: createMetric(prom_client_1.Gauge, {
80
+ name: 'minecraft_proxy_process_memory_bytes',
81
+ help: 'Process memory usage in bytes',
82
+ registers: [register],
83
+ labelNames: ['type'],
84
+ collect() {
85
+ try {
86
+ const usage = process.memoryUsage();
87
+ this.set({ type: 'rss' }, usage.rss);
88
+ this.set({ type: 'heapTotal' }, usage.heapTotal);
89
+ this.set({ type: 'heapUsed' }, usage.heapUsed);
90
+ this.set({ type: 'external' }, usage.external);
91
+ }
92
+ catch (error) {
93
+ console.error('Error collecting process memory metrics:', error);
94
+ }
95
+ }
96
+ })
97
+ };
98
+ // Custom metrics with proper label definitions
99
+ exports.activeConnectionsGauge = createMetric(prom_client_1.Gauge, {
100
+ name: 'minecraft_proxy_active_connections',
101
+ help: 'Number of active connections',
102
+ registers: [register],
103
+ });
104
+ exports.totalConnectionsCounter = createMetric(prom_client_1.Counter, {
105
+ name: 'minecraft_proxy_total_connections',
106
+ help: 'Total number of connections ever made',
107
+ registers: [register],
108
+ });
109
+ exports.authRequestsCounter = createMetric(prom_client_1.Counter, {
110
+ name: 'minecraft_proxy_auth_requests_total',
111
+ help: 'Total number of authentication requests',
112
+ labelNames: ['status'],
113
+ registers: [register],
114
+ });
115
+ exports.connectionDurationGauge = createMetric(prom_client_1.Gauge, {
116
+ name: 'minecraft_proxy_connection_duration_seconds',
117
+ help: 'Duration of connections in seconds',
118
+ labelNames: ['token', 'host', 'ip'],
119
+ registers: [register],
120
+ });
121
+ // Express middleware with error handling
122
+ exports.metricsMiddleware = (0, express_prom_bundle_1.default)({
123
+ includeMethod: true,
124
+ includePath: true,
125
+ promRegistry: register,
126
+ normalizePath: [
127
+ ['^/api/vm/net/.*', '/api/vm/net/#endpoint']
128
+ ],
129
+ buckets: [0.1, 0.5, 1, 2, 5],
130
+ });
131
+ // Safe metric update functions
132
+ exports.updateMetrics = {
133
+ addConnection() {
134
+ try {
135
+ exports.activeConnectionsGauge.inc();
136
+ exports.totalConnectionsCounter.inc();
137
+ }
138
+ catch (error) {
139
+ console.error('Error updating connection metrics:', error);
140
+ }
141
+ },
142
+ removeConnection() {
143
+ try {
144
+ exports.activeConnectionsGauge.dec();
145
+ }
146
+ catch (error) {
147
+ console.error('Error updating connection metrics:', error);
148
+ }
149
+ },
150
+ recordAuthRequest(success) {
151
+ try {
152
+ exports.authRequestsCounter.inc({ status: success ? 'success' : 'failure' });
153
+ }
154
+ catch (error) {
155
+ console.error('Error updating auth metrics:', error);
156
+ }
157
+ },
158
+ updateConnectionDuration(token, duration, host, ip) {
159
+ try {
160
+ exports.connectionDurationGauge.set({ token, host, ip }, duration);
161
+ }
162
+ catch (error) {
163
+ console.error('Error updating duration metrics:', error);
164
+ }
165
+ }
166
+ };
167
+ // Initialize frequent CPU metrics collection
168
+ // setInterval(() => {
169
+ // try {
170
+ // systemMetrics.cpuUsage.collect()
171
+ // } catch (error) {
172
+ // console.error('Error collecting CPU metrics:', error)
173
+ // }
174
+ // }, 2000) // Update every 2 seconds
@@ -0,0 +1,32 @@
1
+ export type SignalClientOptions = {
2
+ enabled: boolean;
3
+ signalServerUrl: string;
4
+ description?: string;
5
+ domain?: string;
6
+ acknowledgeInterval?: number;
7
+ /** Port this proxy listens on; when set, self-check (public IP + loop request) runs before reporting */
8
+ listenPort?: number;
9
+ /** URL path prefix for self-check request (e.g. /api/vm/net) */
10
+ urlRoot?: string;
11
+ getSystemInfo?: () => {
12
+ cpuLoad?: number;
13
+ ramLoad?: number;
14
+ connectedPlayers?: number;
15
+ };
16
+ };
17
+ export declare class SignalClient {
18
+ private readonly options;
19
+ private intervalId?;
20
+ private backoffUntil;
21
+ private selfCheckPassed;
22
+ private selfCheckDone;
23
+ constructor(options: Partial<SignalClientOptions>);
24
+ private runSelfCheckAndStart;
25
+ private getPublicIp;
26
+ private selfCheckLoop;
27
+ private start;
28
+ private sendAcknowledgment;
29
+ stop(): void;
30
+ isEnabled(): boolean;
31
+ }
32
+ //# sourceMappingURL=signal-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signal-client.d.ts","sourceRoot":"","sources":["../src/signal-client.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,mBAAmB,GAAG;IAC9B,OAAO,EAAE,OAAO,CAAA;IAChB,eAAe,EAAE,MAAM,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,wGAAwG;IACxG,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,gEAAgE;IAChE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,MAAM;QAClB,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAC5B,CAAA;CACJ,CAAA;AAED,qBAAa,YAAY;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,UAAU,CAAC,CAAgB;IACnC,OAAO,CAAC,YAAY,CAAI;IACxB,OAAO,CAAC,eAAe,CAAQ;IAC/B,OAAO,CAAC,aAAa,CAAQ;gBAEjB,OAAO,EAAE,OAAO,CAAC,mBAAmB,CAAC;YAuBnC,oBAAoB;YA6BpB,WAAW;IAQzB,OAAO,CAAC,aAAa;IAmBrB,OAAO,CAAC,KAAK;IAQb,OAAO,CAAC,kBAAkB;IA+D1B,IAAI;IAOJ,SAAS,IAAI,OAAO;CAGvB"}
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SignalClient = void 0;
7
+ const https_1 = __importDefault(require("https"));
8
+ const http_1 = __importDefault(require("http"));
9
+ const public_ip_1 = require("public-ip");
10
+ const SELF_CHECK_TIMEOUT_MS = 8000;
11
+ const BACKOFF_AFTER_502_MS = 5 * 60 * 1000; // 5 minutes
12
+ class SignalClient {
13
+ options;
14
+ intervalId;
15
+ backoffUntil = 0;
16
+ selfCheckPassed = false;
17
+ selfCheckDone = false;
18
+ constructor(options) {
19
+ this.options = {
20
+ enabled: options.enabled ?? true,
21
+ signalServerUrl: options.signalServerUrl ?? 'https://signal.mcraft.fun',
22
+ description: options.description,
23
+ domain: options.domain,
24
+ acknowledgeInterval: options.acknowledgeInterval ?? 10_000,
25
+ listenPort: options.listenPort,
26
+ urlRoot: options.urlRoot ?? '/api/vm/net',
27
+ getSystemInfo: options.getSystemInfo,
28
+ };
29
+ if (this.options.enabled) {
30
+ if (this.options.listenPort != null) {
31
+ this.runSelfCheckAndStart();
32
+ }
33
+ else {
34
+ this.selfCheckPassed = true;
35
+ this.selfCheckDone = true;
36
+ this.start();
37
+ }
38
+ }
39
+ }
40
+ async runSelfCheckAndStart() {
41
+ const port = this.options.listenPort;
42
+ const urlRoot = (this.options.urlRoot ?? '').replace(/\/$/, '');
43
+ try {
44
+ const publicIp = await this.getPublicIp();
45
+ if (!publicIp) {
46
+ console.warn('Signal: could not get public IP, skipping signal server integration');
47
+ this.selfCheckDone = true;
48
+ return;
49
+ }
50
+ const ok = await this.selfCheckLoop(publicIp, port, urlRoot);
51
+ this.selfCheckDone = true;
52
+ if (!ok) {
53
+ console.warn(`Signal: self-check failed (not reachable at ${publicIp}:${port}), skipping signal server integration`);
54
+ return;
55
+ }
56
+ this.selfCheckPassed = true;
57
+ this.start();
58
+ }
59
+ catch (err) {
60
+ this.selfCheckDone = true;
61
+ console.warn('Signal: self-check failed:', err.message);
62
+ }
63
+ }
64
+ async getPublicIp() {
65
+ try {
66
+ return await (0, public_ip_1.publicIpv4)();
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ }
72
+ selfCheckLoop(publicIp, port, urlRoot) {
73
+ return new Promise((resolve) => {
74
+ const path = `${urlRoot}/connect`;
75
+ const url = `http://${publicIp}:${port}${path}`;
76
+ const req = http_1.default.get(url, (res) => {
77
+ if (res.statusCode === 502) {
78
+ resolve(false);
79
+ return;
80
+ }
81
+ resolve(res.statusCode !== undefined && res.statusCode >= 200 && res.statusCode < 500);
82
+ });
83
+ req.on('error', () => resolve(false));
84
+ req.setTimeout(SELF_CHECK_TIMEOUT_MS, () => {
85
+ req.destroy();
86
+ resolve(false);
87
+ });
88
+ });
89
+ }
90
+ start() {
91
+ console.log(`Signal Server integration enabled, reporting to ${this.options.signalServerUrl}`);
92
+ this.sendAcknowledgment();
93
+ this.intervalId = setInterval(() => {
94
+ this.sendAcknowledgment();
95
+ }, this.options.acknowledgeInterval);
96
+ }
97
+ sendAcknowledgment() {
98
+ if (!this.options.enabled || !this.selfCheckPassed)
99
+ return;
100
+ if (Date.now() < this.backoffUntil)
101
+ return;
102
+ const params = new URLSearchParams();
103
+ if (this.options.description) {
104
+ params.append('description', this.options.description);
105
+ }
106
+ if (this.options.domain) {
107
+ params.append('domain', this.options.domain);
108
+ }
109
+ const info = this.options.getSystemInfo?.();
110
+ if (info?.connectedPlayers !== undefined) {
111
+ params.append('players', info.connectedPlayers.toString());
112
+ }
113
+ if (info?.cpuLoad !== undefined) {
114
+ params.append('cpuLoad', info.cpuLoad.toString());
115
+ }
116
+ if (info?.ramLoad !== undefined) {
117
+ params.append('ramLoad', info.ramLoad.toString());
118
+ }
119
+ const url = `${this.options.signalServerUrl}/api/acknowledge?${params.toString()}`;
120
+ const protocol = this.options.signalServerUrl.startsWith('https') ? https_1.default : http_1.default;
121
+ const req = protocol.get(url, (res) => {
122
+ let data = '';
123
+ res.on('data', (chunk) => {
124
+ data += chunk;
125
+ });
126
+ res.on('end', () => {
127
+ if (res.statusCode === 502) {
128
+ this.backoffUntil = Date.now() + BACKOFF_AFTER_502_MS;
129
+ console.warn(`Signal server returned 502, stopping reports for ${BACKOFF_AFTER_502_MS / 60000} minutes`);
130
+ return;
131
+ }
132
+ if (res.statusCode === 200) {
133
+ try {
134
+ const response = JSON.parse(data);
135
+ if (response.ok && response.approved) {
136
+ // Successfully acknowledged
137
+ }
138
+ }
139
+ catch (err) {
140
+ console.error('Failed to parse signal server response:', err);
141
+ }
142
+ }
143
+ else {
144
+ console.error(`Signal server returned status ${res.statusCode}: ${data}`);
145
+ }
146
+ });
147
+ });
148
+ req.on('error', (err) => {
149
+ console.error('Failed to send acknowledgment to signal server:', err.message);
150
+ });
151
+ req.setTimeout(5000, () => {
152
+ req.destroy();
153
+ console.error('Signal server acknowledgment timed out');
154
+ });
155
+ }
156
+ stop() {
157
+ if (this.intervalId) {
158
+ clearInterval(this.intervalId);
159
+ this.intervalId = undefined;
160
+ }
161
+ }
162
+ isEnabled() {
163
+ return this.options.enabled;
164
+ }
165
+ }
166
+ exports.SignalClient = SignalClient;