summaryrefslogblamecommitdiff
path: root/src/routers/vault/middlewares/legacy/account.js
blob: dddabdb781751c53031d7481f81d39d9480ac4ae (plain) (tree)
1
2
3
4
5
6
7
8






                                                            
                                                                             


























































































































































                                                                                                                         

                                                                   













































































































































































































                                                                                                                                                                               
                      
             
                                                               




























































                                                                                                                                   
"use strict";
const uuidv4 = require("uuid/v4");
const md5saltcrypt = require("../../utils/md5saltcrypt.js");
const flatfile = require("../../utils/flatfile.js");

const regexes = {
    token: /^[a-zA-Z0-9-_]{6,128}$/, // UUID
    any23: /^[^\s][^\t\r\n]{2,21}[^\s]$/, // tmwa password (this looks scary)
    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_legacy_accounts.findAll({
        where: {vaultId: vault_id},
    });

    for (let acc of claimed) {
        acc = await req.app.locals.legacy.login.findByPk(acc.accountId);
        const chars = [];
        const chars_ = await req.app.locals.legacy.char.findAll({
            where: {accountId: acc.accountId},
        });

        for (const char of chars_) {
            chars.push({
                name: char.name,
                charId: char.charId,
                revoltId: char.revoltId,
                level: char.baseLevel,
                sex: char.sex,
            });
        }

        accounts.push({
            name: acc.userid,
            accountId: acc.accountId,
            revoltId: acc.revoltId,
            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.legacy.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.legacy.account: blocked an attempt to bypass authentication [${req.ip}]`);
        req.app.locals.cooldown(req, 3e5);
        return;
    }

    let accounts = session.legacyAccounts;

    if (accounts.length < 1) {
        console.info(`Vault.legacy.account: fetching legacy accounts {${session.vault}} [${req.ip}]`);
        accounts = await get_account_list(req, session.vault);
        session.legacyAccounts = accounts;
        req.app.locals.cooldown(req, 3e3);
    } else {
        req.app.locals.cooldown(req, 1e3);
    }

    res.status(200).json({
        status: "success",
        accounts,
    });
};

const claim_by_password = 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.legacy.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.any23)) {
        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.legacy.account: blocked an attempt to bypass authentication [${req.ip}]`);
        req.app.locals.cooldown(req, 3e5);
        return;
    }

    const legacy = await req.app.locals.legacy.login.findOne({
        where: {userid: req.body.username}
    });

    if (legacy === null) {
        res.status(404).json({
            status: "error",
            error: "not found",
        });
        req.app.locals.cooldown(req, 1e3);
        return;
    }

    if (!md5saltcrypt.verify(legacy.userPass, req.body.password)) {
        // check to see if the password has been updated since it was dumped to SQL
        const flatfile_account = await flatfile.findAccount(legacy.accountId, legacy.userid); // this operation is costly
        if (flatfile_account !== null &&
            md5saltcrypt.verify(flatfile_account.password, req.body.password)) {
            // update the password in SQL (deferred)
            console.log(`Vault.legacy.account: updating SQL password from flatfile for account ${legacy.accountId}`);
            legacy.userPass = md5saltcrypt.hash(req.body.password);
            legacy.save();
        } else {
            // the password is just plain wrong
            res.status(404).json({
                status: "error",
                error: "not found",
            });
            console.warn(`Vault.legacy.account: failed to log in to Legacy account {${session.vault}} [${req.ip}]`);
            req.app.locals.cooldown(req, 3e5);
            // TODO: huge cooldown after 8 attempts
            return;
        }
    }

    const claimed = await req.app.locals.vault.claimed_legacy_accounts.findByPk(legacy.accountId);

    if (claimed !== null) {
        res.status(409).json({
            status: "error",
            error: "already assigned",
        });
        req.app.locals.logger.warn(`Vault.legacy.account: blocked an attempt to link an already-linked account {${session.vault}} [${req.ip}]`);
        req.app.locals.cooldown(req, 3e5);
        return;
    }

    await req.app.locals.vault.claimed_legacy_accounts.create({
        accountId: legacy.accountId,
        vaultId: session.vault,
    });

    // log this action:
    req.app.locals.vault.account_log.create({
        vaultId: session.vault,
        accountType: "LEGACY",
        actionType: "LINK",
        accountId: legacy.accountId,
        ip: req.app.locals.sequelize.vault.fn("INET6_ATON", req.ip),
    });

    // now we must update the session cache:
    const chars = [];
    const chars_ = await req.app.locals.legacy.char.findAll({
        where: {accountId: legacy.accountId},
    });

    for (const char of chars_) {
        chars.push({
            // TODO: make this a class
            name: char.name,
            charId: char.charId,
            revoltId: char.revoltId,
            level: char.baseLevel,
            sex: char.sex,
        });
    }

    const account = {
        name: legacy.userid,
        accountId: legacy.accountId,
        revoltId: legacy.revoltId,
        chars,
    };
    session.legacyAccounts.push(account);

    res.status(200).json({
        status: "success",
        account
    });

    req.app.locals.logger.info(`Vault.legacy.account: linked Legacy account ${legacy.accountId} to Vault account {${session.vault}} [${req.ip}]`);
    req.app.locals.cooldown(req, 8e3);
};

const migrate = 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.legacy.account: blocked an attempt to bypass authentication [${req.ip}]`);
        req.app.locals.cooldown(req, 3e5);
        return;
    }

    if (!req.body || !Reflect.has(req.body, "accountId") ||
        !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)
        !String(req.body.accountId).match(regexes.gid)) {
        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.legacy.account: blocked an attempt to bypass authentication [${req.ip}]`);
        req.app.locals.cooldown(req, 3e5);
        return;
    }

    let legacy = null;

    // check if we own it
    // NOTE: this cached data is never stale because we update it when operations are performed
    for (const acc of session.legacyAccounts) {
        if (acc.accountId === req.body.accountId) {
            legacy = acc;
            break;
        }
    }

    if (legacy === null) {
        res.status(404).json({
            status: "error",
            error: "not found",
        });
        req.app.locals.logger.warn(`Vault.legacy.account: blocked an attempt to migrate a Legacy account not owned by the user {${session.vault}} [${req.ip}]`);
        req.app.locals.cooldown(req, 3e5);
        return;
    }

    if (legacy.revoltId) {
        res.status(409).json({
            status: "error",
            error: "already migrated",
        });
        req.app.locals.logger.warn(`Vault.legacy.account: blocked an attempt to migrate an already-migrated Legacy account {${session.vault}} [${req.ip}]`);
        req.app.locals.cooldown(req, 3e5);
        return;
    }

    // lots of queries (expensive!):

    // 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(req, 2e3);
        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.migration_log.create({
        vaultId: session.vault,
        legacyId: legacy.accountId,
        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 cache_key = session.gameAccounts.push({
        name: evol_acc.userid,
        accountId: evol_acc.accountId,
        chars: [],
    }) - 1;

    legacy.revoltId = evol_acc.accountId; // update legacy cache
    await req.app.locals.legacy.login.update({ // update sql
        revoltId: evol_acc.accountId,
    }, {where: {
        accountId: legacy.accountId,
    }});

    // XXX: ideally we should be using createBulk but we also want to update
    for (const [num, char] of legacy.chars.entries()) {
        if (char.revoltId) {
            continue;
        }

        let evol_char;
        try {
            evol_char = await req.app.locals.evol.char.create({
                name: char.name,
                charNum: num,
                accountId: evol_acc.accountId,
                hairColor: Math.floor(Math.random() * 21), // range: [0,21[
                hair: (Math.floor(Math.random() * 28) + 1), // range: [1,28]
                sex: char.sex === "F" ? "F" : (char.sex === "M" ? "M" : "U"), // non-binary is undefined in evol
            });
        } catch (err) {
            // char.name has a UNIQUE constraint but an actual collision would never happen
            console.error(err);
            continue;
        }

        // remove the name reservation
        req.app.locals.evol.char_reservation.destroy({
            where: { name: char.name }
        });

        // update the evol cache
        session.gameAccounts[cache_key].chars.push({
            name: evol_char.name,
            charId: evol_char.charId,
            level: 1,
            sex: evol_char.sex,
        });

        char.revoltId = evol_char.charId; // update legacy cache
        await req.app.locals.legacy.char.update({ // update sql
            revoltId: evol_char.charId,
        }, {where: {
            charId: char.charId,
        }});
    }

    // TODO: try/catch each of the await operations

    res.status(200).json({
        status: "success",
        account: session.gameAccounts[cache_key],
    });

    req.app.locals.logger.info(`Vault.legacy.account: migrated Legacy account ${legacy.accountId} {${session.vault}} [${req.ip}]`);
    req.app.locals.cooldown(req, 15e3);
};

module.exports = exports = async (req, res, next) => {
    switch(req.method) {
        case "GET":
            // list accounts
            return await get_accounts(req, res, next);
        case "POST":
            // add account (by password)
            return await claim_by_password(req, res, next);
        case "PATCH":
            // migrate to new server
            return await migrate(req, res, next);
        // TODO: password reset
        default:
            next(); // fallthrough to default endpoint (404)
    }
};