Node.js/nodeJS 기본

[ NodeJS 기본 ] 파일 입출력 구현과 보안

OnnJE 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을 통해 민감한 스크립트가 포함된 경우 이를 제거합니다.

반응형