diff options
author | gumi <git@gumi.ca> | 2019-05-25 21:07:57 -0400 |
---|---|---|
committer | gumi <git@gumi.ca> | 2019-05-25 22:36:57 -0400 |
commit | 6731de13eab82469ac0ccd365cf84586b08ee0c5 (patch) | |
tree | e9a88efbfb3892765e47d2c65d9693bbdf4dc531 | |
parent | b8cfb2a02fdfa74086b2e34a87af1f687a0b8fd0 (diff) | |
download | apiv1-6731de13eab82469ac0ccd365cf84586b08ee0c5.tar.gz apiv1-6731de13eab82469ac0ccd365cf84586b08ee0c5.tar.bz2 apiv1-6731de13eab82469ac0ccd365cf84586b08ee0c5.tar.xz apiv1-6731de13eab82469ac0ccd365cf84586b08ee0c5.zip |
use integrated nodemailer and connect to tmwa-admin directly
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | package.json | 28 | ||||
-rw-r--r-- | src/api.js | 16 | ||||
-rw-r--r-- | src/routers/tmwa/middlewares/account.js | 276 |
4 files changed, 172 insertions, 150 deletions
@@ -2,3 +2,5 @@ /node_modules /package-lock.json /online.txt +/account.txt +/athena.txt diff --git a/package.json b/package.json index 7201388..5dadaad 100644 --- a/package.json +++ b/package.json @@ -1,25 +1,23 @@ { "name": "tmw-api", - "version": "1.1.0", + "version": "2.0.0", "description": "TMW RESTful API", "author": "The Mana World", "license": "CC0-1.0", "config": { "port": 8080, "timezone": "UTC", - "sql": { - "host": "localhost", - "user": "db user", - "password": "db password", - "database": "db", - "table": "table" - }, "recaptcha": { "secret": "recaptcha secret key" }, "tmwa": { "name": "The Mana World Legacy Server", - "url": "tmwa://server.themanaworld.org:6901" + "url": "tmwa://server.themanaworld.org:6901", + "root": "/mnt/tmwAthena/tmwa-server-data", + "home": "/home/tmw" + }, + "mailer": { + "from": "The Mana World <noreply@themanaworld.org>" } }, "repository": { @@ -36,11 +34,11 @@ "start": "node ." }, "dependencies": { - "express": "^4.16.3", - "mysql": "^2.15.0", - "random-number-csprng": "^1.0.2" + "express": "^4.17.0", + "nodemailer": "^6.2.1", + "random-number-csprng": "^1.0.2", + "ripgrep-bin": "^11.0.1", + "uuid": "^3.3.2" }, - "devDependencies": { - "nsp": "^3.2.1" - } + "devDependencies": {} } @@ -1,5 +1,4 @@ const express = require("express"); // from npm registry -const mysql = require("mysql"); // from npm registry const https = require("https"); // built-in const api = express(); @@ -13,6 +12,9 @@ if (process.env.npm_package_config_port === undefined) { // config common to all routers: api.locals = Object.assign({ rate_limiting: new Set(), // XXX: or do we want routers to each have their own rate limiter? + mailer: { + from: process.env.npm_package_config_mailer_from, + } }, api.locals); @@ -95,16 +97,8 @@ const tmwa_router = new (require("./routers/tmwa"))({ timezone: process.env.npm_package_config_timezone, name: process.env.npm_package_config_tmwa_name, url: process.env.npm_package_config_tmwa_url, - db_pool: mysql.createPool({ - connectionLimit: 10, - host : process.env.npm_package_config_sql_host, - user : process.env.npm_package_config_sql_user, - password : process.env.npm_package_config_sql_password, - database : process.env.npm_package_config_sql_database - }), - db_tables: { - register: process.env.npm_package_config_sql_table, - }, + root: process.env.npm_package_config_tmwa_root, + home: process.env.npm_package_config_tmwa_home, }, api, checkCaptcha, checkRateLimiting); global_router.use("/tmwa", tmwa_router); diff --git a/src/routers/tmwa/middlewares/account.js b/src/routers/tmwa/middlewares/account.js index 9c3afce..3c82dbc 100644 --- a/src/routers/tmwa/middlewares/account.js +++ b/src/routers/tmwa/middlewares/account.js @@ -1,4 +1,93 @@ -const randomNumber = require("random-number-csprng"); +// this whole file would be a lot prettier with typescript interfaces +"use strict"; +const uuidv4 = require("uuid/v4"); +const execFile = require("child_process").execFile; +const spawn = require("child_process").spawn; +const ripgrep = require("ripgrep-bin"); +const nodemailer = require("nodemailer"); + +const execAsync = (cmd, par) => + new Promise((resolve, reject) => + execFile(cmd, par, (error, stdout, stderr) => + resolve(error ? "" : (stdout ? stdout : stderr)))); + +const tmwa_account_regex = new RegExp("^(?<id>[0-9]+)\t(?<name>[^\t]+)\t[^\t]+\t(?<time>[^\t]+)\t[MFSN]\t(?<logins>[0-9]+)\t(?<state>[0-9]+)\t(?<email>[^\t]+)\t[^\t]+\t[0-9]+\t(?<ip>[0-9\.]+)"); +const tmwa_char_regex = new RegExp("^(?<id>[0-9]+)\t(?<account>[0-9]+),[0-9]+\t(?<name>[^\t]+)\t"); + +const parseAccountLine = (line) => { + const { groups: account } = tmwa_account_regex.exec(line); + return { + id: +account.id, + name: account.name, + last_login: Date.parse(account.time), + active: !!+account.logins, + banned: +account.state !== 0, + email: account.email === "a@a.com" ? null : account.email, + ip: (account.ip !== "0.0.0.0" && account.ip !== "127.0.0.1") ? account.ip : null, + }; +} + +const parseCharLine = (line) => { + const { groups: char } = tmwa_char_regex.exec(line); + return { + id: +char.id, + account: +char.account, + name: char.name, + }; +} + +const findAccounts = async (regex, max) => { + const stdout = await execAsync(ripgrep, ["--case-sensitive", `--max-count=${max}`, regex, "account.txt"]); + const accounts = new Set(); + if (stdout.length) + stdout.slice(0, -1).split("\n").forEach(line => accounts.add(parseAccountLine(line))); + return accounts; +}; + +const findAccountsByID = async (account_id) => await findAccounts(`^${account_id}\t`, 1); +const findAccountsByName = async (name) => await findAccounts(`^[0-9]+\t${name}\t`, 1); +const findAccountsByEmail = async (email, max=20) => await findAccounts(`^[0-9]+\t[^\t]+\t[^\t]+\t[^\t]+\t[MFSN]\t[0-9]+\t[0-9]+\t${email}\t`, max); + +const findChars = async (regex, max) => { + const stdout = await execAsync(ripgrep, ["--case-sensitive", `--max-count=${max}`, regex, "athena.txt"]); + const chars = new Set(); + if (stdout.length) + stdout.slice(0, -1).split("\n").forEach(line => chars.add(parseCharLine(line))); + return chars; +}; + +const findCharsByID = async (char_id) => await findChars(`^${char_id}\t`, 1); +const findCharsByName = async (name) => await findChars(`^[0-9]+\t[0-9]+,[0-9]+\t${name}\t`, 1); +const findCharsByAccount = async (account_id, max=20) => await findChars(`^[0-9]+\t${account_id},`, max); + +const findCharsByAccountName = async (name) => { + const accounts = await findAccountsByName(name); + if (accounts.size > 0) + return await findCharsByAccount(accounts.values().next().value.id); + else + return new Set(); +}; + +const findCharsByEmail = async (email) => { + const chars = new Set(); + const accounts = await findAccountsByEmail(email); + for (const account of accounts) { + const account_chars = await findCharsByAccount(account.id); + account_chars.forEach(char => + chars.add(char)) + } + return chars; +}; + +/// tmwa flatfile searching ^ + +let transporter = nodemailer.createTransport({ + sendmail: true, + newline: 'unix', + path: '/usr/sbin/sendmail' +}); + +const pending_operations = new Map(); const create_account = (req, res, next) => { if (!req.body || !Reflect.has(req.body, "username") || @@ -17,50 +106,59 @@ const create_account = (req, res, next) => { return; } - req.app.locals.tmwa.db_pool.getConnection((err, db) => { - if (err) { - res.status(500).json({ + findAccountsByName(req.body.username).then(acc => { + if (acc.size > 0) { + res.status(409).json({ status: "error", - error: "couldn't reach the database" + error: "already exists" }); - console.warn("TMWA.account: a connection with the database couldn't be established"); + req.app.locals.rate_limiting.add(req.ip); + setTimeout(() => req.app.locals.rate_limiting.delete(req.ip), 2000); return; } - const query_params = { - "USERNAME": req.body.username, - "PASSWORD": req.body.password, - "EMAIL": req.body.email || "a@a.com", - "GENDER": "N", - }; - - db.query(`INSERT INTO ${req.app.locals.tmwa.db_tables.register} SET ?`, query_params, (err, rows, fields) => { - if (err) { - if (err.code === "ER_DUP_ENTRY") { - res.status(409).json({ - status: "error", - error: "already exists" - }); - req.app.locals.rate_limiting.add(req.ip); - setTimeout(() => req.app.locals.rate_limiting.delete(req.ip), 2000); - } else { - res.status(500).json({ - status: "error", - error: "couldn't add the user" - }); - console.error("TMWA.account: an unexpected sql error occured: %s", err.code); - } - } else { - res.status(201).json({ - status: "success" - }); - console.info("TMWA.account: an account was created: %s [%s]", query_params.USERNAME, req.ip); - req.app.locals.rate_limiting.add(req.ip); - setTimeout(() => req.app.locals.rate_limiting.delete(req.ip), 300000); + const child = execFile(`${req.app.locals.tmwa.home}/.local/bin/tmwa-admin`, [], { + cwd: `${req.app.locals.tmwa.root}/login/`, + env: { + LD_LIBRARY_PATH: `${req.app.locals.tmwa.home}/.local/lib`, } + }); + + const email = req.body.email.length >= 3 ? req.body.email : "a@a.com"; + + child.stdin.write(`create ${req.body.username} N ${email} ${req.body.password}\n`); + child.stderr.on("data", data => { + console.error("TMWA.account: an unexpected tmwa-admin error occured: %s", data); + return; + }); + child.stdout.on("data", data => { + if (!data.includes("successfully")) { + if (!data.includes("have a connection")) + console.error("TMWA.account: an unexpected tmwa-admin error occured: %s", data); + child.kill(); + return; + } + + res.status(201).json({ + status: "success" + }); + console.info("TMWA.account: an account was created: %s [%s]", req.body.username, req.ip); + req.app.locals.rate_limiting.add(req.ip); + setTimeout(() => req.app.locals.rate_limiting.delete(req.ip), 300000); + + if (email === "a@a.com") + return; - db.release(); // return this connection to the pool + transporter.sendMail({ + from: req.app.locals.mailer.from, + to: email, + subject: "The Mana World account registration", + text: `Your account (\"${req.body.username}\") was created successfully.\nHave fun playing The Mana World!` + }, (err, info) => { + console.info("TMWA.account: sent account creation email: %s %s", req.body.username, info.messageId); + }); }); + child.stdin.end(); }); }; @@ -68,56 +166,27 @@ const create_account = (req, res, next) => { const reset_password = async (req, res, next) => { if (req.body && Reflect.has(req.body, "email") && - Reflect.has(req.body, "username") && - req.body.username.match(/^[a-zA-Z0-9]{4,23}$/) && req.body.email.match(/^(?:[a-zA-Z0-9.$&+=_~-]{1,34}@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,35}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?){0,9})$/) && - req.body.email.length >= 3 && req.body.email.length < 40) - { - req.app.locals.tmwa.db_pool.getConnection(async (err, db) => { - if (err) { - res.status(500).json({ - status: "error", - error: "couldn't reach the database" - }); - console.warn("TMWA.account: a connection with the database couldn't be established"); - return; - } - - // XXX: we might want to use uuid instead, and put the value in the url directly - const rng = await randomNumber(1000000, 999999999999); - const code = String(rng).padStart(12, "0"); - - const query_params = { // SET - "PASSWORD": code, - "STATE": 3, - }; - - db.query(`UPDATE ${req.app.locals.tmwa.db_tables.register} SET ? WHERE USERNAME = ? AND EMAIL = ? AND STATE = 1`, [query_params, req.body.username, req.body.email], (err, rows, fields) => { - if (err) { - res.status(500).json({ - status: "error", - error: "couldn't send a password reset" - }); - console.error("TMWA.account: an unexpected sql error occured: %s", err.code); - } else { - res.status(200).json({ - status: "success" - }); - req.app.locals.rate_limiting.add(req.ip); - setTimeout(() => req.app.locals.rate_limiting.delete(req.ip), 5000); - - // TODO: make the request expire and change the STATE back to 1 upon expiration - } - - db.release(); // return this connection to the pool - }); + req.body.email.length >= 3 && req.body.email.length < 40 && + req.body.email !== "a@a.com") { + // recover by email (currently unsupported) + res.status(501).json({ + status: "error", + error: "not yet implemented" + }); + return; + } else if (req.body && Reflect.has(req.body, "username") && + req.body.username.match(/^[a-zA-Z0-9]{4,23}$/)) { + // recover by username (currently unsupported) + res.status(501).json({ + status: "error", + error: "not yet implemented" }); return; } - if (!req.body || !Reflect.has(req.body, "username") || - !Reflect.has(req.body, "password") || !Reflect.has(req.body, "code") || - !req.body.username.match(/^[a-zA-Z0-9]{4,23}$/) || + if (!req.body || !Reflect.has(req.body, "password") || + !Reflect.has(req.body, "code") || !req.body.password.match(/^[a-zA-Z0-9]{4,23}$/) || !req.body.code.match(/^[a-zA-Z0-9-_]{6,128}$/)) { @@ -130,48 +199,7 @@ const reset_password = async (req, res, next) => { return; } - req.app.locals.tmwa.db_pool.getConnection((err, db) => { - if (err) { - res.status(500).json({ - status: "error", - error: "couldn't reach the database" - }); - console.warn("TMWA.account: a connection with the database couldn't be established"); - return; - } - - const query_params = { // SET - "PASSWORD": req.body.password, - "STATE": 5, - }; - - db.query(`UPDATE ${req.app.locals.tmwa.db_tables.register} SET ? WHERE USERNAME = ? AND PASSWORD = ? AND STATE = 4`, [query_params, req.body.username, req.body.code], (err, rows, fields) => { - if (err) { - res.status(500).json({ - status: "error", - error: "couldn't change the password" - }); - console.error("TMWA.account: an unexpected sql error occured: %s", err.code); - } else if (rows.affectedRows < 1) { - res.status(403).json({ - status: "error", - error: "invalid code" - }); - req.app.locals.rate_limiting.add(req.ip); - setTimeout(() => req.app.locals.rate_limiting.delete(req.ip), 2000); - return; - } else { - res.status(200).json({ - status: "success" - }); - console.info("TMWA.account: a password was reset: %s [%s]", query_params.USERNAME, req.ip); - req.app.locals.rate_limiting.add(req.ip); - setTimeout(() => req.app.locals.rate_limiting.delete(req.ip), 300000); - } - - db.release(); // return this connection to the pool - }); - }); + // actual reset happens here }; |