94 lines
2.6 KiB
TypeScript
94 lines
2.6 KiB
TypeScript
import fs from "fs";
|
|
import path from "path";
|
|
import { getAppDataDir, getTempDir } from "./constants";
|
|
|
|
export class LocalFileAccessError extends Error {
|
|
constructor(
|
|
message: string,
|
|
public readonly code: "INVALID_PATH" | "NOT_FOUND" | "ACCESS_DENIED",
|
|
) {
|
|
super(message);
|
|
this.name = "LocalFileAccessError";
|
|
}
|
|
}
|
|
|
|
function resolveBaseDir(baseDir: string): string {
|
|
const resolvedBaseDir = path.resolve(baseDir);
|
|
try {
|
|
return fs.realpathSync(resolvedBaseDir);
|
|
} catch {
|
|
return resolvedBaseDir;
|
|
}
|
|
}
|
|
|
|
function allowedReadableFileBaseDirs(): string[] {
|
|
return [getAppDataDir(), getTempDir()].map(resolveBaseDir);
|
|
}
|
|
|
|
function assertPathAllowed(candidatePath: string, baseDirs: string[]): void {
|
|
for (const baseDir of baseDirs) {
|
|
if (candidatePath === baseDir || candidatePath.startsWith(`${baseDir}${path.sep}`)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
throw new LocalFileAccessError(
|
|
"Access denied: File path not allowed",
|
|
"ACCESS_DENIED",
|
|
);
|
|
}
|
|
|
|
export function resolveReadableLocalFile(filePath: unknown): string {
|
|
if (typeof filePath !== "string" || filePath.trim().length === 0) {
|
|
throw new LocalFileAccessError("Invalid file path", "INVALID_PATH");
|
|
}
|
|
|
|
const requestedPath = path.resolve(filePath);
|
|
const allowedBaseDirs = allowedReadableFileBaseDirs();
|
|
assertPathAllowed(requestedPath, allowedBaseDirs);
|
|
|
|
|
|
let resolvedPath: string;
|
|
try {
|
|
resolvedPath = fs.realpathSync(requestedPath);
|
|
} catch {
|
|
throw new LocalFileAccessError("File not found", "NOT_FOUND");
|
|
}
|
|
|
|
assertPathAllowed(resolvedPath, allowedBaseDirs);
|
|
return resolvedPath;
|
|
}
|
|
|
|
export function readReadableLocalFile(filePath: unknown): string {
|
|
if (typeof filePath !== "string" || filePath.trim().length === 0) {
|
|
throw new LocalFileAccessError("Invalid file path", "INVALID_PATH");
|
|
}
|
|
|
|
const requestedPath = path.resolve(filePath);
|
|
const [appDataDir, tempDir] = allowedReadableFileBaseDirs();
|
|
|
|
if (!(requestedPath.startsWith(appDataDir) || requestedPath.startsWith(tempDir))) {
|
|
throw new LocalFileAccessError(
|
|
"Access denied: File path not allowed",
|
|
"ACCESS_DENIED",
|
|
);
|
|
}
|
|
|
|
let resolvedPath: string;
|
|
try {
|
|
resolvedPath = fs.realpathSync(requestedPath);
|
|
} catch {
|
|
throw new LocalFileAccessError("File not found", "NOT_FOUND");
|
|
}
|
|
|
|
if (!(resolvedPath.startsWith(appDataDir) || resolvedPath.startsWith(tempDir))) {
|
|
throw new LocalFileAccessError(
|
|
"Access denied: File path not allowed",
|
|
"ACCESS_DENIED",
|
|
);
|
|
}
|
|
assertPathAllowed(resolvedPath, [appDataDir, tempDir]);
|
|
|
|
return fs.readFileSync(resolvedPath, "utf-8");
|
|
}
|