summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorgumi <git@gumi.ca>2019-05-25 21:07:57 -0400
committergumi <git@gumi.ca>2019-05-25 22:36:57 -0400
commit6731de13eab82469ac0ccd365cf84586b08ee0c5 (patch)
treee9a88efbfb3892765e47d2c65d9693bbdf4dc531
parentb8cfb2a02fdfa74086b2e34a87af1f687a0b8fd0 (diff)
downloadapi-6731de13eab82469ac0ccd365cf84586b08ee0c5.tar.gz
api-6731de13eab82469ac0ccd365cf84586b08ee0c5.tar.bz2
api-6731de13eab82469ac0ccd365cf84586b08ee0c5.tar.xz
api-6731de13eab82469ac0ccd365cf84586b08ee0c5.zip
use integrated nodemailer and connect to tmwa-admin directly
-rw-r--r--.gitignore2
-rw-r--r--package.json28
-rw-r--r--src/api.js16
-rw-r--r--src/routers/tmwa/middlewares/account.js276
4 files changed, 172 insertions, 150 deletions
diff --git a/.gitignore b/.gitignore
index 7297aeb..5b55916 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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": {}
}
diff --git a/src/api.js b/src/api.js
index 4e1e6f8..33cfb42 100644
--- a/src/api.js
+++ b/src/api.js
@@ -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
};