-
📁
-
- Glissez votre fichier .lic ici
- ou cliquez pour sélectionner
-
-
+
+
Fonctionnalités:
+
${features}
-
-
-
+
+ Utilisation:
+ Total pushes: ${usage.total_pushes} | Aujourd'hui: ${usage.pushes_today}
-
-
-
-
-
`;
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 = `
+
+
+
+
+ ${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 = `
+
+
+
+
+
+ | 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