/** * @author : 하석형 * @since : 2023.08.24 * @dscription : Express 라이브러리 활용 HTTP Web Server 모듈입니다. */ const { BASE_DIR, PORT, API_SERVER_HOST } = require("../../../Global"); const Logger = require("../log/Logger"); //Logger(필수) const express = require("express"); const webServer = express(); const expressProxy = require("express-http-proxy"); //파일 시스템 관련 라이브러리 const FS = require("fs"); //stream: 특정 자원을 Streaming 하기 위한 라이브러리 => Transform: Streaming 중인 자원의 Data에 Data 수정 및 추가를 지원해주는 객체 const Transform = require("stream").Transform; //Streaming 중인 자원에 새로운 데이터를 stream 공간에 추가하기 위한 라이브러리 const newLineStream = require("new-line"); const axios = require('axios'); // 정적 파일 제공을 위한 설정 const path = require('path'); webServer.use(express.static('public')); webServer.timeout = 200000; // 정상적으로 동작하는 호스트를 찾아 요청을 보내는 미들웨어 webServer.use(async function (req, res, next) { // 작업페이지 // res.status(503).sendFile(path.join(__dirname, 'public', 'error.html')); // return; const selectedServer = await findHealthyHost(); if (!selectedServer) { res.status(503).send('All servers are down'); // res.status(503).sendFile(path.join(__dirname, 'public', 'error.html')); return; } // 선택된 호스트로 요청을 보냄 req.selectedServer = selectedServer.inside; req.selectedOutSideServer = selectedServer.outside; next(); }); // Health Check 함수 async function checkHealth(serverUrl) { try { // Health Check API 호출 const response = await axios.get(`${serverUrl.inside}/cmmn/health-check.json`,{ timeout : 500, }); // HTTP 상태 코드가 200인 경우에만 정상 동작으로 판단 if (response.status === 200) { return true; } } catch (error) { console.error(`Health check failed for server ${serverUrl.inside}: ${error.message}`); } return false; } // 배열을 무작위로 섞는 함수 function shuffle(array) { const shuffledArray = array.slice(); // 원본 배열 복사 for (let i = shuffledArray.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]]; // 요소들을 무작위로 섞음 } return shuffledArray; } webServer.use((req, res, next) => { res.header("X-Frame-Options", "SAMEORIGIN"); // or 'SAMEORIGIN' or 'ALLOW-FROM uri' next(); }); // 모든 호스트에 대한 헬스 체크를 수행하여 정상적으로 동작하는 호스트를 찾음 async function findHealthyHost() { let healthyHost = null; const shuffledHosts = shuffle(API_SERVER_HOST); // 호스트를 무작위로 섞음 // 각 호스트에 대한 헬스 체크를 수행 for (const serverUrl of shuffledHosts) { if (await checkHealth(serverUrl)) { // 현재 호스트가 정상적으로 동작하면 선택하고 루프를 종료 healthyHost = serverUrl; break; } } return healthyHost; } /** * @author : 하석형 * @since : 2023.08.24 * @dscription : HTTP Server start */ webServer.listen(PORT, function () { Logger.logging(`★★★ Node.js를 활용한 Web Server 구동(Port:${PORT}) ★★★`); }); /** * @author : 하석형 * @since : 2023.08.24 * @dscription : Intercepter 역할을 하는 미들웨어 기능 */ webServer.use(function (request, response, next) { let ip = request.headers["x-forwarded-for"] || request.socket.remoteAddress; Logger.logging( `[HTTP] ${request.url} (Method: ${request.method}, IP: ${ip})` ); next(); }); /** * @author : 김성원 * @since : 2024.05.30 * @dscription : robots.txt */ webServer.get("/robots.txt", function (request, response) { //response.sendFile을 통한 HTTP html reponse (html내용 Streaming) response.sendFile(`${BASE_DIR}/client/views/robots.txt`); }); /** * @author : 하석형 * @since : 2023.08.24 * @dscription : ROOT URL -> index.html */ webServer.get("/", function (request, response) { //response.sendFile을 통한 HTTP html reponse (html내용 Streaming) response.sendFile(`${BASE_DIR}/client/views/index.html`); }); /** * @author : 하석형 * @since : 2023.08.24 * @dscription : 화면요청 URL 처리 */ webServer.get("*.page", function (request, response, next) { //index.html 내용을 직접 Streaming하여 Response, Streaming 중간에 내용 수정 //수정 내용 : URL 요청이 아닌, 브라우저에 표시된 URL만 변경하여, 해당하는 URL PATH의 Vue Component를 routing하기 위함 const StreamTransform = new Transform(); StreamTransform._transform = function (data, encoding, done) { let fileContent = data.toString(); let replaceBeforeContent = ``; let replaceAfterContent = ``; fileContent.replace(replaceBeforeContent, replaceAfterContent); this.push(fileContent); done(); }; //Streaming 진행 FS.createReadStream(`${BASE_DIR}/client/views/index.html`) .pipe(newLineStream()) .pipe(StreamTransform) .pipe(response); }); /** * @author : 김성훈 * @since : 2024.04.16 * @dscription : 이미지 불러오기 */ webServer.use('/nas_data/files/*', expressProxy(function (req, res) { return req.selectedServer; }, { proxyReqPathResolver: function (request) { // Logger.logging('/AIDT/FILES/* : ' + `/AIDT/FILES/${request.params['0']}`); return `/nas_data/files/${request.params['0']}`; } })); /** * @author : 하석형 * @since : 2023.08.24 * @dscription : REST API 서버에 데이터 요청 보내기(Proxy) */ webServer.use('*.json', expressProxy(function (req, res) { // Logger.logging('json selectedServer : ' + req.selectedServer); return req.selectedServer; }, { timeout: 120000, proxyReqPathResolver: function (request) { return `${request.params['0']}.json`; }, proxyReqOptDecorator: function (proxyReqOpts, srcReq) { proxyReqOpts.headers['X-Forwarded-For'] = srcReq.headers['x-forwarded-for'] || srcReq.connection.remoteAddress; return proxyReqOpts; } })); /** * @author : 하석형 * @since : 2023.08.24 * @dscription : REST API 서버에 데이터 요청 보내기(Proxy) */ // webServer.use( // "*.json", // expressProxy(API_SERVER_HOST, { // proxyReqPathResolver: function (request) { // return `${request.params["0"]}.json`; // }, // proxyReqOptDecorator: function (proxyReqOpts, srcReq) { // proxyReqOpts.headers['X-Forwarded-For'] = srcReq.headers['x-forwarded-for'] || srcReq.connection.remoteAddress; // return proxyReqOpts; // } // }) // ); /** * @author : 하석형 * @since : 2023.08.24 * @dscription : REST API 서버에 데이터 요청 보내기(Proxy) */ webServer.use('*.file', expressProxy(function (req) { // Logger.logging('file selectedServer : ' + req.selectedServer); return req.selectedServer; }, { parseReqBody : false, proxyReqPathResolver: function (request) { return `${request.params['0']}.file`; } })); // /** // * @author : 하석형 // * @since : 2023.08.24 // * @dscription : REST API 서버에 데이터 요청 보내기(Proxy) // */ // webServer.use( // "*.file", // expressProxy(API_SERVER_HOST, { // parseReqBody: false, // proxyReqPathResolver: function (request) { // console.log("request : ", request.url, request.params[0]); // return `${request.params["0"]}.file`; // }, // }) // ); /** * @author : 하석형 * @since : 2023.08.24 * @dscription : ROOT URL, Router's, 화면요청 URL 등.. 이 외 나머지 정적 자원에 대한 처리 기능 */ webServer.get("*.*", function (request, response, next) { response.sendFile(`${BASE_DIR}${request.params["0"]}.${request.params["1"]}`); }); /** * @author : 김성훈 * @since : 2024.04.19 * @dscription : 404 오류 처리 */ webServer.use(function(req, res, next) { res.status(404).redirect('/notFound.page'); }); /** * @author : 하석형 * @since : 2023.08.24 * @dscription : Global Error Handler (*맨 마지막에 적용해야됨) */ webServer.use(function (error, request, response, next) { const errorCode = !error.statusCode ? 500 : error.statusCode; //response // .status(errorCode) // .send("에러가 발생하였습니다. 관리자에게 문의바랍니다."); let message = `[Error:${errorCode}] ${request.url}/n ${error.stack}/n`; Logger.logging(message); response.status(errorCode).redirect('/notFound.page'); //next(); });