Add emulated-license-patcher.js

This commit is contained in:
andrea 2025-07-31 23:48:36 +08:00
commit 1f3654a04c

802
emulated-license-patcher.js Normal file
View File

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