dynamic-self-register-proxy 1.0.4 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/proxy.js +180 -51
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dynamic-self-register-proxy",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "Dynamic reverse proxy with self-registration API - applications can register themselves and receive an automatically assigned port",
|
|
5
5
|
"main": "proxy-client.js",
|
|
6
6
|
"bin": {
|
package/proxy.js
CHANGED
|
@@ -9,7 +9,7 @@ app.use(express.json());
|
|
|
9
9
|
// ============================================
|
|
10
10
|
// CONFIGURATION
|
|
11
11
|
// ============================================
|
|
12
|
-
const PROXY_PORT = process.env.PROXY_PORT ||
|
|
12
|
+
const PROXY_PORT = process.env.PROXY_PORT || 3000;
|
|
13
13
|
const INTERNAL_PORT_START = 4000;
|
|
14
14
|
const INTERNAL_PORT_END = 5000;
|
|
15
15
|
const HEALTH_CHECK_INTERVAL = process.env.HEALTH_CHECK_INTERVAL || 30000; // 30 secondes par défaut
|
|
@@ -413,6 +413,52 @@ app.get('/proxy/routes', (req, res) => {
|
|
|
413
413
|
});
|
|
414
414
|
});
|
|
415
415
|
|
|
416
|
+
/**
|
|
417
|
+
* POST /proxy/check
|
|
418
|
+
* Déclenche manuellement un health check pour une route spécifique
|
|
419
|
+
* Body: { path: "/myapp" }
|
|
420
|
+
*/
|
|
421
|
+
app.post('/proxy/check', async (req, res) => {
|
|
422
|
+
const { path } = req.body;
|
|
423
|
+
|
|
424
|
+
if (!path) {
|
|
425
|
+
return res.status(400).json({
|
|
426
|
+
success: false,
|
|
427
|
+
error: 'Path is required'
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
432
|
+
const route = registry.routes.get(normalizedPath);
|
|
433
|
+
|
|
434
|
+
if (!route) {
|
|
435
|
+
return res.status(404).json({
|
|
436
|
+
success: false,
|
|
437
|
+
error: 'Path not found'
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
console.log(`[MANUAL HEALTH CHECK] Checking ${normalizedPath}...`);
|
|
442
|
+
const isHealthy = await checkServerHealth(normalizedPath, route);
|
|
443
|
+
|
|
444
|
+
if (!isHealthy) {
|
|
445
|
+
await unregisterServer(normalizedPath);
|
|
446
|
+
return res.json({
|
|
447
|
+
success: true,
|
|
448
|
+
path: normalizedPath,
|
|
449
|
+
healthy: false,
|
|
450
|
+
message: 'Server was unhealthy and has been removed'
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
res.json({
|
|
455
|
+
success: true,
|
|
456
|
+
path: normalizedPath,
|
|
457
|
+
healthy: true,
|
|
458
|
+
message: 'Server is healthy'
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
416
462
|
/**
|
|
417
463
|
* GET /proxy/health
|
|
418
464
|
* Health check du proxy
|
|
@@ -581,6 +627,40 @@ app.get('/', (req, res) => {
|
|
|
581
627
|
.service-link:hover {
|
|
582
628
|
opacity: 0.9;
|
|
583
629
|
}
|
|
630
|
+
.service-actions {
|
|
631
|
+
display: flex;
|
|
632
|
+
gap: 1rem;
|
|
633
|
+
align-items: center;
|
|
634
|
+
}
|
|
635
|
+
.check-btn {
|
|
636
|
+
background: rgba(255, 255, 255, 0.1);
|
|
637
|
+
color: #e4e4e7;
|
|
638
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
639
|
+
padding: 0.5rem 1rem;
|
|
640
|
+
border-radius: 8px;
|
|
641
|
+
cursor: pointer;
|
|
642
|
+
font-size: 0.85rem;
|
|
643
|
+
font-weight: 500;
|
|
644
|
+
transition: all 0.2s;
|
|
645
|
+
}
|
|
646
|
+
.check-btn:hover {
|
|
647
|
+
background: rgba(255, 255, 255, 0.15);
|
|
648
|
+
border-color: rgba(255, 255, 255, 0.3);
|
|
649
|
+
}
|
|
650
|
+
.check-btn.loading {
|
|
651
|
+
opacity: 0.5;
|
|
652
|
+
cursor: not-allowed;
|
|
653
|
+
}
|
|
654
|
+
.check-btn.healthy {
|
|
655
|
+
background: rgba(16, 185, 129, 0.2);
|
|
656
|
+
border-color: rgba(16, 185, 129, 0.4);
|
|
657
|
+
color: #10b981;
|
|
658
|
+
}
|
|
659
|
+
.check-btn.unhealthy {
|
|
660
|
+
background: rgba(239, 68, 68, 0.2);
|
|
661
|
+
border-color: rgba(239, 68, 68, 0.4);
|
|
662
|
+
color: #ef4444;
|
|
663
|
+
}
|
|
584
664
|
.empty-state {
|
|
585
665
|
text-align: center;
|
|
586
666
|
padding: 4rem 2rem;
|
|
@@ -648,9 +728,14 @@ app.get('/', (req, res) => {
|
|
|
648
728
|
<span>Port interne: ${route.port}</span>
|
|
649
729
|
<span>Enregistré: ${new Date(route.registeredAt).toLocaleString('fr-FR')}</span>
|
|
650
730
|
</div>
|
|
651
|
-
<
|
|
652
|
-
|
|
653
|
-
|
|
731
|
+
<div class="service-actions">
|
|
732
|
+
<a href="${escapeHtml(route.path)}" class="service-link">
|
|
733
|
+
Accéder au service →
|
|
734
|
+
</a>
|
|
735
|
+
<button onclick="checkHealth('${escapeHtml(route.path)}', this)" class="check-btn">
|
|
736
|
+
Vérifier la santé
|
|
737
|
+
</button>
|
|
738
|
+
</div>
|
|
654
739
|
</div>
|
|
655
740
|
`).join('')}
|
|
656
741
|
</div>
|
|
@@ -666,6 +751,51 @@ app.get('/', (req, res) => {
|
|
|
666
751
|
<p>API: <a href="/proxy/routes">/proxy/routes</a> | <a href="/proxy/health">/proxy/health</a></p>
|
|
667
752
|
</footer>
|
|
668
753
|
</div>
|
|
754
|
+
|
|
755
|
+
<script>
|
|
756
|
+
async function checkHealth(path, btn) {
|
|
757
|
+
if (btn.classList.contains('loading')) return;
|
|
758
|
+
|
|
759
|
+
const originalText = btn.innerText;
|
|
760
|
+
btn.innerText = 'Vérification...';
|
|
761
|
+
btn.classList.add('loading');
|
|
762
|
+
|
|
763
|
+
try {
|
|
764
|
+
const response = await fetch('/proxy/check', {
|
|
765
|
+
method: 'POST',
|
|
766
|
+
headers: { 'Content-Type': 'application/json' },
|
|
767
|
+
body: JSON.stringify({ path })
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
const data = await response.json();
|
|
771
|
+
|
|
772
|
+
if (data.healthy) {
|
|
773
|
+
btn.innerText = '✅ Sain';
|
|
774
|
+
btn.classList.remove('loading');
|
|
775
|
+
btn.classList.add('healthy');
|
|
776
|
+
setTimeout(() => {
|
|
777
|
+
btn.innerText = originalText;
|
|
778
|
+
btn.classList.remove('healthy');
|
|
779
|
+
}, 3000);
|
|
780
|
+
} else {
|
|
781
|
+
btn.innerText = '❌ Hors ligne';
|
|
782
|
+
btn.classList.remove('loading');
|
|
783
|
+
btn.classList.add('unhealthy');
|
|
784
|
+
|
|
785
|
+
// Si le serveur a été supprimé, on rafraîchit la liste après un court délai
|
|
786
|
+
setTimeout(() => {
|
|
787
|
+
window.location.reload();
|
|
788
|
+
}, 2000);
|
|
789
|
+
}
|
|
790
|
+
} catch (error) {
|
|
791
|
+
btn.innerText = '⚠️ Erreur';
|
|
792
|
+
btn.classList.remove('loading');
|
|
793
|
+
setTimeout(() => {
|
|
794
|
+
btn.innerText = originalText;
|
|
795
|
+
}, 3000);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
</script>
|
|
669
799
|
</body>
|
|
670
800
|
</html>
|
|
671
801
|
`.trim();
|
|
@@ -688,22 +818,51 @@ function escapeHtml(text) {
|
|
|
688
818
|
}
|
|
689
819
|
|
|
690
820
|
// ============================================
|
|
691
|
-
// PROXY MIDDLEWARE
|
|
821
|
+
// PROXY MIDDLEWARE (PERSISTENT)
|
|
692
822
|
// ============================================
|
|
693
823
|
|
|
694
824
|
/**
|
|
695
|
-
*
|
|
825
|
+
* Middleware de proxy persistant pour éviter les fuites de mémoire (EventEmitter MaxListenersExceededWarning)
|
|
826
|
+
* Créé une seule fois et réutilisé pour toutes les requêtes.
|
|
696
827
|
*/
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
828
|
+
const persistentProxyMiddleware = createProxyMiddleware({
|
|
829
|
+
target: 'http://localhost', // Cible par défaut (sera surchargée par router)
|
|
830
|
+
router: (req) => req.proxyTarget,
|
|
831
|
+
changeOrigin: true,
|
|
832
|
+
pathRewrite: (path, req) => {
|
|
833
|
+
const routePath = req.proxyRoutePath;
|
|
834
|
+
if (routePath && path.startsWith(routePath)) {
|
|
835
|
+
return path.slice(routePath.length) || '/';
|
|
836
|
+
}
|
|
837
|
+
return path;
|
|
838
|
+
},
|
|
839
|
+
on: {
|
|
840
|
+
proxyReq: (proxyReq, req) => {
|
|
841
|
+
// Ré-écriture du body si présent (nécessaire car express.json() a consommé le stream)
|
|
842
|
+
if (req.body && typeof req.body === 'object' && Object.keys(req.body).length > 0) {
|
|
843
|
+
const bodyData = JSON.stringify(req.body);
|
|
844
|
+
proxyReq.setHeader('Content-Type', 'application/json');
|
|
845
|
+
proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData));
|
|
846
|
+
proxyReq.write(bodyData);
|
|
847
|
+
}
|
|
848
|
+
console.log(`[PROXY] ${req.method} ${req.path} -> ${req.proxyTarget}`);
|
|
849
|
+
},
|
|
850
|
+
error: (err, req, res) => {
|
|
851
|
+
// Si la réponse a déjà été envoyée, on ne fait rien
|
|
852
|
+
if (res.headersSent) return;
|
|
853
|
+
|
|
854
|
+
console.error(`[PROXY ERROR] ${req.path}:`, err.message);
|
|
855
|
+
res.status(502).json({
|
|
856
|
+
error: 'Proxy error',
|
|
857
|
+
message: err.message,
|
|
858
|
+
target: req.proxyTarget
|
|
859
|
+
});
|
|
860
|
+
}
|
|
701
861
|
}
|
|
702
|
-
|
|
703
|
-
};
|
|
862
|
+
});
|
|
704
863
|
|
|
705
864
|
/**
|
|
706
|
-
* Middleware
|
|
865
|
+
* Middleware principal qui gère le routage vers le proxy
|
|
707
866
|
*/
|
|
708
867
|
app.use((req, res, next) => {
|
|
709
868
|
// Ignore les routes de l'API proxy
|
|
@@ -711,9 +870,9 @@ app.use((req, res, next) => {
|
|
|
711
870
|
return next();
|
|
712
871
|
}
|
|
713
872
|
|
|
714
|
-
const
|
|
873
|
+
const route = findRouteForPath(req.path);
|
|
715
874
|
|
|
716
|
-
if (!
|
|
875
|
+
if (!route) {
|
|
717
876
|
return res.status(404).json({
|
|
718
877
|
error: 'No route registered for this path',
|
|
719
878
|
path: req.path,
|
|
@@ -721,45 +880,15 @@ app.use((req, res, next) => {
|
|
|
721
880
|
});
|
|
722
881
|
}
|
|
723
882
|
|
|
883
|
+
// Stocke les informations de la route dans l'objet req pour le middleware persistant
|
|
884
|
+
req.proxyTarget = `http://localhost:${route.port}`;
|
|
885
|
+
|
|
724
886
|
// Trouve le path de la route pour le pathRewrite
|
|
725
|
-
|
|
726
|
-
const routePath = [...registry.routes.entries()]
|
|
887
|
+
req.proxyRoutePath = [...registry.routes.entries()]
|
|
727
888
|
.find(([, v]) => v === route)?.[0];
|
|
728
889
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
changeOrigin: true,
|
|
732
|
-
pathRewrite: (path) => {
|
|
733
|
-
// Retire le préfixe de la route du path
|
|
734
|
-
if (routePath && path.startsWith(routePath)) {
|
|
735
|
-
const newPath = path.slice(routePath.length) || '/';
|
|
736
|
-
return newPath;
|
|
737
|
-
}
|
|
738
|
-
return path;
|
|
739
|
-
},
|
|
740
|
-
on: {
|
|
741
|
-
proxyReq: (proxyReq, req) => {
|
|
742
|
-
// Ré-écriture du body si présent (nécessaire car express.json() a consommé le stream)
|
|
743
|
-
if (req.body && Object.keys(req.body).length > 0) {
|
|
744
|
-
const bodyData = JSON.stringify(req.body);
|
|
745
|
-
proxyReq.setHeader('Content-Type', 'application/json');
|
|
746
|
-
proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData));
|
|
747
|
-
proxyReq.write(bodyData);
|
|
748
|
-
}
|
|
749
|
-
console.log(`[PROXY] ${req.method} ${req.path} -> ${target}`);
|
|
750
|
-
},
|
|
751
|
-
error: (err, req, res) => {
|
|
752
|
-
console.error(`[PROXY ERROR] ${req.path}:`, err.message);
|
|
753
|
-
res.status(502).json({
|
|
754
|
-
error: 'Proxy error',
|
|
755
|
-
message: err.message,
|
|
756
|
-
target
|
|
757
|
-
});
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
});
|
|
761
|
-
|
|
762
|
-
proxyMiddleware(req, res, next);
|
|
890
|
+
// Délègue au middleware de proxy persistant
|
|
891
|
+
persistentProxyMiddleware(req, res, next);
|
|
763
892
|
});
|
|
764
893
|
|
|
765
894
|
// ============================================
|