diff --git a/src/api/Server.ts b/src/api/Server.ts index 08a1e8749..9e4bb9373 100644 --- a/src/api/Server.ts +++ b/src/api/Server.ts @@ -62,7 +62,7 @@ export class SpacebarServer extends Server { await ConnectionConfig.init(); await initInstance(); WebAuthn.init(); - await BcryptWorkerPool.Init(4); // TODO: make configurable + // await BcryptWorkerPool.Init(8); // TODO: make configurable const logRequests = process.env["LOG_REQUESTS"] != undefined; if (logRequests) { diff --git a/src/api/routes/auth/register.ts b/src/api/routes/auth/register.ts index d8313135d..4f2f4a7db 100644 --- a/src/api/routes/auth/register.ts +++ b/src/api/routes/auth/register.ts @@ -144,79 +144,81 @@ router.post( logTrace("Basic checks"); //region IP checks - const cacheBlockedIp = (ip: string, reason: string) => { - recentlyBlockedIps[ip] = { - firstHit: new Date(), - lastHit: new Date(), - hits: 0, - reason, + if (register.enableAbuseIpDb || register.enableIpData) { + const cacheBlockedIp = (ip: string, reason: string) => { + recentlyBlockedIps[ip] = { + firstHit: new Date(), + lastHit: new Date(), + hits: 0, + reason, + }; + console.log(`[Register] ${ip} blocked from registration:`, reason); }; - console.log(`[Register] ${ip} blocked from registration:`, reason); - }; - if (!regTokenUsed && recentlyBlockedIps[ip]) { - if (new TimeSpan(recentlyBlockedIps[ip].firstHit.getTime(), new Date().getTime()).totalHours >= 24) delete recentlyBlockedIps[ip]; - else { - recentlyBlockedIps[ip].lastHit = new Date(); - recentlyBlockedIps[ip].hits++; - console.log( - `[Register] ${ip} blocked from registration: blocked since ${recentlyBlockedIps[ip].firstHit} with ${recentlyBlockedIps[ip].hits} hits and reason:`, - recentlyBlockedIps[ip].reason, - ); - throw new HTTPError("Your IP is blocked from registration"); - } - } - - if (!regTokenUsed && register.enableAbuseIpDb) { - const blacklist = await AbuseIpDbClient.getBlacklist(); - if (blacklist) { - const entry = blacklist.data.find((e) => e.ipAddress === ip); - - if (entry && entry.abuseConfidenceScore >= register.blockAbuseIpDbAboveScore) { - cacheBlockedIp(ip, `AbuseIPDB score ${entry.abuseConfidenceScore} >= ${register.blockAbuseIpDbAboveScore} (BLACKLIST)`); + if (!regTokenUsed && recentlyBlockedIps[ip]) { + if (new TimeSpan(recentlyBlockedIps[ip].firstHit.getTime(), new Date().getTime()).totalHours >= 24) delete recentlyBlockedIps[ip]; + else { + recentlyBlockedIps[ip].lastHit = new Date(); + recentlyBlockedIps[ip].hits++; + console.log( + `[Register] ${ip} blocked from registration: blocked since ${recentlyBlockedIps[ip].firstHit} with ${recentlyBlockedIps[ip].hits} hits and reason:`, + recentlyBlockedIps[ip].reason, + ); throw new HTTPError("Your IP is blocked from registration"); } } - const checkIp = await AbuseIpDbClient.checkIpAddress(ip); - if (checkIp?.data && checkIp.data.abuseConfidenceScore >= register.blockAbuseIpDbAboveScore) { - cacheBlockedIp(ip, `AbuseIPDB score ${checkIp.data.abuseConfidenceScore} >= ${register.blockAbuseIpDbAboveScore} (CHECK)`); - throw new HTTPError("Your IP is blocked from registration"); - } - } + if (!regTokenUsed && register.enableAbuseIpDb) { + const blacklist = await AbuseIpDbClient.getBlacklist(); + if (blacklist) { + const entry = blacklist.data.find((e) => e.ipAddress === ip); - if (!regTokenUsed && register.enableIpData) { - const ipData = await IpDataClient.getIpInfo(ip); - if (ipData) { - if (!ipData.threat) { - console.log("Invalid IPData.co response, missing threat field", ipData); - } - const categories = Object.entries(ipData.threat) - .filter(([key, value]) => key.startsWith("is_") && value === true) - .map(([key]) => key.replace("is_", "")); - const blockedCategories = new Set(categories).intersection(new Set(register.blockIpDataCoThreatTypes)); - if (blockedCategories.size > 0) { - cacheBlockedIp(ip, `IPData.co threat types ${Array.from(blockedCategories).join(", ")}`); - throw new HTTPError("Your IP is blocked from registration"); + if (entry && entry.abuseConfidenceScore >= register.blockAbuseIpDbAboveScore) { + cacheBlockedIp(ip, `AbuseIPDB score ${entry.abuseConfidenceScore} >= ${register.blockAbuseIpDbAboveScore} (BLACKLIST)`); + throw new HTTPError("Your IP is blocked from registration"); + } } - if (ipData.asn.type && register.blockAsnTypes.includes(ipData.asn.type)) { - cacheBlockedIp(ip, `IPData.co ASN type ${ipData.asn.type} is blocked`); + const checkIp = await AbuseIpDbClient.checkIpAddress(ip); + if (checkIp?.data && checkIp.data.abuseConfidenceScore >= register.blockAbuseIpDbAboveScore) { + cacheBlockedIp(ip, `AbuseIPDB score ${checkIp.data.abuseConfidenceScore} >= ${register.blockAbuseIpDbAboveScore} (CHECK)`); throw new HTTPError("Your IP is blocked from registration"); - } else if (!ipData.asn.type) { - console.log("[Register] IPData.co response missing asn.type field", ipData); - } - - if (ipData.asn.asn && register.blockAsns.includes(ipData.asn.asn)) { - cacheBlockedIp(ip, `IPData.co ASN ${ipData.asn.name} is blocked`); - throw new HTTPError("Your IP is blocked from registration"); - } else if (!ipData.asn.asn) { - console.log("[Register] IPData.co response missing asn.asn field", ipData); } } + + if (!regTokenUsed && register.enableIpData) { + const ipData = await IpDataClient.getIpInfo(ip); + if (ipData) { + if (!ipData.threat) { + console.log("Invalid IPData.co response, missing threat field", ipData); + } + const categories = Object.entries(ipData.threat) + .filter(([key, value]) => key.startsWith("is_") && value === true) + .map(([key]) => key.replace("is_", "")); + const blockedCategories = new Set(categories).intersection(new Set(register.blockIpDataCoThreatTypes)); + if (blockedCategories.size > 0) { + cacheBlockedIp(ip, `IPData.co threat types ${Array.from(blockedCategories).join(", ")}`); + throw new HTTPError("Your IP is blocked from registration"); + } + + if (ipData.asn.type && register.blockAsnTypes.includes(ipData.asn.type)) { + cacheBlockedIp(ip, `IPData.co ASN type ${ipData.asn.type} is blocked`); + throw new HTTPError("Your IP is blocked from registration"); + } else if (!ipData.asn.type) { + console.log("[Register] IPData.co response missing asn.type field", ipData); + } + + if (ipData.asn.asn && register.blockAsns.includes(ipData.asn.asn)) { + cacheBlockedIp(ip, `IPData.co ASN ${ipData.asn.name} is blocked`); + throw new HTTPError("Your IP is blocked from registration"); + } else if (!ipData.asn.asn) { + console.log("[Register] IPData.co response missing asn.asn field", ipData); + } + } + } + logTrace("IP checks"); } //endregion - logTrace("IP checks"); // TODO: gift_code_sku_id? // TODO: check password strength @@ -306,8 +308,8 @@ router.post( }); } // the salt is saved in the password refer to bcrypt docs - body.password = await BcryptWorkerPool.GetBcryptWorker().hashPassword(body.password, 12); - // body.password = await bcrypt.hash(body.password, 12); + // body.password = await BcryptWorkerPool.GetBcryptWorker().hashPassword(body.password, 12); + body.password = await bcrypt.hash(body.password, 12); } else if (register.password.required) { throw FieldErrors({ password: { diff --git a/src/util/entities/Member.ts b/src/util/entities/Member.ts index 67c7e4c64..00178d264 100644 --- a/src/util/entities/Member.ts +++ b/src/util/entities/Member.ts @@ -304,7 +304,7 @@ export class Member extends BaseClassWithoutId { ]); } - static async addToGuild(user_id: string, guild_id: string) { + static async addToGuild(user_id: string, guild_id: string, isRegistration: boolean = false) { const totalSw = Stopwatch.startNew(); const incSw = Stopwatch.startNew(); const logTrace = (...data: unknown[]) => { @@ -312,19 +312,23 @@ export class Member extends BaseClassWithoutId { console.log("[Member.addToGuild]", ...data, `[${totalSw.elapsed().toString()} (+${incSw.getElapsedAndReset().totalMilliseconds}ms)]`); }; - const isBanned = await Ban.exists({ where: { guild_id, user_id } }); - if (isBanned) throw DiscordApiErrors.USER_BANNED; - logTrace("Check bans"); + if (!isRegistration) { + const isBanned = Ban.exists({ where: { guild_id, user_id } }); + const isMember = Member.exists({ where: { id: user_id, guild_id } }); - if (await Member.exists({ where: { id: user_id, guild_id } })) throw new HTTPError("You are already a member of this guild", 400); - logTrace("Check existing membership"); + if (await isBanned) throw DiscordApiErrors.USER_BANNED; + logTrace("Check bans"); - const { maxGuilds } = Config.get().limits.user; - const guild_count = await Member.count({ where: { id: user_id } }); - if (guild_count >= maxGuilds) { - throw new HTTPError(`You are at the ${maxGuilds} guild limit.`, 403); + if (await isMember) throw new HTTPError("You are already a member of this guild", 400); + logTrace("Check existing membership"); + + const { maxGuilds } = Config.get().limits.user; + const guild_count = await Member.count({ where: { id: user_id } }); + if (guild_count >= maxGuilds) { + throw new HTTPError(`You are at the ${maxGuilds} guild limit.`, 403); + } + logTrace("Enforce max guilds"); } - logTrace("Enforce max guilds"); const guild = await Guild.findOneOrFail({ where: { @@ -339,30 +343,6 @@ export class Member extends BaseClassWithoutId { }); logTrace("Find guild"); - for await (const channel of guild.channels) { - channel.position = await Channel.calculatePosition(channel.id, guild_id, channelPositionsGuild); - } - logTrace("Reorder channels"); - - const memberCount = await Member.count({ where: { guild_id } }); - logTrace("Get member count"); - - const memberPreview = ( - await Member.find({ - where: { - guild_id, - user: { - sessions: { - status: Not("invisible" as const), // lol typescript? - }, - }, - }, - relations: { user: true, roles: true }, - take: 10, - }) - ).map((member) => member.toPublicMember()); - logTrace("Calculate member preview"); - const newMember = Member.create({ id: user_id, guild_id, @@ -392,11 +372,41 @@ export class Member extends BaseClassWithoutId { // Member.save is needed because else the roles relations wouldn't be updated }); + let memberCount = 0; + let memberPreview: PublicMember[] = []; + if (!isRegistration) { + for await (const channel of guild.channels) { + channel.position = await Channel.calculatePosition(channel.id, guild_id, channelPositionsGuild); + } + + logTrace("Reorder channels"); + + memberCount = isRegistration ? 0 : await Member.count({ where: { guild_id } }); + logTrace("Get member count"); + + memberPreview = ( + await Member.find({ + where: { + guild_id, + user: { + id: Not(user_id), + sessions: { + status: Not("invisible" as const), // lol typescript? + }, + }, + }, + relations: { user: true, roles: true }, + take: 10, + }) + ).map((member) => member.toPublicMember()); + logTrace("Calculate member preview"); + } + const user = await User.getPublicUser(user_id); logTrace("Get user"); await Promise.all([ - newMember.save(), + newMember.save(), // TODO: can we somehow insert the roles manually? We have no entity for this... Would skip a few select's Guild.increment({ id: guild_id }, "member_count", 1), emitEvent({ event: "GUILD_MEMBER_ADD", @@ -408,23 +418,25 @@ export class Member extends BaseClassWithoutId { guild_id, origin: "util/entities/Member.ts:377/addToGuild(user_id, guild_id)", } satisfies GuildMemberAddEvent), - emitEvent({ - event: "GUILD_CREATE", - data: { - ...new ReadyGuildDTO(guild).toJSON(), - members: [...memberPreview, { ...newMember.toPublicMember(), user }], - member_count: memberCount + 1, - guild_hashes: {}, - guild_scheduled_events: [], - joined_at: newMember.joined_at, - presences: [], - stage_instances: [], - threads: [], - embedded_activities: [], - voice_states: guild.voice_states.map((x) => x.toPublicVoiceState()), - }, - user_id, - } satisfies GuildCreateEvent), + isRegistration + ? null + : emitEvent({ + event: "GUILD_CREATE", + data: { + ...new ReadyGuildDTO(guild).toJSON(), + members: [...memberPreview, { ...newMember.toPublicMember(), user }], + member_count: memberCount + 1, + guild_hashes: {}, + guild_scheduled_events: [], + joined_at: newMember.joined_at, + presences: [], + stage_instances: [], + threads: [], + embedded_activities: [], + voice_states: guild.voice_states.map((x) => x.toPublicVoiceState()), + }, + user_id, + } satisfies GuildCreateEvent), ]); logTrace("Save member info"); @@ -448,13 +460,12 @@ export class Member extends BaseClassWithoutId { mention_everyone: false, }); - await message.insert(); - const publicMsg = message.toJSON(); await Promise.all([ + message.insert(), emitEvent({ event: "MESSAGE_CREATE", channel_id: message.channel_id, - data: publicMsg, + data: message.toJSON(), } satisfies MessageCreateEvent), Channel.update({ id: welcomeChannelId }, { last_message_id: message.id }), ]); diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts index a58b6579a..afde3e9ba 100644 --- a/src/util/entities/User.ts +++ b/src/util/entities/User.ts @@ -363,7 +363,7 @@ export class User extends BaseClass { const { autoJoin } = Config.get().guild; if (autoJoin.enabled && autoJoin.guilds.length > 0 && !(bot && !autoJoin.bots)) { - await Promise.all(autoJoin.guilds.map((guild) => Member.addToGuild(user.id, guild).catch((e) => console.error("[Autojoin]", e)))); + await Promise.all(autoJoin.guilds.map((guild) => Member.addToGuild(user.id, guild, true).catch((e) => console.error("[Autojoin]", e)))); logTrace("Autojoin", autoJoin.guilds.length, "guilds"); } diff --git a/src/util/util/workers/bcrypt/BcryptWorkerPool.ts b/src/util/util/workers/bcrypt/BcryptWorkerPool.ts index bda5a5761..1d1705ef0 100644 --- a/src/util/util/workers/bcrypt/BcryptWorkerPool.ts +++ b/src/util/util/workers/bcrypt/BcryptWorkerPool.ts @@ -58,7 +58,7 @@ class BcryptWorker { if (msg.type == "hash" && msg.requestId === requestId) { res((msg as BcryptHashMessage).password); this.worker.off("message", handler); - console.log("[BcryptWorker] Got response to hashPassword in", sw.elapsed().toString()); + if (sw.elapsed().totalMilliseconds > 5000) console.log("[BcryptWorker] Got slow response to hashPassword in", sw.elapsed().toString()); } }; this.worker.on("message", handler); @@ -86,7 +86,7 @@ interface BcryptHashVerify extends BcryptWorkerMessage { //region Worker implementation if (!isMainThread) { parentPort!.on("message", async (msg: BcryptWorkerMessage) => { - console.log("[BcryptWorker] Received", msg.type, "message"); + // console.log("[BcryptWorker] Received", msg.type, "message"); switch (msg.type) { case "hash": parentPort?.postMessage({