ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [ NodeJS 기본 ] 파일 입출력 구현과 보안
    Node.js/nodeJS 기본 2023. 3. 20. 17:37
    반응형

      아래는 사용자 입력에 대한 출력 및 수정 삭제를 구현한 코드입니다. 홈에서는 쓰여진 글의 목록을 출력 및 글쓰기 화면으로의 접근, 쓰여진 글 보기 등 을 가능하게 했으며, 각 글에 접근해 수정 혹은 삭제가 가능도록 하였습니다.

     

    let http = require("http");
    let fs = require("fs");
    let URL = require("url");
    let qs = require("querystring");
    let path = require("path");
    let sanitizeHtml = require("sanitize-html");
    let tp = require("./lib/template.js");
    
    let app = http
      .createServer((req, res) => {
        let parse = URL.parse(req.url, true);
        let pathname = parse.pathname;
        let query = parse.query;
        let filteredTitle;
    
        if (query.title) filteredTitle = path.parse(query.title).base;
    
        if (pathname === "/") {
          fs.readdir(__dirname + "/data", (err, files) => {
            navigate(res, err, tp.home(files));
          });
        } else if (pathname === "/article") {
          fs.readFile(
            __dirname + `/data/${filteredTitle}`,
            "utf-8",
            (err, data) => {
              console.log(data, query.title);
              navigate(
                res,
                err,
                tp.article(sanitizeHtml(query.title), sanitizeHtml(data))
              );
            }
          );
        } else if (pathname === "/update") {
          fs.readFile(
            __dirname + `/data/${filteredTitle}`,
            "utf-8",
            (err, data) => {
              navigate(
                res,
                err,
                tp.update(sanitizeHtml(query.title), sanitizeHtml(data))
              );
            }
          );
        } else if (pathname === "/src") {
          fs.readFile(__dirname + `/src/${filteredTitle}`, "utf-8", (err, data) => {
            navigate(res, err, data);
          });
        } else if (pathname === "/process_write") {
          let body = "";
    
          req.on("data", (data) => {
            body += data;
          });
    
          req.on("end", () => {
            let post = qs.parse(body);
    
            fs.writeFile(
              __dirname + `/data/${post.title}`,
              post.description,
              (err) => {
                redir(res, err, "/");
              }
            );
          });
        } else if (pathname === "/process_delete") {
          fs.unlink(__dirname + `/data/${filteredTitle}`, (err) => {
            redir(res, err, "/");
          });
        } else if (pathname === "/process_update") {
          let body = "";
    
          req.on("data", (data) => {
            body += data;
          });
    
          req.on("end", () => {
            let post = qs.parse(body);
    
            fs.rename(
              __dirname + `/data/${filteredTitle}`,
              __dirname + `/data/${post.title}`,
              (err) => {
                fs.writeFile(
                  __dirname + `/data/${post.title}`,
                  post.description,
                  (err) => {
                    redir(res, err, "/");
                  }
                );
              }
            );
          });
        } else {
          res.writeHead(404);
          res.end("404 not found");
          return;
        }
      })
      .listen(5000);
    
    let navigate = function (res, err, callback) {
      if (err) {
        res.writeHead(404);
        res.end("404 not found");
      } else {
        res.writeHead(200);
        res.end(callback);
      }
      return;
    };
    
    let redir = function (res, err, location) {
      if (err) {
        res.writeHead(404);
        res.end(`ERROR : ${err}`);
      } else {
        res.writeHead(302, { Location: location });
        res.end();
      }
      return;
    };

     

    1. navigate

      각 페이지로의 이동은 request요청의 url의 path를 통해 이루어집니다. 

     

    if (pathname === "/") {
          fs.readdir(__dirname + "/data", (err, files) => {
            navigate(res, err, tp.home(files));
          });
        }

     

      홈의 경우 data디렉토리의 파일들을 읽어 각 데이터 목록을 출력합니다. 

     

    else if (pathname === "/article") {
          fs.readFile(
            __dirname + `/data/${filteredTitle}`,
            "utf-8",
            (err, data) => {
              navigate(
                res,
                err,
                tp.article(sanitizeHtml(query.title), sanitizeHtml(data))
              );
            }
          );
        }

     

      각 글에대한 접근의 경우 url의 path와 query string을 읽어 동작 방식과 읽어올 글을 특정합니다. query string으로 얻은 제목을 통해 해당 글을 읽어 본문 내용을 얻은 뒤 이를 미리 정의해 둔 템플릿을 이용해 출력합니다.

     

    else if (pathname === "/update") {
          fs.readFile(
            __dirname + `/data/${filteredTitle}`,
            "utf-8",
            (err, data) => {
              navigate(
                res,
                err,
                tp.update(sanitizeHtml(query.title), sanitizeHtml(data))
              );
            }
          );
        }

     

      글 수정 또한 앞선 경우와 유사하게 작동합니다. path를 통해 동작 방식을 특정하고 query string을 통해 수정할 데이터에 관련된 템플릿을 제공합니다.

     

    2. src loading

     

    else if (pathname === "/src") {
          fs.readFile(__dirname + `/src/${filteredTitle}`, "utf-8", (err, data) => {
            navigate(res, err, data);
          });
        }

     

      각 html 에서 사용되는 css, js의 경우 src 디렉토리에 정리한 뒤 요청 주소를 '/src/title'로 주어 위와 같은 코드로 로드하도록 하였습니다. 

     

    3. process

      앞서 navigate 부분에서는 form요소를 통해 글쓰기, 수정등을 위한 페이지를 구성합니다. 이번 process 부분에서는 각 요소로 입력받은 데이터를 http request에 query string 형식으로 전송하고, 페이지를 리디렉션하여 글쓰기, 수정, 삭제등의 연산을 수행합니다. 

     

    else if (pathname === "/process_write") {
          let body = "";
    
          req.on("data", (data) => {
            body += data;
          });
    
          req.on("end", () => {
            let post = qs.parse(body);
    
            fs.writeFile(
              __dirname + `/data/${post.title}`,
              post.description,
              (err) => {
                redir(res, err, "/");
              }
            );
          });
        }

      글쓰기 페이지에서 입력받은 제목, 내용 등의 데이터는 form 요소를 통해  '/process_write'로 전송됩니다. 이때, 각 데이터는 http request body에 담겨 전달되며 key=value 쌍으로 구성됩니다. req.on은 이러한 데이터 chunk들이 전송될 때 마다 호출되며 위 코드에선 콜백함수로 해당 데이터 청크를 외부에 정의해둔 body 변수에 누산합니다. 이후 모든 데이터를 전송받았을 때 호출되는 이벤트인  'end'에서 누산 된 body를 파싱하여 객체로 만들고 입력받은 title과 description을 writeFile에 인자로 주어 글을 생성합니다.

     

    else if (pathname === "/process_delete") {
          fs.unlink(__dirname + `/data/${filteredTitle}`, (err) => {
            redir(res, err, "/");
          });
        }

      글의 삭제는 fs모듈의 unlink를 이용해 이루어 집니다. 

      

    else if (pathname === "/process_update") {
          let body = "";
    
          req.on("data", (data) => {
            body += data;
          });
    
          req.on("end", () => {
            let post = qs.parse(body);
    
            fs.rename(
              __dirname + `/data/${filteredTitle}`,
              __dirname + `/data/${post.title}`,
              (err) => {
                fs.writeFile(
                  __dirname + `/data/${post.title}`,
                  post.description,
                  (err) => {
                    redir(res, err, "/");
                  }
                );
              }
            );
          });
        }

     

      글의 수정은 앞서 글쓰기와 거의 동일한 단계를 거쳐 이루어집니다. 다만, 글을 쓰기 전에 기존 글을 수정된 제목으로 rename한 뒤 writeFile을 적용합니다.

     

    4. not found

     

    else {
          res.writeHead(404);
          res.end("404 not found");
          return;
        }

     

      not found 페이지는 정의된 path를 제외한 경우 출력되는 페이지입니다. 404로 not found 상태를 전달합니다.

     

    5. secure - path.parse, sanitizeHtml

     

      입출력 보안은 path모듈의 parse, sanitize-html 모듈의 sanitizeHtml 을 사용해 이루어집니다. 입력의 경우 ㅔpath 모듈의 parse 함수를 사용해 입력정보에서 ../과 같은 움직임을 제한하며 출력의 경우 sanitize-html을 통해 민감한 스크립트가 포함된 경우 이를 제거합니다.

    반응형

    댓글

Designed by Tistory.