diff --git a/w5/.dockerignore b/w5/.dockerignore new file mode 100644 index 0000000..674b3b4 --- /dev/null +++ b/w5/.dockerignore @@ -0,0 +1,6 @@ +node_modules +Dockerfile +.dockerignore +.git +.gitignore +users.json \ No newline at end of file diff --git a/w5/Cat_poster_1.png b/w5/Cat_poster_1.png new file mode 100644 index 0000000..647e82e Binary files /dev/null and b/w5/Cat_poster_1.png differ diff --git a/w5/Dockerfile b/w5/Dockerfile new file mode 100644 index 0000000..9982b97 --- /dev/null +++ b/w5/Dockerfile @@ -0,0 +1,12 @@ +FROM node:16.13.2-alpine3.14 + +RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app +WORKDIR /home/node/app + +COPY --chown=node:node . . + +USER node + +EXPOSE 9889/tcp + +CMD [ "node", "." ] \ No newline at end of file diff --git a/w5/helloworld.js b/w5/helloworld.js new file mode 100644 index 0000000..f7e2eb0 --- /dev/null +++ b/w5/helloworld.js @@ -0,0 +1,3 @@ +"use strict"; + +console.log("Hello, World!"); \ No newline at end of file diff --git a/w5/package.json b/w5/package.json new file mode 100644 index 0000000..4944ef9 --- /dev/null +++ b/w5/package.json @@ -0,0 +1,12 @@ +{ + "name": "w5", + "version": "1.0.0", + "description": "", + "main": "webserver.js", + "type": "module", + "scripts": { + "start": "node ." + }, + "author": "nuark", + "license": "MIT" +} diff --git a/w5/router.js b/w5/router.js new file mode 100644 index 0000000..9eecf16 --- /dev/null +++ b/w5/router.js @@ -0,0 +1,54 @@ +"use strict"; + +/** + * + * @param {String} method + * @param {String} path + */ +export function route(method, path) { + return `route::${method}::${path}`; +} + +export class Router { + /** @type {Map.>} */ + routes = new Map(); + + /** + * + * @param {Route} route + * @param {Function.} func + */ + addRoute(route, func) { + if (this.routes.has(route)) { + throw new Error(`Route ${route} already exists!`); + } + this.routes.set(route, func); + } + + /** + * + * @param {Route} route + * @returns Function for dispatchng this route + */ + dispatch(route) { + if (this.routes.has(route)) { + return this.routes.get(route); + } + return this.returnError(404, "Not found!"); + } + + /** + * + * @param {Number} code + * @param {String} message + * @param {String} contentType + */ + returnError(code, message, contentType="text/plain") { + return (req, res) => { + res.setHeader("Content-Type", contentType); + res.statusCode = code; + res.write(message); + res.end(); + }; + } +} \ No newline at end of file diff --git a/w5/templates/dashboard.html b/w5/templates/dashboard.html new file mode 100644 index 0000000..1fc9fdb --- /dev/null +++ b/w5/templates/dashboard.html @@ -0,0 +1,13 @@ + + + + + + + Dashboard + + +

Hello, {{user.name}} you are {{user.status}}

+ + + \ No newline at end of file diff --git a/w5/templates/login.html b/w5/templates/login.html new file mode 100644 index 0000000..850427a --- /dev/null +++ b/w5/templates/login.html @@ -0,0 +1,52 @@ + + + + + + + Login page + + +
+

Login page

+
+ + +
+ + +
+
+ +
+ No accoount? Register here! + + + diff --git a/w5/templates/register.html b/w5/templates/register.html new file mode 100644 index 0000000..b984b4b --- /dev/null +++ b/w5/templates/register.html @@ -0,0 +1,60 @@ + + + + + + + Registration page + + +
+

Registration page

