// server.js - Node.js Express proxy server for Docker Registry Browser const express = require('express'); const { createProxyMiddleware } = require('http-proxy-middleware'); const path = require('path'); const app = express(); // Environment variables const REGISTRY_HOST = process.env.REGISTRY_HOST || 'localhost:5000'; const REGISTRY_PROTOCOL = process.env.REGISTRY_PROTOCOL || 'http'; const REGISTRY_USERNAME = process.env.REGISTRY_USERNAME || ''; const REGISTRY_PASSWORD = process.env.REGISTRY_PASSWORD || ''; const registryUrl = `${REGISTRY_PROTOCOL}://${REGISTRY_HOST}`; console.log('=== Docker Registry Browser Node.js Proxy ==='); console.log('Target Registry:', registryUrl); console.log('Has Authentication:', !!(REGISTRY_USERNAME && REGISTRY_PASSWORD)); // Serve static files from the Angular build app.use(express.static(path.join(__dirname, 'dist/docker-registry-browser'))); // Create proxy middleware with comprehensive options const proxyOptions = { target: registryUrl, changeOrigin: true, secure: false, // Allow self-signed certificates pathRewrite: { '^/api': '', // Remove /api prefix }, timeout: 30000, // 30 second timeout proxyTimeout: 30000, onProxyReq: (proxyReq, req, res) => { // Add auth header if configured if (REGISTRY_USERNAME && REGISTRY_PASSWORD) { const auth = Buffer.from(`${REGISTRY_USERNAME}:${REGISTRY_PASSWORD}`).toString('base64'); proxyReq.setHeader('Authorization', `Basic ${auth}`); } // Log requests for debugging console.log(`[PROXY] ${req.method} ${req.url} -> ${registryUrl}${req.url.replace('/api', '')}`); // Ensure proper headers proxyReq.setHeader('User-Agent', 'Docker-Registry-Browser/1.2'); // Handle DELETE operations with special logging if (req.method === 'DELETE') { console.log(`[DELETE OPERATION] Deleting: ${req.url}`); // Add Docker-specific headers for delete operations proxyReq.setHeader('Accept', 'application/vnd.docker.distribution.manifest.v2+json'); } }, onProxyRes: (proxyRes, req, res) => { // Add comprehensive CORS headers res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS, POST, PUT, DELETE'); res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization, Cache-Control, Pragma'); res.setHeader('Access-Control-Expose-Headers', 'Content-Length, Content-Range, Docker-Content-Digest, Docker-Distribution-Api-Version, X-Registry-Auth, WWW-Authenticate'); res.setHeader('Access-Control-Allow-Credentials', 'true'); res.setHeader('Access-Control-Max-Age', '86400'); // Log successful responses console.log(`[PROXY] Response: ${proxyRes.statusCode} ${proxyRes.statusMessage}`); }, onError: (err, req, res) => { console.error(`[PROXY ERROR] ${req.method} ${req.url}:`, err.message); // Send detailed error response res.status(502).json({ error: 'Registry proxy error', message: err.message, code: err.code, target: registryUrl, url: req.url, timestamp: new Date().toISOString() }); } }; // Registry API proxy - this handles all /api/* requests app.use('/api', createProxyMiddleware(proxyOptions)); // Handle CORS preflight requests explicitly app.options('/api/*', (req, res) => { console.log('[CORS] Handling OPTIONS preflight request'); res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS, POST, PUT, DELETE'); res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization, Cache-Control, Pragma'); res.header('Access-Control-Max-Age', '86400'); res.sendStatus(204); }); // Delete operation validation middleware app.delete('/api/v2/:repository/manifests/:reference', (req, res, next) => { const { repository, reference } = req.params; console.log(`[DELETE VALIDATION] Repository: ${repository}, Reference: ${reference}`); // Log the delete operation for audit purposes console.log(`[AUDIT] DELETE request for ${repository}:${reference} at ${new Date().toISOString()}`); // Continue to proxy next(); }); app.delete('/api/v2/:repository/tags/:tag', (req, res, next) => { const { repository, tag } = req.params; console.log(`[DELETE VALIDATION] Repository: ${repository}, Tag: ${tag}`); // Log the delete operation for audit purposes console.log(`[AUDIT] DELETE request for tag ${repository}:${tag} at ${new Date().toISOString()}`); // Continue to proxy next(); }); // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'healthy', registry: registryUrl, hasAuth: !!(REGISTRY_USERNAME && REGISTRY_PASSWORD), timestamp: new Date().toISOString(), version: '1.1.0' }); }); // Proxy status endpoint for debugging app.get('/proxy-status', (req, res) => { res.json({ proxy_target: registryUrl, status: 'configured', has_auth: !!(REGISTRY_USERNAME && REGISTRY_PASSWORD), proxy_middleware: 'http-proxy-middleware', node_version: process.version, timestamp: new Date().toISOString() }); }); // Test registry connection endpoint app.get('/test-registry', (req, res) => { // Simple test endpoint - actual testing is done by the proxy res.json({ message: 'Registry connection testing is handled by the proxy', registry: registryUrl, hasAuth: !!(REGISTRY_USERNAME && REGISTRY_PASSWORD), timestamp: new Date().toISOString() }); }); // Catch-all handler: send back Angular's index.html file for any non-API routes app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'dist/docker-registry-browser/index.html')); }); // Error handling middleware app.use((err, req, res, next) => { console.error('[SERVER ERROR]:', err); res.status(500).json({ error: 'Internal server error', message: err.message, timestamp: new Date().toISOString() }); }); // Start the server const PORT = process.env.PORT || 80; app.listen(PORT, '0.0.0.0', () => { console.log(`=== Server Started ===`); console.log(`Port: ${PORT}`); console.log(`Registry proxy: /api/* -> ${registryUrl}/*`); console.log(`Health check: http://localhost:${PORT}/health`); console.log(`Proxy status: http://localhost:${PORT}/proxy-status`); console.log('======================'); }); // Graceful shutdown process.on('SIGTERM', () => { console.log('SIGTERM received, shutting down gracefully'); process.exit(0); }); process.on('SIGINT', () => { console.log('SIGINT received, shutting down gracefully'); process.exit(0); }); module.exports = app;