const path = require("path"); const bodyParser = require("koa-body")({multipart: true}); const fetch = require("node-fetch"); const Koa = require("koa"); const koaPug = require("koa-pug"); const koaRouter = require("koa-router"); const { MysqlError } = require("mysql"); const config = require("./config"); const guestbook = require("./guestbook"); const mindflow = require("./mindflow"); const util = require("../shared/util"); const { URLSearchParams } = require("url"); const articles_meta = { "rpi_root_on_external_drive": { title: "How to move a root from SD card to external drive on Raspberry Pi", description: "How to move a root partition to an ext. drive on Raspberry Pi." }, "setting_up_a_tor_proxy_relay_hiddenserv": { title: "Setting up a Tor proxy, relay and hidden service", description: "How to setup a Tor proxy, relay and hidden service." }, "setting_up_a_mail_server": { title: "Setting up a mail server", description: "How to create your own mail server using Postfix and Dovecot." }, "nginx_recipes_and_tips": { title: "NGiNX's recipes & tips", description: "Tips and recipes for NGiNX webserver." }, "hardening_mikrotik": { title: "Hardening Mikrotik", description: "How to harden security of your Mikrotik router." }, }; async function getProcesses() { try { let reimu = await fetch("http://reimu.arav.home.arpa:14882/processes").then(r => r.json()); let sakuya = await fetch("http://sakuya.arav.home.arpa:14882/processes").then(r => r.json()); return Object.fromEntries(Object.entries(reimu).concat(Object.entries(sakuya))); } catch { return []; } } // 127.0.0.1:19322 async function getNewCaptcha() { try { return await fetch("http://startpage.arav.home.arpa/captcha/", {method: "POST"}).then(r => r.text()); } catch { return null; } } async function solveCaptcha(id, answer) { try { let body = new URLSearchParams(); body.append('answer', answer); let result = fetch(`http://startpage.arav.home.arpa/captcha/${id}`, { method: "POST", body: body } ); return await result.then(r => { return r.status == 202; }); } catch { return false; } } function setRoutes() { return koaRouter().get('/', async ctx => { await ctx.render('index', { description: "A homepage of a russian guy Arav. Not just homepage, but FTP, radio, and smth more as well." }); }) .get('/rss.xml', async ctx => { ctx.type = 'xml'; await ctx.render('rss', { doctype: 'xml', schemahost: `${ctx.header.schema}://${ctx.header.host}`, author: "me@arav.top (Alexander \"Arav\" Andreev)", items: await mindflow.getPosts() }) }) .get('/stuff', async ctx => { await ctx.render('stuff', { title: "/ Stuff", description: "Here I share my programs, scripts, articles, may be other stuff.", git_site: util.getServiceByHost(ctx.request.host, "git"), files_site: util.getServiceByHost(ctx.request.host, "files") }) }) .get('/mindflow', async ctx => { await ctx.render('mindflow', { title: "/ Mindflow", description: "Here I will post updates on my infrastructure, my very important opinions and thoughts.", files_site: util.getServiceByHost(ctx.request.host, "files"), tz: util.getClientTimezone(ctx), posts: await mindflow.getPosts() }) }) .get('/stuff/article/:article', async ctx => { if (ctx.params.article) { return await ctx.render("articles/" + ctx.params.article, { title: "/ Article / " + articles_meta[ctx.params.article].title, description: articles_meta[ctx.params.article].description }); } else { return ctx.throw(404); } }) .get('/about', async ctx => { await ctx.render('about', { title: "/ About", description: "This is a page where I'm telling about me and my home server.", files_site: util.getServiceByHost(ctx.request.host, "files"), services: await getProcesses() }) }) .get('/guestbook', async ctx => { const page = ctx.request.query.p !== undefined ? ctx.request.query.p : 1; const page_size = ctx.request.query.ps !== undefined ? ctx.request.query.ps : guestbook.pageSize; let posts; try { posts = await guestbook.getPosts(page, page_size); } catch(err) { posts = null; } return await ctx.render('guestbook', { title: "/ Guestbook", description: "This is my guestbook. Welcome.", tz: util.getClientTimezone(ctx), owner: config.guestbook.owner, posts: posts, pages_count: Math.ceil(await guestbook.getPostsCount() / page_size), captcha_id: await getNewCaptcha() }); }) .post('/guestbook', bodyParser, async ctx => { const post = ctx.request.body; post.hide_website = post.hide_website !== undefined; try { let check = await solveCaptcha(post.captcha_id, post.captcha_answer); if (!check) throw "CAPTCHA is wrong or expired"; if (await guestbook.addPost(post)) ctx.redirect("/guestbook"); } catch(err) { if (typeof err == 'object' && err instanceof MysqlError) { ctx.response.status = 500; ctx.response.body = { error: `Database failed so your post wasn't added. Here's your message:`, message: post.message }; } else if (typeof err == 'string') { ctx.response.status = 403; ctx.response.body = { error: `Reason why your post was rejected is "${err}". Here's your message:`, message: post.message }; } } }); } const app = new Koa(); const pug = new koaPug({ viewPath: path.join(__dirname, "views"), locals: { date_: (date, tz) => util.datetime(date, util.date_formats.post_date, tz), date_id: (date, tz) => util.datetime(date, util.date_formats.id_date, tz), date_rss: date => util.datetime(date, util.date_formats.rss_date, "UTC"), rssLink: util.rssLink }, app: app }); app.proxy = true; app .use(async (ctx, next) => { try { await next(); if (ctx.status !== 200) ctx.throw(ctx.status); } catch (err) { ctx.status = err.status || 500; if (ctx.status === 404) await ctx.render('404'); else if (ctx.status === 403) await ctx.render('403', { error: ctx.body }); else await ctx.render('500', { error: ctx.body }); } }) .use(setRoutes().routes()) .listen(config.port, config.host);