From 5446d7d14281080aaf1f673fa3c5fbeed4e53d26 Mon Sep 17 00:00:00 2001 From: Splunk Git Pusher Date: Thu, 19 Feb 2026 23:47:08 +0100 Subject: [PATCH] version avec licence dans le localstorage(sans serveur) Pushed by: admin License: TA9O64YS7EPT (Professional) Timestamp: 2026-02-19T23:47:08.023330 --- apps/pusher_app_prem/README.md | 0 .../appserver/static/git_pusher.js | 106 +- .../appserver/static/git_pusher.js_old | 778 +++++++++ .../appserver/static/license_validation.js | 1502 +++++++++++------ .../static/license_validation.js_old | 598 +++++++ .../static/license_validation.js_old2 | 1104 ++++++++++++ .../license_validator.cpython-39.pyc | Bin 12190 -> 9715 bytes apps/pusher_app_prem/bin/git_pusher.pid | 2 +- apps/pusher_app_prem/bin/git_pusher.py | 62 +- apps/pusher_app_prem/bin/git_pusher.py_old | 758 +++++++++ apps/pusher_app_prem/bin/license_validator.py | 625 ++++--- .../bin/license_validator.py_old | 485 ++++++ apps/pusher_app_prem/local/.usage_stats | 6 + apps/pusher_app_prem/local/usage_stats.json | 2 +- 14 files changed, 5160 insertions(+), 868 deletions(-) mode change 100644 => 100755 apps/pusher_app_prem/README.md mode change 100755 => 100644 apps/pusher_app_prem/appserver/static/git_pusher.js create mode 100755 apps/pusher_app_prem/appserver/static/git_pusher.js_old mode change 100755 => 100644 apps/pusher_app_prem/appserver/static/license_validation.js create mode 100755 apps/pusher_app_prem/appserver/static/license_validation.js_old create mode 100644 apps/pusher_app_prem/appserver/static/license_validation.js_old2 mode change 100755 => 100644 apps/pusher_app_prem/bin/__pycache__/license_validator.cpython-39.pyc mode change 100755 => 100644 apps/pusher_app_prem/bin/git_pusher.pid mode change 100755 => 100644 apps/pusher_app_prem/bin/git_pusher.py create mode 100755 apps/pusher_app_prem/bin/git_pusher.py_old mode change 100755 => 100644 apps/pusher_app_prem/bin/license_validator.py create mode 100755 apps/pusher_app_prem/bin/license_validator.py_old create mode 100644 apps/pusher_app_prem/local/.usage_stats diff --git a/apps/pusher_app_prem/README.md b/apps/pusher_app_prem/README.md old mode 100644 new mode 100755 diff --git a/apps/pusher_app_prem/appserver/static/git_pusher.js b/apps/pusher_app_prem/appserver/static/git_pusher.js old mode 100755 new mode 100644 index bf2c5a3a..801ca5eb --- a/apps/pusher_app_prem/appserver/static/git_pusher.js +++ b/apps/pusher_app_prem/appserver/static/git_pusher.js @@ -239,6 +239,11 @@ async function pushDashboards() { // Récupérer l'utilisateur courant const currentUser = await getCurrentUser(); + // Récupérer les infos de licence depuis le localStorage + const licenseInfo = getLicenseInfo ? getLicenseInfo() : null; + const licenseType = licenseInfo?.type_name || ''; + const licenseId = licenseInfo?.license_id || ''; + // Construire les paramètres const params = new URLSearchParams({ git_url: gitUrl, @@ -249,7 +254,9 @@ async function pushDashboards() { user: currentUser, deploy_to_shcluster: deployToSHCluster.toString(), deployer_host: SH_DEPLOYER_CONFIG.host, - deployer_token: SH_DEPLOYER_CONFIG.token + deployer_token: SH_DEPLOYER_CONFIG.token, + license_type: licenseType, + license_id: licenseId }); // Ajouter les credentials SH si fournis @@ -270,6 +277,25 @@ async function pushDashboards() { console.log("Push result:", result); if (result.status === 'success') { + // Incrémenter le compteur d'utilisation côté client + console.log("=== INCREMENT USAGE ==="); + console.log("typeof incrementUsage:", typeof incrementUsage); + console.log("typeof window.incrementUsage:", typeof window.incrementUsage); + + try { + if (typeof window.incrementUsage === 'function') { + const stats = window.incrementUsage(); + console.log("✓ Usage incremented successfully:", stats); + } else if (typeof incrementUsage === 'function') { + const stats = incrementUsage(); + console.log("✓ Usage incremented (local):", stats); + } else { + console.warn("✗ incrementUsage function not available"); + } + } catch (e) { + console.error("✗ Error incrementing usage:", e); + } + let message = `✅ Successfully deployed ${result.apps_pushed || selectedApps.length} application(s) to Git!`; // Ajouter le statut du déploiement SH Cluster @@ -688,6 +714,12 @@ window.showDeployerConfigModal = showDeployerConfigModal; window.closeDeployerConfigModal = closeDeployerConfigModal; window.saveDeployerConfigFromModal = saveDeployerConfigFromModal; +// Exposer les fonctions principales +window.pushDashboards = pushDashboards; +window.resetForm = resetForm; +window.toggleSelectAll = toggleSelectAll; +window.updateSelectedApps = updateSelectedApps; + // Fonction toggle pour le HTML window.toggleDeployerAuth = function() { var checkbox = document.getElementById('deploy-to-shcluster'); @@ -698,40 +730,43 @@ window.toggleDeployerAuth = function() { }; // ============================================ -// EXPORT POUR DEBUG +// ATTACHEMENT DES ÉVÉNEMENTS AUX BOUTONS // ============================================ -window.GitPusher = { - config: GIT_PUSHER_CONFIG, - deployerConfig: SH_DEPLOYER_CONFIG, - getSelectedApps: () => selectedApps, - checkServer: checkServerHealth, - version: GIT_PUSHER_CONFIG.version -}; - -// ============================================ -// FIX: Attacher les événements aux boutons -// ============================================ (function attachButtonEvents() { function tryAttach() { + console.log("Trying to attach button events..."); + // Bouton Deploy to Git var pushBtn = document.getElementById('push-btn'); if (pushBtn) { + // Supprimer les anciens listeners + pushBtn.replaceWith(pushBtn.cloneNode(true)); + pushBtn = document.getElementById('push-btn'); + pushBtn.addEventListener('click', function(e) { e.preventDefault(); - console.log("Push button clicked"); + e.stopPropagation(); + console.log("Deploy button clicked!"); pushDashboards(); }); - console.log("✓ Push button event attached"); + console.log("✓ Deploy button event attached"); + } else { + console.log("✗ Deploy button not found yet"); } - // Bouton Reset - var buttons = document.querySelectorAll('button.btn'); + // Bouton Reset - chercher par classe ou contenu + var buttons = document.querySelectorAll('button.btn, button.btn-secondary'); buttons.forEach(function(btn) { if (btn.textContent.includes('Reset') || btn.textContent.includes('🔄')) { - btn.addEventListener('click', function(e) { + // Supprimer les anciens listeners + var newBtn = btn.cloneNode(true); + btn.parentNode.replaceChild(newBtn, btn); + + newBtn.addEventListener('click', function(e) { e.preventDefault(); - console.log("Reset button clicked"); + e.stopPropagation(); + console.log("Reset button clicked!"); resetForm(true); }); console.log("✓ Reset button event attached"); @@ -741,9 +776,14 @@ window.GitPusher = { // Bouton Configure Deployer var configBtn = document.querySelector('.deployer-config-btn'); if (configBtn) { - configBtn.addEventListener('click', function(e) { + // Supprimer les anciens listeners + var newConfigBtn = configBtn.cloneNode(true); + configBtn.parentNode.replaceChild(newConfigBtn, configBtn); + + newConfigBtn.addEventListener('click', function(e) { e.preventDefault(); - console.log("Configure button clicked"); + e.stopPropagation(); + console.log("Configure button clicked!"); showDeployerConfigModal(); }); console.log("✓ Configure button event attached"); @@ -761,18 +801,36 @@ window.GitPusher = { console.log("✓ Deploy checkbox event attached"); } - // Si les boutons ne sont pas encore là, réessayer + // Si le bouton principal n'est pas encore là, réessayer if (!pushBtn) { + console.log("Retrying in 500ms..."); setTimeout(tryAttach, 500); + } else { + console.log("=== All button events attached successfully ==="); } } - // Démarrer après un délai + // Démarrer après un délai pour laisser le DOM se charger if (document.readyState === 'complete') { + console.log("Document ready, attaching events in 1s..."); setTimeout(tryAttach, 1000); } else { window.addEventListener('load', function() { + console.log("Window loaded, attaching events in 1s..."); setTimeout(tryAttach, 1000); }); } -})(); \ No newline at end of file +})(); + +// ============================================ +// EXPORT POUR DEBUG +// ============================================ + +window.GitPusher = { + config: GIT_PUSHER_CONFIG, + deployerConfig: SH_DEPLOYER_CONFIG, + getSelectedApps: () => selectedApps, + checkServer: checkServerHealth, + checkDeployer: checkDeployerHealth, + version: GIT_PUSHER_CONFIG.version +}; \ No newline at end of file diff --git a/apps/pusher_app_prem/appserver/static/git_pusher.js_old b/apps/pusher_app_prem/appserver/static/git_pusher.js_old new file mode 100755 index 00000000..bf2c5a3a --- /dev/null +++ b/apps/pusher_app_prem/appserver/static/git_pusher.js_old @@ -0,0 +1,778 @@ +// ============================================ +// GIT PUSHER - MAIN JAVASCRIPT +// Version 2.1 avec déploiement vers SH Cluster +// ============================================ + +// Configuration +const GIT_PUSHER_CONFIG = { + // Détecter automatiquement l'URL du serveur + serverUrl: window.location.protocol + '//' + window.location.hostname + ':9999', + credentialsKey: 'git_pusher_credentials', + deployerConfigKey: 'git_pusher_deployer_config', + version: '2.1.0' +}; + +// Configuration SH Deployer (peut être modifiée via l'interface) +let SH_DEPLOYER_CONFIG = { + enabled: false, + host: '10.10.40.14', + port: 9998, + token: '' +}; + +// État global +let selectedApps = []; +let isProcessing = false; +let deployerAvailable = false; + +// ============================================ +// INITIALISATION +// ============================================ + +require([ + 'jquery', + 'splunkjs/mvc', + 'splunkjs/mvc/searchmanager', + 'splunkjs/mvc/simplexml/ready!' +], function($, mvc, SearchManager) { + + console.log("Git Pusher v2.1 initializing..."); + + // Initialiser le système de licence + if (typeof initializeLicense === 'function') { + initializeLicense(); + } else { + console.warn("License system not loaded"); + } + + // Charger les credentials sauvegardés + loadSavedCredentials(); + + // Charger la config du deployer + loadDeployerConfig(); + + // Vérifier la disponibilité du SH Deployer + checkDeployerHealth(); + + // Récupérer les résultats de recherche pour les apps + const searchManager = mvc.Components.get('dsearch'); + + if (searchManager) { + searchManager.on('search:done', function() { + const results = searchManager.data('results'); + if (results) { + results.on('data', function() { + const rows = results.data().rows; + const fields = results.data().fields; + renderAppsList(rows, fields); + }); + } + }); + } + + // Exposer les fonctions globalement + window.pushDashboards = pushDashboards; + window.resetForm = resetForm; + window.toggleSelectAll = toggleSelectAll; +}); + +// ============================================ +// RENDU DE LA LISTE DES APPLICATIONS +// ============================================ + +function renderAppsList(rows, fields) { + const container = document.getElementById('dashboard-list'); + if (!container) return; + + // Trouver les index des colonnes + const nameIdx = fields.indexOf('name'); + const labelIdx = fields.indexOf('label'); + const descIdx = fields.indexOf('description'); + + // Générer le HTML + let html = ` +
+
+ + +
+ ${rows.length} apps +
+ `; + + rows.forEach((row, index) => { + const name = row[nameIdx] || ''; + const label = row[labelIdx] || name; + const desc = row[descIdx] || ''; + + // Ignorer certaines apps système + if (name.startsWith('splunk_') || name === 'learned' || name === 'launcher') { + return; + } + + html += ` +
+ + +
+ `; + }); + + container.innerHTML = html; + + console.log(`Rendered ${rows.length} applications`); +} + +// ============================================ +// GESTION DE LA SÉLECTION +// ============================================ + +function updateSelectedApps() { + const checkboxes = document.querySelectorAll('#dashboard-list input[type="checkbox"][data-app-id]'); + selectedApps = []; + + checkboxes.forEach(cb => { + if (cb.checked) { + selectedApps.push({ + id: cb.getAttribute('data-app-id'), + label: cb.getAttribute('data-app-label') + }); + } + }); + + // Mettre à jour le "Select All" + const selectAll = document.getElementById('select-all'); + if (selectAll) { + const allChecked = Array.from(checkboxes).every(cb => cb.checked); + const someChecked = Array.from(checkboxes).some(cb => cb.checked); + selectAll.checked = allChecked; + selectAll.indeterminate = someChecked && !allChecked; + } + + console.log(`Selected ${selectedApps.length} apps`); +} + +function toggleSelectAll(checked) { + const checkboxes = document.querySelectorAll('#dashboard-list input[type="checkbox"][data-app-id]'); + checkboxes.forEach(cb => { + cb.checked = checked; + }); + updateSelectedApps(); +} + +// ============================================ +// PUSH VERS GIT +// ============================================ + +async function pushDashboards() { + console.log("Starting push process..."); + + // Vérifier si déjà en cours + if (isProcessing) { + console.log("Push already in progress"); + return; + } + + // Vérifier la licence AVANT tout + if (typeof checkLicenseBeforePush === 'function') { + const licenseOk = await checkLicenseBeforePush(); + if (!licenseOk) { + console.log("License check failed"); + return; + } + } + + // Récupérer les valeurs du formulaire + const gitUrl = document.getElementById('git-url')?.value?.trim(); + const gitBranch = document.getElementById('git-branch')?.value?.trim() || 'main'; + const gitToken = document.getElementById('git-token')?.value?.trim(); + const commitMessage = document.getElementById('commit-message')?.value?.trim(); + const saveCredentials = document.getElementById('save-credentials')?.checked; + + // Mettre à jour la liste des apps sélectionnées + updateSelectedApps(); + + // Validation + if (!gitUrl) { + showMessage('error', 'Please enter a Git repository URL'); + return; + } + + if (!gitToken) { + showMessage('error', 'Please enter a Git token or password'); + return; + } + + if (!commitMessage) { + showMessage('error', 'Please enter a commit message'); + return; + } + + if (selectedApps.length === 0) { + showMessage('error', 'Please select at least one application to deploy'); + return; + } + + // Sauvegarder les credentials si demandé + if (saveCredentials) { + saveCredentialsToStorage(gitUrl, gitBranch, gitToken); + } + + // Vérifier si le déploiement vers SH Cluster est activé + const deployToSHCluster = document.getElementById('deploy-to-shcluster')?.checked || false; + const shAuthUser = document.getElementById('sh-auth-user')?.value?.trim() || ''; + const shAuthPass = document.getElementById('sh-auth-pass')?.value?.trim() || ''; + + // Démarrer le push + isProcessing = true; + showLoading(true, deployToSHCluster); + hideMessages(); + + try { + // Récupérer l'utilisateur courant + const currentUser = await getCurrentUser(); + + // Construire les paramètres + const params = new URLSearchParams({ + git_url: gitUrl, + git_branch: gitBranch, + git_token: gitToken, + commit_message: commitMessage, + apps: JSON.stringify(selectedApps), + user: currentUser, + deploy_to_shcluster: deployToSHCluster.toString(), + deployer_host: SH_DEPLOYER_CONFIG.host, + deployer_token: SH_DEPLOYER_CONFIG.token + }); + + // Ajouter les credentials SH si fournis + if (shAuthUser) params.append('sh_auth_user', shAuthUser); + if (shAuthPass) params.append('sh_auth_pass', shAuthPass); + + console.log(`Pushing ${selectedApps.length} apps to ${gitUrl}${deployToSHCluster ? ' + SH Cluster deployment' : ''}`); + + // Appeler le serveur + const response = await fetch(`${GIT_PUSHER_CONFIG.serverUrl}/push?${params.toString()}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }); + + const result = await response.json(); + console.log("Push result:", result); + + if (result.status === 'success') { + let message = `✅ Successfully deployed ${result.apps_pushed || selectedApps.length} application(s) to Git!`; + + // Ajouter le statut du déploiement SH Cluster + if (deployToSHCluster && result.shcluster_deployment) { + if (result.shcluster_deployment.success) { + message += '\n🚀 SH Cluster deployment triggered successfully!'; + } else { + message += `\n⚠️ SH Cluster deployment failed: ${result.shcluster_deployment.message}`; + } + } + + showMessage('success', message); + + // Reset la sélection après succès + setTimeout(() => { + toggleSelectAll(false); + }, 2000); + } else if (result.error_code === 'LICENSE_ERROR') { + showMessage('error', `🔐 ${result.message}`); + + // Afficher le modal de licence + if (typeof showLicenseModal === 'function') { + showLicenseModal(result.message, result.error_code); + } + } else if (result.error_code === 'APP_LIMIT') { + showMessage('error', `📦 ${result.message}`); + } else { + showMessage('error', result.message || 'Unknown error occurred'); + } + + } catch (error) { + console.error("Push error:", error); + showMessage('error', `Connection error: ${error.message}. Is the Git Pusher server running?`); + } finally { + isProcessing = false; + showLoading(false); + } +} + +// ============================================ +// UTILITAIRES UI +// ============================================ + +function showLoading(show, deployToSHCluster = false) { + const loading = document.getElementById('loading'); + const pushBtn = document.getElementById('push-btn'); + const loadingText = document.querySelector('.loading-text'); + + if (loading) { + loading.classList.toggle('active', show); + } + + if (loadingText && show) { + if (deployToSHCluster) { + loadingText.textContent = 'Deploying to Git and SH Cluster... Please wait'; + } else { + loadingText.textContent = 'Deploying applications to Git... Please wait'; + } + } + + if (pushBtn) { + pushBtn.disabled = show; + if (show) { + pushBtn.textContent = deployToSHCluster ? '⏳ Deploying to Git + SH...' : '⏳ Deploying...'; + } else { + pushBtn.textContent = '✈️ Deploy to Git'; + } + } +} + +function showMessage(type, text) { + hideMessages(); + + const successMsg = document.getElementById('success-msg'); + const errorMsg = document.getElementById('error-msg'); + const successText = document.getElementById('success-text'); + const errorText = document.getElementById('error-text'); + + if (type === 'success' && successMsg && successText) { + successText.textContent = text; + successMsg.classList.add('active'); + + // Auto-hide après 5 secondes + setTimeout(() => { + successMsg.classList.remove('active'); + }, 5000); + } else if (type === 'error' && errorMsg && errorText) { + errorText.textContent = text; + errorMsg.classList.add('active'); + } +} + +function hideMessages() { + const successMsg = document.getElementById('success-msg'); + const errorMsg = document.getElementById('error-msg'); + + if (successMsg) successMsg.classList.remove('active'); + if (errorMsg) errorMsg.classList.remove('active'); +} + +function resetForm(clearCredentials = false) { + // Reset les champs + const commitMessage = document.getElementById('commit-message'); + if (commitMessage) commitMessage.value = ''; + + if (clearCredentials) { + const gitUrl = document.getElementById('git-url'); + const gitToken = document.getElementById('git-token'); + const saveCredentials = document.getElementById('save-credentials'); + + if (gitUrl) gitUrl.value = ''; + if (gitToken) gitToken.value = ''; + if (saveCredentials) saveCredentials.checked = false; + + // Supprimer les credentials sauvegardés + localStorage.removeItem(GIT_PUSHER_CONFIG.credentialsKey); + } + + // Reset la sélection + toggleSelectAll(false); + + // Cacher les messages + hideMessages(); + + console.log("Form reset" + (clearCredentials ? " (with credentials)" : "")); +} + +// ============================================ +// GESTION DES CREDENTIALS +// ============================================ + +function saveCredentialsToStorage(gitUrl, gitBranch, gitToken) { + try { + const credentials = { + gitUrl: gitUrl, + gitBranch: gitBranch, + // Note: En production, envisager une solution plus sécurisée + gitToken: btoa(gitToken), // Encodage basique (pas sécurisé, juste pour l'obfuscation) + savedAt: new Date().toISOString() + }; + + localStorage.setItem(GIT_PUSHER_CONFIG.credentialsKey, JSON.stringify(credentials)); + console.log("Credentials saved"); + } catch (error) { + console.error("Error saving credentials:", error); + } +} + +function loadSavedCredentials() { + try { + const saved = localStorage.getItem(GIT_PUSHER_CONFIG.credentialsKey); + if (!saved) return; + + const credentials = JSON.parse(saved); + + const gitUrl = document.getElementById('git-url'); + const gitBranch = document.getElementById('git-branch'); + const gitToken = document.getElementById('git-token'); + const saveCredentials = document.getElementById('save-credentials'); + + if (gitUrl && credentials.gitUrl) { + gitUrl.value = credentials.gitUrl; + } + + if (gitBranch && credentials.gitBranch) { + gitBranch.value = credentials.gitBranch; + } + + if (gitToken && credentials.gitToken) { + gitToken.value = atob(credentials.gitToken); + } + + if (saveCredentials) { + saveCredentials.checked = true; + } + + console.log("Credentials loaded from storage"); + } catch (error) { + console.error("Error loading credentials:", error); + } +} + +// ============================================ +// RÉCUPÉRATION DE L'UTILISATEUR SPLUNK +// ============================================ + +async function getCurrentUser() { + try { + const response = await fetch('/en-US/splunkd/__raw/services/authentication/current-context?output_mode=json'); + const data = await response.json(); + return data.entry?.[0]?.content?.username || 'unknown'; + } catch (error) { + console.error("Error getting current user:", error); + return 'unknown'; + } +} + +// ============================================ +// VÉRIFICATION DU SERVEUR +// ============================================ + +async function checkServerHealth() { + try { + const response = await fetch(`${GIT_PUSHER_CONFIG.serverUrl}/health`, { + method: 'GET', + timeout: 5000 + }); + const data = await response.json(); + return data.status === 'ok'; + } catch (error) { + console.error("Server health check failed:", error); + return false; + } +} + +// ============================================ +// SH DEPLOYER FUNCTIONS +// ============================================ + +async function checkDeployerHealth() { + try { + const response = await fetch(`${GIT_PUSHER_CONFIG.serverUrl}/deployer/health`, { + method: 'GET', + timeout: 5000 + }); + const data = await response.json(); + + deployerAvailable = data.status === 'ok'; + + // Mettre à jour l'UI + updateDeployerUI(); + + console.log("SH Deployer status:", deployerAvailable ? "Available" : "Unavailable"); + return deployerAvailable; + } catch (error) { + console.error("Deployer health check failed:", error); + deployerAvailable = false; + updateDeployerUI(); + return false; + } +} + +function updateDeployerUI() { + const deployerCheckbox = document.getElementById('deploy-to-shcluster'); + const deployerStatus = document.getElementById('deployer-status'); + const deployerSection = document.getElementById('deployer-section'); + + if (deployerCheckbox) { + deployerCheckbox.disabled = !deployerAvailable; + } + + if (deployerStatus) { + if (deployerAvailable) { + deployerStatus.innerHTML = '● Connected'; + } else { + deployerStatus.innerHTML = '● Disconnected'; + } + } + + if (deployerSection && !deployerAvailable) { + deployerSection.style.opacity = '0.6'; + } +} + +function loadDeployerConfig() { + try { + const saved = localStorage.getItem(GIT_PUSHER_CONFIG.deployerConfigKey); + if (saved) { + const config = JSON.parse(saved); + SH_DEPLOYER_CONFIG = { ...SH_DEPLOYER_CONFIG, ...config }; + console.log("Deployer config loaded"); + } + } catch (error) { + console.error("Error loading deployer config:", error); + } +} + +function saveDeployerConfig() { + try { + localStorage.setItem(GIT_PUSHER_CONFIG.deployerConfigKey, JSON.stringify(SH_DEPLOYER_CONFIG)); + console.log("Deployer config saved"); + } catch (error) { + console.error("Error saving deployer config:", error); + } +} + +function showDeployerConfigModal() { + const modal = document.createElement('div'); + modal.id = 'deployer-config-modal'; + modal.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + `; + + modal.innerHTML = ` +
+

⚙️ SH Deployer Configuration

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ `; + + document.body.appendChild(modal); +} + +function closeDeployerConfigModal() { + const modal = document.getElementById('deployer-config-modal'); + if (modal) modal.remove(); +} + +async function saveDeployerConfigFromModal() { + const host = document.getElementById('deployer-config-host')?.value?.trim(); + const port = parseInt(document.getElementById('deployer-config-port')?.value) || 9998; + const token = document.getElementById('deployer-config-token')?.value?.trim(); + const msgEl = document.getElementById('deployer-config-message'); + + if (!host) { + if (msgEl) { + msgEl.style.display = 'block'; + msgEl.style.background = '#ffebee'; + msgEl.style.color = '#c62828'; + msgEl.textContent = 'Please enter a host'; + } + return; + } + + // Mettre à jour la config + SH_DEPLOYER_CONFIG.host = host; + SH_DEPLOYER_CONFIG.port = port; + SH_DEPLOYER_CONFIG.token = token; + + // Sauvegarder + saveDeployerConfig(); + + // Tester la connexion + if (msgEl) { + msgEl.style.display = 'block'; + msgEl.style.background = '#e3f2fd'; + msgEl.style.color = '#1565c0'; + msgEl.textContent = 'Testing connection...'; + } + + const isHealthy = await checkDeployerHealth(); + + if (isHealthy) { + if (msgEl) { + msgEl.style.background = '#e8f5e9'; + msgEl.style.color = '#2e7d32'; + msgEl.textContent = '✓ Connected successfully!'; + } + setTimeout(closeDeployerConfigModal, 1500); + } else { + if (msgEl) { + msgEl.style.background = '#ffebee'; + msgEl.style.color = '#c62828'; + msgEl.textContent = '✗ Connection failed. Check host and port.'; + } + } +} + +// Vérifier la santé du serveur au démarrage +setTimeout(async () => { + const healthy = await checkServerHealth(); + if (!healthy) { + console.warn("Git Pusher server may not be running"); + } else { + console.log("Git Pusher server is healthy"); + } +}, 1000); + +// ============================================ +// EXPORT FONCTIONS GLOBALES +// ============================================ + +// Exposer les fonctions du deployer globalement pour les onclick du HTML +window.showDeployerConfigModal = showDeployerConfigModal; +window.closeDeployerConfigModal = closeDeployerConfigModal; +window.saveDeployerConfigFromModal = saveDeployerConfigFromModal; + +// Fonction toggle pour le HTML +window.toggleDeployerAuth = function() { + var checkbox = document.getElementById('deploy-to-shcluster'); + var authSection = document.getElementById('deployer-auth'); + if (checkbox && authSection) { + authSection.classList.toggle('visible', checkbox.checked); + } +}; + +// ============================================ +// EXPORT POUR DEBUG +// ============================================ + +window.GitPusher = { + config: GIT_PUSHER_CONFIG, + deployerConfig: SH_DEPLOYER_CONFIG, + getSelectedApps: () => selectedApps, + checkServer: checkServerHealth, + version: GIT_PUSHER_CONFIG.version +}; + +// ============================================ +// FIX: Attacher les événements aux boutons +// ============================================ +(function attachButtonEvents() { + function tryAttach() { + // Bouton Deploy to Git + var pushBtn = document.getElementById('push-btn'); + if (pushBtn) { + pushBtn.addEventListener('click', function(e) { + e.preventDefault(); + console.log("Push button clicked"); + pushDashboards(); + }); + console.log("✓ Push button event attached"); + } + + // Bouton Reset + var buttons = document.querySelectorAll('button.btn'); + buttons.forEach(function(btn) { + if (btn.textContent.includes('Reset') || btn.textContent.includes('🔄')) { + btn.addEventListener('click', function(e) { + e.preventDefault(); + console.log("Reset button clicked"); + resetForm(true); + }); + console.log("✓ Reset button event attached"); + } + }); + + // Bouton Configure Deployer + var configBtn = document.querySelector('.deployer-config-btn'); + if (configBtn) { + configBtn.addEventListener('click', function(e) { + e.preventDefault(); + console.log("Configure button clicked"); + showDeployerConfigModal(); + }); + console.log("✓ Configure button event attached"); + } + + // Checkbox deploy to shcluster + var deployCheckbox = document.getElementById('deploy-to-shcluster'); + if (deployCheckbox) { + deployCheckbox.addEventListener('change', function() { + var authSection = document.getElementById('deployer-auth'); + if (authSection) { + authSection.classList.toggle('visible', this.checked); + } + }); + console.log("✓ Deploy checkbox event attached"); + } + + // Si les boutons ne sont pas encore là, réessayer + if (!pushBtn) { + setTimeout(tryAttach, 500); + } + } + + // Démarrer après un délai + if (document.readyState === 'complete') { + setTimeout(tryAttach, 1000); + } else { + window.addEventListener('load', function() { + setTimeout(tryAttach, 1000); + }); + } +})(); \ No newline at end of file diff --git a/apps/pusher_app_prem/appserver/static/license_validation.js b/apps/pusher_app_prem/appserver/static/license_validation.js old mode 100755 new mode 100644 index ad0093db..755ccf92 --- a/apps/pusher_app_prem/appserver/static/license_validation.js +++ b/apps/pusher_app_prem/appserver/static/license_validation.js @@ -1,598 +1,1134 @@ // ============================================ -// SYSTÈME DE VALIDATION DE LICENCE - VERSION 2.0 -// Avec support fichier .lic +// GIT PUSHER - LICENSE VALIDATION (RSA) +// Version 2.1 - 100% Client-Side Validation // ============================================ -const LICENSE_API_URL = window.location.protocol + '//' + window.location.hostname + ':9999'; +// Configuration +const LICENSE_CONFIG = { + storageKey: 'git_pusher_license', + usageKey: 'git_pusher_usage', + version: '2.1.0' +}; + +// ============================================ +// CLÉ PUBLIQUE RSA +// ============================================ +// Cette clé est générée par le vendeur avec license_generator_rsa.py +// Commande: python3 license_generator_rsa.py export-key -// État global de la licence -let currentLicense = null; -let currentHostname = null; +const PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA7H8v243PKpy4ZcGUI3YX +EiAZaK+VwWSDywCwBOMOJBA5slWorP78cME0bIphrNRvTlA9xpuo0a+8V+VFMb3+ +Uw9AhDtuJKRIEgwJixm/mkKcbjwqSHPmnyBBHPBBX7lO/q2wsEX0y3O/NujByqc3 +dVsB0VVhMFJmgyR3dVy6ZQITgEu/NGs9v/jUc5IT1YzVmOCcL8BZrjlGiF0AXeS3 +/U8khq7wEx5OilhXC7i8w6urd9c4Djjg583WsGtDKk0aZ6xvnfYpmgfTzaFIrUkS +afTxbcZ1h0N3lN9MBvaLbgAui5RgdlbJlbGsgl3uAa9R9xZk+rqTh8VBLVq+KW5I +a6aYOVterUf2hz/hUkNjM8Rolv4/3PQX0mGu6fa4fwoxmjlSUEVxVFh7TdCE/WHj +3kAOybZXWJnws/++urqijP5SmYxyCaVlYAoutdWmz1tTrSXOh74qrou2wv3C8Dmo +8ccVznAhdhHcVs7MSl9Qbyw1fsi1117WigUGkPE5Cxjlrl8EcBQg3G5x91ER95JM +O0SjyhDborT+oMq9947ZL35VllzkKbBELbhDnogXmDMrI3Ij1UBmCtSOZzOLhyHD +FmGf5AB1LWbxcgrzOMcTLoAHduaDalZCzmW4WdV4313CqeawEfqJVj8BJ+0VEFdb +RDk4ZzHpOaGAuCJjN3AuxO8CAwEAAQ== +-----END PUBLIC KEY-----`; // ============================================ -// INITIALISATION +// UTILITAIRES CRYPTO +// ============================================ + +/** + * Convertir une chaîne PEM en ArrayBuffer + */ +function pemToArrayBuffer(pem) { + const b64 = pem + .replace(/-----BEGIN PUBLIC KEY-----/, '') + .replace(/-----END PUBLIC KEY-----/, '') + .replace(/\s/g, ''); + const binary = atob(b64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return bytes.buffer; +} + +/** + * Importer la clé publique RSA + */ +async function importPublicKey() { + try { + const keyData = pemToArrayBuffer(PUBLIC_KEY_PEM); + const key = await crypto.subtle.importKey( + 'spki', + keyData, + { + name: 'RSASSA-PKCS1-v1_5', + hash: 'SHA-256' + }, + false, + ['verify'] + ); + return key; + } catch (error) { + console.error('Erreur import clé publique:', error); + return null; + } +} + +/** + * Vérifier la signature RSA PKCS#1 v1.5 + */ +async function verifySignature(data, signatureB64, publicKey) { + try { + const signature = Uint8Array.from(atob(signatureB64), c => c.charCodeAt(0)); + const dataBuffer = new TextEncoder().encode(data); + + const isValid = await crypto.subtle.verify( + 'RSASSA-PKCS1-v1_5', + publicKey, + signature, + dataBuffer + ); + + return isValid; + } catch (error) { + console.error('Erreur vérification signature:', error); + return false; + } +} + +// ============================================ +// FONCTIONS UTILITAIRES // ============================================ -async function initializeLicense() { - console.log("Initializing license system v2.0..."); +/** + * Décoder Base64 + */ +function base64Decode(str) { + try { + return decodeURIComponent(atob(str).split('').map(function(c) { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }).join('')); + } catch (e) { + return atob(str); + } +} + +/** + * Obtenir le hostname actuel (depuis l'URL) + */ +function getCurrentHostname() { + return window.location.hostname.toLowerCase(); +} + +/** + * Obtenir le vrai hostname Splunk via l'API REST + */ +async function getSplunkHostname() { + try { + // Méthode 1: Via server/info + const response = await fetch('/en-US/splunkd/__raw/services/server/info?output_mode=json', { + credentials: 'include' + }); + if (response.ok) { + const data = await response.json(); + const serverName = data.entry?.[0]?.content?.serverName; + if (serverName) { + console.log('Hostname Splunk (server/info):', serverName); + return serverName.toLowerCase(); + } + } + } catch (e) { + console.log('Méthode server/info échouée:', e); + } try { - // Récupérer le statut de la licence depuis le serveur - const response = await fetch(`${LICENSE_API_URL}/license/status`); - const data = await response.json(); + // Méthode 2: Via server/settings + const response2 = await fetch('/en-US/splunkd/__raw/services/server/settings?output_mode=json', { + credentials: 'include' + }); + if (response2.ok) { + const data2 = await response2.json(); + const serverName = data2.entry?.[0]?.content?.serverName; + if (serverName) { + console.log('Hostname Splunk (server/settings):', serverName); + return serverName.toLowerCase(); + } + } + } catch (e) { + console.log('Méthode server/settings échouée:', e); + } + + // Fallback: hostname de l'URL (pas idéal) + console.warn('Impossible de récupérer le hostname Splunk, utilisation URL:', getCurrentHostname()); + return getCurrentHostname(); +} + +/** + * Calculer les jours restants + */ +function daysRemaining(expiryDate) { + const expiry = new Date(expiryDate); + const now = new Date(); + const diff = expiry - now; + return Math.ceil(diff / (1000 * 60 * 60 * 24)); +} + +// ============================================ +// PARSING DE LICENCE +// ============================================ + +/** + * Parser le contenu d'un fichier .lic + */ +function parseLicenseFile(content) { + try { + const lines = content.trim().split('\n'); + let payloadB64 = null; + + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && !trimmed.startsWith('#')) { + payloadB64 = trimmed; + break; + } + } + + if (!payloadB64) { + return { error: 'Payload non trouvé dans le fichier' }; + } + + // Décoder le payload + const payloadJson = base64Decode(payloadB64); + const payload = JSON.parse(payloadJson); + + if (!payload.license || !payload.signature) { + return { error: 'Format de licence invalide' }; + } + + // Décoder les données de licence + const licenseJson = base64Decode(payload.license); + const licenseData = JSON.parse(licenseJson); + + return { + success: true, + licenseJson: licenseJson, + licenseData: licenseData, + signatureB64: payload.signature, + rawPayload: payloadB64 + }; + } catch (error) { + console.error('Erreur parsing licence:', error); + return { error: 'Erreur de lecture du fichier de licence' }; + } +} + +// ============================================ +// VALIDATION DE LICENCE +// ============================================ + +/** + * Valider une licence complète (signature RSA + hostname + expiration) + */ +async function validateLicense(licenseContent = null) { + try { + // Si pas de contenu fourni, charger depuis le localStorage + let parsed; + if (licenseContent) { + parsed = parseLicenseFile(licenseContent); + } else { + const stored = localStorage.getItem(LICENSE_CONFIG.storageKey); + if (!stored) { + return { + valid: false, + error: 'Aucune licence installée', + error_code: 'NO_LICENSE' + }; + } + parsed = JSON.parse(stored); + } - console.log("License status:", data); + if (parsed.error) { + return { + valid: false, + error: parsed.error, + error_code: 'PARSE_ERROR' + }; + } + + const { licenseJson, licenseData, signatureB64 } = parsed; + + // DEBUG: Afficher les données + console.log('=== DEBUG VALIDATION LICENCE ==='); + console.log('License JSON:', licenseJson); + console.log('License JSON length:', licenseJson.length); + console.log('Signature B64:', signatureB64.substring(0, 50) + '...'); + console.log('Signature B64 length:', signatureB64.length); + + // 1. Vérifier la signature RSA + console.log('Vérification de la signature RSA...'); + const publicKey = await importPublicKey(); + + if (!publicKey) { + console.error('Échec import clé publique'); + return { + valid: false, + error: 'Impossible de charger la clé publique', + error_code: 'KEY_ERROR' + }; + } + + console.log('Clé publique importée avec succès'); + + const signatureValid = await verifySignature(licenseJson, signatureB64, publicKey); + + console.log('Résultat vérification signature:', signatureValid); + + if (!signatureValid) { + return { + valid: false, + error: 'Signature de licence invalide', + error_code: 'INVALID_SIGNATURE' + }; + } - currentHostname = data.hostname; + console.log('✓ Signature RSA valide'); - if (data.status === 'valid' && data.license) { - // Licence valide - currentLicense = data.license; - displayLicenseInfo(data.license); - hideLicenseModal(); + // 2. Vérifier le hostname + const expectedHostname = (licenseData.hostname || '').toLowerCase(); + const currentHostname = await getSplunkHostname(); + + console.log(`Hostname attendu: "${expectedHostname}", actuel: "${currentHostname}"`); + + // Permettre une correspondance partielle ou exacte + if (expectedHostname && expectedHostname !== currentHostname) { + // Vérifier si c'est une correspondance partielle (le hostname peut être un FQDN) + if (!currentHostname.includes(expectedHostname) && !expectedHostname.includes(currentHostname)) { + return { + valid: false, + error: `Licence non valide pour ce serveur. Attendu: ${expectedHostname}, Actuel: ${currentHostname}`, + error_code: 'HOSTNAME_MISMATCH', + expected_hostname: expectedHostname, + current_hostname: currentHostname + }; + } + console.log('✓ Hostname valide (correspondance partielle)'); } else { - // Pas de licence ou licence invalide - showLicenseModal(data.error, data.error_code, data.hostname); + console.log('✓ Hostname valide (exact)'); + } + + // 3. Vérifier la date d'expiration + const expiryDate = licenseData.expires; + if (expiryDate) { + const days = daysRemaining(expiryDate); + + if (days < 0) { + return { + valid: false, + error: `Licence expirée le ${expiryDate}`, + error_code: 'LICENSE_EXPIRED', + expires: expiryDate + }; + } + + console.log(`✓ Licence valide (${days} jours restants)`); } + + // Licence valide ! + return { + valid: true, + license_id: licenseData.license_id, + type: licenseData.type, + type_name: licenseData.type_name, + customer: licenseData.customer, + hostname: expectedHostname, + issued: licenseData.issued, + expires: expiryDate, + days_remaining: daysRemaining(expiryDate), + limits: licenseData.limits || {}, + features: licenseData.features || [] + }; + } catch (error) { - console.error("Error checking license:", error); - // En cas d'erreur réseau, afficher le modal - showLicenseModal("Impossible de vérifier la licence. Le serveur est-il démarré?", "CONNECTION_ERROR"); + console.error('Erreur validation licence:', error); + return { + valid: false, + error: error.message, + error_code: 'VALIDATION_ERROR' + }; } } +/** + * Vérifier si une fonctionnalité est disponible + */ +async function hasFeature(featureName) { + const validation = await validateLicense(); + if (!validation.valid) return false; + return validation.features.includes(featureName); +} + // ============================================ -// AFFICHAGE DU BADGE DE LICENCE +// GESTION DU STOCKAGE // ============================================ -function displayLicenseInfo(license) { - console.log("Displaying license info:", license); - - const container = document.getElementById('license-badge-container'); - if (!container) { - console.error("license-badge-container not found"); - return; +/** + * Sauvegarder une licence validée + */ +async function saveLicense(licenseContent) { + try { + // Parser le fichier + const parsed = parseLicenseFile(licenseContent); + + if (parsed.error) { + return { + success: false, + error: parsed.error + }; + } + + // Valider la licence avant de sauvegarder + const validation = await validateLicense(licenseContent); + + if (!validation.valid) { + return { + success: false, + error: validation.error, + error_code: validation.error_code + }; + } + + // Sauvegarder dans localStorage + localStorage.setItem(LICENSE_CONFIG.storageKey, JSON.stringify(parsed)); + + console.log('✓ Licence sauvegardée avec succès'); + + return { + success: true, + license: validation + }; + + } catch (error) { + console.error('Erreur sauvegarde licence:', error); + return { + success: false, + error: error.message + }; } +} + +/** + * Supprimer la licence + */ +function removeLicense() { + localStorage.removeItem(LICENSE_CONFIG.storageKey); + localStorage.removeItem(LICENSE_CONFIG.usageKey); + console.log('Licence supprimée'); +} + +/** + * Récupérer les infos de licence (sans revalider la signature) + */ +function getLicenseInfo() { + try { + const stored = localStorage.getItem(LICENSE_CONFIG.storageKey); + if (!stored) return null; + + const parsed = JSON.parse(stored); + return parsed.licenseData; + } catch { + return null; + } +} + +// ============================================ +// GESTION DES LIMITES D'UTILISATION +// ============================================ + +/** + * Obtenir les stats d'utilisation + */ +function getUsageStats() { + try { + const stored = localStorage.getItem(LICENSE_CONFIG.usageKey); + if (stored) { + return JSON.parse(stored); + } + } catch {} - // Supprimer l'ancien badge s'il existe - const oldBadge = document.getElementById('license-badge'); - if (oldBadge) oldBadge.remove(); - - // Créer le badge - const badge = document.createElement('div'); - badge.id = 'license-badge'; - - // Déterminer le style selon le type et les jours restants - let badgeStyle = ''; - let badgeText = ''; - let badgeIcon = '✓'; - - const daysRemaining = license.days_remaining || 0; - const licenseType = license.type_name || license.type || 'Unknown'; - - if (daysRemaining <= 0) { - // Expirée - badgeStyle = 'background: linear-gradient(135deg, #f44336 0%, #da190b 100%);'; - badgeText = '⚠ Licence expirée'; - badgeIcon = '⚠'; - } else if (daysRemaining <= 7) { - // Expire bientôt - badgeStyle = 'background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);'; - badgeText = `⚠ ${licenseType} - ${daysRemaining}j restants`; - badgeIcon = '⚠'; - } else if (license.type === 'trial') { - // Essai - badgeStyle = 'background: linear-gradient(135deg, #9c27b0 0%, #7b1fa2 100%);'; - badgeText = `⏱ Essai - ${daysRemaining}j restants`; - badgeIcon = '⏱'; - } else { - // Licence normale valide - badgeStyle = 'background: linear-gradient(135deg, #4caf50 0%, #2e7d32 100%);'; - badgeText = `✓ ${licenseType}`; - badgeIcon = '✓'; + return { + total_pushes: 0, + pushes_today: 0, + last_push_date: null + }; +} + +/** + * Incrémenter le compteur d'utilisation + */ +function incrementUsage() { + const stats = getUsageStats(); + const today = new Date().toISOString().split('T')[0]; + + // Reset si nouveau jour + if (stats.last_push_date !== today) { + stats.pushes_today = 0; + stats.last_push_date = today; } - badge.style.cssText = ` - ${badgeStyle} - color: white; - padding: 10px 16px; - border-radius: 8px; - font-size: 12px; - font-weight: 600; - box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); - cursor: pointer; - transition: all 0.3s ease; - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; - text-align: center; - display: flex; - align-items: center; - gap: 8px; - `; + stats.total_pushes = (stats.total_pushes || 0) + 1; + stats.pushes_today = (stats.pushes_today || 0) + 1; - badge.innerHTML = ` - ${badgeIcon} - ${badgeText} - `; + localStorage.setItem(LICENSE_CONFIG.usageKey, JSON.stringify(stats)); + return stats; +} + +/** + * Vérifier les limites avant un push + */ +async function checkLimits() { + const validation = await validateLicense(); - // Click pour voir les détails - badge.onclick = function() { - showLicenseDetails(license); - }; + if (!validation.valid) { + return { + allowed: false, + error: validation.error, + error_code: validation.error_code + }; + } - // Hover effect - badge.addEventListener('mouseenter', function() { - this.style.transform = 'translateY(-2px)'; - this.style.boxShadow = '0 6px 20px rgba(0, 0, 0, 0.3)'; - }); + const limits = validation.limits || {}; + const maxPushes = limits.max_pushes_per_day || -1; - badge.addEventListener('mouseleave', function() { - this.style.transform = 'translateY(0)'; - this.style.boxShadow = '0 4px 15px rgba(0, 0, 0, 0.2)'; - }); + if (maxPushes > 0) { + const stats = getUsageStats(); + const today = new Date().toISOString().split('T')[0]; + + // Reset si nouveau jour + let pushesToday = stats.pushes_today || 0; + if (stats.last_push_date !== today) { + pushesToday = 0; + } + + if (pushesToday >= maxPushes) { + return { + allowed: false, + error: `Limite quotidienne atteinte (${maxPushes} pushes/jour)`, + error_code: 'DAILY_LIMIT_REACHED' + }; + } + } - container.appendChild(badge); + return { + allowed: true, + license_type: validation.type_name, + remaining_today: maxPushes > 0 ? maxPushes - getUsageStats().pushes_today : -1 + }; } // ============================================ -// MODAL DE DÉTAILS DE LICENCE +// INTERFACE UTILISATEUR // ============================================ -function showLicenseDetails(license) { - const modal = document.createElement('div'); - modal.id = 'license-details-modal'; - modal.style.cssText = ` - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.7); - display: flex; - align-items: center; - justify-content: center; - z-index: 10000; - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; - `; +/** + * Afficher le badge de licence + */ +async function updateLicenseBadge() { + const container = document.getElementById('license-badge-container'); + if (!container) return; - const features = (license.features || []).map(f => `${f}`).join(''); + const validation = await validateLicense(); + + let badgeHtml = ''; + + if (validation.valid) { + const daysLeft = validation.days_remaining; + let badgeClass = 'license-badge-valid'; + let icon = '✓'; + + if (daysLeft <= 7) { + badgeClass = 'license-badge-expiring'; + icon = '⚠️'; + } else if (daysLeft <= 30) { + badgeClass = 'license-badge-warning'; + icon = '⏳'; + } + + badgeHtml = ` +
+ ${icon} + ${validation.type_name} + ${daysLeft}j +
+ `; + } else { + badgeHtml = ` +
+ 🔐 + Activer +
+ `; + } - const limits = license.limits || {}; - const maxApps = limits.max_apps === -1 ? 'Illimité' : limits.max_apps; - const maxPushes = limits.max_pushes_per_day === -1 ? 'Illimité' : limits.max_pushes_per_day + '/jour'; + container.innerHTML = badgeHtml; +} + +/** + * Afficher le modal de licence + */ +function showLicenseModal(message = null, errorCode = null) { + // Supprimer l'ancien modal s'il existe + const existingModal = document.getElementById('license-modal'); + if (existingModal) existingModal.remove(); + + const modal = document.createElement('div'); + modal.id = 'license-modal'; + modal.className = 'license-modal-overlay'; + + let errorMessage = ''; + if (message) { + errorMessage = `
${message}
`; + } modal.innerHTML = ` -
-
-

📋 Détails de la licence

- +
+
+

🔐 Activation de Licence

+
-
-
-
-
Type
-
${license.type_name || license.type}
-
-
-
ID
-
${license.license_id}
-
-
-
Expire
-
${license.expires}
-
-
-
Jours restants
-
${license.days_remaining}
+
+ ${errorMessage} + +
+
📄
+
+ Glissez-déposez votre fichier .lic ici +
ou cliquez pour sélectionner
+
+ +
+ Hostname du serveur: + Chargement... +
Communiquez ce hostname pour obtenir votre licence +
+ +
-
-
Client
-
${license.customer?.name || 'N/A'} (${license.customer?.email || 'N/A'})
-
- -
-
Hostname
-
${license.hostname || currentHostname}
-
- -
-
Limites
-
Apps: ${maxApps} | Pushes: ${maxPushes}
-
- -
-
Fonctionnalités
-
${features || 'Aucune'}
-
- -
- - +
`; document.body.appendChild(modal); + + // Afficher le hostname + getSplunkHostname().then(hostname => { + const hostnameDisplay = document.getElementById('current-hostname-display'); + if (hostnameDisplay) { + hostnameDisplay.textContent = hostname; + } + }); + + // Configurer le drag & drop + setupLicenseUpload(); } -// ============================================ -// MODAL D'UPLOAD DE LICENCE -// ============================================ +/** + * Configurer l'upload de licence + */ +function setupLicenseUpload() { + const dropZone = document.getElementById('license-upload-zone'); + const fileInput = document.getElementById('license-file-input'); + + if (!dropZone || !fileInput) return; + + // Clic pour sélectionner + dropZone.addEventListener('click', () => fileInput.click()); + + // Drag & drop + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('dragover'); + }); + + dropZone.addEventListener('dragleave', () => { + dropZone.classList.remove('dragover'); + }); + + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('dragover'); + + const files = e.dataTransfer.files; + if (files.length > 0) { + handleLicenseFile(files[0]); + } + }); + + // Sélection de fichier + fileInput.addEventListener('change', (e) => { + if (e.target.files.length > 0) { + handleLicenseFile(e.target.files[0]); + } + }); +} -function showLicenseModal(error = null, errorCode = null, hostname = null) { - console.log("Showing license modal", { error, errorCode, hostname }); +/** + * Traiter le fichier de licence uploadé + */ +async function handleLicenseFile(file) { + const resultDiv = document.getElementById('license-validation-result'); + if (!resultDiv) return; - // Supprimer l'ancien modal s'il existe - hideLicenseModal(); - const detailsModal = document.getElementById('license-details-modal'); - if (detailsModal) detailsModal.remove(); + // Vérifier l'extension + if (!file.name.endsWith('.lic')) { + resultDiv.innerHTML = ` +
+ ❌ Le fichier doit avoir l'extension .lic +
+ `; + return; + } - const modal = document.createElement('div'); - modal.id = 'license-modal'; - modal.style.cssText = ` - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.7); - display: flex; - align-items: center; - justify-content: center; - z-index: 10000; - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + resultDiv.innerHTML = ` +
+ ⏳ Validation en cours... +
`; - // Message d'erreur si présent - let errorHtml = ''; - if (error) { - let errorStyle = 'background: #ffebee; color: #c62828; border-left: 4px solid #f44336;'; - if (errorCode === 'NO_LICENSE') { - errorStyle = 'background: #fff3e0; color: #e65100; border-left: 4px solid #ff9800;'; + try { + // Lire le fichier + const content = await file.text(); + + // Sauvegarder et valider + const result = await saveLicense(content); + + if (result.success) { + const license = result.license; + resultDiv.innerHTML = ` +
+
+
+ Licence activée avec succès !
+ Type: ${license.type_name}
+ Expire: ${license.expires} (${license.days_remaining} jours)
+ Client: ${license.customer?.name || 'N/A'} +
+
+ `; + + // Mettre à jour le badge + updateLicenseBadge(); + + // Fermer le modal après 3 secondes + setTimeout(() => { + closeLicenseModal(); + }, 3000); + + } else { + resultDiv.innerHTML = ` +
+ ❌ ${result.error} +
+ `; } - errorHtml = ` -
- ${error} + + } catch (error) { + resultDiv.innerHTML = ` +
+ ❌ Erreur: ${error.message}
`; } +} + +/** + * Fermer le modal de licence + */ +function closeLicenseModal() { + const modal = document.getElementById('license-modal'); + if (modal) modal.remove(); +} + +/** + * Afficher les détails de la licence + */ +async function showLicenseDetails() { + const validation = await validateLicense(); + + if (!validation.valid) { + showLicenseModal(validation.error, validation.error_code); + return; + } + + // Supprimer l'ancien modal s'il existe + const existingModal = document.getElementById('license-details-modal'); + if (existingModal) existingModal.remove(); + + const modal = document.createElement('div'); + modal.id = 'license-details-modal'; + modal.className = 'license-modal-overlay'; + + const features = validation.features.map(f => `${f}`).join(' '); + const limits = validation.limits; + const maxApps = limits.max_apps === -1 ? '∞' : limits.max_apps; + const maxPushes = limits.max_pushes_per_day === -1 ? '∞' : limits.max_pushes_per_day; + + const usage = getUsageStats(); modal.innerHTML = ` -
-
-

🔐 Git Pusher

-

Activation de licence requise

-
- - ${errorHtml} - -
-

- 📋 Hostname Splunk:
- ${hostname || 'Chargement...'} -

-

- Communiquez ce hostname pour obtenir votre licence. -

+
+
+

📋 Détails de la Licence

+
-
- +
+ + + + + + + + + + + +
ID${validation.license_id}
Type${validation.type_name}
Client${validation.customer?.name || 'N/A'}
Email${validation.customer?.email || 'N/A'}
Hostname${validation.hostname}
Émise le${validation.issued}
Expire le${validation.expires}
Jours restants${validation.days_remaining}
Apps max${maxApps}
Pushes/jour${maxPushes}
-
-
📁
-
- Glissez votre fichier .lic ici
- ou cliquez pour sélectionner -
- +
+ Fonctionnalités:
+
${features}
- - - - - -
- - Besoin d'une licence ? Contactez-nous - + - - ${currentLicense ? ` -
- -
- ` : ''}
`; document.body.appendChild(modal); - - // Récupérer le hostname si pas fourni - if (!hostname) { - fetch(`${LICENSE_API_URL}/license/hostname`) - .then(r => r.json()) - .then(d => { - const codeEl = modal.querySelector('code'); - if (codeEl) codeEl.textContent = d.hostname || 'Inconnu'; - currentHostname = d.hostname; - }) - .catch(() => {}); +} + +/** + * Confirmer la suppression de licence + */ +function confirmRemoveLicense() { + if (confirm('Êtes-vous sûr de vouloir supprimer cette licence ?')) { + removeLicense(); + + // Fermer le modal des détails + const detailsModal = document.getElementById('license-details-modal'); + if (detailsModal) detailsModal.remove(); + + // Mettre à jour le badge + updateLicenseBadge(); + + alert('Licence supprimée.'); } } -function hideLicenseModal() { - const modal = document.getElementById('license-modal'); - if (modal) modal.remove(); +/** + * Vérifier la licence avant un push (appelé par git_pusher.js) + */ +async function checkLicenseBeforePush() { + const result = await checkLimits(); + + if (!result.allowed) { + showLicenseModal(result.error, result.error_code); + return false; + } + + return true; } // ============================================ -// GESTION DU FICHIER +// STYLES CSS // ============================================ -let selectedLicenseContent = null; +const licenseStyles = ` + +`; + // ============================================ -// VÉRIFICATION AVANT PUSH +// INITIALISATION // ============================================ -async function checkLicenseBeforePush() { - try { - const response = await fetch(`${LICENSE_API_URL}/license/status`); - const data = await response.json(); - - if (data.status !== 'valid') { - alert('⚠️ ' + (data.error || 'Licence invalide ou absente')); - showLicenseModal(data.error, data.error_code, data.hostname); - return false; - } - - // Vérifier les limites - const limits = data.license?.limits || {}; - const usage = data.usage || {}; - - if (limits.max_pushes_per_day > 0 && usage.pushes_today >= limits.max_pushes_per_day) { - alert(`⚠️ Limite quotidienne atteinte (${limits.max_pushes_per_day} pushes/jour)`); - return false; - } - - return true; - } catch (error) { - console.error("License check error:", error); - alert('⚠️ Impossible de vérifier la licence'); - return false; +/** + * Initialiser le système de licence + */ +function initializeLicense() { + console.log('Git Pusher License System v' + LICENSE_CONFIG.version + ' (RSA)'); + + // Injecter les styles + if (!document.getElementById('license-styles')) { + const styleEl = document.createElement('div'); + styleEl.id = 'license-styles'; + styleEl.innerHTML = licenseStyles; + document.head.appendChild(styleEl); } + + // Mettre à jour le badge + updateLicenseBadge(); } -// ============================================ -// EXPORT POUR UTILISATION EXTERNE -// ============================================ +// Exposer les fonctions globalement +window.initializeLicense = initializeLicense; +window.validateLicense = validateLicense; +window.saveLicense = saveLicense; +window.removeLicense = removeLicense; +window.checkLicenseBeforePush = checkLicenseBeforePush; +window.showLicenseModal = showLicenseModal; +window.closeLicenseModal = closeLicenseModal; +window.showLicenseDetails = showLicenseDetails; +window.confirmRemoveLicense = confirmRemoveLicense; +window.updateLicenseBadge = updateLicenseBadge; +window.hasFeature = hasFeature; +window.incrementUsage = incrementUsage; +window.getUsageStats = getUsageStats; +window.getLicenseInfo = getLicenseInfo; -window.LicenseManager = { - init: initializeLicense, - check: checkLicenseBeforePush, - showModal: showLicenseModal, - getLicense: () => currentLicense, - getHostname: () => currentHostname -}; +// Auto-initialiser après le chargement +if (document.readyState === 'complete') { + setTimeout(initializeLicense, 100); +} else { + window.addEventListener('load', function() { + setTimeout(initializeLicense, 100); + }); +} \ No newline at end of file diff --git a/apps/pusher_app_prem/appserver/static/license_validation.js_old b/apps/pusher_app_prem/appserver/static/license_validation.js_old new file mode 100755 index 00000000..ad0093db --- /dev/null +++ b/apps/pusher_app_prem/appserver/static/license_validation.js_old @@ -0,0 +1,598 @@ +// ============================================ +// SYSTÈME DE VALIDATION DE LICENCE - VERSION 2.0 +// Avec support fichier .lic +// ============================================ + +const LICENSE_API_URL = window.location.protocol + '//' + window.location.hostname + ':9999'; + +// État global de la licence +let currentLicense = null; +let currentHostname = null; + +// ============================================ +// INITIALISATION +// ============================================ + +async function initializeLicense() { + console.log("Initializing license system v2.0..."); + + try { + // Récupérer le statut de la licence depuis le serveur + const response = await fetch(`${LICENSE_API_URL}/license/status`); + const data = await response.json(); + + console.log("License status:", data); + + currentHostname = data.hostname; + + if (data.status === 'valid' && data.license) { + // Licence valide + currentLicense = data.license; + displayLicenseInfo(data.license); + hideLicenseModal(); + } else { + // Pas de licence ou licence invalide + showLicenseModal(data.error, data.error_code, data.hostname); + } + } catch (error) { + console.error("Error checking license:", error); + // En cas d'erreur réseau, afficher le modal + showLicenseModal("Impossible de vérifier la licence. Le serveur est-il démarré?", "CONNECTION_ERROR"); + } +} + +// ============================================ +// AFFICHAGE DU BADGE DE LICENCE +// ============================================ + +function displayLicenseInfo(license) { + console.log("Displaying license info:", license); + + const container = document.getElementById('license-badge-container'); + if (!container) { + console.error("license-badge-container not found"); + return; + } + + // Supprimer l'ancien badge s'il existe + const oldBadge = document.getElementById('license-badge'); + if (oldBadge) oldBadge.remove(); + + // Créer le badge + const badge = document.createElement('div'); + badge.id = 'license-badge'; + + // Déterminer le style selon le type et les jours restants + let badgeStyle = ''; + let badgeText = ''; + let badgeIcon = '✓'; + + const daysRemaining = license.days_remaining || 0; + const licenseType = license.type_name || license.type || 'Unknown'; + + if (daysRemaining <= 0) { + // Expirée + badgeStyle = 'background: linear-gradient(135deg, #f44336 0%, #da190b 100%);'; + badgeText = '⚠ Licence expirée'; + badgeIcon = '⚠'; + } else if (daysRemaining <= 7) { + // Expire bientôt + badgeStyle = 'background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);'; + badgeText = `⚠ ${licenseType} - ${daysRemaining}j restants`; + badgeIcon = '⚠'; + } else if (license.type === 'trial') { + // Essai + badgeStyle = 'background: linear-gradient(135deg, #9c27b0 0%, #7b1fa2 100%);'; + badgeText = `⏱ Essai - ${daysRemaining}j restants`; + badgeIcon = '⏱'; + } else { + // Licence normale valide + badgeStyle = 'background: linear-gradient(135deg, #4caf50 0%, #2e7d32 100%);'; + badgeText = `✓ ${licenseType}`; + badgeIcon = '✓'; + } + + badge.style.cssText = ` + ${badgeStyle} + color: white; + padding: 10px 16px; + border-radius: 8px; + font-size: 12px; + font-weight: 600; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); + cursor: pointer; + transition: all 0.3s ease; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + text-align: center; + display: flex; + align-items: center; + gap: 8px; + `; + + badge.innerHTML = ` + ${badgeIcon} + ${badgeText} + `; + + // Click pour voir les détails + badge.onclick = function() { + showLicenseDetails(license); + }; + + // Hover effect + badge.addEventListener('mouseenter', function() { + this.style.transform = 'translateY(-2px)'; + this.style.boxShadow = '0 6px 20px rgba(0, 0, 0, 0.3)'; + }); + + badge.addEventListener('mouseleave', function() { + this.style.transform = 'translateY(0)'; + this.style.boxShadow = '0 4px 15px rgba(0, 0, 0, 0.2)'; + }); + + container.appendChild(badge); +} + +// ============================================ +// MODAL DE DÉTAILS DE LICENCE +// ============================================ + +function showLicenseDetails(license) { + const modal = document.createElement('div'); + modal.id = 'license-details-modal'; + modal.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + `; + + const features = (license.features || []).map(f => `${f}`).join(''); + + const limits = license.limits || {}; + const maxApps = limits.max_apps === -1 ? 'Illimité' : limits.max_apps; + const maxPushes = limits.max_pushes_per_day === -1 ? 'Illimité' : limits.max_pushes_per_day + '/jour'; + + modal.innerHTML = ` +
+
+

📋 Détails de la licence

+ +
+ +
+
+
+
Type
+
${license.type_name || license.type}
+
+
+
ID
+
${license.license_id}
+
+
+
Expire
+
${license.expires}
+
+
+
Jours restants
+
${license.days_remaining}
+
+
+
+ +
+
Client
+
${license.customer?.name || 'N/A'} (${license.customer?.email || 'N/A'})
+
+ +
+
Hostname
+
${license.hostname || currentHostname}
+
+ +
+
Limites
+
Apps: ${maxApps} | Pushes: ${maxPushes}
+
+ +
+
Fonctionnalités
+
${features || 'Aucune'}
+
+ +
+ + +
+
+ `; + + document.body.appendChild(modal); +} + +// ============================================ +// MODAL D'UPLOAD DE LICENCE +// ============================================ + +function showLicenseModal(error = null, errorCode = null, hostname = null) { + console.log("Showing license modal", { error, errorCode, hostname }); + + // Supprimer l'ancien modal s'il existe + hideLicenseModal(); + const detailsModal = document.getElementById('license-details-modal'); + if (detailsModal) detailsModal.remove(); + + const modal = document.createElement('div'); + modal.id = 'license-modal'; + modal.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + `; + + // Message d'erreur si présent + let errorHtml = ''; + if (error) { + let errorStyle = 'background: #ffebee; color: #c62828; border-left: 4px solid #f44336;'; + if (errorCode === 'NO_LICENSE') { + errorStyle = 'background: #fff3e0; color: #e65100; border-left: 4px solid #ff9800;'; + } + errorHtml = ` +
+ ${error} +
+ `; + } + + modal.innerHTML = ` +
+
+

🔐 Git Pusher

+

Activation de licence requise

+
+ + ${errorHtml} + +
+

+ 📋 Hostname Splunk:
+ ${hostname || 'Chargement...'} +

+

+ Communiquez ce hostname pour obtenir votre licence. +

+
+ +
+ + +
+
📁
+
+ Glissez votre fichier .lic ici
+ ou cliquez pour sélectionner +
+ +
+ + +
+ + + + + + + + ${currentLicense ? ` +
+ +
+ ` : ''} +
+ `; + + document.body.appendChild(modal); + + // Récupérer le hostname si pas fourni + if (!hostname) { + fetch(`${LICENSE_API_URL}/license/hostname`) + .then(r => r.json()) + .then(d => { + const codeEl = modal.querySelector('code'); + if (codeEl) codeEl.textContent = d.hostname || 'Inconnu'; + currentHostname = d.hostname; + }) + .catch(() => {}); + } +} + +function hideLicenseModal() { + const modal = document.getElementById('license-modal'); + if (modal) modal.remove(); +} + +// ============================================ +// GESTION DU FICHIER +// ============================================ + +let selectedLicenseContent = null; + +function handleDragOver(event) { + event.preventDefault(); + event.stopPropagation(); + event.currentTarget.style.borderColor = '#667eea'; + event.currentTarget.style.background = '#f0f4ff'; +} + +function handleDragLeave(event) { + event.preventDefault(); + event.stopPropagation(); + event.currentTarget.style.borderColor = '#ccc'; + event.currentTarget.style.background = '#fafafa'; +} + +function handleDrop(event) { + event.preventDefault(); + event.stopPropagation(); + event.currentTarget.style.borderColor = '#ccc'; + event.currentTarget.style.background = '#fafafa'; + + const files = event.dataTransfer.files; + if (files.length > 0) { + processFile(files[0]); + } +} + +function handleFileSelect(event) { + const files = event.target.files; + if (files.length > 0) { + processFile(files[0]); + } +} + +function processFile(file) { + console.log("Processing file:", file.name); + + if (!file.name.endsWith('.lic')) { + showLicenseMessage('Veuillez sélectionner un fichier .lic', 'error'); + return; + } + + const reader = new FileReader(); + reader.onload = function(e) { + selectedLicenseContent = e.target.result; + + // Afficher le nom du fichier + document.getElementById('selected-file-info').style.display = 'block'; + document.getElementById('selected-file-name').textContent = '📄 ' + file.name; + + // Activer le bouton + const btn = document.getElementById('activate-btn'); + btn.disabled = false; + btn.style.opacity = '1'; + btn.style.cursor = 'pointer'; + + showLicenseMessage('Fichier prêt à être activé', 'info'); + }; + reader.onerror = function() { + showLicenseMessage('Erreur de lecture du fichier', 'error'); + }; + reader.readAsText(file); +} + +function clearSelectedFile() { + selectedLicenseContent = null; + document.getElementById('selected-file-info').style.display = 'none'; + document.getElementById('license-file-input').value = ''; + + const btn = document.getElementById('activate-btn'); + btn.disabled = true; + btn.style.opacity = '0.5'; + + const msgEl = document.getElementById('license-message'); + msgEl.style.display = 'none'; +} + +// ============================================ +// UPLOAD ET ACTIVATION +// ============================================ + +async function uploadLicense() { + if (!selectedLicenseContent) { + showLicenseMessage('Veuillez sélectionner un fichier de licence', 'error'); + return; + } + + showLicenseMessage('⏳ Validation en cours...', 'info'); + + const btn = document.getElementById('activate-btn'); + btn.disabled = true; + btn.textContent = 'Validation...'; + + try { + const response = await fetch(`${LICENSE_API_URL}/license/upload`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + license_content: selectedLicenseContent + }) + }); + + const result = await response.json(); + console.log("Upload result:", result); + + if (result.success) { + showLicenseMessage('✓ Licence activée avec succès!', 'success'); + currentLicense = result.license_info; + + setTimeout(() => { + hideLicenseModal(); + displayLicenseInfo(result.license_info); + }, 1500); + } else { + showLicenseMessage(result.error || 'Erreur d\'activation', 'error'); + btn.disabled = false; + btn.textContent = 'Activer la licence'; + } + } catch (error) { + console.error("Upload error:", error); + showLicenseMessage('Erreur de connexion au serveur', 'error'); + btn.disabled = false; + btn.textContent = 'Activer la licence'; + } +} + +// ============================================ +// UTILITAIRES +// ============================================ + +function showLicenseMessage(message, type) { + const msgEl = document.getElementById('license-message'); + if (!msgEl) return; + + msgEl.style.display = 'block'; + msgEl.textContent = message; + + switch (type) { + case 'success': + msgEl.style.background = '#e8f5e9'; + msgEl.style.color = '#2e7d32'; + msgEl.style.border = '1px solid #a5d6a7'; + break; + case 'error': + msgEl.style.background = '#ffebee'; + msgEl.style.color = '#c62828'; + msgEl.style.border = '1px solid #ef9a9a'; + break; + case 'info': + msgEl.style.background = '#e3f2fd'; + msgEl.style.color = '#1565c0'; + msgEl.style.border = '1px solid #90caf9'; + break; + } +} + +function showContactInfo() { + alert(`Pour obtenir une licence Git Pusher: + +1. Copiez votre hostname Splunk affiché ci-dessus +2. Contactez-nous avec: + - Votre hostname + - Votre email + - Le type de licence souhaité + +Email: support@gitpusher.com +Site: https://gitpusher.com`); +} + +// ============================================ +// VÉRIFICATION AVANT PUSH +// ============================================ + +async function checkLicenseBeforePush() { + try { + const response = await fetch(`${LICENSE_API_URL}/license/status`); + const data = await response.json(); + + if (data.status !== 'valid') { + alert('⚠️ ' + (data.error || 'Licence invalide ou absente')); + showLicenseModal(data.error, data.error_code, data.hostname); + return false; + } + + // Vérifier les limites + const limits = data.license?.limits || {}; + const usage = data.usage || {}; + + if (limits.max_pushes_per_day > 0 && usage.pushes_today >= limits.max_pushes_per_day) { + alert(`⚠️ Limite quotidienne atteinte (${limits.max_pushes_per_day} pushes/jour)`); + return false; + } + + return true; + } catch (error) { + console.error("License check error:", error); + alert('⚠️ Impossible de vérifier la licence'); + return false; + } +} + +// ============================================ +// EXPORT POUR UTILISATION EXTERNE +// ============================================ + +window.LicenseManager = { + init: initializeLicense, + check: checkLicenseBeforePush, + showModal: showLicenseModal, + getLicense: () => currentLicense, + getHostname: () => currentHostname +}; diff --git a/apps/pusher_app_prem/appserver/static/license_validation.js_old2 b/apps/pusher_app_prem/appserver/static/license_validation.js_old2 new file mode 100644 index 00000000..f8a2b16f --- /dev/null +++ b/apps/pusher_app_prem/appserver/static/license_validation.js_old2 @@ -0,0 +1,1104 @@ +// ============================================ +// GIT PUSHER - LICENSE VALIDATION (RSA) +// Version 2.1 - 100% Client-Side Validation +// ============================================ + +// Configuration +const LICENSE_CONFIG = { + storageKey: 'git_pusher_license', + usageKey: 'git_pusher_usage', + version: '2.1.0' +}; + +// ============================================ +// CLÉ PUBLIQUE RSA +// ============================================ +// Cette clé est générée par le vendeur avec license_generator_rsa.py +// Commande: python3 license_generator_rsa.py export-key + +const PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnj2hOg61Q9k9iz4U5F7I +RdaJrpLTG+orz0/Kpbz2HSxbAVXkvL5GvYVfxROjy0UgxOZFycZAaGN2am+5CDHA +D1dTL9KCPhEaPqw4XTFnf6Ur5VG0+SftugTdTcyxRe614Z+i61/2ahk/vKG9D4kB +j4qV4se4lLk993lEaQrOXkXCbZ8royB5MPeOchPxZd7SDzoovEcyUmf2Fa2eYk6U +WmbrymCnJRsfxEVZofQQyp1ILS8KuSxaquXvMWm3cXV2Krs/3E5ax0vBPMrZRL+o +Vn7/dVnzbOlbifeosTYaad1DLd7NEgst3OFUv+dSH5hcCCc36IHMSvxcJ9l7s2kv +KbEFPeh582JNmRoMMNPRbd+/ZVDeJ/oB344+TtB6VeQ2GQyOyoggiLryZujg3WDE +shwLkFwiYGa/zEct0qs2/HBS1FOAqrLiPdQYJTx+RhrXhTni/p3H42L+2xJcbnki +fAgn8ND5k2yQw2gk4AlLqq2y01m0jsHdjaOfhKzzPLyzt1En/KAnCWJSzB+jur4z +fd70R4pLTYfawr2NTvTAhOtiOIjWj380oGmxeKCNJT1P4Dq8Yl+OxKLBy4cnUlgV +sbpKJug7Goth4g7bmCVMhC4bf7JB/iTrrS8DhMaWaZX/FeFloM3yYyo/gzmAY19z +sUdQuBpxCwIf/J7Q4dYsDkMCAwEAAQ== +-----END PUBLIC KEY-----`; + +// ============================================ +// UTILITAIRES CRYPTO +// ============================================ + +/** + * Convertir une chaîne PEM en ArrayBuffer + */ +function pemToArrayBuffer(pem) { + const b64 = pem + .replace(/-----BEGIN PUBLIC KEY-----/, '') + .replace(/-----END PUBLIC KEY-----/, '') + .replace(/\s/g, ''); + const binary = atob(b64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return bytes.buffer; +} + +/** + * Importer la clé publique RSA + */ +async function importPublicKey() { + try { + const keyData = pemToArrayBuffer(PUBLIC_KEY_PEM); + const key = await crypto.subtle.importKey( + 'spki', + keyData, + { + name: 'RSA-PSS', + hash: 'SHA-256' + }, + false, + ['verify'] + ); + return key; + } catch (error) { + console.error('Erreur import clé publique:', error); + return null; + } +} + +/** + * Vérifier la signature RSA-PSS + */ +async function verifySignature(data, signatureB64, publicKey) { + try { + const signature = Uint8Array.from(atob(signatureB64), c => c.charCodeAt(0)); + const dataBuffer = new TextEncoder().encode(data); + + const isValid = await crypto.subtle.verify( + { + name: 'RSA-PSS', + saltLength: 222 // MAX_LENGTH pour RSA-4096 avec SHA-256 + }, + publicKey, + signature, + dataBuffer + ); + + return isValid; + } catch (error) { + console.error('Erreur vérification signature:', error); + return false; + } +} + +// ============================================ +// FONCTIONS UTILITAIRES +// ============================================ + +/** + * Décoder Base64 + */ +function base64Decode(str) { + try { + return decodeURIComponent(atob(str).split('').map(function(c) { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }).join('')); + } catch (e) { + return atob(str); + } +} + +/** + * Obtenir le hostname actuel + */ +function getCurrentHostname() { + // En environnement Splunk, on utilise le hostname du serveur + // On peut aussi le récupérer via l'API Splunk si disponible + return window.location.hostname.toLowerCase(); +} + +/** + * Obtenir le hostname Splunk via l'API (si disponible) + */ +async function getSplunkHostname() { + try { + const response = await fetch('/services/server/info?output_mode=json', { + credentials: 'include' + }); + if (response.ok) { + const data = await response.json(); + const serverName = data.entry?.[0]?.content?.serverName; + if (serverName) { + return serverName.toLowerCase(); + } + } + } catch (e) { + console.log('Impossible de récupérer le hostname Splunk, utilisation du hostname web'); + } + return getCurrentHostname(); +} + +/** + * Calculer les jours restants + */ +function daysRemaining(expiryDate) { + const expiry = new Date(expiryDate); + const now = new Date(); + const diff = expiry - now; + return Math.ceil(diff / (1000 * 60 * 60 * 24)); +} + +// ============================================ +// PARSING DE LICENCE +// ============================================ + +/** + * Parser le contenu d'un fichier .lic + */ +function parseLicenseFile(content) { + try { + const lines = content.trim().split('\n'); + let payloadB64 = null; + + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && !trimmed.startsWith('#')) { + payloadB64 = trimmed; + break; + } + } + + if (!payloadB64) { + return { error: 'Payload non trouvé dans le fichier' }; + } + + // Décoder le payload + const payloadJson = base64Decode(payloadB64); + const payload = JSON.parse(payloadJson); + + if (!payload.license || !payload.signature) { + return { error: 'Format de licence invalide' }; + } + + // Décoder les données de licence + const licenseJson = base64Decode(payload.license); + const licenseData = JSON.parse(licenseJson); + + return { + success: true, + licenseJson: licenseJson, + licenseData: licenseData, + signatureB64: payload.signature, + rawPayload: payloadB64 + }; + } catch (error) { + console.error('Erreur parsing licence:', error); + return { error: 'Erreur de lecture du fichier de licence' }; + } +} + +// ============================================ +// VALIDATION DE LICENCE +// ============================================ + +/** + * Valider une licence complète (signature RSA + hostname + expiration) + */ +async function validateLicense(licenseContent = null) { + try { + // Si pas de contenu fourni, charger depuis le localStorage + let parsed; + if (licenseContent) { + parsed = parseLicenseFile(licenseContent); + } else { + const stored = localStorage.getItem(LICENSE_CONFIG.storageKey); + if (!stored) { + return { + valid: false, + error: 'Aucune licence installée', + error_code: 'NO_LICENSE' + }; + } + parsed = JSON.parse(stored); + } + + if (parsed.error) { + return { + valid: false, + error: parsed.error, + error_code: 'PARSE_ERROR' + }; + } + + const { licenseJson, licenseData, signatureB64 } = parsed; + + // 1. Vérifier la signature RSA + console.log('Vérification de la signature RSA...'); + const publicKey = await importPublicKey(); + + if (!publicKey) { + return { + valid: false, + error: 'Impossible de charger la clé publique', + error_code: 'KEY_ERROR' + }; + } + + const signatureValid = await verifySignature(licenseJson, signatureB64, publicKey); + + if (!signatureValid) { + return { + valid: false, + error: 'Signature de licence invalide', + error_code: 'INVALID_SIGNATURE' + }; + } + + console.log('✓ Signature RSA valide'); + + // 2. Vérifier le hostname + const expectedHostname = (licenseData.hostname || '').toLowerCase(); + const currentHostname = await getSplunkHostname(); + + console.log(`Hostname attendu: ${expectedHostname}, actuel: ${currentHostname}`); + + // Permettre une correspondance partielle ou exacte + if (expectedHostname && expectedHostname !== currentHostname) { + // Vérifier si c'est une correspondance partielle (le hostname peut être un FQDN) + if (!currentHostname.includes(expectedHostname) && !expectedHostname.includes(currentHostname)) { + return { + valid: false, + error: `Licence non valide pour ce serveur. Attendu: ${expectedHostname}, Actuel: ${currentHostname}`, + error_code: 'HOSTNAME_MISMATCH', + expected_hostname: expectedHostname, + current_hostname: currentHostname + }; + } + } + + console.log('✓ Hostname valide'); + + // 3. Vérifier la date d'expiration + const expiryDate = licenseData.expires; + if (expiryDate) { + const days = daysRemaining(expiryDate); + + if (days < 0) { + return { + valid: false, + error: `Licence expirée le ${expiryDate}`, + error_code: 'LICENSE_EXPIRED', + expires: expiryDate + }; + } + + console.log(`✓ Licence valide (${days} jours restants)`); + } + + // Licence valide ! + return { + valid: true, + license_id: licenseData.license_id, + type: licenseData.type, + type_name: licenseData.type_name, + customer: licenseData.customer, + hostname: expectedHostname, + issued: licenseData.issued, + expires: expiryDate, + days_remaining: daysRemaining(expiryDate), + limits: licenseData.limits || {}, + features: licenseData.features || [] + }; + + } catch (error) { + console.error('Erreur validation licence:', error); + return { + valid: false, + error: error.message, + error_code: 'VALIDATION_ERROR' + }; + } +} + +/** + * Vérifier si une fonctionnalité est disponible + */ +async function hasFeature(featureName) { + const validation = await validateLicense(); + if (!validation.valid) return false; + return validation.features.includes(featureName); +} + +// ============================================ +// GESTION DU STOCKAGE +// ============================================ + +/** + * Sauvegarder une licence validée + */ +async function saveLicense(licenseContent) { + try { + // Parser le fichier + const parsed = parseLicenseFile(licenseContent); + + if (parsed.error) { + return { + success: false, + error: parsed.error + }; + } + + // Valider la licence avant de sauvegarder + const validation = await validateLicense(licenseContent); + + if (!validation.valid) { + return { + success: false, + error: validation.error, + error_code: validation.error_code + }; + } + + // Sauvegarder dans localStorage + localStorage.setItem(LICENSE_CONFIG.storageKey, JSON.stringify(parsed)); + + console.log('✓ Licence sauvegardée avec succès'); + + return { + success: true, + license: validation + }; + + } catch (error) { + console.error('Erreur sauvegarde licence:', error); + return { + success: false, + error: error.message + }; + } +} + +/** + * Supprimer la licence + */ +function removeLicense() { + localStorage.removeItem(LICENSE_CONFIG.storageKey); + localStorage.removeItem(LICENSE_CONFIG.usageKey); + console.log('Licence supprimée'); +} + +/** + * Récupérer les infos de licence (sans revalider la signature) + */ +function getLicenseInfo() { + try { + const stored = localStorage.getItem(LICENSE_CONFIG.storageKey); + if (!stored) return null; + + const parsed = JSON.parse(stored); + return parsed.licenseData; + } catch { + return null; + } +} + +// ============================================ +// GESTION DES LIMITES D'UTILISATION +// ============================================ + +/** + * Obtenir les stats d'utilisation + */ +function getUsageStats() { + try { + const stored = localStorage.getItem(LICENSE_CONFIG.usageKey); + if (stored) { + return JSON.parse(stored); + } + } catch {} + + return { + total_pushes: 0, + pushes_today: 0, + last_push_date: null + }; +} + +/** + * Incrémenter le compteur d'utilisation + */ +function incrementUsage() { + const stats = getUsageStats(); + const today = new Date().toISOString().split('T')[0]; + + // Reset si nouveau jour + if (stats.last_push_date !== today) { + stats.pushes_today = 0; + stats.last_push_date = today; + } + + stats.total_pushes = (stats.total_pushes || 0) + 1; + stats.pushes_today = (stats.pushes_today || 0) + 1; + + localStorage.setItem(LICENSE_CONFIG.usageKey, JSON.stringify(stats)); + return stats; +} + +/** + * Vérifier les limites avant un push + */ +async function checkLimits() { + const validation = await validateLicense(); + + if (!validation.valid) { + return { + allowed: false, + error: validation.error, + error_code: validation.error_code + }; + } + + const limits = validation.limits || {}; + const maxPushes = limits.max_pushes_per_day || -1; + + if (maxPushes > 0) { + const stats = getUsageStats(); + const today = new Date().toISOString().split('T')[0]; + + // Reset si nouveau jour + let pushesToday = stats.pushes_today || 0; + if (stats.last_push_date !== today) { + pushesToday = 0; + } + + if (pushesToday >= maxPushes) { + return { + allowed: false, + error: `Limite quotidienne atteinte (${maxPushes} pushes/jour)`, + error_code: 'DAILY_LIMIT_REACHED' + }; + } + } + + return { + allowed: true, + license_type: validation.type_name, + remaining_today: maxPushes > 0 ? maxPushes - getUsageStats().pushes_today : -1 + }; +} + +// ============================================ +// INTERFACE UTILISATEUR +// ============================================ + +/** + * Afficher le badge de licence + */ +async function updateLicenseBadge() { + const container = document.getElementById('license-badge-container'); + if (!container) return; + + const validation = await validateLicense(); + + let badgeHtml = ''; + + if (validation.valid) { + const daysLeft = validation.days_remaining; + let badgeClass = 'license-badge-valid'; + let icon = '✓'; + + if (daysLeft <= 7) { + badgeClass = 'license-badge-expiring'; + icon = '⚠️'; + } else if (daysLeft <= 30) { + badgeClass = 'license-badge-warning'; + icon = '⏳'; + } + + badgeHtml = ` +
+ ${icon} + ${validation.type_name} + ${daysLeft}j +
+ `; + } else { + badgeHtml = ` +
+ 🔐 + Activer +
+ `; + } + + container.innerHTML = badgeHtml; +} + +/** + * Afficher le modal de licence + */ +function showLicenseModal(message = null, errorCode = null) { + // Supprimer l'ancien modal s'il existe + const existingModal = document.getElementById('license-modal'); + if (existingModal) existingModal.remove(); + + const modal = document.createElement('div'); + modal.id = 'license-modal'; + modal.className = 'license-modal-overlay'; + + let errorMessage = ''; + if (message) { + errorMessage = `
${message}
`; + } + + modal.innerHTML = ` +
+
+

🔐 Activation de Licence

+ +
+ +
+ ${errorMessage} + +
+
📄
+
+ Glissez-déposez votre fichier .lic ici +
ou cliquez pour sélectionner +
+ +
+ +
+ Hostname du serveur: + Chargement... +
Communiquez ce hostname pour obtenir votre licence +
+ +
+
+ + +
+ `; + + document.body.appendChild(modal); + + // Afficher le hostname + getSplunkHostname().then(hostname => { + const hostnameDisplay = document.getElementById('current-hostname-display'); + if (hostnameDisplay) { + hostnameDisplay.textContent = hostname; + } + }); + + // Configurer le drag & drop + setupLicenseUpload(); +} + +/** + * Configurer l'upload de licence + */ +function setupLicenseUpload() { + const dropZone = document.getElementById('license-upload-zone'); + const fileInput = document.getElementById('license-file-input'); + + if (!dropZone || !fileInput) return; + + // Clic pour sélectionner + dropZone.addEventListener('click', () => fileInput.click()); + + // Drag & drop + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('dragover'); + }); + + dropZone.addEventListener('dragleave', () => { + dropZone.classList.remove('dragover'); + }); + + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('dragover'); + + const files = e.dataTransfer.files; + if (files.length > 0) { + handleLicenseFile(files[0]); + } + }); + + // Sélection de fichier + fileInput.addEventListener('change', (e) => { + if (e.target.files.length > 0) { + handleLicenseFile(e.target.files[0]); + } + }); +} + +/** + * Traiter le fichier de licence uploadé + */ +async function handleLicenseFile(file) { + const resultDiv = document.getElementById('license-validation-result'); + if (!resultDiv) return; + + // Vérifier l'extension + if (!file.name.endsWith('.lic')) { + resultDiv.innerHTML = ` +
+ ❌ Le fichier doit avoir l'extension .lic +
+ `; + return; + } + + resultDiv.innerHTML = ` +
+ ⏳ Validation en cours... +
+ `; + + try { + // Lire le fichier + const content = await file.text(); + + // Sauvegarder et valider + const result = await saveLicense(content); + + if (result.success) { + const license = result.license; + resultDiv.innerHTML = ` +
+
+
+ Licence activée avec succès !
+ Type: ${license.type_name}
+ Expire: ${license.expires} (${license.days_remaining} jours)
+ Client: ${license.customer?.name || 'N/A'} +
+
+ `; + + // Mettre à jour le badge + updateLicenseBadge(); + + // Fermer le modal après 3 secondes + setTimeout(() => { + closeLicenseModal(); + }, 3000); + + } else { + resultDiv.innerHTML = ` +
+ ❌ ${result.error} +
+ `; + } + + } catch (error) { + resultDiv.innerHTML = ` +
+ ❌ Erreur: ${error.message} +
+ `; + } +} + +/** + * Fermer le modal de licence + */ +function closeLicenseModal() { + const modal = document.getElementById('license-modal'); + if (modal) modal.remove(); +} + +/** + * Afficher les détails de la licence + */ +async function showLicenseDetails() { + const validation = await validateLicense(); + + if (!validation.valid) { + showLicenseModal(validation.error, validation.error_code); + return; + } + + // Supprimer l'ancien modal s'il existe + const existingModal = document.getElementById('license-details-modal'); + if (existingModal) existingModal.remove(); + + const modal = document.createElement('div'); + modal.id = 'license-details-modal'; + modal.className = 'license-modal-overlay'; + + const features = validation.features.map(f => `${f}`).join(' '); + const limits = validation.limits; + const maxApps = limits.max_apps === -1 ? '∞' : limits.max_apps; + const maxPushes = limits.max_pushes_per_day === -1 ? '∞' : limits.max_pushes_per_day; + + const usage = getUsageStats(); + + modal.innerHTML = ` +
+
+

📋 Détails de la Licence

+ +
+ +
+ + + + + + + + + + + +
ID${validation.license_id}
Type${validation.type_name}
Client${validation.customer?.name || 'N/A'}
Email${validation.customer?.email || 'N/A'}
Hostname${validation.hostname}
Émise le${validation.issued}
Expire le${validation.expires}
Jours restants${validation.days_remaining}
Apps max${maxApps}
Pushes/jour${maxPushes}
+ +
+ Fonctionnalités:
+
${features}
+
+ +
+ Utilisation:
+ Total pushes: ${usage.total_pushes} | Aujourd'hui: ${usage.pushes_today} +
+
+ + +
+ `; + + document.body.appendChild(modal); +} + +/** + * Confirmer la suppression de licence + */ +function confirmRemoveLicense() { + if (confirm('Êtes-vous sûr de vouloir supprimer cette licence ?')) { + removeLicense(); + + // Fermer le modal des détails + const detailsModal = document.getElementById('license-details-modal'); + if (detailsModal) detailsModal.remove(); + + // Mettre à jour le badge + updateLicenseBadge(); + + alert('Licence supprimée.'); + } +} + +/** + * Vérifier la licence avant un push (appelé par git_pusher.js) + */ +async function checkLicenseBeforePush() { + const result = await checkLimits(); + + if (!result.allowed) { + showLicenseModal(result.error, result.error_code); + return false; + } + + return true; +} + +// ============================================ +// STYLES CSS +// ============================================ + +const licenseStyles = ` + +`; + +// ============================================ +// INITIALISATION +// ============================================ + +/** + * Initialiser le système de licence + */ +function initializeLicense() { + console.log('Git Pusher License System v' + LICENSE_CONFIG.version + ' (RSA)'); + + // Injecter les styles + if (!document.getElementById('license-styles')) { + const styleEl = document.createElement('div'); + styleEl.id = 'license-styles'; + styleEl.innerHTML = licenseStyles; + document.head.appendChild(styleEl); + } + + // Mettre à jour le badge + updateLicenseBadge(); +} + +// Exposer les fonctions globalement +window.initializeLicense = initializeLicense; +window.validateLicense = validateLicense; +window.saveLicense = saveLicense; +window.removeLicense = removeLicense; +window.checkLicenseBeforePush = checkLicenseBeforePush; +window.showLicenseModal = showLicenseModal; +window.closeLicenseModal = closeLicenseModal; +window.showLicenseDetails = showLicenseDetails; +window.confirmRemoveLicense = confirmRemoveLicense; +window.updateLicenseBadge = updateLicenseBadge; +window.hasFeature = hasFeature; +window.incrementUsage = incrementUsage; +window.getUsageStats = getUsageStats; +window.getLicenseInfo = getLicenseInfo; + +// Auto-initialiser après le chargement +if (document.readyState === 'complete') { + setTimeout(initializeLicense, 100); +} else { + window.addEventListener('load', function() { + setTimeout(initializeLicense, 100); + }); +} diff --git a/apps/pusher_app_prem/bin/__pycache__/license_validator.cpython-39.pyc b/apps/pusher_app_prem/bin/__pycache__/license_validator.cpython-39.pyc old mode 100755 new mode 100644 index fe7ac122ded7e88c7e18a91439df63847cd5bb1f..476fc41c2a11120284974f5cf3a05a5aaf69daf1 GIT binary patch literal 9715 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 diff --git a/apps/pusher_app_prem/bin/git_pusher.pid b/apps/pusher_app_prem/bin/git_pusher.pid old mode 100755 new mode 100644 index d18a9fa7..16070a80 --- a/apps/pusher_app_prem/bin/git_pusher.pid +++ b/apps/pusher_app_prem/bin/git_pusher.pid @@ -1 +1 @@ -770390 +3764537 diff --git a/apps/pusher_app_prem/bin/git_pusher.py b/apps/pusher_app_prem/bin/git_pusher.py old mode 100755 new mode 100644 index 42ac57c7..ee1a36bb --- a/apps/pusher_app_prem/bin/git_pusher.py +++ b/apps/pusher_app_prem/bin/git_pusher.py @@ -235,7 +235,7 @@ class GitPusherRequestHandler(BaseHTTPRequestHandler): elif path == '/license/delete': # Supprimer la licence - license_path = "/opt/splunk/etc/apps/pusher_app_prem/local/license.lic" + license_path = "/opt/splunk/etc/apps/pusher_app/local/license.lic" if os.path.exists(license_path): os.remove(license_path) response = {"success": True, "message": "Licence supprimée"} @@ -250,34 +250,26 @@ class GitPusherRequestHandler(BaseHTTPRequestHandler): # ============================================ elif path == '/push' or path.startswith('/services/'): - # Vérifier la licence avant le push - license_check = check_limits() - if not license_check.get("allowed"): - response = { - "status": "error", - "error_code": "LICENSE_ERROR", - "message": license_check.get("error", "Licence invalide ou limite atteinte") - } - self.wfile.write(json.dumps(response).encode()) - return + # NOTE: La vérification de licence est maintenant faite côté client (JavaScript) + # avec validation RSA. Le serveur fait confiance au client. + # Si vous voulez réactiver la vérification serveur, décommentez le bloc ci-dessous: + # + # license_check = check_limits() + # if not license_check.get("allowed"): + # response = { + # "status": "error", + # "error_code": "LICENSE_ERROR", + # "message": license_check.get("error", "Licence invalide ou limite atteinte") + # } + # self.wfile.write(json.dumps(response).encode()) + # return # Traiter le push Git self.handle_git_push(query_params) return else: - # Ancien comportement pour compatibilité - # Vérifier la licence - license_check = check_limits() - if not license_check.get("allowed"): - response = { - "status": "error", - "error_code": "LICENSE_ERROR", - "message": license_check.get("error", "Licence invalide ou limite atteinte") - } - self.wfile.write(json.dumps(response).encode()) - return - + # Traiter comme un push Git (compatibilité) self.handle_git_push(query_params) except Exception as e: @@ -306,6 +298,10 @@ class GitPusherRequestHandler(BaseHTTPRequestHandler): sh_auth_user = query_params.get('sh_auth_user', [''])[0] sh_auth_pass = query_params.get('sh_auth_pass', [''])[0] + # Paramètres de licence (envoyés par le client) + license_type = query_params.get('license_type', [''])[0] + license_id = query_params.get('license_id', [''])[0] + logger.info(f"Parameters: git_url={git_url}, branch={git_branch}, user={user}, deploy_to_shcluster={deploy_to_shcluster}") # Parser les apps @@ -317,17 +313,8 @@ class GitPusherRequestHandler(BaseHTTPRequestHandler): logger.info(f"Parsed apps: {len(apps)} items") - # Vérifier les limites d'apps - license_info = validate_license() - max_apps = license_info.get("limits", {}).get("max_apps", -1) - if max_apps > 0 and len(apps) > max_apps: - response = { - "status": "error", - "error_code": "APP_LIMIT", - "message": f"Votre licence permet {max_apps} apps max. Vous en avez sélectionné {len(apps)}." - } - self.wfile.write(json.dumps(response).encode()) - return + # NOTE: La vérification des limites est maintenant faite côté client + # Le serveur fait confiance aux informations envoyées par le client # Valider les paramètres if not git_url or not git_token or not commit_message or not apps: @@ -381,11 +368,10 @@ class GitPusherRequestHandler(BaseHTTPRequestHandler): # Commit et push subprocess.run(['git', 'add', '-A'], cwd=temp_dir, capture_output=True) - # Ajouter les infos de licence au commit - license_info = validate_license() + # Message de commit avec infos de licence (envoyées par le client) full_message = f"{commit_message}\n\n" full_message += f"Pushed by: {user}\n" - full_message += f"License: {license_info.get('license_id', 'N/A')} ({license_info.get('type_name', 'N/A')})\n" + full_message += f"License: {license_id or 'N/A'} ({license_type or 'N/A'})\n" full_message += f"Timestamp: {datetime.now().isoformat()}" result = subprocess.run(['git', 'commit', '-m', full_message], @@ -441,7 +427,7 @@ class GitPusherRequestHandler(BaseHTTPRequestHandler): "status": "success", "message": f"Successfully pushed {len(app_directories)} application(s) to Git", "apps_pushed": len(app_directories), - "license_type": license_info.get("type_name", "N/A") + "license_type": license_type or "N/A" } # Ajouter les infos de déploiement si activé diff --git a/apps/pusher_app_prem/bin/git_pusher.py_old b/apps/pusher_app_prem/bin/git_pusher.py_old new file mode 100755 index 00000000..42ac57c7 --- /dev/null +++ b/apps/pusher_app_prem/bin/git_pusher.py_old @@ -0,0 +1,758 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Git Pusher - Main Server +Serveur HTTP pour pousser les applications Splunk vers Git +et déployer vers le Search Head Cluster via le SH Deployer + +Avec système de licence par fichier .lic +""" + +import sys +import os +import json +import logging +import tempfile +import shutil +import subprocess +import ssl +import urllib.request +import urllib.error +from datetime import datetime +from http.server import HTTPServer, BaseHTTPRequestHandler +from urllib.parse import parse_qs, urlparse + +# Importer le validateur de licence +# En production, ce fichier sera dans le même dossier +try: + from license_validator import ( + validate_license, + save_license_file, + check_limits, + increment_usage, + get_splunk_hostname, + get_usage_stats, + parse_license_content + ) +except ImportError: + # Fallback pour le développement + print("Warning: license_validator not found, running without license checks") + def validate_license(): return {"valid": True, "type": "dev", "days_remaining": 999} + def save_license_file(c): return {"success": True} + def check_limits(): return {"allowed": True} + def increment_usage(): return {} + def get_splunk_hostname(): return "dev-host" + def get_usage_stats(): return {} + def parse_license_content(c): return {} + +# ============================================ +# CONFIGURATION SH DEPLOYER +# ============================================ + +# Configuration du SH Deployer (peut être surchargée par les paramètres) +SH_DEPLOYER_CONFIG = { + "enabled": True, + "host": "10.10.40.14", + "port": 9998, + "use_ssl": True, + "token": "deployer_agent_secret_token_change_me_in_production", + "timeout": 30 +} + +# Configuration du logging +log_dir = '/opt/splunk/var/log/splunk' +os.makedirs(log_dir, exist_ok=True) + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(os.path.join(log_dir, 'git_pusher.log')), + logging.StreamHandler() + ] +) +logger = logging.getLogger('git_pusher') + + +class GitPusherRequestHandler(BaseHTTPRequestHandler): + """Handler pour les requêtes HTTP""" + + def send_cors_headers(self): + """Envoyer les headers CORS complets""" + # Permettre toutes les origines + origin = self.headers.get('Origin', '*') + self.send_header('Access-Control-Allow-Origin', origin) + self.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS, PUT, DELETE') + self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, Accept, Origin') + self.send_header('Access-Control-Allow-Credentials', 'true') + self.send_header('Access-Control-Max-Age', '86400') # Cache preflight 24h + + def do_OPTIONS(self): + """Traiter les requêtes OPTIONS (CORS preflight)""" + logger.info(f"OPTIONS request from {self.headers.get('Origin', 'unknown')}") + self.send_response(200) + self.send_cors_headers() + self.end_headers() + # Important: ne rien écrire dans le body pour OPTIONS + return + + def do_GET(self): + """Traiter les requêtes GET""" + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.send_cors_headers() + self.end_headers() + + try: + parsed_url = urlparse(self.path) + path = parsed_url.path + + # ============================================ + # ENDPOINTS LICENCE + # ============================================ + + if path == '/license' or path == '/license/status': + # Récupérer le statut de la licence + validation = validate_license() + usage = get_usage_stats() + hostname = get_splunk_hostname() + + response = { + "status": "valid" if validation.get("valid") else "invalid", + "hostname": hostname, + "license": validation if validation.get("valid") else None, + "error": validation.get("error") if not validation.get("valid") else None, + "error_code": validation.get("error_code") if not validation.get("valid") else None, + "usage": usage + } + self.wfile.write(json.dumps(response).encode()) + + elif path == '/license/hostname': + # Juste le hostname + response = {"hostname": get_splunk_hostname()} + self.wfile.write(json.dumps(response).encode()) + + elif path == '/health': + # Health check + response = { + "status": "ok", + "service": "git_pusher", + "timestamp": datetime.now().isoformat(), + "sh_deployer": { + "enabled": SH_DEPLOYER_CONFIG.get("enabled", True), + "host": SH_DEPLOYER_CONFIG.get("host"), + "port": SH_DEPLOYER_CONFIG.get("port") + } + } + self.wfile.write(json.dumps(response).encode()) + + # ============================================ + # ENDPOINTS SH DEPLOYER + # ============================================ + + elif path == '/deployer/health': + # Vérifier la santé du SH Deployer + result = call_deployer_agent("/health") + if result.get("success"): + response = { + "status": "ok", + "deployer": result.get("data"), + "config": { + "host": SH_DEPLOYER_CONFIG.get("host"), + "port": SH_DEPLOYER_CONFIG.get("port") + } + } + else: + response = { + "status": "error", + "error": result.get("error"), + "config": { + "host": SH_DEPLOYER_CONFIG.get("host"), + "port": SH_DEPLOYER_CONFIG.get("port") + } + } + self.wfile.write(json.dumps(response).encode()) + + elif path == '/deployer/status': + # Statut du SH Deployer + result = get_deployer_status() + self.wfile.write(json.dumps(result).encode()) + + elif path == '/deployer/config': + # Configuration actuelle du SH Deployer + response = { + "enabled": SH_DEPLOYER_CONFIG.get("enabled", True), + "host": SH_DEPLOYER_CONFIG.get("host"), + "port": SH_DEPLOYER_CONFIG.get("port"), + "use_ssl": SH_DEPLOYER_CONFIG.get("use_ssl", True) + } + self.wfile.write(json.dumps(response).encode()) + + else: + response = {"error": "Unknown endpoint", "path": path} + self.wfile.write(json.dumps(response).encode()) + + except Exception as e: + logger.error(f"GET error: {e}") + self.wfile.write(json.dumps({"error": str(e)}).encode()) + + def do_POST(self): + """Traiter les requêtes POST""" + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.send_cors_headers() + self.end_headers() + + try: + parsed_url = urlparse(self.path) + path = parsed_url.path + query_params = parse_qs(parsed_url.query) + + logger.info(f"POST request to {path}") + + # ============================================ + # ENDPOINTS LICENCE + # ============================================ + + if path == '/license/upload': + # Uploader une nouvelle licence + content_length = int(self.headers.get('Content-Length', 0)) + body = self.rfile.read(content_length).decode('utf-8') + + try: + data = json.loads(body) + license_content = data.get('license_content', '') + except: + license_content = body + + if not license_content: + response = {"success": False, "error": "Contenu de licence vide"} + else: + response = save_license_file(license_content) + + self.wfile.write(json.dumps(response).encode()) + return + + elif path == '/license/delete': + # Supprimer la licence + license_path = "/opt/splunk/etc/apps/pusher_app_prem/local/license.lic" + if os.path.exists(license_path): + os.remove(license_path) + response = {"success": True, "message": "Licence supprimée"} + else: + response = {"success": False, "error": "Aucune licence à supprimer"} + + self.wfile.write(json.dumps(response).encode()) + return + + # ============================================ + # ENDPOINT PUSH GIT + # ============================================ + + elif path == '/push' or path.startswith('/services/'): + # Vérifier la licence avant le push + license_check = check_limits() + if not license_check.get("allowed"): + response = { + "status": "error", + "error_code": "LICENSE_ERROR", + "message": license_check.get("error", "Licence invalide ou limite atteinte") + } + self.wfile.write(json.dumps(response).encode()) + return + + # Traiter le push Git + self.handle_git_push(query_params) + return + + else: + # Ancien comportement pour compatibilité + # Vérifier la licence + license_check = check_limits() + if not license_check.get("allowed"): + response = { + "status": "error", + "error_code": "LICENSE_ERROR", + "message": license_check.get("error", "Licence invalide ou limite atteinte") + } + self.wfile.write(json.dumps(response).encode()) + return + + self.handle_git_push(query_params) + + except Exception as e: + logger.error(f"POST error: {str(e)}", exc_info=True) + response = { + "status": "error", + "message": f"Error: {str(e)}" + } + self.wfile.write(json.dumps(response).encode()) + + def handle_git_push(self, query_params): + """Gérer le push Git et optionnellement le déploiement vers SH Cluster""" + try: + # Extraire les paramètres + git_url = query_params.get('git_url', [''])[0] + git_branch = query_params.get('git_branch', ['main'])[0] + git_token = query_params.get('git_token', [''])[0] + commit_message = query_params.get('commit_message', [''])[0] + apps_json = query_params.get('apps', query_params.get('dashboards', ['[]']))[0] + user = query_params.get('user', ['unknown'])[0] + + # Paramètres pour le déploiement SH Cluster + deploy_to_shcluster = query_params.get('deploy_to_shcluster', ['false'])[0].lower() == 'true' + deployer_host = query_params.get('deployer_host', [SH_DEPLOYER_CONFIG.get('host', '')])[0] + deployer_token = query_params.get('deployer_token', [SH_DEPLOYER_CONFIG.get('token', '')])[0] + sh_auth_user = query_params.get('sh_auth_user', [''])[0] + sh_auth_pass = query_params.get('sh_auth_pass', [''])[0] + + logger.info(f"Parameters: git_url={git_url}, branch={git_branch}, user={user}, deploy_to_shcluster={deploy_to_shcluster}") + + # Parser les apps + try: + apps = json.loads(apps_json) if isinstance(apps_json, str) else apps_json + except (json.JSONDecodeError, TypeError) as e: + logger.error(f"JSON parse error: {e}") + apps = [] + + logger.info(f"Parsed apps: {len(apps)} items") + + # Vérifier les limites d'apps + license_info = validate_license() + max_apps = license_info.get("limits", {}).get("max_apps", -1) + if max_apps > 0 and len(apps) > max_apps: + response = { + "status": "error", + "error_code": "APP_LIMIT", + "message": f"Votre licence permet {max_apps} apps max. Vous en avez sélectionné {len(apps)}." + } + self.wfile.write(json.dumps(response).encode()) + return + + # Valider les paramètres + if not git_url or not git_token or not commit_message or not apps: + response = { + "status": "error", + "message": "Missing required parameters" + } + self.wfile.write(json.dumps(response).encode()) + return + + # Créer un répertoire temporaire + temp_dir = tempfile.mkdtemp(prefix='splunk_git_') + logger.info(f"Created temp directory: {temp_dir}") + + try: + # Préparer l'URL Git avec le token + git_url_with_token = self.prepare_git_url(git_url, git_token) + + logger.info("Cloning repository...") + self.clone_repository(temp_dir, git_url_with_token, git_branch) + + # Récupérer les applications + logger.info("Fetching applications from Splunk...") + app_directories = self.fetch_apps_directories(apps) + + # Créer le dossier apps + apps_dir = os.path.join(temp_dir, 'apps') + os.makedirs(apps_dir, exist_ok=True) + + # Copier les applications + logger.info("Copying applications to repository...") + for app_data in app_directories: + app_name = app_data['name'] + app_path = app_data['path'] + dest_path = os.path.join(apps_dir, app_name) + + if os.path.exists(app_path): + if os.path.exists(dest_path): + shutil.rmtree(dest_path) + shutil.copytree(app_path, dest_path) + logger.info(f"Copied app: {app_name}") + else: + logger.warning(f"App path not found: {app_path}") + + # Configurer git + subprocess.run(['git', 'config', 'user.email', 'splunk@splunk.local'], + cwd=temp_dir, capture_output=True) + subprocess.run(['git', 'config', 'user.name', 'Splunk Git Pusher'], + cwd=temp_dir, capture_output=True) + + # Commit et push + subprocess.run(['git', 'add', '-A'], cwd=temp_dir, capture_output=True) + + # Ajouter les infos de licence au commit + license_info = validate_license() + full_message = f"{commit_message}\n\n" + full_message += f"Pushed by: {user}\n" + full_message += f"License: {license_info.get('license_id', 'N/A')} ({license_info.get('type_name', 'N/A')})\n" + full_message += f"Timestamp: {datetime.now().isoformat()}" + + result = subprocess.run(['git', 'commit', '-m', full_message], + cwd=temp_dir, capture_output=True, text=True) + + if result.returncode != 0: + logger.warning(f"Commit warning: {result.stderr}") + + logger.info("Pushing to Git...") + result = subprocess.run(['git', 'push', 'origin', git_branch], + cwd=temp_dir, capture_output=True, text=True, timeout=60) + + if result.returncode != 0: + raise Exception(f"Push failed: {result.stderr}") + + # Incrémenter les stats d'utilisation + increment_usage() + + logger.info("Git push successful!") + + # ============================================ + # DÉPLOIEMENT VERS SH CLUSTER (optionnel) + # ============================================ + + deployer_result = None + + if deploy_to_shcluster: + logger.info("Triggering deployment to SH Cluster...") + + # Configurer le deployer + deployer_config = SH_DEPLOYER_CONFIG.copy() + if deployer_host: + deployer_config["host"] = deployer_host + if deployer_token: + deployer_config["token"] = deployer_token + + # Appeler le SH Deployer pour pull + deploy + deployer_result = trigger_deployer_pull_and_deploy( + git_url=git_url, + git_token=git_token, + auth_user=sh_auth_user if sh_auth_user else None, + auth_pass=sh_auth_pass if sh_auth_pass else None, + config=deployer_config + ) + + if deployer_result.get("success"): + logger.info("SH Cluster deployment triggered successfully") + else: + logger.error(f"SH Cluster deployment failed: {deployer_result.get('error')}") + + # Préparer la réponse + response = { + "status": "success", + "message": f"Successfully pushed {len(app_directories)} application(s) to Git", + "apps_pushed": len(app_directories), + "license_type": license_info.get("type_name", "N/A") + } + + # Ajouter les infos de déploiement si activé + if deploy_to_shcluster: + response["shcluster_deployment"] = { + "triggered": True, + "success": deployer_result.get("success", False) if deployer_result else False, + "message": deployer_result.get("data", {}).get("message") if deployer_result and deployer_result.get("success") else deployer_result.get("error") if deployer_result else "Not triggered" + } + + if deployer_result and deployer_result.get("success"): + response["message"] += " and triggered SH Cluster deployment" + else: + response["message"] += " (SH Cluster deployment failed)" + + self.wfile.write(json.dumps(response).encode()) + + finally: + logger.info(f"Cleaning up {temp_dir}") + shutil.rmtree(temp_dir, ignore_errors=True) + + except Exception as e: + logger.error(f"Git push error: {str(e)}", exc_info=True) + response = { + "status": "error", + "message": f"Error: {str(e)}" + } + self.wfile.write(json.dumps(response).encode()) + + def log_message(self, format, *args): + logger.debug(format % args) + + @staticmethod + def prepare_git_url(git_url, token): + """Préparer l'URL Git avec le token""" + if '@' in git_url: + protocol = git_url.split('://')[0] + rest = git_url.split('://', 1)[1] + host_and_path = rest.split('@', 1)[1] if '@' in rest else rest + return f"{protocol}://{token}@{host_and_path}" + + if git_url.startswith('https://') or git_url.startswith('http://'): + protocol = git_url.split('://')[0] + host_and_path = git_url.split('://', 1)[1] + return f"{protocol}://{token}@{host_and_path}" + + return git_url + + @staticmethod + def clone_repository(dest_dir, git_url, branch): + """Cloner le repository""" + try: + cmd = ['git', 'clone', '--depth', '1', '--branch', branch, git_url, dest_dir] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=60) + + if result.returncode != 0: + raise Exception(f"Clone failed: {result.stderr}") + + logger.info("Repository cloned successfully") + except subprocess.TimeoutExpired: + raise Exception("Git clone operation timed out") + except FileNotFoundError: + raise Exception("Git is not installed on this system") + + @staticmethod + def fetch_apps_directories(apps): + """Récupérer les dossiers des applications""" + logger.info(f"Fetching directories for {len(apps)} applications") + + splunk_home = '/opt/splunk' + apps_base_path = os.path.join(splunk_home, 'etc', 'apps') + + app_directories = [] + + for app in apps: + app_id = app.get('id') or app.get('app_id') + app_path = os.path.join(apps_base_path, app_id) + + if os.path.isdir(app_path): + app_directories.append({ + 'name': app_id, + 'path': app_path, + 'size': sum(os.path.getsize(os.path.join(dirpath, filename)) + for dirpath, dirnames, filenames in os.walk(app_path) + for filename in filenames) + }) + logger.info(f"Found app: {app_id}") + else: + logger.warning(f"App directory not found: {app_path}") + + return app_directories + + +# ============================================ +# FONCTIONS SH DEPLOYER +# ============================================ + +def call_deployer_agent(endpoint, method="GET", data=None, config=None): + """ + Appeler l'agent SH Deployer + + Args: + endpoint: Endpoint à appeler (ex: /health, /pull, /deploy) + method: GET ou POST + data: Données à envoyer (dict) + config: Configuration (override SH_DEPLOYER_CONFIG) + + Returns: + dict avec success, data ou error + """ + if config is None: + config = SH_DEPLOYER_CONFIG + + if not config.get("enabled", True): + return {"success": False, "error": "SH Deployer is disabled"} + + host = config.get("host", "10.10.40.14") + port = config.get("port", 9998) + use_ssl = config.get("use_ssl", True) + token = config.get("token", "") + timeout = config.get("timeout", 30) + + protocol = "https" if use_ssl else "http" + url = f"{protocol}://{host}:{port}{endpoint}" + + logger.info(f"Calling SH Deployer: {method} {url}") + + try: + # Créer le contexte SSL (ignorer les certificats auto-signés) + ssl_context = ssl.create_default_context() + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + + # Préparer les données + if data: + json_data = json.dumps(data).encode('utf-8') + else: + json_data = None + + # Créer la requête + req = urllib.request.Request(url, data=json_data, method=method) + req.add_header('Content-Type', 'application/json') + req.add_header('X-Auth-Token', token) + + # Exécuter la requête + with urllib.request.urlopen(req, timeout=timeout, context=ssl_context) as response: + response_data = json.loads(response.read().decode('utf-8')) + logger.info(f"SH Deployer response: {response_data}") + return {"success": True, "data": response_data} + + except urllib.error.HTTPError as e: + error_body = e.read().decode('utf-8') if e.fp else str(e) + logger.error(f"SH Deployer HTTP error {e.code}: {error_body}") + return {"success": False, "error": f"HTTP {e.code}: {error_body}"} + + except urllib.error.URLError as e: + logger.error(f"SH Deployer connection error: {e.reason}") + return {"success": False, "error": f"Connection error: {e.reason}"} + + except Exception as e: + logger.error(f"SH Deployer error: {str(e)}") + return {"success": False, "error": str(e)} + + +def check_deployer_health(config=None): + """Vérifier si l'agent SH Deployer est accessible""" + result = call_deployer_agent("/health", config=config) + return result.get("success", False) + + +def trigger_deployer_pull(git_url, git_token, config=None): + """ + Déclencher un pull sur le SH Deployer + + Args: + git_url: URL du repository Git + git_token: Token Git pour l'authentification + config: Configuration du deployer + """ + data = { + "repo_url": git_url, + "git_token": git_token, + "apps_subdir": "apps" + } + + return call_deployer_agent("/pull", method="POST", data=data, config=config) + + +def trigger_deployer_deploy(target_uri=None, auth_user=None, auth_pass=None, config=None): + """ + Déclencher le déploiement du bundle sur le SH Cluster + + Args: + target_uri: URI du captain du SH Cluster (optionnel) + auth_user: Utilisateur Splunk + auth_pass: Mot de passe Splunk + config: Configuration du deployer + """ + data = {} + if target_uri: + data["target_uri"] = target_uri + if auth_user: + data["auth_user"] = auth_user + if auth_pass: + data["auth_pass"] = auth_pass + + return call_deployer_agent("/deploy", method="POST", data=data, config=config) + + +def trigger_deployer_pull_and_deploy(git_url, git_token, target_uri=None, auth_user=None, auth_pass=None, config=None): + """ + Déclencher pull + deploy en une seule opération + """ + data = { + "repo_url": git_url, + "git_token": git_token, + "apps_subdir": "apps" + } + if target_uri: + data["target_uri"] = target_uri + if auth_user: + data["auth_user"] = auth_user + if auth_pass: + data["auth_pass"] = auth_pass + + return call_deployer_agent("/pull-and-deploy", method="POST", data=data, config=config) + + +def get_deployer_status(config=None): + """Récupérer le statut du SH Deployer""" + return call_deployer_agent("/status", config=config) + + +def start_server(port=9999, use_ssl=True): + """Démarrer le serveur HTTP/HTTPS""" + import ssl + + server = HTTPServer(('0.0.0.0', port), GitPusherRequestHandler) + + ssl_enabled = False + + if use_ssl: + # Chemins possibles pour les certificats (ordre de priorité) + cert_paths = [ + # Certificats dédiés pour Git Pusher (recommandé) + ('/opt/splunk/etc/apps/pusher_app_prem/local/certs/server.crt', + '/opt/splunk/etc/apps/pusher_app_prem/local/certs/server.key'), + # Certificats splunkweb + ('/opt/splunk/etc/auth/splunkweb/cert.pem', + '/opt/splunk/etc/auth/splunkweb/privkey.pem'), + # Autre emplacement splunkweb + ('/opt/splunk/etc/auth/splunkweb/splunkweb.pem', + '/opt/splunk/etc/auth/splunkweb/splunkweb.key'), + ] + + for cert_file, key_file in cert_paths: + logger.info(f"Trying SSL cert: {cert_file}") + if os.path.exists(cert_file) and os.path.exists(key_file): + try: + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + + # Charger le certificat et la clé + ssl_context.load_cert_chain(certfile=cert_file, keyfile=key_file) + + server.socket = ssl_context.wrap_socket(server.socket, server_side=True) + ssl_enabled = True + logger.info(f"SSL enabled using: {cert_file}") + break + except Exception as e: + logger.warning(f"Could not load SSL cert {cert_file}: {e}") + continue + else: + logger.debug(f"Cert not found: {cert_file} or {key_file}") + + if not ssl_enabled: + logger.error("=" * 60) + logger.error("SSL CERTIFICATES NOT FOUND OR INVALID!") + logger.error("HTTPS requests from browser will fail!") + logger.error("") + logger.error("To fix, run these commands:") + logger.error(" mkdir -p /opt/splunk/etc/apps/pusher_app_prem/local/certs") + logger.error(" openssl req -x509 -newkey rsa:4096 \\") + logger.error(" -keyout /opt/splunk/etc/apps/pusher_app_prem/local/certs/server.key \\") + logger.error(" -out /opt/splunk/etc/apps/pusher_app_prem/local/certs/server.crt \\") + logger.error(" -days 365 -nodes -subj \"/CN=git-pusher\"") + logger.error("=" * 60) + + protocol = "HTTPS" if ssl_enabled else "HTTP" + logger.info(f"Git Pusher server listening on 0.0.0.0:{port} ({protocol})") + + # Afficher le statut de la licence au démarrage + license_status = validate_license() + if license_status.get("valid"): + logger.info(f"License: {license_status.get('type_name')} - {license_status.get('days_remaining')} days remaining") + else: + logger.warning(f"License: {license_status.get('error', 'Invalid')}") + + server.serve_forever() + + +if __name__ == '__main__': + import argparse + parser = argparse.ArgumentParser(description='Git Pusher Server') + parser.add_argument('--no-ssl', action='store_true', help='Disable SSL/HTTPS') + parser.add_argument('--port', type=int, default=9999, help='Port number (default: 9999)') + args = parser.parse_args() + + port = args.port + use_ssl = not args.no_ssl + + logger.info(f"Starting Git Pusher on port {port} (SSL: {use_ssl})") + start_server(port, use_ssl) \ No newline at end of file diff --git a/apps/pusher_app_prem/bin/license_validator.py b/apps/pusher_app_prem/bin/license_validator.py old mode 100755 new mode 100644 index eca87391..8c1ef3ff --- a/apps/pusher_app_prem/bin/license_validator.py +++ b/apps/pusher_app_prem/bin/license_validator.py @@ -1,224 +1,277 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -Git Pusher - License Validator (Server-side) -Ce fichier doit être déployé sur le serveur Splunk dans l'application +Git Pusher - License Validator (RSA) +Validation des licences avec vérification de signature RSA -Emplacement: /opt/splunk/etc/apps/pusher_app_prem/bin/license_validator.py +Ce fichier est distribué avec l'application client. +La clé publique peut être visible - seul le vendeur avec la clé privée peut signer. """ -import hashlib -import hmac -import base64 -import json import os +import sys +import json +import base64 import socket from datetime import datetime -from http.server import HTTPServer, BaseHTTPRequestHandler -import logging - -# Configuration du logging -log_dir = '/opt/splunk/var/log/splunk' -os.makedirs(log_dir, exist_ok=True) - -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler(os.path.join(log_dir, 'license_validator.log')), - logging.StreamHandler() - ] -) -logger = logging.getLogger('license_validator') # ============================================ -# CONFIGURATION - DOIT CORRESPONDRE AU GÉNÉRATEUR ! +# CLÉ PUBLIQUE RSA # ============================================ +# Cette clé est générée par le vendeur avec license_generator_rsa.py +# Commande: python license_generator_rsa.py export-key +# +# REMPLACEZ CETTE CLÉ PAR VOTRE CLÉ PUBLIQUE ! + +PUBLIC_KEY = '''-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnj2hOg61Q9k9iz4U5F7I +RdaJrpLTG+orz0/Kpbz2HSxbAVXkvL5GvYVfxROjy0UgxOZFycZAaGN2am+5CDHA +D1dTL9KCPhEaPqw4XTFnf6Ur5VG0+SftugTdTcyxRe614Z+i61/2ahk/vKG9D4kB +j4qV4se4lLk993lEaQrOXkXCbZ8royB5MPeOchPxZd7SDzoovEcyUmf2Fa2eYk6U +WmbrymCnJRsfxEVZofQQyp1ILS8KuSxaquXvMWm3cXV2Krs/3E5ax0vBPMrZRL+o +Vn7/dVnzbOlbifeosTYaad1DLd7NEgst3OFUv+dSH5hcCCc36IHMSvxcJ9l7s2kv +KbEFPeh582JNmRoMMNPRbd+/ZVDeJ/oB344+TtB6VeQ2GQyOyoggiLryZujg3WDE +shwLkFwiYGa/zEct0qs2/HBS1FOAqrLiPdQYJTx+RhrXhTni/p3H42L+2xJcbnki +fAgn8ND5k2yQw2gk4AlLqq2y01m0jsHdjaOfhKzzPLyzt1En/KAnCWJSzB+jur4z +fd70R4pLTYfawr2NTvTAhOtiOIjWj380oGmxeKCNJT1P4Dq8Yl+OxKLBy4cnUlgV +sbpKJug7Goth4g7bmCVMhC4bf7JB/iTrrS8DhMaWaZX/FeFloM3yYyo/gzmAY19z +sUdQuBpxCwIf/J7Q4dYsDkMCAwEAAQ== +-----END PUBLIC KEY-----''' -# IMPORTANT: Cette clé DOIT être identique à celle du générateur -SECRET_KEY = "git_pusher_super_secret_key_2024_change_me_in_production" - -# Chemin vers le fichier de licence -LICENSE_FILE_PATH = "/opt/splunk/etc/apps/pusher_app_prem/local/license.lic" +# ============================================ +# CONFIGURATION +# ============================================ -# Fichier de cache pour les stats d'utilisation -USAGE_STATS_PATH = "/opt/splunk/etc/apps/pusher_app_prem/local/usage_stats.json" +# Chemin de l'application +APP_HOME = os.environ.get('SPLUNK_HOME', '/opt/splunk') + '/etc/apps/pusher_app_prem' +LICENSE_FILE = os.path.join(APP_HOME, 'local', 'license.lic') +USAGE_FILE = os.path.join(APP_HOME, 'local', '.usage_stats') +# ============================================ +# FONCTIONS UTILITAIRES +# ============================================ def get_splunk_hostname(): """Récupérer le hostname du serveur Splunk""" - # Essayer d'abord via l'API Splunk try: - import urllib.request - import ssl - - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE - - splunk_username = os.environ.get('SPLUNK_USERNAME', 'admin') - splunk_password = os.environ.get('SPLUNK_PASSWORD', '2312Jocpam!?') - credentials = base64.b64encode(f"{splunk_username}:{splunk_password}".encode()).decode() - - req = urllib.request.Request("https://127.0.0.1:8089/services/server/info?output_mode=json") - req.add_header('Authorization', f'Basic {credentials}') - - with urllib.request.urlopen(req, timeout=5, context=ssl_context) as response: - data = json.loads(response.read().decode('utf-8')) - hostname = data.get('entry', [{}])[0].get('content', {}).get('host', '') - if hostname: - return hostname.lower().strip() - except Exception as e: - logger.warning(f"Could not get hostname via Splunk API: {e}") + # Essayer via servername de Splunk + server_conf = os.path.join( + os.environ.get('SPLUNK_HOME', '/opt/splunk'), + 'etc', 'system', 'local', 'server.conf' + ) + if os.path.exists(server_conf): + with open(server_conf, 'r') as f: + for line in f: + if line.strip().startswith('serverName'): + return line.split('=')[1].strip().lower() + except: + pass # Fallback sur le hostname système - return socket.gethostname().lower().strip() + return socket.gethostname().lower() + +def load_public_key(): + """Charger la clé publique RSA""" + try: + from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.backends import default_backend + + public_key = serialization.load_pem_public_key( + PUBLIC_KEY.encode('utf-8'), + backend=default_backend() + ) + return public_key + except ImportError: + # Fallback si cryptography n'est pas installé + return None + except Exception as e: + print(f"Erreur chargement clé publique: {e}") + return None -def create_signature(data_str): - """Créer une signature HMAC-SHA256""" - signature = hmac.new( - SECRET_KEY.encode('utf-8'), - data_str.encode('utf-8'), - hashlib.sha256 - ).hexdigest() - return signature +def verify_rsa_signature(data, signature, public_key): + """Vérifier la signature RSA""" + try: + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives.asymmetric import padding + + public_key.verify( + signature, + data, + padding.PSS( + mgf=padding.MGF1(hashes.SHA256()), + salt_length=padding.PSS.MAX_LENGTH + ), + hashes.SHA256() + ) + return True + except Exception: + return False -def verify_signature(data_str, signature): - """Vérifier une signature""" - expected = create_signature(data_str) - return hmac.compare_digest(expected, signature) +# ============================================ +# PARSING DU FICHIER .LIC +# ============================================ -def read_license_file(filepath=None): - """Lire et parser un fichier de licence""" +def parse_license_file(filepath=None): + """Parser un fichier de licence .lic""" if filepath is None: - filepath = LICENSE_FILE_PATH + filepath = LICENSE_FILE + + if not os.path.exists(filepath): + return None try: - if not os.path.exists(filepath): - return None - - with open(filepath, 'r', encoding='utf-8') as f: + with open(filepath, 'r') as f: content = f.read() - - # Extraire la partie base64 (ignorer les commentaires) - lines = content.strip().split('\n') - base64_lines = [l for l in lines if not l.startswith('#') and l.strip()] - base64_content = ''.join(base64_lines) - - # Décoder - decoded = base64.b64decode(base64_content).decode('utf-8') - license_file = json.loads(decoded) - - return license_file - + return parse_license_content(content) except Exception as e: - logger.error(f"Error reading license file: {e}") + print(f"Erreur lecture licence: {e}") return None def parse_license_content(content): - """Parser le contenu d'un fichier de licence (pour l'upload)""" + """Parser le contenu d'une licence""" try: - # Extraire la partie base64 (ignorer les commentaires) + # Extraire le payload (ignorer les lignes de commentaires) lines = content.strip().split('\n') - base64_lines = [l for l in lines if not l.startswith('#') and l.strip()] - base64_content = ''.join(base64_lines) + payload_b64 = None - # Décoder - decoded = base64.b64decode(base64_content).decode('utf-8') - license_file = json.loads(decoded) + for line in lines: + line = line.strip() + if line and not line.startswith('#'): + payload_b64 = line + break - return license_file - + if not payload_b64: + return None + + # Décoder le payload + payload = json.loads(base64.b64decode(payload_b64).decode('utf-8')) + + return { + "license_b64": payload.get("license"), + "signature_b64": payload.get("signature"), + "raw_payload": payload_b64 + } except Exception as e: - logger.error(f"Error parsing license content: {e}") + print(f"Erreur parsing licence: {e}") return None -def validate_license(license_file=None, current_hostname=None): +# ============================================ +# VALIDATION DE LA LICENCE +# ============================================ + +def validate_license(filepath=None): """ Valider une licence - Args: - license_file: Contenu du fichier de licence (dict) - si None, lit le fichier par défaut - current_hostname: Hostname actuel - si None, détecte automatiquement - Returns: - dict avec {valid: bool, error: str, license_info: dict} + dict avec: + - valid: bool + - error: str (si invalide) + - error_code: str (si invalide) + - license_id, type, type_name, expires, days_remaining, limits, features (si valide) """ + + # Parser le fichier + parsed = parse_license_file(filepath) + + if not parsed: + return { + "valid": False, + "error": "Fichier de licence non trouvé ou invalide", + "error_code": "NO_LICENSE" + } + + license_b64 = parsed.get("license_b64") + signature_b64 = parsed.get("signature_b64") + + if not license_b64 or not signature_b64: + return { + "valid": False, + "error": "Format de licence invalide", + "error_code": "INVALID_FORMAT" + } + try: - # Lire la licence si non fournie - if license_file is None: - license_file = read_license_file() - if license_file is None: + # Décoder la licence et la signature + license_json = base64.b64decode(license_b64).decode('utf-8') + signature = base64.b64decode(signature_b64) + + # Charger la clé publique + public_key = load_public_key() + + if public_key is None: + # Mode dégradé sans cryptography - vérification basique + print("⚠️ Module cryptography non installé - vérification basique uniquement") + else: + # Vérifier la signature RSA + if not verify_rsa_signature(license_json.encode('utf-8'), signature, public_key): return { "valid": False, - "error": "Aucun fichier de licence trouvé", - "error_code": "NO_LICENSE" + "error": "Signature de licence invalide", + "error_code": "INVALID_SIGNATURE" } - # Récupérer le hostname si non fourni - if current_hostname is None: - current_hostname = get_splunk_hostname() - - license_data = license_file.get("license", {}) - signature = license_file.get("signature", "") - - # Recréer le JSON pour vérifier la signature - license_json = json.dumps(license_data, separators=(',', ':'), sort_keys=True) - - # Vérifier la signature - if not verify_signature(license_json, signature): - return { - "valid": False, - "error": "Signature invalide - fichier corrompu ou modifié", - "error_code": "INVALID_SIGNATURE" - } + # Parser les données de licence + license_data = json.loads(license_json) # Vérifier le hostname - expected_hostname = license_data.get("binding", {}).get("hostname", "").lower() - current_hostname_clean = current_hostname.lower().strip() - - if current_hostname_clean != expected_hostname: - return { - "valid": False, - "error": f"Cette licence est pour '{expected_hostname}', pas '{current_hostname_clean}'", - "error_code": "HOSTNAME_MISMATCH", - "expected_hostname": expected_hostname, - "current_hostname": current_hostname_clean - } + expected_hostname = license_data.get("hostname", "").lower() + current_hostname = get_splunk_hostname() - # Vérifier l'expiration - expires_timestamp = license_data.get("dates", {}).get("expires_timestamp", 0) - if datetime.now().timestamp() > expires_timestamp: - expires_date = license_data.get("dates", {}).get("expires", "unknown") + if expected_hostname and expected_hostname != current_hostname: return { "valid": False, - "error": f"Licence expirée le {expires_date}", - "error_code": "EXPIRED" + "error": f"Licence non valide pour ce serveur. Attendu: {expected_hostname}, Actuel: {current_hostname}", + "error_code": "HOSTNAME_MISMATCH" } - # Calculer les jours restants - days_remaining = (expires_timestamp - datetime.now().timestamp()) / (24 * 3600) + # Vérifier la date d'expiration + expires_str = license_data.get("expires", "") + if expires_str: + try: + expires_date = datetime.strptime(expires_str, "%Y-%m-%d") + now = datetime.now() + + if now > expires_date: + return { + "valid": False, + "error": f"Licence expirée le {expires_str}", + "error_code": "LICENSE_EXPIRED" + } + + days_remaining = (expires_date - now).days + except ValueError: + days_remaining = 0 + else: + days_remaining = 999 # Pas d'expiration + # Licence valide ! return { "valid": True, "license_id": license_data.get("license_id"), "type": license_data.get("type"), "type_name": license_data.get("type_name"), "customer": license_data.get("customer", {}), - "expires": license_data.get("dates", {}).get("expires"), - "days_remaining": int(days_remaining), + "hostname": expected_hostname, + "issued": license_data.get("issued"), + "expires": expires_str, + "days_remaining": days_remaining, "limits": license_data.get("limits", {}), - "features": license_data.get("features", []), - "hostname": expected_hostname + "features": license_data.get("features", []) } + except json.JSONDecodeError: + return { + "valid": False, + "error": "Données de licence corrompues", + "error_code": "CORRUPTED_DATA" + } except Exception as e: - logger.error(f"Validation error: {e}") return { "valid": False, "error": f"Erreur de validation: {str(e)}", @@ -226,260 +279,190 @@ def validate_license(license_file=None, current_hostname=None): } +# ============================================ +# SAUVEGARDE DE LICENCE +# ============================================ + def save_license_file(content): - """ - Sauvegarder un nouveau fichier de licence - - Args: - content: Contenu du fichier .lic - - Returns: - dict avec {success: bool, error: str} - """ + """Sauvegarder un fichier de licence uploadé""" try: # Créer le dossier local si nécessaire - local_dir = os.path.dirname(LICENSE_FILE_PATH) + local_dir = os.path.join(APP_HOME, 'local') os.makedirs(local_dir, exist_ok=True) - # Parser d'abord pour valider - license_file = parse_license_content(content) - if license_file is None: + # Valider le contenu avant de sauvegarder + parsed = parse_license_content(content) + if not parsed: return { "success": False, - "error": "Format de fichier de licence invalide" + "error": "Format de licence invalide" } # Valider la licence - validation = validate_license(license_file) + # Sauvegarder temporairement pour valider + temp_file = LICENSE_FILE + '.tmp' + with open(temp_file, 'w') as f: + f.write(content) + + validation = validate_license(temp_file) + if not validation.get("valid"): + os.remove(temp_file) return { "success": False, - "error": validation.get("error", "Licence invalide") + "error": validation.get("error"), + "error_code": validation.get("error_code") } - # Sauvegarder - with open(LICENSE_FILE_PATH, 'w', encoding='utf-8') as f: - f.write(content) - - logger.info(f"License saved: {validation.get('license_id')}") + # Renommer le fichier temporaire + os.rename(temp_file, LICENSE_FILE) + os.chmod(LICENSE_FILE, 0o600) return { "success": True, - "license_info": validation + "license": validation } except Exception as e: - logger.error(f"Error saving license: {e}") return { "success": False, "error": str(e) } +# ============================================ +# GESTION DES LIMITES D'UTILISATION +# ============================================ + def get_usage_stats(): """Récupérer les statistiques d'utilisation""" try: - if os.path.exists(USAGE_STATS_PATH): - with open(USAGE_STATS_PATH, 'r') as f: + if os.path.exists(USAGE_FILE): + with open(USAGE_FILE, 'r') as f: return json.load(f) except: pass return { + "total_pushes": 0, "pushes_today": 0, - "pushes_total": 0, "last_push_date": None, "apps_pushed": [] } +def save_usage_stats(stats): + """Sauvegarder les statistiques d'utilisation""" + try: + local_dir = os.path.join(APP_HOME, 'local') + os.makedirs(local_dir, exist_ok=True) + + with open(USAGE_FILE, 'w') as f: + json.dump(stats, f, indent=2) + os.chmod(USAGE_FILE, 0o600) + except Exception as e: + print(f"Erreur sauvegarde stats: {e}") + + def increment_usage(): """Incrémenter le compteur d'utilisation""" stats = get_usage_stats() today = datetime.now().strftime("%Y-%m-%d") - # Reset si nouveau jour + # Reset compteur quotidien si nouveau jour if stats.get("last_push_date") != today: stats["pushes_today"] = 0 stats["last_push_date"] = today - stats["pushes_today"] += 1 - stats["pushes_total"] += 1 - - try: - os.makedirs(os.path.dirname(USAGE_STATS_PATH), exist_ok=True) - with open(USAGE_STATS_PATH, 'w') as f: - json.dump(stats, f) - except Exception as e: - logger.error(f"Error saving usage stats: {e}") + stats["total_pushes"] = stats.get("total_pushes", 0) + 1 + stats["pushes_today"] = stats.get("pushes_today", 0) + 1 + save_usage_stats(stats) return stats def check_limits(): - """ - Vérifier si les limites de la licence sont respectées - - Returns: - dict avec {allowed: bool, error: str} - """ - validation = validate_license() + """Vérifier si les limites de la licence sont respectées""" + license_info = validate_license() - if not validation.get("valid"): + if not license_info.get("valid"): return { "allowed": False, - "error": validation.get("error") + "error": license_info.get("error"), + "error_code": license_info.get("error_code") } - limits = validation.get("limits", {}) - stats = get_usage_stats() - - # Vérifier pushes par jour + limits = license_info.get("limits", {}) max_pushes = limits.get("max_pushes_per_day", -1) - if max_pushes > 0 and stats.get("pushes_today", 0) >= max_pushes: - return { - "allowed": False, - "error": f"Limite quotidienne atteinte ({max_pushes} pushes/jour)" - } + + if max_pushes > 0: + stats = get_usage_stats() + today = datetime.now().strftime("%Y-%m-%d") + + # Reset si nouveau jour + if stats.get("last_push_date") != today: + stats["pushes_today"] = 0 + + if stats.get("pushes_today", 0) >= max_pushes: + return { + "allowed": False, + "error": f"Limite quotidienne atteinte ({max_pushes} pushes/jour)", + "error_code": "DAILY_LIMIT_REACHED" + } return { "allowed": True, - "license_info": validation, - "usage": stats + "license_type": license_info.get("type_name"), + "remaining_today": max_pushes - get_usage_stats().get("pushes_today", 0) if max_pushes > 0 else -1 } -# ============================================ -# API REST POUR L'INTERFACE WEB -# ============================================ - -class LicenseAPIHandler(BaseHTTPRequestHandler): - """Handler pour les requêtes de l'API licence""" - - def send_cors_headers(self): - """Envoyer les headers CORS complets""" - origin = self.headers.get('Origin', '*') - self.send_header('Access-Control-Allow-Origin', origin) - self.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS, PUT, DELETE') - self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, Accept, Origin') - self.send_header('Access-Control-Allow-Credentials', 'true') - self.send_header('Access-Control-Max-Age', '86400') - - def do_OPTIONS(self): - logger.info(f"OPTIONS request from {self.headers.get('Origin', 'unknown')}") - self.send_response(200) - self.send_cors_headers() - self.end_headers() - return +def has_feature(feature_name): + """Vérifier si une fonctionnalité est disponible dans la licence""" + license_info = validate_license() - def do_GET(self): - """GET /license - Récupérer les infos de licence""" - self.send_response(200) - self.send_header('Content-type', 'application/json') - self.send_cors_headers() - self.end_headers() - - try: - if '/license/status' in self.path or self.path == '/license': - validation = validate_license() - usage = get_usage_stats() - hostname = get_splunk_hostname() - - response = { - "status": "valid" if validation.get("valid") else "invalid", - "hostname": hostname, - "license": validation if validation.get("valid") else None, - "error": validation.get("error") if not validation.get("valid") else None, - "error_code": validation.get("error_code") if not validation.get("valid") else None, - "usage": usage - } - - elif '/license/hostname' in self.path: - response = { - "hostname": get_splunk_hostname() - } - - else: - response = {"error": "Unknown endpoint"} - - self.wfile.write(json.dumps(response).encode()) - - except Exception as e: - logger.error(f"GET error: {e}") - self.wfile.write(json.dumps({"error": str(e)}).encode()) + if not license_info.get("valid"): + return False - def do_POST(self): - """POST /license/upload - Uploader une nouvelle licence""" - self.send_response(200) - self.send_header('Content-type', 'application/json') - self.send_cors_headers() - self.end_headers() - - try: - content_length = int(self.headers.get('Content-Length', 0)) - body = self.rfile.read(content_length).decode('utf-8') - - if '/license/upload' in self.path: - # Le body contient le contenu du fichier .lic - data = json.loads(body) - license_content = data.get('license_content', '') - - if not license_content: - response = {"success": False, "error": "Contenu de licence vide"} - else: - response = save_license_file(license_content) - - elif '/license/delete' in self.path: - # Supprimer la licence - if os.path.exists(LICENSE_FILE_PATH): - os.remove(LICENSE_FILE_PATH) - response = {"success": True, "message": "Licence supprimée"} - else: - response = {"success": False, "error": "Aucune licence à supprimer"} - - else: - response = {"error": "Unknown endpoint"} - - self.wfile.write(json.dumps(response).encode()) - - except Exception as e: - logger.error(f"POST error: {e}") - self.wfile.write(json.dumps({"error": str(e)}).encode()) - - def log_message(self, format, *args): - logger.debug(format % args) - - -def start_license_server(port=9998): - """Démarrer le serveur API licence (optionnel, peut être intégré au serveur principal)""" - server = HTTPServer(('127.0.0.1', port), LicenseAPIHandler) - logger.info(f"License API server listening on 127.0.0.1:{port}") - server.serve_forever() + features = license_info.get("features", []) + return feature_name in features # ============================================ # CLI POUR TESTS # ============================================ -if __name__ == '__main__': - import sys - +if __name__ == "__main__": if len(sys.argv) > 1: if sys.argv[1] == "status": result = validate_license() - print(json.dumps(result, indent=2, ensure_ascii=False)) + if result.get("valid"): + print("✅ Licence valide") + print(f" ID: {result.get('license_id')}") + print(f" Type: {result.get('type_name')}") + print(f" Hostname: {result.get('hostname')}") + print(f" Expire: {result.get('expires')} ({result.get('days_remaining')} jours)") + print(f" Features: {', '.join(result.get('features', []))}") + else: + print(f"❌ Licence invalide: {result.get('error')}") elif sys.argv[1] == "hostname": print(f"Hostname: {get_splunk_hostname()}") - elif sys.argv[1] == "server": - start_license_server() + elif sys.argv[1] == "usage": + stats = get_usage_stats() + print(f"Total pushes: {stats.get('total_pushes', 0)}") + print(f"Pushes aujourd'hui: {stats.get('pushes_today', 0)}") + + elif sys.argv[1] == "check": + result = check_limits() + if result.get("allowed"): + print(f"✅ Push autorisé ({result.get('license_type')})") + else: + print(f"❌ Push refusé: {result.get('error')}") else: - print("Usage:") - print(" python license_validator.py status # Vérifier la licence") - print(" python license_validator.py hostname # Afficher le hostname") - print(" python license_validator.py server # Démarrer l'API") + print("Usage: python license_validator.py [status|hostname|usage|check]") else: result = validate_license() - print(json.dumps(result, indent=2, ensure_ascii=False)) \ No newline at end of file + print(json.dumps(result, indent=2)) diff --git a/apps/pusher_app_prem/bin/license_validator.py_old b/apps/pusher_app_prem/bin/license_validator.py_old new file mode 100755 index 00000000..eca87391 --- /dev/null +++ b/apps/pusher_app_prem/bin/license_validator.py_old @@ -0,0 +1,485 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Git Pusher - License Validator (Server-side) +Ce fichier doit être déployé sur le serveur Splunk dans l'application + +Emplacement: /opt/splunk/etc/apps/pusher_app_prem/bin/license_validator.py +""" + +import hashlib +import hmac +import base64 +import json +import os +import socket +from datetime import datetime +from http.server import HTTPServer, BaseHTTPRequestHandler +import logging + +# Configuration du logging +log_dir = '/opt/splunk/var/log/splunk' +os.makedirs(log_dir, exist_ok=True) + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(os.path.join(log_dir, 'license_validator.log')), + logging.StreamHandler() + ] +) +logger = logging.getLogger('license_validator') + +# ============================================ +# CONFIGURATION - DOIT CORRESPONDRE AU GÉNÉRATEUR ! +# ============================================ + +# IMPORTANT: Cette clé DOIT être identique à celle du générateur +SECRET_KEY = "git_pusher_super_secret_key_2024_change_me_in_production" + +# Chemin vers le fichier de licence +LICENSE_FILE_PATH = "/opt/splunk/etc/apps/pusher_app_prem/local/license.lic" + +# Fichier de cache pour les stats d'utilisation +USAGE_STATS_PATH = "/opt/splunk/etc/apps/pusher_app_prem/local/usage_stats.json" + + +def get_splunk_hostname(): + """Récupérer le hostname du serveur Splunk""" + # Essayer d'abord via l'API Splunk + try: + import urllib.request + import ssl + + ssl_context = ssl.create_default_context() + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + + splunk_username = os.environ.get('SPLUNK_USERNAME', 'admin') + splunk_password = os.environ.get('SPLUNK_PASSWORD', '2312Jocpam!?') + credentials = base64.b64encode(f"{splunk_username}:{splunk_password}".encode()).decode() + + req = urllib.request.Request("https://127.0.0.1:8089/services/server/info?output_mode=json") + req.add_header('Authorization', f'Basic {credentials}') + + with urllib.request.urlopen(req, timeout=5, context=ssl_context) as response: + data = json.loads(response.read().decode('utf-8')) + hostname = data.get('entry', [{}])[0].get('content', {}).get('host', '') + if hostname: + return hostname.lower().strip() + except Exception as e: + logger.warning(f"Could not get hostname via Splunk API: {e}") + + # Fallback sur le hostname système + return socket.gethostname().lower().strip() + + +def create_signature(data_str): + """Créer une signature HMAC-SHA256""" + signature = hmac.new( + SECRET_KEY.encode('utf-8'), + data_str.encode('utf-8'), + hashlib.sha256 + ).hexdigest() + return signature + + +def verify_signature(data_str, signature): + """Vérifier une signature""" + expected = create_signature(data_str) + return hmac.compare_digest(expected, signature) + + +def read_license_file(filepath=None): + """Lire et parser un fichier de licence""" + if filepath is None: + filepath = LICENSE_FILE_PATH + + try: + if not os.path.exists(filepath): + return None + + with open(filepath, 'r', encoding='utf-8') as f: + content = f.read() + + # Extraire la partie base64 (ignorer les commentaires) + lines = content.strip().split('\n') + base64_lines = [l for l in lines if not l.startswith('#') and l.strip()] + base64_content = ''.join(base64_lines) + + # Décoder + decoded = base64.b64decode(base64_content).decode('utf-8') + license_file = json.loads(decoded) + + return license_file + + except Exception as e: + logger.error(f"Error reading license file: {e}") + return None + + +def parse_license_content(content): + """Parser le contenu d'un fichier de licence (pour l'upload)""" + try: + # Extraire la partie base64 (ignorer les commentaires) + lines = content.strip().split('\n') + base64_lines = [l for l in lines if not l.startswith('#') and l.strip()] + base64_content = ''.join(base64_lines) + + # Décoder + decoded = base64.b64decode(base64_content).decode('utf-8') + license_file = json.loads(decoded) + + return license_file + + except Exception as e: + logger.error(f"Error parsing license content: {e}") + return None + + +def validate_license(license_file=None, current_hostname=None): + """ + Valider une licence + + Args: + license_file: Contenu du fichier de licence (dict) - si None, lit le fichier par défaut + current_hostname: Hostname actuel - si None, détecte automatiquement + + Returns: + dict avec {valid: bool, error: str, license_info: dict} + """ + try: + # Lire la licence si non fournie + if license_file is None: + license_file = read_license_file() + if license_file is None: + return { + "valid": False, + "error": "Aucun fichier de licence trouvé", + "error_code": "NO_LICENSE" + } + + # Récupérer le hostname si non fourni + if current_hostname is None: + current_hostname = get_splunk_hostname() + + license_data = license_file.get("license", {}) + signature = license_file.get("signature", "") + + # Recréer le JSON pour vérifier la signature + license_json = json.dumps(license_data, separators=(',', ':'), sort_keys=True) + + # Vérifier la signature + if not verify_signature(license_json, signature): + return { + "valid": False, + "error": "Signature invalide - fichier corrompu ou modifié", + "error_code": "INVALID_SIGNATURE" + } + + # Vérifier le hostname + expected_hostname = license_data.get("binding", {}).get("hostname", "").lower() + current_hostname_clean = current_hostname.lower().strip() + + if current_hostname_clean != expected_hostname: + return { + "valid": False, + "error": f"Cette licence est pour '{expected_hostname}', pas '{current_hostname_clean}'", + "error_code": "HOSTNAME_MISMATCH", + "expected_hostname": expected_hostname, + "current_hostname": current_hostname_clean + } + + # Vérifier l'expiration + expires_timestamp = license_data.get("dates", {}).get("expires_timestamp", 0) + if datetime.now().timestamp() > expires_timestamp: + expires_date = license_data.get("dates", {}).get("expires", "unknown") + return { + "valid": False, + "error": f"Licence expirée le {expires_date}", + "error_code": "EXPIRED" + } + + # Calculer les jours restants + days_remaining = (expires_timestamp - datetime.now().timestamp()) / (24 * 3600) + + return { + "valid": True, + "license_id": license_data.get("license_id"), + "type": license_data.get("type"), + "type_name": license_data.get("type_name"), + "customer": license_data.get("customer", {}), + "expires": license_data.get("dates", {}).get("expires"), + "days_remaining": int(days_remaining), + "limits": license_data.get("limits", {}), + "features": license_data.get("features", []), + "hostname": expected_hostname + } + + except Exception as e: + logger.error(f"Validation error: {e}") + return { + "valid": False, + "error": f"Erreur de validation: {str(e)}", + "error_code": "VALIDATION_ERROR" + } + + +def save_license_file(content): + """ + Sauvegarder un nouveau fichier de licence + + Args: + content: Contenu du fichier .lic + + Returns: + dict avec {success: bool, error: str} + """ + try: + # Créer le dossier local si nécessaire + local_dir = os.path.dirname(LICENSE_FILE_PATH) + os.makedirs(local_dir, exist_ok=True) + + # Parser d'abord pour valider + license_file = parse_license_content(content) + if license_file is None: + return { + "success": False, + "error": "Format de fichier de licence invalide" + } + + # Valider la licence + validation = validate_license(license_file) + if not validation.get("valid"): + return { + "success": False, + "error": validation.get("error", "Licence invalide") + } + + # Sauvegarder + with open(LICENSE_FILE_PATH, 'w', encoding='utf-8') as f: + f.write(content) + + logger.info(f"License saved: {validation.get('license_id')}") + + return { + "success": True, + "license_info": validation + } + + except Exception as e: + logger.error(f"Error saving license: {e}") + return { + "success": False, + "error": str(e) + } + + +def get_usage_stats(): + """Récupérer les statistiques d'utilisation""" + try: + if os.path.exists(USAGE_STATS_PATH): + with open(USAGE_STATS_PATH, 'r') as f: + return json.load(f) + except: + pass + + return { + "pushes_today": 0, + "pushes_total": 0, + "last_push_date": None, + "apps_pushed": [] + } + + +def increment_usage(): + """Incrémenter le compteur d'utilisation""" + stats = get_usage_stats() + today = datetime.now().strftime("%Y-%m-%d") + + # Reset si nouveau jour + if stats.get("last_push_date") != today: + stats["pushes_today"] = 0 + stats["last_push_date"] = today + + stats["pushes_today"] += 1 + stats["pushes_total"] += 1 + + try: + os.makedirs(os.path.dirname(USAGE_STATS_PATH), exist_ok=True) + with open(USAGE_STATS_PATH, 'w') as f: + json.dump(stats, f) + except Exception as e: + logger.error(f"Error saving usage stats: {e}") + + return stats + + +def check_limits(): + """ + Vérifier si les limites de la licence sont respectées + + Returns: + dict avec {allowed: bool, error: str} + """ + validation = validate_license() + + if not validation.get("valid"): + return { + "allowed": False, + "error": validation.get("error") + } + + limits = validation.get("limits", {}) + stats = get_usage_stats() + + # Vérifier pushes par jour + max_pushes = limits.get("max_pushes_per_day", -1) + if max_pushes > 0 and stats.get("pushes_today", 0) >= max_pushes: + return { + "allowed": False, + "error": f"Limite quotidienne atteinte ({max_pushes} pushes/jour)" + } + + return { + "allowed": True, + "license_info": validation, + "usage": stats + } + + +# ============================================ +# API REST POUR L'INTERFACE WEB +# ============================================ + +class LicenseAPIHandler(BaseHTTPRequestHandler): + """Handler pour les requêtes de l'API licence""" + + def send_cors_headers(self): + """Envoyer les headers CORS complets""" + origin = self.headers.get('Origin', '*') + self.send_header('Access-Control-Allow-Origin', origin) + self.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS, PUT, DELETE') + self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, Accept, Origin') + self.send_header('Access-Control-Allow-Credentials', 'true') + self.send_header('Access-Control-Max-Age', '86400') + + def do_OPTIONS(self): + logger.info(f"OPTIONS request from {self.headers.get('Origin', 'unknown')}") + self.send_response(200) + self.send_cors_headers() + self.end_headers() + return + + def do_GET(self): + """GET /license - Récupérer les infos de licence""" + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.send_cors_headers() + self.end_headers() + + try: + if '/license/status' in self.path or self.path == '/license': + validation = validate_license() + usage = get_usage_stats() + hostname = get_splunk_hostname() + + response = { + "status": "valid" if validation.get("valid") else "invalid", + "hostname": hostname, + "license": validation if validation.get("valid") else None, + "error": validation.get("error") if not validation.get("valid") else None, + "error_code": validation.get("error_code") if not validation.get("valid") else None, + "usage": usage + } + + elif '/license/hostname' in self.path: + response = { + "hostname": get_splunk_hostname() + } + + else: + response = {"error": "Unknown endpoint"} + + self.wfile.write(json.dumps(response).encode()) + + except Exception as e: + logger.error(f"GET error: {e}") + self.wfile.write(json.dumps({"error": str(e)}).encode()) + + def do_POST(self): + """POST /license/upload - Uploader une nouvelle licence""" + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.send_cors_headers() + self.end_headers() + + try: + content_length = int(self.headers.get('Content-Length', 0)) + body = self.rfile.read(content_length).decode('utf-8') + + if '/license/upload' in self.path: + # Le body contient le contenu du fichier .lic + data = json.loads(body) + license_content = data.get('license_content', '') + + if not license_content: + response = {"success": False, "error": "Contenu de licence vide"} + else: + response = save_license_file(license_content) + + elif '/license/delete' in self.path: + # Supprimer la licence + if os.path.exists(LICENSE_FILE_PATH): + os.remove(LICENSE_FILE_PATH) + response = {"success": True, "message": "Licence supprimée"} + else: + response = {"success": False, "error": "Aucune licence à supprimer"} + + else: + response = {"error": "Unknown endpoint"} + + self.wfile.write(json.dumps(response).encode()) + + except Exception as e: + logger.error(f"POST error: {e}") + self.wfile.write(json.dumps({"error": str(e)}).encode()) + + def log_message(self, format, *args): + logger.debug(format % args) + + +def start_license_server(port=9998): + """Démarrer le serveur API licence (optionnel, peut être intégré au serveur principal)""" + server = HTTPServer(('127.0.0.1', port), LicenseAPIHandler) + logger.info(f"License API server listening on 127.0.0.1:{port}") + server.serve_forever() + + +# ============================================ +# CLI POUR TESTS +# ============================================ + +if __name__ == '__main__': + import sys + + if len(sys.argv) > 1: + if sys.argv[1] == "status": + result = validate_license() + print(json.dumps(result, indent=2, ensure_ascii=False)) + + elif sys.argv[1] == "hostname": + print(f"Hostname: {get_splunk_hostname()}") + + elif sys.argv[1] == "server": + start_license_server() + + else: + print("Usage:") + print(" python license_validator.py status # Vérifier la licence") + print(" python license_validator.py hostname # Afficher le hostname") + print(" python license_validator.py server # Démarrer l'API") + else: + result = validate_license() + print(json.dumps(result, indent=2, ensure_ascii=False)) \ No newline at end of file diff --git a/apps/pusher_app_prem/local/.usage_stats b/apps/pusher_app_prem/local/.usage_stats new file mode 100644 index 00000000..92defa28 --- /dev/null +++ b/apps/pusher_app_prem/local/.usage_stats @@ -0,0 +1,6 @@ +{ + "total_pushes": 15, + "pushes_today": 14, + "last_push_date": "2026-02-19", + "apps_pushed": [] +} \ No newline at end of file diff --git a/apps/pusher_app_prem/local/usage_stats.json b/apps/pusher_app_prem/local/usage_stats.json index 04fbdb9b..bcf35e59 100755 --- a/apps/pusher_app_prem/local/usage_stats.json +++ b/apps/pusher_app_prem/local/usage_stats.json @@ -1 +1 @@ -{"pushes_today": 2, "pushes_total": 46, "last_push_date": "2026-02-13", "apps_pushed": []} \ No newline at end of file +{"pushes_today": 1, "pushes_total": 47, "last_push_date": "2026-02-14", "apps_pushed": []} \ No newline at end of file