summaryrefslogtreecommitdiff
path: root/src/routers/vault/utils
diff options
context:
space:
mode:
Diffstat (limited to 'src/routers/vault/utils')
-rw-r--r--src/routers/vault/utils/claim.js83
-rw-r--r--src/routers/vault/utils/ephemeral.js87
-rw-r--r--src/routers/vault/utils/flatfile.js31
-rw-r--r--src/routers/vault/utils/md5saltcrypt.js38
4 files changed, 239 insertions, 0 deletions
diff --git a/src/routers/vault/utils/claim.js b/src/routers/vault/utils/claim.js
new file mode 100644
index 0000000..472e05a
--- /dev/null
+++ b/src/routers/vault/utils/claim.js
@@ -0,0 +1,83 @@
+const { Op } = require("sequelize");
+
+// claim by email // TODO: DRY this
+const claim_accounts = async (req, email, vault_id, session = null) => {
+ const locals = req.app.locals;
+
+ if (email === null || email.length < 5 || email === "a@a.com")
+ return Promise.resolve(false);
+
+ if (session === null) {
+ for (const [key, sess] of locals.session) {
+ // try to find the session
+ if (sess.authenticated === true && sess.vault === vault_id) {
+ session = sess;
+ }
+ }
+ }
+
+ // TODO: make these operations less expensive (foreign keys could help)
+ let already_claimed = await locals.vault.claimed_legacy_accounts.findAll({
+ where: {vaultId: vault_id},
+ });
+
+ already_claimed = already_claimed.map(acc => {
+ return {accountId: {
+ [Op.not]: acc.accountId, // NOTE: if query is larger than 65535 this will throw
+ }};
+ });
+
+ const to_claim = await locals.legacy.login.findAll({
+ where: {
+ email: email,
+ [Op.and]: already_claimed,
+ },
+ });
+
+ for (const acc of to_claim) {
+ await locals.vault.claimed_legacy_accounts.create({
+ accountId: acc.accountId,
+ vaultId: vault_id,
+ });
+
+ req.app.locals.vault.account_log.create({
+ vaultId: vault_id,
+ accountType: "LEGACY",
+ actionType: "LINK",
+ accountId: acc.accountId,
+ ip: req.app.locals.sequelize.vault.fn("INET6_ATON", req.ip),
+ });
+
+ if (session !== null) {
+ const chars = [];
+ const chars_ = await 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,
+ });
+ }
+ // add to session cache
+ session.legacyAccounts.push({
+ name: acc.userid,
+ accountId: acc.accountId,
+ revoltId: acc.revoltId,
+ chars,
+ });
+ }
+
+ locals.logger.info(`Vault: linked Legacy account ${acc.accountId} to Vault account {${vault_id}} [${req.ip}]`);
+ }
+
+ // TODO: split TMWA claiming into its own function, add forums and wiki claiming
+};
+
+module.exports = {
+ claim_accounts,
+};
diff --git a/src/routers/vault/utils/ephemeral.js b/src/routers/vault/utils/ephemeral.js
new file mode 100644
index 0000000..211e84b
--- /dev/null
+++ b/src/routers/vault/utils/ephemeral.js
@@ -0,0 +1,87 @@
+// TODO: use Redis for in-memory caching of sessions
+
+// this was originally a Proxy<Map<String, Session>> but was replaced because of a nodejs bug
+// XXX: maybe we should use an already-existing Express session manager // NIH syndrome
+const timeout_symbol = Symbol("timeout");
+const hydrate_symbol = Symbol("hydrate");
+const container_symbol = Symbol("container");
+const session_handler = {
+ [container_symbol]: new Map(),
+ [hydrate_symbol] (key, obj) {
+ if (obj === null || obj === undefined)
+ return obj;
+
+ if (Reflect.has(obj, timeout_symbol))
+ clearTimeout(obj[timeout_symbol]);
+
+ let expires = new Date();
+ expires.setUTCHours(expires.getUTCHours() + 6);
+ obj.expires = expires // this could also be a symbol
+ obj[timeout_symbol] = setTimeout(() => session_handler.delete(key), 6 * 3600000); // 6 hours
+
+ return obj;
+ },
+ has (key) {
+ return session_handler[container_symbol].has(key);
+ },
+ get (key) {
+ return session_handler[hydrate_symbol](key, session_handler[container_symbol].get(key));
+ },
+ set (key, obj) {
+ return session_handler[container_symbol].set(key, session_handler[hydrate_symbol](key, obj));
+ },
+ delete (key) {
+ if (session_handler[container_symbol].get(key) && session_handler[container_symbol].get(key)[timeout_symbol])
+ clearTimeout(session_handler[container_symbol].get(key)[timeout_symbol]);
+ return session_handler[container_symbol].delete(key);
+ },
+ [Symbol.iterator]: function* () {
+ for (const [key, obj] of session_handler[container_symbol]) {
+ yield [key, obj];
+ }
+ },
+};
+
+// TODO: DRY this shit
+const identity_handler = {
+ [container_symbol]: new Map(),
+ [hydrate_symbol] (key, obj) {
+ if (obj === null || obj === undefined)
+ return obj;
+
+ if (Reflect.has(obj, timeout_symbol))
+ clearTimeout(obj[timeout_symbol]);
+
+ let expires = new Date();
+ expires.setUTCMinutes(expires.getUTCMinutes() + 30);
+ obj.expires = expires // this could also be a symbol
+ obj[timeout_symbol] = setTimeout(() => identity_handler.delete(key), 30 * 60000); // 30 minutes
+
+ return obj;
+ },
+ has (key) {
+ return identity_handler[container_symbol].has(key);
+ },
+ get (key) {
+ return identity_handler[container_symbol].get(key);
+ },
+ set (key, obj) {
+ return identity_handler[container_symbol].set(key, identity_handler[hydrate_symbol](key, obj));
+ },
+ delete (key) {
+ if (identity_handler[container_symbol].get(key) && identity_handler[container_symbol].get(key)[timeout_symbol])
+ clearTimeout(identity_handler[container_symbol].get(key)[timeout_symbol]);
+ return identity_handler[container_symbol].delete(key);
+ },
+ [Symbol.iterator]: function* () {
+ for (const [key, obj] of identity_handler[container_symbol]) {
+ yield [key, obj];
+ }
+ },
+};
+
+
+module.exports = {
+ session_handler,
+ identity_handler,
+}
diff --git a/src/routers/vault/utils/flatfile.js b/src/routers/vault/utils/flatfile.js
new file mode 100644
index 0000000..e9d6fee
--- /dev/null
+++ b/src/routers/vault/utils/flatfile.js
@@ -0,0 +1,31 @@
+const execFile = require("child_process").execFile;
+const ripgrep = require("ripgrep-bin");
+
+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(?<password>[^\t]+)\t");
+
+const parseAccountLine = (line) => {
+ const { groups: account } = tmwa_account_regex.exec(line);
+ return {
+ id: +account.id,
+ name: account.name,
+ password: account.password,
+ };
+}
+
+const findAccount = async (account_id, name) => {
+ const regex = `^${account_id}\t${name}\t`;
+ const stdout = await execAsync(ripgrep, ["--case-sensitive", `--max-count=1`, regex, "account.txt"]);
+ let account = null;
+ if (stdout.length)
+ account = parseAccountLine(stdout.slice(0, -1).split("\n")[0]);
+ return account;
+};
+
+module.exports = {
+ findAccount,
+};
diff --git a/src/routers/vault/utils/md5saltcrypt.js b/src/routers/vault/utils/md5saltcrypt.js
new file mode 100644
index 0000000..55933f5
--- /dev/null
+++ b/src/routers/vault/utils/md5saltcrypt.js
@@ -0,0 +1,38 @@
+// password hashing for the Legacy server
+// https://gitlab.com/evol/evol-hercules/blob/master/src/elogin/md5calc.c
+// https://github.com/themanaworld/tmwa/blob/c82c9741bc1a0b110bccce1bcc76903a6e747a00/src/high/md5more.cpp
+
+const crypto = require("crypto"); // native
+
+// generate md5 from string
+const md5 = (str) => crypto.createHash("md5").update(str).digest("hex");
+
+// weak md5 password hashing and salting (eAthena)
+const md5saltcrypt = (salt, plain) => md5(md5(plain) + md5(salt)).slice(0, -8);
+
+// check plain password against its salted hash
+const verify = (salt, hashed, plain) => md5saltcrypt(salt, plain) === hashed;
+
+// takes apart a password string (!salt$hash) and verifies it
+const verify_ea = (raw, plain) => verify(raw.slice(1, 6), raw.slice(-24), plain);
+
+// generate a new salt
+const new_salt = () => {
+ let salt = "";
+ do {
+ salt += String.fromCharCode(Math.floor(78 * Math.random() + 48));
+ } while (salt.length < 5);
+ return salt;
+};
+
+// generate a password string with the given salt
+const hash = (salt, plain) => `!${salt}$${md5saltcrypt(salt, plain)}`;
+
+// generate a password string with a new salt
+const hash_new = (plain) => hash(new_salt(), plain);
+
+
+module.exports = {
+ verify: verify_ea,
+ hash: hash_new,
+};