refactor progress
This commit is contained in:
39
packages/media_proxy/src/middleware/AuthMiddleware.tsx
Normal file
39
packages/media_proxy/src/middleware/AuthMiddleware.tsx
Normal 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();
|
||||
});
|
||||
}
|
||||
61
packages/media_proxy/src/middleware/CloudflareFirewall.tsx
Normal file
61
packages/media_proxy/src/middleware/CloudflareFirewall.tsx
Normal 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();
|
||||
});
|
||||
}
|
||||
154
packages/media_proxy/src/middleware/MetricsMiddleware.tsx
Normal file
154
packages/media_proxy/src/middleware/MetricsMiddleware.tsx
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user