diff options
Diffstat (limited to 'src/routers/vault/middlewares/evol/account.js')
-rw-r--r-- | src/routers/vault/middlewares/evol/account.js | 326 |
1 files changed, 326 insertions, 0 deletions
diff --git a/src/routers/vault/middlewares/evol/account.js b/src/routers/vault/middlewares/evol/account.js new file mode 100644 index 0000000..5067334 --- /dev/null +++ b/src/routers/vault/middlewares/evol/account.js @@ -0,0 +1,326 @@ +"use strict"; + +const regexes = { + token: /^[a-zA-Z0-9-_]{6,128}$/, // UUID + any30: /^[^\s][^\t\r\n]{6,28}[^\s]$/, // herc password (this looks scary) + alnum23: /^[a-zA-Z0-9_]{4,23}$/, // mostly for username + gid: /^[23][0-9]{6}$/, // account id +}; + +const get_account_list = async (req, vault_id) => { + const accounts = []; + const claimed = await req.app.locals.vault.claimed_game_accounts.findAll({ + where: {vaultId: vault_id}, + }); + + for (let acc of claimed) { + acc = await req.app.locals.evol.login.findByPk(acc.accountId); + const chars = []; + const chars_ = await req.app.locals.evol.char.findAll({ + where: {accountId: acc.accountId}, + }); + + for (const char of chars_) { + chars.push({ + // TODO: make this a class + name: char.name, + charId: char.charId, + level: char.baseLevel, + sex: char.sex, + }); + } + + accounts.push({ + // TODO: make this a class + name: acc.userid, + accountId: acc.accountId, + chars, + }); + } + + return accounts; +}; + +const get_accounts = async (req, res, next) => { + const token = String(req.get("X-VAULT-SESSION") || ""); + + if (!token.match(regexes.token)) { + res.status(400).json({ + status: "error", + error: "missing session key", + }); + req.app.locals.logger.warn(`Vault.evol.account: blocked an attempt to bypass authentication [${req.ip}]`); + req.app.locals.cooldown(req, 3e5); + return; + } + + const session = req.app.locals.session.get(token); + + if (session === null || session === undefined) { + res.status(410).json({ + status: "error", + error: "session expired", + }); + req.app.locals.cooldown(req, 5e3); + return; + } + + if (session.authenticated !== true) { + res.status(401).json({ + status: "error", + error: "not authenticated", + }); + req.app.locals.logger.warn(`Vault.evol.account: blocked an attempt to bypass authentication [${req.ip}]`); + req.app.locals.cooldown(req, 3e5); + return; + } + + let accounts = session.gameAccounts; + + if (accounts.length < 1) { + console.info(`Vault.evol.account: fetching evol accounts {${session.vault}} [${req.ip}]`); + accounts = await get_account_list(req, session.vault); + session.gameAccounts = accounts; + req.app.locals.cooldown(req, 3e3); + } else { + req.app.locals.cooldown(req, 1e3); + } + + res.status(200).json({ + status: "success", + accounts, + }); +}; + +const new_account = async (req, res, next) => { + const token = String(req.get("X-VAULT-SESSION") || ""); + + if (!token.match(regexes.token)) { + res.status(400).json({ + status: "error", + error: "missing session key", + }); + req.app.locals.logger.warn(`Vault.evol.account: blocked an attempt to bypass authentication [${req.ip}]`); + req.app.locals.cooldown(req, 3e5); + return; + } + + if (!req.body || + !Reflect.has(req.body, "username") || !Reflect.has(req.body, "password") || + !req.body.username.match(regexes.alnum23) || + !req.body.password.match(regexes.any30)) { // FIXME: this is unsafe: can cause a promise rejection if something else than a string is passed (no Number.match() exists) + res.status(400).json({ + status: "error", + error: "invalid format", + }); + req.app.locals.cooldown(req, 5e3); + return; + } + + const session = req.app.locals.session.get(token); + + if (session === null || session === undefined) { + res.status(410).json({ + status: "error", + error: "session expired", + }); + req.app.locals.cooldown(req, 5e3); + return; + } + + if (session.authenticated !== true) { + res.status(401).json({ + status: "error", + error: "not authenticated", + }); + req.app.locals.logger.warn(`Vault.evol.account: blocked an attempt to bypass authentication [${req.ip}]`); + req.app.locals.cooldown(req, 3e5); + return; + } + + // this check is necessary because login.userid has no UNIQUE constraint + const existing = await req.app.locals.evol.login.findOne({ + where: {userid: req.body.username} + }); + + if (existing !== null) { + res.status(409).json({ + status: "error", + error: "already exists", + }); + req.app.locals.cooldown(1e3); + return; + } + + const evol_acc = await req.app.locals.evol.login.create({ + userid: req.body.username, + userPass: req.body.password, + email: `${session.vault}@vault`, // setting an actual email is pointless + }); + + req.app.locals.vault.account_log.create({ + vaultId: session.vault, + accountType: "EVOL", + actionType: "CREATE", + accountId: evol_acc.accountId, + ip: req.app.locals.sequelize.vault.fn("INET6_ATON", req.ip), + }); + + // immediately claim it + await req.app.locals.vault.claimed_game_accounts.create({ + accountId: evol_acc.accountId, + vaultId: session.vault, + }); + + // now add it to the evol cache + const account = { + name: evol_acc.userid, + accountId: evol_acc.accountId, + chars: [], + }; + session.gameAccounts.push(account); + + req.app.locals.logger.info(`Vault.evol.account: created a new game account: ${account.accountId} {${session.vault}} [${req.ip}]`); + + res.status(200).json({ + status: "success", + account, + }); + + req.app.locals.cooldown(5e3); +}; + +const update_account = async (req, res, next) => { + const token = String(req.get("X-VAULT-SESSION") || ""); + + if (!token.match(regexes.token)) { + res.status(400).json({ + status: "error", + error: "missing session key", + }); + req.app.locals.logger.warn(`Vault.evol.account: blocked an attempt to bypass authentication [${req.ip}]`); + req.app.locals.cooldown(req, 3e5); + return; + } + + if (!req.body || !Reflect.has(req.body, "accountId") || + !String(req.body.accountId).match(regexes.gid) || !( + (Reflect.has(req.body, "username") && req.body.username.match(regexes.alnum23)) || + (Reflect.has(req.body, "password") && req.body.password.match(regexes.any30)))) { // FIXME: this is unsafe: can cause a promise rejection if something else than a string is passed (no Number.match() exists) + res.status(400).json({ + status: "error", + error: "invalid format", + }); + req.app.locals.cooldown(req, 5e3); + return; + } + + const session = req.app.locals.session.get(token); + + if (session === null || session === undefined) { + res.status(410).json({ + status: "error", + error: "session expired", + }); + req.app.locals.cooldown(req, 5e3); + return; + } + + if (session.authenticated !== true) { + res.status(401).json({ + status: "error", + error: "not authenticated", + }); + req.app.locals.logger.warn(`Vault.evol.account: blocked an attempt to bypass authentication [${req.ip}]`); + req.app.locals.cooldown(req, 3e5); + return; + } + + let account = null; + for (const acc of session.gameAccounts) { + if (acc.accountId === req.body.accountId) { + account = acc; + break; + } + } + + if (account === null) { + res.status(404).json({ + status: "error", + error: "account not found", + }); + req.app.locals.logger.warn(`Vault.evol.account: blocked an attempt to modify a game account not owned by the user {${session.vault}} [${req.ip}]`); + req.app.locals.cooldown(req, 3e5); + return; + } + + let update_fields = {}; + if (Reflect.has(req.body, "username")) { + // check if the name exists + const existing = await req.app.locals.evol.login.findOne({ + where: {userid: req.body.username} + }); + + if (existing !== null) { + res.status(409).json({ + status: "error", + error: "already exists", + }); + req.app.locals.cooldown(req, 500); + return; + } + + update_fields = { + userid: req.body.username, + }; + account.name = req.body.username; + req.app.locals.logger.info(`Vault.evol.account: changed username of game account ${account.accountId} {${session.vault}} [${req.ip}]`); + req.app.locals.vault.account_log.create({ + vaultId: session.vault, + accountType: "EVOL", + actionType: "UPDATE", + details: "username", + accountId: account.accountId, + ip: req.app.locals.sequelize.vault.fn("INET6_ATON", req.ip), + }); + } else { + update_fields = { + userPass: req.body.password, + }; + req.app.locals.logger.info(`Vault.evol.account: changed password of game account ${account.accountId} {${session.vault}} [${req.ip}]`); + req.app.locals.vault.account_log.create({ + vaultId: session.vault, + accountType: "EVOL", + actionType: "UPDATE", + details: "password", + accountId: account.accountId, + ip: req.app.locals.sequelize.vault.fn("INET6_ATON", req.ip), + }); + } + + await req.app.locals.evol.login.update(update_fields, {where: { + accountId: account.accountId, + }}); + + res.status(200).json({ + status: "success", + account, + }); + + req.app.locals.cooldown(req, 5e3); +}; + +module.exports = exports = async (req, res, next) => { + switch(req.method) { + case "GET": // list accounts + return await get_accounts(req, res, next); + case "POST": // new account + return await new_account(req, res, next); + case "PATCH": // change username/password + return await update_account(req, res, next); + // TODO: PUT: move char + // TODO: DELETE: delete account and related data + default: + next(); // fallthrough to default endpoint (404) + } +}; |