n8n-license-emulator/emulated-license-patcher.js

802 lines
28 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 <file> # Patch docker-compose.yml');
console.log(' node emulated-license-patcher.js docker-unpatch <file> # 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 <file>');
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 <file>');
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);
}