From bdbf6d06b25fcdb2e9cbcdd32872ae62bfc8893c Mon Sep 17 00:00:00 2001 From: Rory& Date: Sun, 7 Dec 2025 23:18:57 +0100 Subject: [PATCH] Fix IP tracking for bans --- src/api/routes/guilds/#guild_id/bans.ts | 13 ++++- src/gateway/Server.ts | 5 ++ src/gateway/events/Connection.ts | 7 +++ src/gateway/listener/listener.ts | 11 ++++ src/gateway/util/WebSocket.ts | 1 + src/schemas/uncategorised/BanCreateSchema.ts | 4 +- src/util/entities/InstanceBan.ts | 57 ++++++++++++++++++++ src/util/interfaces/Event.ts | 1 + 8 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 src/util/entities/InstanceBan.ts diff --git a/src/api/routes/guilds/#guild_id/bans.ts b/src/api/routes/guilds/#guild_id/bans.ts index 30ef8df7b..39eee34aa 100644 --- a/src/api/routes/guilds/#guild_id/bans.ts +++ b/src/api/routes/guilds/#guild_id/bans.ts @@ -20,7 +20,7 @@ import { route } from "@spacebar/api"; import { Ban, DiscordApiErrors, GuildBanAddEvent, GuildBanRemoveEvent, Member, User, emitEvent } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; -import { APIBansArray, BanRegistrySchema, GuildBansResponse } from "@spacebar/schemas"; +import { APIBansArray, BanCreateSchema, BanRegistrySchema, GuildBansResponse } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); @@ -198,6 +198,15 @@ router.put( async (req: Request, res: Response) => { const { guild_id } = req.params; const banned_user_id = req.params.user_id; + const opts = req.body as BanCreateSchema; + + let deleteMessagesMs = opts.delete_message_days + ? (opts.delete_message_days as number) * 86400000 + : opts.delete_message_seconds + ? (opts.delete_message_seconds as number) * 1000 + : 0; + + if (deleteMessagesMs < 0) deleteMessagesMs = 0; if (req.user_id === banned_user_id && banned_user_id === req.permission?.cache.guild?.owner_id) throw new HTTPError("You are the guild owner, hence can't ban yourself", 403); @@ -207,6 +216,7 @@ router.put( const existingBan = await Ban.findOne({ where: { guild_id: guild_id, user_id: banned_user_id }, }); + // Bans on already banned users are silently ignored if (existingBan) return res.status(204).send(); @@ -227,6 +237,7 @@ router.put( data: { guild_id: guild_id, user: banned_user.toPublicUser(), + delete_message_secs: Math.floor(deleteMessagesMs / 1000), }, guild_id: guild_id, } as GuildBanAddEvent), diff --git a/src/gateway/Server.ts b/src/gateway/Server.ts index 269cc2ed2..5d7f8d87a 100644 --- a/src/gateway/Server.ts +++ b/src/gateway/Server.ts @@ -28,6 +28,7 @@ import ws from "ws"; import { Connection } from "./events/Connection"; import http from "http"; import { cleanupOnStartup } from "./util/Utils"; +import { randomString } from "@spacebar/api"; export class Server { public ws: ws.Server; @@ -50,6 +51,10 @@ export class Server { if (server) this.server = server; else { this.server = http.createServer(function (req, res) { + if(!req.headers.cookie?.split("; ").find(x => x.startsWith("__sb_sessid="))) { + res.setHeader("Set-Cookie", `__sb_sessid=${randomString(32)}; Secure; HttpOnly; SameSite=None`); + } + res.writeHead(200).end("Online"); }); } diff --git a/src/gateway/events/Connection.ts b/src/gateway/events/Connection.ts index bc74dee65..9b132d4b9 100644 --- a/src/gateway/events/Connection.ts +++ b/src/gateway/events/Connection.ts @@ -72,6 +72,13 @@ export async function Connection( ); } + if (request.headers.cookie?.split("; ").find(x => x.startsWith("__sb_sessid="))) { + socket.fingerprint = request.headers.cookie + .split("; ") + .find((x) => x.startsWith("__sb_sessid=")) + ?.split("=")[1]; + } + //Create session ID when the connection is opened. This allows gateway dump to group the initial websocket messages with the rest of the conversation. const session_id = genSessionId(); socket.session_id = session_id; //Set the session of the WebSocket object diff --git a/src/gateway/listener/listener.ts b/src/gateway/listener/listener.ts index 75cb6bc61..f76adfb29 100644 --- a/src/gateway/listener/listener.ts +++ b/src/gateway/listener/listener.ts @@ -29,6 +29,7 @@ import { Message, NewUrlUserSignatureData, GuildMemberAddEvent, + Ban, } from "@spacebar/util"; import { OPCODES } from "../util/Constants"; import { Send } from "../util/Send"; @@ -174,6 +175,16 @@ async function consume(this: WebSocket, opts: EventOpts) { case "GUILD_DELETE": this.events[id]?.(); delete this.events[id]; + if (event === "GUILD_DELETE" && this.ipAddress) { + const ban = await Ban.findOne({ + where: { guild_id: id, user_id: this.user_id }, + }); + + if (ban) { + ban.ip = this.ipAddress || undefined; + await ban.save(); + } + } break; case "CHANNEL_CREATE": if (!permission.overwriteChannel(data.permission_overwrites).has("VIEW_CHANNEL")) { diff --git a/src/gateway/util/WebSocket.ts b/src/gateway/util/WebSocket.ts index 00ef9e98f..d0a42eafa 100644 --- a/src/gateway/util/WebSocket.ts +++ b/src/gateway/util/WebSocket.ts @@ -33,6 +33,7 @@ export interface WebSocket extends WS { compress?: "zlib-stream" | "zstd-stream"; ipAddress?: string; userAgent?: string; // for cdn request signing + fingerprint?: string; shard_count?: bigint; shard_id?: bigint; deflate?: Deflate; diff --git a/src/schemas/uncategorised/BanCreateSchema.ts b/src/schemas/uncategorised/BanCreateSchema.ts index 6d30ca221..8f349f4bf 100644 --- a/src/schemas/uncategorised/BanCreateSchema.ts +++ b/src/schemas/uncategorised/BanCreateSchema.ts @@ -17,7 +17,7 @@ */ export interface BanCreateSchema { - delete_message_seconds?: string; - delete_message_days?: string; + delete_message_seconds?: number; + delete_message_days?: number; reason?: string; } diff --git a/src/util/entities/InstanceBan.ts b/src/util/entities/InstanceBan.ts new file mode 100644 index 000000000..8f8cd29d2 --- /dev/null +++ b/src/util/entities/InstanceBan.ts @@ -0,0 +1,57 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 Spacebar and Spacebar Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, OneToOne, RelationId } from "typeorm"; +import { BaseClass } from "./BaseClass"; +import { Team } from "./Team"; +import { User } from "./User"; +import { Guild } from "./Guild"; + +@Entity({ + name: "instance_bans", +}) +export class InstanceBan extends BaseClass { + @Column({ type: "bigint" }) + @CreateDateColumn() + created_at: Date = new Date(); + + @Column() + reason: string; + + @Column({ nullable: true }) + user_id: string; + + @Column({ nullable: true }) + fingerprint: string; + + @Column({ nullable: true }) + ip_address: string; + + // chain of trust type tracking + + @Column() + is_from_other_instance_ban: boolean; + + @Column({ nullable: true }) + @RelationId((instance_ban: InstanceBan) => instance_ban.origin_instance_ban) + origin_instance_ban_id?: string; + + @JoinColumn({ name: "origin_instance_ban_id" }) + @OneToOne(() => InstanceBan, { nullable: true, onDelete: "SET NULL" }) + origin_instance_ban?: InstanceBan; +} diff --git a/src/util/interfaces/Event.ts b/src/util/interfaces/Event.ts index 24157efd1..d0a604937 100644 --- a/src/util/interfaces/Event.ts +++ b/src/util/interfaces/Event.ts @@ -219,6 +219,7 @@ export interface GuildBanAddEvent extends Event { data: { guild_id: string; user: User; + delete_message_secs?: number }; }