+
+ + +
+ + + + + +
+
+ +
+ Already have an accoount? Login here! + + + diff --git a/w5/userdb.js b/w5/userdb.js new file mode 100644 index 0000000..08db656 --- /dev/null +++ b/w5/userdb.js @@ -0,0 +1,90 @@ +"use strict"; + +import { readFileSync, writeFileSync } from "fs"; + +export class User { + /** + * + * @param {String} login + * @param {String} password + * @param {String} status + */ + constructor(login, password, status) { + this.login = login; + this.password = password; + this.status = status; + } +} + +export class UserDB { + /** @type {Map.} */ + users = new Map(); + + constructor() { + this.#loadData(); + } + + #loadData() { + try { + const fileData = readFileSync("users.json", "utf-8"); + const usersData = JSON.parse(fileData); + usersData.forEach(user => { + this.users.set( + user["login"], + new User(user["login"], user["password"], user["status"]) + ); + }); + } catch (_) { } + } + + saveData() { + const data = JSON.stringify(Array.from(this.users.keys()).map(k => { + const user = this.users.get(k); + return { + login: user.login, + password: user.password, + status: user.status + }; + })); + writeFileSync("./users.json", data, { encoding: "utf-8", flag: "w+" }); + } + + /** + * + * @param {User} login + * @returns Exists user in DB or not + */ + hasUser(login) { + return this.users.has(login); + } + + /** + * + * @param {User} user + */ + putUser(user) { + if (this.hasUser(user.login)) { + throw new Error(`User ${user.login} exists!`); + } + + this.users.set(user.login, user); + this.saveData(); + } + + /** + * + * @param {String} login + * @returns User object + */ + getUser(login) { + return this.users.get(login); + } + + /** + * + * @returns Users count + */ + getUsersCount() { + return this.users.size; + } +} \ No newline at end of file diff --git a/w5/users.json b/w5/users.json new file mode 100644 index 0000000..6fb6927 --- /dev/null +++ b/w5/users.json @@ -0,0 +1 @@ +[{"login":"nuark","password":"a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3","status":"admin"},{"login":"123","password":"a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3","status":"user"}] \ No newline at end of file diff --git a/w5/utils.js b/w5/utils.js new file mode 100644 index 0000000..ab46e89 --- /dev/null +++ b/w5/utils.js @@ -0,0 +1,89 @@ +"use strict"; + +import { readFileSync } from "fs"; +import { IncomingMessage, ServerResponse } from "http"; +import { parse as urlParse } from "querystring"; + +/** + * + * @param {ServerResponse} res + * @param {String} filename + * @param {Array.>} params + */ +export function renderTemplate(res, filename, params = []) { + let templateText = readFileSync(filename, "utf-8"); + params.forEach(el => { + const [k, v] = el; + templateText = templateText.replace(new RegExp(`\{\{${k}\}\}`, "g"), v); + }); + res.writeHead(200, { "Content-Type": "text/html" }); + res.write(templateText); + res.end(); +} + +/** + * + * @param {IncomingMessage} req + */ +export function getCookies(req) { + const list = {}; + const cookieHeader = req.headers?.cookie; + if (!cookieHeader) return list; + + cookieHeader.split(";").forEach(function (cookie) { + let [name, ...rest] = cookie.split("="); + name = name?.trim(); + if (!name) return; + const value = rest.join("=").trim(); + if (!value) return; + list[name] = decodeURIComponent(value); + }); + + return list; +} + +/** + * + * @param {ServerResponse} res + * @param {Object} cookies + */ +export function setCookies(res, cookies = {}) { + res.setHeader( + "Set-Cookie", + Array.from(Object.keys(cookies)) + .map(e => `${encodeURIComponent(e)}=${encodeURIComponent(cookies[e])}`) + .join(";") + ); +} + +/** + * + * @param {ServerResponse} res + * @param {String} path + */ +export function redirect(res, path) { + res.statusCode = 307; + res.setHeader("Location", path); + res.end(); +} + +export function consumePostForm(req) { + return new Promise((resolve, reject) => { + let body = ""; + req.on("data", function (data) { + try { + body += data; + if (body.length > 1e6) { + req.destroy(); + } + } catch (e) { reject(e); } + }); + req.on("end", function () { + try { + const post = urlParse(body); + resolve(post); + } catch (e) { reject(e); } + }); + }); +} + diff --git a/w5/webserver.js b/w5/webserver.js new file mode 100644 index 0000000..49f7cb6 --- /dev/null +++ b/w5/webserver.js @@ -0,0 +1,240 @@ +"use strict"; + +import { createServer, IncomingMessage, ServerResponse } from "http"; +import { parse as urlParse } from "querystring"; +import { readFileSync } from "fs"; +import { createHash } from "crypto"; +import { Router, route } from "./router.js"; +import { getCookies, renderTemplate, redirect, consumePostForm, setCookies } from "./utils.js"; +import { User, UserDB } from "./userdb.js"; + +const udb = new UserDB(); +const hasher = (hashable) => createHash("sha256").update(hashable).digest("hex"); + +/** + * + * @param {IncomingMessage} req + * @param {ServerResponse} res + * @param {String} path + * @param {Object} params + */ +const mainPage = function (req, res, path, params) { + res.writeHead(200, { "Content-Type": "text/html" }); + res.write( + `Hello on ${path}!\n You can go on:
` + + `1. Parameters display (in JSON): *click*
` + + `2. Parameters display (in TEXT): *click*
` + + `3. See some pretty cats: *click*
` + + `4. Open dashboard: *click*` + ); + res.end(); +} + +const showParameters = function (type) { + const contentType = type === "JSON" ? "application/json" : "text/plain"; + const serializer = type === "JSON" ? JSON.stringify : (obj => Array.from(Object.keys(obj)).map(e => `Key: ${e}; Value: ${obj[e]}`).join("\n")); + + /** + * + * @param {IncomingMessage} req + * @param {ServerResponse} res + * @param {String} path + * @param {Object} params + */ + return function (req, res, path, params) { + res.writeHead(200, { "Content-Type": contentType }); + res.write(serializer(params)); + res.end(); + } +} + +/** + * + * @param {IncomingMessage} req + * @param {ServerResponse} res + * @param {String} path + * @param {Object} params + */ +const showACat = function (req, res, path, params) { + res.writeHead(200, { "Content-Type": "image/png" }); + res.write(readFileSync("./Cat_poster_1.png")); + res.end(); +} + +const router = new Router(); +router.addRoute(route("GET", "/"), mainPage); +router.addRoute(route("GET", "/sparamsAsJson"), showParameters("JSON")); +router.addRoute(route("GET", "/sparamsAsText"), showParameters("TEXT")); +router.addRoute(route("GET", "/showMeACat"), showACat); + + +/** + * + * @param {IncomingMessage} req + * @param {ServerResponse} res + * @param {String} path + * @param {Object} params + */ +const userRegisterView = (req, res, path, params) => { + const cookies = getCookies(req); + if (cookies["user"]) return redirect(res, "/dashboard"); + + return renderTemplate(res, "./templates/register.html"); +} + +/** + * + * @param {IncomingMessage} req + * @param {ServerResponse} res + * @param {String} path + * @param {Object} params + */ +const userRegister = async function (req, res, path, params) { + const cookies = getCookies(req); + if (cookies["user"]) return redirect(res, "/dashboard"); + + const response = { + ok: true, + message: "" + }; + try { + const post = await consumePostForm(req); + console.log(post); + const login = post.login; + const password = post.password; + const passwordRepeated = post.passwordRepeated; + const status = udb.getUsersCount() === 0 ? "admin" : "user"; + if (udb.hasUser(login)) { + throw new Error("User already exists!"); + } else if (password != passwordRepeated) { + throw new Error("Passwords must match!"); + } else { + udb.putUser(new User(login, hasher(password), status)); + } + } catch (e) { + console.error(e); + response.ok = false; + response.message = e.message; + } + + res.statusCode = 200; + res.setHeader("Content-Type", "application/json"); + res.write(JSON.stringify(response)); + res.end(); +} + +/** + * + * @param {IncomingMessage} req + * @param {ServerResponse} res + * @param {String} path + * @param {Object} params + */ +const userLoginView = (req, res, path, params) => { + const cookies = getCookies(req); + if (cookies["user"]) return redirect(res, "/dashboard"); + + return renderTemplate(res, "./templates/login.html"); +} + +/** + * + * @param {IncomingMessage} req + * @param {ServerResponse} res + * @param {String} path + * @param {Object} params + */ +const userLogin = async function (req, res, path, params) { + const cookies = getCookies(req); + if (cookies["user"]) return redirect(res, "/dashboard"); + + let response = { + ok: true, + message: "" + }; + try { + const post = await consumePostForm(req); + const login = post.login; + const password = hasher(post.password); + if (!udb.hasUser(login)) { + throw new Error("User not exists!"); + } + const user = udb.getUser(login); + if (password !== user.password) { + throw new Error("Password doesn't match!"); + } + response["user"] = { + login, + status: user.status + }; + } catch (e) { + console.error(e); + response.ok = false; + response.message = e.message; + } + + res.statusCode = 200; + res.setHeader("Content-Type", "application/json"); + if (response.ok) { + setCookies(res, { + user: JSON.stringify(response["user"]) + }); + } + res.write(JSON.stringify(response)); + res.end(); +} + +/** + * + * @param {IncomingMessage} req + * @param {ServerResponse} res + * @param {String} path + * @param {Object} params + */ +const userDashboardView = function (req, res, path, params) { + const cookies = getCookies(req); + if (!cookies["user"]) return redirect(res, "/login"); + + const user = JSON.parse(cookies["user"]); + return renderTemplate(res, "./templates/dashboard.html", [ + ["user.name", user.login], + ["user.status", user.status], + ]); +} + +/** + * Sadly, not working( + * @param {IncomingMessage} req + * @param {ServerResponse} res + * @param {String} path + * @param {Object} params + */ +const userDashboardLogout = function (req, res, path, params) { + let cookies = getCookies(req); + if (cookies["user"]) { + cookies["user"] = ""; + setCookies(res, cookies); + } + + return redirect(res, "/login"); +} + +router.addRoute(route("GET", "/register"), userRegisterView); +router.addRoute(route("POST", "/register"), userRegister); +router.addRoute(route("GET", "/login"), userLoginView); +router.addRoute(route("POST", "/login"), userLogin); +router.addRoute(route("GET", "/dashboard"), userDashboardView); +router.addRoute(route("GET", "/dashboard/logout"), userDashboardLogout); + +/** + * + * @param {IncomingMessage} req + * @param {ServerResponse} res + */ +const requestDispatcher = function (req, res) { + const [path, params] = req.url.includes("?") ? req.url.split("?", 2) : [req.url, ""]; + const parsedParams = urlParse(params); + router.dispatch(route(req.method, path))(req, res, path, parsedParams); +} + +createServer(requestDispatcher).listen(9889);