191 lines
6.6 KiB
JavaScript
191 lines
6.6 KiB
JavaScript
// 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;
|