@miraj181/ipingyou 2.0.13 → 2.0.14
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/package.json +1 -1
- package/src/lib/path-browser.js +24 -2
- package/src/modes/client.js +41 -6
- package/src/modes/host.js +1 -1
package/package.json
CHANGED
package/src/lib/path-browser.js
CHANGED
|
@@ -6,6 +6,15 @@ import path from 'node:path';
|
|
|
6
6
|
import os from 'node:os';
|
|
7
7
|
import { buildSshArgs, formatRemoteCd } from './ssh.js';
|
|
8
8
|
|
|
9
|
+
class RemoteDirectoryError extends Error {
|
|
10
|
+
constructor(message, remoteDir, stderr = '') {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'RemoteDirectoryError';
|
|
13
|
+
this.remoteDir = remoteDir;
|
|
14
|
+
this.stderr = stderr;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
9
18
|
function expandLocalPath(inputPath) {
|
|
10
19
|
const trimmed = inputPath.trim();
|
|
11
20
|
if (trimmed === '~') return os.homedir();
|
|
@@ -108,12 +117,13 @@ async function listRemoteDirectory(username, hostname, privateKeyPath, remoteDir
|
|
|
108
117
|
sshArgs.push(`${username}@${hostname}`, command);
|
|
109
118
|
|
|
110
119
|
const result = await execa('ssh', sshArgs, {
|
|
111
|
-
stdio: ['inherit', 'pipe', '
|
|
120
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
112
121
|
reject: false,
|
|
113
122
|
});
|
|
114
123
|
|
|
115
124
|
if (result.exitCode !== 0) {
|
|
116
|
-
|
|
125
|
+
const detail = result.stderr.trim() || `ssh exited with code ${result.exitCode}`;
|
|
126
|
+
throw new RemoteDirectoryError(`Remote directory listing failed: ${detail}`, remoteDir, result.stderr);
|
|
117
127
|
}
|
|
118
128
|
|
|
119
129
|
const lines = result.stdout.split(/\r?\n/).filter(Boolean);
|
|
@@ -176,6 +186,18 @@ export async function promptRemotePath(username, hostname, privateKeyPath, purpo
|
|
|
176
186
|
listing = await listRemoteDirectory(username, hostname, privateKeyPath, currentDir);
|
|
177
187
|
} catch (err) {
|
|
178
188
|
console.log(chalk.yellow(` ⚠️ Could not browse host files: ${err.message}`));
|
|
189
|
+
if (/Operation not permitted/i.test(err.stderr || err.message)) {
|
|
190
|
+
console.log(chalk.dim(' macOS privacy blocked this SSH session from listing that folder.'));
|
|
191
|
+
console.log(chalk.dim(' On the host, allow Full Disk Access for sshd/Remote Login, or choose a non-protected folder.'));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const parentDir = currentDir === '/' ? '/' : path.posix.dirname(currentDir);
|
|
195
|
+
if (parentDir && parentDir !== currentDir) {
|
|
196
|
+
console.log(chalk.dim(` Returning to ${parentDir}.`));
|
|
197
|
+
currentDir = parentDir;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
179
201
|
console.log(chalk.dim(' Falling back to manual host path entry.'));
|
|
180
202
|
const { remotePath } = await inquirer.prompt([{
|
|
181
203
|
type: 'input',
|
package/src/modes/client.js
CHANGED
|
@@ -42,6 +42,28 @@ async function promptUsername() {
|
|
|
42
42
|
return username.trim();
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
function normalizePrivateKey(privateKey) {
|
|
46
|
+
const normalized = String(privateKey || '').replace(/\\n/g, '\n').replace(/\r\n/g, '\n');
|
|
47
|
+
return normalized.endsWith('\n') ? normalized : `${normalized}\n`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function writeEphemeralPrivateKey(privateKey) {
|
|
51
|
+
const keyPath = path.join(os.tmpdir(), `ipingyou_client_${Date.now()}`);
|
|
52
|
+
fs.writeFileSync(keyPath, normalizePrivateKey(privateKey), { mode: 0o600 });
|
|
53
|
+
|
|
54
|
+
const result = await execa('ssh-keygen', ['-y', '-f', keyPath], {
|
|
55
|
+
reject: false,
|
|
56
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (result.exitCode !== 0) {
|
|
60
|
+
try { fs.unlinkSync(keyPath); } catch {}
|
|
61
|
+
throw new Error(result.stderr.trim() || 'OpenSSH could not parse the host-provided private key');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return keyPath;
|
|
65
|
+
}
|
|
66
|
+
|
|
45
67
|
/**
|
|
46
68
|
* Start SSH connection through the Cloudflare tunnel.
|
|
47
69
|
*/
|
|
@@ -162,6 +184,7 @@ async function performSCP(username, hostname, direction, privateKeyPath) {
|
|
|
162
184
|
if (direction === 'upload' && localHash) {
|
|
163
185
|
console.log(chalk.dim(' 🔍 Verifying remote SHA-256 checksum...'));
|
|
164
186
|
try {
|
|
187
|
+
const remoteChecksumPath = joinRemotePath(remotePath, path.basename(localPath));
|
|
165
188
|
const sshArgs = [
|
|
166
189
|
'-o', `ProxyCommand=${proxyCommand}`,
|
|
167
190
|
'-o', 'StrictHostKeyChecking=accept-new',
|
|
@@ -169,7 +192,7 @@ async function performSCP(username, hostname, direction, privateKeyPath) {
|
|
|
169
192
|
...getSshControlOptions(hostname)
|
|
170
193
|
];
|
|
171
194
|
if (privateKeyPath) sshArgs.push('-i', privateKeyPath, '-o', 'IdentityAgent=none');
|
|
172
|
-
sshArgs.push(`${username}@${hostname}`, `shasum -a 256 ${quoteRemoteShell(remotePath)} || sha256sum ${quoteRemoteShell(remotePath)}`);
|
|
195
|
+
sshArgs.push(`${username}@${hostname}`, `shasum -a 256 ${quoteRemoteShell(remoteChecksumPath)} 2>/dev/null || sha256sum ${quoteRemoteShell(remoteChecksumPath)} 2>/dev/null || shasum -a 256 ${quoteRemoteShell(remotePath)} 2>/dev/null || sha256sum ${quoteRemoteShell(remotePath)}`);
|
|
173
196
|
|
|
174
197
|
const { stdout } = await execa('ssh', sshArgs, { reject: false });
|
|
175
198
|
const remoteHash = stdout.split(' ')[0].trim();
|
|
@@ -198,6 +221,13 @@ async function performSCP(username, hostname, direction, privateKeyPath) {
|
|
|
198
221
|
}
|
|
199
222
|
}
|
|
200
223
|
|
|
224
|
+
function joinRemotePath(parent, child) {
|
|
225
|
+
const cleanParent = String(parent || '').replace(/\/+$/, '');
|
|
226
|
+
if (!cleanParent) return child;
|
|
227
|
+
if (cleanParent === '/') return `/${child}`;
|
|
228
|
+
return `${cleanParent}/${child}`;
|
|
229
|
+
}
|
|
230
|
+
|
|
201
231
|
/**
|
|
202
232
|
* Main Client Mode entry point.
|
|
203
233
|
*/
|
|
@@ -334,11 +364,16 @@ export async function startClientMode(options = {}) {
|
|
|
334
364
|
let privateKeyPath = null;
|
|
335
365
|
if (payload.privateKey) {
|
|
336
366
|
console.log(chalk.green(' 🔑 Host provided an ephemeral SSH key for passwordless entry!'));
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
367
|
+
try {
|
|
368
|
+
privateKeyPath = await writeEphemeralPrivateKey(payload.privateKey);
|
|
369
|
+
addCleanupHook(() => {
|
|
370
|
+
try { fs.unlinkSync(privateKeyPath); } catch {}
|
|
371
|
+
});
|
|
372
|
+
} catch (err) {
|
|
373
|
+
console.log(chalk.yellow(` ⚠️ Could not use ephemeral SSH key: ${err.message}`));
|
|
374
|
+
console.log(chalk.dim(' Falling back to standard OS password.'));
|
|
375
|
+
privateKeyPath = null;
|
|
376
|
+
}
|
|
342
377
|
}
|
|
343
378
|
|
|
344
379
|
// Ask to save alias if we entered manually
|
package/src/modes/host.js
CHANGED
|
@@ -154,7 +154,7 @@ async function generateEphemeralKey() {
|
|
|
154
154
|
|
|
155
155
|
await execa('ssh-keygen', ['-t', 'ed25519', '-C', 'ipingyou-ephemeral', '-f', keyPath, '-N', '']);
|
|
156
156
|
|
|
157
|
-
const privKey =
|
|
157
|
+
const privKey = await fs.promises.readFile(keyPath, 'utf8');
|
|
158
158
|
const pubKey = (await fs.promises.readFile(`${keyPath}.pub`, 'utf8')).trim();
|
|
159
159
|
|
|
160
160
|
return { keyPath, privKey, pubKey };
|