Project-Image-Uploader/backend/tests/unit/middlewares/hostGate.test.js
matthias.lotz 712b8477b9 feat: Implement public/internal host separation
Backend:
- Add hostGate middleware for host-based API protection
- Extend rate limiter with publicUploadLimiter (20/hour)
- Add source_host and source_type to audit logs
- Database migration for audit log source tracking
- Unit tests for hostGate middleware (10/20 passing)

Frontend:
- Add hostDetection utility for runtime host detection
- Implement React code splitting with lazy loading
- Update App.js with ProtectedRoute component
- Customize 404 page for public vs internal hosts
- Update env-config.js for host configuration

Docker:
- Add environment variables to prod/dev docker-compose
- Configure ENABLE_HOST_RESTRICTION flags
- Set PUBLIC_HOST and INTERNAL_HOST variables

Infrastructure:
- Prepared for nginx-proxy-manager setup
- Trust proxy configuration (TRUST_PROXY_HOPS=1)

Note: Some unit tests still need adjustment for ENV handling
2025-11-25 20:26:59 +01:00

277 lines
8.7 KiB
JavaScript

/**
* Unit Tests für hostGate Middleware
* Testet Host-basierte Zugriffskontrolle
*/
// Setup ENV VOR dem Require
process.env.ENABLE_HOST_RESTRICTION = 'true';
process.env.PUBLIC_HOST = 'public.example.com';
process.env.INTERNAL_HOST = 'internal.example.com';
process.env.NODE_ENV = 'development';
const hostGate = require('../../../src/middlewares/hostGate');
describe('Host Gate Middleware', () => {
let req, res, next;
let originalEnv;
beforeAll(() => {
// Sichere Original-Env
originalEnv = { ...process.env };
});
beforeEach(() => {
// Mock Request
req = {
get: jest.fn(),
path: '/api/admin/test'
};
// Mock Response
res = {
status: jest.fn().mockReturnThis(),
json: jest.fn()
};
// Mock Next
next = jest.fn();
// Setup Environment
process.env.ENABLE_HOST_RESTRICTION = 'true';
process.env.PUBLIC_HOST = 'public.example.com';
process.env.INTERNAL_HOST = 'internal.example.com';
process.env.NODE_ENV = 'development'; // NOT 'test' to enable restrictions
});
afterEach(() => {
jest.clearAllMocks();
});
afterAll(() => {
// Restore Original-Env
process.env = originalEnv;
});
describe('Host Detection', () => {
test('should detect public host from X-Forwarded-Host header', () => {
req.get.mockImplementation((header) => {
if (header === 'x-forwarded-host') return 'public.example.com';
return null;
});
hostGate(req, res, next);
expect(req.isPublicHost).toBe(true);
expect(req.isInternalHost).toBe(false);
expect(req.requestSource).toBe('public');
});
test('should detect internal host from X-Forwarded-Host header', () => {
req.get.mockImplementation((header) => {
if (header === 'x-forwarded-host') return 'internal.example.com';
return null;
});
hostGate(req, res, next);
expect(req.isPublicHost).toBe(false);
expect(req.isInternalHost).toBe(true);
expect(req.requestSource).toBe('internal');
});
test('should fallback to Host header if X-Forwarded-Host not present', () => {
req.get.mockImplementation((header) => {
if (header === 'x-forwarded-host') return null;
if (header === 'host') return 'public.example.com';
return null;
});
hostGate(req, res, next);
expect(req.isPublicHost).toBe(true);
});
test('should handle localhost as internal host', () => {
req.get.mockImplementation((header) => {
if (header === 'x-forwarded-host') return null;
if (header === 'host') return 'localhost:3000';
return null;
});
hostGate(req, res, next);
expect(req.isInternalHost).toBe(true);
expect(req.isPublicHost).toBe(false);
});
test('should strip port from hostname', () => {
req.get.mockReturnValue('public.example.com:8080');
hostGate(req, res, next);
expect(req.isPublicHost).toBe(true);
});
});
describe('Route Protection', () => {
test('should block admin routes on public host', () => {
req.get.mockReturnValue('public.example.com');
req.path = '/api/admin/deletion-log';
hostGate(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
expect(res.json).toHaveBeenCalledWith({
error: 'Not available on public host',
message: 'This endpoint is only available on the internal network'
});
expect(next).not.toHaveBeenCalled();
});
test('should block groups routes on public host', () => {
req.get.mockReturnValue('public.example.com');
req.path = '/api/groups';
hostGate(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
});
test('should block slideshow routes on public host', () => {
req.get.mockReturnValue('public.example.com');
req.path = '/api/slideshow';
hostGate(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
});
test('should block migration routes on public host', () => {
req.get.mockReturnValue('public.example.com');
req.path = '/api/migration/start';
hostGate(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
});
test('should block auth login on public host', () => {
req.get.mockReturnValue('public.example.com');
req.path = '/api/auth/login';
hostGate(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
});
});
describe('Allowed Routes', () => {
test('should allow upload route on public host', () => {
req.get.mockReturnValue('public.example.com');
req.path = '/api/upload';
hostGate(req, res, next);
expect(next).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled();
});
test('should allow manage routes on public host', () => {
req.get.mockReturnValue('public.example.com');
req.path = '/api/manage/abc-123';
hostGate(req, res, next);
expect(next).toHaveBeenCalled();
});
test('should allow preview routes on public host', () => {
req.get.mockReturnValue('public.example.com');
req.path = '/api/previews/image.jpg';
hostGate(req, res, next);
expect(next).toHaveBeenCalled();
});
test('should allow consent routes on public host', () => {
req.get.mockReturnValue('public.example.com');
req.path = '/api/consent';
hostGate(req, res, next);
expect(next).toHaveBeenCalled();
});
test('should allow all routes on internal host', () => {
req.get.mockReturnValue('internal.example.com');
req.path = '/api/admin/deletion-log';
hostGate(req, res, next);
expect(next).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled();
});
});
describe('Feature Flags', () => {
test('should bypass restriction when NODE_ENV is test and not explicitly enabled', () => {
// Reload module with test environment
delete require.cache[require.resolve('../../../src/middlewares/hostGate')];
process.env.NODE_ENV = 'test';
process.env.ENABLE_HOST_RESTRICTION = 'false'; // Not explicitly enabled
const hostGateTest = require('../../../src/middlewares/hostGate');
req.get.mockReturnValue('public.example.com');
req.path = '/api/admin/test';
hostGateTest(req, res, next);
expect(next).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled();
expect(req.isInternalHost).toBe(true);
// Restore
delete require.cache[require.resolve('../../../src/middlewares/hostGate')];
process.env.NODE_ENV = 'development';
process.env.ENABLE_HOST_RESTRICTION = 'true';
});
test('should work in test environment when explicitly enabled', () => {
// Already set up correctly
process.env.NODE_ENV = 'development';
expect(req.isInternalHost).toBeUndefined(); // Not processed yet, just checking setup
});
});
describe('Request Source Tracking', () => {
test('should set requestSource to "public" for public host', () => {
req.get.mockReturnValue('public.example.com');
req.path = '/api/upload';
hostGate(req, res, next);
expect(req.requestSource).toBe('public');
});
test('should set requestSource to "internal" for internal host', () => {
req.get.mockReturnValue('internal.example.com');
req.path = '/api/admin/test';
hostGate(req, res, next);
expect(req.requestSource).toBe('internal');
});
test('should set requestSource to "internal" when restrictions disabled', () => {
process.env.ENABLE_HOST_RESTRICTION = 'false';
req.get.mockReturnValue('anything.example.com');
req.path = '/api/test';
hostGate(req, res, next);
expect(req.requestSource).toBe('internal');
});
});
});