From 1f3654a04c188ce82f63c4cac6b7e2fdd0aa44ab Mon Sep 17 00:00:00 2001 From: andrea Date: Thu, 31 Jul 2025 23:48:36 +0800 Subject: [PATCH] Add emulated-license-patcher.js --- emulated-license-patcher.js | 802 ++++++++++++++++++++++++++++++++++++ 1 file changed, 802 insertions(+) create mode 100644 emulated-license-patcher.js diff --git a/emulated-license-patcher.js b/emulated-license-patcher.js new file mode 100644 index 0000000..101840b --- /dev/null +++ b/emulated-license-patcher.js @@ -0,0 +1,802 @@ +#!/usr/bin/env node +/** + * Emulated N8N License Patcher + * Replaces minified LicenseManager with clean implementation + */ + +const fs = require('fs'); +const { execSync } = require('child_process'); + +class EmulatedLicensePatcher { + constructor() { + this.installations = []; + } + + findN8NInstallations() { + console.log('šŸ” Finding n8n installations...'); + + const searchPaths = [ + // Current directory and parent (for local installs) + './node_modules/@n8n_io/license-sdk/dist/LicenseManager.js', + '../node_modules/@n8n_io/license-sdk/dist/LicenseManager.js', + '../../node_modules/@n8n_io/license-sdk/dist/LicenseManager.js', + ]; + + // Try to find pnpm installations + try { + const pnpmPaths = execSync('find . -path "*/node_modules/.pnpm/@n8n_io+license-sdk*/node_modules/@n8n_io/license-sdk/dist/LicenseManager.js" 2>/dev/null || true', { encoding: 'utf8' }).trim(); + if (pnpmPaths) { + pnpmPaths.split('\n').forEach(path => path.trim() && searchPaths.push(path)); + } + } catch (e) {} + + // Try to find npm global installation + try { + const globalPath = execSync('npm root -g', { encoding: 'utf8' }).trim(); + searchPaths.push(`${globalPath}/n8n/node_modules/@n8n_io/license-sdk/dist/LicenseManager.js`); + } catch (e) {} + + // Try to find where n8n is installed globally + try { + const n8nPath = execSync('which n8n', { encoding: 'utf8' }).trim(); + if (n8nPath) { + const realPath = fs.realpathSync(n8nPath); + const licenseSDKPath = realPath.replace(/bin\/n8n$/, 'node_modules/@n8n_io/license-sdk/dist/LicenseManager.js'); + searchPaths.push(licenseSDKPath); + } + } catch (e) {} + + // Try to find via yarn global + try { + const yarnGlobalDir = execSync('yarn global dir', { encoding: 'utf8' }).trim(); + searchPaths.push(`${yarnGlobalDir}/node_modules/n8n/node_modules/@n8n_io/license-sdk/dist/LicenseManager.js`); + } catch (e) {} + + // Try common nvm paths (cross-platform) + const os = require('os'); + const homeDir = os.homedir(); + + try { + // Find nvm node versions + const nvmDir = process.env.NVM_DIR || `${homeDir}/.nvm`; + if (fs.existsSync(nvmDir)) { + const versionsDir = `${nvmDir}/versions/node`; + if (fs.existsSync(versionsDir)) { + const versions = fs.readdirSync(versionsDir); + versions.forEach(version => { + searchPaths.push(`${versionsDir}/${version}/lib/node_modules/n8n/node_modules/@n8n_io/license-sdk/dist/LicenseManager.js`); + }); + } + } + } catch (e) {} + + // Try Windows AppData paths + if (process.platform === 'win32') { + const appData = process.env.APPDATA || `${homeDir}/AppData/Roaming`; + searchPaths.push(`${appData}/npm/node_modules/n8n/node_modules/@n8n_io/license-sdk/dist/LicenseManager.js`); + } + + // Try macOS/Linux common paths + if (process.platform !== 'win32') { + searchPaths.push('/usr/local/lib/node_modules/n8n/node_modules/@n8n_io/license-sdk/dist/LicenseManager.js'); + searchPaths.push('/usr/lib/node_modules/n8n/node_modules/@n8n_io/license-sdk/dist/LicenseManager.js'); + } + + // Try Docker container paths (when running inside containers) + searchPaths.push('/usr/local/lib/node_modules/n8n/node_modules/@n8n_io/license-sdk/dist/LicenseManager.js'); + searchPaths.push('/usr/local/bin/node_modules/n8n/node_modules/@n8n_io/license-sdk/dist/LicenseManager.js'); + + // Remove duplicates and filter existing paths + const uniquePaths = [...new Set(searchPaths)]; + + for (const path of uniquePaths) { + if (fs.existsSync(path)) { + this.installations.push({ + path: path, + backupPath: path + '.original' + }); + } + } + + console.log(`Found ${this.installations.length} installation(s):`); + this.installations.forEach((inst, i) => { + const status = fs.existsSync(inst.backupPath) ? '(has backup)' : '(no backup)'; + console.log(` ${i + 1}. ${inst.path} ${status}`); + }); + + return this.installations; + } + + generateCleanLicenseManager() { + return `"use strict"; + +// Emulated LicenseManager that bypasses all validation +const AUTORENEWAL_INTERVAL = 15 * 60 * 1000; + +class LicenseManager { + constructor(config) { + this.config = config; + this.expirySoonCallbackFired = false; + this.isShuttingDown = false; + this.initializationPromise = null; + this.deviceFingerprint = null; + this.currentFeatures = {}; + + // Mock license certificate + this.licenseCert = { + consumerId: "emulated-consumer", + version: 1, + tenantId: config.tenantId || 1, + renewalToken: "emulated-token", + deviceLock: false, + deviceFingerprint: "", + createdAt: new Date("2025-01-01"), + issuedAt: new Date("2025-01-01"), + expiresAt: new Date("2030-01-01"), + terminatesAt: new Date("2030-01-01"), + planName: "Enterprise", + entitlements: [{ + id: "emulated-entitlement", + productId: "emulated-product", + productMetadata: { terms: { isMainPlan: true }, planName: "Enterprise" }, + features: { + "feat:sharing": true, + "feat:ldap": true, + "feat:saml": true, + "feat:variables": true, + "feat:external-secrets": true, + "feat:workflow-history": true, + "feat:binary-data-s3": true, + "feat:multiple-main-instances": true, + "feat:source-control": true, + "feat:advanced-permissions": true, + "feat:debug-in-editor": true, + "feat:showNonProdBanner": false, + "quota:users": -1, + "quota:activeWorkflowTriggers": -1, + "quota:variables": -1 + }, + featureOverrides: {}, + validFrom: new Date("2025-01-01"), + validTo: new Date("2030-01-01"), + isFloatable: false + }], + detachedEntitlementsCount: 0, + managementJwt: "", + isEphemeral: true + }; + + this.updateCurrentFeatures(); + + // Setup logger + if (config.logger) { + this.logger = config.logger; + } else { + this.logger = { + error: () => console.log("ERROR:", ...arguments), + warn: () => console.log("WARN:", ...arguments), + info: () => console.log("INFO:", ...arguments), + debug: () => console.log("DEBUG:", ...arguments) + }; + } + } + + get isInitialized() { + return !!this.initializationPromise; + } + + log(message, level) { + this.logger[level](\`[license SDK] \${message}\`); + } + + async initialize() { + if (this.initializationPromise) { + return this.initializationPromise; + } + + this.initializationPromise = this._doInitialization(); + return await this.initializationPromise; + } + + async _doInitialization() { + this.deviceFingerprint = await this.computeDeviceFingerprint(); + this.log(\`initializing for deviceFingerprint \${this.deviceFingerprint}\`, "debug"); + this.log("Emulated license manager initialized with Enterprise features", "info"); + } + + async computeDeviceFingerprint() { + if (this.config.deviceFingerprint && typeof this.config.deviceFingerprint === "function") { + return await this.config.deviceFingerprint(); + } + return "emulated-device-fingerprint"; + } + + // All validation methods return true + isValid(logErrors = true) { + return true; + } + + hasFeatureEnabled(feature, validateLicense = true) { + // Special case: this feature should be FALSE for licensed instances + if (feature === 'feat:showNonProdBanner') { + return false; + } + // All other enterprise features should be true + return true; + } + + hasFeatureDefined(feature, validateLicense = true) { + return true; + } + + hasQuotaLeft(feature, used) { + return true; + } + + getFeatureValue(feature, validateLicense = true) { + // Special case: this feature should be FALSE for licensed instances + if (feature === 'feat:showNonProdBanner') { + return false; + } + // Return unlimited quota for quota features + if (feature.startsWith("quota:")) { + return -1; + } + // Return true for boolean features + return true; + } + + hasCert() { + return true; + } + + isTerminated() { + return false; + } + + getExpiryDate() { + return this.licenseCert.expiresAt; + } + + getTerminationDate() { + return this.licenseCert.terminatesAt; + } + + getFeatures() { + return this.currentFeatures; + } + + updateCurrentFeatures() { + this.currentFeatures = { + "feat:sharing": true, + "feat:ldap": true, + "feat:saml": true, + "feat:variables": true, + "feat:external-secrets": true, + "feat:workflow-history": true, + "feat:binary-data-s3": true, + "feat:multiple-main-instances": true, + "feat:source-control": true, + "feat:advanced-permissions": true, + "feat:debug-in-editor": true, + "feat:showNonProdBanner": false, + "quota:users": -1, + "quota:activeWorkflowTriggers": -1, + "quota:variables": -1 + }; + } + + getCurrentEntitlements() { + return this.licenseCert.entitlements; + } + + getManagementJwt() { + return this.licenseCert.managementJwt; + } + + async getCertStr() { + return "emulated-cert-string"; + } + + getConsumerId() { + return this.licenseCert.consumerId; + } + + isRenewalDue() { + return false; + } + + // License management methods (no-ops) + async activate(activationKey) { + this.log("license activation bypassed (emulated)", "info"); + return Promise.resolve(); + } + + async renew() { + this.log("license renewal bypassed (emulated)", "info"); + return Promise.resolve(); + } + + async clear() { + this.log("license clear bypassed (emulated)", "info"); + return Promise.resolve(); + } + + async shutdown() { + this.isShuttingDown = true; + this.log("license manager shutdown (emulated)", "info"); + return Promise.resolve(); + } + + // Timer management (no-ops) + setupSingleTimer(callback, delay) { + return setTimeout(callback, delay); + } + + clearSingleTimer(timer) { + if (timer) clearTimeout(timer); + } + + setupRepeatingTimer(callback, interval) { + return setInterval(callback, interval); + } + + clearRepeatingTimer(timer) { + if (timer) clearInterval(timer); + } + + // Utility methods + formatDuration(seconds) { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = seconds % 60; + return \`\${hours}h \${minutes}m \${secs}s\`; + } + + toString() { + return \`## EMULATED LICENSE MANAGER ## +License: Enterprise (Emulated) +Valid: true +Features: All Enterprise features enabled +Expiry: \${this.licenseCert.expiresAt} +Status: Active (Bypassed)\`; + } + + // Additional methods that might be called + async reload() { + await this.initialize(); + } + + async reset() { + // No-op for emulated manager + } + + triggerOnFeatureChangeCallback() { + if (this.config.onFeatureChange) { + this.config.onFeatureChange(this.currentFeatures); + } + } + + enableAutoRenewals() { + this.log("Auto-renewals enabled (emulated)", "info"); + } + + disableAutoRenewals() { + this.log("Auto-renewals disabled (emulated)", "info"); + } +} + +// Export in the expected format +module.exports = { + AUTORENEWAL_INTERVAL, + LicenseManager +}; +`; + } + + debugFilePermissions(filePath) { + try { + const stats = fs.statSync(filePath); + const perms = (stats.mode & parseInt('777', 8)).toString(8); + console.log(`šŸ“‹ File: ${filePath}`); + console.log(` Permissions: ${perms}`); + console.log(` Owner: ${stats.uid}:${stats.gid}`); + console.log(` Size: ${stats.size} bytes`); + + // Check current user + try { + const whoami = execSync('whoami', { encoding: 'utf8' }).trim(); + const id = execSync('id', { encoding: 'utf8' }).trim(); + console.log(` Current user: ${whoami}`); + console.log(` User info: ${id}`); + } catch (e) {} + + // Check if directory is writable + const dirPath = require('path').dirname(filePath); + try { + fs.accessSync(dirPath, fs.constants.W_OK); + console.log(` Directory writable: āœ…`); + } catch (e) { + console.log(` Directory writable: āŒ ${e.message}`); + } + + } catch (e) { + console.log(`āŒ Could not debug file: ${e.message}`); + } + } + + patchInstallation(installation) { + console.log(`\nšŸ”§ Patching: ${installation.path}`); + + // Debug file permissions + this.debugFilePermissions(installation.path); + + try { + // Create backup first if possible + if (!fs.existsSync(installation.backupPath)) { + try { + fs.copyFileSync(installation.path, installation.backupPath); + console.log('āœ… Created backup'); + } catch (backupError) { + console.log('āš ļø Could not create backup, proceeding anyway...'); + } + } + + // Try direct write first + const cleanImplementation = this.generateCleanLicenseManager(); + fs.writeFileSync(installation.path, cleanImplementation); + + console.log('āœ… Replaced with clean emulated license manager'); + return true; + + } catch (error) { + console.error(`āŒ Direct patching failed: ${error.message}`); + + // Try alternative approaches for Docker/readonly filesystems + const alternatives = [ + () => { + console.log('šŸ”§ Trying to change file permissions...'); + execSync(`chmod 666 "${installation.path}"`); + fs.writeFileSync(installation.path, this.generateCleanLicenseManager()); + }, + () => { + console.log('šŸ”§ Trying to remove and recreate file...'); + execSync(`rm -f "${installation.path}"`); + fs.writeFileSync(installation.path, this.generateCleanLicenseManager()); + }, + () => { + console.log('šŸ”§ Trying with sudo-like approach...'); + const tempFile = '/tmp/LicenseManager.js.patched'; + fs.writeFileSync(tempFile, this.generateCleanLicenseManager()); + execSync(`cat "${tempFile}" > "${installation.path}"`); + }, + () => { + console.log('šŸ”§ Trying to mount tmpfs overlay...'); + const tempFile = '/tmp/LicenseManager.js.patched'; + fs.writeFileSync(tempFile, this.generateCleanLicenseManager()); + execSync(`mount --bind "${tempFile}" "${installation.path}"`); + } + ]; + + for (let i = 0; i < alternatives.length; i++) { + try { + alternatives[i](); + console.log(`āœ… Alternative method ${i + 1} succeeded`); + return true; + } catch (altError) { + console.log(`āŒ Alternative method ${i + 1} failed: ${altError.message}`); + } + } + + console.log('šŸ’” All patching methods failed. This might be a read-only container.'); + console.log(' Consider running the container with --privileged or mounting the file as writable.'); + return false; + } + } + + restoreInstallation(installation) { + console.log(`\nšŸ”„ Restoring: ${installation.path}`); + + if (!fs.existsSync(installation.backupPath)) { + console.log('āŒ No backup found'); + return false; + } + + try { + fs.copyFileSync(installation.backupPath, installation.path); + console.log('āœ… Restored successfully'); + return true; + } catch (error) { + console.error(`āŒ Error restoring: ${error.message}`); + return false; + } + } + + patchAll() { + const installations = this.findN8NInstallations(); + + if (installations.length === 0) { + console.log('\nāŒ No n8n installations found!'); + console.log('Make sure n8n is installed or check the search paths.'); + return; + } + + console.log('\nšŸš€ Applying patches...'); + + let successCount = 0; + for (const installation of installations) { + if (this.patchInstallation(installation)) { + successCount++; + } + } + + console.log(`\nāœ… Successfully patched ${successCount}/${installations.length} installations!`); + + if (successCount > 0) { + console.log('\nšŸŽ‰ N8N License Manager Replaced with Emulated Version!'); + console.log('\nWhat this does:'); + console.log(' • Replaces minified code with clean implementation'); + console.log(' • All license validation methods return true'); + console.log(' • All Enterprise features are enabled'); + console.log(' • No certificate or signature validation'); + console.log(' • Clean, readable code that\'s easy to maintain'); + console.log('\nTry running n8n now:'); + console.log(' n8n --help'); + console.log(' n8n start'); + } + } + + restoreAll() { + const installations = this.findN8NInstallations(); + + console.log('\nšŸ”„ Restoring all installations...'); + + let successCount = 0; + for (const installation of installations) { + if (this.restoreInstallation(installation)) { + successCount++; + } + } + + console.log(`\nāœ… Successfully restored ${successCount} installations!`); + } + + patchDockerCompose(filePath) { + console.log(`\n🐳 Patching Docker Compose file: ${filePath}`); + + if (!fs.existsSync(filePath)) { + console.log('āŒ Docker Compose file not found'); + return false; + } + + try { + const content = fs.readFileSync(filePath, 'utf8'); + const backupPath = filePath + '.backup'; + + // Create backup if it doesn't exist + if (!fs.existsSync(backupPath)) { + fs.writeFileSync(backupPath, content); + console.log('āœ… Created backup'); + } + + // Check if already patched + if (content.includes('./emulated-license-patcher.js:/tmp/patcher.js:ro')) { + console.log('āš ļø File appears to already be patched'); + return true; + } + + // Find n8n service + const lines = content.split('\n'); + let n8nServiceIndex = -1; + let volumesIndex = -1; + let inN8nService = false; + let serviceIndent = ''; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Look for n8n service + if (line.match(/^\s+n8n:\s*$/) || line.match(/^\s+n8n:\s+#/)) { + n8nServiceIndex = i; + inN8nService = true; + serviceIndent = line.match(/^(\s+)/)?.[1] || ' '; + continue; + } + + // If we're in n8n service, look for volumes + if (inN8nService && line.match(/^\s+volumes:\s*$/)) { + volumesIndex = i; + continue; + } + + // Stop when we hit another service or end of current service + if (inN8nService && line.match(/^\s+\w+:\s*$/) && !line.match(/^\s+(volumes|environment|ports|links|depends_on|entrypoint|command):/)) { + inN8nService = false; + } + } + + if (n8nServiceIndex === -1) { + console.log('āŒ Could not find n8n service in docker-compose file'); + return false; + } + + console.log(`āœ… Found n8n service at line ${n8nServiceIndex + 1}`); + + // Insert patcher volume mount and entrypoint override + let insertionPoint = -1; + let hasEntrypoint = false; + + // Find where to insert the volume mount + if (volumesIndex !== -1) { + // Add to existing volumes section + let lastVolumeIndex = volumesIndex; + for (let i = volumesIndex + 1; i < lines.length; i++) { + if (lines[i].match(/^\s+- /)) { + lastVolumeIndex = i; + } else if (lines[i].match(/^\s+\w+:/)) { + break; + } + } + insertionPoint = lastVolumeIndex + 1; + } else { + // Create volumes section + let servicesEndIndex = n8nServiceIndex + 1; + for (let i = n8nServiceIndex + 1; i < lines.length; i++) { + if (lines[i].match(/^\s+\w+:\s*$/) && !lines[i].match(/^\s+(image|restart|environment|ports|links|depends_on|entrypoint|command):/)) { + break; + } + if (lines[i].match(/^\s+(ports|links|depends_on):/)) { + servicesEndIndex = i; + } + } + + // Insert volumes section + lines.splice(servicesEndIndex + 1, 0, `${serviceIndent} volumes:`); + insertionPoint = servicesEndIndex + 2; + volumesIndex = servicesEndIndex + 1; + } + + // Add patcher volume mount + lines.splice(insertionPoint, 0, `${serviceIndent} - ./emulated-license-patcher.js:/tmp/patcher.js:ro`); + + // Find or add entrypoint + for (let i = n8nServiceIndex; i < lines.length; i++) { + if (lines[i].match(/^\s+entrypoint:/)) { + hasEntrypoint = true; + break; + } + if (lines[i].match(/^\s+\w+:\s*$/) && !lines[i].match(/^\s+(image|restart|environment|ports|links|depends_on|volumes|entrypoint|command):/)) { + break; + } + } + + if (hasEntrypoint) { + console.log('āš ļø Found existing entrypoint - you may need to manually merge the patching logic'); + } else { + // Add entrypoint after volumes section + const entrypointLines = [ + `${serviceIndent} # Override entrypoint to patch before starting`, + `${serviceIndent} entrypoint: >`, + `${serviceIndent} sh -c "`, + `${serviceIndent} echo 'šŸ”‘ Applying license patch...';`, + `${serviceIndent} node /tmp/patcher.js patch;`, + `${serviceIndent} echo 'āœ… Patch applied, starting n8n...';`, + `${serviceIndent} exec tini -- n8n start;`, + `${serviceIndent} "` + ]; + + // Find where to insert entrypoint (after volumes) + let entrypointInsertIndex = insertionPoint + 1; + for (let i = insertionPoint + 1; i < lines.length; i++) { + if (lines[i].match(/^\s+\w+:/)) { + entrypointInsertIndex = i; + break; + } + } + + lines.splice(entrypointInsertIndex, 0, ...entrypointLines); + } + + // Write patched file + fs.writeFileSync(filePath, lines.join('\n')); + console.log('āœ… Docker Compose file patched successfully'); + + console.log('\nšŸ“‹ What was added:'); + console.log(' • Volume mount: ./emulated-license-patcher.js:/tmp/patcher.js:ro'); + if (!hasEntrypoint) { + console.log(' • Entrypoint override to run patcher before n8n starts'); + } + console.log('\nāš ļø Make sure emulated-license-patcher.js is in the same directory as your docker-compose.yml'); + + return true; + + } catch (error) { + console.error(`āŒ Error patching Docker Compose: ${error.message}`); + return false; + } + } + + unpatchDockerCompose(filePath) { + console.log(`\nšŸ”„ Unpatching Docker Compose file: ${filePath}`); + + const backupPath = filePath + '.backup'; + + if (!fs.existsSync(backupPath)) { + console.log('āŒ No backup found'); + return false; + } + + try { + const backupContent = fs.readFileSync(backupPath, 'utf8'); + fs.writeFileSync(filePath, backupContent); + console.log('āœ… Docker Compose file restored from backup'); + return true; + } catch (error) { + console.error(`āŒ Error restoring Docker Compose: ${error.message}`); + return false; + } + } + + showUsage() { + console.log('šŸ”‘ Emulated N8N License Patcher'); + console.log('==============================='); + console.log(''); + console.log('Replaces minified LicenseManager with clean emulated implementation.'); + console.log(''); + console.log('Usage:'); + console.log(' node emulated-license-patcher.js patch # Apply patches'); + console.log(' node emulated-license-patcher.js restore # Restore original files'); + console.log(' node emulated-license-patcher.js find # Just find installations'); + console.log(' node emulated-license-patcher.js docker-patch # Patch docker-compose.yml'); + console.log(' node emulated-license-patcher.js docker-unpatch # Unpatch docker-compose.yml'); + console.log(' node emulated-license-patcher.js help # Show this help'); + console.log(''); + console.log('Docker Examples:'); + console.log(' node emulated-license-patcher.js docker-patch docker-compose.yml'); + console.log(' node emulated-license-patcher.js docker-unpatch docker-compose.yml'); + console.log(''); + console.log('āš ļø For development/testing purposes only!'); + } +} + +// Main execution +const patcher = new EmulatedLicensePatcher(); +const command = process.argv[2]; +const filePath = process.argv[3]; + +switch (command) { + case 'patch': + patcher.patchAll(); + break; + case 'restore': + patcher.restoreAll(); + break; + case 'find': + patcher.findN8NInstallations(); + break; + case 'docker-patch': + if (!filePath) { + console.log('āŒ Please specify docker-compose file path'); + console.log('Usage: node emulated-license-patcher.js docker-patch '); + process.exit(1); + } + patcher.patchDockerCompose(filePath); + break; + case 'docker-unpatch': + if (!filePath) { + console.log('āŒ Please specify docker-compose file path'); + console.log('Usage: node emulated-license-patcher.js docker-unpatch '); + process.exit(1); + } + patcher.unpatchDockerCompose(filePath); + break; + case 'help': + case '--help': + case '-h': + case undefined: + patcher.showUsage(); + break; + default: + console.log('āŒ Invalid command. Use "help" for usage information.'); + process.exit(1); +} \ No newline at end of file