version avec licence dans le localstorage(sans serveur)

Pushed by: admin
License: TA9O64YS7EPT (Professional)
Timestamp: 2026-02-19T23:47:08.023330
masterdev
Splunk Git Pusher 2 months ago
parent bf90b22250
commit 5446d7d142

@ -239,6 +239,11 @@ async function pushDashboards() {
// Récupérer l'utilisateur courant
const currentUser = await getCurrentUser();
// Récupérer les infos de licence depuis le localStorage
const licenseInfo = getLicenseInfo ? getLicenseInfo() : null;
const licenseType = licenseInfo?.type_name || '';
const licenseId = licenseInfo?.license_id || '';
// Construire les paramètres
const params = new URLSearchParams({
git_url: gitUrl,
@ -249,7 +254,9 @@ async function pushDashboards() {
user: currentUser,
deploy_to_shcluster: deployToSHCluster.toString(),
deployer_host: SH_DEPLOYER_CONFIG.host,
deployer_token: SH_DEPLOYER_CONFIG.token
deployer_token: SH_DEPLOYER_CONFIG.token,
license_type: licenseType,
license_id: licenseId
});
// Ajouter les credentials SH si fournis
@ -270,6 +277,25 @@ async function pushDashboards() {
console.log("Push result:", result);
if (result.status === 'success') {
// Incrémenter le compteur d'utilisation côté client
console.log("=== INCREMENT USAGE ===");
console.log("typeof incrementUsage:", typeof incrementUsage);
console.log("typeof window.incrementUsage:", typeof window.incrementUsage);
try {
if (typeof window.incrementUsage === 'function') {
const stats = window.incrementUsage();
console.log("✓ Usage incremented successfully:", stats);
} else if (typeof incrementUsage === 'function') {
const stats = incrementUsage();
console.log("✓ Usage incremented (local):", stats);
} else {
console.warn("✗ incrementUsage function not available");
}
} catch (e) {
console.error("✗ Error incrementing usage:", e);
}
let message = `✅ Successfully deployed ${result.apps_pushed || selectedApps.length} application(s) to Git!`;
// Ajouter le statut du déploiement SH Cluster
@ -688,6 +714,12 @@ window.showDeployerConfigModal = showDeployerConfigModal;
window.closeDeployerConfigModal = closeDeployerConfigModal;
window.saveDeployerConfigFromModal = saveDeployerConfigFromModal;
// Exposer les fonctions principales
window.pushDashboards = pushDashboards;
window.resetForm = resetForm;
window.toggleSelectAll = toggleSelectAll;
window.updateSelectedApps = updateSelectedApps;
// Fonction toggle pour le HTML
window.toggleDeployerAuth = function() {
var checkbox = document.getElementById('deploy-to-shcluster');
@ -698,40 +730,43 @@ window.toggleDeployerAuth = function() {
};
// ============================================
// EXPORT POUR DEBUG
// ATTACHEMENT DES ÉVÉNEMENTS AUX BOUTONS
// ============================================
window.GitPusher = {
config: GIT_PUSHER_CONFIG,
deployerConfig: SH_DEPLOYER_CONFIG,
getSelectedApps: () => selectedApps,
checkServer: checkServerHealth,
version: GIT_PUSHER_CONFIG.version
};
// ============================================
// FIX: Attacher les événements aux boutons
// ============================================
(function attachButtonEvents() {
function tryAttach() {
console.log("Trying to attach button events...");
// Bouton Deploy to Git
var pushBtn = document.getElementById('push-btn');
if (pushBtn) {
// Supprimer les anciens listeners
pushBtn.replaceWith(pushBtn.cloneNode(true));
pushBtn = document.getElementById('push-btn');
pushBtn.addEventListener('click', function(e) {
e.preventDefault();
console.log("Push button clicked");
e.stopPropagation();
console.log("Deploy button clicked!");
pushDashboards();
});
console.log("✓ Push button event attached");
console.log("✓ Deploy button event attached");
} else {
console.log("✗ Deploy button not found yet");
}
// Bouton Reset
var buttons = document.querySelectorAll('button.btn');
// Bouton Reset - chercher par classe ou contenu
var buttons = document.querySelectorAll('button.btn, button.btn-secondary');
buttons.forEach(function(btn) {
if (btn.textContent.includes('Reset') || btn.textContent.includes('🔄')) {
btn.addEventListener('click', function(e) {
// Supprimer les anciens listeners
var newBtn = btn.cloneNode(true);
btn.parentNode.replaceChild(newBtn, btn);
newBtn.addEventListener('click', function(e) {
e.preventDefault();
console.log("Reset button clicked");
e.stopPropagation();
console.log("Reset button clicked!");
resetForm(true);
});
console.log("✓ Reset button event attached");
@ -741,9 +776,14 @@ window.GitPusher = {
// Bouton Configure Deployer
var configBtn = document.querySelector('.deployer-config-btn');
if (configBtn) {
configBtn.addEventListener('click', function(e) {
// Supprimer les anciens listeners
var newConfigBtn = configBtn.cloneNode(true);
configBtn.parentNode.replaceChild(newConfigBtn, configBtn);
newConfigBtn.addEventListener('click', function(e) {
e.preventDefault();
console.log("Configure button clicked");
e.stopPropagation();
console.log("Configure button clicked!");
showDeployerConfigModal();
});
console.log("✓ Configure button event attached");
@ -761,18 +801,36 @@ window.GitPusher = {
console.log("✓ Deploy checkbox event attached");
}
// Si les boutons ne sont pas encore là, réessayer
// Si le bouton principal n'est pas encore là, réessayer
if (!pushBtn) {
console.log("Retrying in 500ms...");
setTimeout(tryAttach, 500);
} else {
console.log("=== All button events attached successfully ===");
}
}
// Démarrer après un délai
// Démarrer après un délai pour laisser le DOM se charger
if (document.readyState === 'complete') {
console.log("Document ready, attaching events in 1s...");
setTimeout(tryAttach, 1000);
} else {
window.addEventListener('load', function() {
console.log("Window loaded, attaching events in 1s...");
setTimeout(tryAttach, 1000);
});
}
})();
})();
// ============================================
// EXPORT POUR DEBUG
// ============================================
window.GitPusher = {
config: GIT_PUSHER_CONFIG,
deployerConfig: SH_DEPLOYER_CONFIG,
getSelectedApps: () => selectedApps,
checkServer: checkServerHealth,
checkDeployer: checkDeployerHealth,
version: GIT_PUSHER_CONFIG.version
};

@ -0,0 +1,778 @@
// ============================================
// GIT PUSHER - MAIN JAVASCRIPT
// Version 2.1 avec déploiement vers SH Cluster
// ============================================
// Configuration
const GIT_PUSHER_CONFIG = {
// Détecter automatiquement l'URL du serveur
serverUrl: window.location.protocol + '//' + window.location.hostname + ':9999',
credentialsKey: 'git_pusher_credentials',
deployerConfigKey: 'git_pusher_deployer_config',
version: '2.1.0'
};
// Configuration SH Deployer (peut être modifiée via l'interface)
let SH_DEPLOYER_CONFIG = {
enabled: false,
host: '10.10.40.14',
port: 9998,
token: ''
};
// État global
let selectedApps = [];
let isProcessing = false;
let deployerAvailable = false;
// ============================================
// INITIALISATION
// ============================================
require([
'jquery',
'splunkjs/mvc',
'splunkjs/mvc/searchmanager',
'splunkjs/mvc/simplexml/ready!'
], function($, mvc, SearchManager) {
console.log("Git Pusher v2.1 initializing...");
// Initialiser le système de licence
if (typeof initializeLicense === 'function') {
initializeLicense();
} else {
console.warn("License system not loaded");
}
// Charger les credentials sauvegardés
loadSavedCredentials();
// Charger la config du deployer
loadDeployerConfig();
// Vérifier la disponibilité du SH Deployer
checkDeployerHealth();
// Récupérer les résultats de recherche pour les apps
const searchManager = mvc.Components.get('dsearch');
if (searchManager) {
searchManager.on('search:done', function() {
const results = searchManager.data('results');
if (results) {
results.on('data', function() {
const rows = results.data().rows;
const fields = results.data().fields;
renderAppsList(rows, fields);
});
}
});
}
// Exposer les fonctions globalement
window.pushDashboards = pushDashboards;
window.resetForm = resetForm;
window.toggleSelectAll = toggleSelectAll;
});
// ============================================
// RENDU DE LA LISTE DES APPLICATIONS
// ============================================
function renderAppsList(rows, fields) {
const container = document.getElementById('dashboard-list');
if (!container) return;
// Trouver les index des colonnes
const nameIdx = fields.indexOf('name');
const labelIdx = fields.indexOf('label');
const descIdx = fields.indexOf('description');
// Générer le HTML
let html = `
<div class="app-header">
<div>
<input type="checkbox" id="select-all" onchange="toggleSelectAll(this.checked)" />
<label for="select-all" style="cursor: pointer; margin-left: 8px;">Select All</label>
</div>
<span class="app-count">${rows.length} apps</span>
</div>
`;
rows.forEach((row, index) => {
const name = row[nameIdx] || '';
const label = row[labelIdx] || name;
const desc = row[descIdx] || '';
// Ignorer certaines apps système
if (name.startsWith('splunk_') || name === 'learned' || name === 'launcher') {
return;
}
html += `
<div class="app-item">
<input type="checkbox"
id="app-${index}"
data-app-id="${name}"
data-app-label="${label}"
onchange="updateSelectedApps()" />
<label for="app-${index}">
<span class="app-badge">${name}</span>
${label !== name ? label : ''}
</label>
</div>
`;
});
container.innerHTML = html;
console.log(`Rendered ${rows.length} applications`);
}
// ============================================
// GESTION DE LA SÉLECTION
// ============================================
function updateSelectedApps() {
const checkboxes = document.querySelectorAll('#dashboard-list input[type="checkbox"][data-app-id]');
selectedApps = [];
checkboxes.forEach(cb => {
if (cb.checked) {
selectedApps.push({
id: cb.getAttribute('data-app-id'),
label: cb.getAttribute('data-app-label')
});
}
});
// Mettre à jour le "Select All"
const selectAll = document.getElementById('select-all');
if (selectAll) {
const allChecked = Array.from(checkboxes).every(cb => cb.checked);
const someChecked = Array.from(checkboxes).some(cb => cb.checked);
selectAll.checked = allChecked;
selectAll.indeterminate = someChecked && !allChecked;
}
console.log(`Selected ${selectedApps.length} apps`);
}
function toggleSelectAll(checked) {
const checkboxes = document.querySelectorAll('#dashboard-list input[type="checkbox"][data-app-id]');
checkboxes.forEach(cb => {
cb.checked = checked;
});
updateSelectedApps();
}
// ============================================
// PUSH VERS GIT
// ============================================
async function pushDashboards() {
console.log("Starting push process...");
// Vérifier si déjà en cours
if (isProcessing) {
console.log("Push already in progress");
return;
}
// Vérifier la licence AVANT tout
if (typeof checkLicenseBeforePush === 'function') {
const licenseOk = await checkLicenseBeforePush();
if (!licenseOk) {
console.log("License check failed");
return;
}
}
// Récupérer les valeurs du formulaire
const gitUrl = document.getElementById('git-url')?.value?.trim();
const gitBranch = document.getElementById('git-branch')?.value?.trim() || 'main';
const gitToken = document.getElementById('git-token')?.value?.trim();
const commitMessage = document.getElementById('commit-message')?.value?.trim();
const saveCredentials = document.getElementById('save-credentials')?.checked;
// Mettre à jour la liste des apps sélectionnées
updateSelectedApps();
// Validation
if (!gitUrl) {
showMessage('error', 'Please enter a Git repository URL');
return;
}
if (!gitToken) {
showMessage('error', 'Please enter a Git token or password');
return;
}
if (!commitMessage) {
showMessage('error', 'Please enter a commit message');
return;
}
if (selectedApps.length === 0) {
showMessage('error', 'Please select at least one application to deploy');
return;
}
// Sauvegarder les credentials si demandé
if (saveCredentials) {
saveCredentialsToStorage(gitUrl, gitBranch, gitToken);
}
// Vérifier si le déploiement vers SH Cluster est activé
const deployToSHCluster = document.getElementById('deploy-to-shcluster')?.checked || false;
const shAuthUser = document.getElementById('sh-auth-user')?.value?.trim() || '';
const shAuthPass = document.getElementById('sh-auth-pass')?.value?.trim() || '';
// Démarrer le push
isProcessing = true;
showLoading(true, deployToSHCluster);
hideMessages();
try {
// Récupérer l'utilisateur courant
const currentUser = await getCurrentUser();
// Construire les paramètres
const params = new URLSearchParams({
git_url: gitUrl,
git_branch: gitBranch,
git_token: gitToken,
commit_message: commitMessage,
apps: JSON.stringify(selectedApps),
user: currentUser,
deploy_to_shcluster: deployToSHCluster.toString(),
deployer_host: SH_DEPLOYER_CONFIG.host,
deployer_token: SH_DEPLOYER_CONFIG.token
});
// Ajouter les credentials SH si fournis
if (shAuthUser) params.append('sh_auth_user', shAuthUser);
if (shAuthPass) params.append('sh_auth_pass', shAuthPass);
console.log(`Pushing ${selectedApps.length} apps to ${gitUrl}${deployToSHCluster ? ' + SH Cluster deployment' : ''}`);
// Appeler le serveur
const response = await fetch(`${GIT_PUSHER_CONFIG.serverUrl}/push?${params.toString()}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const result = await response.json();
console.log("Push result:", result);
if (result.status === 'success') {
let message = `✅ Successfully deployed ${result.apps_pushed || selectedApps.length} application(s) to Git!`;
// Ajouter le statut du déploiement SH Cluster
if (deployToSHCluster && result.shcluster_deployment) {
if (result.shcluster_deployment.success) {
message += '\n🚀 SH Cluster deployment triggered successfully!';
} else {
message += `\n⚠ SH Cluster deployment failed: ${result.shcluster_deployment.message}`;
}
}
showMessage('success', message);
// Reset la sélection après succès
setTimeout(() => {
toggleSelectAll(false);
}, 2000);
} else if (result.error_code === 'LICENSE_ERROR') {
showMessage('error', `🔐 ${result.message}`);
// Afficher le modal de licence
if (typeof showLicenseModal === 'function') {
showLicenseModal(result.message, result.error_code);
}
} else if (result.error_code === 'APP_LIMIT') {
showMessage('error', `📦 ${result.message}`);
} else {
showMessage('error', result.message || 'Unknown error occurred');
}
} catch (error) {
console.error("Push error:", error);
showMessage('error', `Connection error: ${error.message}. Is the Git Pusher server running?`);
} finally {
isProcessing = false;
showLoading(false);
}
}
// ============================================
// UTILITAIRES UI
// ============================================
function showLoading(show, deployToSHCluster = false) {
const loading = document.getElementById('loading');
const pushBtn = document.getElementById('push-btn');
const loadingText = document.querySelector('.loading-text');
if (loading) {
loading.classList.toggle('active', show);
}
if (loadingText && show) {
if (deployToSHCluster) {
loadingText.textContent = 'Deploying to Git and SH Cluster... Please wait';
} else {
loadingText.textContent = 'Deploying applications to Git... Please wait';
}
}
if (pushBtn) {
pushBtn.disabled = show;
if (show) {
pushBtn.textContent = deployToSHCluster ? '⏳ Deploying to Git + SH...' : '⏳ Deploying...';
} else {
pushBtn.textContent = '✈️ Deploy to Git';
}
}
}
function showMessage(type, text) {
hideMessages();
const successMsg = document.getElementById('success-msg');
const errorMsg = document.getElementById('error-msg');
const successText = document.getElementById('success-text');
const errorText = document.getElementById('error-text');
if (type === 'success' && successMsg && successText) {
successText.textContent = text;
successMsg.classList.add('active');
// Auto-hide après 5 secondes
setTimeout(() => {
successMsg.classList.remove('active');
}, 5000);
} else if (type === 'error' && errorMsg && errorText) {
errorText.textContent = text;
errorMsg.classList.add('active');
}
}
function hideMessages() {
const successMsg = document.getElementById('success-msg');
const errorMsg = document.getElementById('error-msg');
if (successMsg) successMsg.classList.remove('active');
if (errorMsg) errorMsg.classList.remove('active');
}
function resetForm(clearCredentials = false) {
// Reset les champs
const commitMessage = document.getElementById('commit-message');
if (commitMessage) commitMessage.value = '';
if (clearCredentials) {
const gitUrl = document.getElementById('git-url');
const gitToken = document.getElementById('git-token');
const saveCredentials = document.getElementById('save-credentials');
if (gitUrl) gitUrl.value = '';
if (gitToken) gitToken.value = '';
if (saveCredentials) saveCredentials.checked = false;
// Supprimer les credentials sauvegardés
localStorage.removeItem(GIT_PUSHER_CONFIG.credentialsKey);
}
// Reset la sélection
toggleSelectAll(false);
// Cacher les messages
hideMessages();
console.log("Form reset" + (clearCredentials ? " (with credentials)" : ""));
}
// ============================================
// GESTION DES CREDENTIALS
// ============================================
function saveCredentialsToStorage(gitUrl, gitBranch, gitToken) {
try {
const credentials = {
gitUrl: gitUrl,
gitBranch: gitBranch,
// Note: En production, envisager une solution plus sécurisée
gitToken: btoa(gitToken), // Encodage basique (pas sécurisé, juste pour l'obfuscation)
savedAt: new Date().toISOString()
};
localStorage.setItem(GIT_PUSHER_CONFIG.credentialsKey, JSON.stringify(credentials));
console.log("Credentials saved");
} catch (error) {
console.error("Error saving credentials:", error);
}
}
function loadSavedCredentials() {
try {
const saved = localStorage.getItem(GIT_PUSHER_CONFIG.credentialsKey);
if (!saved) return;
const credentials = JSON.parse(saved);
const gitUrl = document.getElementById('git-url');
const gitBranch = document.getElementById('git-branch');
const gitToken = document.getElementById('git-token');
const saveCredentials = document.getElementById('save-credentials');
if (gitUrl && credentials.gitUrl) {
gitUrl.value = credentials.gitUrl;
}
if (gitBranch && credentials.gitBranch) {
gitBranch.value = credentials.gitBranch;
}
if (gitToken && credentials.gitToken) {
gitToken.value = atob(credentials.gitToken);
}
if (saveCredentials) {
saveCredentials.checked = true;
}
console.log("Credentials loaded from storage");
} catch (error) {
console.error("Error loading credentials:", error);
}
}
// ============================================
// RÉCUPÉRATION DE L'UTILISATEUR SPLUNK
// ============================================
async function getCurrentUser() {
try {
const response = await fetch('/en-US/splunkd/__raw/services/authentication/current-context?output_mode=json');
const data = await response.json();
return data.entry?.[0]?.content?.username || 'unknown';
} catch (error) {
console.error("Error getting current user:", error);
return 'unknown';
}
}
// ============================================
// VÉRIFICATION DU SERVEUR
// ============================================
async function checkServerHealth() {
try {
const response = await fetch(`${GIT_PUSHER_CONFIG.serverUrl}/health`, {
method: 'GET',
timeout: 5000
});
const data = await response.json();
return data.status === 'ok';
} catch (error) {
console.error("Server health check failed:", error);
return false;
}
}
// ============================================
// SH DEPLOYER FUNCTIONS
// ============================================
async function checkDeployerHealth() {
try {
const response = await fetch(`${GIT_PUSHER_CONFIG.serverUrl}/deployer/health`, {
method: 'GET',
timeout: 5000
});
const data = await response.json();
deployerAvailable = data.status === 'ok';
// Mettre à jour l'UI
updateDeployerUI();
console.log("SH Deployer status:", deployerAvailable ? "Available" : "Unavailable");
return deployerAvailable;
} catch (error) {
console.error("Deployer health check failed:", error);
deployerAvailable = false;
updateDeployerUI();
return false;
}
}
function updateDeployerUI() {
const deployerCheckbox = document.getElementById('deploy-to-shcluster');
const deployerStatus = document.getElementById('deployer-status');
const deployerSection = document.getElementById('deployer-section');
if (deployerCheckbox) {
deployerCheckbox.disabled = !deployerAvailable;
}
if (deployerStatus) {
if (deployerAvailable) {
deployerStatus.innerHTML = '<span style="color: #4CAF50;">● Connected</span>';
} else {
deployerStatus.innerHTML = '<span style="color: #f44336;">● Disconnected</span>';
}
}
if (deployerSection && !deployerAvailable) {
deployerSection.style.opacity = '0.6';
}
}
function loadDeployerConfig() {
try {
const saved = localStorage.getItem(GIT_PUSHER_CONFIG.deployerConfigKey);
if (saved) {
const config = JSON.parse(saved);
SH_DEPLOYER_CONFIG = { ...SH_DEPLOYER_CONFIG, ...config };
console.log("Deployer config loaded");
}
} catch (error) {
console.error("Error loading deployer config:", error);
}
}
function saveDeployerConfig() {
try {
localStorage.setItem(GIT_PUSHER_CONFIG.deployerConfigKey, JSON.stringify(SH_DEPLOYER_CONFIG));
console.log("Deployer config saved");
} catch (error) {
console.error("Error saving deployer config:", error);
}
}
function showDeployerConfigModal() {
const modal = document.createElement('div');
modal.id = 'deployer-config-modal';
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
`;
modal.innerHTML = `
<div style="background: white; border-radius: 16px; padding: 30px; max-width: 500px; width: 90%; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);">
<h2 style="margin: 0 0 20px 0; color: #333;">⚙️ SH Deployer Configuration</h2>
<div style="margin-bottom: 15px;">
<label style="display: block; font-weight: 600; margin-bottom: 5px;">Deployer Host</label>
<input type="text" id="deployer-config-host" value="${SH_DEPLOYER_CONFIG.host}"
style="width: 100%; padding: 10px; border: 2px solid #e0e0e0; border-radius: 6px; box-sizing: border-box;">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; font-weight: 600; margin-bottom: 5px;">Port</label>
<input type="number" id="deployer-config-port" value="${SH_DEPLOYER_CONFIG.port}"
style="width: 100%; padding: 10px; border: 2px solid #e0e0e0; border-radius: 6px; box-sizing: border-box;">
</div>
<div style="margin-bottom: 20px;">
<label style="display: block; font-weight: 600; margin-bottom: 5px;">Auth Token</label>
<input type="password" id="deployer-config-token" value="${SH_DEPLOYER_CONFIG.token}"
placeholder="Deployer agent token"
style="width: 100%; padding: 10px; border: 2px solid #e0e0e0; border-radius: 6px; box-sizing: border-box;">
</div>
<div style="display: flex; gap: 10px;">
<button onclick="saveDeployerConfigFromModal()" style="
flex: 1; padding: 12px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; border: none; border-radius: 8px; font-weight: 600; cursor: pointer;
">Save & Test</button>
<button onclick="closeDeployerConfigModal()" style="
flex: 1; padding: 12px; background: #f5f5f5; color: #333;
border: none; border-radius: 8px; font-weight: 600; cursor: pointer;
">Cancel</button>
</div>
<div id="deployer-config-message" style="margin-top: 15px; display: none; padding: 10px; border-radius: 6px;"></div>
</div>
`;
document.body.appendChild(modal);
}
function closeDeployerConfigModal() {
const modal = document.getElementById('deployer-config-modal');
if (modal) modal.remove();
}
async function saveDeployerConfigFromModal() {
const host = document.getElementById('deployer-config-host')?.value?.trim();
const port = parseInt(document.getElementById('deployer-config-port')?.value) || 9998;
const token = document.getElementById('deployer-config-token')?.value?.trim();
const msgEl = document.getElementById('deployer-config-message');
if (!host) {
if (msgEl) {
msgEl.style.display = 'block';
msgEl.style.background = '#ffebee';
msgEl.style.color = '#c62828';
msgEl.textContent = 'Please enter a host';
}
return;
}
// Mettre à jour la config
SH_DEPLOYER_CONFIG.host = host;
SH_DEPLOYER_CONFIG.port = port;
SH_DEPLOYER_CONFIG.token = token;
// Sauvegarder
saveDeployerConfig();
// Tester la connexion
if (msgEl) {
msgEl.style.display = 'block';
msgEl.style.background = '#e3f2fd';
msgEl.style.color = '#1565c0';
msgEl.textContent = 'Testing connection...';
}
const isHealthy = await checkDeployerHealth();
if (isHealthy) {
if (msgEl) {
msgEl.style.background = '#e8f5e9';
msgEl.style.color = '#2e7d32';
msgEl.textContent = '✓ Connected successfully!';
}
setTimeout(closeDeployerConfigModal, 1500);
} else {
if (msgEl) {
msgEl.style.background = '#ffebee';
msgEl.style.color = '#c62828';
msgEl.textContent = '✗ Connection failed. Check host and port.';
}
}
}
// Vérifier la santé du serveur au démarrage
setTimeout(async () => {
const healthy = await checkServerHealth();
if (!healthy) {
console.warn("Git Pusher server may not be running");
} else {
console.log("Git Pusher server is healthy");
}
}, 1000);
// ============================================
// EXPORT FONCTIONS GLOBALES
// ============================================
// Exposer les fonctions du deployer globalement pour les onclick du HTML
window.showDeployerConfigModal = showDeployerConfigModal;
window.closeDeployerConfigModal = closeDeployerConfigModal;
window.saveDeployerConfigFromModal = saveDeployerConfigFromModal;
// Fonction toggle pour le HTML
window.toggleDeployerAuth = function() {
var checkbox = document.getElementById('deploy-to-shcluster');
var authSection = document.getElementById('deployer-auth');
if (checkbox && authSection) {
authSection.classList.toggle('visible', checkbox.checked);
}
};
// ============================================
// EXPORT POUR DEBUG
// ============================================
window.GitPusher = {
config: GIT_PUSHER_CONFIG,
deployerConfig: SH_DEPLOYER_CONFIG,
getSelectedApps: () => selectedApps,
checkServer: checkServerHealth,
version: GIT_PUSHER_CONFIG.version
};
// ============================================
// FIX: Attacher les événements aux boutons
// ============================================
(function attachButtonEvents() {
function tryAttach() {
// Bouton Deploy to Git
var pushBtn = document.getElementById('push-btn');
if (pushBtn) {
pushBtn.addEventListener('click', function(e) {
e.preventDefault();
console.log("Push button clicked");
pushDashboards();
});
console.log("✓ Push button event attached");
}
// Bouton Reset
var buttons = document.querySelectorAll('button.btn');
buttons.forEach(function(btn) {
if (btn.textContent.includes('Reset') || btn.textContent.includes('🔄')) {
btn.addEventListener('click', function(e) {
e.preventDefault();
console.log("Reset button clicked");
resetForm(true);
});
console.log("✓ Reset button event attached");
}
});
// Bouton Configure Deployer
var configBtn = document.querySelector('.deployer-config-btn');
if (configBtn) {
configBtn.addEventListener('click', function(e) {
e.preventDefault();
console.log("Configure button clicked");
showDeployerConfigModal();
});
console.log("✓ Configure button event attached");
}
// Checkbox deploy to shcluster
var deployCheckbox = document.getElementById('deploy-to-shcluster');
if (deployCheckbox) {
deployCheckbox.addEventListener('change', function() {
var authSection = document.getElementById('deployer-auth');
if (authSection) {
authSection.classList.toggle('visible', this.checked);
}
});
console.log("✓ Deploy checkbox event attached");
}
// Si les boutons ne sont pas encore là, réessayer
if (!pushBtn) {
setTimeout(tryAttach, 500);
}
}
// Démarrer après un délai
if (document.readyState === 'complete') {
setTimeout(tryAttach, 1000);
} else {
window.addEventListener('load', function() {
setTimeout(tryAttach, 1000);
});
}
})();

File diff suppressed because it is too large Load Diff

@ -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 = `
<span style="font-size: 14px;">${badgeIcon}</span>
<span>${badgeText}</span>
`;
// 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 => `<span style="background: #e3f2fd; color: #1565c0; padding: 4px 8px; border-radius: 4px; font-size: 11px; margin: 2px;">${f}</span>`).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 = `
<div style="background: white; border-radius: 16px; padding: 30px; max-width: 500px; width: 90%; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2 style="margin: 0; color: #333;">📋 Détails de la licence</h2>
<button onclick="this.closest('#license-details-modal').remove()" style="background: none; border: none; font-size: 24px; cursor: pointer; color: #999;">&times;</button>
</div>
<div style="background: #f5f5f5; padding: 15px; border-radius: 8px; margin-bottom: 15px;">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<div>
<div style="color: #666; font-size: 11px; text-transform: uppercase;">Type</div>
<div style="font-weight: 600; color: #333;">${license.type_name || license.type}</div>
</div>
<div>
<div style="color: #666; font-size: 11px; text-transform: uppercase;">ID</div>
<div style="font-weight: 600; color: #333; font-family: monospace;">${license.license_id}</div>
</div>
<div>
<div style="color: #666; font-size: 11px; text-transform: uppercase;">Expire</div>
<div style="font-weight: 600; color: #333;">${license.expires}</div>
</div>
<div>
<div style="color: #666; font-size: 11px; text-transform: uppercase;">Jours restants</div>
<div style="font-weight: 600; color: ${license.days_remaining <= 7 ? '#f44336' : '#4caf50'};">${license.days_remaining}</div>
</div>
</div>
</div>
<div style="margin-bottom: 15px;">
<div style="color: #666; font-size: 11px; text-transform: uppercase; margin-bottom: 5px;">Client</div>
<div style="color: #333;">${license.customer?.name || 'N/A'} (${license.customer?.email || 'N/A'})</div>
</div>
<div style="margin-bottom: 15px;">
<div style="color: #666; font-size: 11px; text-transform: uppercase; margin-bottom: 5px;">Hostname</div>
<div style="color: #333; font-family: monospace;">${license.hostname || currentHostname}</div>
</div>
<div style="margin-bottom: 15px;">
<div style="color: #666; font-size: 11px; text-transform: uppercase; margin-bottom: 5px;">Limites</div>
<div style="color: #333;">Apps: <strong>${maxApps}</strong> | Pushes: <strong>${maxPushes}</strong></div>
</div>
<div style="margin-bottom: 20px;">
<div style="color: #666; font-size: 11px; text-transform: uppercase; margin-bottom: 8px;">Fonctionnalités</div>
<div style="display: flex; flex-wrap: wrap; gap: 4px;">${features || '<span style="color: #999;">Aucune</span>'}</div>
</div>
<div style="display: flex; gap: 10px;">
<button onclick="showLicenseModal(null, null, '${currentHostname}')" style="
flex: 1;
padding: 10px;
background: #667eea;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
">🔄 Changer de licence</button>
<button onclick="this.closest('#license-details-modal').remove()" style="
flex: 1;
padding: 10px;
background: #f5f5f5;
color: #333;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
">Fermer</button>
</div>
</div>
`;
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 = `
<div style="${errorStyle} padding: 12px; border-radius: 4px; margin-bottom: 20px; font-size: 13px;">
${error}
</div>
`;
}
modal.innerHTML = `
<div style="background: white; border-radius: 16px; padding: 40px; max-width: 550px; width: 90%; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);">
<div style="text-align: center; margin-bottom: 25px;">
<h1 style="font-size: 28px; margin: 0 0 8px 0; color: #333;">🔐 Git Pusher</h1>
<p style="color: #666; margin: 0; font-size: 14px;">Activation de licence requise</p>
</div>
${errorHtml}
<div style="background: #e8f5e9; padding: 15px; border-radius: 8px; margin-bottom: 20px; border-left: 4px solid #4caf50;">
<p style="margin: 0; color: #2e7d32; font-size: 13px;">
<strong>📋 Hostname Splunk:</strong><br>
<code style="background: white; padding: 4px 8px; border-radius: 4px; font-size: 14px; display: inline-block; margin-top: 5px;">${hostname || 'Chargement...'}</code>
</p>
<p style="margin: 10px 0 0 0; color: #558b2f; font-size: 11px;">
Communiquez ce hostname pour obtenir votre licence.
</p>
</div>
<div style="margin-bottom: 20px;">
<label style="display: block; font-weight: 600; color: #333; margin-bottom: 10px;">
📄 Fichier de licence (.lic)
</label>
<div id="license-dropzone" style="
border: 2px dashed #ccc;
border-radius: 8px;
padding: 30px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
background: #fafafa;
" ondragover="handleDragOver(event)" ondragleave="handleDragLeave(event)" ondrop="handleDrop(event)" onclick="document.getElementById('license-file-input').click()">
<div style="font-size: 40px; margin-bottom: 10px;">📁</div>
<div style="color: #666; font-size: 14px;">
Glissez votre fichier <strong>.lic</strong> ici<br>
<span style="color: #999; font-size: 12px;">ou cliquez pour sélectionner</span>
</div>
<input type="file" id="license-file-input" accept=".lic" style="display: none;" onchange="handleFileSelect(event)">
</div>
<div id="selected-file-info" style="display: none; margin-top: 10px; padding: 10px; background: #e3f2fd; border-radius: 6px;">
<span id="selected-file-name" style="color: #1565c0; font-weight: 500;"></span>
<button onclick="clearSelectedFile()" style="float: right; background: none; border: none; color: #f44336; cursor: pointer;">✕</button>
</div>
</div>
<div id="license-message" style="display: none; padding: 12px; border-radius: 8px; margin-bottom: 20px; font-size: 14px;"></div>
<button id="activate-btn" onclick="uploadLicense()" disabled style="
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
font-size: 15px;
transition: all 0.3s ease;
opacity: 0.5;
">Activer la licence</button>
<div style="text-align: center; margin-top: 20px;">
<a href="#" onclick="showContactInfo(); return false;" style="color: #667eea; text-decoration: none; font-size: 13px;">
Besoin d'une licence ? Contactez-nous
</a>
</div>
${currentLicense ? `
<div style="text-align: center; margin-top: 15px;">
<button onclick="hideLicenseModal()" style="background: none; border: none; color: #999; cursor: pointer; font-size: 13px;">
Annuler
</button>
</div>
` : ''}
</div>
`;
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
};

@ -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é

@ -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)

@ -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))
print(json.dumps(result, indent=2))

@ -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))

@ -0,0 +1,6 @@
{
"total_pushes": 15,
"pushes_today": 14,
"last_push_date": "2026-02-19",
"apps_pushed": []
}

@ -1 +1 @@
{"pushes_today": 2, "pushes_total": 46, "last_push_date": "2026-02-13", "apps_pushed": []}
{"pushes_today": 1, "pushes_total": 47, "last_push_date": "2026-02-14", "apps_pushed": []}
Loading…
Cancel
Save