feat(media-proxy): sign attachment URLs and gate origins (#2830)

This commit is contained in:
Hampus
2026-09-18 15:57:32 +02:00
committed by GitHub
parent 025c01ab13
commit 522cf08e61
108 changed files with 10148 additions and 441 deletions
+26
View File
@@ -144,6 +144,9 @@ function defaultConfig(): MasterConfig {
token_ttl_secs: 900,
keep_direct_countries: [],
},
attachment_urls: {
secrets_base64: [],
},
},
gateway: {
port: 8771,
@@ -354,6 +357,28 @@ function validateUploadRelaySecret(value: string, mode: string): void {
}
}
function isCanonicalStandardBase64(value: string): boolean {
if (!/^[A-Za-z0-9+/]+={0,2}$/u.test(value)) {
return false;
}
return Buffer.from(value, 'base64').toString('base64') === value;
}
function validateAttachmentUrlSecrets(values: Array<string>): void {
for (const value of values) {
const trimmed = value.trim();
if (trimmed.length === 0) {
continue;
}
if (!isCanonicalStandardBase64(trimmed)) {
throw new Error('FLUXER_MEDIA_PROXY_ATTACHMENT_URL_SECRETS_BASE64 entries must be standard base64');
}
if (Buffer.from(trimmed, 'base64').length < 32) {
throw new Error('FLUXER_MEDIA_PROXY_ATTACHMENT_URL_SECRETS_BASE64 entries must decode to at least 32 bytes');
}
}
}
function assertBoolean(value: unknown, envName: string): asserts value is boolean {
if (typeof value !== 'boolean') {
throw new Error(`${envName} must be true or false`);
@@ -575,6 +600,7 @@ function normalizeConfig(config: MasterConfig): MasterConfig {
requireString(config.s3?.secret_access_key, 'FLUXER_S3_SECRET_ACCESS_KEY');
requireString(config.services.media_proxy.secret_key, 'FLUXER_MEDIA_PROXY_SECRET_KEY');
validateUploadRelaySecret(config.services.media_proxy.upload_relay.secret_base64, config.services.media_proxy.mode);
validateAttachmentUrlSecrets(config.services.media_proxy.attachment_urls.secrets_base64);
requireString(config.services.admin.secret_key_base, 'FLUXER_ADMIN_SECRET_KEY_BASE');
requireString(config.services.admin.oauth_client_secret, 'FLUXER_ADMIN_OAUTH_CLIENT_SECRET');
requireString(config.services.gateway.rpc_auth_token, 'FLUXER_GATEWAY_RPC_AUTH_TOKEN');
+3
View File
@@ -152,6 +152,9 @@ export interface MasterConfig {
token_ttl_secs: number;
keep_direct_countries: Array<string>;
};
attachment_urls: {
secrets_base64: Array<string>;
};
};
gateway: {
port: number;
@@ -172,6 +172,10 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record<string, NamedEnvOverride> = {
path: ['services', 'media_proxy', 'upload_relay', 'keep_direct_countries'],
parse: parseCsv,
},
FLUXER_MEDIA_PROXY_ATTACHMENT_URL_SECRETS_BASE64: {
path: ['services', 'media_proxy', 'attachment_urls', 'secrets_base64'],
parse: parseCsv,
},
FLUXER_ADMIN_PORT: {path: ['services', 'admin', 'port'], parse: parseInteger},
FLUXER_ADMIN_BASE_PATH: {path: ['services', 'admin', 'base_path']},
FLUXER_ADMIN_SECRET_KEY_BASE: {path: ['services', 'admin', 'secret_key_base']},
@@ -54,3 +54,30 @@ export const ClientAttachmentReferenceRequest = ClientAttachmentBase.extend({
});
export type ClientAttachmentReferenceRequest = z.infer<typeof ClientAttachmentReferenceRequest>;
export const RefreshAttachmentUrlsRequest = z.object({
attachment_urls: z
.array(z.string().max(2048))
.min(1)
.max(50)
.describe('Attachment URLs to refresh (1-50 entries, each at most 2048 characters)'),
});
export type RefreshAttachmentUrlsRequest = z.infer<typeof RefreshAttachmentUrlsRequest>;
export const RefreshedAttachmentUrl = z.object({
original: z.string().describe('The requested URL, echoed back unchanged'),
refreshed: z
.string()
.describe('The same URL carrying a fresh signature, or the original when it is not an attachment URL of ours'),
});
export type RefreshedAttachmentUrl = z.infer<typeof RefreshedAttachmentUrl>;
export const RefreshAttachmentUrlsResponse = z.object({
refreshed_urls: z
.array(RefreshedAttachmentUrl)
.describe('One entry per requested URL, in the order they were requested'),
});
export type RefreshAttachmentUrlsResponse = z.infer<typeof RefreshAttachmentUrlsResponse>;