Skip to content

Commit

Permalink
merge: [BE] 다른 언어 지원 기능 구현 - 코드 저장 및 실행 #149
Browse files Browse the repository at this point in the history
[BE] feature: 다른 언어 지원 기능 구현 - 코드 저장 및 실행
  • Loading branch information
Gseungmin authored Nov 29, 2023
2 parents 2fc80f3 + 44210af commit 19c13b1
Show file tree
Hide file tree
Showing 22 changed files with 188 additions and 99 deletions.
8 changes: 6 additions & 2 deletions backEnd/api/src/codes/codes.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { ResourceNotFound } from '../common/exception/exception';
import { Serialize } from '../common/interceptor/serialize.interceptor';
import { SaveResultDto } from './dto/saveResult.dto';
import { GetCodeDto } from './dto/getCode.dto';
import { SaveCodePipe } from './pipes/saveCode.pipe';

@UseGuards(JwtAuthGuard)
@Controller('codes')
Expand All @@ -28,7 +29,10 @@ export class CodesController {

@Post()
@Serialize(SaveResultDto)
async save(@Body() saveCodeDto: SaveCodeDto, @Req() req: Request) {
async save(
@Body(SaveCodePipe) saveCodeDto: SaveCodeDto,
@Req() req: Request,
) {
const user: UserInfoDto = req.user as UserInfoDto;
saveCodeDto.userID = user.id;
return await this.codesService.save(saveCodeDto);
Expand Down Expand Up @@ -58,7 +62,7 @@ export class CodesController {
async update(
@Req() req: Request,
@Param('id') id: string,
@Body() saveCodeDto: SaveCodeDto,
@Body(SaveCodePipe) saveCodeDto: SaveCodeDto,
) {
const user: UserInfoDto = req.user as UserInfoDto;
const userID = user.id;
Expand Down
3 changes: 2 additions & 1 deletion backEnd/api/src/codes/codes.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { MongooseModule } from '@nestjs/mongoose';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { Code, CodeSchema } from './schemas/code.schemas';
import { AuthModule } from '../auth/auth.module';
import { SaveCodePipe } from './pipes/saveCode.pipe';

@Module({
imports: [
Expand All @@ -22,6 +23,6 @@ import { AuthModule } from '../auth/auth.module';
AuthModule,
],
controllers: [CodesController],
providers: [CodesService],
providers: [CodesService, SaveCodePipe],
})
export class CodesModule {}
4 changes: 4 additions & 0 deletions backEnd/api/src/codes/dto/getCode.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Expose } from 'class-transformer';
import { supportLang } from '../../common/type';

export class GetCodeDto {
@Expose()
Expand All @@ -9,4 +10,7 @@ export class GetCodeDto {

@Expose()
content: string;

@Expose()
language: supportLang;
}
4 changes: 4 additions & 0 deletions backEnd/api/src/codes/dto/saveCode.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { IsString } from 'class-validator';
import { supportLang } from '../../common/type';

export class SaveCodeDto {
userID?: number;
Expand All @@ -8,4 +9,7 @@ export class SaveCodeDto {

@IsString()
content: string;

@IsString()
language: supportLang;
}
19 changes: 19 additions & 0 deletions backEnd/api/src/codes/pipes/saveCode.pipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { PipeTransform, Injectable } from '@nestjs/common';
import * as path from 'path';
import { SaveCodeDto } from '../dto/saveCode.dto';
import { ExtNameException } from '../../common/exception/exception';

const languageExtName = {
'.js': 'javascript',
'.py': 'python',
};

@Injectable()
export class SaveCodePipe implements PipeTransform {
transform(value: SaveCodeDto) {
if (languageExtName[path.extname(value.title)] !== value.language) {
throw new ExtNameException();
}
return value;
}
}
4 changes: 4 additions & 0 deletions backEnd/api/src/codes/schemas/code.schemas.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';
import { supportLang } from '../../common/type';

export type CodeDocument = HydratedDocument<Code>;

Expand All @@ -13,6 +14,9 @@ export class Code {

@Prop({ require: true })
content: string;

@Prop({ require: true, default: 'python' })
language: supportLang;
}

export const CodeSchema = SchemaFactory.createForClass(Code);
6 changes: 6 additions & 0 deletions backEnd/api/src/common/exception/exception.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,9 @@ export class ResourceNotFound extends HttpException {
super(message, HttpStatus.BAD_REQUEST);
}
}

export class ExtNameException extends HttpException {
constructor(message = '저장 파일의 확장자와 설정 언어가 일치하지 않습니다.') {
super(message, HttpStatus.BAD_REQUEST);
}
}
1 change: 1 addition & 0 deletions backEnd/api/src/common/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type supportLang = 'python' | 'javascript';
11 changes: 9 additions & 2 deletions backEnd/api/src/common/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import * as process from 'process';
import { supportLang } from './type';
export const supportLangEnum = {
PYTHON: 'python',
JAVASCRIPT: 'javascript',
};

export const requestPath = {
RUN_PYTHON: '/codes/python',
export const requestPath: Record<supportLang, string> = {
python: '/codes/python',
javascript: '/codes/js',
};

const timeUnit = {
Expand Down Expand Up @@ -53,6 +59,7 @@ export const jwtError = {
export const ResponseMessage = {
NEED_LOGIN: '로그인이 필요합니다.',
INTERNAL_SERVER_ERROR: 'Internal server error',
NOT_SUPPORT: '지원하지 않는 언어 타입입니다.',
};

export const SOCKET_EVENT = {
Expand Down
6 changes: 3 additions & 3 deletions backEnd/api/src/mq/mq.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
import { InjectQueue } from '@nestjs/bull';
import { Queue } from 'bull';
import { ConfigService } from '@nestjs/config';
import { RequestCodeblockDto } from '../run/dto/request-codeblock.dto';
import { RequestCodeBlockDto } from '../run/dto/request-codeblock.dto';
import Redis from 'ioredis';
import { EVENT, REDIS } from '../common/utils';
import { EventEmitter2 } from '@nestjs/event-emitter';
Expand Down Expand Up @@ -47,8 +47,8 @@ export class MqService {
});
}

async addMessage(data: RequestCodeblockDto) {
const job = await this.queue.add(REDIS.QUEUE, data.code, {
async addMessage(data: RequestCodeBlockDto) {
const job = await this.queue.add(REDIS.QUEUE, data, {
removeOnComplete: true,
});
this.logger.log(`push to task Queue ${job.id}`);
Expand Down
6 changes: 5 additions & 1 deletion backEnd/api/src/run/dto/request-codeblock.dto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { IsString } from 'class-validator';
import { supportLang } from '../../common/type';

export class RequestCodeblockDto {
export class RequestCodeBlockDto {
@IsString()
code: string;

@IsString()
language: supportLang;
}
14 changes: 14 additions & 0 deletions backEnd/api/src/run/pipes/saveCode.pipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { PipeTransform, Injectable } from '@nestjs/common';
import { ExtNameException } from '../../common/exception/exception';
import { RequestCodeBlockDto } from '../dto/request-codeblock.dto';
import { ResponseMessage } from '../../common/utils';

@Injectable()
export class RequestRunPipe implements PipeTransform {
transform(value: RequestCodeBlockDto) {
if (!['python', 'javascript'].includes(value.language)) {
throw new ExtNameException(ResponseMessage.NOT_SUPPORT);
}
return value;
}
}
45 changes: 20 additions & 25 deletions backEnd/api/src/run/run.controller.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Body, Controller, Get, HttpCode, Post, Query } from '@nestjs/common';
import { RunService } from './run.service';
import { RequestCodeblockDto } from './dto/request-codeblock.dto';
import { RequestCodeBlockDto } from './dto/request-codeblock.dto';
import { returnCode } from '../common/returnCode';
import { VulnerableException } from '../common/exception/exception';
import { MqService } from '../mq/mq.service';
import { RedisService } from '../redis/redis.service';
import { RequestRunPipe } from './pipes/saveCode.pipe';

@Controller('run')
export class RunController {
Expand All @@ -16,53 +17,47 @@ export class RunController {
) {}
@HttpCode(200)
@Post('v1')
async requestRunCode(@Body() codeBlock: RequestCodeblockDto) {
const { code } = codeBlock;
const securityCheck = this.runService.securityCheck(code);
if (securityCheck === returnCode['vulnerable']) {
// fail
throw new VulnerableException();
}

async requestRunCode(@Body(RequestRunPipe) codeBlock: RequestCodeBlockDto) {
this.securityCheck(codeBlock);
const responseCodeBlockDto =
await this.runService.requestRunningApi(codeBlock);
return responseCodeBlockDto;
}

@HttpCode(200)
@Post('v2')
async requestRunCodeV2(@Body() codeBlock: RequestCodeblockDto) {
const { code } = codeBlock;
const securityCheck = this.runService.securityCheck(code);
if (securityCheck === returnCode['vulnerable']) {
// fail
throw new VulnerableException();
}
async requestRunCodeV2(@Body(RequestRunPipe) codeBlock: RequestCodeBlockDto) {
this.securityCheck(codeBlock);

const responseCodeBlockDto =
await this.runService.requestRunningMQ(codeBlock);

return responseCodeBlockDto;
}

@Get('avgTime')
showAvgTrialTime() {
return this.redisService.showTrialTimeAvg();
}

@HttpCode(202)
@Post('v3')
async requestRunCodeV3(
@Query('id') socketID: string,
@Body() codeBlock: RequestCodeblockDto,
@Body(RequestRunPipe) codeBlock: RequestCodeBlockDto,
): Promise<void> {
const { code } = codeBlock;
const securityCheck = this.runService.securityCheck(code);
this.securityCheck(codeBlock);

await this.runService.requestRunningMQPubSub(codeBlock, socketID);
}

securityCheck(codeBlock: RequestCodeBlockDto) {
const { code, language } = codeBlock;
const securityCheck = this.runService.securityCheck(code, language);

if (securityCheck === returnCode['vulnerable']) {
// fail
throw new VulnerableException();
}
}

await this.runService.requestRunningMQPubSub(codeBlock, socketID);
@Get('avgTime')
showAvgTrialTime() {
return this.redisService.showTrialTimeAvg();
}
}
36 changes: 25 additions & 11 deletions backEnd/api/src/run/run.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
Logger,
} from '@nestjs/common';
import { returnCode } from '../common/returnCode';
import { RequestCodeblockDto } from './dto/request-codeblock.dto';
import { RequestCodeBlockDto } from './dto/request-codeblock.dto';
import axios from 'axios';
import { ConfigService } from '@nestjs/config';
import { requestPath } from '../common/utils';
Expand All @@ -14,6 +14,7 @@ import { MqService } from '../mq/mq.service';
import { RedisService } from '../redis/redis.service';
import { TimeoutCodeRunning } from '../common/exception/exception';
import { ResponseCodeBlockDto } from './dto/response-codeblock.dto';
import { supportLang } from '../common/type';

@Injectable()
export class RunService {
Expand All @@ -24,7 +25,16 @@ export class RunService {
private mqService: MqService,
private redisService: RedisService,
) {}
securityCheck(data): number {
securityCheck(code: string, language: supportLang): number {
switch (language) {
case 'python':
return this.pythonCheck(code);
case 'javascript':
return this.javascriptCheck(code);
}
}

pythonCheck(code: string) {
// 모듈 제한
const blockedModules = [
'os',
Expand All @@ -51,31 +61,35 @@ export class RunService {

// check
for (const pattern of [...blockModulesPattern, ...inputPattern]) {
if (pattern.test(data)) {
this.logger.warn(`⚠️Invalid Code Requested⚠️\n${data}`);
if (pattern.test(code)) {
this.logger.warn(`⚠️Invalid Code Requested⚠️\n${code}`);
return returnCode['vulnerable'];
}
}

return returnCode['safe'];
}

javascriptCheck(code: string) {
return returnCode['safe'];
}

async requestRunningApi(
codeBlock: RequestCodeblockDto,
codeBlock: RequestCodeBlockDto,
): Promise<ResponseCodeBlockDto> {
const url =
'http://' +
path.join(
this.configService.get<string>('RUNNING_SERVER'),
requestPath.RUN_PYTHON,
requestPath[codeBlock.language],
);
// console.log(url);

try {
const result = await axios.post(url, codeBlock);
return new ResponseCodeBlockDto(
result.status,
result.data.output,
'Running Python Code Success',
'Running Code Success',
);
} catch (e) {
this.logger.error(e.message);
Expand All @@ -91,7 +105,7 @@ export class RunService {
}

async requestRunningMQ(
codeBlock: RequestCodeblockDto,
codeBlock: RequestCodeBlockDto,
): Promise<ResponseCodeBlockDto> {
const job = await this.mqService.addMessage(codeBlock);
this.logger.log(`added message queue job#${job.id}`);
Expand All @@ -102,11 +116,11 @@ export class RunService {
this.logger.error(`${job.id} failed to find completed job : ${result}`);
throw new TimeoutCodeRunning();
}
this.logger.log(`get completed result ${result}`);
this.logger.log(`get completed result ${JSON.stringify(result)}`);
return result;
}
async requestRunningMQPubSub(
codeBlock: RequestCodeblockDto,
codeBlock: RequestCodeBlockDto,
socketID: string,
): Promise<void> {
const job = await this.mqService.addMessage(codeBlock);
Expand Down
11 changes: 5 additions & 6 deletions backEnd/running/src/codes/codes.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@ export class CodesController {
constructor(private readonly codesService: CodesService) {}

@Post('/python')
async runPython(@Body() data: RequestCodeDto) {
return await this.codesService.testCode(data.code);
async runPython(@Body() codeBlock: RequestCodeDto) {
return await this.codesService.runCode(codeBlock);
}

@Post('/test')
async test() {
const code = 'N,M = 5,6; print(N+M)';
return await this.codesService.testCode(code);
@Post('/js')
async runJavascript(@Body() codeBlock: RequestCodeDto) {
return await this.codesService.runCode(codeBlock);
}
}
Loading

0 comments on commit 19c13b1

Please sign in to comment.