This commit is contained in:
@@ -1,10 +1,30 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const allowedRepositoryProtocols = new Set(['https:', 'git:']);
|
||||
const repositoryUrlPrefixPattern = /^(?:https|git):\/\//i;
|
||||
const repositoryUrlError = 'repoUrl must be a valid https:// or git:// URL';
|
||||
|
||||
function assertAllowedRepositoryUrl(repoUrl: string): void {
|
||||
if (repoUrl.startsWith('-') || !repositoryUrlPrefixPattern.test(repoUrl)) {
|
||||
throw new BadRequestException(repositoryUrlError);
|
||||
}
|
||||
|
||||
let parsedUrl: URL;
|
||||
try {
|
||||
parsedUrl = new URL(repoUrl);
|
||||
} catch {
|
||||
throw new BadRequestException(repositoryUrlError);
|
||||
}
|
||||
|
||||
if (!allowedRepositoryProtocols.has(parsedUrl.protocol) || parsedUrl.hostname.length === 0) {
|
||||
throw new BadRequestException(repositoryUrlError);
|
||||
}
|
||||
}
|
||||
|
||||
export interface WorkspaceProject {
|
||||
id: string;
|
||||
@@ -39,14 +59,32 @@ export class WorkspaceService {
|
||||
* If repoUrl is provided, clone instead of init.
|
||||
*/
|
||||
async create(project: WorkspaceProject, repoUrl?: string): Promise<string> {
|
||||
if (repoUrl !== undefined) {
|
||||
assertAllowedRepositoryUrl(repoUrl);
|
||||
}
|
||||
|
||||
const workspacePath = this.resolvePath(project);
|
||||
|
||||
// Create directory
|
||||
await fs.mkdir(workspacePath, { recursive: true });
|
||||
|
||||
if (repoUrl) {
|
||||
// Clone existing repo
|
||||
await execFileAsync('git', ['clone', repoUrl, '.'], { cwd: workspacePath });
|
||||
if (repoUrl !== undefined) {
|
||||
// Clone existing repo. Defense in depth keeps dangerous local helpers
|
||||
// disabled and terminates option parsing before positional arguments.
|
||||
await execFileAsync(
|
||||
'git',
|
||||
[
|
||||
'-c',
|
||||
'protocol.ext.allow=never',
|
||||
'-c',
|
||||
'protocol.file.allow=never',
|
||||
'clone',
|
||||
'--',
|
||||
repoUrl,
|
||||
'.',
|
||||
],
|
||||
{ cwd: workspacePath },
|
||||
);
|
||||
this.logger.log(`Cloned ${repoUrl} into workspace ${workspacePath}`);
|
||||
} else {
|
||||
// Init new git repo
|
||||
|
||||
Reference in New Issue
Block a user