Initial U-Navigator prototype
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
export class Sandbox {
|
||||
constructor(roots, options = {}) {
|
||||
if (!roots.length) {
|
||||
throw new Error('At least one allowed root is required');
|
||||
}
|
||||
|
||||
this.readOnly = Boolean(options.readOnly);
|
||||
this.roots = roots.map((root, index) => ({
|
||||
id: `root${index + 1}`,
|
||||
label: root,
|
||||
path: path.resolve(root)
|
||||
}));
|
||||
}
|
||||
|
||||
async init() {
|
||||
const usable = [];
|
||||
|
||||
for (const root of this.roots) {
|
||||
const real = await fs.realpath(root.path);
|
||||
const stat = await fs.stat(real);
|
||||
if (!stat.isDirectory()) {
|
||||
throw new Error(`Allowed root is not a directory: ${root.path}`);
|
||||
}
|
||||
usable.push({ ...root, real });
|
||||
}
|
||||
|
||||
this.roots = usable;
|
||||
return this;
|
||||
}
|
||||
|
||||
publicConfig() {
|
||||
return {
|
||||
readOnly: this.readOnly,
|
||||
roots: this.roots.map((root) => ({
|
||||
id: root.id,
|
||||
label: root.label,
|
||||
path: root.real
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
assertWritable() {
|
||||
if (this.readOnly) {
|
||||
const error = new Error('Read-only mode is enabled');
|
||||
error.status = 403;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
rootFor(inputPath = '/') {
|
||||
const normalized = String(inputPath || '/');
|
||||
|
||||
if (normalized === '/' || normalized === '') {
|
||||
return this.roots[0];
|
||||
}
|
||||
|
||||
for (const root of this.roots) {
|
||||
if (normalized === root.real || normalized.startsWith(`${root.real}${path.sep}`)) {
|
||||
return root;
|
||||
}
|
||||
}
|
||||
|
||||
const error = new Error('Path is outside allowed roots');
|
||||
error.status = 403;
|
||||
throw error;
|
||||
}
|
||||
|
||||
rootForReal(realPath) {
|
||||
for (const root of this.roots) {
|
||||
if (this.isInside(realPath, root.real)) {
|
||||
return root;
|
||||
}
|
||||
}
|
||||
|
||||
const error = new Error('Path is outside allowed roots');
|
||||
error.status = 403;
|
||||
throw error;
|
||||
}
|
||||
|
||||
async resolveExisting(inputPath = '/') {
|
||||
const candidate = path.resolve(String(inputPath || this.roots[0].real));
|
||||
const real = await fs.realpath(candidate);
|
||||
const root = this.rootForReal(real);
|
||||
|
||||
return { root, path: candidate, real };
|
||||
}
|
||||
|
||||
async resolveDestination(inputPath) {
|
||||
const candidate = path.resolve(String(inputPath || ''));
|
||||
const parent = path.dirname(candidate);
|
||||
const parentReal = await fs.realpath(parent);
|
||||
const root = this.rootForReal(parentReal);
|
||||
|
||||
return { root, path: candidate, parentReal };
|
||||
}
|
||||
|
||||
isInside(candidate, root) {
|
||||
const relative = path.relative(root, candidate);
|
||||
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
||||
}
|
||||
}
|
||||
|
||||
export function rootsFromEnv(env = process.env) {
|
||||
if (env.U_NAV_ROOTS) {
|
||||
return env.U_NAV_ROOTS.split(',').map((item) => item.trim()).filter(Boolean);
|
||||
}
|
||||
if (env.U_NAV_ROOT) {
|
||||
return [env.U_NAV_ROOT];
|
||||
}
|
||||
return ['/mnt/user'];
|
||||
}
|
||||
Reference in New Issue
Block a user