Make IP/user-agent validation in CDN work
This commit is contained in:
parent
8bf45dcdc1
commit
2e196573ea
@ -17,7 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { route } from "@spacebar/api";
|
import { route } from "@spacebar/api";
|
||||||
import { RefreshUrlsRequestSchema, resignUrl } from "@spacebar/util";
|
import { RefreshUrlsRequestSchema, getUrlSignature, NewUrlSignatureData } from "@spacebar/util";
|
||||||
import { Request, Response, Router } from "express";
|
import { Request, Response, Router } from "express";
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@ -37,7 +37,13 @@ router.post(
|
|||||||
async (req: Request, res: Response) => {
|
async (req: Request, res: Response) => {
|
||||||
const { attachment_urls } = req.body as RefreshUrlsRequestSchema;
|
const { attachment_urls } = req.body as RefreshUrlsRequestSchema;
|
||||||
|
|
||||||
const refreshed_urls = attachment_urls.map(url => resignUrl(url, req));
|
const refreshed_urls = attachment_urls.map(url => {
|
||||||
|
return getUrlSignature(new NewUrlSignatureData({
|
||||||
|
url: url,
|
||||||
|
ip: req.ip,
|
||||||
|
userAgent: req.headers["user-agent"] as string
|
||||||
|
})).applyToUrl(url).toString();
|
||||||
|
});
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
refreshed_urls,
|
refreshed_urls,
|
||||||
|
|||||||
@ -30,7 +30,7 @@ import {
|
|||||||
emitEvent,
|
emitEvent,
|
||||||
getPermission,
|
getPermission,
|
||||||
getRights,
|
getRights,
|
||||||
uploadFile, Config, getUrlSignature,
|
uploadFile
|
||||||
} from "@spacebar/util";
|
} from "@spacebar/util";
|
||||||
import { Request, Response, Router } from "express";
|
import { Request, Response, Router } from "express";
|
||||||
import { HTTPError } from "lambert-server";
|
import { HTTPError } from "lambert-server";
|
||||||
|
|||||||
@ -35,8 +35,8 @@ import {
|
|||||||
emitEvent,
|
emitEvent,
|
||||||
getPermission,
|
getPermission,
|
||||||
isTextChannel,
|
isTextChannel,
|
||||||
resignUrl,
|
getUrlSignature,
|
||||||
uploadFile,
|
uploadFile, NewUrlSignatureData,
|
||||||
} from "@spacebar/util";
|
} from "@spacebar/util";
|
||||||
import { Request, Response, Router } from "express";
|
import { Request, Response, Router } from "express";
|
||||||
import { HTTPError } from "lambert-server";
|
import { HTTPError } from "lambert-server";
|
||||||
@ -210,8 +210,17 @@ router.get(
|
|||||||
|
|
||||||
y.proxy_url = url.toString();
|
y.proxy_url = url.toString();
|
||||||
|
|
||||||
y.proxy_url = resignUrl(y.proxy_url, req);
|
y.proxy_url = getUrlSignature(new NewUrlSignatureData({
|
||||||
y.url = resignUrl(y.url, req);
|
url: y.proxy_url,
|
||||||
|
userAgent: req.headers["user-agent"],
|
||||||
|
ip: req.ip,
|
||||||
|
})).applyToUrl(y.proxy_url).toString();
|
||||||
|
|
||||||
|
y.url = getUrlSignature(new NewUrlSignatureData({
|
||||||
|
url: y.url,
|
||||||
|
userAgent: req.headers["user-agent"],
|
||||||
|
ip: req.ip,
|
||||||
|
})).applyToUrl(y.url).toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -18,9 +18,8 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
Config,
|
Config,
|
||||||
getUrlSignature,
|
hasValidSignature, NewUrlUserSignatureData,
|
||||||
hasValidSignature,
|
Snowflake, UrlSignResult,
|
||||||
Snowflake,
|
|
||||||
} from "@spacebar/util";
|
} from "@spacebar/util";
|
||||||
import { Request, Response, Router } from "express";
|
import { Request, Response, Router } from "express";
|
||||||
import FileType from "file-type";
|
import FileType from "file-type";
|
||||||
@ -94,9 +93,16 @@ router.get(
|
|||||||
|
|
||||||
const path = `attachments/${channel_id}/${id}/${filename}`;
|
const path = `attachments/${channel_id}/${id}/${filename}`;
|
||||||
|
|
||||||
|
const fullUrl = (req.headers["x-forwarded-proto"] ?? req.protocol) + "://"
|
||||||
|
+ (req.headers["x-forwarded-host"] ?? req.hostname)
|
||||||
|
+ req.originalUrl;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
Config.get().security.cdnSignUrls &&
|
Config.get().security.cdnSignUrls &&
|
||||||
!hasValidSignature(path, req.query, req)
|
!hasValidSignature(new NewUrlUserSignatureData({
|
||||||
|
ip: req.ip,
|
||||||
|
userAgent: req.headers["user-agent"] as string,
|
||||||
|
}), UrlSignResult.fromUrl(fullUrl))
|
||||||
) {
|
) {
|
||||||
return res.status(404).send("This content is no longer available.");
|
return res.status(404).send("This content is no longer available.");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,7 +28,6 @@ import { Message } from "./Message";
|
|||||||
import { Deflate, Inflate } from "fast-zlib";
|
import { Deflate, Inflate } from "fast-zlib";
|
||||||
import { URL } from "url";
|
import { URL } from "url";
|
||||||
import { Config, ErlpackType } from "@spacebar/util";
|
import { Config, ErlpackType } from "@spacebar/util";
|
||||||
import { Request } from "express";
|
|
||||||
|
|
||||||
let erlpack: ErlpackType | null = null;
|
let erlpack: ErlpackType | null = null;
|
||||||
try {
|
try {
|
||||||
@ -48,11 +47,28 @@ export async function Connection(
|
|||||||
) {
|
) {
|
||||||
const forwardedFor = Config.get().security.forwardedFor;
|
const forwardedFor = Config.get().security.forwardedFor;
|
||||||
const ipAddress = forwardedFor
|
const ipAddress = forwardedFor
|
||||||
? (request.headers[forwardedFor] as string)
|
? (request.headers[forwardedFor.toLowerCase()] as string)
|
||||||
: request.socket.remoteAddress;
|
: request.socket.remoteAddress;
|
||||||
|
|
||||||
socket.ipAddress = ipAddress;
|
socket.ipAddress = ipAddress;
|
||||||
socket.request = request as unknown as Request;
|
socket.userAgent = request.headers["user-agent"] as string;
|
||||||
|
|
||||||
|
if (!ipAddress && Config.get().security.cdnSignatureIncludeIp) {
|
||||||
|
return socket.close(
|
||||||
|
CLOSECODES.Decode_error,
|
||||||
|
"Gateway connection rejected: IP address is required.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!socket.userAgent &&
|
||||||
|
Config.get().security.cdnSignatureIncludeUserAgent
|
||||||
|
) {
|
||||||
|
return socket.close(
|
||||||
|
CLOSECODES.Decode_error,
|
||||||
|
"Gateway connection rejected: User-Agent header is required.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
//Create session ID when the connection is opened. This allows gateway dump to group the initial websocket messages with the rest of the conversation.
|
//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();
|
const session_id = genSessionId();
|
||||||
@ -66,9 +82,9 @@ export async function Connection(
|
|||||||
|
|
||||||
socket.on("error", (err) => console.error("[Gateway]", err));
|
socket.on("error", (err) => console.error("[Gateway]", err));
|
||||||
|
|
||||||
// console.log(
|
console.log(
|
||||||
// `[Gateway] New connection from ${socket.ipAddress}, total ${this.clients.size}`,
|
`[Gateway] New connection from ${ipAddress}, total ${this.clients.size}`,
|
||||||
// );
|
);
|
||||||
|
|
||||||
if (process.env.WS_LOGEVENTS)
|
if (process.env.WS_LOGEVENTS)
|
||||||
[
|
[
|
||||||
|
|||||||
@ -27,7 +27,7 @@ import {
|
|||||||
EVENTEnum,
|
EVENTEnum,
|
||||||
Relationship,
|
Relationship,
|
||||||
RelationshipType,
|
RelationshipType,
|
||||||
Message,
|
Message, NewUrlUserSignatureData,
|
||||||
} from "@spacebar/util";
|
} from "@spacebar/util";
|
||||||
import { OPCODES } from "../util/Constants";
|
import { OPCODES } from "../util/Constants";
|
||||||
import { Send } from "../util/Send";
|
import { Send } from "../util/Send";
|
||||||
@ -290,8 +290,12 @@ async function consume(this: WebSocket, opts: EventOpts) {
|
|||||||
switch (event) {
|
switch (event) {
|
||||||
case "MESSAGE_CREATE":
|
case "MESSAGE_CREATE":
|
||||||
case "MESSAGE_UPDATE":
|
case "MESSAGE_UPDATE":
|
||||||
|
// console.log(this.request)
|
||||||
if(data["attachments"])
|
if(data["attachments"])
|
||||||
data["attachments"] = Message.prototype.withSignedAttachments.call(data, this.request).attachments;
|
data["attachments"] = Message.prototype.withSignedAttachments.call(data, new NewUrlUserSignatureData({
|
||||||
|
ip: this.ipAddress,
|
||||||
|
userAgent: this.userAgent
|
||||||
|
})).attachments;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -29,6 +29,7 @@ export interface WebSocket extends WS {
|
|||||||
encoding: "etf" | "json";
|
encoding: "etf" | "json";
|
||||||
compress?: "zlib-stream";
|
compress?: "zlib-stream";
|
||||||
ipAddress?: string;
|
ipAddress?: string;
|
||||||
|
userAgent?: string; // for cdn request signing
|
||||||
shard_count?: bigint;
|
shard_count?: bigint;
|
||||||
shard_id?: bigint;
|
shard_id?: bigint;
|
||||||
deflate?: Deflate;
|
deflate?: Deflate;
|
||||||
@ -43,9 +44,4 @@ export interface WebSocket extends WS {
|
|||||||
listen_options: ListenEventOpts;
|
listen_options: ListenEventOpts;
|
||||||
capabilities?: Capabilities;
|
capabilities?: Capabilities;
|
||||||
large_threshold: number;
|
large_threshold: number;
|
||||||
/**
|
|
||||||
* The request object for the WebSocket connection.
|
|
||||||
* WARNING: This is not a proper Express Request object, it may be missing expected properties.
|
|
||||||
*/
|
|
||||||
request: Request; // For signed attachment URLs
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,14 +19,105 @@
|
|||||||
import { Config } from "@spacebar/util";
|
import { Config } from "@spacebar/util";
|
||||||
import { createHmac, timingSafeEqual } from "crypto";
|
import { createHmac, timingSafeEqual } from "crypto";
|
||||||
import ms, { StringValue } from "ms";
|
import ms, { StringValue } from "ms";
|
||||||
import { ParsedQs } from "qs";
|
import * as console from "node:console";
|
||||||
import { Request } from "express";
|
|
||||||
import attachments from "../cdn/routes/attachments";
|
|
||||||
|
|
||||||
export const getUrlSignature = (
|
export class NewUrlUserSignatureData {
|
||||||
path: string,
|
ip?: string;
|
||||||
req: Request,
|
userAgent?: string;
|
||||||
) => {
|
constructor(data: NewUrlUserSignatureData) {
|
||||||
|
this.ip = data.ip;
|
||||||
|
this.userAgent = data.userAgent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NewUrlSignatureData extends NewUrlUserSignatureData {
|
||||||
|
path?: string;
|
||||||
|
url?: string;
|
||||||
|
|
||||||
|
constructor(data: NewUrlSignatureData) {
|
||||||
|
super(data);
|
||||||
|
this.path = data.path;
|
||||||
|
this.url = data.url;
|
||||||
|
if (!this.path && !this.url) {
|
||||||
|
throw new Error("Either path or url must be provided for URL signing");
|
||||||
|
}
|
||||||
|
if (this.path && this.url) {
|
||||||
|
console.warn(
|
||||||
|
"[Signing] Both path and url are provided, using path for signing",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (this.url) {
|
||||||
|
try {
|
||||||
|
const parsedUrl = new URL(this.url);
|
||||||
|
this.path = parsedUrl.pathname;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error("Invalid URL provided for signing: " + this.url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UrlSignatureData extends NewUrlSignatureData {
|
||||||
|
issuedAt: string;
|
||||||
|
expiresAt: string;
|
||||||
|
|
||||||
|
constructor(data: UrlSignatureData) {
|
||||||
|
super(data);
|
||||||
|
this.issuedAt = data.issuedAt;
|
||||||
|
this.expiresAt = data.expiresAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UrlSignResult {
|
||||||
|
path: string;
|
||||||
|
hash: string;
|
||||||
|
issuedAt: string;
|
||||||
|
expiresAt: string;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @param data {UrlSignResult}
|
||||||
|
*/
|
||||||
|
constructor(data: any) {
|
||||||
|
for (const key in data) {
|
||||||
|
// @ts-ignore TS7053 - We dont care about string indexing a class
|
||||||
|
this[key] = data[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyToUrl(url: URL | string): URL {
|
||||||
|
if (typeof url === "string") {
|
||||||
|
url = new URL(url);
|
||||||
|
}
|
||||||
|
url.searchParams.set("ex", this.expiresAt);
|
||||||
|
url.searchParams.set("is", this.issuedAt);
|
||||||
|
url.searchParams.set("hm", this.hash);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromUrl(url: URL | string): UrlSignResult {
|
||||||
|
if (typeof url === "string") {
|
||||||
|
console.debug("[Signing] Parsing URL from string:", url);
|
||||||
|
url = new URL(url);
|
||||||
|
}
|
||||||
|
console.debug("[Signing] Parsing URL from URL object:", url.toString());
|
||||||
|
const ex = url.searchParams.get("ex");
|
||||||
|
const is = url.searchParams.get("is");
|
||||||
|
const hm = url.searchParams.get("hm");
|
||||||
|
|
||||||
|
if (!ex || !is || !hm) {
|
||||||
|
throw new Error("Invalid URL signature parameters");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new UrlSignResult({
|
||||||
|
path: url.pathname,
|
||||||
|
issuedAt: is,
|
||||||
|
expiresAt: ex,
|
||||||
|
hash: hm,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getUrlSignature = (data: NewUrlSignatureData): UrlSignResult => {
|
||||||
const { cdnSignatureKey, cdnSignatureDuration } = Config.get().security;
|
const { cdnSignatureKey, cdnSignatureDuration } = Config.get().security;
|
||||||
|
|
||||||
// calculate the expiration time
|
// calculate the expiration time
|
||||||
@ -37,135 +128,142 @@ export const getUrlSignature = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
// hash the url with the cdnSignatureKey
|
// hash the url with the cdnSignatureKey
|
||||||
const hash = calculateHash(path, issuedAt, expiresAt, req);
|
return calculateHash(
|
||||||
|
new UrlSignatureData({
|
||||||
return {
|
...data,
|
||||||
hash,
|
issuedAt,
|
||||||
issuedAt,
|
expiresAt,
|
||||||
expiresAt,
|
}),
|
||||||
};
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const calculateHash = (
|
function calculateHash(request: UrlSignatureData): UrlSignResult {
|
||||||
url: string,
|
|
||||||
issuedAt: string,
|
|
||||||
expiresAt: string,
|
|
||||||
req: Request,
|
|
||||||
) => {
|
|
||||||
const { cdnSignatureKey } = Config.get().security;
|
const { cdnSignatureKey } = Config.get().security;
|
||||||
const data = createHmac("sha256", cdnSignatureKey as string)
|
const data = createHmac("sha256", cdnSignatureKey as string)
|
||||||
.update(url)
|
.update(request.path!)
|
||||||
.update(issuedAt)
|
.update(request.issuedAt)
|
||||||
.update(expiresAt);
|
.update(request.expiresAt);
|
||||||
|
|
||||||
if (Config.get().security.cdnSignatureIncludeIp) {
|
if (Config.get().security.cdnSignatureIncludeIp) {
|
||||||
if (!req || !req.ip)
|
if (!request.ip)
|
||||||
console.log(
|
console.log(
|
||||||
"[Signing] CDN Signature IP is enabled but no request object was provided. This may cause issues with signature validation. Please report this to the Spacebar team!",
|
"[Signing] CDN Signature IP is enabled but we couldn't find the IP field in the request. This may cause issues with signature validation. Please report this to the Spacebar team!",
|
||||||
);
|
);
|
||||||
console.log("[Signing] CDN Signature IP is enabled, adding IP to hash:", req.ip);
|
else {
|
||||||
data.update(req.ip!);
|
console.log(
|
||||||
|
"[Signing] CDN Signature IP is enabled, adding IP to hash:",
|
||||||
|
request.ip,
|
||||||
|
);
|
||||||
|
data.update(request.ip!);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Config.get().security.cdnSignatureIncludeUserAgent) {
|
if (Config.get().security.cdnSignatureIncludeUserAgent) {
|
||||||
if (!req || !req.headers || !req.headers["user-agent"])
|
if (!request.userAgent)
|
||||||
console.log(
|
console.log(
|
||||||
"[Signing] CDN Signature User-Agent is enabled but no request object was provided. This may cause issues with signature validation. Please report this to the Spacebar team!",
|
"[Signing] CDN Signature User-Agent is enabled but we couldn't find the user-agent header in the request. This may cause issues with signature validation. Please report this to the Spacebar team!",
|
||||||
);
|
);
|
||||||
data.update(req.headers["user-agent"] as string);
|
else {
|
||||||
|
console.log(
|
||||||
|
"[Signing] CDN Signature User-Agent is enabled, adding User-Agent to hash:",
|
||||||
|
request.userAgent,
|
||||||
|
);
|
||||||
|
data.update(request.userAgent!);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const hash = data.digest("hex");
|
const hash = data.digest("hex");
|
||||||
|
const result = new UrlSignResult({
|
||||||
|
path: request.path,
|
||||||
|
issuedAt: request.issuedAt,
|
||||||
|
expiresAt: request.expiresAt,
|
||||||
|
hash,
|
||||||
|
});
|
||||||
console.log("[Signing]", {
|
console.log("[Signing]", {
|
||||||
url,
|
path: request.path,
|
||||||
issuedAt,
|
validity: request.issuedAt + " .. " + request.expiresAt,
|
||||||
expiresAt,
|
ua: Config.get().security.cdnSignatureIncludeUserAgent ? request.userAgent : "[disabled]",
|
||||||
includeUA: Config.get().security.cdnSignatureIncludeUserAgent,
|
ip: Config.get().security.cdnSignatureIncludeIp ? request.ip : "[disabled]"
|
||||||
ua: req.headers["user-agent"],
|
}, "->", result);
|
||||||
includeIP: Config.get().security.cdnSignatureIncludeIp,
|
return result;
|
||||||
ip: req.ip,
|
}
|
||||||
}, "->", hash);
|
|
||||||
return hash;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isExpired = (ex: string, is: string) => {
|
export const isExpired = (data: UrlSignResult | UrlSignatureData) => {
|
||||||
// convert issued at
|
// convert issued at
|
||||||
const issuedAt = parseInt(is, 16);
|
const issuedAt = parseInt(data.issuedAt, 16);
|
||||||
const expiresAt = parseInt(ex, 16);
|
const expiresAt = parseInt(data.expiresAt, 16);
|
||||||
|
|
||||||
if (Number.isNaN(issuedAt) || Number.isNaN(expiresAt)) {
|
if (Number.isNaN(issuedAt) || Number.isNaN(expiresAt)) {
|
||||||
// console.debug("Invalid timestamps in query");
|
console.debug("[Signing] Invalid timestamps in query");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentTime = Date.now();
|
const currentTime = Date.now();
|
||||||
|
|
||||||
const isExpired = expiresAt < currentTime;
|
const isExpired = expiresAt < currentTime;
|
||||||
|
if (isExpired) {
|
||||||
|
console.debug("[Signing] Signature expired");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const isValidIssuedAt = issuedAt < currentTime;
|
const isValidIssuedAt = issuedAt < currentTime;
|
||||||
if (isExpired || !isValidIssuedAt) {
|
if (!isValidIssuedAt) {
|
||||||
// console.debug("Signature expired");
|
console.debug("[Signing] Signature issued in the future");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hasValidSignature = (path: string, query: ParsedQs, req: Request) => {
|
export const hasValidSignature = (req: NewUrlUserSignatureData, sig: UrlSignResult) => {
|
||||||
// get url path
|
|
||||||
const { ex, is, hm } = query;
|
|
||||||
|
|
||||||
// if the required query parameters are not present, return false
|
// if the required query parameters are not present, return false
|
||||||
if (!ex || !is || !hm) {
|
if (!sig.expiresAt || !sig.issuedAt || !sig.hash) {
|
||||||
console.debug("Missing required query parameters for signature validation");
|
console.warn(
|
||||||
|
"[Signing] Missing required query parameters for signature validation",
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the signature is expired
|
// check if the signature is expired
|
||||||
if (isExpired(ex as string, is as string)) {
|
if (isExpired(sig)) {
|
||||||
console.debug("Signature is expired");
|
console.warn("[Signing] Signature is expired");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const calcd = calculateHash(path, is as string, ex as string, req);
|
const calcResult = calculateHash(new UrlSignatureData({
|
||||||
|
path: sig.path,
|
||||||
|
issuedAt: sig.issuedAt,
|
||||||
|
expiresAt: sig.expiresAt,
|
||||||
|
ip: req.ip,
|
||||||
|
userAgent: req.userAgent
|
||||||
|
}));
|
||||||
|
const calcd = calcResult.hash;
|
||||||
const calculated = Buffer.from(calcd);
|
const calculated = Buffer.from(calcd);
|
||||||
const received = Buffer.from(hm as string);
|
const received = Buffer.from(sig.hash as string);
|
||||||
|
|
||||||
|
console.assert(sig.issuedAt == calcResult.issuedAt, "[Signing] Mismatched issuedAt", {
|
||||||
|
is: sig.issuedAt,
|
||||||
|
calculated: calcResult.issuedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.assert(sig.expiresAt == calcResult.expiresAt, "[Signing] Mismatched expiresAt", {
|
||||||
|
ex: sig.expiresAt,
|
||||||
|
calculated: calcResult.expiresAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.assert(calculated.length === received.length, "[Signing] Mismatched hash length", {
|
||||||
|
calculated: calculated.length,
|
||||||
|
received: received.length,
|
||||||
|
});
|
||||||
|
|
||||||
const isHashValid =
|
const isHashValid =
|
||||||
calculated.length === received.length &&
|
calculated.length === received.length &&
|
||||||
timingSafeEqual(calculated, received);
|
timingSafeEqual(calculated, received);
|
||||||
|
|
||||||
console.debug(
|
if (!isHashValid)
|
||||||
`Signature validation for ${path} - isHashValid: ${isHashValid}, calculated: ${calcd}, received: ${hm}`,
|
console.warn(
|
||||||
);
|
`Signature validation for ${sig.path} (is=${sig.issuedAt}, ex=${sig.expiresAt}) failed: calculated: ${calcd}, received: ${sig.hash}`,
|
||||||
|
);
|
||||||
|
|
||||||
return isHashValid;
|
return isHashValid;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resignUrl = (attachmentUrl: string, req: Request) => {
|
|
||||||
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, req);
|
|
||||||
url.searchParams.set("ex", expiresAt);
|
|
||||||
url.searchParams.set("is", issuedAt);
|
|
||||||
url.searchParams.set("hm", hash);
|
|
||||||
|
|
||||||
return url.toString();
|
|
||||||
};
|
|
||||||
|
|||||||
@ -28,8 +28,11 @@ import { URL } from "url";
|
|||||||
import { deleteFile } from "../util/cdn";
|
import { deleteFile } from "../util/cdn";
|
||||||
import { BaseClass } from "./BaseClass";
|
import { BaseClass } from "./BaseClass";
|
||||||
import { dbEngine } from "../util/Database";
|
import { dbEngine } from "../util/Database";
|
||||||
import { Request } from "express";
|
import {
|
||||||
import { resignUrl } from "../Signing";
|
getUrlSignature,
|
||||||
|
NewUrlUserSignatureData,
|
||||||
|
NewUrlSignatureData,
|
||||||
|
} from "../Signing";
|
||||||
|
|
||||||
@Entity({
|
@Entity({
|
||||||
name: "attachments",
|
name: "attachments",
|
||||||
@ -76,11 +79,15 @@ export class Attachment extends BaseClass {
|
|||||||
return deleteFile(new URL(this.url).pathname);
|
return deleteFile(new URL(this.url).pathname);
|
||||||
}
|
}
|
||||||
|
|
||||||
signUrls(req: Request) {
|
signUrls(data: NewUrlUserSignatureData): Attachment {
|
||||||
return {
|
return {
|
||||||
...this,
|
...this,
|
||||||
url: resignUrl(this.url, req),
|
url: getUrlSignature(
|
||||||
proxy_url: resignUrl(this.proxy_url, req),
|
new NewUrlSignatureData({ ...data, url: this.url }),
|
||||||
}
|
).applyToUrl(this.url).toString(),
|
||||||
|
proxy_url: getUrlSignature(
|
||||||
|
new NewUrlSignatureData({ ...data, url: this.proxy_url }),
|
||||||
|
).applyToUrl(this.proxy_url).toString(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,7 +40,7 @@ import { Webhook } from "./Webhook";
|
|||||||
import { Sticker } from "./Sticker";
|
import { Sticker } from "./Sticker";
|
||||||
import { Attachment } from "./Attachment";
|
import { Attachment } from "./Attachment";
|
||||||
import { dbEngine } from "../util/Database";
|
import { dbEngine } from "../util/Database";
|
||||||
import { Request } from "express";
|
import { NewUrlUserSignatureData } from "../Signing";
|
||||||
|
|
||||||
export enum MessageType {
|
export enum MessageType {
|
||||||
DEFAULT = 0,
|
DEFAULT = 0,
|
||||||
@ -262,11 +262,11 @@ export class Message extends BaseClass {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
withSignedAttachments(req: Request) {
|
withSignedAttachments(data: NewUrlUserSignatureData) {
|
||||||
return {
|
return {
|
||||||
...this,
|
...this,
|
||||||
attachments: this.attachments?.map((attachment: Attachment) =>
|
attachments: this.attachments?.map((attachment: Attachment) =>
|
||||||
Attachment.prototype.signUrls.call(attachment, req)
|
Attachment.prototype.signUrls.call(attachment, data)
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user