[ NodeJS 기본 ] 파일 입출력 구현과 보안
아래는 사용자 입력에 대한 출력 및 수정 삭제를 구현한 코드입니다. 홈에서는 쓰여진 글의 목록을 출력 및 글쓰기 화면으로의 접근, 쓰여진 글 보기 등 을 가능하게 했으며, 각 글에 접근해 수정 혹은 삭제가 가능도록 하였습니다.
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을 통해 민감한 스크립트가 포함된 경우 이를 제거합니다.