refactor progress

This commit is contained in:
Hampus Kraft
2026-02-17 12:22:36 +00:00
parent cb31608523
commit d5abd1a7e4
8257 changed files with 1190207 additions and 761040 deletions

View File

@@ -0,0 +1,39 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer 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.
*
* Fluxer 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 Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {timingSafeEqual} from 'node:crypto';
import type {HonoEnv} from '@fluxer/media_proxy/src/types/HonoEnv';
import {createMiddleware} from 'hono/factory';
import {HTTPException} from 'hono/http-exception';
export function createInternalNetworkRequired(secretKey: string) {
return createMiddleware<HonoEnv>(async (ctx, next) => {
const authHeader = ctx.req.header('Authorization');
const expectedAuth = `Bearer ${secretKey}`;
if (!authHeader) {
throw new HTTPException(401, {message: 'Unauthorized'});
}
const authBuffer = Buffer.from(authHeader, 'utf8');
const expectedBuffer = Buffer.from(expectedAuth, 'utf8');
if (authBuffer.length !== expectedBuffer.length || !timingSafeEqual(authBuffer, expectedBuffer)) {
throw new HTTPException(401, {message: 'Unauthorized'});
}
await next();
});
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer 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.
*
* Fluxer 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 Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {LoggerInterface} from '@fluxer/logger/src/LoggerInterface';
import type {CloudflareEdgeIPService} from '@fluxer/media_proxy/src/lib/CloudflareEdgeIPService';
import type {HonoEnv} from '@fluxer/media_proxy/src/types/HonoEnv';
import {createMiddleware} from 'hono/factory';
import {HTTPException} from 'hono/http-exception';
interface CloudflareFirewallOptions {
enabled: boolean;
exemptPaths?: Array<string>;
}
export function createCloudflareFirewall(
ipService: CloudflareEdgeIPService,
logger: LoggerInterface,
{enabled, exemptPaths = ['/_health', '/_metadata']}: CloudflareFirewallOptions,
) {
return createMiddleware<HonoEnv>(async (ctx, next) => {
if (!enabled) {
await next();
return;
}
const path = ctx.req.path;
if (exemptPaths.some((prefix) => path === prefix || path.startsWith(prefix))) {
await next();
return;
}
const xff = ctx.req.header('x-forwarded-for');
if (!xff) {
logger.warn({path}, 'Rejected request without X-Forwarded-For header');
throw new HTTPException(403, {message: 'Forbidden'});
}
const connectingIP = xff.split(',')[0]?.trim();
if (!connectingIP || !ipService.isFromCloudflareEdge(connectingIP)) {
logger.warn({connectingIP, path}, 'Rejected request from non-Cloudflare edge IP');
throw new HTTPException(403, {message: 'Forbidden'});
}
await next();
});
}

View File

@@ -0,0 +1,154 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer 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.
*
* Fluxer 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 Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {ErrorContext, ErrorType, HonoEnv} from '@fluxer/media_proxy/src/types/HonoEnv';
import type {MetricsInterface} from '@fluxer/media_proxy/src/types/Metrics';
import {createMiddleware} from 'hono/factory';
function getRouteFromPath(path: string): string | null {
if (path === '/_health' || path === '/internal/telemetry') return null;
if (path.startsWith('/avatars/')) return 'avatars';
if (path.startsWith('/icons/')) return 'icons';
if (path.startsWith('/banners/')) return 'banners';
if (path.startsWith('/emojis/')) return 'emojis';
if (path.startsWith('/stickers/')) return 'stickers';
if (path.startsWith('/attachments/')) return 'attachments';
if (path.startsWith('/external/')) return 'external';
if (path.startsWith('/guilds/')) return 'guild_assets';
return 'other';
}
function getErrorTypeFromStatus(status: number): ErrorType {
switch (status) {
case 400:
return 'bad_request';
case 401:
return 'unauthorized';
case 403:
return 'forbidden';
case 404:
return 'not_found';
case 408:
return 'timeout';
case 413:
return 'payload_too_large';
default:
if (status >= 500 && status < 600) {
return 'upstream_5xx';
}
return 'other';
}
}
export function createMetricsMiddleware(metrics: MetricsInterface) {
return createMiddleware<HonoEnv>(async (ctx, next) => {
const start = Date.now();
let errorType: ErrorType | undefined;
let errorSource: string | undefined;
try {
await next();
} catch (error) {
if (error instanceof Error) {
const message = error.message.toLowerCase();
if (message.includes('timeout') || message.includes('timed out') || message.includes('etimedout')) {
errorType = 'timeout';
errorSource = 'network';
} else if (
message.includes('econnrefused') ||
message.includes('econnreset') ||
message.includes('enotfound')
) {
errorType = 'upstream_5xx';
errorSource = 'network';
}
}
throw error;
} finally {
const duration = Date.now() - start;
const route = getRouteFromPath(ctx.req.path);
if (route !== null) {
const status = ctx.res.status;
const baseDimensions = {
'http.request.method': ctx.req.method,
'url.path': route,
'http.response.status_code': String(status),
};
metrics.histogram({
name: 'http.server.request.duration',
dimensions: baseDimensions,
valueMs: duration,
});
metrics.counter({
name: 'http.server.request.count',
dimensions: baseDimensions,
value: 1,
});
metrics.histogram({
name: 'media_proxy.latency',
dimensions: {route},
valueMs: duration,
});
metrics.counter({
name: 'media_proxy.request',
dimensions: {route, status: String(status)},
});
if (status >= 400) {
const errorContext = ctx.get('metricsErrorContext') as ErrorContext | undefined;
const finalErrorType = errorContext?.errorType ?? errorType ?? getErrorTypeFromStatus(status);
const finalErrorSource = errorContext?.errorSource ?? errorSource ?? 'handler';
metrics.counter({
name: 'media_proxy.failure',
dimensions: {
route,
status: String(status),
error_type: finalErrorType,
error_source: finalErrorSource,
},
});
} else {
metrics.counter({
name: 'media_proxy.success',
dimensions: {route, status: String(status)},
});
}
const contentLength = ctx.res.headers.get('content-length');
if (contentLength) {
const bytes = Number.parseInt(contentLength, 10);
if (!Number.isNaN(bytes)) {
metrics.counter({
name: 'media_proxy.bytes',
dimensions: {route},
value: bytes,
});
}
}
}
}
});
}