2023-10-23 04:39:35 -07:00
import { createHash , randomBytes } from 'crypto' ;
2024-10-23 02:54:53 -07:00
import { chmodSync , existsSync , mkdirSync , readFileSync , statSync , writeFileSync } from 'fs' ;
import { ApplicationError , jsonParse , ALPHABET , toResult } from 'n8n-workflow' ;
2024-10-15 05:55:13 -07:00
import { customAlphabet } from 'nanoid' ;
2024-09-30 06:38:56 -07:00
import path from 'path' ;
import { Service } from 'typedi' ;
2023-10-23 04:39:35 -07:00
2024-10-23 02:54:53 -07:00
import { InstanceSettingsConfig } from './InstanceSettingsConfig' ;
2024-10-15 05:55:13 -07:00
const nanoid = customAlphabet ( ALPHABET , 16 ) ;
2023-10-23 04:39:35 -07:00
interface ReadOnlySettings {
encryptionKey : string ;
}
interface WritableSettings {
tunnelSubdomain? : string ;
}
type Settings = ReadOnlySettings & WritableSettings ;
2024-08-02 06:18:33 -07:00
type InstanceRole = 'unset' | 'leader' | 'follower' ;
2024-09-16 04:37:14 -07:00
export type InstanceType = 'main' | 'webhook' | 'worker' ;
2024-01-16 09:25:53 -08:00
const inTest = process . env . NODE_ENV === 'test' ;
2023-10-23 04:39:35 -07:00
@Service ( )
export class InstanceSettings {
2023-11-07 06:58:28 -08:00
private readonly userHome = this . getUserHome ( ) ;
2023-10-23 04:39:35 -07:00
/** The path to the n8n folder in which all n8n related data gets saved */
readonly n8nFolder = path . join ( this . userHome , '.n8n' ) ;
2023-11-07 06:58:28 -08:00
/** The path to the folder where all generated static assets are copied to */
readonly staticCacheDir = path . join ( this . userHome , '.cache/n8n/public' ) ;
2023-10-23 04:39:35 -07:00
/** The path to the folder containing custom nodes and credentials */
readonly customExtensionDir = path . join ( this . n8nFolder , 'custom' ) ;
/** The path to the folder containing installed nodes (like community nodes) */
readonly nodesDownloadDir = path . join ( this . n8nFolder , 'nodes' ) ;
private readonly settingsFile = path . join ( this . n8nFolder , 'config' ) ;
2024-10-23 02:54:53 -07:00
readonly enforceSettingsFilePermissions = this . loadEnforceSettingsFilePermissionsFlag ( ) ;
2023-10-23 04:39:35 -07:00
private settings = this . loadOrCreate ( ) ;
2024-10-15 05:55:13 -07:00
/ * *
* Fixed ID of this n8n instance , for telemetry .
* Derived from encryption key . Do not confuse with ` hostId ` .
*
* @example '258fce876abf5ea60eb86a2e777e5e190ff8f3e36b5b37aafec6636c31d4d1f9'
* /
2023-10-24 00:55:57 -07:00
readonly instanceId = this . generateInstanceId ( ) ;
2024-09-16 04:37:14 -07:00
readonly instanceType : InstanceType ;
2024-10-23 02:54:53 -07:00
constructor ( private readonly config : InstanceSettingsConfig ) {
2024-09-16 04:37:14 -07:00
const command = process . argv [ 2 ] ;
this . instanceType = [ 'webhook' , 'worker' ] . includes ( command )
? ( command as InstanceType )
: 'main' ;
2024-10-15 05:55:13 -07:00
this . hostId = ` ${ this . instanceType } - ${ nanoid ( ) } ` ;
2024-09-16 04:37:14 -07:00
}
2024-08-26 03:35:39 -07:00
/ * *
* A main is :
* - ` unset ` during bootup ,
* - ` leader ` after bootup in single - main setup ,
* - ` leader ` or ` follower ` after bootup in multi - main setup .
*
* A non - main instance type ( e . g . ` worker ` ) is always ` unset ` .
* /
instanceRole : InstanceRole = 'unset' ;
2024-08-02 06:18:33 -07:00
2024-10-15 05:55:13 -07:00
/ * *
* Transient ID of this n8n instance , for scaling mode .
* Reset on restart . Do not confuse with ` instanceId ` .
*
* @example 'main-bnxa1riryKUNHtln'
* @example 'worker-nDJR0FnSd2Vf6DB5'
* @example 'webhook-jxQ7AO8IzxEtfW1F'
* /
readonly hostId : string ;
2024-08-02 06:18:33 -07:00
get isLeader() {
return this . instanceRole === 'leader' ;
}
markAsLeader() {
this . instanceRole = 'leader' ;
}
get isFollower() {
return this . instanceRole === 'follower' ;
}
markAsFollower() {
this . instanceRole = 'follower' ;
}
2023-10-23 04:39:35 -07:00
get encryptionKey() {
return this . settings . encryptionKey ;
}
get tunnelSubdomain() {
return this . settings . tunnelSubdomain ;
}
update ( newSettings : WritableSettings ) {
this . save ( { . . . this . settings , . . . newSettings } ) ;
}
/ * *
* The home folder path of the user .
* If none can be found it falls back to the current working directory
* /
private getUserHome() {
const homeVarName = process . platform === 'win32' ? 'USERPROFILE' : 'HOME' ;
return process . env . N8N_USER_FOLDER ? ? process . env [ homeVarName ] ? ? process . cwd ( ) ;
}
2024-01-16 09:25:53 -08:00
/ * *
* Load instance settings from the settings file . If missing , create a new
* settings file with an auto - generated encryption key .
* /
2023-10-23 04:39:35 -07:00
private loadOrCreate ( ) : Settings {
2024-01-16 09:25:53 -08:00
if ( existsSync ( this . settingsFile ) ) {
const content = readFileSync ( this . settingsFile , 'utf8' ) ;
2024-10-23 02:54:53 -07:00
this . ensureSettingsFilePermissions ( ) ;
2024-01-16 09:25:53 -08:00
const settings = jsonParse < Settings > ( content , {
errorMessage : ` Error parsing n8n-config file " ${ this . settingsFile } ". It does not seem to be valid JSON. ` ,
2023-10-23 04:39:35 -07:00
} ) ;
2024-01-16 09:25:53 -08:00
if ( ! inTest ) console . info ( ` User settings loaded from: ${ this . settingsFile } ` ) ;
const { encryptionKey , tunnelSubdomain } = settings ;
if ( process . env . N8N_ENCRYPTION_KEY && encryptionKey !== process . env . N8N_ENCRYPTION_KEY ) {
throw new ApplicationError (
` Mismatching encryption keys. The encryption key in the settings file ${ this . settingsFile } does not match the N8N_ENCRYPTION_KEY env var. Please make sure both keys match. More information: https://docs.n8n.io/hosting/environment-variables/configuration-methods/#encryption-key ` ,
) ;
}
return { encryptionKey , tunnelSubdomain } ;
}
mkdirSync ( this . n8nFolder , { recursive : true } ) ;
const encryptionKey = process . env . N8N_ENCRYPTION_KEY ? ? randomBytes ( 24 ) . toString ( 'base64' ) ;
const settings : Settings = { encryptionKey } ;
this . save ( settings ) ;
if ( ! inTest && ! process . env . N8N_ENCRYPTION_KEY ) {
console . info ( ` No encryption key found - Auto-generated and saved to: ${ this . settingsFile } ` ) ;
2023-10-23 04:39:35 -07:00
}
2024-10-23 02:54:53 -07:00
this . ensureSettingsFilePermissions ( ) ;
2023-10-23 04:39:35 -07:00
2024-01-16 09:25:53 -08:00
return settings ;
2023-10-24 00:55:57 -07:00
}
private generateInstanceId() {
const { encryptionKey } = this ;
return createHash ( 'sha256' )
2023-10-23 04:39:35 -07:00
. update ( encryptionKey . slice ( Math . round ( encryptionKey . length / 2 ) ) )
. digest ( 'hex' ) ;
}
private save ( settings : Settings ) {
this . settings = settings ;
2024-10-23 02:54:53 -07:00
writeFileSync ( this . settingsFile , JSON . stringify ( this . settings , null , '\t' ) , {
mode : this.enforceSettingsFilePermissions.enforce ? 0o600 : undefined ,
encoding : 'utf-8' ,
} ) ;
}
private loadEnforceSettingsFilePermissionsFlag ( ) : {
isSet : boolean ;
enforce : boolean ;
} {
const { enforceSettingsFilePermissions } = this . config ;
const isEnvVarSet = ! ! process . env . N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS ;
if ( this . isWindows ( ) ) {
if ( isEnvVarSet ) {
console . warn (
'Ignoring N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS as it is not supported on Windows.' ,
) ;
}
return {
isSet : isEnvVarSet ,
enforce : false ,
} ;
}
return {
isSet : isEnvVarSet ,
enforce : enforceSettingsFilePermissions ,
} ;
}
/ * *
* Ensures that the settings file has the r / w permissions only for the owner .
* /
private ensureSettingsFilePermissions() {
// If the flag is explicitly set to false, skip the check
if ( this . enforceSettingsFilePermissions . isSet && ! this . enforceSettingsFilePermissions . enforce ) {
return ;
}
if ( this . isWindows ( ) ) {
// Ignore windows as it does not support chmod. We have already logged a warning
return ;
}
const permissionsResult = toResult ( ( ) = > {
const stats = statSync ( this . settingsFile ) ;
return stats . mode & 0 o777 ;
} ) ;
// If we can't determine the permissions, log a warning and skip the check
if ( ! permissionsResult . ok ) {
console . warn (
` Could not ensure settings file permissions: ${ permissionsResult . error . message } . To skip this check, set N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=false. ` ,
) ;
return ;
}
const arePermissionsCorrect = permissionsResult . result === 0 o600 ;
if ( arePermissionsCorrect ) {
return ;
}
// If the permissions are incorrect and the flag is not set, log a warning
if ( ! this . enforceSettingsFilePermissions . isSet ) {
console . warn (
` Permissions 0 ${ permissionsResult . result . toString ( 8 ) } for n8n settings file ${ this . settingsFile } are too wide. This is ignored for now, but in the future n8n will attempt to change the permissions automatically. To automatically enforce correct permissions now set N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true (recommended), or turn this check off set N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=false. ` ,
) ;
// The default is false so we skip the enforcement for now
return ;
}
if ( this . enforceSettingsFilePermissions . enforce ) {
console . warn (
` Permissions 0 ${ permissionsResult . result . toString ( 8 ) } for n8n settings file ${ this . settingsFile } are too wide. Changing permissions to 0600.. ` ,
) ;
const chmodResult = toResult ( ( ) = > chmodSync ( this . settingsFile , 0 o600 ) ) ;
if ( ! chmodResult . ok ) {
// Some filesystems don't support permissions. In this case we log the
// error and ignore it. We might want to prevent the app startup in the
// future in this case.
console . warn (
` Could not enforce settings file permissions: ${ chmodResult . error . message } . To skip this check, set N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=false. ` ,
) ;
}
}
}
private isWindows() {
return process . platform === 'win32' ;
2023-10-23 04:39:35 -07:00
}
}