summary refs log tree commit diff
path: root/src/api/util/utility
diff options
context:
space:
mode:
Diffstat (limited to 'src/api/util/utility')
-rw-r--r--src/api/util/utility/Base64.ts47
-rw-r--r--src/api/util/utility/RandomInviteID.ts32
-rw-r--r--src/api/util/utility/String.ts18
-rw-r--r--src/api/util/utility/captcha.ts46
-rw-r--r--src/api/util/utility/ipAddress.ts95
-rw-r--r--src/api/util/utility/passwordStrength.ts60
6 files changed, 298 insertions, 0 deletions
diff --git a/src/api/util/utility/Base64.ts b/src/api/util/utility/Base64.ts
new file mode 100644
index 00000000..46cff77a
--- /dev/null
+++ b/src/api/util/utility/Base64.ts
@@ -0,0 +1,47 @@
+const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+";
+
+// binary to string lookup table
+const b2s = alphabet.split("");
+
+// string to binary lookup table
+// 123 == 'z'.charCodeAt(0) + 1
+const s2b = new Array(123);
+for (let i = 0; i < alphabet.length; i++) {
+	s2b[alphabet.charCodeAt(i)] = i;
+}
+
+// number to base64
+export const ntob = (n: number): string => {
+	if (n < 0) return `-${ntob(-n)}`;
+
+	let lo = n >>> 0;
+	let hi = (n / 4294967296) >>> 0;
+
+	let right = "";
+	while (hi > 0) {
+		right = b2s[0x3f & lo] + right;
+		lo >>>= 6;
+		lo |= (0x3f & hi) << 26;
+		hi >>>= 6;
+	}
+
+	let left = "";
+	do {
+		left = b2s[0x3f & lo] + left;
+		lo >>>= 6;
+	} while (lo > 0);
+
+	return left + right;
+};
+
+// base64 to number
+export const bton = (base64: string) => {
+	let number = 0;
+	const sign = base64.charAt(0) === "-" ? 1 : 0;
+
+	for (let i = sign; i < base64.length; i++) {
+		number = number * 64 + s2b[base64.charCodeAt(i)];
+	}
+
+	return sign ? -number : number;
+};
diff --git a/src/api/util/utility/RandomInviteID.ts b/src/api/util/utility/RandomInviteID.ts
new file mode 100644
index 00000000..7ea344e0
--- /dev/null
+++ b/src/api/util/utility/RandomInviteID.ts
@@ -0,0 +1,32 @@
+import { Snowflake } from "@fosscord/util";
+
+export function random(length = 6) {
+	// Declare all characters
+	let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+
+	// Pick characers randomly
+	let str = "";
+	for (let i = 0; i < length; i++) {
+		str += chars.charAt(Math.floor(Math.random() * chars.length));
+	}
+
+	return str;
+}
+
+export function snowflakeBasedInvite() {
+	// Declare all characters
+	let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+	let base = BigInt(chars.length);
+	let snowflake = Snowflake.generateWorkerProcess();
+
+	// snowflakes hold ~10.75 characters worth of entropy;
+	// safe to generate a 8-char invite out of them
+	let str = "";
+	for (let i=0; i < 10; i++) {
+		
+		str.concat(chars.charAt(Number(snowflake % base)));
+		snowflake = snowflake / base;
+	}
+	
+	return str.substr(3,8).split("").reverse().join("");
+}
diff --git a/src/api/util/utility/String.ts b/src/api/util/utility/String.ts
new file mode 100644
index 00000000..982b7e11
--- /dev/null
+++ b/src/api/util/utility/String.ts
@@ -0,0 +1,18 @@
+import { Request } from "express";
+import { ntob } from "./Base64";
+import { FieldErrors } from "@fosscord/util";
+
+export function checkLength(str: string, min: number, max: number, key: string, req: Request) {
+	if (str.length < min || str.length > max) {
+		throw FieldErrors({
+			[key]: {
+				code: "BASE_TYPE_BAD_LENGTH",
+				message: req.t("common:field.BASE_TYPE_BAD_LENGTH", { length: `${min} - ${max}` })
+			}
+		});
+	}
+}
+
+export function generateCode() {
+	return ntob(Date.now() + Math.randomIntBetween(0, 10000));
+}
diff --git a/src/api/util/utility/captcha.ts b/src/api/util/utility/captcha.ts
new file mode 100644
index 00000000..739647d2
--- /dev/null
+++ b/src/api/util/utility/captcha.ts
@@ -0,0 +1,46 @@
+import { Config } from "@fosscord/util";
+import fetch from "node-fetch";
+
+export interface hcaptchaResponse {
+	success: boolean;
+	challenge_ts: string;
+	hostname: string;
+	credit: boolean;
+	"error-codes": string[];
+	score: number;	// enterprise only
+	score_reason: string[];	// enterprise only
+}
+
+export interface recaptchaResponse {
+	success: boolean;
+	score: number; // between 0 - 1
+	action: string;
+	challenge_ts: string;
+	hostname: string;
+	"error-codes"?: string[];
+}
+
+const verifyEndpoints = {
+	hcaptcha: "https://hcaptcha.com/siteverify",
+	recaptcha: "https://www.google.com/recaptcha/api/siteverify",
+}
+
+export async function verifyCaptcha(response: string, ip?: string) {
+	const { security } = Config.get();
+	const { service, secret, sitekey } = security.captcha;
+
+	if (!service) throw new Error("Cannot verify captcha without service");
+
+	const res = await fetch(verifyEndpoints[service], {
+		method: "POST",
+		headers: {
+			"Content-Type": "application/x-www-form-urlencoded",
+		},
+		body: `response=${encodeURIComponent(response)}`
+			+ `&secret=${encodeURIComponent(secret!)}`
+			+ `&sitekey=${encodeURIComponent(sitekey!)}`
+			+ (ip ? `&remoteip=${encodeURIComponent(ip!)}` : ""),
+	});
+
+	return await res.json() as hcaptchaResponse | recaptchaResponse;
+}
\ No newline at end of file
diff --git a/src/api/util/utility/ipAddress.ts b/src/api/util/utility/ipAddress.ts
new file mode 100644
index 00000000..f17b145e
--- /dev/null
+++ b/src/api/util/utility/ipAddress.ts
@@ -0,0 +1,95 @@
+import { Config } from "@fosscord/util";
+import { Request } from "express";
+// use ipdata package instead of simple fetch because of integrated caching
+import fetch from "node-fetch";
+
+const exampleData = {
+	ip: "",
+	is_eu: true,
+	city: "",
+	region: "",
+	region_code: "",
+	country_name: "",
+	country_code: "",
+	continent_name: "",
+	continent_code: "",
+	latitude: 0,
+	longitude: 0,
+	postal: "",
+	calling_code: "",
+	flag: "",
+	emoji_flag: "",
+	emoji_unicode: "",
+	asn: {
+		asn: "",
+		name: "",
+		domain: "",
+		route: "",
+		type: "isp"
+	},
+	languages: [
+		{
+			name: "",
+			native: ""
+		}
+	],
+	currency: {
+		name: "",
+		code: "",
+		symbol: "",
+		native: "",
+		plural: ""
+	},
+	time_zone: {
+		name: "",
+		abbr: "",
+		offset: "",
+		is_dst: true,
+		current_time: ""
+	},
+	threat: {
+		is_tor: false,
+		is_proxy: false,
+		is_anonymous: false,
+		is_known_attacker: false,
+		is_known_abuser: false,
+		is_threat: false,
+		is_bogon: false
+	},
+	count: 0,
+	status: 200
+};
+
+//TODO add function that support both ip and domain names
+export async function IPAnalysis(ip: string): Promise<typeof exampleData> {
+	const { ipdataApiKey } = Config.get().security;
+	if (!ipdataApiKey) return { ...exampleData, ip };
+
+	return (await fetch(`https://api.ipdata.co/${ip}?api-key=${ipdataApiKey}`)).json() as any; // TODO: types
+}
+
+export function isProxy(data: typeof exampleData) {
+	if (!data || !data.asn || !data.threat) return false;
+	if (data.asn.type !== "isp") return true;
+	if (Object.values(data.threat).some((x) => x)) return true;
+
+	return false;
+}
+
+export function getIpAdress(req: Request): string {
+	// @ts-ignore
+	return req.headers[Config.get().security.forwadedFor] || req.socket.remoteAddress;
+}
+
+export function distanceBetweenLocations(loc1: any, loc2: any): number {
+	return distanceBetweenCoords(loc1.latitude, loc1.longitude, loc2.latitude, loc2.longitude);
+}
+
+//Haversine function
+function distanceBetweenCoords(lat1: number, lon1: number, lat2: number, lon2: number) {
+	const p = 0.017453292519943295; // Math.PI / 180
+	const c = Math.cos;
+	const a = 0.5 - c((lat2 - lat1) * p) / 2 + (c(lat1 * p) * c(lat2 * p) * (1 - c((lon2 - lon1) * p))) / 2;
+
+	return 12742 * Math.asin(Math.sqrt(a)); // 2 * R; R = 6371 km
+}
diff --git a/src/api/util/utility/passwordStrength.ts b/src/api/util/utility/passwordStrength.ts
new file mode 100644
index 00000000..439700d0
--- /dev/null
+++ b/src/api/util/utility/passwordStrength.ts
@@ -0,0 +1,60 @@
+import { Config } from "@fosscord/util";
+import "missing-native-js-functions";
+
+const reNUMBER = /[0-9]/g;
+const reUPPERCASELETTER = /[A-Z]/g;
+const reSYMBOLS = /[A-Z,a-z,0-9]/g;
+
+const blocklist: string[] = []; // TODO: update ones passwordblocklist is stored in db
+/*
+ * https://en.wikipedia.org/wiki/Password_policy
+ * password must meet following criteria, to be perfect:
+ *  - min <n> chars
+ *  - min <n> numbers
+ *  - min <n> symbols
+ *  - min <n> uppercase chars
+ *  - shannon entropy folded into [0, 1) interval
+ *
+ * Returns: 0 > pw > 1
+ */
+export function checkPassword(password: string): number {
+	const { minLength, minNumbers, minUpperCase, minSymbols } = Config.get().register.password;
+	var strength = 0;
+
+	// checks for total password len
+	if (password.length >= minLength - 1) {
+		strength += 0.05;
+	}
+
+	// checks for amount of Numbers
+	if (password.count(reNUMBER) >= minNumbers - 1) {
+		strength += 0.05;
+	}
+
+	// checks for amount of Uppercase Letters
+	if (password.count(reUPPERCASELETTER) >= minUpperCase - 1) {
+		strength += 0.05;
+	}
+
+	// checks for amount of symbols
+	if (password.replace(reSYMBOLS, "").length >= minSymbols - 1) {
+		strength += 0.05;
+	}
+
+	// checks if password only consists of numbers or only consists of chars
+	if (password.length == password.count(reNUMBER) || password.length === password.count(reUPPERCASELETTER)) {
+		strength = 0;
+	}
+	
+	let entropyMap: { [key: string]: number } = {};
+	for (let i = 0; i < password.length; i++) {
+		if (entropyMap[password[i]]) entropyMap[password[i]]++;
+		else entropyMap[password[i]] = 1;
+	}
+	
+	let entropies = Object.values(entropyMap);
+	
+	entropies.map(x => (x / entropyMap.length));
+	strength += entropies.reduceRight((a: number, x: number) => a - (x * Math.log2(x))) / Math.log2(password.length);	
+	return strength;
+}