mirror of
https://github.com/spacebarchat/server.git
synced 2026-05-18 02:35:24 +00:00
Instance ban stuff
This commit is contained in:
Generated
+10
-7
@@ -55,9 +55,10 @@
|
||||
"Node.js.Server.ts.executor": "Debug",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"git-widget-placeholder": "master",
|
||||
"javascript.nodejs.core.library.configured.version": "24.8.0",
|
||||
"javascript.nodejs.core.library.typings.version": "24.7.0",
|
||||
"last_opened_file_path": "/home/Rory/git/spacebar/server-master/src/util/migration/postgres",
|
||||
"last_opened_file_path": "/home/Rory/git/spacebar/server-master/src/schemas/api/users",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.standard": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
@@ -71,7 +72,7 @@
|
||||
"npm.build.executor": "Run",
|
||||
"npm.start.executor": "Debug",
|
||||
"prettierjs.PrettierConfiguration.Package": "/home/Rory/git/spacebar/server-master/node_modules/prettier",
|
||||
"settings.editor.selected.configurable": "preferences.sourceCode",
|
||||
"settings.editor.selected.configurable": "com.github.copilot.settings.customInstructions.CopilotInstructionsConfigurable",
|
||||
"ts.external.directory.path": "/home/Rory/git/spacebar/server-master/node_modules/typescript/lib"
|
||||
},
|
||||
"keyToStringList": {
|
||||
@@ -87,11 +88,11 @@
|
||||
}]]></component>
|
||||
<component name="RecentsManager">
|
||||
<key name="CopyFile.RECENT_KEYS">
|
||||
<recent name="$PROJECT_DIR$/src/util/migration/postgres" />
|
||||
<recent name="$PROJECT_DIR$/src/util/entities" />
|
||||
<recent name="$PROJECT_DIR$/src/schemas/api/guilds" />
|
||||
<recent name="$PROJECT_DIR$/src/schemas" />
|
||||
<recent name="$PROJECT_DIR$/src/schemas/api/users" />
|
||||
<recent name="$PROJECT_DIR$/src/util/entities" />
|
||||
<recent name="$PROJECT_DIR$/src/gateway/opcodes" />
|
||||
<recent name="$PROJECT_DIR$/src/api/routes/users/@me/billing" />
|
||||
<recent name="$PROJECT_DIR$/src/util/util/networking/abuseipdb" />
|
||||
</key>
|
||||
<key name="MoveFile.RECENT_KEYS">
|
||||
<recent name="$PROJECT_DIR$/src/schemas/api/guilds" />
|
||||
@@ -161,7 +162,9 @@
|
||||
<workItem from="1760044946282" duration="43683000" />
|
||||
<workItem from="1760402350251" duration="49898000" />
|
||||
<workItem from="1760538864442" duration="1330000" />
|
||||
<workItem from="1764432507485" duration="22274000" />
|
||||
<workItem from="1764432507485" duration="30779000" />
|
||||
<workItem from="1764521899659" duration="22029000" />
|
||||
<workItem from="1765142585081" duration="16912000" />
|
||||
</task>
|
||||
<servers />
|
||||
</component>
|
||||
|
||||
@@ -49,7 +49,7 @@ function proxy(file, apiMethod, apiPathPrefix, apiPath, ...args) {
|
||||
const opts = args.find((x) => x?.prototype?.OPTS_MARKER == true);
|
||||
if (!opts)
|
||||
return console.error(
|
||||
` \x1b[5m${bgRedBright("ERROR")}\x1b[25m ${file.replace(path.resolve(__dirname, "..", "..", "dist"), "/src/")} has route without route() description middleware: ${colorizeMethod(apiMethod)} ${formatPath(apiPath)}`,
|
||||
` \x1b[5m${bgRedBright("ERROR")}\x1b[25m ${file.replace(path.resolve(__dirname, "..", "..", "dist"), "/src")} has route without route() description middleware: ${colorizeMethod(apiMethod)} ${formatPath(apiPath)}`,
|
||||
);
|
||||
|
||||
console.log(`${colorizeMethod(apiMethod).padStart("DELETE".length + 10)} ${formatPath(apiPathPrefix + apiPath)}`);
|
||||
|
||||
@@ -70,6 +70,7 @@ declare global {
|
||||
user_bot: boolean;
|
||||
token: { id: string; iat: number };
|
||||
rights: Rights;
|
||||
fingerprint?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,6 +78,15 @@ declare global {
|
||||
export async function Authentication(req: Request, res: Response, next: NextFunction) {
|
||||
if (req.method === "OPTIONS") return res.sendStatus(204);
|
||||
const url = req.url.replace(API_PREFIX, "");
|
||||
|
||||
if (req.headers.cookie?.split("; ").find((x) => x.startsWith("__sb_sessid=")))
|
||||
req.fingerprint = req.headers.cookie
|
||||
.split("; ")
|
||||
.find((x) => x.startsWith("__sb_sessid="))!
|
||||
.split("=")[1];
|
||||
// for some reason we need to require here, else the openapi generator fails with "route is not a function"
|
||||
else res.setHeader("Set-Cookie", `__sb_sessid=${req.fingerprint = (await require("../util")).randomString(32)}; Secure; HttpOnly; SameSite=None`);
|
||||
|
||||
if (
|
||||
NO_AUTHORIZATION_ROUTES.some((x) => {
|
||||
if (typeof x !== "string") {
|
||||
@@ -102,10 +112,14 @@ export async function Authentication(req: Request, res: Response, next: NextFunc
|
||||
})
|
||||
)
|
||||
return next();
|
||||
|
||||
if (!req.headers.authorization) return next(new HTTPError("Missing Authorization Header", 401));
|
||||
|
||||
try {
|
||||
const { decoded, user } = await checkToken(req.headers.authorization);
|
||||
const { decoded, user } = await checkToken(req.headers.authorization, {
|
||||
ipAddress: req.ip,
|
||||
fingerprint: req.fingerprint,
|
||||
});
|
||||
|
||||
req.token = decoded;
|
||||
req.user_id = decoded.id;
|
||||
|
||||
@@ -51,6 +51,8 @@ router.post(
|
||||
try {
|
||||
const userTokenData = await checkToken(token, {
|
||||
select: ["email"],
|
||||
fingerprint: req.fingerprint,
|
||||
ipAddress: req.ip
|
||||
});
|
||||
user = userTokenData.user;
|
||||
} catch {
|
||||
|
||||
@@ -75,7 +75,10 @@ router.post(
|
||||
let user;
|
||||
|
||||
try {
|
||||
const userTokenData = await checkToken(token);
|
||||
const userTokenData = await checkToken(token, {
|
||||
fingerprint: req.fingerprint,
|
||||
ipAddress: req.ip
|
||||
});
|
||||
user = userTokenData.user;
|
||||
} catch {
|
||||
throw FieldErrors({
|
||||
|
||||
@@ -24,7 +24,7 @@ export class SecurityConfiguration {
|
||||
twoFactor: TwoFactorConfiguration = new TwoFactorConfiguration();
|
||||
autoUpdate: boolean | number = true;
|
||||
requestSignature: string = crypto.randomBytes(32).toString("base64");
|
||||
jwtSecret: string = crypto.randomBytes(256).toString("base64");
|
||||
jwtSecret: string = crypto.randomBytes(256).toString("base64"); // deprecated
|
||||
// header to get the real user ip address
|
||||
// X-Forwarded-For for nginx/reverse proxies
|
||||
// CF-Connecting-IP for cloudflare
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, OneToOne, RelationId } from "typeorm";
|
||||
import { Column, CreateDateColumn, Entity, FindOptionsWhere, JoinColumn, ManyToOne, OneToOne, RelationId } from "typeorm";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
import { Team } from "./Team";
|
||||
import { User } from "./User";
|
||||
@@ -44,6 +44,9 @@ export class InstanceBan extends BaseClass {
|
||||
|
||||
// chain of trust type tracking
|
||||
|
||||
@Column({ default: false })
|
||||
is_allowlisted: boolean = false;
|
||||
|
||||
@Column({ default: false })
|
||||
is_from_other_instance_ban: boolean = false;
|
||||
|
||||
@@ -54,4 +57,55 @@ export class InstanceBan extends BaseClass {
|
||||
@JoinColumn({ name: "origin_instance_ban_id" })
|
||||
@OneToOne(() => InstanceBan, { nullable: true, onDelete: "SET NULL" })
|
||||
origin_instance_ban?: InstanceBan;
|
||||
|
||||
static async findInstanceBans(opts: { userId?: string; ipAddress?: string; fingerprint?: string; propagateBan?: boolean }) {
|
||||
const optionalChecks: FindOptionsWhere<InstanceBan>[] = [{ user_id: opts.userId }];
|
||||
if (opts?.ipAddress) optionalChecks.push({ ip_address: opts.ipAddress });
|
||||
if (opts?.fingerprint) optionalChecks.push({ fingerprint: opts.fingerprint });
|
||||
const instanceBans = await InstanceBan.find({ where: optionalChecks });
|
||||
|
||||
const banReasons = [];
|
||||
for (const ban of instanceBans) {
|
||||
if (ban.is_allowlisted) continue;
|
||||
if (opts?.fingerprint && ban.fingerprint === opts.fingerprint) banReasons.push("fingerprint");
|
||||
if (opts?.ipAddress && ban.ip_address === opts.ipAddress) banReasons.push("ipAddress");
|
||||
if (opts?.userId && ban.user_id === opts?.userId) banReasons.push("userId");
|
||||
}
|
||||
|
||||
const banViralityPromises: Promise<InstanceBan>[] = [];
|
||||
if (opts.propagateBan && banReasons.length > 0) {
|
||||
if (opts?.ipAddress && !instanceBans.find((b) => b.ip_address === opts.ipAddress))
|
||||
banViralityPromises.push(
|
||||
InstanceBan.create({
|
||||
user_id: opts.userId,
|
||||
ip_address: opts.ipAddress,
|
||||
reason: "Propagated from other instance ban",
|
||||
is_from_other_instance_ban: true,
|
||||
origin_instance_ban: instanceBans[0],
|
||||
}).save(),
|
||||
);
|
||||
if (opts?.fingerprint && !instanceBans.find((b) => b.fingerprint === opts.fingerprint))
|
||||
banViralityPromises.push(
|
||||
InstanceBan.create({
|
||||
user_id: opts.userId,
|
||||
fingerprint: opts.fingerprint,
|
||||
reason: "Propagated from other instance ban",
|
||||
is_from_other_instance_ban: true,
|
||||
origin_instance_ban: instanceBans[0],
|
||||
}).save(),
|
||||
);
|
||||
if (opts?.userId && !instanceBans.find((b) => b.user_id === opts.userId))
|
||||
banViralityPromises.push(
|
||||
InstanceBan.create({
|
||||
user_id: opts.userId,
|
||||
reason: "Propagated from other instance ban",
|
||||
is_from_other_instance_ban: true,
|
||||
origin_instance_ban: instanceBans[0],
|
||||
}).save(),
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(banViralityPromises);
|
||||
return banReasons;
|
||||
}
|
||||
}
|
||||
|
||||
+21
-45
@@ -18,15 +18,12 @@
|
||||
|
||||
import jwt, { VerifyOptions } from "jsonwebtoken";
|
||||
import { Config } from "./Config";
|
||||
import { User } from "../entities";
|
||||
import { InstanceBan, User } from "../entities";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "fs/promises";
|
||||
import { existsSync } from "fs";
|
||||
// TODO: dont use deprecated APIs lol
|
||||
import {
|
||||
FindOptionsRelationByString,
|
||||
FindOptionsSelectByString,
|
||||
} from "typeorm";
|
||||
import { FindManyOptions, FindOptions, FindOptionsRelationByString, FindOptionsSelect, FindOptionsSelectByString, FindOptionsWhere } from "typeorm";
|
||||
import * as console from "node:console";
|
||||
|
||||
export const JWTOptions: VerifyOptions = { algorithms: ["HS256"] };
|
||||
@@ -37,7 +34,7 @@ export type UserTokenData = {
|
||||
};
|
||||
|
||||
function logAuth(text: string) {
|
||||
if(process.env.LOG_AUTH !== "true") return;
|
||||
if (process.env.LOG_AUTH !== "true") return;
|
||||
console.log(`[AUTH] ${text}`);
|
||||
}
|
||||
|
||||
@@ -51,6 +48,8 @@ export const checkToken = (
|
||||
opts?: {
|
||||
select?: FindOptionsSelectByString<User>;
|
||||
relations?: FindOptionsRelationByString;
|
||||
ipAddress?: string;
|
||||
fingerprint?: string;
|
||||
},
|
||||
): Promise<UserTokenData> =>
|
||||
new Promise((resolve, reject) => {
|
||||
@@ -66,15 +65,7 @@ export const checkToken = (
|
||||
|
||||
const user = await User.findOne({
|
||||
where: { id: decoded.id },
|
||||
select: [
|
||||
...(opts?.select || []),
|
||||
"id",
|
||||
"bot",
|
||||
"disabled",
|
||||
"deleted",
|
||||
"rights",
|
||||
"data",
|
||||
],
|
||||
select: [...(opts?.select || []), "id", "bot", "disabled", "deleted", "rights", "data"],
|
||||
relations: opts?.relations,
|
||||
});
|
||||
|
||||
@@ -84,10 +75,7 @@ export const checkToken = (
|
||||
}
|
||||
|
||||
// we need to round it to seconds as it saved as seconds in jwt iat and valid_tokens_since is stored in milliseconds
|
||||
if (
|
||||
decoded.iat * 1000 <
|
||||
new Date(user.data.valid_tokens_since).setSeconds(0, 0)
|
||||
) {
|
||||
if (decoded.iat * 1000 < new Date(user.data.valid_tokens_since).setSeconds(0, 0)) {
|
||||
logAuth("validateUser rejected: Token not yet valid");
|
||||
return rejectAndLog(reject, "Invalid Token");
|
||||
}
|
||||
@@ -96,11 +84,18 @@ export const checkToken = (
|
||||
logAuth("validateUser rejected: User disabled");
|
||||
return rejectAndLog(reject, "User disabled");
|
||||
}
|
||||
|
||||
if (user.deleted) {
|
||||
logAuth("validateUser rejected: User deleted");
|
||||
return rejectAndLog(reject, "User not found");
|
||||
}
|
||||
|
||||
const banReasons = await InstanceBan.findInstanceBans({ userId: user.id, ipAddress: opts?.ipAddress, fingerprint: opts?.fingerprint, propagateBan: true });
|
||||
if (banReasons.length > 0) {
|
||||
logAuth("validateUser rejected: User banned for reasons: " + banReasons.join(", "));
|
||||
return rejectAndLog(reject, "Invalid Token");
|
||||
}
|
||||
|
||||
logAuth("validateUser success: " + JSON.stringify({ decoded, user }));
|
||||
return resolve({ decoded, user });
|
||||
};
|
||||
@@ -110,20 +105,10 @@ export const checkToken = (
|
||||
logAuth("Decoded token: " + JSON.stringify(dec));
|
||||
|
||||
if (dec.header.alg == "HS256") {
|
||||
jwt.verify(
|
||||
token,
|
||||
Config.get().security.jwtSecret,
|
||||
JWTOptions,
|
||||
validateUser,
|
||||
);
|
||||
jwt.verify(token, Config.get().security.jwtSecret, JWTOptions, validateUser);
|
||||
} else if (dec.header.alg == "ES512") {
|
||||
loadOrGenerateKeypair().then((keyPair) => {
|
||||
jwt.verify(
|
||||
token,
|
||||
keyPair.publicKey,
|
||||
{ algorithms: ["ES512"] },
|
||||
validateUser,
|
||||
);
|
||||
jwt.verify(token, keyPair.publicKey, { algorithms: ["ES512"] }, validateUser);
|
||||
});
|
||||
} else return reject("Invalid token algorithm");
|
||||
});
|
||||
@@ -152,7 +137,7 @@ let cachedKeypair: {
|
||||
privateKey: crypto.KeyObject;
|
||||
publicKey: crypto.KeyObject;
|
||||
fingerprint: string;
|
||||
}
|
||||
};
|
||||
|
||||
// Get ECDSA keypair from file or generate it
|
||||
export async function loadOrGenerateKeypair() {
|
||||
@@ -176,10 +161,7 @@ export async function loadOrGenerateKeypair() {
|
||||
let publicKey: crypto.KeyObject;
|
||||
|
||||
if (existsSync("jwt.key") && existsSync("jwt.key.pub")) {
|
||||
const [loadedPrivateKey, loadedPublicKey] = await Promise.all([
|
||||
fs.readFile("jwt.key"),
|
||||
fs.readFile("jwt.key.pub"),
|
||||
]);
|
||||
const [loadedPrivateKey, loadedPublicKey] = await Promise.all([fs.readFile("jwt.key"), fs.readFile("jwt.key.pub")]);
|
||||
|
||||
privateKey = crypto.createPrivateKey(loadedPrivateKey);
|
||||
publicKey = crypto.createPublicKey(loadedPublicKey);
|
||||
@@ -192,14 +174,8 @@ export async function loadOrGenerateKeypair() {
|
||||
publicKey = res.publicKey;
|
||||
|
||||
await Promise.all([
|
||||
fs.writeFile(
|
||||
"jwt.key",
|
||||
privateKey.export({ format: "pem", type: "sec1" }),
|
||||
),
|
||||
fs.writeFile(
|
||||
"jwt.key.pub",
|
||||
publicKey.export({ format: "pem", type: "spki" }),
|
||||
),
|
||||
fs.writeFile("jwt.key", privateKey.export({ format: "pem", type: "sec1" })),
|
||||
fs.writeFile("jwt.key.pub", publicKey.export({ format: "pem", type: "spki" })),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -209,5 +185,5 @@ export async function loadOrGenerateKeypair() {
|
||||
.digest("hex");
|
||||
|
||||
lastFsCheck = Date.now();
|
||||
return cachedKeypair = { privateKey, publicKey, fingerprint };
|
||||
return (cachedKeypair = { privateKey, publicKey, fingerprint });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user