diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-07 05:04:39 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-07 05:04:39 +0000 |
| commit | e270e477f362dd68249bb4a013c66eab293bba82 (patch) | |
| tree | ef29ec732fc62c220ca2f6ab12aa282b0db7c9ab /pages/api | |
| parent | a5cecbef039e29beaa616e3a36e7f15b8b35623c (diff) | |
(최겸) PQ요청+기본계약 로직 수정(한글화 미적용)
Diffstat (limited to 'pages/api')
| -rw-r--r-- | pages/api/pdftron/createBasicContractPdf.ts | 359 |
1 files changed, 359 insertions, 0 deletions
diff --git a/pages/api/pdftron/createBasicContractPdf.ts b/pages/api/pdftron/createBasicContractPdf.ts new file mode 100644 index 00000000..1122c022 --- /dev/null +++ b/pages/api/pdftron/createBasicContractPdf.ts @@ -0,0 +1,359 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import type { File as FormidableFile } from "formidable"; +import formidable from "formidable"; +import fs from "fs/promises"; +import path from "path"; +import { createBasicContractPdf } from "@/lib/pdftron/serverSDK/createBasicContractPdf"; + +export const config = { + api: { + bodyParser: false, + }, +}; + +// 보안 설정 +const SECURITY_CONFIG = { + ALLOWED_EXTENSIONS: new Set(['docx', 'doc']), + FORBIDDEN_EXTENSIONS: new Set([ + 'exe', 'bat', 'cmd', 'scr', 'vbs', 'js', 'jar', 'com', 'pif', + 'msi', 'reg', 'ps1', 'sh', 'php', 'asp', 'jsp', 'py', 'pl', + 'html', 'htm', 'xhtml', 'xml', 'svg' + ]), + MAX_FILE_SIZE: 50 * 1024 * 1024, // 50MB + MAX_FILENAME_LENGTH: 255, +}; + +// 간단한 보안 검증 함수들 +function validateExtension(fileName: string): { valid: boolean; error?: string } { + const extension = path.extname(fileName).toLowerCase().substring(1); + + if (!extension) { + return { valid: false, error: "파일 확장자가 없습니다" }; + } + + if (SECURITY_CONFIG.FORBIDDEN_EXTENSIONS.has(extension)) { + return { valid: false, error: `금지된 파일 형식입니다: .${extension}` }; + } + + if (!SECURITY_CONFIG.ALLOWED_EXTENSIONS.has(extension)) { + return { valid: false, error: `허용되지 않은 파일 형식입니다: .${extension}` }; + } + + return { valid: true }; +} + +function validateFileName(fileName: string): { valid: boolean; error?: string } { + if (fileName.length > SECURITY_CONFIG.MAX_FILENAME_LENGTH) { + return { valid: false, error: "파일명이 너무 깁니다" }; + } + + // 위험한 문자 체크 + const dangerousPatterns = [ + /[<>:"'|?*]/, + /[\x00-\x1f]/, + /^\./, + /\.\./, + /\/|\\$/, + /javascript:/i, + /data:/i, + /vbscript:/i, + /on\w+=/i, + /<script/i, + /<iframe/i, + ]; + + for (const pattern of dangerousPatterns) { + if (pattern.test(fileName)) { + return { valid: false, error: "안전하지 않은 파일명입니다" }; + } + } + + return { valid: true }; +} + +function validateFileSize(size: number): { valid: boolean; error?: string } { + if (size <= 0) { + return { valid: false, error: "파일이 비어있습니다" }; + } + + if (size > SECURITY_CONFIG.MAX_FILE_SIZE) { + const maxSizeMB = Math.round(SECURITY_CONFIG.MAX_FILE_SIZE / (1024 * 1024)); + return { valid: false, error: `파일 크기가 너무 큽니다 (최대 ${maxSizeMB}MB)` }; + } + + return { valid: true }; +} + +function validateTemplateData(templateData: any): { valid: boolean; error?: string } { + try { + if (typeof templateData !== 'object' || templateData === null) { + return { valid: false, error: "템플릿 데이터는 객체여야 합니다" }; + } + + // 객체 크기 제한 + const dataString = JSON.stringify(templateData); + if (dataString.length > 100000) { // 100KB + return { valid: false, error: "템플릿 데이터가 너무 큽니다" }; + } + + // XSS 방지 검증 + const dangerousPatterns = [ + /<script[\s\S]*?>/i, + /<iframe[\s\S]*?>/i, + /javascript\s*:/i, + /vbscript\s*:/i, + /on\w+\s*=/i, + ]; + + for (const pattern of dangerousPatterns) { + if (pattern.test(dataString)) { + return { valid: false, error: "템플릿 데이터에 잠재적으로 위험한 스크립트가 포함되어 있습니다" }; + } + } + + return { valid: true }; + } catch (error) { + return { valid: false, error: "템플릿 데이터 검증 중 오류가 발생했습니다" }; + } +} + +async function validateFileContent(buffer: Buffer, fileName: string): Promise<{ valid: boolean; error?: string }> { + try { + // 실행 파일 패턴 검색 + const executablePatterns = [ + Buffer.from([0x4D, 0x5A]), // MZ (Windows executable) + Buffer.from([0x7F, 0x45, 0x4C, 0x46]), // ELF (Linux executable) + ]; + + for (const pattern of executablePatterns) { + if (buffer.subarray(0, pattern.length).equals(pattern)) { + return { valid: false, error: "실행 파일은 업로드할 수 없습니다" }; + } + } + + return { valid: true }; + } catch (error) { + console.error("파일 내용 검증 오류:", error); + return { valid: false, error: "파일 내용을 검증할 수 없습니다" }; + } +} + +// DRM 복호화 함수 (create report 로직과 동일) +async function decryptBufferWithDRM(buffer: Buffer, originalFileName: string): Promise<Buffer> { + try { + // 1. 기본 보안 검증 + const extensionCheck = validateExtension(originalFileName); + if (!extensionCheck.valid) { + console.error(`🚨 파일 확장자 보안 위반: ${originalFileName} - ${extensionCheck.error}`); + throw new Error(extensionCheck.error); + } + + const fileNameCheck = validateFileName(originalFileName); + if (!fileNameCheck.valid) { + console.error(`🚨 파일명 보안 위반: ${originalFileName} - ${fileNameCheck.error}`); + throw new Error(fileNameCheck.error); + } + + const sizeCheck = validateFileSize(buffer.length); + if (!sizeCheck.valid) { + console.error(`🚨 파일 크기 보안 위반: ${originalFileName} - ${sizeCheck.error}`); + throw new Error(sizeCheck.error); + } + + // 2. 파일 내용 기본 검증 + const contentCheck = await validateFileContent(buffer, originalFileName); + if (!contentCheck.valid) { + console.error(`🚨 파일 내용 보안 위반: ${originalFileName} - ${contentCheck.error}`); + throw new Error(contentCheck.error); + } + + console.log(`✅ 보안 검증 완료: ${originalFileName}`); + + // 3. DRM 복호화 진행 + const blob = new Blob([buffer]); + const file = new File([blob], originalFileName); + + const formData = new FormData(); + formData.append('file', file); + + const backendUrl = "http://localhost:6543/api/drm-proxy/decrypt"; + + console.log(`[DRM] 서버에서 파일 복호화 시도: ${originalFileName} (크기: ${buffer.length} bytes)`); + + const response = await fetch(backendUrl, { + method: "POST", + body: formData, + }); + + if (!response.ok) { + const errorText = await response.text().catch(() => '응답 텍스트를 가져올 수 없음'); + throw new Error(`DRM 서버 응답 오류 [${response.status}]: ${errorText}`); + } + + const arrayBuffer = await response.arrayBuffer(); + const decryptedBuffer = Buffer.from(arrayBuffer); + + console.log(`[DRM] 서버에서 파일 복호화 성공: ${originalFileName} (결과 크기: ${decryptedBuffer.length} bytes)`); + + return decryptedBuffer; + + } catch (error) { + const errorMessage = error instanceof Error + ? `${error.name}: ${error.message}` + : String(error); + + console.error(`[DRM] 서버 복호화 오류: ${errorMessage}`, { + fileName: originalFileName, + fileSize: buffer.length, + error + }); + + // 보안 검증 실패인 경우 원본도 반환하지 않음 + if (errorMessage.includes('보안') || errorMessage.includes('위반') || errorMessage.includes('금지된') || errorMessage.includes('허용되지')) { + throw error; // 보안 오류는 재throw + } + + return buffer; // 일반적인 DRM 오류는 원본 버퍼 반환 (폴백) + } +} + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== "POST") { + return res.status(405).json({ error: "Method not allowed" }); + } + + // 요청 ID 생성 (로깅용) + const requestId = `basiccontract_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + console.log(`🚀 [${requestId}] 기본계약서 PDF 변환 요청 시작`); + + try { + const form = formidable({ + multiples: false, + maxFileSize: SECURITY_CONFIG.MAX_FILE_SIZE, + maxFieldsSize: 10 * 1024 * 1024, // 10MB 필드 크기 제한 + maxFields: 20, // 최대 필드 수 제한 + }); + + form.parse(req, async (err, fields, files) => { + if (err) { + console.error(`❌ [${requestId}] Form parsing 오류:`, err); + return res.status(500).json({ + error: "파일 업로드 처리 중 오류가 발생했습니다", + requestId + }); + } + + try { + const outputFileName = fields?.outputFileName?.[0] ?? ""; + const templateFile: FormidableFile | undefined = files?.templateFile?.[0]; + + // 1. 기본 필드 검증 + if (!templateFile || outputFileName.length === 0) { + return res.status(400).json({ + error: "필수 데이터가 누락되었습니다 (템플릿 파일, 출력 파일명)", + requestId + }); + } + + // 2. 파일명 보안 검증 + const fileNameCheck = validateFileName(outputFileName); + if (!fileNameCheck.valid) { + console.error(`🚨 [${requestId}] 출력 파일명 보안 위반:`, fileNameCheck.error); + return res.status(400).json({ + error: `출력 파일명 오류: ${fileNameCheck.error}`, + requestId + }); + } + + // 3. 템플릿 데이터 파싱 및 검증 + let templateData: any = {}; + try { + const templateDataString = fields?.templateData?.[0] ?? "{}"; + templateData = JSON.parse(templateDataString); + } catch (parseError) { + console.error(`❌ [${requestId}] 템플릿 데이터 파싱 오류:`, parseError); + return res.status(400).json({ + error: "템플릿 데이터 형식이 올바르지 않습니다", + requestId + }); + } + + const dataValidation = validateTemplateData(templateData); + if (!dataValidation.valid) { + console.error(`🚨 [${requestId}] 템플릿 데이터 보안 위반:`, dataValidation.error); + return res.status(400).json({ + error: `템플릿 데이터 오류: ${dataValidation.error}`, + requestId + }); + } + + console.log(`✅ [${requestId}] 모든 보안 검증 완료 - 파일 처리 시작`); + + // 4. 원본 파일 읽기 + const originalBuffer = await fs.readFile(templateFile.filepath); + + // 5. DRM 복호화 처리 (보안 검증 포함) + console.log(`🔐 [${requestId}] DRM 복호화 시작: ${templateFile.originalFilename || 'unknown'}`); + const decryptedBuffer = await decryptBufferWithDRM( + originalBuffer, + templateFile.originalFilename || 'template.docx' + ); + + // 6. 복호화된 버퍼로 기본계약서 PDF 생성 + console.log(`📄 [${requestId}] 기본계약서 PDF 생성 시작`); + const { + result, + buffer: pdfBuffer, + error, + } = await createBasicContractPdf(decryptedBuffer, templateData); + + if (result && pdfBuffer) { + console.log(`✅ [${requestId}] 기본계약서 PDF 생성 성공: ${outputFileName}`); + + // 보안 헤더 설정 + res.setHeader("Content-Type", "application/pdf"); + + // 한글 파일명을 위한 UTF-8 인코딩 + const encodedFileName = encodeURIComponent(outputFileName); + res.setHeader("Content-Disposition", `attachment; filename*=UTF-8''${encodedFileName}`); + + res.setHeader("X-Content-Type-Options", "nosniff"); + res.setHeader("X-Frame-Options", "DENY"); + res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + + return res.send(Buffer.from(pdfBuffer)); + } + + console.error(`❌ [${requestId}] 기본계약서 PDF 생성 실패:`, error); + return res.status(500).json({ + success: false, + message: "기본계약서 PDF 생성에 실패하였습니다.", + error, + requestId, + }); + + } catch (e) { + console.error(`❌ [${requestId}] 처리 중 오류:`, e); + + // 보안 오류와 일반 오류 구분 + const errorMessage = e instanceof Error ? e.message : "처리 중 오류가 발생했습니다"; + const isSecurityError = errorMessage.includes('보안') || errorMessage.includes('위반') || + errorMessage.includes('금지된') || errorMessage.includes('허용되지'); + + return res.status(isSecurityError ? 400 : 500).json({ + error: isSecurityError ? errorMessage : "처리 중 오류가 발생했습니다", + requestId + }); + } + }); + } catch (err) { + console.error(`❌ [${requestId}] 전역 오류:`, err); + return res.status(500).json({ + error: "서버 내부 오류가 발생했습니다", + requestId + }); + } +} |
