From 080b2c7d383b5e09fee97f267d35f4e7cd22f0a9 Mon Sep 17 00:00:00 2001 From: Puyodead1 Date: Wed, 16 Apr 2025 23:28:43 -0400 Subject: [PATCH] Implement signed cdn urls --- assets/openapi.json | Bin 637397 -> 642373 bytes assets/schemas.json | Bin 26102937 -> 26419270 bytes package-lock.json | Bin 397407 -> 397427 bytes package.json | 1 + src/api/routes/attachments/refresh-urls.ts | 48 +++++++ .../channels/#channel_id/messages/index.ts | 19 +-- src/cdn/routes/attachments.ts | 32 ++++- src/util/Signing.ts | 136 ++++++++++++++++++ .../config/types/SecurityConfiguration.ts | 4 + src/util/index.ts | 3 + src/util/schemas/RefreshUrlsRequestSchema.ts | 21 +++ src/util/schemas/index.ts | 1 + .../schemas/responses/RefreshUrlsResponse.ts | 26 ++++ src/util/schemas/responses/index.ts | 1 + 14 files changed, 279 insertions(+), 13 deletions(-) create mode 100644 src/api/routes/attachments/refresh-urls.ts create mode 100644 src/util/Signing.ts create mode 100644 src/util/schemas/RefreshUrlsRequestSchema.ts create mode 100644 src/util/schemas/responses/RefreshUrlsResponse.ts diff --git a/assets/openapi.json b/assets/openapi.json index c9fdd76caf9f50b1e94555fa778f5dfbf6782e12..c3124e1e755a0966205e8c635dc6e5c8f78d374f 100644 GIT binary patch delta 787 zcmZvaT}V@57{__vbKaeEX0$C#`(QRwrzGa4IQzg{HJAmFSklM9oZC^SbIzR&guoOj zttb|!&I4XlMmGi13E?XhwJwyXiw02_T}{ZMn=&JkP}V-uyy)V&d7l6O&-47=_uUWU z%xFopMh^)cA{$PGS|wM&;}3dzd@weGTOlFx0%!-BVr2*33*FVoD4Y6<GMJZ?WCDO~`2@bm2v5Ag&u;`%iU-&2c-xHQHz;x?@Nryt-t}L}OJ68Y+g{cA4 z8B`4oZ&U>kdxQwTs)uM5V@R{wCWkxfIVsB>23znJ2+!g|cyCp0hGqpjAa)rw)7T8w zRnq4x7_6EMsHT8dY7~1zq6Cxs7#Hj|BQ<+26!5~0`|LQFj$wPI$E12NS0)t7ab81D z9$-%OuCh)=z(PACrU9x%cg8$9kFE*po?;`L3k{bT0b0J2S~(mtq{Sp7&ZNR!4b}}u zCKwu-U~lq5Fz9r3bc?;v{!`5k*TBnK64%e57j<|Y!Z5|p;l6pjrw|0sy_`ZkF z2kRBim}XG`)>?8F2A0TnNH|E*a3o1mrAie?BT4>Kf8JWYR1wIoS)RUmaxt~1c2u~n uykfs>`umsuAqrCGPEJAUtRvm0V@_Vcpp^t0RG3i?xGoBZ>79PTvi}eNMlAvW delta 295 zcmX>)Oa1C+wGHO7&5x9}KT=|>__SR>oXJOgyDuw?DdS}S*$LAHCNrf@PjF=7+-`5k zbcSQ{1x2>$=X#m*rz?1~%1vMJg;{*Mz;h+jB~Pxqu_Lh3rzCU=N2&WO@AKC6uTWL{xfR2KpivdcK=IE_l>r5 zoMQUIGF{*-6We5arMBq;*^H*sKiDyHPS?Kxbeb+J^9+aW)$Yuph3&I;F>jx>i)9)| pd-`FP?dgYEbKTo@c-erM9f&!$>+o_ut(*RDHYd+^{z;r>)&OQUXA1xT diff --git a/assets/schemas.json b/assets/schemas.json index ef00cc31a03b2bbbed7787bb36e74a535263f7d0..44c77a771df04a52394616f988d1c0f5e8204b19 100755 GIT binary patch delta 16773 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 IrNlJrKdLh}OaK4? delta 17755 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 delta 38 ncmeyoK;r%ai49yV&2lX5ax9EM%ml>DKn&uuZkJ. +*/ + +import { route } from "@spacebar/api"; +import { RefreshUrlsRequestSchema, resignUrl } from "@spacebar/util"; +import { Request, Response, Router } from "express"; +const router = Router(); + +router.post( + "/", + route({ + requestBody: "RefreshUrlsRequestSchema", + responses: { + 200: { + body: "RefreshUrlsResponse", + }, + 400: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { attachment_urls } = req.body as RefreshUrlsRequestSchema; + + const refreshed_urls = attachment_urls.map(resignUrl); + + return res.status(200).json({ + refreshed_urls, + }); + }, +); + +export default router; diff --git a/src/api/routes/channels/#channel_id/messages/index.ts b/src/api/routes/channels/#channel_id/messages/index.ts index 4ba28927..a540dce6 100644 --- a/src/api/routes/channels/#channel_id/messages/index.ts +++ b/src/api/routes/channels/#channel_id/messages/index.ts @@ -35,6 +35,7 @@ import { emitEvent, getPermission, isTextChannel, + resignUrl, uploadFile, } from "@spacebar/util"; import { Request, Response, Router } from "express"; @@ -199,16 +200,18 @@ router.get( ? y.proxy_url : `https://example.org${y.proxy_url}`; - let pathname = new URL(uri).pathname; - while ( - pathname.split("/")[0] != "attachments" && - pathname.length > 30 - ) { - pathname = pathname.split("/").slice(1).join("/"); + const url = new URL(uri); + if (endpoint) { + const newBase = new URL(endpoint); + url.protocol = newBase.protocol; + url.hostname = newBase.hostname; + url.port = newBase.port; } - if (!endpoint?.endsWith("/")) pathname = "/" + pathname; - y.proxy_url = `${endpoint == null ? "" : endpoint}${pathname}`; + y.proxy_url = url.toString(); + + y.proxy_url = resignUrl(y.proxy_url); + y.url = resignUrl(y.url); }); /** diff --git a/src/cdn/routes/attachments.ts b/src/cdn/routes/attachments.ts index 19bb0b90..3b79e7f8 100644 --- a/src/cdn/routes/attachments.ts +++ b/src/cdn/routes/attachments.ts @@ -16,13 +16,18 @@ along with this program. If not, see . */ -import { Router, Response, Request } from "express"; -import { Config, Snowflake } from "@spacebar/util"; -import { storage } from "../util/Storage"; +import { + Config, + getUrlSignature, + hasValidSignature, + Snowflake, +} from "@spacebar/util"; +import { Request, Response, Router } from "express"; import FileType from "file-type"; +import imageSize from "image-size"; import { HTTPError } from "lambert-server"; import { multer } from "../util/multer"; -import imageSize from "image-size"; +import { storage } from "../util/Storage"; const router = Router(); @@ -39,6 +44,7 @@ router.post( async (req: Request, res: Response) => { if (req.headers.signature !== Config.get().security.requestSignature) throw new HTTPError("Invalid request signature"); + if (!req.file) throw new HTTPError("file missing"); const { buffer, mimetype, size, originalname } = req.file; @@ -63,12 +69,20 @@ router.post( } } + let finalUrl = `${endpoint}/${path}`; + + if (Config.get().security.cdnSignUrls) { + const signatureData = getUrlSignature(path); + console.log(signatureData); + finalUrl = `${finalUrl}?ex=${signatureData.expiresAt}&is=${signatureData.issuedAt}&hm=${signatureData.hash}&`; + } + const file = { id, content_type: mimetype, filename: filename, size, - url: `${endpoint}/${path}`, + url: finalUrl, width, height, }; @@ -84,6 +98,14 @@ router.get( // const { format } = req.query; const path = `attachments/${channel_id}/${id}/${filename}`; + + if ( + Config.get().security.cdnSignUrls && + !hasValidSignature(path, req.query) + ) { + return res.status(404).send("This content is no longer available."); + } + const file = await storage.get(path); if (!file) throw new HTTPError("File not found"); const type = await FileType.fromBuffer(file); diff --git a/src/util/Signing.ts b/src/util/Signing.ts new file mode 100644 index 00000000..5763ed16 --- /dev/null +++ b/src/util/Signing.ts @@ -0,0 +1,136 @@ +/* + 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 { Config } from "@spacebar/util"; +import { createHmac, timingSafeEqual } from "crypto"; +import ms, { StringValue } from "ms"; +import { ParsedQs } from "qs"; + +export const getUrlSignature = (path: string) => { + const { cdnSignatureKey, cdnSignatureDuration } = Config.get().security; + + // calculate the expiration time + const now = Date.now(); + const issuedAt = now.toString(16); + const expiresAt = (now + ms(cdnSignatureDuration as StringValue)).toString( + 16, + ); + + // hash the url with the cdnSignatureKey + const hash = createHmac("sha256", cdnSignatureKey as string) + .update(path) + .update(issuedAt) + .update(expiresAt) + .digest("hex"); + + return { + hash, + issuedAt, + expiresAt, + }; +}; + +export const calculateHash = ( + url: string, + issuedAt: string, + expiresAt: string, +) => { + const { cdnSignatureKey } = Config.get().security; + const hash = createHmac("sha256", cdnSignatureKey as string) + .update(url) + .update(issuedAt) + .update(expiresAt) + .digest("hex"); + return hash; +}; + +export const isExpired = (ex: string, is: string) => { + // convert issued at + const issuedAt = parseInt(is, 16); + const expiresAt = parseInt(ex, 16); + + if (Number.isNaN(issuedAt) || Number.isNaN(expiresAt)) { + // console.debug("Invalid timestamps in query"); + return true; + } + + const currentTime = Date.now(); + const isExpired = expiresAt < currentTime; + const isValidIssuedAt = issuedAt < currentTime; + if (isExpired || !isValidIssuedAt) { + // console.debug("Signature expired"); + return true; + } + + return false; +}; + +export const hasValidSignature = (path: string, query: ParsedQs) => { + // get url path + const { ex, is, hm } = query; + + // if the required query parameters are not present, return false + if (!ex || !is || !hm) return false; + + // check if the signature is expired + if (isExpired(ex as string, is as string)) { + return false; + } + + const calcd = calculateHash(path, is as string, ex as string); + const calculated = Buffer.from(calcd); + const received = Buffer.from(hm as string); + + const isHashValid = + calculated.length === received.length && + timingSafeEqual(calculated, received); + // if (!isHashValid) { + // console.debug("Invalid signature"); + // console.debug(calcd, hm); + // } + return isHashValid; +}; + +export const resignUrl = (attachmentUrl: string) => { + const url = new URL(attachmentUrl); + + // if theres an existing signature, check if its expired or not. no reason to resign if its not expired + if (url.searchParams.has("ex") && url.searchParams.has("is")) { + // extract the ex and is + const ex = url.searchParams.get("ex"); + const is = url.searchParams.get("is"); + + if (!isExpired(ex as string, is as string)) { + // if the signature is not expired, return the url as is + return attachmentUrl; + } + } + + let path = url.pathname; + // strip / from the start + if (path.startsWith("/")) { + path = path.slice(1); + } + + const { hash, issuedAt, expiresAt } = getUrlSignature(path); + url.searchParams.set("ex", expiresAt); + url.searchParams.set("is", issuedAt); + url.searchParams.set("hm", hash); + + return url.toString(); +}; diff --git a/src/util/config/types/SecurityConfiguration.ts b/src/util/config/types/SecurityConfiguration.ts index 38aab6f8..fb8b550e 100644 --- a/src/util/config/types/SecurityConfiguration.ts +++ b/src/util/config/types/SecurityConfiguration.ts @@ -37,4 +37,8 @@ export class SecurityConfiguration { mfaBackupCodeCount: number = 10; statsWorldReadable: boolean = true; defaultRegistrationTokenExpiration: number = 1000 * 60 * 60 * 24 * 7; //1 week + // cdn signed urls + cdnSignUrls: boolean = false; + cdnSignatureKey: string = crypto.randomBytes(32).toString("base64"); + cdnSignatureDuration: string = "24h"; } diff --git a/src/util/index.ts b/src/util/index.ts index c3d32bba..dba69812 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -16,6 +16,8 @@ along with this program. If not, see . */ +// NOTE: !! DO NOT REORDER THE IMPORTS !! + import "reflect-metadata"; export * from "./util/index"; @@ -26,3 +28,4 @@ export * from "./schemas"; export * from "./imports"; export * from "./config"; export * from "./connections"; +export * from "./Signing" \ No newline at end of file diff --git a/src/util/schemas/RefreshUrlsRequestSchema.ts b/src/util/schemas/RefreshUrlsRequestSchema.ts new file mode 100644 index 00000000..9c1df548 --- /dev/null +++ b/src/util/schemas/RefreshUrlsRequestSchema.ts @@ -0,0 +1,21 @@ +/* + 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 . +*/ + +export interface RefreshUrlsRequestSchema { + attachment_urls: string[]; +} diff --git a/src/util/schemas/index.ts b/src/util/schemas/index.ts index f2d5844b..9701faec 100644 --- a/src/util/schemas/index.ts +++ b/src/util/schemas/index.ts @@ -59,6 +59,7 @@ export * from "./MfaCodesSchema"; export * from "./ModifyGuildStickerSchema"; export * from "./PasswordResetSchema"; export * from "./PurgeSchema"; +export * from "./RefreshUrlsRequestSchema"; export * from "./RegisterSchema"; export * from "./RelationshipPostSchema"; export * from "./RelationshipPutSchema"; diff --git a/src/util/schemas/responses/RefreshUrlsResponse.ts b/src/util/schemas/responses/RefreshUrlsResponse.ts new file mode 100644 index 00000000..b08efaa4 --- /dev/null +++ b/src/util/schemas/responses/RefreshUrlsResponse.ts @@ -0,0 +1,26 @@ +/* + 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 . +*/ + +export interface RefreshedUrl { + original: string; + refreshed: string; +} + +export interface RefreshUrlsResponse { + refreshed_urls: RefreshedUrl[]; +} diff --git a/src/util/schemas/responses/index.ts b/src/util/schemas/responses/index.ts index d65c7404..856b4a3e 100644 --- a/src/util/schemas/responses/index.ts +++ b/src/util/schemas/responses/index.ts @@ -44,6 +44,7 @@ export * from "./InstanceStatsResponse"; export * from "./LocationMetadataResponse"; export * from "./MemberJoinGuildResponse"; export * from "./OAuthAuthorizeResponse"; +export * from "./RefreshUrlsResponse"; export * from "./TeamListResponse"; export * from "./Tenor"; export * from "./TokenResponse";