Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 56 additions & 13 deletions nest/src/services/ipfs/ipfs.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,20 +93,23 @@ describe('IpfsService', () => {
return { tempDir, repoDir, outsideDir };
}

async function collectFiles(repoDir: string) {
return (service as any).getFilesRecursively(await fs.realpath(repoDir));
}

function relativeFiles(files: Array<{ relativePath: string }>): string[] {
return files.map((file) => file.relativePath).sort();
}

it('should skip symlinked files while collecting IPFS files', async () => {
const { repoDir, outsideDir } = await createTempRepo();
const outsideFile = path.join(outsideDir, 'env');
await fs.writeFile(outsideFile, 'NEAR_MAINNET_PRIVATE_KEY=secret');
await fs.symlink(outsideFile, path.join(repoDir, 'src', 'leak.env'));

const files = await (service as any).getFilesRecursively(
await fs.realpath(repoDir),
);
const relativeFiles = files.map((file: string) =>
path.relative(repoDir, file),
);
const files = await collectFiles(repoDir);

expect(relativeFiles).toEqual(['src/lib.rs']);
expect(relativeFiles(files)).toEqual(['src/lib.rs']);
});

it('should skip symlinked directories while collecting IPFS files', async () => {
Expand All @@ -117,21 +120,61 @@ describe('IpfsService', () => {
);
await fs.symlink(outsideDir, path.join(repoDir, 'linked-outside-dir'));

const files = await (service as any).getFilesRecursively(
await fs.realpath(repoDir),
const files = await collectFiles(repoDir);

expect(relativeFiles(files)).toEqual(['src/lib.rs']);
});

it('should preserve symlinked files inside the IPFS root', async () => {
const { repoDir } = await createTempRepo();
const targetFile = path.join(repoDir, 'src', 'lib.rs');
await fs.symlink(targetFile, path.join(repoDir, 'src', 'lib-link.rs'));

const files = await collectFiles(repoDir);
const linkedFile = files.find(
(file: { relativePath: string }) =>
file.relativePath === 'src/lib-link.rs',
);
const relativeFiles = files.map((file: string) =>
path.relative(repoDir, file),

expect(relativeFiles(files)).toEqual(['src/lib-link.rs', 'src/lib.rs']);
expect(linkedFile?.sourcePath).toBe(await fs.realpath(targetFile));
});

it('should preserve symlinked directories inside the IPFS root', async () => {
const { repoDir } = await createTempRepo();
await fs.mkdir(path.join(repoDir, 'shared'), { recursive: true });
await fs.writeFile(path.join(repoDir, 'shared', 'mod.rs'), 'mod shared;');
await fs.symlink(
path.join(repoDir, 'shared'),
path.join(repoDir, 'src', 'shared-link'),
);

expect(relativeFiles).toEqual(['src/lib.rs']);
const files = await collectFiles(repoDir);

expect(relativeFiles(files)).toEqual([
'shared/mod.rs',
'src/lib.rs',
'src/shared-link/mod.rs',
]);
});

it('should skip recursive symlinks inside the IPFS root', async () => {
const { repoDir } = await createTempRepo();
await fs.symlink(repoDir, path.join(repoDir, 'src', 'loop'));

const files = await collectFiles(repoDir);

expect(relativeFiles(files)).toEqual(['src/lib.rs']);
});

it('should reject resolved paths outside the IPFS root', () => {
const rootPath = path.resolve('/tmp/sourcescan-ipfs-root');

expect(
(service as any).isPathInsideRoot(rootPath, path.join(rootPath, 'a.rs')),
(service as any).isPathInsideRoot(
rootPath,
path.join(rootPath, 'a.rs'),
),
).toBe(true);
expect(
(service as any).isPathInsideRoot(
Expand Down
105 changes: 95 additions & 10 deletions nest/src/services/ipfs/ipfs.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export interface IpfsFileEntry {
Type: number; // 1 = directory, 2 = file
}

interface IpfsUploadFile {
sourcePath: string;
relativePath: string;
}

@Injectable()
export class IpfsService {
private readonly ipfsHost = process.env.IPFS_HOST;
Expand All @@ -32,11 +37,10 @@ export class IpfsService {
const files = await this.getFilesRecursively(rootPath);

for (const file of files) {
const relativePath = path.relative(rootPath, file);
const content = await fs.readFile(file);
const content = await fs.readFile(file.sourcePath);
form.append('file', content, {
filename: relativePath,
filepath: relativePath,
filename: file.relativePath,
filepath: file.relativePath,
});
}

Expand Down Expand Up @@ -118,23 +122,52 @@ export class IpfsService {
private async getFilesRecursively(
dir: string,
rootPath = dir,
): Promise<string[]> {
const files: string[] = [];
relativeDir = '',
ancestorDirs = new Set<string>([rootPath]),
): Promise<IpfsUploadFile[]> {
const files: IpfsUploadFile[] = [];
const entries = await fs.readdir(dir, { withFileTypes: true });

for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
const relativePath = relativeDir
? path.join(relativeDir, entry.name)
: entry.name;
const stats = await fs.lstat(fullPath);

if (stats.isSymbolicLink()) {
this.logger.warn(
`Skipping symbolic link while adding to IPFS: ${fullPath}`,
await this.addSymbolicLinkTarget(
files,
fullPath,
relativePath,
rootPath,
ancestorDirs,
);
continue;
}

if (stats.isDirectory()) {
files.push(...(await this.getFilesRecursively(fullPath, rootPath)));
const realPath = await fs.realpath(fullPath);
if (!this.isPathInsideRoot(rootPath, realPath)) {
this.logger.warn(`Skipping directory outside IPFS root: ${fullPath}`);
continue;
}

if (ancestorDirs.has(realPath)) {
this.logger.warn(
`Skipping recursive directory while adding to IPFS: ${fullPath}`,
);
continue;
}

files.push(
...(await this.getFilesRecursively(
realPath,
rootPath,
relativePath,
new Set([...ancestorDirs, realPath]),
)),
);
continue;
}

Expand All @@ -151,12 +184,64 @@ export class IpfsService {
continue;
}

files.push(realPath);
files.push({ sourcePath: realPath, relativePath });
}

return files;
}

private async addSymbolicLinkTarget(
files: IpfsUploadFile[],
symlinkPath: string,
relativePath: string,
rootPath: string,
ancestorDirs: Set<string>,
): Promise<void> {
let realPath: string;
try {
realPath = await fs.realpath(symlinkPath);
} catch {
this.logger.warn(`Skipping broken symlink: ${symlinkPath}`);
return;
}

if (!this.isPathInsideRoot(rootPath, realPath)) {
this.logger.warn(
`Skipping symlink outside IPFS root while adding to IPFS: ${symlinkPath}`,
);
return;
}

const stats = await fs.stat(realPath);
if (stats.isDirectory()) {
if (ancestorDirs.has(realPath)) {
this.logger.warn(
`Skipping recursive symlink while adding to IPFS: ${symlinkPath}`,
);
return;
}

files.push(
...(await this.getFilesRecursively(
realPath,
rootPath,
relativePath,
new Set([...ancestorDirs, realPath]),
)),
);
return;
}

if (!stats.isFile()) {
this.logger.warn(
`Skipping symlink to non-regular file while adding to IPFS: ${symlinkPath}`,
);
return;
}

files.push({ sourcePath: realPath, relativePath });
}

private isPathInsideRoot(rootPath: string, candidatePath: string): boolean {
const relativePath = path.relative(rootPath, candidatePath);
return (
Expand Down
Loading