summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Server.ts4
-rw-r--r--src/middlewares/Authentication.ts6
-rw-r--r--src/middlewares/GlobalRateLimit.ts5
-rw-r--r--src/routes/auth/login.ts8
-rw-r--r--src/routes/auth/register.ts14
-rw-r--r--src/routes/channels/#channel_id/messages/bulk-delete.ts4
-rw-r--r--src/routes/channels/#channel_id/pins.ts4
-rw-r--r--src/routes/gateway.ts7
-rw-r--r--src/routes/guilds/index.ts4
-rw-r--r--src/routes/guilds/templates/index.ts4
-rw-r--r--src/util/Config.ts354
-rw-r--r--src/util/Member.ts4
-rw-r--r--src/util/passwordStrength.ts4
13 files changed, 301 insertions, 121 deletions
diff --git a/src/Server.ts b/src/Server.ts

index e6d3d9c9..ca1d1c1c 100644 --- a/src/Server.ts +++ b/src/Server.ts
@@ -3,7 +3,7 @@ import fs from "fs/promises"; import { Connection } from "mongoose"; import { Server, ServerOptions } from "lambert-server"; import { Authentication, CORS, GlobalRateLimit } from "./middlewares/"; -import Config from "./util/Config"; +import * as Config from "./util/Config"; import { db } from "@fosscord/server-util"; import i18next from "i18next"; import i18nextMiddleware, { I18next } from "i18next-http-middleware"; @@ -55,7 +55,7 @@ export class FosscordServer extends Server { await (db as Promise<Connection>); await this.setupSchema(); console.log("[DB] connected"); - await Promise.all([Config.init()]); + //await Promise.all([Config.init()]); this.app.use(GlobalRateLimit); this.app.use(Authentication); diff --git a/src/middlewares/Authentication.ts b/src/middlewares/Authentication.ts
index 0ecc1bc0..050c427f 100644 --- a/src/middlewares/Authentication.ts +++ b/src/middlewares/Authentication.ts
@@ -1,6 +1,7 @@ import { NextFunction, Request, Response } from "express"; import { HTTPError } from "lambert-server"; import { checkToken } from "@fosscord/server-util"; +import * as Config from "../util/Config" export const NO_AUTHORIZATION_ROUTES = [ "/api/v8/auth/login", @@ -27,7 +28,10 @@ export async function Authentication(req: Request, res: Response, next: NextFunc // TODO: check if user is banned/token expired try { - const decoded: any = await checkToken(req.headers.authorization); + + const { jwtSecret } = Config.apiConfig.getAll().security; + + const decoded: any = await checkToken(req.headers.authorization, jwtSecret); req.token = decoded; req.user_id = decoded.id; diff --git a/src/middlewares/GlobalRateLimit.ts b/src/middlewares/GlobalRateLimit.ts
index fc121911..38098981 100644 --- a/src/middlewares/GlobalRateLimit.ts +++ b/src/middlewares/GlobalRateLimit.ts
@@ -1,5 +1,6 @@ import { NextFunction, Request, Response } from "express"; -import Config from "../util/Config"; +import * as Config from '../util/Config' +import crypto from "crypto"; // TODO: use mongodb ttl index // TODO: increment count on serverside @@ -43,7 +44,7 @@ export async function GlobalRateLimit(req: Request, res: Response, next: NextFun } export function getIpAdress(req: Request): string { - const { forwadedFor } = Config.get().security; + const { forwadedFor } = Config.apiConfig.getAll().security; const ip = forwadedFor ? <string>req.headers[forwadedFor] : req.ip; return ip.replaceAll(".", "_").replaceAll(":", "_"); } diff --git a/src/routes/auth/login.ts b/src/routes/auth/login.ts
index a0fc1190..1938b794 100644 --- a/src/routes/auth/login.ts +++ b/src/routes/auth/login.ts
@@ -3,7 +3,7 @@ import { check, FieldErrors, Length } from "../../util/instanceOf"; import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; import { UserModel } from "@fosscord/server-util"; -import Config from "../../util/Config"; +import * as Config from "../../util/Config"; import { adjustEmail } from "./register"; const router: Router = Router(); @@ -25,7 +25,9 @@ router.post( const query: any[] = [{ phone: login }]; if (email) query.push({ email }); - const config = Config.get(); + // TODO: Rewrite this to have the proper config syntax on the new method + + const config = Config.apiConfig.getAll(); if (config.login.requireCaptcha && config.security.captcha.enabled) { if (!captcha_key) { @@ -69,7 +71,7 @@ export async function generateToken(id: string) { return new Promise((res, rej) => { jwt.sign( { id: id, iat }, - Config.get().security.jwtSecret, + Config.apiConfig.getAll().security.jwtSecret, { algorithm, }, diff --git a/src/routes/auth/register.ts b/src/routes/auth/register.ts
index 265516d7..98fa768c 100644 --- a/src/routes/auth/register.ts +++ b/src/routes/auth/register.ts
@@ -1,5 +1,5 @@ import { Request, Response, Router } from "express"; -import Config from "../../util/Config"; +import * as Config from "../../util/Config"; import { trimSpecial, User, Snowflake, UserModel } from "@fosscord/server-util"; import bcrypt from "bcrypt"; import { check, Email, EMAIL_REGEX, FieldErrors, Length } from "../../util/instanceOf"; @@ -40,7 +40,7 @@ router.post( // TODO: check password strength // adjusted_email will be slightly modified version of the user supplied email -> e.g. protection against GMail Trick - let adjusted_email: string | undefined = adjustEmail(email); + let adjusted_email: string | null = adjustEmail(email); // adjusted_password will be the hash of the password let adjusted_password: string = ""; @@ -52,7 +52,7 @@ router.post( let discriminator = ""; // get register Config - const { register, security } = Config.get(); + const { register, security } = Config.apiConfig.getAll(); // check if registration is allowed if (!register.allowNewRegistration) { @@ -90,13 +90,13 @@ router.post( }, }); } - } else if (register.email.required) { + } else if (register.email.necessary) { throw FieldErrors({ email: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") }, }); } - if (register.dateOfBirth.required && !date_of_birth) { + if (register.dateOfBirth.necessary && !date_of_birth) { throw FieldErrors({ date_of_birth: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") }, }); @@ -181,7 +181,7 @@ router.post( mobile: false, premium: false, premium_type: 0, - phone: undefined, + phone: null, mfa_enabled: false, verified: false, presence: { @@ -253,7 +253,7 @@ router.post( } ); -export function adjustEmail(email: string): string | undefined { +export function adjustEmail(email: string): string | null { // body parser already checked if it is a valid email const parts = <RegExpMatchArray>email.match(EMAIL_REGEX); // @ts-ignore diff --git a/src/routes/channels/#channel_id/messages/bulk-delete.ts b/src/routes/channels/#channel_id/messages/bulk-delete.ts
index ac032c0e..8a11475e 100644 --- a/src/routes/channels/#channel_id/messages/bulk-delete.ts +++ b/src/routes/channels/#channel_id/messages/bulk-delete.ts
@@ -1,7 +1,7 @@ import { Router } from "express"; import { ChannelModel, getPermission, MessageDeleteBulkEvent, MessageModel } from "@fosscord/server-util"; import { HTTPError } from "lambert-server"; -import Config from "../../../../util/Config"; +import * as Config from "../../../../util/Config"; import { emitEvent } from "../../../../util/Event"; import { check } from "../../../../util/instanceOf"; @@ -20,7 +20,7 @@ router.post("/", check({ messages: [String] }), async (req, res) => { const permission = await getPermission(req.user_id, channel?.guild_id, channel_id, { channel }); permission.hasThrow("MANAGE_MESSAGES"); - const { maxBulkDelete } = Config.get().limits.message; + const { maxBulkDelete } = Config.apiConfig.getAll().limits.message; const { messages } = req.body as { messages: string[] }; if (messages.length < 2) throw new HTTPError("You must at least specify 2 messages to bulk delete"); diff --git a/src/routes/channels/#channel_id/pins.ts b/src/routes/channels/#channel_id/pins.ts
index 9d36b5c1..ccb909b8 100644 --- a/src/routes/channels/#channel_id/pins.ts +++ b/src/routes/channels/#channel_id/pins.ts
@@ -1,6 +1,6 @@ import { ChannelModel, ChannelPinsUpdateEvent, getPermission, MessageModel, MessageUpdateEvent, toObject } from "@fosscord/server-util"; import { Router, Request, Response } from "express"; -import Config from "../../../util/Config"; +import * as Config from "../../../util/Config"; import { HTTPError } from "lambert-server"; import { emitEvent } from "../../../util/Event"; @@ -18,7 +18,7 @@ router.put("/:message_id", async (req: Request, res: Response) => { if (channel.guild_id) permission.hasThrow("MANAGE_MESSAGES"); const pinned_count = await MessageModel.count({ channel_id, pinned: true }).exec(); - const { maxPins } = Config.get().limits.channel; + const { maxPins } = Config.apiConfig.getAll().limits.channel; if (pinned_count >= maxPins) throw new HTTPError("Max pin count reached: " + maxPins); await MessageModel.updateOne({ id: message_id }, { pinned: true }).exec(); diff --git a/src/routes/gateway.ts b/src/routes/gateway.ts
index 5b6a87e7..04ab1248 100644 --- a/src/routes/gateway.ts +++ b/src/routes/gateway.ts
@@ -1,12 +1,11 @@ import { Router } from "express"; -import Config from "../util/Config"; +import * as Config from "../util/Config" const router = Router(); router.get("/", (req, res) => { - const endpoint = Config.getAll()?.gateway?.endpoint; - - res.send({ url: endpoint || "ws://localhost:3002" }); + const { gateway } = Config.apiConfig.getAll(); + res.send({ url: gateway || "ws://localhost:3002" }); }); export default router; diff --git a/src/routes/guilds/index.ts b/src/routes/guilds/index.ts
index c286ad51..8860bcdf 100644 --- a/src/routes/guilds/index.ts +++ b/src/routes/guilds/index.ts
@@ -3,7 +3,7 @@ import { RoleModel, GuildModel, Snowflake, Guild, RoleDocument } from "@fosscord import { HTTPError } from "lambert-server"; import { check } from "./../../util/instanceOf"; import { GuildCreateSchema } from "../../schema/Guild"; -import Config from "../../util/Config"; +import * as Config from "../../util/Config"; import { getPublicUser } from "../../util/User"; import { addMember } from "../../util/Member"; import { createChannel } from "../../util/Channel"; @@ -15,7 +15,7 @@ const router: Router = Router(); router.post("/", check(GuildCreateSchema), async (req: Request, res: Response) => { const body = req.body as GuildCreateSchema; - const { maxGuilds } = Config.get().limits.user; + const { maxGuilds } = Config.apiConfig.getAll().limits.user; const user = await getPublicUser(req.user_id, { guilds: true }); if (user.guilds.length >= maxGuilds) { diff --git a/src/routes/guilds/templates/index.ts b/src/routes/guilds/templates/index.ts
index 7e32e94c..a7af8295 100644 --- a/src/routes/guilds/templates/index.ts +++ b/src/routes/guilds/templates/index.ts
@@ -5,7 +5,7 @@ import { HTTPError } from "lambert-server"; import { GuildTemplateCreateSchema } from "../../../schema/Guild"; import { getPublicUser } from "../../../util/User"; import { check } from "../../../util/instanceOf"; -import Config from "../../../util/Config"; +import * as Config from "../../../util/Config"; import { addMember } from "../../../util/Member"; router.get("/:code", async (req: Request, res: Response) => { @@ -21,7 +21,7 @@ router.post("/:code", check(GuildTemplateCreateSchema), async (req: Request, res const { code } = req.params; const body = req.body as GuildTemplateCreateSchema; - const { maxGuilds } = Config.get().limits.user; + const { maxGuilds } = Config.apiConfig.getAll().limits.user; const user = await getPublicUser(req.user_id, { guilds: true }); if (user.guilds.length >= maxGuilds) { diff --git a/src/util/Config.ts b/src/util/Config.ts
index f1f0f458..89f35901 100644 --- a/src/util/Config.ts +++ b/src/util/Config.ts
@@ -1,19 +1,6 @@ -import { Config, Snowflake } from "@fosscord/server-util"; -import crypto from "crypto"; - -export default { - init() { - return Config.init({ api: DefaultOptions }); - }, - get(): DefaultOptions { - return Config.getAll().api; - }, - set(val: any) { - return Config.setAll({ api: val }); - }, - getAll: Config.getAll, - setAll: Config.setAll, -}; +import Ajv, { JSONSchemaType } from "ajv" +import { getConfigPathForFile } from "@fosscord/server-util/dist/util/Config"; +import {Config} from "@fosscord/server-util" export interface RateLimitOptions { count: number; @@ -21,6 +8,7 @@ export interface RateLimitOptions { } export interface DefaultOptions { + gateway: string; general: { instance_id: string; }; @@ -64,7 +52,7 @@ export interface DefaultOptions { login?: RateLimitOptions; register?: RateLimitOptions; }; - channel?: {}; + channel?: string; // TODO: rate limit configuration for all routes }; }; @@ -84,13 +72,13 @@ export interface DefaultOptions { }; register: { email: { - required: boolean; + necessary: boolean; allowlist: boolean; blocklist: boolean; domains: string[]; }; dateOfBirth: { - required: boolean; + necessary: boolean; minimum: number; // in years }; requireCaptcha: boolean; @@ -107,85 +95,271 @@ export interface DefaultOptions { }; } -export const DefaultOptions: DefaultOptions = { - general: { - instance_id: Snowflake.generate(), - }, - permissions: { - user: { - createGuilds: true, + +const schema: JSONSchemaType<DefaultOptions> & { + definitions: { + rateLimitOptions: JSONSchemaType<RateLimitOptions> + } +} = { + type: "object", + definitions: { + rateLimitOptions: { + type: "object", + properties: { + count: { type: "number" }, + timespan: { type: "number" }, + }, + required: ["count", "timespan"], }, }, - limits: { - user: { - maxGuilds: 100, - maxUsername: 32, - maxFriends: 1000, - }, - guild: { - maxRoles: 250, - maxMembers: 250000, - maxChannels: 500, - maxChannelsInCategory: 50, - hideOfflineMember: 1000, - }, - message: { - characters: 2000, - ttsCharacters: 200, - maxReactions: 20, - maxAttachmentSize: 8388608, - maxBulkDelete: 100, + properties: { + gateway: { + type: "string" }, - channel: { - maxPins: 50, - maxTopic: 1024, + general: { + type: "object", + properties: { + instance_id: { + type: "string" + } + }, + required: ["instance_id"], + additionalProperties: false }, - rate: { - ip: { - enabled: true, - count: 1000, - timespan: 1000 * 60 * 10, + permissions: { + type: "object", + properties: { + user: { + type: "object", + properties: { + createGuilds: { + type: "boolean" + } + }, + required: ["createGuilds"], + additionalProperties: false + } }, - routes: {}, + required: ["user"], + additionalProperties: false }, - }, - security: { - jwtSecret: crypto.randomBytes(256).toString("base64"), - forwadedFor: null, - // forwadedFor: "X-Forwarded-For" // nginx/reverse proxy - // forwadedFor: "CF-Connecting-IP" // cloudflare: - captcha: { - enabled: false, - service: null, - sitekey: null, - secret: null, + limits: { + type: "object", + properties: { + user: { + type: "object", + properties: { + maxFriends: { + type: "number" + }, + maxGuilds: { + type: "number" + }, + maxUsername: { + type: "number" + } + }, + required: ["maxFriends", "maxGuilds", "maxUsername"], + additionalProperties: false + }, + guild: { + type: "object", + properties: { + maxRoles: { + type: "number" + }, + maxMembers: { + type: "number" + }, + maxChannels: { + type: "number" + }, + maxChannelsInCategory: { + type: "number" + }, + hideOfflineMember: { + type: "number" + } + }, + required: ["maxRoles", "maxMembers", "maxChannels", "maxChannelsInCategory", "hideOfflineMember"], + additionalProperties: false + }, + message: { + type: "object", + properties: { + characters: { + type: "number" + }, + ttsCharacters: { + type: "number" + }, + maxReactions: { + type: "number" + }, + maxAttachmentSize: { + type: "number" + }, + maxBulkDelete: { + type: "number" + } + }, + required: ["characters", "ttsCharacters", "maxReactions", "maxAttachmentSize", "maxBulkDelete"], + additionalProperties: false + }, + channel: { + type: "object", + properties: { + maxPins: { + type: "number" + }, + maxTopic: { + type: "number" + } + }, + required: ["maxPins", "maxTopic"], + additionalProperties: false + }, + rate: { + type: "object", + properties: { + ip: { + type: "object", + properties: { + enabled: { type: "boolean" }, + count: { type: "number" }, + timespan: { type: "number" } + }, + required: ["enabled", "count", "timespan"], + additionalProperties: false + }, + routes: { + type: "object", + properties: { + auth: { + type: "object", + properties: { + login: { $ref: '#/definitions/rateLimitOptions' }, + register: { $ref: '#/definitions/rateLimitOptions' } + }, + nullable: true, + required: [], + additionalProperties: false + }, + channel: { + type: "string", + nullable: true + } + }, + required: [], + additionalProperties: false + } + }, + required: ["ip", "routes"] + } + }, + required: ["channel", "guild", "message", "rate", "user"], + additionalProperties: false }, - }, - login: { - requireCaptcha: false, - }, - register: { - email: { - required: true, - allowlist: false, - blocklist: true, - domains: [], // TODO: efficiently save domain blocklist in database - // domains: fs.readFileSync(__dirname + "/blockedEmailDomains.txt", { encoding: "utf8" }).split("\n"), + security: { + type: "object", + properties: { + jwtSecret: { + type: "string" + }, + forwadedFor: { + type: "string", + nullable: true + }, + captcha: { + type: "object", + properties: { + enabled: { type: "boolean" }, + service: { + type: "string", + enum: ["hcaptcha", "recaptcha", null], + nullable: true + }, + sitekey: { + type: "string", + nullable: true + }, + secret: { + type: "string", + nullable: true + } + }, + required: ["enabled", "secret", "service", "sitekey"], + additionalProperties: false + } + }, + required: ["captcha", "forwadedFor", "jwtSecret"], + additionalProperties: false }, - dateOfBirth: { - required: true, - minimum: 13, + login: { + type: "object", + properties: { + requireCaptcha: { type: "boolean" } + }, + required: ["requireCaptcha"], + additionalProperties: false }, - requireInvite: false, - requireCaptcha: true, - allowNewRegistration: true, - allowMultipleAccounts: true, - password: { - minLength: 8, - minNumbers: 2, - minUpperCase: 2, - minSymbols: 0, - blockInsecureCommonPasswords: false, + register: { + type: "object", + properties: { + email: { + type: "object", + properties: { + necessary: { type: "boolean" }, + allowlist: { type: "boolean" }, + blocklist: { type: "boolean" }, + domains: { + type: "array", + items: { + type: "string" + } + } + }, + required: ["allowlist", "blocklist", "domains", "necessary"], + additionalProperties: false + }, + dateOfBirth: { + type: "object", + properties: { + necessary: { type: "boolean" }, + minimum: { type: "number" } + }, + required: ["minimum", "necessary"], + additionalProperties: false + }, + requireCaptcha: { type: "boolean" }, + requireInvite: { type: "boolean" }, + allowNewRegistration: { type: "boolean" }, + allowMultipleAccounts: { type: "boolean" }, + password: { + type: "object", + properties: { + minLength: { type: "number" }, + minNumbers: { type: "number" }, + minUpperCase: { type: "number" }, + minSymbols: { type: "number" }, + blockInsecureCommonPasswords: { type: "boolean" } + }, + required: ["minLength", "minNumbers", "minUpperCase", "minSymbols", "blockInsecureCommonPasswords"], + additionalProperties: false + } + }, + required: ["allowMultipleAccounts", "allowNewRegistration", "dateOfBirth", "email", "password", "requireCaptcha", "requireInvite"], + additionalProperties: false }, }, -}; + required: ["gateway", "general", "limits", "login", "permissions", "register", "security"], + additionalProperties: false +} + + +const ajv = new Ajv(); +const validator = ajv.compile(schema); + +const configPath = getConfigPathForFile("fosscord", "api", ".json"); + +export const apiConfig = new Config<DefaultOptions>({path: configPath, schemaValidator: validator, schema: schema}); \ No newline at end of file diff --git a/src/util/Member.ts b/src/util/Member.ts
index e6df5d2c..d03a8f12 100644 --- a/src/util/Member.ts +++ b/src/util/Member.ts
@@ -14,7 +14,7 @@ import { } from "@fosscord/server-util"; import { HTTPError } from "lambert-server"; -import Config from "./Config"; +import * as Config from "./Config"; import { emitEvent } from "./Event"; import { getPublicUser } from "./User"; @@ -39,7 +39,7 @@ export async function isMember(user_id: string, guild_id: string) { export async function addMember(user_id: string, guild_id: string, cache?: { guild?: GuildDocument }) { const user = await getPublicUser(user_id, { guilds: true }); - const { maxGuilds } = Config.get().limits.user; + const { maxGuilds } = Config.apiConfig.getAll().limits.user; if (user.guilds.length >= maxGuilds) { throw new HTTPError(`You are at the ${maxGuilds} server limit.`, 403); } diff --git a/src/util/passwordStrength.ts b/src/util/passwordStrength.ts
index f6cec9da..7196f797 100644 --- a/src/util/passwordStrength.ts +++ b/src/util/passwordStrength.ts
@@ -1,5 +1,5 @@ import "missing-native-js-functions"; -import Config from "./Config"; +import * as Config from "./Config"; const reNUMBER = /[0-9]/g; const reUPPERCASELETTER = /[A-Z]/g; @@ -23,7 +23,7 @@ export function check(password: string): number { minUpperCase, minSymbols, blockInsecureCommonPasswords, - } = Config.get().register.password; + } = Config.apiConfig.getAll().register.password; var strength = 0; // checks for total password len