feat: initial release v1.0
This commit is contained in:
commit
77de4481a7
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
15
Dockerfile
Normal file
15
Dockerfile
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
FROM node:22-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY tsconfig.json ./
|
||||||
|
COPY src/ ./src/
|
||||||
|
RUN npx tsc
|
||||||
|
|
||||||
|
FROM node:22-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json ./
|
||||||
|
RUN npm install --omit=dev && npm cache clean --force
|
||||||
|
COPY --from=builder /app/dist/ ./dist/
|
||||||
|
USER node
|
||||||
|
CMD ["node", "dist/index.js"]
|
||||||
193
README.md
Normal file
193
README.md
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
# EasyBot Tunnel Client
|
||||||
|
|
||||||
|
Lightweight reverse-tunnel client that connects private services (databases, APIs, etc.) behind firewalls to your EasyBot runtime via a secure WebSocket connection.
|
||||||
|
|
||||||
|
The client connects **outbound** to your EasyBot server — no inbound ports, firewall rules, or public IPs needed on the client side.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. An EasyBot instance with the Tunnels feature enabled
|
||||||
|
2. A tunnel created from the EasyBot dashboard (Settings > Tunnels)
|
||||||
|
3. The **token** shown once at creation time (starts with `eb_tun_`)
|
||||||
|
|
||||||
|
## Quick Start (Docker)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d --restart always --name easybot-tunnel \
|
||||||
|
-e TUNNEL_SERVER=wss://YOUR_RUNTIME_URL/ws/tunnel \
|
||||||
|
-e TUNNEL_TOKEN=eb_tun_YOUR_TOKEN_HERE \
|
||||||
|
repo.pyp.ar/public-pull/easybot-tunnel-client:1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace:
|
||||||
|
- `YOUR_RUNTIME_URL` with your EasyBot runtime hostname (e.g. `runtime.example.com`)
|
||||||
|
- `eb_tun_YOUR_TOKEN_HERE` with the token from the dashboard
|
||||||
|
|
||||||
|
## Installation by OS
|
||||||
|
|
||||||
|
### Linux (Docker)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pull and run
|
||||||
|
docker run -d --restart always --name easybot-tunnel \
|
||||||
|
-e TUNNEL_SERVER=wss://runtime.example.com/ws/tunnel \
|
||||||
|
-e TUNNEL_TOKEN=eb_tun_xxxxx \
|
||||||
|
repo.pyp.ar/public-pull/easybot-tunnel-client:1.0
|
||||||
|
|
||||||
|
# Check logs
|
||||||
|
docker logs -f easybot-tunnel
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linux (systemd, without Docker)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Node.js 20+
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo bash -
|
||||||
|
sudo apt-get install -y nodejs
|
||||||
|
|
||||||
|
# Download and install
|
||||||
|
git clone https://repo.pyp.ar/public-pull/easybot-tunnel-client.git
|
||||||
|
cd easybot-tunnel-client
|
||||||
|
npm install --omit=dev
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Create systemd service
|
||||||
|
sudo tee /etc/systemd/system/easybot-tunnel.service <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=EasyBot Tunnel Client
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
Environment=TUNNEL_SERVER=wss://runtime.example.com/ws/tunnel
|
||||||
|
Environment=TUNNEL_TOKEN=eb_tun_xxxxx
|
||||||
|
WorkingDirectory=$(pwd)
|
||||||
|
ExecStart=/usr/bin/node dist/index.js
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now easybot-tunnel
|
||||||
|
sudo journalctl -u easybot-tunnel -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows (Docker Desktop)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker run -d --restart always --name easybot-tunnel `
|
||||||
|
-e TUNNEL_SERVER=wss://runtime.example.com/ws/tunnel `
|
||||||
|
-e TUNNEL_TOKEN=eb_tun_xxxxx `
|
||||||
|
repo.pyp.ar/public-pull/easybot-tunnel-client:1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows (as a service, without Docker)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Install Node.js 20+ from https://nodejs.org
|
||||||
|
|
||||||
|
# Download and build
|
||||||
|
git clone https://repo.pyp.ar/public-pull/easybot-tunnel-client.git
|
||||||
|
cd easybot-tunnel-client
|
||||||
|
npm install --omit=dev
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Install as Windows service using nssm (https://nssm.cc)
|
||||||
|
nssm install EasyBotTunnel "C:\Program Files\nodejs\node.exe" "C:\easybot-tunnel-client\dist\index.js"
|
||||||
|
nssm set EasyBotTunnel AppEnvironmentExtra TUNNEL_SERVER=wss://runtime.example.com/ws/tunnel TUNNEL_TOKEN=eb_tun_xxxxx
|
||||||
|
nssm set EasyBotTunnel AppRestartDelay 5000
|
||||||
|
nssm start EasyBotTunnel
|
||||||
|
```
|
||||||
|
|
||||||
|
### macOS (Docker)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d --restart always --name easybot-tunnel \
|
||||||
|
-e TUNNEL_SERVER=wss://runtime.example.com/ws/tunnel \
|
||||||
|
-e TUNNEL_TOKEN=eb_tun_xxxxx \
|
||||||
|
repo.pyp.ar/public-pull/easybot-tunnel-client:1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### macOS (launchd, without Docker)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Node.js
|
||||||
|
brew install node@22
|
||||||
|
|
||||||
|
# Download and build
|
||||||
|
git clone https://repo.pyp.ar/public-pull/easybot-tunnel-client.git
|
||||||
|
cd easybot-tunnel-client
|
||||||
|
npm install --omit=dev
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Create launch agent
|
||||||
|
cat > ~/Library/LaunchAgents/com.easybot.tunnel.plist <<EOF
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key><string>com.easybot.tunnel</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>/usr/local/bin/node</string>
|
||||||
|
<string>$(pwd)/dist/index.js</string>
|
||||||
|
</array>
|
||||||
|
<key>EnvironmentVariables</key>
|
||||||
|
<dict>
|
||||||
|
<key>TUNNEL_SERVER</key><string>wss://runtime.example.com/ws/tunnel</string>
|
||||||
|
<key>TUNNEL_TOKEN</key><string>eb_tun_xxxxx</string>
|
||||||
|
</dict>
|
||||||
|
<key>RunAtLoad</key><true/>
|
||||||
|
<key>KeepAlive</key><true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
launchctl load ~/Library/LaunchAgents/com.easybot.tunnel.plist
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Variable | Required | Description |
|
||||||
|
|----------|----------|-------------|
|
||||||
|
| `TUNNEL_SERVER` | Yes | WebSocket URL of your EasyBot runtime (e.g. `wss://runtime.example.com/ws/tunnel`) |
|
||||||
|
| `TUNNEL_TOKEN` | Yes | Authentication token from the dashboard (starts with `eb_tun_`) |
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. The client connects to your EasyBot runtime via WebSocket (outbound only)
|
||||||
|
2. The server sends a **config** with port mappings (e.g. "forward localhost:5432 on this tunnel")
|
||||||
|
3. When EasyBot needs to reach your private service, it opens a TCP connection through the tunnel
|
||||||
|
4. Traffic flows bidirectionally: EasyBot runtime <-> WebSocket <-> Tunnel Client <-> Your private service
|
||||||
|
|
||||||
|
All port mappings are configured **server-side** in the EasyBot dashboard. The client only needs the server URL and token.
|
||||||
|
|
||||||
|
## Verifying the Connection
|
||||||
|
|
||||||
|
After starting the client, check the EasyBot dashboard (Tunnels section). Your tunnel should show:
|
||||||
|
- Status: **Connected** (green)
|
||||||
|
- Latency in ms
|
||||||
|
- Client IP
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Issue | Solution |
|
||||||
|
|-------|----------|
|
||||||
|
| "Connection refused" | Check TUNNEL_SERVER URL is correct and reachable |
|
||||||
|
| "Authentication failed" | Verify TUNNEL_TOKEN is correct (not expired/regenerated) |
|
||||||
|
| Keeps reconnecting | Check firewall allows outbound WSS (port 443) |
|
||||||
|
| Target not reachable | Ensure the target service (e.g. PostgreSQL) is running and accessible from the machine running the tunnel client |
|
||||||
|
|
||||||
|
## Building from Source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://repo.pyp.ar/public-pull/easybot-tunnel-client.git
|
||||||
|
cd easybot-tunnel-client
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
node dist/index.js
|
||||||
|
```
|
||||||
19
package.json
Normal file
19
package.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "@easybot/tunnel-client",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Lightweight tunnel client for EasyBot — connects private services to EasyBot runtime via WebSocket",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"dev": "tsx src/index.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ws": "^8.18.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/ws": "^8.5.13",
|
||||||
|
"@types/node": "^22.0.0",
|
||||||
|
"typescript": "^5.6.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
95
src/connection-manager.ts
Normal file
95
src/connection-manager.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import net from 'net';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages multiplexed TCP connections over a single WebSocket.
|
||||||
|
* Each connection is identified by a 4-byte uint32 connection_id.
|
||||||
|
*/
|
||||||
|
export class ConnectionManager {
|
||||||
|
private connections = new Map<number, net.Socket>();
|
||||||
|
|
||||||
|
/** Open a connection to a target host:port for a given connection_id */
|
||||||
|
open(connId: number, targetHost: string, targetPort: number): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const socket = net.createConnection(targetPort, targetHost, () => {
|
||||||
|
this.connections.set(connId, socket);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('error', (err) => {
|
||||||
|
this.connections.delete(connId);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Timeout for connect phase
|
||||||
|
socket.setTimeout(10_000, () => {
|
||||||
|
socket.destroy(new Error('Connection timeout'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear timeout once connected
|
||||||
|
socket.once('connect', () => socket.setTimeout(0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set up data forwarding from a target socket to a callback */
|
||||||
|
onData(connId: number, callback: (data: Buffer) => void): void {
|
||||||
|
const socket = this.connections.get(connId);
|
||||||
|
if (!socket) return;
|
||||||
|
socket.on('data', callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set up close handler */
|
||||||
|
onClose(connId: number, callback: () => void): void {
|
||||||
|
const socket = this.connections.get(connId);
|
||||||
|
if (!socket) return;
|
||||||
|
socket.on('close', callback);
|
||||||
|
socket.on('error', callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Write data to a target socket */
|
||||||
|
write(connId: number, data: Buffer): boolean {
|
||||||
|
const socket = this.connections.get(connId);
|
||||||
|
if (!socket || socket.destroyed) return false;
|
||||||
|
socket.write(data);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Close a specific connection */
|
||||||
|
close(connId: number): void {
|
||||||
|
const socket = this.connections.get(connId);
|
||||||
|
if (socket) {
|
||||||
|
socket.destroy();
|
||||||
|
this.connections.delete(connId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Close all connections */
|
||||||
|
closeAll(): void {
|
||||||
|
for (const [connId, socket] of this.connections) {
|
||||||
|
socket.destroy();
|
||||||
|
}
|
||||||
|
this.connections.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
get size(): number {
|
||||||
|
return this.connections.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Binary Frame Helpers ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Encode a data frame: [4-byte connId BE][payload] */
|
||||||
|
export function encodeFrame(connId: number, payload: Buffer): Buffer {
|
||||||
|
const frame = Buffer.allocUnsafe(4 + payload.length);
|
||||||
|
frame.writeUInt32BE(connId, 0);
|
||||||
|
payload.copy(frame, 4);
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Decode a data frame: returns { connId, payload } */
|
||||||
|
export function decodeFrame(data: Buffer): { connId: number; payload: Buffer } | null {
|
||||||
|
if (data.length < 5) return null;
|
||||||
|
return {
|
||||||
|
connId: data.readUInt32BE(0),
|
||||||
|
payload: data.subarray(4),
|
||||||
|
};
|
||||||
|
}
|
||||||
203
src/index.ts
Normal file
203
src/index.ts
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
import WebSocket from 'ws';
|
||||||
|
import { ConnectionManager, encodeFrame, decodeFrame } from './connection-manager.js';
|
||||||
|
|
||||||
|
// ── Configuration ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const TUNNEL_SERVER = process.env.TUNNEL_SERVER;
|
||||||
|
const TUNNEL_TOKEN = process.env.TUNNEL_TOKEN;
|
||||||
|
const CLIENT_VERSION = '1.0.0';
|
||||||
|
|
||||||
|
if (!TUNNEL_SERVER || !TUNNEL_TOKEN) {
|
||||||
|
console.error('ERROR: TUNNEL_SERVER and TUNNEL_TOKEN environment variables are required');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── State ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface MappingConfig {
|
||||||
|
id: string;
|
||||||
|
target_host: string;
|
||||||
|
target_port: number;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ws: WebSocket | null = null;
|
||||||
|
let mappings: MappingConfig[] = [];
|
||||||
|
const connManager = new ConnectionManager();
|
||||||
|
let reconnectDelay = 1000;
|
||||||
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let shuttingDown = false;
|
||||||
|
|
||||||
|
// ── Logging ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function log(level: string, msg: string, data?: Record<string, any>): void {
|
||||||
|
const ts = new Date().toISOString();
|
||||||
|
const extra = data ? ' ' + JSON.stringify(data) : '';
|
||||||
|
console.log(`${ts} [${level}] ${msg}${extra}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── WebSocket Connection ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function connect(): void {
|
||||||
|
if (shuttingDown) return;
|
||||||
|
|
||||||
|
log('info', `Connecting to ${TUNNEL_SERVER}...`);
|
||||||
|
|
||||||
|
ws = new WebSocket(TUNNEL_SERVER!, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${TUNNEL_TOKEN}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('open', () => {
|
||||||
|
log('info', 'Connected to tunnel server');
|
||||||
|
reconnectDelay = 1000; // Reset backoff on successful connection
|
||||||
|
|
||||||
|
// Send client info
|
||||||
|
sendControl({ type: 'client_info', version: CLIENT_VERSION });
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('message', (data, isBinary) => {
|
||||||
|
if (isBinary) {
|
||||||
|
handleBinaryMessage(data as Buffer);
|
||||||
|
} else {
|
||||||
|
handleControlMessage(data.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('close', (code, reason) => {
|
||||||
|
log('info', `Disconnected from server`, { code, reason: reason.toString() });
|
||||||
|
cleanup();
|
||||||
|
scheduleReconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('error', (err) => {
|
||||||
|
log('error', `WebSocket error: ${err.message}`);
|
||||||
|
// 'close' event will fire after this, triggering reconnect
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup(): void {
|
||||||
|
connManager.closeAll();
|
||||||
|
ws = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleReconnect(): void {
|
||||||
|
if (shuttingDown) return;
|
||||||
|
if (reconnectTimer) return;
|
||||||
|
|
||||||
|
log('info', `Reconnecting in ${reconnectDelay}ms...`);
|
||||||
|
reconnectTimer = setTimeout(() => {
|
||||||
|
reconnectTimer = null;
|
||||||
|
connect();
|
||||||
|
}, reconnectDelay);
|
||||||
|
|
||||||
|
// Exponential backoff: 1s, 2s, 4s, 8s, 16s, max 30s
|
||||||
|
reconnectDelay = Math.min(reconnectDelay * 2, 30_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Control Message Handling ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function handleControlMessage(raw: string): void {
|
||||||
|
let msg: any;
|
||||||
|
try { msg = JSON.parse(raw); } catch { return; }
|
||||||
|
|
||||||
|
switch (msg.type) {
|
||||||
|
case 'config':
|
||||||
|
mappings = msg.tunnels || [];
|
||||||
|
log('info', `Received config: ${mappings.length} mapping(s)`, {
|
||||||
|
mappings: mappings.map(m => `${m.label}: ${m.target_host}:${m.target_port}`),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'open':
|
||||||
|
handleOpen(msg.connection_id, msg.tunnel_id);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'close': {
|
||||||
|
connManager.close(msg.connection_id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'ping':
|
||||||
|
sendControl({ type: 'pong' });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleOpen(connId: number, mappingId: string): Promise<void> {
|
||||||
|
// Find the mapping config
|
||||||
|
const mapping = mappings.find(m => m.id === mappingId);
|
||||||
|
if (!mapping) {
|
||||||
|
sendControl({ type: 'open_err', connection_id: connId, error: 'Unknown mapping' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connManager.open(connId, mapping.target_host, mapping.target_port);
|
||||||
|
sendControl({ type: 'open_ok', connection_id: connId });
|
||||||
|
|
||||||
|
// Forward data from target back to server
|
||||||
|
connManager.onData(connId, (data) => {
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(encodeFrame(connId, data));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle target close
|
||||||
|
connManager.onClose(connId, () => {
|
||||||
|
sendControl({ type: 'close', connection_id: connId });
|
||||||
|
});
|
||||||
|
|
||||||
|
log('info', `Connection ${connId} opened to ${mapping.target_host}:${mapping.target_port}`);
|
||||||
|
} catch (err: any) {
|
||||||
|
sendControl({ type: 'open_err', connection_id: connId, error: err.message });
|
||||||
|
log('error', `Connection ${connId} failed: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Binary Message Handling ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function handleBinaryMessage(data: Buffer): void {
|
||||||
|
const frame = decodeFrame(data);
|
||||||
|
if (!frame) return;
|
||||||
|
|
||||||
|
if (!connManager.write(frame.connId, frame.payload)) {
|
||||||
|
// Target socket gone — notify server
|
||||||
|
sendControl({ type: 'close', connection_id: frame.connId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function sendControl(msg: Record<string, any>): void {
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify(msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Graceful Shutdown ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function shutdown(signal: string): void {
|
||||||
|
log('info', `Received ${signal} — shutting down`);
|
||||||
|
shuttingDown = true;
|
||||||
|
|
||||||
|
if (reconnectTimer) clearTimeout(reconnectTimer);
|
||||||
|
connManager.closeAll();
|
||||||
|
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.close(1000, 'Client shutting down');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give a moment for close frame to be sent
|
||||||
|
setTimeout(() => process.exit(0), 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
||||||
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
||||||
|
|
||||||
|
// ── Start ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
log('info', `EasyBot Tunnel Client v${CLIENT_VERSION}`);
|
||||||
|
log('info', `Server: ${TUNNEL_SERVER}`);
|
||||||
|
connect();
|
||||||
14
tsconfig.json
Normal file
14
tsconfig.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"declaration": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user