Add options to erquire IP or User Agent to access CDN
This commit is contained in:
parent
6fa604efd1
commit
0e0da6d722
@ -37,7 +37,7 @@ 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(resignUrl);
|
const refreshed_urls = attachment_urls.map(url => resignUrl(url, req));
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
refreshed_urls,
|
refreshed_urls,
|
||||||
|
|||||||
@ -210,8 +210,8 @@ router.get(
|
|||||||
|
|
||||||
y.proxy_url = url.toString();
|
y.proxy_url = url.toString();
|
||||||
|
|
||||||
y.proxy_url = resignUrl(y.proxy_url);
|
y.proxy_url = resignUrl(y.proxy_url, req);
|
||||||
y.url = resignUrl(y.url);
|
y.url = resignUrl(y.url, req);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -72,7 +72,7 @@ router.post(
|
|||||||
let finalUrl = `${endpoint}/${path}`;
|
let finalUrl = `${endpoint}/${path}`;
|
||||||
|
|
||||||
if (Config.get().security.cdnSignUrls) {
|
if (Config.get().security.cdnSignUrls) {
|
||||||
const signatureData = getUrlSignature(path);
|
const signatureData = getUrlSignature(path, req);
|
||||||
finalUrl = `${finalUrl}?ex=${signatureData.expiresAt}&is=${signatureData.issuedAt}&hm=${signatureData.hash}`;
|
finalUrl = `${finalUrl}?ex=${signatureData.expiresAt}&is=${signatureData.issuedAt}&hm=${signatureData.hash}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,7 +100,7 @@ router.get(
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
Config.get().security.cdnSignUrls &&
|
Config.get().security.cdnSignUrls &&
|
||||||
!hasValidSignature(path, req.query)
|
!hasValidSignature(path, req.query, req)
|
||||||
) {
|
) {
|
||||||
return res.status(404).send("This content is no longer available.");
|
return res.status(404).send("This content is no longer available.");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,8 +20,12 @@ 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 { ParsedQs } from "qs";
|
||||||
|
import { Request } from "express";
|
||||||
|
|
||||||
export const getUrlSignature = (path: string) => {
|
export const getUrlSignature = (
|
||||||
|
path: string,
|
||||||
|
req: Request,
|
||||||
|
) => {
|
||||||
const { cdnSignatureKey, cdnSignatureDuration } = Config.get().security;
|
const { cdnSignatureKey, cdnSignatureDuration } = Config.get().security;
|
||||||
|
|
||||||
// calculate the expiration time
|
// calculate the expiration time
|
||||||
@ -32,11 +36,7 @@ export const getUrlSignature = (path: string) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// hash the url with the cdnSignatureKey
|
// hash the url with the cdnSignatureKey
|
||||||
const hash = createHmac("sha256", cdnSignatureKey as string)
|
const hash = calculateHash(path, issuedAt, expiresAt, req);
|
||||||
.update(path)
|
|
||||||
.update(issuedAt)
|
|
||||||
.update(expiresAt)
|
|
||||||
.digest("hex");
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hash,
|
hash,
|
||||||
@ -49,14 +49,31 @@ export const calculateHash = (
|
|||||||
url: string,
|
url: string,
|
||||||
issuedAt: string,
|
issuedAt: string,
|
||||||
expiresAt: string,
|
expiresAt: string,
|
||||||
|
req: Request,
|
||||||
) => {
|
) => {
|
||||||
const { cdnSignatureKey } = Config.get().security;
|
const { cdnSignatureKey } = Config.get().security;
|
||||||
const hash = createHmac("sha256", cdnSignatureKey as string)
|
const hash = createHmac("sha256", cdnSignatureKey as string)
|
||||||
.update(url)
|
.update(url)
|
||||||
.update(issuedAt)
|
.update(issuedAt)
|
||||||
.update(expiresAt)
|
.update(expiresAt);
|
||||||
.digest("hex");
|
|
||||||
return hash;
|
if (Config.get().security.cdnSignatureIncludeIp) {
|
||||||
|
if (!req || !req.ip)
|
||||||
|
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!",
|
||||||
|
);
|
||||||
|
hash.update(req.ip!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Config.get().security.cdnSignatureIncludeUserAgent) {
|
||||||
|
if (!req || !req.headers || !req.headers["user-agent"])
|
||||||
|
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!",
|
||||||
|
);
|
||||||
|
hash.update(req.headers["user-agent"] as string);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash.digest("hex");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isExpired = (ex: string, is: string) => {
|
export const isExpired = (ex: string, is: string) => {
|
||||||
@ -80,7 +97,7 @@ export const isExpired = (ex: string, is: string) => {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hasValidSignature = (path: string, query: ParsedQs) => {
|
export const hasValidSignature = (path: string, query: ParsedQs, req: Request) => {
|
||||||
// get url path
|
// get url path
|
||||||
const { ex, is, hm } = query;
|
const { ex, is, hm } = query;
|
||||||
|
|
||||||
@ -92,21 +109,18 @@ export const hasValidSignature = (path: string, query: ParsedQs) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const calcd = calculateHash(path, is as string, ex as string);
|
const calcd = calculateHash(path, is as string, ex as string, req);
|
||||||
const calculated = Buffer.from(calcd);
|
const calculated = Buffer.from(calcd);
|
||||||
const received = Buffer.from(hm as string);
|
const received = Buffer.from(hm as string);
|
||||||
|
|
||||||
const isHashValid =
|
const isHashValid =
|
||||||
calculated.length === received.length &&
|
calculated.length === received.length &&
|
||||||
timingSafeEqual(calculated, received);
|
timingSafeEqual(calculated, received);
|
||||||
// if (!isHashValid) {
|
|
||||||
// console.debug("Invalid signature");
|
|
||||||
// console.debug(calcd, hm);
|
|
||||||
// }
|
|
||||||
return isHashValid;
|
return isHashValid;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resignUrl = (attachmentUrl: string) => {
|
export const resignUrl = (attachmentUrl: string, req: Request) => {
|
||||||
const url = new URL(attachmentUrl);
|
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 theres an existing signature, check if its expired or not. no reason to resign if its not expired
|
||||||
@ -127,7 +141,7 @@ export const resignUrl = (attachmentUrl: string) => {
|
|||||||
path = path.slice(1);
|
path = path.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { hash, issuedAt, expiresAt } = getUrlSignature(path);
|
const { hash, issuedAt, expiresAt } = getUrlSignature(path, req);
|
||||||
url.searchParams.set("ex", expiresAt);
|
url.searchParams.set("ex", expiresAt);
|
||||||
url.searchParams.set("is", issuedAt);
|
url.searchParams.set("is", issuedAt);
|
||||||
url.searchParams.set("hm", hash);
|
url.searchParams.set("hm", hash);
|
||||||
|
|||||||
@ -22,9 +22,5 @@ export class CdnConfiguration extends EndpointConfiguration {
|
|||||||
resizeHeightMax: number = 1000;
|
resizeHeightMax: number = 1000;
|
||||||
resizeWidthMax: number = 1000;
|
resizeWidthMax: number = 1000;
|
||||||
imagorServerUrl: string | null = null;
|
imagorServerUrl: string | null = null;
|
||||||
|
|
||||||
endpointPublic: string | null = null;
|
|
||||||
endpointPrivate: string | null = null;
|
|
||||||
|
|
||||||
proxyCacheHeaderSeconds: number = 60 * 60 * 24;
|
proxyCacheHeaderSeconds: number = 60 * 60 * 24;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,4 +41,6 @@ export class SecurityConfiguration {
|
|||||||
cdnSignUrls: boolean = false;
|
cdnSignUrls: boolean = false;
|
||||||
cdnSignatureKey: string = crypto.randomBytes(32).toString("base64");
|
cdnSignatureKey: string = crypto.randomBytes(32).toString("base64");
|
||||||
cdnSignatureDuration: string = "24h";
|
cdnSignatureDuration: string = "24h";
|
||||||
|
cdnSignatureIncludeIp: boolean = true;
|
||||||
|
cdnSignatureIncludeUserAgent: boolean = true;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user