@ebowwa/terminal 0.2.1 → 0.3.0
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/dist/index.js +6855 -28130
- package/dist/mcp/index.js +7244 -27317
- package/package.json +1 -1
- package/src/api.js +0 -861
- package/src/client.js +0 -92
- package/src/config.js +0 -490
- package/src/error.js +0 -32
- package/src/exec.js +0 -183
- package/src/files.js +0 -521
- package/src/fingerprint.js +0 -336
- package/src/index.js +0 -127
- package/src/manager.js +0 -358
- package/src/mcp/index.js +0 -555
- package/src/mcp/stdio.js +0 -840
- package/src/network-error-detector.js +0 -101
- package/src/pool.js +0 -840
- package/src/pty.js +0 -344
- package/src/resources.js +0 -64
- package/src/scp.js +0 -166
- package/src/sessions.js +0 -895
- package/src/tmux-exec.js +0 -169
- package/src/tmux-local.js +0 -937
- package/src/tmux-manager.js +0 -1026
- package/src/tmux.js +0 -826
- package/src/types.js +0 -5
package/src/files.js
DELETED
|
@@ -1,521 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Remote file operations via SSH
|
|
4
|
-
*/
|
|
5
|
-
var __extends = (this && this.__extends) || (function () {
|
|
6
|
-
var extendStatics = function (d, b) {
|
|
7
|
-
extendStatics = Object.setPrototypeOf ||
|
|
8
|
-
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
|
9
|
-
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
|
|
10
|
-
return extendStatics(d, b);
|
|
11
|
-
};
|
|
12
|
-
return function (d, b) {
|
|
13
|
-
if (typeof b !== "function" && b !== null)
|
|
14
|
-
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
|
|
15
|
-
extendStatics(d, b);
|
|
16
|
-
function __() { this.constructor = d; }
|
|
17
|
-
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
|
18
|
-
};
|
|
19
|
-
})();
|
|
20
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
21
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
22
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
23
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
24
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
25
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
26
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
27
|
-
});
|
|
28
|
-
};
|
|
29
|
-
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
30
|
-
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
|
31
|
-
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
32
|
-
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
33
|
-
function step(op) {
|
|
34
|
-
if (f) throw new TypeError("Generator is already executing.");
|
|
35
|
-
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
36
|
-
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
37
|
-
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
38
|
-
switch (op[0]) {
|
|
39
|
-
case 0: case 1: t = op; break;
|
|
40
|
-
case 4: _.label++; return { value: op[1], done: false };
|
|
41
|
-
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
42
|
-
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
43
|
-
default:
|
|
44
|
-
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
45
|
-
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
46
|
-
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
47
|
-
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
48
|
-
if (t[2]) _.ops.pop();
|
|
49
|
-
_.trys.pop(); continue;
|
|
50
|
-
}
|
|
51
|
-
op = body.call(thisArg, _);
|
|
52
|
-
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
53
|
-
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
|
-
exports.PathTraversalError = void 0;
|
|
58
|
-
exports.getSecurityEvents = getSecurityEvents;
|
|
59
|
-
exports.clearSecurityEvents = clearSecurityEvents;
|
|
60
|
-
exports.sanitizePath = sanitizePath;
|
|
61
|
-
exports.listFiles = listFiles;
|
|
62
|
-
exports.previewFile = previewFile;
|
|
63
|
-
var client_js_1 = require("./client.js");
|
|
64
|
-
var error_js_1 = require("./error.js");
|
|
65
|
-
/**
|
|
66
|
-
* Path traversal security error
|
|
67
|
-
*/
|
|
68
|
-
var PathTraversalError = /** @class */ (function (_super) {
|
|
69
|
-
__extends(PathTraversalError, _super);
|
|
70
|
-
function PathTraversalError(message, attemptedPath, reason) {
|
|
71
|
-
var _this = _super.call(this, message) || this;
|
|
72
|
-
_this.attemptedPath = attemptedPath;
|
|
73
|
-
_this.reason = reason;
|
|
74
|
-
_this.name = "PathTraversalError";
|
|
75
|
-
return _this;
|
|
76
|
-
}
|
|
77
|
-
return PathTraversalError;
|
|
78
|
-
}(error_js_1.SSHError));
|
|
79
|
-
exports.PathTraversalError = PathTraversalError;
|
|
80
|
-
var securityEvents = [];
|
|
81
|
-
var MAX_SECURITY_EVENTS = 1000;
|
|
82
|
-
/**
|
|
83
|
-
* Log a security event
|
|
84
|
-
*/
|
|
85
|
-
function logSecurityEvent(attemptedPath, reason, severity) {
|
|
86
|
-
var event = {
|
|
87
|
-
timestamp: new Date().toISOString(),
|
|
88
|
-
attemptedPath: attemptedPath,
|
|
89
|
-
reason: reason,
|
|
90
|
-
severity: severity,
|
|
91
|
-
};
|
|
92
|
-
securityEvents.push(event);
|
|
93
|
-
// Keep only recent events
|
|
94
|
-
if (securityEvents.length > MAX_SECURITY_EVENTS) {
|
|
95
|
-
securityEvents.shift();
|
|
96
|
-
}
|
|
97
|
-
// Log to console with appropriate severity
|
|
98
|
-
var logPrefix = {
|
|
99
|
-
blocked: "[SECURITY BLOCKED]",
|
|
100
|
-
suspicious: "[SECURITY SUSPICIOUS]",
|
|
101
|
-
warning: "[SECURITY WARNING]",
|
|
102
|
-
}[severity];
|
|
103
|
-
console.error("".concat(logPrefix, " Path traversal attempt detected:"), JSON.stringify(event));
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Get recent security events for monitoring
|
|
107
|
-
*/
|
|
108
|
-
function getSecurityEvents(limit) {
|
|
109
|
-
if (limit === void 0) { limit = 50; }
|
|
110
|
-
return securityEvents.slice(-limit);
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Clear old security events (for maintenance)
|
|
114
|
-
*/
|
|
115
|
-
function clearSecurityEvents(olderThanMs) {
|
|
116
|
-
if (olderThanMs === void 0) { olderThanMs = 24 * 60 * 60 * 1000; }
|
|
117
|
-
var cutoff = Date.now() - olderThanMs;
|
|
118
|
-
var initialLength = securityEvents.length;
|
|
119
|
-
for (var i = securityEvents.length - 1; i >= 0; i--) {
|
|
120
|
-
var eventTime = new Date(securityEvents[i].timestamp).getTime();
|
|
121
|
-
if (eventTime < cutoff) {
|
|
122
|
-
securityEvents.splice(i, 1);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return initialLength - securityEvents.length;
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Normalize a path by resolving . and removing redundant slashes
|
|
129
|
-
* Does NOT resolve .. (those are checked separately)
|
|
130
|
-
*/
|
|
131
|
-
function normalizePath(path) {
|
|
132
|
-
// Remove redundant slashes
|
|
133
|
-
var normalized = path.replace(/\/+/g, "/");
|
|
134
|
-
// Remove trailing slash (unless it's just "/")
|
|
135
|
-
if (normalized.length > 1 && normalized.endsWith("/")) {
|
|
136
|
-
normalized = normalized.slice(0, -1);
|
|
137
|
-
}
|
|
138
|
-
// Resolve single dots (current directory)
|
|
139
|
-
normalized = normalized.replace(/\/\.\//g, "/").replace(/\/\.$/, "");
|
|
140
|
-
return normalized;
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* Check if a path contains parent directory references
|
|
144
|
-
*/
|
|
145
|
-
function hasParentDirReference(path) {
|
|
146
|
-
// Check for .. in path components
|
|
147
|
-
var parts = path.split("/");
|
|
148
|
-
return parts.some(function (part) { return part === ".." || part.includes("\\.."); });
|
|
149
|
-
}
|
|
150
|
-
/**
|
|
151
|
-
* Check if path attempts to escape using null bytes
|
|
152
|
-
*/
|
|
153
|
-
function hasNullByte(path) {
|
|
154
|
-
return path.includes("\0");
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Calculate path depth (number of directories)
|
|
158
|
-
*/
|
|
159
|
-
function calculatePathDepth(path) {
|
|
160
|
-
return path.split("/").filter(function (p) { return p.length > 0; }).length;
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Validate that a resolved path stays within allowed base directories
|
|
164
|
-
*/
|
|
165
|
-
function validatePathInAllowedDirs(resolvedPath, allowedDirs) {
|
|
166
|
-
// Normalize all paths for comparison
|
|
167
|
-
var normalizedResolved = normalizePath(resolvedPath);
|
|
168
|
-
var normalizedAllowed = allowedDirs.map(normalizePath);
|
|
169
|
-
// Check if resolved path starts with any allowed directory
|
|
170
|
-
return normalizedAllowed.some(function (allowedDir) {
|
|
171
|
-
// Ensure allowed directory ends with / for proper prefix matching
|
|
172
|
-
var prefix = allowedDir.endsWith("/") ? allowedDir : allowedDir + "/";
|
|
173
|
-
return (normalizedResolved === allowedDir || normalizedResolved.startsWith(prefix));
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* Resolve a path relative to a base directory
|
|
178
|
-
* Throws if the result would escape the base directory
|
|
179
|
-
*/
|
|
180
|
-
function resolveRelativePath(baseDir, inputPath) {
|
|
181
|
-
var normalizedBase = normalizePath(baseDir);
|
|
182
|
-
var normalizedInput = normalizePath(inputPath);
|
|
183
|
-
// If input is absolute, reject (unless allowed by options)
|
|
184
|
-
if (normalizedInput.startsWith("/")) {
|
|
185
|
-
throw new Error("Absolute paths are not allowed");
|
|
186
|
-
}
|
|
187
|
-
// Build full path
|
|
188
|
-
var fullPath = normalizedBase + "/" + normalizedInput;
|
|
189
|
-
fullPath = normalizePath(fullPath);
|
|
190
|
-
// Check for parent directory references in the full path
|
|
191
|
-
if (hasParentDirReference(fullPath)) {
|
|
192
|
-
throw new Error("Path contains parent directory references");
|
|
193
|
-
}
|
|
194
|
-
// Verify the path is still within base directory
|
|
195
|
-
if (!fullPath.startsWith(normalizedBase + "/") && fullPath !== normalizedBase) {
|
|
196
|
-
throw new Error("Path escapes allowed directory");
|
|
197
|
-
}
|
|
198
|
-
return fullPath;
|
|
199
|
-
}
|
|
200
|
-
/**
|
|
201
|
-
* Sanitize and validate a file path for security
|
|
202
|
-
*
|
|
203
|
-
* This function prevents path traversal attacks by:
|
|
204
|
-
* 1. Rejecting paths with .. components
|
|
205
|
-
* 2. Validating against allowed base directories
|
|
206
|
-
* 3. Normalizing paths to remove . and redundant /
|
|
207
|
-
* 4. Checking for null bytes and other escape sequences
|
|
208
|
-
* 5. Limiting path depth to prevent deep traversal
|
|
209
|
-
*
|
|
210
|
-
* @param inputPath - The user-provided path to sanitize
|
|
211
|
-
* @param options - Sanitization options
|
|
212
|
-
* @returns Sanitized absolute path
|
|
213
|
-
* @throws PathTraversalError if path is suspicious or invalid
|
|
214
|
-
*
|
|
215
|
-
* @example
|
|
216
|
-
* ```ts
|
|
217
|
-
* // Safe: within /root
|
|
218
|
-
* sanitizePath("project/file.txt", { user: "root" })
|
|
219
|
-
* // Returns: "/root/project/file.txt"
|
|
220
|
-
*
|
|
221
|
-
* // BLOCKED: attempts to escape
|
|
222
|
-
* sanitizePath("../../../etc/passwd", { user: "root" })
|
|
223
|
-
* // Throws: PathTraversalError
|
|
224
|
-
*
|
|
225
|
-
* // BLOCKED: null byte injection
|
|
226
|
-
* sanitizePath("file.txt\0../../../etc/passwd", { user: "root" })
|
|
227
|
-
* // Throws: PathTraversalError
|
|
228
|
-
* ```
|
|
229
|
-
*/
|
|
230
|
-
function sanitizePath(inputPath, options) {
|
|
231
|
-
if (options === void 0) { options = {}; }
|
|
232
|
-
var allowedBaseDirs = options.allowedBaseDirs, _a = options.user, user = _a === void 0 ? "root" : _a, _b = options.allowAbsolutePaths, allowAbsolutePaths = _b === void 0 ? false : _b, _c = options.maxDepth, maxDepth = _c === void 0 ? 20 : _c, _d = options.logSuspicious, logSuspicious = _d === void 0 ? true : _d;
|
|
233
|
-
// Determine default allowed base directories based on user
|
|
234
|
-
var defaultAllowedDirs = user === "root"
|
|
235
|
-
? ["/root"]
|
|
236
|
-
: ["/home/".concat(user), "/tmp"];
|
|
237
|
-
var finalAllowedDirs = allowedBaseDirs || defaultAllowedDirs;
|
|
238
|
-
// ===== SECURITY CHECKS =====
|
|
239
|
-
// 1. Check for null byte injection (CWE-158)
|
|
240
|
-
if (hasNullByte(inputPath)) {
|
|
241
|
-
var error = "Path contains null byte (possible injection attempt)";
|
|
242
|
-
if (logSuspicious) {
|
|
243
|
-
logSecurityEvent(inputPath, "Null byte injection detected", "blocked");
|
|
244
|
-
}
|
|
245
|
-
throw new PathTraversalError("Invalid path: contains null byte", inputPath, error);
|
|
246
|
-
}
|
|
247
|
-
// 2. Check for parent directory references (CWE-22)
|
|
248
|
-
if (hasParentDirReference(inputPath)) {
|
|
249
|
-
var error = "Path contains parent directory reference (..)";
|
|
250
|
-
if (logSuspicious) {
|
|
251
|
-
logSecurityEvent(inputPath, "Path traversal attempt detected", "blocked");
|
|
252
|
-
}
|
|
253
|
-
throw new PathTraversalError("Path traversal blocked: parent directory references not allowed", inputPath, error);
|
|
254
|
-
}
|
|
255
|
-
// 3. Check for backslashes (Windows path separator, potential escape)
|
|
256
|
-
if (inputPath.includes("\\")) {
|
|
257
|
-
var error = "Path contains backslashes";
|
|
258
|
-
if (logSuspicious) {
|
|
259
|
-
logSecurityEvent(inputPath, "Backslash in path detected", "suspicious");
|
|
260
|
-
}
|
|
261
|
-
throw new PathTraversalError("Invalid path: backslashes not allowed", inputPath, error);
|
|
262
|
-
}
|
|
263
|
-
// 4. Check for URL-encoded characters (possible bypass attempt)
|
|
264
|
-
if (/%2e|%2f|%5c/i.test(inputPath)) {
|
|
265
|
-
var error = "Path contains URL-encoded characters";
|
|
266
|
-
if (logSuspicious) {
|
|
267
|
-
logSecurityEvent(inputPath, "URL encoding detected in path", "suspicious");
|
|
268
|
-
}
|
|
269
|
-
throw new PathTraversalError("Invalid path: URL-encoded characters not allowed", inputPath, error);
|
|
270
|
-
}
|
|
271
|
-
// 5. Check path depth
|
|
272
|
-
var pathDepth = calculatePathDepth(inputPath);
|
|
273
|
-
if (pathDepth > maxDepth) {
|
|
274
|
-
var error = "Path depth ".concat(pathDepth, " exceeds maximum ").concat(maxDepth);
|
|
275
|
-
if (logSuspicious) {
|
|
276
|
-
logSecurityEvent(inputPath, error, "suspicious");
|
|
277
|
-
}
|
|
278
|
-
throw new PathTraversalError("Path too deep: possible traversal attempt", inputPath, error);
|
|
279
|
-
}
|
|
280
|
-
// 6. Check for suspicious patterns
|
|
281
|
-
var suspiciousPatterns = [
|
|
282
|
-
/\.\.[\/\\]/, // ../ or ..\
|
|
283
|
-
/\/\.\./, // /..
|
|
284
|
-
/\.\.$/, // ends with ..
|
|
285
|
-
/^\.\./, // starts with ..
|
|
286
|
-
];
|
|
287
|
-
for (var _i = 0, suspiciousPatterns_1 = suspiciousPatterns; _i < suspiciousPatterns_1.length; _i++) {
|
|
288
|
-
var pattern = suspiciousPatterns_1[_i];
|
|
289
|
-
if (pattern.test(inputPath)) {
|
|
290
|
-
var error = "Path matches suspicious pattern: ".concat(pattern);
|
|
291
|
-
if (logSuspicious) {
|
|
292
|
-
logSecurityEvent(inputPath, error, "blocked");
|
|
293
|
-
}
|
|
294
|
-
throw new PathTraversalError("Path blocked: matches suspicious pattern", inputPath, error);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
// ===== PATH RESOLUTION =====
|
|
298
|
-
var sanitizedPath;
|
|
299
|
-
if (inputPath.startsWith("/")) {
|
|
300
|
-
// Handle absolute paths
|
|
301
|
-
if (!allowAbsolutePaths) {
|
|
302
|
-
var error = "Absolute paths are not allowed";
|
|
303
|
-
if (logSuspicious) {
|
|
304
|
-
logSecurityEvent(inputPath, error, "blocked");
|
|
305
|
-
}
|
|
306
|
-
throw new PathTraversalError(error + ": use relative paths only", inputPath, error);
|
|
307
|
-
}
|
|
308
|
-
// Normalize the absolute path
|
|
309
|
-
sanitizedPath = normalizePath(inputPath);
|
|
310
|
-
// Validate against allowed directories
|
|
311
|
-
if (!validatePathInAllowedDirs(sanitizedPath, finalAllowedDirs)) {
|
|
312
|
-
var error = "Absolute path not within allowed directories: ".concat(finalAllowedDirs.join(", "));
|
|
313
|
-
if (logSuspicious) {
|
|
314
|
-
logSecurityEvent(inputPath, error, "blocked");
|
|
315
|
-
}
|
|
316
|
-
throw new PathTraversalError("Access denied: path outside allowed directories", inputPath, error);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
else {
|
|
320
|
-
// Handle relative paths - resolve against each allowed base directory
|
|
321
|
-
// Use the first valid resolution
|
|
322
|
-
var resolved = false;
|
|
323
|
-
for (var _e = 0, finalAllowedDirs_1 = finalAllowedDirs; _e < finalAllowedDirs_1.length; _e++) {
|
|
324
|
-
var baseDir = finalAllowedDirs_1[_e];
|
|
325
|
-
try {
|
|
326
|
-
sanitizedPath = resolveRelativePath(baseDir, inputPath);
|
|
327
|
-
resolved = true;
|
|
328
|
-
break;
|
|
329
|
-
}
|
|
330
|
-
catch (_f) {
|
|
331
|
-
// Try next base directory
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
if (!resolved) {
|
|
336
|
-
var error = "Path cannot be resolved within allowed directories: ".concat(finalAllowedDirs.join(", "));
|
|
337
|
-
if (logSuspicious) {
|
|
338
|
-
logSecurityEvent(inputPath, error, "blocked");
|
|
339
|
-
}
|
|
340
|
-
throw new PathTraversalError("Access denied: path outside allowed directories", inputPath, error);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
// Final validation
|
|
344
|
-
if (!sanitizedPath || sanitizedPath.length === 0) {
|
|
345
|
-
throw new PathTraversalError("Invalid path: resulted in empty path", inputPath, "Empty path after sanitization");
|
|
346
|
-
}
|
|
347
|
-
return sanitizedPath;
|
|
348
|
-
}
|
|
349
|
-
/**
|
|
350
|
-
* List files in a directory on remote server
|
|
351
|
-
* @param path - Directory path to list (default: .)
|
|
352
|
-
* @param options - SSH connection options
|
|
353
|
-
* @returns List of files with metadata
|
|
354
|
-
* @throws PathTraversalError if path attempts to escape allowed directories
|
|
355
|
-
* @throws SSHError if SSH command fails
|
|
356
|
-
*/
|
|
357
|
-
function listFiles() {
|
|
358
|
-
return __awaiter(this, arguments, void 0, function (path, options) {
|
|
359
|
-
var host, _a, user, _b, timeout, sanitizedPath, command, output, lines, files, _i, lines_1, line, parts, permissions, isDir, fileName, filePath, error_1;
|
|
360
|
-
var _c, _d;
|
|
361
|
-
if (path === void 0) { path = "."; }
|
|
362
|
-
return __generator(this, function (_e) {
|
|
363
|
-
switch (_e.label) {
|
|
364
|
-
case 0:
|
|
365
|
-
host = options.host, _a = options.user, user = _a === void 0 ? "root" : _a, _b = options.timeout, timeout = _b === void 0 ? 5 : _b;
|
|
366
|
-
try {
|
|
367
|
-
sanitizedPath = sanitizePath(path, {
|
|
368
|
-
user: user,
|
|
369
|
-
allowAbsolutePaths: false,
|
|
370
|
-
logSuspicious: true,
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
catch (error) {
|
|
374
|
-
if (error instanceof PathTraversalError) {
|
|
375
|
-
// Re-throw path traversal errors with security context
|
|
376
|
-
throw error;
|
|
377
|
-
}
|
|
378
|
-
throw new error_js_1.SSHError("Failed to sanitize path: ".concat(path), error);
|
|
379
|
-
}
|
|
380
|
-
_e.label = 1;
|
|
381
|
-
case 1:
|
|
382
|
-
_e.trys.push([1, 3, , 4]);
|
|
383
|
-
command = "ls -la \"".concat(sanitizedPath, "\" 2>/dev/null || echo \"FAILED\"");
|
|
384
|
-
return [4 /*yield*/, (0, client_js_1.execSSH)(command, { host: host, user: user, timeout: timeout })];
|
|
385
|
-
case 2:
|
|
386
|
-
output = _e.sent();
|
|
387
|
-
if (output === "FAILED" || output === "0") {
|
|
388
|
-
throw new error_js_1.SSHError("Failed to list directory: ".concat(sanitizedPath, " (original: ").concat(path, ")"));
|
|
389
|
-
}
|
|
390
|
-
lines = output.split("\n").slice(1);
|
|
391
|
-
files = [];
|
|
392
|
-
for (_i = 0, lines_1 = lines; _i < lines_1.length; _i++) {
|
|
393
|
-
line = lines_1[_i];
|
|
394
|
-
if (!line.trim())
|
|
395
|
-
continue;
|
|
396
|
-
parts = line.split(/\s+/);
|
|
397
|
-
if (parts.length < 8)
|
|
398
|
-
continue;
|
|
399
|
-
permissions = parts[0];
|
|
400
|
-
isDir = permissions.startsWith("d");
|
|
401
|
-
fileName = ((_c = parts[8]) === null || _c === void 0 ? void 0 : _c.split(" ->")[0]) || parts[8];
|
|
402
|
-
filePath = "".concat(sanitizedPath, "/").concat(fileName).replace(/\/\//g, "/");
|
|
403
|
-
files.push({
|
|
404
|
-
name: fileName,
|
|
405
|
-
path: filePath,
|
|
406
|
-
size: isDir ? "-" : parts[4],
|
|
407
|
-
modified: parts[5] +
|
|
408
|
-
" " +
|
|
409
|
-
parts[6] +
|
|
410
|
-
" " +
|
|
411
|
-
((_d = parts[7]) === null || _d === void 0 ? void 0 : _d.split(".").slice(0, 2).join(":")) || "",
|
|
412
|
-
type: isDir ? "directory" : "file",
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
return [2 /*return*/, files];
|
|
416
|
-
case 3:
|
|
417
|
-
error_1 = _e.sent();
|
|
418
|
-
throw new error_js_1.SSHError("Failed to list files: ".concat(path), error_1);
|
|
419
|
-
case 4: return [2 /*return*/];
|
|
420
|
-
}
|
|
421
|
-
});
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
/**
|
|
425
|
-
* Preview a file's content from remote server
|
|
426
|
-
* @param filePath - Path to the file to preview
|
|
427
|
-
* @param options - SSH connection options
|
|
428
|
-
* @returns File content for preview
|
|
429
|
-
* @throws PathTraversalError if path attempts to escape allowed directories
|
|
430
|
-
* @throws SSHError if SSH command fails
|
|
431
|
-
*/
|
|
432
|
-
function previewFile(filePath, options) {
|
|
433
|
-
return __awaiter(this, void 0, void 0, function () {
|
|
434
|
-
var host, _a, user, _b, timeout, absolutePath, checkCmd, checkOutput, readCmd, content, hasNullBytes, nonPrintableCount, isLikelyBinary, imageExtensions, isImage, maxPreviewSize, trimmedContent, error_2;
|
|
435
|
-
return __generator(this, function (_c) {
|
|
436
|
-
switch (_c.label) {
|
|
437
|
-
case 0:
|
|
438
|
-
host = options.host, _a = options.user, user = _a === void 0 ? "root" : _a, _b = options.timeout, timeout = _b === void 0 ? 10 : _b;
|
|
439
|
-
try {
|
|
440
|
-
absolutePath = sanitizePath(filePath, {
|
|
441
|
-
user: user,
|
|
442
|
-
allowAbsolutePaths: false,
|
|
443
|
-
logSuspicious: true,
|
|
444
|
-
});
|
|
445
|
-
}
|
|
446
|
-
catch (error) {
|
|
447
|
-
if (error instanceof PathTraversalError) {
|
|
448
|
-
// Re-throw path traversal errors with security context
|
|
449
|
-
return [2 /*return*/, {
|
|
450
|
-
type: "error",
|
|
451
|
-
error: "Access denied: path outside allowed directories",
|
|
452
|
-
}];
|
|
453
|
-
}
|
|
454
|
-
return [2 /*return*/, { type: "error", error: "Failed to validate file path" }];
|
|
455
|
-
}
|
|
456
|
-
_c.label = 1;
|
|
457
|
-
case 1:
|
|
458
|
-
_c.trys.push([1, 4, , 5]);
|
|
459
|
-
checkCmd = "test -f \"".concat(absolutePath, "\" && echo \"EXISTS\" || echo \"NOFILE\"");
|
|
460
|
-
return [4 /*yield*/, (0, client_js_1.execSSH)(checkCmd, { host: host, user: user, timeout: timeout })];
|
|
461
|
-
case 2:
|
|
462
|
-
checkOutput = _c.sent();
|
|
463
|
-
if (checkOutput !== "EXISTS") {
|
|
464
|
-
return [2 /*return*/, { type: "error", error: "File not found" }];
|
|
465
|
-
}
|
|
466
|
-
readCmd = "cat \"".concat(absolutePath, "\" 2>/dev/null || echo \"READFAIL\"");
|
|
467
|
-
return [4 /*yield*/, (0, client_js_1.execSSH)(readCmd, { host: host, user: user, timeout: 15 })];
|
|
468
|
-
case 3:
|
|
469
|
-
content = _c.sent();
|
|
470
|
-
if (content === "READFAIL" || content === "0") {
|
|
471
|
-
return [2 /*return*/, { type: "error", error: "Failed to read file content" }];
|
|
472
|
-
}
|
|
473
|
-
hasNullBytes = content.includes("\u0000");
|
|
474
|
-
nonPrintableCount = (content.match(/[\x00-\x08\x0E-\x1F]/g) || [])
|
|
475
|
-
.length;
|
|
476
|
-
isLikelyBinary = hasNullBytes ||
|
|
477
|
-
(content.length > 0 && nonPrintableCount / content.length > 0.3);
|
|
478
|
-
imageExtensions = [
|
|
479
|
-
".png",
|
|
480
|
-
".jpg",
|
|
481
|
-
".jpeg",
|
|
482
|
-
".gif",
|
|
483
|
-
".bmp",
|
|
484
|
-
".svg",
|
|
485
|
-
".webp",
|
|
486
|
-
".ico",
|
|
487
|
-
];
|
|
488
|
-
isImage = imageExtensions.some(function (ext) {
|
|
489
|
-
return absolutePath.toLowerCase().endsWith(ext);
|
|
490
|
-
});
|
|
491
|
-
if (isImage) {
|
|
492
|
-
return [2 /*return*/, { type: "image", content: "[Image file - download to view]" }];
|
|
493
|
-
}
|
|
494
|
-
else if (isLikelyBinary) {
|
|
495
|
-
return [2 /*return*/, { type: "binary", content: "[Binary file - download to view]" }];
|
|
496
|
-
}
|
|
497
|
-
else {
|
|
498
|
-
maxPreviewSize = 10000;
|
|
499
|
-
trimmedContent = content.length > maxPreviewSize
|
|
500
|
-
? content.slice(0, maxPreviewSize) + "\n\n... (truncated)"
|
|
501
|
-
: content;
|
|
502
|
-
return [2 /*return*/, {
|
|
503
|
-
type: "text",
|
|
504
|
-
content: trimmedContent,
|
|
505
|
-
}];
|
|
506
|
-
}
|
|
507
|
-
return [3 /*break*/, 5];
|
|
508
|
-
case 4:
|
|
509
|
-
error_2 = _c.sent();
|
|
510
|
-
// Log the error for security auditing
|
|
511
|
-
console.error("[File Preview Error] path=\"".concat(filePath, "\", sanitized=\"").concat(absolutePath, "\", error=").concat(error_2));
|
|
512
|
-
// Return error without exposing internal details
|
|
513
|
-
if (error_2 instanceof error_js_1.SSHError || error_2 instanceof PathTraversalError) {
|
|
514
|
-
return [2 /*return*/, { type: "error", error: "Access denied" }];
|
|
515
|
-
}
|
|
516
|
-
return [2 /*return*/, { type: "error", error: "Failed to preview file" }];
|
|
517
|
-
case 5: return [2 /*return*/];
|
|
518
|
-
}
|
|
519
|
-
});
|
|
520
|
-
});
|
|
521
|
-
}
|