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 : "<email redacted>"
}, ${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<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;
-}
-
-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 <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;
- 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;
-}
|