From 258b96757f2d30f68ce873be04b5169de1e1eb9b Mon Sep 17 00:00:00 2001 From: TheArcaneBrony Date: Sat, 17 Sep 2022 23:35:31 +0200 Subject: Cryptographically secure invites, add generation of tokens --- .gitignore | 2 + package.json | 2 + src/api/middlewares/Authentication.ts | 2 +- src/api/middlewares/RateLimit.ts | 2 +- .../routes/auth/generate-registration-tokens.ts | 29 +++ src/api/routes/auth/location-metadata.ts | 3 +- src/api/routes/auth/login.ts | 4 +- src/api/routes/auth/register.ts | 85 ++++--- src/api/routes/guilds/#guild_id/bans.ts | 5 +- src/api/routes/guilds/#guild_id/regions.ts | 4 +- src/api/routes/guilds/#guild_id/templates.ts | 4 +- src/api/routes/guilds/#guild_id/widget.json.ts | 4 +- src/api/routes/voice/regions.ts | 3 +- src/api/util/handlers/Voice.ts | 3 +- src/api/util/index.ts | 8 +- src/api/util/utility/Base64.ts | 47 ---- src/api/util/utility/RandomInviteID.ts | 31 --- src/api/util/utility/String.ts | 18 -- src/api/util/utility/captcha.ts | 47 ---- src/api/util/utility/ipAddress.ts | 99 --------- src/api/util/utility/passwordStrength.ts | 59 ----- src/util/config/types/GeneralConfiguration.ts | 1 + src/util/config/types/RegisterConfiguration.ts | 1 - src/util/config/types/SecurityConfiguration.ts | 1 + src/util/entities/Attachment.ts | 2 +- src/util/entities/Invite.ts | 2 +- src/util/entities/ValidRegistrationTokens.ts | 12 + src/util/entities/index.ts | 1 + .../mariadb/1663440589234-registration_tokens.ts | 31 +++ ...663448562034-drop_id_for_registration_tokens.ts | 33 +++ .../postgres/1663440587650-registration_tokens.ts | 33 +++ ...663448561249-drop_id_for_registration_tokens.ts | 33 +++ .../sqlite/1663440585960-registration_tokens.ts | 246 +++++++++++++++++++++ ...663448560501-drop_id_for_registration_tokens.ts | 97 ++++++++ src/util/util/Base64.ts | 47 ++++ src/util/util/CDN.ts | 55 +++++ src/util/util/Captcha.ts | 47 ++++ src/util/util/IPAddress.ts | 99 +++++++++ src/util/util/PasswordStrength.ts | 59 +++++ src/util/util/RandomInviteID.ts | 31 +++ src/util/util/String.ts | 18 ++ src/util/util/cdn.ts | 55 ----- src/util/util/index.ts | 5 +- 43 files changed, 940 insertions(+), 430 deletions(-) create mode 100644 src/api/routes/auth/generate-registration-tokens.ts delete mode 100644 src/api/util/utility/Base64.ts delete mode 100644 src/api/util/utility/RandomInviteID.ts delete mode 100644 src/api/util/utility/String.ts delete mode 100644 src/api/util/utility/captcha.ts delete mode 100644 src/api/util/utility/ipAddress.ts delete mode 100644 src/api/util/utility/passwordStrength.ts create mode 100644 src/util/entities/ValidRegistrationTokens.ts create mode 100644 src/util/migrations/mariadb/1663440589234-registration_tokens.ts create mode 100644 src/util/migrations/mariadb/1663448562034-drop_id_for_registration_tokens.ts create mode 100644 src/util/migrations/postgres/1663440587650-registration_tokens.ts create mode 100644 src/util/migrations/postgres/1663448561249-drop_id_for_registration_tokens.ts create mode 100644 src/util/migrations/sqlite/1663440585960-registration_tokens.ts create mode 100644 src/util/migrations/sqlite/1663448560501-drop_id_for_registration_tokens.ts create mode 100644 src/util/util/Base64.ts create mode 100644 src/util/util/CDN.ts create mode 100644 src/util/util/Captcha.ts create mode 100644 src/util/util/IPAddress.ts create mode 100644 src/util/util/PasswordStrength.ts create mode 100644 src/util/util/RandomInviteID.ts delete mode 100644 src/util/util/cdn.ts diff --git a/.gitignore b/.gitignore index a582a2f3..8d2feb42 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,5 @@ yarn.lock dbconf.json migrations.db + +package-lock.json diff --git a/package.json b/package.json index 878c2e2d..c9149afd 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "missing-native-js-functions": "^1.2.18", "morgan": "^1.10.0", "multer": "^1.4.5-lts.1", + "mysql2": "^2.3.3", "node-2fa": "^2.0.3", "node-fetch": "^2.6.7", "patch-package": "^6.4.7", @@ -90,6 +91,7 @@ "prettier": "^2.7.1", "proxy-agent": "^5.0.0", "reflect-metadata": "^0.1.13", + "sqlite3": "^5.1.1", "typeorm": "^0.3.7", "typescript": "^4.2.3", "ws": "^8.8.1" diff --git a/src/api/middlewares/Authentication.ts b/src/api/middlewares/Authentication.ts index fbf71cd5..00c2e5e6 100644 --- a/src/api/middlewares/Authentication.ts +++ b/src/api/middlewares/Authentication.ts @@ -53,7 +53,7 @@ export async function Authentication(req: Request, res: Response, next: NextFunc }) ) return next(); - if (!req.headers.authorization) return next(new HTTPError(req.t("auth:generic.MISSING_AUTH_HEADER"), 401)); + if (!req.headers.authorization) return next(new HTTPError("Missing authorization header!", 401)); try { const { jwtSecret } = Config.get().security; diff --git a/src/api/middlewares/RateLimit.ts b/src/api/middlewares/RateLimit.ts index dc93dcef..bb9a334c 100644 --- a/src/api/middlewares/RateLimit.ts +++ b/src/api/middlewares/RateLimit.ts @@ -1,4 +1,4 @@ -import { getIpAdress } from "@fosscord/api"; +import { getIpAdress } from "@fosscord/util"; import { Config, getRights, listenEvent } from "@fosscord/util"; import { NextFunction, Request, Response, Router } from "express"; import { API_PREFIX_TRAILING_SLASH } from "./Authentication"; diff --git a/src/api/routes/auth/generate-registration-tokens.ts b/src/api/routes/auth/generate-registration-tokens.ts new file mode 100644 index 00000000..322db33c --- /dev/null +++ b/src/api/routes/auth/generate-registration-tokens.ts @@ -0,0 +1,29 @@ +import { route } from "@fosscord/api"; +import { Config, random, Rights, ValidRegistrationToken } from "@fosscord/util"; +import { Request, Response, Router } from "express"; + + +const router: Router = Router(); +export default router; + +router.get("/", route({ right: "OPERATOR" }), async (req: Request, res: Response) => { + let count = (req.query.count as unknown) as number ?? 1; + let tokens: string[] = []; + let dbtokens: ValidRegistrationToken[] = []; + for(let i = 0; i < count; i++) { + let token = random((req.query.length as unknown as number) ?? 255); + let vrt = new ValidRegistrationToken(); + vrt.token = token; + dbtokens.push(vrt); + if(req.query.include_url == "true") token = `${Config.get().general.publicUrl}/register?token=${token}`; + tokens.push(token); + } + await ValidRegistrationToken.save(dbtokens, { chunk: 1000, reload: false, transaction: false }); + + if(req.query.plain == "true") { + if(count == 1) res.send(tokens[0]); + else res.send(tokens.join("\n")); + } + else if(count == 1) res.json({ token: tokens[0] }); + else res.json({ tokens }); +}); \ No newline at end of file diff --git a/src/api/routes/auth/location-metadata.ts b/src/api/routes/auth/location-metadata.ts index b8caf579..4bc7da28 100644 --- a/src/api/routes/auth/location-metadata.ts +++ b/src/api/routes/auth/location-metadata.ts @@ -1,4 +1,5 @@ -import { getIpAdress, IPAnalysis, route } from "@fosscord/api"; +import { route } from "@fosscord/api"; +import {getIpAdress, IPAnalysis} from "@fosscord/util"; import { Request, Response, Router } from "express"; const router = Router(); diff --git a/src/api/routes/auth/login.ts b/src/api/routes/auth/login.ts index 045b86eb..bbd9cf93 100644 --- a/src/api/routes/auth/login.ts +++ b/src/api/routes/auth/login.ts @@ -1,5 +1,5 @@ -import { getIpAdress, route, verifyCaptcha } from "@fosscord/api"; -import { adjustEmail, Config, FieldErrors, generateToken, LoginSchema, User } from "@fosscord/util"; +import { route } from "@fosscord/api"; +import { adjustEmail, Config, FieldErrors, generateToken, LoginSchema, User, getIpAdress, verifyCaptcha } from "@fosscord/util"; import crypto from "crypto"; import { Request, Response, Router } from "express"; diff --git a/src/api/routes/auth/register.ts b/src/api/routes/auth/register.ts index 638b6b79..08e9f7bb 100644 --- a/src/api/routes/auth/register.ts +++ b/src/api/routes/auth/register.ts @@ -1,7 +1,7 @@ -import { getIpAdress, IPAnalysis, isProxy, route, verifyCaptcha } from "@fosscord/api"; -import { adjustEmail, Config, FieldErrors, generateToken, HTTPError, Invite, RegisterSchema, User } from "@fosscord/util"; +import { route } from "@fosscord/api"; +import { adjustEmail, Config, FieldErrors, generateToken, HTTPError, Invite, RegisterSchema, User, ValidRegistrationToken, getIpAdress, IPAnalysis, isProxy, verifyCaptcha } from "@fosscord/util"; import { Request, Response, Router } from "express"; -import { yellow } from "picocolors"; +import { red, yellow } from "picocolors"; import { MoreThan } from "typeorm"; let bcrypt: any; @@ -22,13 +22,6 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re // email will be slightly modified version of the user supplied email -> e.g. protection against GMail Trick let email = adjustEmail(body.email); - // check if registration is allowed - if (!register.allowNewRegistration) { - throw FieldErrors({ - email: { code: "REGISTRATION_DISABLED", message: req.t("auth:register.REGISTRATION_DISABLED") } - }); - } - // check if the user agreed to the Terms of Service if (!body.consent) { throw FieldErrors({ @@ -36,21 +29,6 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re }); } - if (register.disabled) { - throw FieldErrors({ - email: { - code: "DISABLED", - message: "registration is disabled on this instance" - } - }); - } - - if (!register.allowGuests) { - throw FieldErrors({ - email: { code: "GUESTS_DISABLED", message: req.t("auth:register.GUESTS_DISABLED") } - }); - } - if (register.requireCaptcha && security.captcha.enabled) { const { sitekey, service } = security.captcha; if (!body.captcha_key) { @@ -71,24 +49,24 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re } } - if (!register.allowMultipleAccounts) { - // TODO: check if fingerprint was eligible generated - const exists = await User.findOne({ where: { fingerprints: body.fingerprint }, select: ["id"] }); - - if (exists) { - throw FieldErrors({ - email: { - code: "EMAIL_ALREADY_REGISTERED", - message: req.t("auth:register.EMAIL_ALREADY_REGISTERED") - } - }); - } + // check if registration is allowed + if (!register.allowNewRegistration) { + throw FieldErrors({ + email: { code: "REGISTRATION_DISABLED", message: req.t("auth:register.REGISTRATION_DISABLED") } + }); } if (register.blockProxies) { - if (isProxy(await IPAnalysis(ip))) { - console.log(`proxy ${ip} blocked from registration`); - throw new HTTPError("Your IP is blocked from registration"); + let data; + try { + data = await IPAnalysis(ip); + } catch (e: any) { + console.warn(red(`[REGISTER]: Failed to analyze IP ${ip}: failed to contact api.ipdata.co!`), e.message); + } + + if (data && isProxy(data)) { + console.log(yellow(`[REGISTER] Proxy ${ip} blocked from registration!`)); + throw new HTTPError(req.t("auth:register.IP_BLOCKED")); } } @@ -96,15 +74,10 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re // TODO: check password strength if (email) { - // replace all dots and chars after +, if its a gmail.com email - if (!email) { - throw FieldErrors({ email: { code: "INVALID_EMAIL", message: req?.t("auth:register.INVALID_EMAIL") } }); - } - // check if there is already an account with this email const exists = await User.findOne({ where: { email: email } }); - if (exists) { + if (exists && !register.disabled) { throw FieldErrors({ email: { code: "EMAIL_ALREADY_REGISTERED", @@ -155,14 +128,32 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re }); } + //check if email starts with any valid registration token + let validToken = false; + if (req.get("Referrer") && req.get("Referrer")?.includes("token=")) { + let token = req.get("Referrer")?.split("token=")[1].split("&")[0]; + if (token) { + let registrationToken = await ValidRegistrationToken.findOne({ where: { token: token } }); + if (registrationToken) { + console.log(yellow(`[REGISTER] Registration token ${token} used for registration!`)); + await ValidRegistrationToken.delete(token); + validToken = true; + } + else { + console.log(yellow(`[REGISTER] Invalid registration token ${token} used for registration by ${ip}!`)); + } + } + } + if ( + !validToken && limits.absoluteRate.register.enabled && (await await User.count({ where: { created_at: MoreThan(new Date(Date.now() - limits.absoluteRate.register.window)) } })) >= limits.absoluteRate.register.limit ) { console.log( yellow( - `Global register rate limit exceeded for ${getIpAdress(req)}: ${ + `[REGISTER] Global register rate limit exceeded for ${getIpAdress(req)}: ${ process.env.LOG_SENSITIVE ? req.body.email : "" }, ${req.body.username}, ${req.body.invite ?? "No invite given"}` ) diff --git a/src/api/routes/guilds/#guild_id/bans.ts b/src/api/routes/guilds/#guild_id/bans.ts index 4600b4cb..e4fe605b 100644 --- a/src/api/routes/guilds/#guild_id/bans.ts +++ b/src/api/routes/guilds/#guild_id/bans.ts @@ -1,4 +1,4 @@ -import { getIpAdress, route } from "@fosscord/api"; +import { route } from "@fosscord/api"; import { Ban, BanModeratorSchema, @@ -10,7 +10,8 @@ import { HTTPError, Member, OrmUtils, - User + User, + getIpAdress } from "@fosscord/util"; import { Request, Response, Router } from "express"; diff --git a/src/api/routes/guilds/#guild_id/regions.ts b/src/api/routes/guilds/#guild_id/regions.ts index aa57ec65..d32ff118 100644 --- a/src/api/routes/guilds/#guild_id/regions.ts +++ b/src/api/routes/guilds/#guild_id/regions.ts @@ -1,5 +1,5 @@ -import { getIpAdress, getVoiceRegions, route } from "@fosscord/api"; -import { Guild } from "@fosscord/util"; +import { getVoiceRegions, route } from "@fosscord/api"; +import { Guild, getIpAdress } from "@fosscord/util"; import { Request, Response, Router } from "express"; const router = Router(); diff --git a/src/api/routes/guilds/#guild_id/templates.ts b/src/api/routes/guilds/#guild_id/templates.ts index 448ee033..1f85cdcf 100644 --- a/src/api/routes/guilds/#guild_id/templates.ts +++ b/src/api/routes/guilds/#guild_id/templates.ts @@ -1,5 +1,5 @@ -import { generateCode, route } from "@fosscord/api"; -import { Guild, HTTPError, OrmUtils, Template } from "@fosscord/util"; +import { route } from "@fosscord/api"; +import { Guild, HTTPError, OrmUtils, Template, generateCode } from "@fosscord/util"; import { Request, Response, Router } from "express"; const router: Router = Router(); diff --git a/src/api/routes/guilds/#guild_id/widget.json.ts b/src/api/routes/guilds/#guild_id/widget.json.ts index 368fe46e..66cc456f 100644 --- a/src/api/routes/guilds/#guild_id/widget.json.ts +++ b/src/api/routes/guilds/#guild_id/widget.json.ts @@ -1,5 +1,5 @@ -import { random, route } from "@fosscord/api"; -import { Channel, Guild, HTTPError, Invite, Member, OrmUtils, Permissions } from "@fosscord/util"; +import { route } from "@fosscord/api"; +import { Channel, Guild, HTTPError, Invite, Member, OrmUtils, Permissions, random } from "@fosscord/util"; import { Request, Response, Router } from "express"; const router: Router = Router(); diff --git a/src/api/routes/voice/regions.ts b/src/api/routes/voice/regions.ts index eacdcf11..9071fcd5 100644 --- a/src/api/routes/voice/regions.ts +++ b/src/api/routes/voice/regions.ts @@ -1,5 +1,6 @@ -import { getIpAdress, getVoiceRegions, route } from "@fosscord/api"; +import { getVoiceRegions, route } from "@fosscord/api"; import { Request, Response, Router } from "express"; +import { getIpAdress } from "@fosscord/util"; const router: Router = Router(); diff --git a/src/api/util/handlers/Voice.ts b/src/api/util/handlers/Voice.ts index 4d60eb91..98d28ff0 100644 --- a/src/api/util/handlers/Voice.ts +++ b/src/api/util/handlers/Voice.ts @@ -1,5 +1,4 @@ -import { Config } from "@fosscord/util"; -import { distanceBetweenLocations, IPAnalysis } from "../utility/ipAddress"; +import { Config, distanceBetweenLocations, IPAnalysis } from "@fosscord/util"; export async function getVoiceRegions(ipAddress: string, vip: boolean) { const regions = Config.get().regions; diff --git a/src/api/util/index.ts b/src/api/util/index.ts index d06860cd..7223d6f4 100644 --- a/src/api/util/index.ts +++ b/src/api/util/index.ts @@ -1,10 +1,4 @@ export * from "./entities/AssetCacheItem"; export * from "./handlers/Message"; export * from "./handlers/route"; -export * from "./handlers/Voice"; -export * from "./utility/Base64"; -export * from "./utility/captcha"; -export * from "./utility/ipAddress"; -export * from "./utility/passwordStrength"; -export * from "./utility/RandomInviteID"; -export * from "./utility/String"; +export * from "./handlers/Voice"; \ No newline at end of file diff --git a/src/api/util/utility/Base64.ts b/src/api/util/utility/Base64.ts deleted file mode 100644 index 46cff77a..00000000 --- a/src/api/util/utility/Base64.ts +++ /dev/null @@ -1,47 +0,0 @@ -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 deleted file mode 100644 index feebfd3d..00000000 --- a/src/api/util/utility/RandomInviteID.ts +++ /dev/null @@ -1,31 +0,0 @@ -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 deleted file mode 100644 index a2e491e4..00000000 --- a/src/api/util/utility/String.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { FieldErrors } from "@fosscord/util"; -import { Request } from "express"; -import { ntob } from "./Base64"; - -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 deleted file mode 100644 index 02983f3f..00000000 --- a/src/api/util/utility/captcha.ts +++ /dev/null @@ -1,47 +0,0 @@ -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; -} diff --git a/src/api/util/utility/ipAddress.ts b/src/api/util/utility/ipAddress.ts deleted file mode 100644 index c96feb9e..00000000 --- a/src/api/util/utility/ipAddress.ts +++ /dev/null @@ -1,99 +0,0 @@ -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 { - const { ipdataApiKey } = Config.get().security; - if (!ipdataApiKey) return { ...exampleData, ip }; - - return (await fetch(`https://api.ipdata.co/${ip}?api-key=${ipdataApiKey}`)).json() as any; -} - -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 as string] || - req.headers[Config.get().security.forwadedFor?.toLowerCase() as string] || - 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 deleted file mode 100644 index ff83d3df..00000000 --- a/src/api/util/utility/passwordStrength.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Config } from "@fosscord/util"; - -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 chars - * - min numbers - * - min symbols - * - min 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; - let 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; -} diff --git a/src/util/config/types/GeneralConfiguration.ts b/src/util/config/types/GeneralConfiguration.ts index 5cb8df89..6d030645 100644 --- a/src/util/config/types/GeneralConfiguration.ts +++ b/src/util/config/types/GeneralConfiguration.ts @@ -3,6 +3,7 @@ import { Snowflake } from "../../util"; export class GeneralConfiguration { instanceName: string = "Fosscord Instance"; instanceDescription: string | null = "This is a Fosscord instance made in the pre-release days"; + publicUrl: string = "http://localhost:3001"; frontPage: string | null = null; tosPage: string | null = null; correspondenceEmail: string | null = "noreply@localhost.local"; diff --git a/src/util/config/types/RegisterConfiguration.ts b/src/util/config/types/RegisterConfiguration.ts index 68946272..caeab123 100644 --- a/src/util/config/types/RegisterConfiguration.ts +++ b/src/util/config/types/RegisterConfiguration.ts @@ -12,7 +12,6 @@ export class RegisterConfiguration { allowGuests: boolean = true; guestsRequireInvite: boolean = true; allowNewRegistration: boolean = true; - allowMultipleAccounts: boolean = true; blockProxies: boolean = true; incrementingDiscriminators: boolean = false; // random otherwise defaultRights: string = "0"; diff --git a/src/util/config/types/SecurityConfiguration.ts b/src/util/config/types/SecurityConfiguration.ts index 5a3d5aa6..229587c3 100644 --- a/src/util/config/types/SecurityConfiguration.ts +++ b/src/util/config/types/SecurityConfiguration.ts @@ -17,4 +17,5 @@ export class SecurityConfiguration { mfaBackupCodeCount: number = 10; mfaBackupCodeBytes: number = 4; statsWorldReadable: boolean = true; + defaultRegistrationTokenExpiration: number = 1000 * 60 * 60 * 24 * 7; //1 week } diff --git a/src/util/entities/Attachment.ts b/src/util/entities/Attachment.ts index 8392f415..c0ea3dec 100644 --- a/src/util/entities/Attachment.ts +++ b/src/util/entities/Attachment.ts @@ -1,6 +1,6 @@ import { BeforeRemove, Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; import { URL } from "url"; -import { deleteFile } from "../util/cdn"; +import { deleteFile } from "../util/CDN"; import { BaseClass } from "./BaseClass"; @Entity("attachments") diff --git a/src/util/entities/Invite.ts b/src/util/entities/Invite.ts index f6ba85d7..151fcc59 100644 --- a/src/util/entities/Invite.ts +++ b/src/util/entities/Invite.ts @@ -1,4 +1,4 @@ -import { random } from "@fosscord/api"; +import { random } from "@fosscord/util"; import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn, RelationId } from "typeorm"; import { BaseClassWithoutId } from "./BaseClass"; import { Channel } from "./Channel"; diff --git a/src/util/entities/ValidRegistrationTokens.ts b/src/util/entities/ValidRegistrationTokens.ts new file mode 100644 index 00000000..5d0747b8 --- /dev/null +++ b/src/util/entities/ValidRegistrationTokens.ts @@ -0,0 +1,12 @@ +import { BaseEntity, Column, Entity, PrimaryColumn } from "typeorm"; +import { Config } from ".."; + +@Entity("valid_registration_tokens") +export class ValidRegistrationToken extends BaseEntity { + @PrimaryColumn() + token: string; + @Column() + created_at: Date = new Date(); + @Column() + expires_at: Date = new Date(Date.now() + Config.get().security.defaultRegistrationTokenExpiration); +} diff --git a/src/util/entities/index.ts b/src/util/entities/index.ts index 2b91c2ba..673aac36 100644 --- a/src/util/entities/index.ts +++ b/src/util/entities/index.ts @@ -31,3 +31,4 @@ export * from "./User"; export * from "./UserSettings"; export * from "./VoiceState"; export * from "./Webhook"; +export * from "./ValidRegistrationTokens"; \ No newline at end of file diff --git a/src/util/migrations/mariadb/1663440589234-registration_tokens.ts b/src/util/migrations/mariadb/1663440589234-registration_tokens.ts new file mode 100644 index 00000000..12690ac4 --- /dev/null +++ b/src/util/migrations/mariadb/1663440589234-registration_tokens.ts @@ -0,0 +1,31 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class registrationTokens1663440589234 implements MigrationInterface { + name = 'registrationTokens1663440589234' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE \`valid_registration_tokens\` ( + \`id\` varchar(255) NOT NULL, + \`token\` varchar(255) NOT NULL, + \`created_at\` datetime NOT NULL, + \`expires_at\` datetime NOT NULL, + PRIMARY KEY (\`id\`) + ) ENGINE = InnoDB + `); + await queryRunner.query(` + ALTER TABLE \`users\` DROP COLUMN \`notes\` + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE \`users\` + ADD \`notes\` text NOT NULL + `); + await queryRunner.query(` + DROP TABLE \`valid_registration_tokens\` + `); + } + +} diff --git a/src/util/migrations/mariadb/1663448562034-drop_id_for_registration_tokens.ts b/src/util/migrations/mariadb/1663448562034-drop_id_for_registration_tokens.ts new file mode 100644 index 00000000..d4b13abb --- /dev/null +++ b/src/util/migrations/mariadb/1663448562034-drop_id_for_registration_tokens.ts @@ -0,0 +1,33 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class dropIdForRegistrationTokens1663448562034 implements MigrationInterface { + name = 'dropIdForRegistrationTokens1663448562034' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE \`valid_registration_tokens\` DROP PRIMARY KEY + `); + await queryRunner.query(` + ALTER TABLE \`valid_registration_tokens\` DROP COLUMN \`id\` + `); + await queryRunner.query(` + ALTER TABLE \`valid_registration_tokens\` + ADD PRIMARY KEY (\`token\`) + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE \`valid_registration_tokens\` DROP PRIMARY KEY + `); + await queryRunner.query(` + ALTER TABLE \`valid_registration_tokens\` + ADD \`id\` varchar(255) NOT NULL + `); + await queryRunner.query(` + ALTER TABLE \`valid_registration_tokens\` + ADD PRIMARY KEY (\`id\`) + `); + } + +} diff --git a/src/util/migrations/postgres/1663440587650-registration_tokens.ts b/src/util/migrations/postgres/1663440587650-registration_tokens.ts new file mode 100644 index 00000000..a794262c --- /dev/null +++ b/src/util/migrations/postgres/1663440587650-registration_tokens.ts @@ -0,0 +1,33 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class registrationTokens1663440587650 implements MigrationInterface { + name = 'registrationTokens1663440587650' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE "valid_registration_tokens" ( + "id" character varying NOT NULL, + "token" character varying NOT NULL, + "created_at" TIMESTAMP NOT NULL, + "expires_at" TIMESTAMP NOT NULL, + CONSTRAINT "PK_aac42a46cd46369450217de1c8a" PRIMARY KEY ("id") + ) + `); + await queryRunner.query(` + ALTER TABLE "members" + ALTER COLUMN "bio" DROP DEFAULT + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "members" + ALTER COLUMN "bio" + SET DEFAULT '' + `); + await queryRunner.query(` + DROP TABLE "valid_registration_tokens" + `); + } + +} diff --git a/src/util/migrations/postgres/1663448561249-drop_id_for_registration_tokens.ts b/src/util/migrations/postgres/1663448561249-drop_id_for_registration_tokens.ts new file mode 100644 index 00000000..ce4b72f4 --- /dev/null +++ b/src/util/migrations/postgres/1663448561249-drop_id_for_registration_tokens.ts @@ -0,0 +1,33 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class dropIdForRegistrationTokens1663448561249 implements MigrationInterface { + name = 'dropIdForRegistrationTokens1663448561249' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "valid_registration_tokens" DROP CONSTRAINT "PK_aac42a46cd46369450217de1c8a" + `); + await queryRunner.query(` + ALTER TABLE "valid_registration_tokens" DROP COLUMN "id" + `); + await queryRunner.query(` + ALTER TABLE "valid_registration_tokens" + ADD CONSTRAINT "PK_e0f5c8e3fcefe3134a092c50485" PRIMARY KEY ("token") + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "valid_registration_tokens" DROP CONSTRAINT "PK_e0f5c8e3fcefe3134a092c50485" + `); + await queryRunner.query(` + ALTER TABLE "valid_registration_tokens" + ADD "id" character varying NOT NULL + `); + await queryRunner.query(` + ALTER TABLE "valid_registration_tokens" + ADD CONSTRAINT "PK_aac42a46cd46369450217de1c8a" PRIMARY KEY ("id") + `); + } + +} diff --git a/src/util/migrations/sqlite/1663440585960-registration_tokens.ts b/src/util/migrations/sqlite/1663440585960-registration_tokens.ts new file mode 100644 index 00000000..daf76be6 --- /dev/null +++ b/src/util/migrations/sqlite/1663440585960-registration_tokens.ts @@ -0,0 +1,246 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class registrationTokens1663440585960 implements MigrationInterface { + name = 'registrationTokens1663440585960' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE "valid_registration_tokens" ( + "id" varchar PRIMARY KEY NOT NULL, + "token" varchar NOT NULL, + "created_at" datetime NOT NULL, + "expires_at" datetime NOT NULL + ) + `); + await queryRunner.query(` + CREATE TABLE "temporary_users" ( + "id" varchar PRIMARY KEY NOT NULL, + "username" varchar NOT NULL, + "discriminator" varchar NOT NULL, + "avatar" varchar, + "accent_color" integer, + "banner" varchar, + "phone" varchar, + "desktop" boolean NOT NULL, + "mobile" boolean NOT NULL, + "premium" boolean NOT NULL, + "premium_type" integer NOT NULL, + "bot" boolean NOT NULL, + "bio" varchar, + "system" boolean NOT NULL, + "nsfw_allowed" boolean NOT NULL, + "mfa_enabled" boolean, + "totp_secret" varchar, + "totp_last_ticket" varchar, + "created_at" datetime NOT NULL, + "premium_since" datetime, + "verified" boolean NOT NULL, + "disabled" boolean NOT NULL, + "deleted" boolean NOT NULL, + "email" varchar, + "flags" varchar NOT NULL, + "public_flags" integer NOT NULL, + "rights" bigint NOT NULL, + "data" text NOT NULL, + "fingerprints" text NOT NULL, + "extended_settings" text NOT NULL, + "settingsId" varchar, + CONSTRAINT "UQ_b1dd13b6ed980004a795ca184a6" UNIQUE ("settingsId"), + CONSTRAINT "FK_76ba283779c8441fd5ff819c8cf" FOREIGN KEY ("settingsId") REFERENCES "user_settings" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION + ) + `); + await queryRunner.query(` + INSERT INTO "temporary_users"( + "id", + "username", + "discriminator", + "avatar", + "accent_color", + "banner", + "phone", + "desktop", + "mobile", + "premium", + "premium_type", + "bot", + "bio", + "system", + "nsfw_allowed", + "mfa_enabled", + "totp_secret", + "totp_last_ticket", + "created_at", + "premium_since", + "verified", + "disabled", + "deleted", + "email", + "flags", + "public_flags", + "rights", + "data", + "fingerprints", + "extended_settings", + "settingsId" + ) + SELECT "id", + "username", + "discriminator", + "avatar", + "accent_color", + "banner", + "phone", + "desktop", + "mobile", + "premium", + "premium_type", + "bot", + "bio", + "system", + "nsfw_allowed", + "mfa_enabled", + "totp_secret", + "totp_last_ticket", + "created_at", + "premium_since", + "verified", + "disabled", + "deleted", + "email", + "flags", + "public_flags", + "rights", + "data", + "fingerprints", + "extended_settings", + "settingsId" + FROM "users" + `); + await queryRunner.query(` + DROP TABLE "users" + `); + await queryRunner.query(` + ALTER TABLE "temporary_users" + RENAME TO "users" + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "users" + RENAME TO "temporary_users" + `); + await queryRunner.query(` + CREATE TABLE "users" ( + "id" varchar PRIMARY KEY NOT NULL, + "username" varchar NOT NULL, + "discriminator" varchar NOT NULL, + "avatar" varchar, + "accent_color" integer, + "banner" varchar, + "phone" varchar, + "desktop" boolean NOT NULL, + "mobile" boolean NOT NULL, + "premium" boolean NOT NULL, + "premium_type" integer NOT NULL, + "bot" boolean NOT NULL, + "bio" varchar, + "system" boolean NOT NULL, + "nsfw_allowed" boolean NOT NULL, + "mfa_enabled" boolean, + "totp_secret" varchar, + "totp_last_ticket" varchar, + "created_at" datetime NOT NULL, + "premium_since" datetime, + "verified" boolean NOT NULL, + "disabled" boolean NOT NULL, + "deleted" boolean NOT NULL, + "email" varchar, + "flags" varchar NOT NULL, + "public_flags" integer NOT NULL, + "rights" bigint NOT NULL, + "data" text NOT NULL, + "fingerprints" text NOT NULL, + "extended_settings" text NOT NULL, + "notes" text NOT NULL, + "settingsId" varchar, + CONSTRAINT "UQ_b1dd13b6ed980004a795ca184a6" UNIQUE ("settingsId"), + CONSTRAINT "FK_76ba283779c8441fd5ff819c8cf" FOREIGN KEY ("settingsId") REFERENCES "user_settings" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION + ) + `); + await queryRunner.query(` + INSERT INTO "users"( + "id", + "username", + "discriminator", + "avatar", + "accent_color", + "banner", + "phone", + "desktop", + "mobile", + "premium", + "premium_type", + "bot", + "bio", + "system", + "nsfw_allowed", + "mfa_enabled", + "totp_secret", + "totp_last_ticket", + "created_at", + "premium_since", + "verified", + "disabled", + "deleted", + "email", + "flags", + "public_flags", + "rights", + "data", + "fingerprints", + "extended_settings", + "settingsId" + ) + SELECT "id", + "username", + "discriminator", + "avatar", + "accent_color", + "banner", + "phone", + "desktop", + "mobile", + "premium", + "premium_type", + "bot", + "bio", + "system", + "nsfw_allowed", + "mfa_enabled", + "totp_secret", + "totp_last_ticket", + "created_at", + "premium_since", + "verified", + "disabled", + "deleted", + "email", + "flags", + "public_flags", + "rights", + "data", + "fingerprints", + "extended_settings", + "settingsId" + FROM "temporary_users" + `); + await queryRunner.query(` + DROP TABLE "temporary_users" + `); + await queryRunner.query(` + DROP TABLE "valid_registration_tokens" + `); + } + +} diff --git a/src/util/migrations/sqlite/1663448560501-drop_id_for_registration_tokens.ts b/src/util/migrations/sqlite/1663448560501-drop_id_for_registration_tokens.ts new file mode 100644 index 00000000..087cc81f --- /dev/null +++ b/src/util/migrations/sqlite/1663448560501-drop_id_for_registration_tokens.ts @@ -0,0 +1,97 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class dropIdForRegistrationTokens1663448560501 implements MigrationInterface { + name = 'dropIdForRegistrationTokens1663448560501' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE "temporary_valid_registration_tokens" ( + "token" varchar NOT NULL, + "created_at" datetime NOT NULL, + "expires_at" datetime NOT NULL + ) + `); + await queryRunner.query(` + INSERT INTO "temporary_valid_registration_tokens"("token", "created_at", "expires_at") + SELECT "token", + "created_at", + "expires_at" + FROM "valid_registration_tokens" + `); + await queryRunner.query(` + DROP TABLE "valid_registration_tokens" + `); + await queryRunner.query(` + ALTER TABLE "temporary_valid_registration_tokens" + RENAME TO "valid_registration_tokens" + `); + await queryRunner.query(` + CREATE TABLE "temporary_valid_registration_tokens" ( + "token" varchar PRIMARY KEY NOT NULL, + "created_at" datetime NOT NULL, + "expires_at" datetime NOT NULL + ) + `); + await queryRunner.query(` + INSERT INTO "temporary_valid_registration_tokens"("token", "created_at", "expires_at") + SELECT "token", + "created_at", + "expires_at" + FROM "valid_registration_tokens" + `); + await queryRunner.query(` + DROP TABLE "valid_registration_tokens" + `); + await queryRunner.query(` + ALTER TABLE "temporary_valid_registration_tokens" + RENAME TO "valid_registration_tokens" + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "valid_registration_tokens" + RENAME TO "temporary_valid_registration_tokens" + `); + await queryRunner.query(` + CREATE TABLE "valid_registration_tokens" ( + "token" varchar NOT NULL, + "created_at" datetime NOT NULL, + "expires_at" datetime NOT NULL + ) + `); + await queryRunner.query(` + INSERT INTO "valid_registration_tokens"("token", "created_at", "expires_at") + SELECT "token", + "created_at", + "expires_at" + FROM "temporary_valid_registration_tokens" + `); + await queryRunner.query(` + DROP TABLE "temporary_valid_registration_tokens" + `); + await queryRunner.query(` + ALTER TABLE "valid_registration_tokens" + RENAME TO "temporary_valid_registration_tokens" + `); + await queryRunner.query(` + CREATE TABLE "valid_registration_tokens" ( + "id" varchar PRIMARY KEY NOT NULL, + "token" varchar NOT NULL, + "created_at" datetime NOT NULL, + "expires_at" datetime NOT NULL + ) + `); + await queryRunner.query(` + INSERT INTO "valid_registration_tokens"("token", "created_at", "expires_at") + SELECT "token", + "created_at", + "expires_at" + FROM "temporary_valid_registration_tokens" + `); + await queryRunner.query(` + DROP TABLE "temporary_valid_registration_tokens" + `); + } + +} diff --git a/src/util/util/Base64.ts b/src/util/util/Base64.ts new file mode 100644 index 00000000..46cff77a --- /dev/null +++ b/src/util/util/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/util/util/CDN.ts b/src/util/util/CDN.ts new file mode 100644 index 00000000..5573b848 --- /dev/null +++ b/src/util/util/CDN.ts @@ -0,0 +1,55 @@ +import FormData from "form-data"; +import fetch from "node-fetch"; +import { HTTPError } from ".."; +import { Config } from "./Config"; + +export async function uploadFile(path: string, file?: Express.Multer.File) { + if (!file?.buffer) throw new HTTPError("Missing file in body"); + + const form = new FormData(); + form.append("file", file.buffer, { + contentType: file.mimetype, + filename: file.originalname + }); + + const response = await fetch(`${Config.get().cdn.endpointPrivate || "http://localhost:3003"}${path}`, { + headers: { + signature: Config.get().security.requestSignature, + ...form.getHeaders() + }, + method: "POST", + body: form + }); + const result = await response.json(); + + if (response.status !== 200) throw result; + return result; +} + +export async function handleFile(path: string, body?: string): Promise { + if (!body || !body.startsWith("data:")) return undefined; + try { + const mimetype = body.split(":")[1].split(";")[0]; + const buffer = Buffer.from(body.split(",")[1], "base64"); + + // @ts-ignore + const { id } = await uploadFile(path, { buffer, mimetype, originalname: "banner" }); + return id; + } catch (error) { + console.error(error); + throw new HTTPError("Invalid " + path); + } +} + +export async function deleteFile(path: string) { + const response = await fetch(`${Config.get().cdn.endpointPrivate || "http://localhost:3003"}${path}`, { + headers: { + signature: Config.get().security.requestSignature + }, + method: "DELETE" + }); + const result = await response.json(); + + if (response.status !== 200) throw result; + return result; +} diff --git a/src/util/util/Captcha.ts b/src/util/util/Captcha.ts new file mode 100644 index 00000000..02983f3f --- /dev/null +++ b/src/util/util/Captcha.ts @@ -0,0 +1,47 @@ +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; +} diff --git a/src/util/util/IPAddress.ts b/src/util/util/IPAddress.ts new file mode 100644 index 00000000..c96feb9e --- /dev/null +++ b/src/util/util/IPAddress.ts @@ -0,0 +1,99 @@ +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 { + const { ipdataApiKey } = Config.get().security; + if (!ipdataApiKey) return { ...exampleData, ip }; + + return (await fetch(`https://api.ipdata.co/${ip}?api-key=${ipdataApiKey}`)).json() as any; +} + +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 as string] || + req.headers[Config.get().security.forwadedFor?.toLowerCase() as string] || + 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/util/util/PasswordStrength.ts b/src/util/util/PasswordStrength.ts new file mode 100644 index 00000000..ff83d3df --- /dev/null +++ b/src/util/util/PasswordStrength.ts @@ -0,0 +1,59 @@ +import { Config } from "@fosscord/util"; + +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 chars + * - min numbers + * - min symbols + * - min 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; + let 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; +} diff --git a/src/util/util/RandomInviteID.ts b/src/util/util/RandomInviteID.ts new file mode 100644 index 00000000..49302916 --- /dev/null +++ b/src/util/util/RandomInviteID.ts @@ -0,0 +1,31 @@ +import { Snowflake } from "@fosscord/util"; +import crypto from "crypto"; + +export function random(length = 6, chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") { + // Declare all characters + + // Pick characers randomly + let str = ""; + for (let i = 0; i < length; i++) { + str += chars.charAt(Math.floor(crypto.randomInt(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/util/util/String.ts b/src/util/util/String.ts index 55f11e8d..cd5cb4f2 100644 --- a/src/util/util/String.ts +++ b/src/util/util/String.ts @@ -1,4 +1,22 @@ import { SPECIAL_CHAR } from "./Regex"; +import { FieldErrors } from "@fosscord/util"; +import { Request } from "express"; +import { ntob } from "./Base64"; + +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)); +} export function trimSpecial(str?: string): string { // @ts-ignore diff --git a/src/util/util/cdn.ts b/src/util/util/cdn.ts deleted file mode 100644 index 5573b848..00000000 --- a/src/util/util/cdn.ts +++ /dev/null @@ -1,55 +0,0 @@ -import FormData from "form-data"; -import fetch from "node-fetch"; -import { HTTPError } from ".."; -import { Config } from "./Config"; - -export async function uploadFile(path: string, file?: Express.Multer.File) { - if (!file?.buffer) throw new HTTPError("Missing file in body"); - - const form = new FormData(); - form.append("file", file.buffer, { - contentType: file.mimetype, - filename: file.originalname - }); - - const response = await fetch(`${Config.get().cdn.endpointPrivate || "http://localhost:3003"}${path}`, { - headers: { - signature: Config.get().security.requestSignature, - ...form.getHeaders() - }, - method: "POST", - body: form - }); - const result = await response.json(); - - if (response.status !== 200) throw result; - return result; -} - -export async function handleFile(path: string, body?: string): Promise { - if (!body || !body.startsWith("data:")) return undefined; - try { - const mimetype = body.split(":")[1].split(";")[0]; - const buffer = Buffer.from(body.split(",")[1], "base64"); - - // @ts-ignore - const { id } = await uploadFile(path, { buffer, mimetype, originalname: "banner" }); - return id; - } catch (error) { - console.error(error); - throw new HTTPError("Invalid " + path); - } -} - -export async function deleteFile(path: string) { - const response = await fetch(`${Config.get().cdn.endpointPrivate || "http://localhost:3003"}${path}`, { - headers: { - signature: Config.get().security.requestSignature - }, - method: "DELETE" - }); - const result = await response.json(); - - if (response.status !== 200) throw result; - return result; -} diff --git a/src/util/util/index.ts b/src/util/util/index.ts index 11f0b72a..1ef7467c 100644 --- a/src/util/util/index.ts +++ b/src/util/util/index.ts @@ -2,7 +2,7 @@ export * from "./ApiError"; export * from "./Array"; export * from "./BitField"; //export * from "./Categories"; -export * from "./cdn"; +export * from "./CDN"; export * from "./Config"; export * from "./Constants"; export * from "./Database"; @@ -23,3 +23,6 @@ export * from "./Snowflake"; export * from "./String"; export * from "./Token"; export * from "./TraverseDirectory"; +export * from "./IPAddress"; +export * from "./RandomInviteID"; +export * from "./Captcha"; \ No newline at end of file -- cgit 1.4.1