This commit is contained in:
Archer
2024-05-28 14:47:10 +08:00
committed by GitHub
parent d9f5f4ede0
commit 9639139b52
58 changed files with 4715 additions and 283 deletions

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { SandboxController } from './sandbox/sandbox.controller';
import { SandboxService } from './sandbox/sandbox.service';
@Module({
imports: [],
controllers: [SandboxController],
providers: [SandboxService]
})
export class AppModule {}

View File

@@ -0,0 +1,18 @@
import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common';
import { FastifyRequest, FastifyReply } from 'fastify';
import { getErrText } from './utils';
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
catch(error: any, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<FastifyReply>();
const request = ctx.getRequest<FastifyRequest>();
response.status(500).send({
success: false,
time: new Date(),
msg: getErrText(error)
});
}
}

View File

@@ -0,0 +1,38 @@
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
import { AppModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { HttpExceptionFilter } from './http-exception.filter';
import { ResponseInterceptor } from './response';
async function bootstrap(port: number) {
const app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter());
// 使用全局异常过滤器
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalInterceptors(new ResponseInterceptor());
const config = new DocumentBuilder()
.setTitle('Cats example')
.setDescription('The cats API description')
.setVersion('1.0')
.addTag('cats')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
try {
await app.listen(port, '0.0.0.0');
console.log(`Application is running on: ${await app.getUrl()}`);
} catch (error) {
if (error.code === 'EADDRINUSE') {
console.warn(`Port ${port} is already in use, trying next port...`);
await bootstrap(port + 1);
} else {
console.error(`Failed to start application: ${error.message}`);
process.exit(1);
}
}
}
bootstrap(3000);

View File

@@ -0,0 +1,15 @@
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class ResponseInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => ({
success: true,
data
}))
);
}
}

View File

@@ -0,0 +1,4 @@
export class RunCodeDto {
code: string;
variables: object;
}

View File

@@ -0,0 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing';
import { SandboxController } from './sandbox.controller';
import { SandboxService } from './sandbox.service';
describe('SandboxController', () => {
let controller: SandboxController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [SandboxController],
providers: [SandboxService]
}).compile();
controller = module.get<SandboxController>(SandboxController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@@ -0,0 +1,15 @@
import { Controller, Post, Body, HttpCode } from '@nestjs/common';
import { SandboxService } from './sandbox.service';
import { RunCodeDto } from './dto/create-sandbox.dto';
import { WorkerNameEnum, runWorker } from 'src/worker/utils';
@Controller('sandbox')
export class SandboxController {
constructor(private readonly sandboxService: SandboxService) {}
@Post('/js')
@HttpCode(200)
runJs(@Body() codeProps: RunCodeDto) {
return runWorker(WorkerNameEnum.runJs, codeProps);
}
}

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { SandboxService } from './sandbox.service';
import { SandboxController } from './sandbox.controller';
@Module({
controllers: [SandboxController],
providers: [SandboxService]
})
export class SandboxModule {}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { SandboxService } from './sandbox.service';
describe('SandboxService', () => {
let service: SandboxService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [SandboxService]
}).compile();
service = module.get<SandboxService>(SandboxService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,10 @@
import { Injectable } from '@nestjs/common';
import { RunCodeDto } from './dto/create-sandbox.dto';
import { WorkerNameEnum, runWorker } from 'src/worker/utils';
@Injectable()
export class SandboxService {
runJs(params: RunCodeDto) {
return runWorker(WorkerNameEnum.runJs, params);
}
}

View File

@@ -0,0 +1,14 @@
export const replaceSensitiveText = (text: string) => {
// 1. http link
text = text.replace(/(?<=https?:\/\/)[^\s]+/g, 'xxx');
// 2. nx-xxx 全部替换成xxx
text = text.replace(/ns-[\w-]+/g, 'xxx');
return text;
};
export const getErrText = (err: any, def = '') => {
const msg: string = typeof err === 'string' ? err : err?.message ?? def;
msg && console.log('error =>', msg);
return replaceSensitiveText(msg);
};

View File

@@ -0,0 +1,38 @@
import { RunCodeDto } from 'src/sandbox/dto/create-sandbox.dto';
import { parentPort } from 'worker_threads';
import { workerResponse } from './utils';
// @ts-ignore
const ivm = require('isolated-vm');
parentPort?.on('message', ({ code, variables = {} }: RunCodeDto) => {
const resolve = (data: any) => workerResponse({ parentPort, type: 'success', data });
const reject = (error: any) => workerResponse({ parentPort, type: 'error', data: error });
const isolate = new ivm.Isolate({ memoryLimit: 32 });
const context = isolate.createContextSync();
const jail = context.global;
// custom log function
jail.setSync('responseData', function (args: any): any {
if (typeof args === 'object') {
resolve(args);
} else {
reject('Not an invalid response');
}
});
// Add global variables
jail.setSync('variables', new ivm.ExternalCopy(variables).copyInto());
try {
const scriptCode = `
${code}
responseData(main(variables))`;
context.evalSync(scriptCode, { timeout: 6000 });
} catch (err) {
reject(err);
}
process.exit();
});

View File

@@ -0,0 +1,47 @@
import { type MessagePort, Worker } from 'worker_threads';
import * as path from 'path';
export enum WorkerNameEnum {
runJs = 'runJs',
runPy = 'runPy'
}
type WorkerResponseType = { type: 'success' | 'error'; data: any };
export const getWorker = (name: WorkerNameEnum) => {
const baseUrl =
process.env.NODE_ENV === 'production' ? 'projects/sandbox/dist/worker' : 'dist/worker';
const workerPath = path.join(process.cwd(), baseUrl, `${name}.js`);
return new Worker(workerPath);
};
export const runWorker = <T = any>(name: WorkerNameEnum, params?: Record<string, any>) => {
return new Promise<T>((resolve, reject) => {
const worker = getWorker(name);
worker.postMessage(params);
worker.on('message', (msg: WorkerResponseType) => {
if (msg.type === 'error') return reject(msg.data);
resolve(msg.data);
worker.terminate();
});
worker.on('error', (err) => {
reject(err);
worker.terminate();
});
worker.on('messageerror', (err) => {
reject(err);
worker.terminate();
});
});
};
export const workerResponse = ({
parentPort,
...data
}: WorkerResponseType & { parentPort?: MessagePort }) => {
parentPort?.postMessage(data);
};