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 fe7ac122..476fc41c Binary files a/apps/pusher_app_prem/bin/__pycache__/license_validator.cpython-39.pyc and b/apps/pusher_app_prem/bin/__pycache__/license_validator.cpython-39.pyc differ 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