import express from 'express'; import Database from 'better-sqlite3'; import { migrate } from '@blackglory/better-sqlite3-migrations'; import compression from 'compression'; import helmet from 'helmet'; import RateLimit from 'express-rate-limit'; import morgan from 'morgan'; import cors from 'cors'; import { Feed } from 'feed'; // Constants const MAX_LENGTHS = { username: 100, user_website: 200, message_body: 1000 }; // functions function escapeHtml(unsafe) { return unsafe .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } // db const db = new Database(process.env.DB || 'ucs.db'); // , { verbose: console.log }); db.pragma('journal_mode = WAL'); const migrations = [ { version: 1, up: ` CREATE TABLE site ( id integer primary key autoincrement, url text not null, comment_token text not null, length_limit integer ); CREATE TABLE comment ( id integer not null, site integer not null, path text not null, user text not null, user_website text, message text not null, published text default (datetime('now')), reply_to integer, FOREIGN KEY(site) REFERENCES site(id), PRIMARY KEY (site, path, id) ); `, down: ` DROP TABLE comment; DROP TABLE site; ` } ]; migrate(db, migrations, 1); // server const app = express(); const port = process.env.PORT || 8080; app.use(express.urlencoded({ extended: true })); app.use(compression()); app.use( helmet.contentSecurityPolicy({ directives: { "script-src": ["'self'"] }, }) ); app.use(morgan('combined')); const corsOptions = { origin: ['https://alloca.space', 'https://www.alloca.space'], optionsSuccessStatus: 200 }; app.use(cors(corsOptions)); // POST const limiter20 = RateLimit({ windowMs: 1 * 60 * 1000, max: 20, }); app.use(express.json()); app.post('/api/:site/*', limiter20, (req, res) => { const site = req.params.site; const path = req.params[0]; if (!req.body.token || !req.body.message) { res.status(400).json("הודעה ריקה."); return; } let object = { user: escapeHtml(req.body.name) || "Anonymous", user_website: escapeHtml(req.body.website) || null, message: escapeHtml(req.body.message), reply_to: req.body.reply_to || null, site, path, }; // validation const user_token = req.body.token; const site_info = db.prepare(`SELECT comment_token, length_limit as message_length_limit FROM site WHERE url = ?`).get(site); console.log(site_info.comment_token, site_info.message_length_limit); if (user_token !== site_info.comment_token) { res.status(403).json("תשובת סינון שגויה."); } else if (object.user.length > MAX_LENGTHS.username) { res.status(400).json("שם משתמש ארוך מדי."); } else if (object.user_website > MAX_LENGTHS.user_website) { res.status(400).json("כתובת אתר ארוכה מדי."); } else if (object.message > site_info.message_length_limit) { res.status(400).json("הודעה ארוכה מדי."); } else { const stmt = db.prepare(` INSERT INTO comment(id, site, path, user, user_website, message, reply_to) SELECT ( SELECT count(*) FROM (SELECT * FROM comment WHERE path = @path) c JOIN (SELECT id FROM site WHERE url = @site) s ON s.id = c.site ), ( SELECT id FROM site WHERE url = @site ), @path, @user, @user_website, @message, @reply_to RETURNING id as id, user, user_website, message, published, reply_to ; `); const comment = stmt.all(object); res.json(comment); } }); // GET const limiter500 = RateLimit({ windowMs: 1 * 60 * 1000, max: 500, }); app.use(limiter500); app.use(express.static("public")); app.get('/api/:site/*', (req, res) => { const site = req.params.site; const path = req.params[0]; const stmt = db.prepare(` SELECT c.id as id, user, user_website, message, published, reply_to FROM (SELECT id from site where url = @site) s JOIN (SELECT * FROM comment WHERE path = @path) c ON c.site = s.id ORDER BY c.id ;`); const comments = stmt.all({ site, path }); res.json(comments); }); // Feed const domain = process.env.DOMAIN || 'comments.alloca.space'; app.get('/atom/:site', (req, res) => { const site = req.params.site; /* lets create an rss feed */ var feed = new Feed({ title: 'UCS', description: 'תגובות עבור האתר ' + site, id: domain + '/feed/' + site, link: domain, language: 'he' }); const stmt = db.prepare(` SELECT c.id, s.url as site, c.path, c.user, c.user_website, c.message, c.published FROM (SELECT id, url from site where url = @site) s JOIN comment c ON c.site = s.id ORDER BY c.published DESC ;`); const comments = stmt.all({ site }); for (const comment of comments) { feed.addItem({ title: `New message by '${comment.user}' on ${comment.path}`, description: comment.message, id: `${comment.site}/${comment.path}#comment-${comment.id}`, link: `${comment.site}/${comment.path}#comment-${comment.id}`, date: new Date(comment.published) }); } var xml = feed.atom1(); res.set('Content-Type', 'text/xml'); res.send(xml); }); // Listen app.listen(port, () => { console.log(`Listening on port ${port}`); });