clay-server 4.3.0 → 4.3.1-beta.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/lib/project-file-http.js +56 -0
- package/lib/project-file-path.js +45 -9
- package/lib/project-file-watch.js +17 -23
- package/lib/project-filesystem.js +22 -32
- package/lib/project-http.js +7 -105
- package/lib/project-request-access.js +11 -4
- package/lib/project.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
var fs = require("fs");
|
|
2
|
+
var path = require("path");
|
|
3
|
+
var attachRequestAccess = require("./project-request-access").attachRequestAccess;
|
|
4
|
+
var resolveFilePath = require("./project-file-path").resolveFilePath;
|
|
5
|
+
var fileError = require("./project-file-path").fileError;
|
|
6
|
+
|
|
7
|
+
var MIME_TYPES = {
|
|
8
|
+
".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
|
|
9
|
+
".gif": "image/gif", ".webp": "image/webp", ".svg": "image/svg+xml",
|
|
10
|
+
".bmp": "image/bmp", ".ico": "image/x-icon",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function attachFileHTTP(ctx) {
|
|
14
|
+
var access = ctx.requestAccess || attachRequestAccess(Object.assign({}, ctx, {
|
|
15
|
+
getOsUserInfoForWs: ctx.getOsUserInfoForReq,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
function handleHTTP(req, res, urlPath) {
|
|
19
|
+
var download = urlPath.startsWith("/api/file/download?");
|
|
20
|
+
if (req.method !== "GET" || (!download && !urlPath.startsWith("/api/file?"))) return false;
|
|
21
|
+
try {
|
|
22
|
+
var scope = access.fileScope(req);
|
|
23
|
+
var requested = new URLSearchParams(urlPath.substring(urlPath.indexOf("?"))).get("path");
|
|
24
|
+
var file = resolveFilePath(ctx.cwd, requested, scope);
|
|
25
|
+
var mime = MIME_TYPES[path.extname(file).toLowerCase()];
|
|
26
|
+
if (!download && !mime) {
|
|
27
|
+
res.writeHead(403); res.end("Only image files"); return true;
|
|
28
|
+
}
|
|
29
|
+
var content = scope.identity ? ctx.fsAsUser("read_binary", { file: file }, scope.identity).buffer : fs.readFileSync(file);
|
|
30
|
+
var headers = {
|
|
31
|
+
"Content-Type": download ? "application/octet-stream" : mime,
|
|
32
|
+
"Content-Length": content.length,
|
|
33
|
+
"Cache-Control": "no-store",
|
|
34
|
+
"X-Content-Type-Options": "nosniff",
|
|
35
|
+
};
|
|
36
|
+
if (download) {
|
|
37
|
+
var name = path.basename(requested).replace(/[\x00-\x1f\x7f"\\]/g, "_") || "download";
|
|
38
|
+
var ascii = name.replace(/[^\x20-\x7e]/g, "_");
|
|
39
|
+
var encoded = encodeURIComponent(name).replace(/[!'()*]/g, function (character) {
|
|
40
|
+
return "%" + character.charCodeAt(0).toString(16).toUpperCase();
|
|
41
|
+
});
|
|
42
|
+
headers["Content-Disposition"] = "attachment; filename=\"" + ascii + "\"; filename*=UTF-8''" + encoded;
|
|
43
|
+
}
|
|
44
|
+
res.writeHead(200, headers);
|
|
45
|
+
res.end(content);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
var result = fileError(error);
|
|
48
|
+
res.writeHead(result.status, { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" });
|
|
49
|
+
res.end(result.message);
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
return { handleHTTP: handleHTTP };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { attachFileHTTP: attachFileHTTP };
|
package/lib/project-file-path.js
CHANGED
|
@@ -1,13 +1,49 @@
|
|
|
1
1
|
var path = require("path");
|
|
2
|
+
var fs = require("fs");
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
function resolveFilePath(cwd, requested, safePath, osUserInfo) {
|
|
8
|
-
if (typeof requested !== "string" || !requested) return null;
|
|
9
|
-
if (osUserInfo) return path.resolve(cwd, requested);
|
|
10
|
-
return typeof safePath === "function" ? safePath(cwd, requested) : null;
|
|
4
|
+
function failure(code, message) {
|
|
5
|
+
var error = new Error(message);
|
|
6
|
+
error.code = code;
|
|
7
|
+
return error;
|
|
11
8
|
}
|
|
12
9
|
|
|
13
|
-
|
|
10
|
+
function inside(base, target) {
|
|
11
|
+
var relative = path.relative(base, target);
|
|
12
|
+
return relative !== ".." && !relative.startsWith(".." + path.sep) && !path.isAbsolute(relative);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Scope is server-derived for each request. In personal and mapped OS-user
|
|
16
|
+
// modes, the actual filesystem operation enforces read/write permissions.
|
|
17
|
+
// Unmapped multi-user deployments retain the project and symlink boundary.
|
|
18
|
+
function resolveFilePath(cwd, requested, scope) {
|
|
19
|
+
if (typeof requested !== "string" || !requested || requested.indexOf("\0") !== -1) {
|
|
20
|
+
throw failure("INVALID_PATH", "Invalid file path");
|
|
21
|
+
}
|
|
22
|
+
var resolved = path.resolve(cwd, requested);
|
|
23
|
+
if (scope && scope.projectBound === false) return resolved;
|
|
24
|
+
if (!inside(path.resolve(cwd), resolved)) {
|
|
25
|
+
throw failure("FILE_SCOPE", "This file is outside your project's allowed file scope");
|
|
26
|
+
}
|
|
27
|
+
var base = fs.realpathSync(cwd);
|
|
28
|
+
var real = fs.realpathSync(resolved);
|
|
29
|
+
if (!inside(base, real)) {
|
|
30
|
+
throw failure("FILE_SCOPE", "This file links outside your project's allowed file scope");
|
|
31
|
+
}
|
|
32
|
+
return real;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function fileError(error) {
|
|
36
|
+
var code = error.code;
|
|
37
|
+
// OS-user helpers report child-process failures through stderr.
|
|
38
|
+
if (error.stderr) {
|
|
39
|
+
var match = String(error.stderr).match(/code: ['"](EACCES|EPERM|ENOENT|ENOTDIR)['"]/);
|
|
40
|
+
if (match) code = match[1];
|
|
41
|
+
}
|
|
42
|
+
if (code === "ENOENT" || code === "ENOTDIR") return { code: code, status: 404, message: "File or folder not found" };
|
|
43
|
+
if (code === "EACCES" || code === "EPERM") return { code: code, status: 403, message: "The operating system denied access for your account" };
|
|
44
|
+
if (code === "INVALID_PATH") return { code: code, status: 400, message: error.message };
|
|
45
|
+
if (code === "FILE_SCOPE" || code === "FILE_FORBIDDEN" || code === "OS_IDENTITY") return { code: code, status: 403, message: error.message };
|
|
46
|
+
return { code: code || "FILE_ERROR", status: 500, message: error.message || "File operation failed" };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { resolveFilePath: resolveFilePath, fileError: fileError, failure: failure };
|
|
@@ -6,34 +6,28 @@ var resolveFilePath = require("./project-file-path").resolveFilePath;
|
|
|
6
6
|
* Attach file/directory watcher engine to a project context.
|
|
7
7
|
*
|
|
8
8
|
* ctx fields:
|
|
9
|
-
* cwd, send, sendTo,
|
|
9
|
+
* cwd, send, sendTo, BINARY_EXTS, FS_MAX_SIZE, IGNORED_DIRS, requestAccess, fsAsUser
|
|
10
10
|
*/
|
|
11
11
|
function attachFileWatch(ctx) {
|
|
12
12
|
var cwd = ctx.cwd;
|
|
13
13
|
var send = ctx.send;
|
|
14
14
|
var sendTo = ctx.sendTo;
|
|
15
|
-
var safePath = ctx.safePath;
|
|
16
15
|
var BINARY_EXTS = ctx.BINARY_EXTS;
|
|
17
16
|
var FS_MAX_SIZE = ctx.FS_MAX_SIZE;
|
|
18
17
|
var IGNORED_DIRS = ctx.IGNORED_DIRS;
|
|
19
18
|
var access = ctx.requestAccess;
|
|
20
19
|
var osUsers = !!ctx.osUsers;
|
|
21
20
|
|
|
22
|
-
function
|
|
21
|
+
function scopeFor(client) {
|
|
23
22
|
if (!access) {
|
|
24
23
|
if (osUsers) throw new Error("OS user identity is unavailable");
|
|
25
|
-
return null;
|
|
24
|
+
return { identity: null, projectBound: true };
|
|
26
25
|
}
|
|
27
|
-
|
|
28
|
-
var identity = access.osIdentity(client);
|
|
29
|
-
if (osUsers && !identity) throw new Error("OS user identity is unavailable");
|
|
30
|
-
return identity;
|
|
26
|
+
return access.fileScope(client);
|
|
31
27
|
}
|
|
32
28
|
|
|
33
|
-
function checkedPath(relPath,
|
|
34
|
-
|
|
35
|
-
if (!resolved) throw new Error("Access denied");
|
|
36
|
-
return resolved;
|
|
29
|
+
function checkedPath(relPath, scope) {
|
|
30
|
+
return resolveFilePath(cwd, relPath, scope);
|
|
37
31
|
}
|
|
38
32
|
|
|
39
33
|
// --- File watcher ---
|
|
@@ -68,8 +62,9 @@ function attachFileWatch(ctx) {
|
|
|
68
62
|
}
|
|
69
63
|
|
|
70
64
|
function readFileSnapshot(client, relPath) {
|
|
71
|
-
var
|
|
72
|
-
var
|
|
65
|
+
var scope = scopeFor(client);
|
|
66
|
+
var identity = scope.identity;
|
|
67
|
+
var absPath = checkedPath(relPath, scope);
|
|
73
68
|
var stat = identity ? ctx.fsAsUser("stat", { file: absPath }, identity) : fs.statSync(absPath);
|
|
74
69
|
var ext = path.extname(absPath).toLowerCase();
|
|
75
70
|
if (stat.size > FS_MAX_SIZE || BINARY_EXTS.has(ext)) return null;
|
|
@@ -107,10 +102,8 @@ function attachFileWatch(ctx) {
|
|
|
107
102
|
relPath = client;
|
|
108
103
|
client = null;
|
|
109
104
|
}
|
|
110
|
-
var
|
|
111
|
-
try {
|
|
112
|
-
var absPath = resolveFilePath(cwd, relPath, safePath, osUsers ? identity : null);
|
|
113
|
-
if (!absPath) return Promise.resolve(false);
|
|
105
|
+
var absPath;
|
|
106
|
+
try { absPath = checkedPath(relPath, scopeFor(client)); } catch (error) { return Promise.resolve(false); }
|
|
114
107
|
var key = client || "_legacy";
|
|
115
108
|
var existing = fileWatchers.get(key);
|
|
116
109
|
if (existing && existing.relPath === relPath) return existing.ready;
|
|
@@ -186,8 +179,9 @@ function attachFileWatch(ctx) {
|
|
|
186
179
|
var dirWatchers = new Map();
|
|
187
180
|
|
|
188
181
|
function readDirectory(client, relPath) {
|
|
189
|
-
var
|
|
190
|
-
var
|
|
182
|
+
var scope = scopeFor(client);
|
|
183
|
+
var identity = scope.identity;
|
|
184
|
+
var absPath = checkedPath(relPath, scope);
|
|
191
185
|
var items = identity ? ctx.fsAsUser("list", { dir: absPath }, identity) :
|
|
192
186
|
fs.readdirSync(absPath, { withFileTypes: true }).map(function (item) {
|
|
193
187
|
return { name: item.name, isDir: item.isDirectory() };
|
|
@@ -204,12 +198,12 @@ function attachFileWatch(ctx) {
|
|
|
204
198
|
var subscriptions = dirWatchers.get(client);
|
|
205
199
|
if (!subscriptions) { subscriptions = new Map(); dirWatchers.set(client, subscriptions); }
|
|
206
200
|
if (subscriptions.has(relPath)) return;
|
|
207
|
-
var
|
|
201
|
+
var scope;
|
|
208
202
|
try {
|
|
209
|
-
|
|
203
|
+
scope = scopeFor(client);
|
|
210
204
|
readDirectory(client, relPath);
|
|
211
205
|
var entry = { watcher: null, debounce: null };
|
|
212
|
-
entry.watcher = fs.watch(checkedPath(relPath,
|
|
206
|
+
entry.watcher = fs.watch(checkedPath(relPath, scope), function () {
|
|
213
207
|
clearTimeout(entry.debounce);
|
|
214
208
|
entry.debounce = setTimeout(function () {
|
|
215
209
|
try {
|
|
@@ -3,6 +3,7 @@ var path = require("path");
|
|
|
3
3
|
var attachRequestAccess = require("./project-request-access").attachRequestAccess;
|
|
4
4
|
var attachFileHistory = require("./project-file-history").attachFileHistory;
|
|
5
5
|
var resolveFilePath = require("./project-file-path").resolveFilePath;
|
|
6
|
+
var fileError = require("./project-file-path").fileError;
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Attach filesystem-related message handlers to a project context.
|
|
@@ -11,7 +12,6 @@ var resolveFilePath = require("./project-file-path").resolveFilePath;
|
|
|
11
12
|
* cwd, slug, osUsers
|
|
12
13
|
* sm (session manager)
|
|
13
14
|
* send, sendTo
|
|
14
|
-
* safePath, safeAbsPath (functions)
|
|
15
15
|
* getOsUserInfoForWs (function)
|
|
16
16
|
* startFileWatch, stopFileWatch, startDirWatch (from _fileWatch)
|
|
17
17
|
* usersModule, fsAsUser
|
|
@@ -23,8 +23,6 @@ function attachFilesystem(ctx) {
|
|
|
23
23
|
var cwd = ctx.cwd;
|
|
24
24
|
var slug = ctx.slug;
|
|
25
25
|
var sendTo = ctx.sendTo;
|
|
26
|
-
var safePath = ctx.safePath;
|
|
27
|
-
var safeAbsPath = ctx.safeAbsPath;
|
|
28
26
|
var startFileWatch = ctx.startFileWatch;
|
|
29
27
|
var stopFileWatch = ctx.stopFileWatch;
|
|
30
28
|
var startDirWatch = ctx.startDirWatch;
|
|
@@ -41,26 +39,26 @@ function attachFilesystem(ctx) {
|
|
|
41
39
|
var fileHistory = attachFileHistory(ctx, access);
|
|
42
40
|
|
|
43
41
|
function handleFilesystemMessage(ws, msg) {
|
|
44
|
-
|
|
42
|
+
var fileScope;
|
|
43
|
+
var fileTarget;
|
|
44
|
+
// Resolve live authority before every operation; never trust message roles.
|
|
45
45
|
if (/^fs_/.test(msg.type) && msg.type !== "fs_unwatch") {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
try {
|
|
47
|
+
fileScope = access.fileScope(ws);
|
|
48
|
+
if (/^fs_(list|read|write|watch)$/.test(msg.type)) {
|
|
49
|
+
fileTarget = resolveFilePath(cwd, msg.type === "fs_list" ? msg.path || "." : msg.path, fileScope);
|
|
50
|
+
}
|
|
51
|
+
} catch (e) {
|
|
52
|
+
var denied = fileError(e);
|
|
53
|
+
sendTo(ws, { type: msg.type + "_result", path: msg.path, requestId: msg.requestId, projectSlug: msg.projectSlug, sessionId: msg.sessionId, accountId: msg.accountId, ok: false, error: denied.message, errorCode: denied.code });
|
|
52
54
|
return true;
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
// --- fs_list ---
|
|
57
59
|
if (msg.type === "fs_list") {
|
|
58
|
-
var fsListIdentity =
|
|
59
|
-
var fsDir =
|
|
60
|
-
if (!fsDir) {
|
|
61
|
-
sendTo(ws, { type: "fs_list_result", path: msg.path, entries: [], error: "Access denied" });
|
|
62
|
-
return true;
|
|
63
|
-
}
|
|
60
|
+
var fsListIdentity = fileScope.identity;
|
|
61
|
+
var fsDir = fileTarget;
|
|
64
62
|
try {
|
|
65
63
|
var fsListUserInfo = fsListIdentity;
|
|
66
64
|
var entries = [];
|
|
@@ -92,7 +90,7 @@ function attachFilesystem(ctx) {
|
|
|
92
90
|
// Auto-watch the directory for changes
|
|
93
91
|
startDirWatch(ws, msg.path || ".");
|
|
94
92
|
} catch (e) {
|
|
95
|
-
sendTo(ws, { type: "fs_list_result", path: msg.path, entries: [], error: e.message });
|
|
93
|
+
sendTo(ws, { type: "fs_list_result", path: msg.path, entries: [], error: fileError(e).message, errorCode: fileError(e).code });
|
|
96
94
|
}
|
|
97
95
|
return true;
|
|
98
96
|
}
|
|
@@ -138,19 +136,15 @@ function attachFilesystem(ctx) {
|
|
|
138
136
|
walkDir(cwd, "");
|
|
139
137
|
sendTo(ws, { type: "fs_search_result", query: msg.query, entries: searchResults });
|
|
140
138
|
} catch (e) {
|
|
141
|
-
sendTo(ws, { type: "fs_search_result", query: msg.query, entries: [], error: e.message });
|
|
139
|
+
sendTo(ws, { type: "fs_search_result", query: msg.query, entries: [], error: fileError(e).message, errorCode: fileError(e).code });
|
|
142
140
|
}
|
|
143
141
|
return true;
|
|
144
142
|
}
|
|
145
143
|
|
|
146
144
|
// --- fs_read ---
|
|
147
145
|
if (msg.type === "fs_read") {
|
|
148
|
-
var fsReadIdentity =
|
|
149
|
-
var fsFile =
|
|
150
|
-
if (!fsFile) {
|
|
151
|
-
sendTo(ws, { type: "fs_read_result", path: msg.path, requestId: msg.requestId, projectSlug: msg.projectSlug, sessionId: msg.sessionId, accountId: msg.accountId, error: "Access denied" });
|
|
152
|
-
return true;
|
|
153
|
-
}
|
|
146
|
+
var fsReadIdentity = fileScope.identity;
|
|
147
|
+
var fsFile = fileTarget;
|
|
154
148
|
try {
|
|
155
149
|
var fsReadUserInfo = fsReadIdentity;
|
|
156
150
|
var ext = path.extname(fsFile).toLowerCase();
|
|
@@ -185,19 +179,15 @@ function attachFilesystem(ctx) {
|
|
|
185
179
|
sendTo(ws, { type: "fs_read_result", path: msg.path, requestId: msg.requestId, projectSlug: msg.projectSlug, sessionId: msg.sessionId, accountId: msg.accountId, content: content, size: stat.size });
|
|
186
180
|
}
|
|
187
181
|
} catch (e) {
|
|
188
|
-
sendTo(ws, { type: "fs_read_result", path: msg.path, requestId: msg.requestId, projectSlug: msg.projectSlug, sessionId: msg.sessionId, accountId: msg.accountId, error: e.message });
|
|
182
|
+
sendTo(ws, { type: "fs_read_result", path: msg.path, requestId: msg.requestId, projectSlug: msg.projectSlug, sessionId: msg.sessionId, accountId: msg.accountId, error: fileError(e).message, errorCode: fileError(e).code });
|
|
189
183
|
}
|
|
190
184
|
return true;
|
|
191
185
|
}
|
|
192
186
|
|
|
193
187
|
// --- fs_write ---
|
|
194
188
|
if (msg.type === "fs_write") {
|
|
195
|
-
var fsWriteIdentity =
|
|
196
|
-
var fsWriteFile =
|
|
197
|
-
if (!fsWriteFile) {
|
|
198
|
-
sendTo(ws, { type: "fs_write_result", path: msg.path, ok: false, error: "Access denied" });
|
|
199
|
-
return true;
|
|
200
|
-
}
|
|
189
|
+
var fsWriteIdentity = fileScope.identity;
|
|
190
|
+
var fsWriteFile = fileTarget;
|
|
201
191
|
try {
|
|
202
192
|
var fsWriteUserInfo = fsWriteIdentity;
|
|
203
193
|
if (fsWriteUserInfo) {
|
|
@@ -207,7 +197,7 @@ function attachFilesystem(ctx) {
|
|
|
207
197
|
}
|
|
208
198
|
sendTo(ws, { type: "fs_write_result", path: msg.path, ok: true });
|
|
209
199
|
} catch (e) {
|
|
210
|
-
sendTo(ws, { type: "fs_write_result", path: msg.path, ok: false, error: e.message });
|
|
200
|
+
sendTo(ws, { type: "fs_write_result", path: msg.path, ok: false, error: fileError(e).message, errorCode: fileError(e).code });
|
|
211
201
|
}
|
|
212
202
|
return true;
|
|
213
203
|
}
|
package/lib/project-http.js
CHANGED
|
@@ -6,24 +6,9 @@ var { execFileSync, spawn } = require("child_process");
|
|
|
6
6
|
var defaultFsAsUser = require("./os-users").fsAsUser;
|
|
7
7
|
var defaultUsersModule = require("./users");
|
|
8
8
|
var gitCli = require("./git-cli");
|
|
9
|
-
var
|
|
9
|
+
var attachFileHTTP = require("./project-file-http").attachFileHTTP;
|
|
10
10
|
var sharedSkillDiscovery = require("./yoke/skill-discovery");
|
|
11
11
|
|
|
12
|
-
var IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp", ".ico"]);
|
|
13
|
-
var MIME_TYPES = {
|
|
14
|
-
".html": "text/html",
|
|
15
|
-
".css": "text/css",
|
|
16
|
-
".js": "application/javascript",
|
|
17
|
-
".json": "application/json",
|
|
18
|
-
".png": "image/png",
|
|
19
|
-
".jpg": "image/jpeg",
|
|
20
|
-
".jpeg": "image/jpeg",
|
|
21
|
-
".gif": "image/gif",
|
|
22
|
-
".webp": "image/webp",
|
|
23
|
-
".svg": "image/svg+xml",
|
|
24
|
-
".bmp": "image/bmp",
|
|
25
|
-
".ico": "image/x-icon",
|
|
26
|
-
};
|
|
27
12
|
var MAX_UPLOAD_BYTES = 50 * 1024 * 1024; // 50 MB
|
|
28
13
|
|
|
29
14
|
function parseJsonBody(req) {
|
|
@@ -42,7 +27,7 @@ function parseJsonBody(req) {
|
|
|
42
27
|
*
|
|
43
28
|
* ctx fields:
|
|
44
29
|
* cwd, slug, project, sm, send, sendTo, imagesDir, osUsers, pushModule,
|
|
45
|
-
*
|
|
30
|
+
* requestAccess, getOsUserInfoForReq, sendExtensionCommandAny,
|
|
46
31
|
* saveGeneratedImageToProject, _extToken, _browserTabList
|
|
47
32
|
*/
|
|
48
33
|
function attachHTTP(ctx) {
|
|
@@ -56,8 +41,6 @@ function attachHTTP(ctx) {
|
|
|
56
41
|
var saveGeneratedImageToProject = ctx.saveGeneratedImageToProject;
|
|
57
42
|
var osUsers = ctx.osUsers;
|
|
58
43
|
var pushModule = ctx.pushModule;
|
|
59
|
-
var safePath = ctx.safePath;
|
|
60
|
-
var safeAbsPath = ctx.safeAbsPath;
|
|
61
44
|
var getOsUserInfoForReq = ctx.getOsUserInfoForReq;
|
|
62
45
|
var fsAsUser = ctx.fsAsUser || defaultFsAsUser;
|
|
63
46
|
var usersModule = ctx.usersModule || defaultUsersModule;
|
|
@@ -65,6 +48,10 @@ function attachHTTP(ctx) {
|
|
|
65
48
|
var _extToken = ctx._extToken;
|
|
66
49
|
var _browserTabList = ctx._browserTabList;
|
|
67
50
|
|
|
51
|
+
var fileHTTP = attachFileHTTP(Object.assign({}, ctx, {
|
|
52
|
+
usersModule: usersModule, fsAsUser: fsAsUser,
|
|
53
|
+
}));
|
|
54
|
+
|
|
68
55
|
function handleHTTP(req, res, urlPath) {
|
|
69
56
|
// Browser MCP extension bridge: forward commands to Chrome extension
|
|
70
57
|
if (req.method === "POST" && urlPath === "/ext-command") {
|
|
@@ -291,92 +278,7 @@ function attachHTTP(ctx) {
|
|
|
291
278
|
return true;
|
|
292
279
|
}
|
|
293
280
|
|
|
294
|
-
|
|
295
|
-
if (req.method === "GET" && urlPath.startsWith("/api/file/download?")) {
|
|
296
|
-
if (usersModule.isMultiUser()) {
|
|
297
|
-
var downloadUser = req._clayUser;
|
|
298
|
-
var downloadPermissions = downloadUser ? usersModule.getEffectivePermissions(downloadUser, osUsers) : null;
|
|
299
|
-
if (!downloadPermissions || !downloadPermissions.fileBrowser) {
|
|
300
|
-
res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" });
|
|
301
|
-
res.end("File browser access is not permitted");
|
|
302
|
-
return true;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
var downloadQueryIndex = urlPath.indexOf("?");
|
|
307
|
-
var downloadParams = new URLSearchParams(urlPath.substring(downloadQueryIndex));
|
|
308
|
-
var downloadPath = downloadParams.get("path");
|
|
309
|
-
if (!downloadPath) { res.writeHead(400); res.end("Missing path"); return true; }
|
|
310
|
-
var downloadUserInfo = getOsUserInfoForReq(req);
|
|
311
|
-
if (osUsers && !downloadUserInfo) { res.writeHead(403); res.end("OS user identity unavailable"); return true; }
|
|
312
|
-
var downloadFile = resolveFilePath(cwd, downloadPath, safePath, downloadUserInfo);
|
|
313
|
-
if (!downloadFile) { res.writeHead(403); res.end("Access denied"); return true; }
|
|
314
|
-
|
|
315
|
-
try {
|
|
316
|
-
var downloadContent;
|
|
317
|
-
if (downloadUserInfo) {
|
|
318
|
-
downloadContent = fsAsUser("read_binary", { file: downloadFile }, downloadUserInfo).buffer;
|
|
319
|
-
} else {
|
|
320
|
-
downloadContent = fs.readFileSync(downloadFile);
|
|
321
|
-
}
|
|
322
|
-
var downloadName = path.basename(downloadPath).replace(/[\x00-\x1f\x7f"\\]/g, "_") || "download";
|
|
323
|
-
var asciiDownloadName = downloadName.replace(/[^\x20-\x7e]/g, "_");
|
|
324
|
-
var encodedDownloadName = encodeURIComponent(downloadName).replace(/[!'()*]/g, function (character) {
|
|
325
|
-
return "%" + character.charCodeAt(0).toString(16).toUpperCase();
|
|
326
|
-
});
|
|
327
|
-
var contentDisposition = "attachment; filename=\"" + asciiDownloadName + "\"; filename*=UTF-8''" + encodedDownloadName;
|
|
328
|
-
res.writeHead(200, {
|
|
329
|
-
"Content-Type": "application/octet-stream",
|
|
330
|
-
"Content-Length": downloadContent.length,
|
|
331
|
-
"Content-Disposition": contentDisposition,
|
|
332
|
-
"Cache-Control": "no-store",
|
|
333
|
-
"X-Content-Type-Options": "nosniff",
|
|
334
|
-
});
|
|
335
|
-
res.end(downloadContent);
|
|
336
|
-
} catch (e) {
|
|
337
|
-
res.writeHead(404);
|
|
338
|
-
res.end("Not found");
|
|
339
|
-
}
|
|
340
|
-
return true;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// File browser: serve project images
|
|
344
|
-
if (req.method === "GET" && urlPath.startsWith("/api/file?")) {
|
|
345
|
-
var qIdx = urlPath.indexOf("?");
|
|
346
|
-
var params = new URLSearchParams(urlPath.substring(qIdx));
|
|
347
|
-
var reqFilePath = params.get("path");
|
|
348
|
-
if (!reqFilePath) { res.writeHead(400); res.end("Missing path"); return true; }
|
|
349
|
-
if (usersModule.isMultiUser()) {
|
|
350
|
-
var imageUser = req._clayUser;
|
|
351
|
-
var imagePermissions = imageUser ? usersModule.getEffectivePermissions(imageUser, osUsers) : null;
|
|
352
|
-
if (!imagePermissions || !imagePermissions.fileBrowser) {
|
|
353
|
-
res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" });
|
|
354
|
-
res.end("File browser access is not permitted");
|
|
355
|
-
return true;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
var fileServeUserInfo = getOsUserInfoForReq(req);
|
|
359
|
-
if (osUsers && !fileServeUserInfo) { res.writeHead(403); res.end("OS user identity unavailable"); return true; }
|
|
360
|
-
var absFile = resolveFilePath(cwd, reqFilePath, safePath, fileServeUserInfo);
|
|
361
|
-
if (!absFile) { res.writeHead(403); res.end("Access denied"); return true; }
|
|
362
|
-
var fileExt = path.extname(absFile).toLowerCase();
|
|
363
|
-
if (!IMAGE_EXTS.has(fileExt)) { res.writeHead(403); res.end("Only image files"); return true; }
|
|
364
|
-
try {
|
|
365
|
-
var fileContent;
|
|
366
|
-
if (fileServeUserInfo) {
|
|
367
|
-
var binResult = fsAsUser("read_binary", { file: absFile }, fileServeUserInfo);
|
|
368
|
-
fileContent = binResult.buffer;
|
|
369
|
-
} else {
|
|
370
|
-
fileContent = fs.readFileSync(absFile);
|
|
371
|
-
}
|
|
372
|
-
var fileMime = MIME_TYPES[fileExt] || "application/octet-stream";
|
|
373
|
-
res.writeHead(200, { "Content-Type": fileMime, "Cache-Control": "no-cache" });
|
|
374
|
-
res.end(fileContent);
|
|
375
|
-
} catch (e) {
|
|
376
|
-
res.writeHead(404); res.end("Not found");
|
|
377
|
-
}
|
|
378
|
-
return true;
|
|
379
|
-
}
|
|
281
|
+
if (fileHTTP.handleHTTP(req, res, urlPath)) return true;
|
|
380
282
|
|
|
381
283
|
// Skills permission gate
|
|
382
284
|
if (urlPath === "/api/install-skill" || urlPath === "/api/uninstall-skill" || urlPath === "/api/installed-skills") {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Shared authorization for project requests and long-lived file subscriptions.
|
|
2
|
+
var fileFailure = require("./project-file-path").failure;
|
|
2
3
|
function attachRequestAccess(ctx) {
|
|
3
4
|
var users = ctx.usersModule;
|
|
4
5
|
var opts = ctx.opts || {};
|
|
@@ -9,7 +10,7 @@ function attachRequestAccess(ctx) {
|
|
|
9
10
|
|
|
10
11
|
function userFor(ws) {
|
|
11
12
|
var user = ws && ws._clayUser;
|
|
12
|
-
if (user && users.findUserById) return users.findUserById(user.id);
|
|
13
|
+
if (user && users && users.findUserById) return users.findUserById(user.id);
|
|
13
14
|
return user || null;
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -53,14 +54,20 @@ function attachRequestAccess(ctx) {
|
|
|
53
54
|
function osIdentity(ws) {
|
|
54
55
|
if (!ctx.osUsers) return null;
|
|
55
56
|
var user = userFor(ws);
|
|
56
|
-
if (!user || !user.linuxUser) throw
|
|
57
|
+
if (!user || !user.linuxUser) throw fileFailure("OS_IDENTITY", "OS user identity is unavailable");
|
|
57
58
|
var info = ctx.getOsUserInfoForWs({ _clayUser: user });
|
|
58
|
-
if (!info) throw
|
|
59
|
+
if (!info) throw fileFailure("OS_IDENTITY", "OS user identity is unavailable");
|
|
59
60
|
return info;
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
function fileScope(ws) {
|
|
64
|
+
if (!canUseFiles(ws)) throw fileFailure("FILE_FORBIDDEN", "File browser access is not permitted");
|
|
65
|
+
var identity = osIdentity(ws);
|
|
66
|
+
return { identity: identity, projectBound: isMultiUser() && !identity };
|
|
67
|
+
}
|
|
68
|
+
|
|
62
69
|
return { permitMessage: permitMessage, canAccessProject: canAccessProject, hasPermission: hasPermission,
|
|
63
|
-
canUseFiles: canUseFiles, isAdmin: isAdmin, canReadSession: canReadSession, osIdentity: osIdentity };
|
|
70
|
+
canUseFiles: canUseFiles, isAdmin: isAdmin, canReadSession: canReadSession, osIdentity: osIdentity, fileScope: fileScope };
|
|
64
71
|
}
|
|
65
72
|
|
|
66
73
|
module.exports = { attachRequestAccess: attachRequestAccess };
|
package/lib/project.js
CHANGED
package/package.json
CHANGED