9remote 0.1.64 → 2.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.
- package/README.md +179 -62
- package/cli/index.js +701 -293
- package/cli/utils/assets/trayIcon.ico +0 -0
- package/cli/utils/cloudflared.js +72 -36
- package/cli/utils/permissions.js +5 -5
- package/cli/utils/pids.js +114 -0
- package/cli/utils/token.js +3 -1
- package/cli/utils/tray.js +251 -0
- package/cli/utils/tui.js +209 -34
- package/cli/utils/updateChecker.js +107 -13
- package/dist/assets/trayIcon.ico +0 -0
- package/dist/cli.cjs +1 -36
- package/dist/ptyDaemon.cjs +1 -10
- package/dist/server.cjs +2 -184
- package/dist/ui/assets/{index-BfTPkO8b.css → index-COWVKicT.css} +1 -1
- package/dist/ui/assets/index-njTKNAa6.js +8 -0
- package/dist/ui/index.html +2 -2
- package/index.js +174 -618
- package/lib/constants.js +64 -0
- package/lib/deviceApproval.js +116 -0
- package/lib/router.js +134 -0
- package/lib/socketio.js +168 -25
- package/package.json +6 -1
- package/dist/ui/assets/index-B37vtDoz.js +0 -8
package/lib/constants.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import dns from "dns";
|
|
2
|
+
|
|
3
|
+
// Bypass broken macOS system DNS resolver for long hostnames (e.g. trycloudflare).
|
|
4
|
+
// Skip localhost/IP to avoid slow/hanging DNS resolve on Windows.
|
|
5
|
+
const _originalLookup = dns.lookup;
|
|
6
|
+
const IP_REGEX = /^(\d{1,3}\.){3}\d{1,3}$|^::1$|^[0-9a-f:]+$/i;
|
|
7
|
+
dns.lookup = (hostname, options, cb) => {
|
|
8
|
+
if (typeof options === "function") { cb = options; options = {}; }
|
|
9
|
+
if (hostname === "localhost" || IP_REGEX.test(hostname)) {
|
|
10
|
+
return _originalLookup(hostname, options, cb);
|
|
11
|
+
}
|
|
12
|
+
dns.resolve4(hostname, (err, addrs) => {
|
|
13
|
+
if (err) return _originalLookup(hostname, options, cb);
|
|
14
|
+
if (options.all) {
|
|
15
|
+
cb(null, addrs.map(a => ({ address: a, family: 4 })));
|
|
16
|
+
} else {
|
|
17
|
+
cb(null, addrs[0], 4);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Shared constants for agent server + CLI
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
// Connection step states (UI progress tracking)
|
|
27
|
+
export const STEP = {
|
|
28
|
+
STOPPED: 0,
|
|
29
|
+
PREPARING: 1,
|
|
30
|
+
CONNECTING: 2,
|
|
31
|
+
TUNNELING: 3,
|
|
32
|
+
VERIFYING: 4,
|
|
33
|
+
READY: 5,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Debug flags — toggle diagnostic displays without touching logic
|
|
37
|
+
export const DEBUG = {
|
|
38
|
+
showTunnelUrlInMenu: false,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// macOS permission poll — TCC has no change event, so we poll periodically.
|
|
42
|
+
// Fast cadence kicks in right after user clicks "Grant" so UI reflects the toggle quickly.
|
|
43
|
+
export const PERMISSION_POLL_MS = 5000;
|
|
44
|
+
export const PERMISSION_POLL_FAST_MS = 1000;
|
|
45
|
+
export const PERMISSION_POLL_FAST_DURATION = 60000;
|
|
46
|
+
|
|
47
|
+
// Browser-like headers to avoid CDN/firewall blocks
|
|
48
|
+
const BROWSER_HEADERS = {
|
|
49
|
+
"Accept": "application/json, text/plain, */*",
|
|
50
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
51
|
+
"Cache-Control": "no-cache",
|
|
52
|
+
"Pragma": "no-cache",
|
|
53
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* fetch() wrapper with browser-like headers for external requests
|
|
58
|
+
*/
|
|
59
|
+
export function browserFetch(url, options = {}) {
|
|
60
|
+
return fetch(url, {
|
|
61
|
+
...options,
|
|
62
|
+
headers: { ...BROWSER_HEADERS, ...options.headers },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device approval manager — persists approved deviceIds to ~/.9remote/approvedDevices.json
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { homedir } from "os";
|
|
8
|
+
|
|
9
|
+
const STATE_DIR = join(homedir(), ".9remote");
|
|
10
|
+
const DEVICES_FILE = join(STATE_DIR, "approvedDevices.json");
|
|
11
|
+
|
|
12
|
+
// deviceId -> { approvedAt }
|
|
13
|
+
let approvedDevices = new Map();
|
|
14
|
+
|
|
15
|
+
// Pending approval requests: socketId -> { deviceId, ip }
|
|
16
|
+
const pendingApprovals = new Map();
|
|
17
|
+
|
|
18
|
+
// Rejected devices (RAM only, cleared on restart): deviceId -> { ip, rejectedAt, socketId }
|
|
19
|
+
const rejectedDevices = new Map();
|
|
20
|
+
|
|
21
|
+
function ensureDir() {
|
|
22
|
+
if (!existsSync(STATE_DIR)) mkdirSync(STATE_DIR, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function loadApprovedDevices() {
|
|
26
|
+
try {
|
|
27
|
+
ensureDir();
|
|
28
|
+
if (existsSync(DEVICES_FILE)) {
|
|
29
|
+
const data = JSON.parse(readFileSync(DEVICES_FILE, "utf8"));
|
|
30
|
+
// Migrate from old array format to new map format
|
|
31
|
+
if (Array.isArray(data)) {
|
|
32
|
+
approvedDevices = new Map(data.map((id) => [id, { approvedAt: null }]));
|
|
33
|
+
} else {
|
|
34
|
+
approvedDevices = new Map(Object.entries(data));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
approvedDevices = new Map();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function saveApprovedDevices() {
|
|
43
|
+
try {
|
|
44
|
+
ensureDir();
|
|
45
|
+
writeFileSync(DEVICES_FILE, JSON.stringify(Object.fromEntries(approvedDevices), null, 2));
|
|
46
|
+
} catch {}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function isDeviceApproved(deviceId) {
|
|
50
|
+
if (!deviceId) return false;
|
|
51
|
+
return approvedDevices.has(deviceId);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function approveDevice(deviceId) {
|
|
55
|
+
if (!deviceId) return;
|
|
56
|
+
approvedDevices.set(deviceId, { approvedAt: new Date().toISOString() });
|
|
57
|
+
saveApprovedDevices();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function removeDevice(deviceId) {
|
|
61
|
+
approvedDevices.delete(deviceId);
|
|
62
|
+
saveApprovedDevices();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getApprovedDevices() {
|
|
66
|
+
return [...approvedDevices.entries()].map(([id, meta]) => ({ deviceId: id, ...meta }));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Pending approval queue
|
|
70
|
+
export function addPendingApproval(socketId, data) {
|
|
71
|
+
pendingApprovals.set(socketId, data);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getPendingApproval(socketId) {
|
|
75
|
+
return pendingApprovals.get(socketId) || null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function removePendingApproval(socketId) {
|
|
79
|
+
pendingApprovals.delete(socketId);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function isDevicePending(deviceId) {
|
|
83
|
+
if (!deviceId) return false;
|
|
84
|
+
for (const data of pendingApprovals.values()) {
|
|
85
|
+
if (data.deviceId === deviceId) return true;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function getAllPendingApprovals() {
|
|
91
|
+
return [...pendingApprovals.entries()].map(([socketId, data]) => ({ socketId, ...data }));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Rejected devices (pending re-approval from Clients list)
|
|
95
|
+
export function markDeviceRejected(deviceId, data) {
|
|
96
|
+
if (!deviceId) return;
|
|
97
|
+
rejectedDevices.set(deviceId, { ...data, rejectedAt: new Date().toISOString() });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function isDeviceRejected(deviceId) {
|
|
101
|
+
if (!deviceId) return false;
|
|
102
|
+
return rejectedDevices.has(deviceId);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function updateRejectedSocket(deviceId, socketId, ip) {
|
|
106
|
+
const entry = rejectedDevices.get(deviceId);
|
|
107
|
+
if (entry) rejectedDevices.set(deviceId, { ...entry, socketId, ip });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function getRejectedDevices() {
|
|
111
|
+
return [...rejectedDevices.entries()].map(([deviceId, meta]) => ({ deviceId, ...meta }));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function clearRejectedDevice(deviceId) {
|
|
115
|
+
rejectedDevices.delete(deviceId);
|
|
116
|
+
}
|
package/lib/router.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight config-driven HTTP router
|
|
3
|
+
* - Route table with path, method, handler
|
|
4
|
+
* - Public route whitelist (bypass localhost check)
|
|
5
|
+
* - Auto body parsing for POST/PUT
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { parse } from "url";
|
|
9
|
+
|
|
10
|
+
function readBody(req) {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
let body = "";
|
|
13
|
+
req.on("data", (chunk) => body += chunk);
|
|
14
|
+
req.on("end", () => resolve(body));
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function jsonOk(res, data = { ok: true }) {
|
|
19
|
+
res.setHeader("Content-Type", "application/json");
|
|
20
|
+
res.writeHead(200);
|
|
21
|
+
res.end(JSON.stringify(data));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function jsonErr(res, code, msg) {
|
|
25
|
+
res.setHeader("Content-Type", "application/json");
|
|
26
|
+
res.writeHead(code);
|
|
27
|
+
res.end(JSON.stringify({ error: msg }));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse JSON body with error handling, returns parsed object or null
|
|
32
|
+
*/
|
|
33
|
+
export async function parseJsonBody(req, res) {
|
|
34
|
+
try {
|
|
35
|
+
const raw = await readBody(req);
|
|
36
|
+
return JSON.parse(raw || "{}");
|
|
37
|
+
} catch {
|
|
38
|
+
jsonErr(res, 400, "Invalid JSON");
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create router from route config
|
|
45
|
+
* @param {Array<{path: string, method: string, handler: Function, public?: boolean}>} routes
|
|
46
|
+
* @param {Object} options
|
|
47
|
+
* @param {Function} options.fallback - Called when no route matches
|
|
48
|
+
* @returns {Function} HTTP request handler (req, res) => void
|
|
49
|
+
*/
|
|
50
|
+
export function createRouter(routes, { fallback } = {}) {
|
|
51
|
+
// Pre-build lookup map: "METHOD:/path" → { handler, public }
|
|
52
|
+
const exactMap = new Map();
|
|
53
|
+
const prefixRoutes = [];
|
|
54
|
+
|
|
55
|
+
for (const route of routes) {
|
|
56
|
+
const methods = route.method === "*" ? ["GET", "POST", "PUT", "DELETE"] : [route.method];
|
|
57
|
+
for (const m of methods) {
|
|
58
|
+
if (route.path.endsWith("/*")) {
|
|
59
|
+
prefixRoutes.push({ ...route, prefix: route.path.slice(0, -2), method: m });
|
|
60
|
+
} else {
|
|
61
|
+
exactMap.set(`${m}:${route.path}`, route);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Collect public paths for fast localhost check
|
|
67
|
+
const publicExact = new Set();
|
|
68
|
+
const publicPrefixes = [];
|
|
69
|
+
for (const route of routes) {
|
|
70
|
+
if (!route.public) continue;
|
|
71
|
+
if (route.path.endsWith("/*")) {
|
|
72
|
+
publicPrefixes.push(route.path.slice(0, -2));
|
|
73
|
+
} else {
|
|
74
|
+
publicExact.add(route.path);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isPublic(pathname) {
|
|
79
|
+
if (publicExact.has(pathname)) return true;
|
|
80
|
+
return publicPrefixes.some((p) => pathname.startsWith(p));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return async (req, res) => {
|
|
84
|
+
const parsedUrl = parse(req.url, true);
|
|
85
|
+
const { pathname, search } = parsedUrl;
|
|
86
|
+
|
|
87
|
+
// Localhost guard — block non-public routes from remote/tunnel
|
|
88
|
+
const isTunnel = !!req.headers["cf-connecting-ip"];
|
|
89
|
+
const isLocal = req.socket.remoteAddress === "127.0.0.1" || req.socket.remoteAddress === "::1";
|
|
90
|
+
if (!isPublic(pathname) && (isTunnel || !isLocal)) {
|
|
91
|
+
jsonErr(res, 403, "Forbidden");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Exact match
|
|
96
|
+
const key = `${req.method}:${pathname}`;
|
|
97
|
+
const route = exactMap.get(key);
|
|
98
|
+
if (route) {
|
|
99
|
+
try {
|
|
100
|
+
await route.handler(req, res, { pathname, query: parsedUrl.query, search });
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.error("Error:", req.url, err);
|
|
103
|
+
jsonErr(res, 500, err.message);
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Prefix match
|
|
109
|
+
for (const pr of prefixRoutes) {
|
|
110
|
+
if ((pr.method === req.method) && pathname.startsWith(pr.prefix)) {
|
|
111
|
+
try {
|
|
112
|
+
await pr.handler(req, res, { pathname, query: parsedUrl.query, search });
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.error("Error:", req.url, err);
|
|
115
|
+
jsonErr(res, 500, err.message);
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Fallback (static files, etc.)
|
|
122
|
+
if (fallback) {
|
|
123
|
+
try {
|
|
124
|
+
await fallback(req, res, { pathname, search });
|
|
125
|
+
} catch (err) {
|
|
126
|
+
console.error("Error:", req.url, err);
|
|
127
|
+
jsonErr(res, 500, err.message);
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
jsonErr(res, 404, "Not found");
|
|
133
|
+
};
|
|
134
|
+
}
|
package/lib/socketio.js
CHANGED
|
@@ -6,7 +6,20 @@ import { homedir } from "os";
|
|
|
6
6
|
import { setupTerminalSocket } from "../features/terminal/terminalSocket.js";
|
|
7
7
|
import { setupRemoteSocket, checkRemoteAvailable } from "../features/remote/remoteSocket.js";
|
|
8
8
|
import { setupFileExplorerSocket } from "../features/fileExplorer/fileExplorerSocket.js";
|
|
9
|
-
import { trackConnection, untrackConnection, pushUiLog, clearOneTimeKey } from "../
|
|
9
|
+
import { trackConnection, untrackConnection, pushUiLog, clearOneTimeKey, pushUiEvent, setRemoteAvailable } from "../api/ui.js";
|
|
10
|
+
import {
|
|
11
|
+
loadApprovedDevices,
|
|
12
|
+
isDeviceApproved,
|
|
13
|
+
isDevicePending,
|
|
14
|
+
approveDevice,
|
|
15
|
+
addPendingApproval,
|
|
16
|
+
removePendingApproval,
|
|
17
|
+
getPendingApproval,
|
|
18
|
+
markDeviceRejected,
|
|
19
|
+
isDeviceRejected,
|
|
20
|
+
updateRejectedSocket,
|
|
21
|
+
clearRejectedDevice
|
|
22
|
+
} from "./deviceApproval.js";
|
|
10
23
|
|
|
11
24
|
function loadApiKey() {
|
|
12
25
|
try {
|
|
@@ -24,7 +37,106 @@ export function getIO() {
|
|
|
24
37
|
return ioInstance;
|
|
25
38
|
}
|
|
26
39
|
|
|
40
|
+
/** Setup features on an approved socket */
|
|
41
|
+
function setupSocketFeatures(socket) {
|
|
42
|
+
// Clear one-time key if used
|
|
43
|
+
if (socket.handshake.auth?.tempKey) {
|
|
44
|
+
pushUiLog("One-time key used \u2014 clearing from UI");
|
|
45
|
+
clearOneTimeKey();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Approve a pending socket by socketId */
|
|
50
|
+
export function approveSocketDevice(socketId) {
|
|
51
|
+
const io = ioInstance;
|
|
52
|
+
if (!io) return false;
|
|
53
|
+
|
|
54
|
+
const socket = io.sockets.sockets.get(socketId);
|
|
55
|
+
const pending = getPendingApproval(socketId);
|
|
56
|
+
console.log(`[DEBUG-APPROVE] socketId=${socketId}, socketExists=${!!socket}, pendingExists=${!!pending}`);
|
|
57
|
+
if (!socket || !pending) return false;
|
|
58
|
+
|
|
59
|
+
// Save device as approved; clear any prior rejection
|
|
60
|
+
approveDevice(pending.deviceId);
|
|
61
|
+
removePendingApproval(socketId);
|
|
62
|
+
clearRejectedDevice(pending.deviceId);
|
|
63
|
+
|
|
64
|
+
// Unlock socket + notify client
|
|
65
|
+
socket.data.approved = true;
|
|
66
|
+
console.log(`[DEBUG-APPROVE] Emitting device:approved to ${socketId}`);
|
|
67
|
+
socket.emit("device:approved");
|
|
68
|
+
|
|
69
|
+
// Setup features
|
|
70
|
+
setupSocketFeatures(socket);
|
|
71
|
+
pushUiLog(`Device approved: ${pending.deviceId.slice(0, 8)}...`);
|
|
72
|
+
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Approve a previously-rejected device by deviceId (from Clients list) */
|
|
77
|
+
export function approveRejectedDevice(deviceId) {
|
|
78
|
+
const io = ioInstance;
|
|
79
|
+
if (!io || !deviceId) return false;
|
|
80
|
+
|
|
81
|
+
approveDevice(deviceId);
|
|
82
|
+
clearRejectedDevice(deviceId);
|
|
83
|
+
|
|
84
|
+
// Notify any active socket for this device
|
|
85
|
+
for (const socket of io.sockets.sockets.values()) {
|
|
86
|
+
if (socket.handshake.auth?.deviceId === deviceId) {
|
|
87
|
+
socket.data.approved = true;
|
|
88
|
+
socket.emit("device:approved");
|
|
89
|
+
setupSocketFeatures(socket);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
pushUiLog(`Device approved from pending: ${deviceId.slice(0, 8)}...`);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Disconnect all active sockets belonging to a deviceId (device stays approved) */
|
|
97
|
+
export function disconnectDeviceSockets(deviceId) {
|
|
98
|
+
const io = ioInstance;
|
|
99
|
+
if (!io || !deviceId) return 0;
|
|
100
|
+
let count = 0;
|
|
101
|
+
for (const socket of io.sockets.sockets.values()) {
|
|
102
|
+
if (socket.handshake.auth?.deviceId === deviceId) {
|
|
103
|
+
socket.disconnect(true);
|
|
104
|
+
count++;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (count) pushUiLog(`Disconnected ${count} socket(s) for device ${deviceId.slice(0, 8)}...`);
|
|
108
|
+
return count;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Reject a pending socket by socketId (remember deviceId in RAM as pending) */
|
|
112
|
+
export function rejectSocketDevice(socketId) {
|
|
113
|
+
const io = ioInstance;
|
|
114
|
+
if (!io) return false;
|
|
115
|
+
|
|
116
|
+
const pending = getPendingApproval(socketId);
|
|
117
|
+
const socket = io.sockets.sockets.get(socketId);
|
|
118
|
+
removePendingApproval(socketId);
|
|
119
|
+
|
|
120
|
+
// Remember rejection in RAM so it shows up in Clients list as pending
|
|
121
|
+
if (pending?.deviceId) {
|
|
122
|
+
markDeviceRejected(pending.deviceId, { ip: pending.ip, socketId });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (socket) {
|
|
126
|
+
socket.emit("device:rejected");
|
|
127
|
+
socket.disconnect(true);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Notify UI to refresh pending/approved list
|
|
131
|
+
pushUiEvent("deviceApproval", { action: "refresh" });
|
|
132
|
+
pushUiLog(`Device rejected: ${pending?.deviceId?.slice(0, 8) || "unknown"}...`);
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
27
136
|
export async function setupSocketIO(server) {
|
|
137
|
+
// Load approved devices from disk
|
|
138
|
+
loadApprovedDevices();
|
|
139
|
+
|
|
28
140
|
const io = new Server(server, {
|
|
29
141
|
cors: {
|
|
30
142
|
origin: "*",
|
|
@@ -36,41 +148,72 @@ export async function setupSocketIO(server) {
|
|
|
36
148
|
allowEIO3: true,
|
|
37
149
|
allowUpgrades: true,
|
|
38
150
|
pingTimeout: 60000,
|
|
39
|
-
pingInterval: 25000
|
|
151
|
+
pingInterval: 25000,
|
|
152
|
+
maxHttpBufferSize: 1e8
|
|
40
153
|
});
|
|
41
154
|
|
|
42
|
-
// Verify apiKey on every Socket.IO connection
|
|
43
|
-
// io.use((socket, next) => {
|
|
44
|
-
// const serverKey = loadApiKey();
|
|
45
|
-
// console.log("🚀 ~ setupSocketIO ~ serverKey:", serverKey)
|
|
46
|
-
// // If no key configured yet (first run), allow through
|
|
47
|
-
// if (!serverKey) return next();
|
|
48
|
-
// const clientKey = socket.handshake.auth?.apiKey;
|
|
49
|
-
// console.log("🚀 ~ setupSocketIO ~ clientKey:", clientKey)
|
|
50
|
-
// if (clientKey === serverKey) return next();
|
|
51
|
-
// next(new Error("unauthorized"));
|
|
52
|
-
// });
|
|
53
|
-
|
|
54
155
|
// Check remote availability at startup
|
|
55
|
-
await checkRemoteAvailable();
|
|
156
|
+
const hasRemote = await checkRemoteAvailable();
|
|
157
|
+
setRemoteAvailable(hasRemote);
|
|
56
158
|
|
|
57
|
-
// Track connections
|
|
159
|
+
// Track connections + device approval
|
|
58
160
|
io.on("connection", (socket) => {
|
|
59
161
|
const ip = socket.handshake.headers["x-forwarded-for"] || socket.handshake.address || "unknown";
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
162
|
+
const deviceId = socket.handshake.auth?.deviceId || null;
|
|
163
|
+
|
|
164
|
+
// Block all events from unapproved sockets (except device:clientReady)
|
|
165
|
+
socket.data.approved = false;
|
|
166
|
+
socket.use((packet, next) => {
|
|
167
|
+
if (socket.data.approved) return next();
|
|
168
|
+
const event = packet[0];
|
|
169
|
+
if (event === "device:clientReady" || event === "disconnect") return next();
|
|
170
|
+
return next(new Error("Device not approved"));
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
trackConnection(socket.id, ip, deviceId);
|
|
174
|
+
pushUiLog(`Client connected: ${ip} (device: ${deviceId?.slice(0, 8) || "none"})`);
|
|
69
175
|
|
|
70
176
|
socket.on("disconnect", (reason) => {
|
|
71
177
|
untrackConnection(socket.id);
|
|
178
|
+
removePendingApproval(socket.id);
|
|
72
179
|
pushUiLog(`Client disconnected: ${ip} (${reason})`);
|
|
73
180
|
});
|
|
181
|
+
|
|
182
|
+
// Check device approval
|
|
183
|
+
if (deviceId && isDeviceApproved(deviceId)) {
|
|
184
|
+
// Known device — allow immediately
|
|
185
|
+
pushUiLog(`Device recognized: ${deviceId.slice(0, 8)}...`);
|
|
186
|
+
socket.data.approved = true;
|
|
187
|
+
setupSocketFeatures(socket);
|
|
188
|
+
} else if (deviceId && isDeviceRejected(deviceId)) {
|
|
189
|
+
// Previously rejected — keep socket unapproved, no modal, update socketId for later approve
|
|
190
|
+
updateRejectedSocket(deviceId, socket.id, ip);
|
|
191
|
+
pushUiLog(`Rejected device reconnected: ${deviceId.slice(0, 8)} — waiting in Clients list`);
|
|
192
|
+
socket.emit("device:rejected");
|
|
193
|
+
pushUiEvent("deviceApproval", { action: "refresh" });
|
|
194
|
+
} else {
|
|
195
|
+
// Unknown device — hold and request approval
|
|
196
|
+
pushUiLog(`Unknown device: ${deviceId?.slice(0, 8) || "no-id"} — waiting for approval`);
|
|
197
|
+
// Skip if same deviceId already pending (client reconnected)
|
|
198
|
+
if (isDevicePending(deviceId)) {
|
|
199
|
+
pushUiLog(`Device ${deviceId?.slice(0, 8)} already pending, ignoring duplicate`);
|
|
200
|
+
socket.disconnect(true);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
addPendingApproval(socket.id, { deviceId, ip });
|
|
205
|
+
|
|
206
|
+
// Wait for client to signal ready before emitting approval request
|
|
207
|
+
socket.once("device:clientReady", () => {
|
|
208
|
+
socket.emit("device:pendingApproval");
|
|
209
|
+
pushUiEvent("deviceApproval", {
|
|
210
|
+
socketId: socket.id,
|
|
211
|
+
deviceId,
|
|
212
|
+
ip,
|
|
213
|
+
action: "pending"
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
}
|
|
74
217
|
});
|
|
75
218
|
|
|
76
219
|
// Setup Terminal + Remote on same root namespace
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "9remote",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Remote terminal access from anywhere",
|
|
6
6
|
"main": "index.js",
|
|
@@ -31,18 +31,22 @@
|
|
|
31
31
|
],
|
|
32
32
|
"license": "MIT",
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"@julusian/jpeg-turbo": "^2.3.0",
|
|
34
35
|
"bufferutil": "^4.1.0",
|
|
35
36
|
"chalk": "^5.4.1",
|
|
37
|
+
"edge-tts-node": "^1.5.7",
|
|
36
38
|
"http-proxy": "^1.18.1",
|
|
37
39
|
"inquirer": "^9.3.8",
|
|
38
40
|
"node-datachannel": "^0.32.1",
|
|
39
41
|
"node-machine-id": "^1.1.12",
|
|
42
|
+
"node-screenshots": "^0.2.8",
|
|
40
43
|
"ora": "^8.1.1",
|
|
41
44
|
"preact": "^10.26.2",
|
|
42
45
|
"qrcode": "^1.5.4",
|
|
43
46
|
"qrcode-terminal": "^0.12.0",
|
|
44
47
|
"sharp": "^0.33.5",
|
|
45
48
|
"socket.io": "^4.8.3",
|
|
49
|
+
"systray": "^1.0.5",
|
|
46
50
|
"utf-8-validate": "^6.0.6",
|
|
47
51
|
"web-push": "^3.6.7"
|
|
48
52
|
},
|
|
@@ -58,6 +62,7 @@
|
|
|
58
62
|
"postcss": "^8.5.3",
|
|
59
63
|
"tailwindcss": "^3.4.17",
|
|
60
64
|
"vite": "^6.2.2",
|
|
65
|
+
"vite-plugin-javascript-obfuscator": "^3.1.0",
|
|
61
66
|
"wait-on": "^9.0.4"
|
|
62
67
|
}
|
|
63
68
|
}
|