d62dd3e62d
Message stores are more complicated that a sync "fire and forget" API allows for. For starters, non trivial stores (say sqlite) can fail during init and we want to be able to catch that. Second, we really need to be able to run migrations and such, which may block (and fail) the activation of the store. On the plus side, this pushes error handling to the caller rather than the stores, which is a good thing as that allows us to eventually push this to the client in the UI, rather than just logging it in the server on stdout
201 lines
4.3 KiB
TypeScript
201 lines
4.3 KiB
TypeScript
import pkg from "../package.json";
|
|
import _ from "lodash";
|
|
import path from "path";
|
|
import os from "os";
|
|
import fs from "fs";
|
|
import net from "net";
|
|
import bcrypt from "bcryptjs";
|
|
import crypto from "crypto";
|
|
|
|
export type Hostmask = {
|
|
nick: string;
|
|
ident: string;
|
|
hostname: string;
|
|
};
|
|
|
|
const Helper = {
|
|
expandHome,
|
|
getVersion,
|
|
getVersionCacheBust,
|
|
getVersionNumber,
|
|
getGitCommit,
|
|
ip2hex,
|
|
parseHostmask,
|
|
compareHostmask,
|
|
compareWithWildcard,
|
|
catch_to_error,
|
|
|
|
password: {
|
|
hash: passwordHash,
|
|
compare: passwordCompare,
|
|
requiresUpdate: passwordRequiresUpdate,
|
|
},
|
|
};
|
|
|
|
export default Helper;
|
|
|
|
function getVersion() {
|
|
const gitCommit = getGitCommit();
|
|
const version = `v${pkg.version}`;
|
|
return gitCommit ? `source (${gitCommit} / ${version})` : version;
|
|
}
|
|
|
|
function getVersionNumber() {
|
|
return pkg.version;
|
|
}
|
|
|
|
let _gitCommit: string | null = null;
|
|
|
|
function getGitCommit() {
|
|
if (_gitCommit) {
|
|
return _gitCommit;
|
|
}
|
|
|
|
if (!fs.existsSync(path.resolve(__dirname, "..", ".git"))) {
|
|
_gitCommit = null;
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
_gitCommit = require("child_process")
|
|
.execSync(
|
|
"git rev-parse --short HEAD", // Returns hash of current commit
|
|
{stdio: ["ignore", "pipe", "ignore"]}
|
|
)
|
|
.toString()
|
|
.trim();
|
|
return _gitCommit;
|
|
} catch (e: any) {
|
|
// Not a git repository or git is not installed
|
|
_gitCommit = null;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function getVersionCacheBust() {
|
|
const hash = crypto.createHash("sha256").update(Helper.getVersion()).digest("hex");
|
|
|
|
return hash.substring(0, 10);
|
|
}
|
|
|
|
function ip2hex(address: string) {
|
|
// no ipv6 support
|
|
if (!net.isIPv4(address)) {
|
|
return "00000000";
|
|
}
|
|
|
|
return address
|
|
.split(".")
|
|
.map(function (octet) {
|
|
let hex = parseInt(octet, 10).toString(16);
|
|
|
|
if (hex.length === 1) {
|
|
hex = "0" + hex;
|
|
}
|
|
|
|
return hex;
|
|
})
|
|
.join("");
|
|
}
|
|
|
|
// Expand ~ into the current user home dir.
|
|
// This does *not* support `~other_user/tmp` => `/home/other_user/tmp`.
|
|
function expandHome(shortenedPath: string) {
|
|
if (!shortenedPath) {
|
|
return "";
|
|
}
|
|
|
|
const home = os.homedir().replace("$", "$$$$");
|
|
return path.resolve(shortenedPath.replace(/^~($|\/|\\)/, home + "$1"));
|
|
}
|
|
|
|
function passwordRequiresUpdate(password: string) {
|
|
return bcrypt.getRounds(password) !== 11;
|
|
}
|
|
|
|
function passwordHash(password: string) {
|
|
return bcrypt.hashSync(password, bcrypt.genSaltSync(11));
|
|
}
|
|
|
|
function passwordCompare(password: string, expected: string) {
|
|
return bcrypt.compare(password, expected);
|
|
}
|
|
|
|
function parseHostmask(hostmask: string): Hostmask {
|
|
let nick = "";
|
|
let ident = "*";
|
|
let hostname = "*";
|
|
let parts: string[] = [];
|
|
|
|
// Parse hostname first, then parse the rest
|
|
parts = hostmask.split("@");
|
|
|
|
if (parts.length >= 2) {
|
|
hostname = parts[1] || "*";
|
|
hostmask = parts[0];
|
|
}
|
|
|
|
hostname = hostname.toLowerCase();
|
|
|
|
parts = hostmask.split("!");
|
|
|
|
if (parts.length >= 2) {
|
|
ident = parts[1] || "*";
|
|
hostmask = parts[0];
|
|
}
|
|
|
|
ident = ident.toLowerCase();
|
|
|
|
nick = hostmask.toLowerCase() || "*";
|
|
|
|
const result = {
|
|
nick: nick,
|
|
ident: ident,
|
|
hostname: hostname,
|
|
};
|
|
|
|
return result;
|
|
}
|
|
|
|
function compareHostmask(a: Hostmask, b: Hostmask) {
|
|
return (
|
|
compareWithWildcard(a.nick, b.nick) &&
|
|
compareWithWildcard(a.ident, b.ident) &&
|
|
compareWithWildcard(a.hostname, b.hostname)
|
|
);
|
|
}
|
|
|
|
function compareWithWildcard(a: string, b: string) {
|
|
// we allow '*' and '?' wildcards in our comparison.
|
|
// this is mostly aligned with https://modern.ircdocs.horse/#wildcard-expressions
|
|
// but we do not support the escaping. The ABNF does not seem to be clear as to
|
|
// how to escape the escape char '\', which is valid in a nick,
|
|
// whereas the wildcards tend not to be (as per RFC1459).
|
|
|
|
// The "*" wildcard is ".*" in regex, "?" is "."
|
|
// so we tokenize and join with the proper char back together,
|
|
// escaping any other regex modifier
|
|
const wildmany_split = a.split("*").map((sub) => {
|
|
const wildone_split = sub.split("?").map((p) => _.escapeRegExp(p));
|
|
return wildone_split.join(".");
|
|
});
|
|
const user_regex = wildmany_split.join(".*");
|
|
const re = new RegExp(`^${user_regex}$`, "i"); // case insensitive
|
|
return re.test(b);
|
|
}
|
|
|
|
function catch_to_error(prefix: string, err: any): Error {
|
|
let msg: string;
|
|
|
|
if (err instanceof Error) {
|
|
msg = err.message;
|
|
} else if (typeof err === "string") {
|
|
msg = err;
|
|
} else {
|
|
msg = err.toString();
|
|
}
|
|
|
|
return new Error(`${prefix}: ${msg}`);
|
|
}
|