9remote 0.1.63 → 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.
@@ -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 "../index.js";
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 for UI display + log
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
- trackConnection(socket.id, ip);
61
- pushUiLog(`Client connected: ${ip}`);
62
-
63
- // If client connected using a one-time key, clear it from UI state
64
- pushUiLog(`Auth received: tempKey=${socket.handshake.auth?.tempKey ?? "null"}`);
65
- if (socket.handshake.auth?.tempKey) {
66
- pushUiLog(`One-time key used — clearing from UI`);
67
- clearOneTimeKey();
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.63",
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
  }