Skip to content

Commit 4cd8f3b

Browse files
committed
file-server: refactor fs sandbox module
1 parent ebc618b commit 4cd8f3b

File tree

2 files changed

+89
-70
lines changed

2 files changed

+89
-70
lines changed

src/packages/file-server/btrfs/subvolume.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import { type Filesystem, DEFAULT_SUBVOLUME_SIZE } from "./filesystem";
66
import refCache from "@cocalc/util/refcache";
77
import { sudo } from "./util";
88
import { join, normalize } from "path";
9-
import { SubvolumeFilesystem } from "./subvolume-fs";
109
import { SubvolumeBup } from "./subvolume-bup";
1110
import { SubvolumeSnapshots } from "./subvolume-snapshots";
1211
import { SubvolumeQuota } from "./subvolume-quota";
12+
import { SandboxedFilesystem } from "../fs";
1313
import { exists } from "@cocalc/backend/misc/async-utils-node";
1414

1515
import getLogger from "@cocalc/backend/logger";
@@ -26,7 +26,7 @@ export class Subvolume {
2626

2727
public readonly filesystem: Filesystem;
2828
public readonly path: string;
29-
public readonly fs: SubvolumeFilesystem;
29+
public readonly fs: SandboxedFilesystem;
3030
public readonly bup: SubvolumeBup;
3131
public readonly snapshots: SubvolumeSnapshots;
3232
public readonly quota: SubvolumeQuota;
@@ -35,7 +35,7 @@ export class Subvolume {
3535
this.filesystem = filesystem;
3636
this.name = name;
3737
this.path = join(filesystem.opts.mount, name);
38-
this.fs = new SubvolumeFilesystem(this);
38+
this.fs = new SandboxedFilesystem(this.path);
3939
this.bup = new SubvolumeBup(this);
4040
this.snapshots = new SubvolumeSnapshots(this);
4141
this.quota = new SubvolumeQuota(this);
Lines changed: 86 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
/*
2+
Given a path to a folder on the filesystem, this provides
3+
a wrapper class with an API very similar to the fs/promises modules,
4+
but which only allows access to files in that folder.
5+
*/
6+
17
import {
28
appendFile,
39
chmod,
@@ -21,111 +27,100 @@ import {
2127
import { exists } from "@cocalc/backend/misc/async-utils-node";
2228
import { type DirectoryListingEntry } from "@cocalc/util/types";
2329
import getListing from "@cocalc/backend/get-listing";
24-
import { type Subvolume } from "./subvolume";
25-
import { isdir, sudo } from "./util";
26-
27-
export class SubvolumeFilesystem {
28-
constructor(private subvolume: Subvolume) {}
29-
30-
private normalize = this.subvolume.normalize;
30+
import { isdir, sudo } from "../btrfs/util";
31+
import { join, resolve } from "path";
3132

32-
ls = async (
33-
path: string,
34-
{ hidden, limit }: { hidden?: boolean; limit?: number } = {},
35-
): Promise<DirectoryListingEntry[]> => {
36-
return await getListing(this.normalize(path), hidden, {
37-
limit,
38-
home: "/",
39-
});
40-
};
33+
export class SandboxedFilesystem {
34+
// path should be the path to a FOLDER on the filesystem (not a file)
35+
constructor(public readonly path: string) {}
4136

42-
readFile = async (path: string, encoding?: any): Promise<string | Buffer> => {
43-
return await readFile(this.normalize(path), encoding);
37+
private safeAbsPath = (path: string) => {
38+
if (typeof path != "string") {
39+
throw Error(`path must be a string but is of type ${typeof path}`);
40+
}
41+
return join(this.path, resolve("/", path));
4442
};
4543

46-
writeFile = async (path: string, data: string | Buffer) => {
47-
return await writeFile(this.normalize(path), data);
44+
appendFile = async (path: string, data: string | Buffer, encoding?) => {
45+
return await appendFile(this.safeAbsPath(path), data, encoding);
4846
};
4947

50-
appendFile = async (path: string, data: string | Buffer, encoding?) => {
51-
return await appendFile(this.normalize(path), data, encoding);
48+
chmod = async (path: string, mode: string | number) => {
49+
await chmod(this.safeAbsPath(path), mode);
5250
};
5351

54-
unlink = async (path: string) => {
55-
await unlink(this.normalize(path));
52+
copyFile = async (src: string, dest: string) => {
53+
await copyFile(this.safeAbsPath(src), this.safeAbsPath(dest));
5654
};
5755

58-
stat = async (path: string) => {
59-
return await stat(this.normalize(path));
56+
cp = async (src: string, dest: string, options?) => {
57+
await cp(this.safeAbsPath(src), this.safeAbsPath(dest), options);
6058
};
6159

6260
exists = async (path: string) => {
63-
return await exists(this.normalize(path));
61+
return await exists(this.safeAbsPath(path));
6462
};
6563

6664
// hard link
6765
link = async (existingPath: string, newPath: string) => {
68-
return await link(this.normalize(existingPath), this.normalize(newPath));
69-
};
70-
71-
symlink = async (target: string, path: string) => {
72-
return await symlink(this.normalize(target), this.normalize(path));
73-
};
74-
75-
realpath = async (path: string) => {
76-
const x = await realpath(this.normalize(path));
77-
return x.slice(this.subvolume.path.length + 1);
78-
};
79-
80-
rename = async (oldPath: string, newPath: string) => {
81-
await rename(this.normalize(oldPath), this.normalize(newPath));
66+
return await link(
67+
this.safeAbsPath(existingPath),
68+
this.safeAbsPath(newPath),
69+
);
8270
};
8371

84-
utimes = async (
72+
ls = async (
8573
path: string,
86-
atime: number | string | Date,
87-
mtime: number | string | Date,
88-
) => {
89-
await utimes(this.normalize(path), atime, mtime);
74+
{ hidden, limit }: { hidden?: boolean; limit?: number } = {},
75+
): Promise<DirectoryListingEntry[]> => {
76+
return await getListing(this.safeAbsPath(path), hidden, {
77+
limit,
78+
home: "/",
79+
});
9080
};
9181

92-
watch = (filename: string, options?) => {
93-
return watch(this.normalize(filename), options);
82+
mkdir = async (path: string, options?) => {
83+
await mkdir(this.safeAbsPath(path), options);
9484
};
9585

96-
truncate = async (path: string, len?: number) => {
97-
await truncate(this.normalize(path), len);
86+
readFile = async (path: string, encoding?: any): Promise<string | Buffer> => {
87+
return await readFile(this.safeAbsPath(path), encoding);
9888
};
9989

100-
copyFile = async (src: string, dest: string) => {
101-
await copyFile(this.normalize(src), this.normalize(dest));
90+
realpath = async (path: string) => {
91+
const x = await realpath(this.safeAbsPath(path));
92+
return x.slice(this.path.length + 1);
10293
};
10394

104-
cp = async (src: string, dest: string, options?) => {
105-
await cp(this.normalize(src), this.normalize(dest), options);
95+
rename = async (oldPath: string, newPath: string) => {
96+
await rename(this.safeAbsPath(oldPath), this.safeAbsPath(newPath));
10697
};
10798

108-
chmod = async (path: string, mode: string | number) => {
109-
await chmod(this.normalize(path), mode);
99+
rm = async (path: string, options?) => {
100+
await rm(this.safeAbsPath(path), options);
110101
};
111102

112-
mkdir = async (path: string, options?) => {
113-
await mkdir(this.normalize(path), options);
103+
rmdir = async (path: string, options?) => {
104+
await rmdir(this.safeAbsPath(path), options);
114105
};
115106

116107
rsync = async ({
117108
src,
118109
target,
119-
args = ["-axH"],
120110
timeout = 5 * 60 * 1000,
121111
}: {
122112
src: string;
123113
target: string;
124-
args?: string[];
125114
timeout?: number;
126115
}): Promise<{ stdout: string; stderr: string; exit_code: number }> => {
127-
let srcPath = this.normalize(src);
128-
let targetPath = this.normalize(target);
116+
let srcPath = this.safeAbsPath(src);
117+
let targetPath = this.safeAbsPath(target);
118+
if (src.endsWith("/")) {
119+
srcPath += "/";
120+
}
121+
if (target.endsWith("/")) {
122+
targetPath += "/";
123+
}
129124
if (!srcPath.endsWith("/") && (await isdir(srcPath))) {
130125
srcPath += "/";
131126
if (!targetPath.endsWith("/")) {
@@ -134,17 +129,41 @@ export class SubvolumeFilesystem {
134129
}
135130
return await sudo({
136131
command: "rsync",
137-
args: [...args, srcPath, targetPath],
132+
args: [srcPath, targetPath],
138133
err_on_exit: false,
139134
timeout: timeout / 1000,
140135
});
141136
};
142137

143-
rmdir = async (path: string, options?) => {
144-
await rmdir(this.normalize(path), options);
138+
stat = async (path: string) => {
139+
return await stat(this.safeAbsPath(path));
145140
};
146141

147-
rm = async (path: string, options?) => {
148-
await rm(this.normalize(path), options);
142+
symlink = async (target: string, path: string) => {
143+
return await symlink(this.safeAbsPath(target), this.safeAbsPath(path));
144+
};
145+
146+
truncate = async (path: string, len?: number) => {
147+
await truncate(this.safeAbsPath(path), len);
148+
};
149+
150+
unlink = async (path: string) => {
151+
await unlink(this.safeAbsPath(path));
152+
};
153+
154+
utimes = async (
155+
path: string,
156+
atime: number | string | Date,
157+
mtime: number | string | Date,
158+
) => {
159+
await utimes(this.safeAbsPath(path), atime, mtime);
160+
};
161+
162+
watch = (filename: string, options?) => {
163+
return watch(this.safeAbsPath(filename), options);
164+
};
165+
166+
writeFile = async (path: string, data: string | Buffer) => {
167+
return await writeFile(this.safeAbsPath(path), data);
149168
};
150169
}

0 commit comments

Comments
 (0)