This commit is contained in:
Andrew 2022-02-23 02:37:30 +07:00
parent 0e2c114a84
commit 2c5995be3f
13 changed files with 632 additions and 0 deletions

6
w5/.dockerignore Normal file
View file

@ -0,0 +1,6 @@
node_modules
Dockerfile
.dockerignore
.git
.gitignore
users.json

BIN
w5/Cat_poster_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 MiB

12
w5/Dockerfile Normal file
View file

@ -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", "." ]

3
w5/helloworld.js Normal file
View file

@ -0,0 +1,3 @@
"use strict";
console.log("Hello, World!");

12
w5/package.json Normal file
View file

@ -0,0 +1,12 @@
{
"name": "w5",
"version": "1.0.0",
"description": "",
"main": "webserver.js",
"type": "module",
"scripts": {
"start": "node ."
},
"author": "nuark",
"license": "MIT"
}

54
w5/router.js Normal file
View file

@ -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.<Route, Function<http.IncomingMessage, http.ServerResponse, String, Object>>} */
routes = new Map();
/**
*
* @param {Route} route
* @param {Function.<http.IncomingMessage, http.ServerResponse, String, Object>} 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();
};
}
}

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard</title>
</head>
<body>
<h2>Hello, {{user.name}} you are {{user.status}}</h2>
<button onclick="javascript:(location += '/logout')">Log out</button>
</body>
</html>

52
w5/templates/login.html Normal file
View file

@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Login page</title>
</head>
<body>
<div id="errors"></div>
<h3>Login page</h3>
<form action="/login" method="post" name="login">
<label for="login">Login</label>
<input type="text" name="login" id="login" required />
<br />
<label for="password">Password</label>
<input type="password" name="password" id="password" required />
</form>
<br />
<button id="login">Login</button>
<br />
<a href="/register">No accoount? Register here!</a>
</body>
<script>
const init = function () {
const btn = document.querySelector("button#login");
btn.addEventListener("click", async () => {
btn.textContent = "Loading...";
let formData = Array.from(document.forms.login, e => [e.name, encodeURIComponent(e.value)].join("=")).join("&");
try {
const jsonData = await (
await fetch("/login", {
method: "post",
body: formData,
})
).json();
if (jsonData.ok) {
location = "/dashboard";
} else {
alert(jsonData.message);
btn.textContent = "Login";
}
} catch (e) {
alert(e.message);
btn.textContent = "Login";
}
});
};
document.addEventListener("DOMContentLoaded", init);
</script>
</html>

View file

@ -0,0 +1,60 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Registration page</title>
</head>
<body>
<div id="errors"></div>
<h3>Registration page</h3>
<form action="/register" method="post" name="register">
<label for="login">Login</label>
<input type="text" name="login" id="login" required />
<br />
<label for="password">Password</label>
<input type="password" name="password" id="password" required />
<label for="passwordRepeated">Repeat password</label>
<input
type="password"
name="passwordRepeated"
id="passwordRepeated"
required
/>
</form>
<br />
<button id="register">Register</button>
<br />
<a href="/login">Already have an accoount? Login here!</a>
</body>
<script>
const init = function () {
const btn = document.querySelector("button#register");
btn.addEventListener("click", async () => {
btn.textContent = "Loading...";
let formData = Array.from(document.forms.register, e => [e.name, encodeURIComponent(e.value)].join("=")).join("&");
try {
const jsonData = await (
await fetch("/register", {
body: formData,
method: "post",
})
).json();
if (jsonData.ok) {
location = "/login";
} else {
alert(jsonData.message);
btn.textContent = "Register";
}
} catch (e) {
alert(e.message);
btn.textContent = "Register";
}
});
};
document.addEventListener("DOMContentLoaded", init);
</script>
</html>

90
w5/userdb.js Normal file
View file

@ -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.<String, User>} */
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;
}
}

1
w5/users.json Normal file
View file

@ -0,0 +1 @@
[{"login":"nuark","password":"a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3","status":"admin"},{"login":"123","password":"a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3","status":"user"}]

89
w5/utils.js Normal file
View file

@ -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.<Array.<String, Object>>} 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); }
});
});
}

240
w5/webserver.js Normal file
View file

@ -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: <br>` +
`1. Parameters display (in JSON): <a href="/sparamsAsJson?hello=i&am=JSON">*click*</a><br>` +
`2. Parameters display (in TEXT): <a href="/sparamsAsText?why=yes&i=am&a=plain+text">*click*</a><br>` +
`3. See some pretty cats: <a href="/showMeACat">*click*</a><br>` +
`4. Open dashboard: <a href="/dashboard">*click*</a>`
);
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);