2015-07-20 00:09:47 -07:00
'use strict' ;
exports = module . exports = {
CloudronError : CloudronError ,
initialize : initialize ,
uninitialize : uninitialize ,
getConfig : getConfig ,
2017-04-07 18:45:14 +02:00
getDisks : getDisks ,
2017-04-18 15:15:35 -07:00
getLogs : getLogs ,
2015-07-20 00:09:47 -07:00
reboot : reboot ,
2018-11-25 17:02:01 +01:00
isRebootRequired : isRebootRequired ,
2015-10-27 16:00:31 -07:00
2018-01-29 15:47:26 -08:00
onActivated : onActivated ,
2018-12-15 15:27:16 -08:00
prepareDashboardDomain : prepareDashboardDomain ,
2018-12-08 18:18:45 -08:00
setDashboardDomain : setDashboardDomain ,
2019-02-26 19:43:18 -08:00
setDashboardAndMailDomain : setDashboardAndMailDomain ,
2018-12-10 20:20:53 -08:00
renewCerts : renewCerts ,
2018-11-11 09:29:11 -08:00
2019-02-06 11:15:46 -08:00
runSystemChecks : runSystemChecks ,
2019-02-06 11:09:34 -08:00
// exposed for testing
_checkDiskSpace : checkDiskSpace
2015-09-28 23:10:09 -07:00
} ;
2015-07-20 00:09:47 -07:00
2019-03-18 19:13:44 -07:00
var apps = require ( './apps.js' ) ,
assert = require ( 'assert' ) ,
2015-07-20 00:09:47 -07:00
async = require ( 'async' ) ,
2019-02-28 16:46:30 -08:00
backups = require ( './backups.js' ) ,
2018-12-07 16:15:21 -08:00
clients = require ( './clients.js' ) ,
2015-07-20 00:09:47 -07:00
config = require ( './config.js' ) ,
2018-12-14 09:57:28 -08:00
constants = require ( './constants.js' ) ,
2017-01-09 11:00:09 -08:00
cron = require ( './cron.js' ) ,
2015-07-20 00:09:47 -07:00
debug = require ( 'debug' ) ( 'box:cloudron' ) ,
2018-11-11 09:29:11 -08:00
domains = require ( './domains.js' ) ,
2018-12-07 16:15:21 -08:00
DomainsError = require ( './domains.js' ) . DomainsError ,
2017-04-07 18:45:14 +02:00
df = require ( '@sindresorhus/df' ) ,
2019-02-04 20:24:28 -08:00
eventlog = require ( './eventlog.js' ) ,
2019-05-07 09:34:23 -07:00
custom = require ( './custom.js' ) ,
2018-11-25 17:02:01 +01:00
fs = require ( 'fs' ) ,
2019-02-04 14:49:51 -08:00
mail = require ( './mail.js' ) ,
2019-02-06 15:47:56 +01:00
notifications = require ( './notifications.js' ) ,
2015-12-31 10:30:42 +01:00
os = require ( 'os' ) ,
2015-07-20 00:09:47 -07:00
path = require ( 'path' ) ,
paths = require ( './paths.js' ) ,
2017-01-07 23:33:20 -08:00
platform = require ( './platform.js' ) ,
2018-01-30 12:23:27 -08:00
reverseProxy = require ( './reverseproxy.js' ) ,
2015-07-20 00:09:47 -07:00
settings = require ( './settings.js' ) ,
shell = require ( './shell.js' ) ,
2017-04-18 15:15:35 -07:00
spawn = require ( 'child_process' ) . spawn ,
split = require ( 'split' ) ,
2018-12-10 20:20:53 -08:00
tasks = require ( './tasks.js' ) ,
2018-04-29 10:58:45 -07:00
users = require ( './users.js' ) ,
2018-07-31 11:35:23 -07:00
util = require ( 'util' ) ;
2015-07-20 00:09:47 -07:00
2018-07-31 11:35:23 -07:00
var REBOOT _CMD = path . join ( _ _dirname , 'scripts/reboot.sh' ) ;
2015-07-20 00:09:47 -07:00
2015-10-29 12:28:50 -07:00
var NOOP _CALLBACK = function ( error ) { if ( error ) debug ( error ) ; } ;
2015-10-27 21:10:00 -07:00
2015-07-20 00:09:47 -07:00
function CloudronError ( reason , errorOrMessage ) {
assert . strictEqual ( typeof reason , 'string' ) ;
assert ( errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined' ) ;
Error . call ( this ) ;
Error . captureStackTrace ( this , this . constructor ) ;
this . name = this . constructor . name ;
this . reason = reason ;
if ( typeof errorOrMessage === 'undefined' ) {
this . message = reason ;
} else if ( typeof errorOrMessage === 'string' ) {
this . message = errorOrMessage ;
} else {
this . message = 'Internal error' ;
this . nestedError = errorOrMessage ;
}
}
util . inherits ( CloudronError , Error ) ;
CloudronError . BAD _FIELD = 'Field error' ;
CloudronError . INTERNAL _ERROR = 'Internal Error' ;
CloudronError . EXTERNAL _ERROR = 'External Error' ;
CloudronError . BAD _STATE = 'Bad state' ;
2016-01-14 11:13:00 -08:00
CloudronError . ALREADY _UPTODATE = 'No Update Available' ;
2015-07-20 00:09:47 -07:00
function initialize ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
2018-11-10 18:21:15 -08:00
runStartupTasks ( ) ;
2019-05-08 15:24:37 -07:00
callback ( ) ;
2015-07-20 00:09:47 -07:00
}
function uninitialize ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
2017-01-09 09:24:32 -08:00
async . series ( [
2018-09-26 12:44:38 -07:00
cron . stopJobs ,
2018-11-10 13:58:18 -08:00
platform . stop
2017-01-09 09:24:32 -08:00
] , callback ) ;
2015-07-20 00:09:47 -07:00
}
2017-11-22 21:31:30 -08:00
function onActivated ( callback ) {
2018-11-10 18:21:15 -08:00
assert . strictEqual ( typeof callback , 'function' ) ;
2017-11-22 21:31:30 -08:00
// Starting the platform after a user is available means:
// 1. mail bounces can now be sent to the cloudron owner
// 2. the restore code path can run without sudo (since mail/ is non-root)
2018-11-10 18:21:15 -08:00
async . series ( [
platform . start ,
2019-05-08 15:24:37 -07:00
cron . startJobs
2018-11-10 18:21:15 -08:00
] , callback ) ;
}
// each of these tasks can fail. we will add some routes to fix/re-run them
function runStartupTasks ( ) {
// configure nginx to be reachable by IP
reverseProxy . configureDefaultServer ( NOOP _CALLBACK ) ;
2018-11-11 08:19:24 -08:00
// always generate webadmin config since we have no versioning mechanism for the ejs
2018-12-13 22:15:08 -08:00
if ( config . adminDomain ( ) ) reverseProxy . writeAdminConfig ( config . adminDomain ( ) , NOOP _CALLBACK ) ;
2018-11-11 08:19:24 -08:00
2018-11-10 18:21:15 -08:00
// check activation state and start the platform
2018-11-10 18:08:08 -08:00
users . isActivated ( function ( error , activated ) {
2018-11-10 18:21:15 -08:00
if ( error ) return debug ( error ) ;
if ( ! activated ) return debug ( 'initialize: not activated yet' ) ; // not activated
2017-11-22 21:31:30 -08:00
2018-11-10 18:21:15 -08:00
onActivated ( NOOP _CALLBACK ) ;
2017-11-22 21:31:30 -08:00
} ) ;
}
2017-04-07 18:45:14 +02:00
function getDisks ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
var disks = {
boxDataDisk : null ,
platformDataDisk : null ,
appsDataDisk : null
} ;
df . file ( paths . BOX _DATA _DIR ) . then ( function ( result ) {
disks . boxDataDisk = result . filesystem ;
return df . file ( paths . PLATFORM _DATA _DIR ) ;
} ) . then ( function ( result ) {
disks . platformDataDisk = result . filesystem ;
return df . file ( paths . APPS _DATA _DIR ) ;
} ) . then ( function ( result ) {
disks . appsDataDisk = result . filesystem ;
callback ( null , disks ) ;
} ) . catch ( function ( error ) {
callback ( new CloudronError ( CloudronError . INTERNAL _ERROR , error ) ) ;
} ) ;
}
2015-07-20 00:09:47 -07:00
function getConfig ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
2018-08-03 18:25:43 -07:00
settings . getAll ( function ( error , allSettings ) {
2018-06-28 17:18:15 -07:00
if ( error ) return callback ( new CloudronError ( CloudronError . INTERNAL _ERROR , error ) ) ;
2018-01-02 13:05:30 -08:00
2018-06-28 17:40:57 -07:00
// be picky about what we send out here since this is sent for 'normal' users as well
2018-06-28 17:18:15 -07:00
callback ( null , {
apiServerOrigin : config . apiServerOrigin ( ) ,
webServerOrigin : config . webServerOrigin ( ) ,
adminDomain : config . adminDomain ( ) ,
adminFqdn : config . adminFqdn ( ) ,
mailFqdn : config . mailFqdn ( ) ,
version : config . version ( ) ,
isDemo : config . isDemo ( ) ,
memory : os . totalmem ( ) ,
provider : config . provider ( ) ,
2019-05-07 09:34:23 -07:00
cloudronName : allSettings [ settings . CLOUDRON _NAME _KEY ] ,
2019-05-10 15:53:34 -07:00
uiSpec : custom . uiSpec ( )
2015-07-20 00:09:47 -07:00
} ) ;
} ) ;
}
function reboot ( callback ) {
2018-11-25 14:57:17 -08:00
shell . sudo ( 'reboot' , [ REBOOT _CMD ] , { } , callback ) ;
2015-07-20 00:09:47 -07:00
}
2018-11-25 17:02:01 +01:00
function isRebootRequired ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
// https://serverfault.com/questions/92932/how-does-ubuntu-keep-track-of-the-system-restart-required-flag-in-motd
callback ( null , fs . existsSync ( '/var/run/reboot-required' ) ) ;
}
2019-02-06 15:47:56 +01:00
// called from cron.js
2019-02-06 11:15:46 -08:00
function runSystemChecks ( ) {
2019-02-06 15:47:56 +01:00
async . parallel ( [
checkBackupConfiguration ,
2019-02-06 14:58:45 -08:00
checkDiskSpace ,
2019-02-19 09:19:56 -08:00
checkMailStatus ,
checkRebootRequired
2019-02-28 16:46:30 -08:00
] , function ( error ) {
debug ( 'runSystemChecks: done' , error ) ;
2019-02-06 15:47:56 +01:00
} ) ;
}
function checkBackupConfiguration ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
debug ( 'Checking backup configuration' ) ;
2019-02-28 16:46:30 -08:00
backups . checkConfiguration ( function ( error , message ) {
if ( error ) return callback ( error ) ;
2019-02-28 14:52:14 -08:00
2019-03-07 14:49:38 -08:00
notifications . alert ( notifications . ALERT _BACKUP _CONFIG , 'Backup configuration is unsafe' , message , callback ) ;
2019-02-06 15:47:56 +01:00
} ) ;
}
2016-01-22 17:37:41 -08:00
function checkDiskSpace ( callback ) {
2019-02-06 15:47:56 +01:00
assert . strictEqual ( typeof callback , 'function' ) ;
2016-01-22 17:37:41 -08:00
debug ( 'Checking disk space' ) ;
2017-04-07 18:45:14 +02:00
getDisks ( function ( error , disks ) {
2016-01-22 17:37:41 -08:00
if ( error ) {
debug ( 'df error %s' , error . message ) ;
return callback ( ) ;
}
2017-04-07 18:45:14 +02:00
df ( ) . then ( function ( entries ) {
/ *
[ {
filesystem : '/dev/disk1' ,
size : 499046809600 ,
used : 443222245376 ,
available : 55562420224 ,
capacity : 0.89 ,
mountpoint : '/'
} , ... ]
* /
var oos = entries . some ( function ( entry ) {
// ignore other filesystems but where box, app and platform data is
if ( entry . filesystem !== disks . boxDataDisk && entry . filesystem !== disks . platformDataDisk && entry . filesystem !== disks . appsDataDisk ) return false ;
return ( entry . available <= ( 1.25 * 1024 * 1024 * 1024 ) ) ; // 1.5G
} ) ;
2016-01-22 17:37:41 -08:00
2017-04-07 18:45:14 +02:00
debug ( 'Disk space checked. ok: %s' , ! oos ) ;
2016-01-22 17:37:41 -08:00
2019-03-07 14:49:38 -08:00
notifications . alert ( notifications . ALERT _DISK _SPACE , 'Server is running out of disk space' , oos ? JSON . stringify ( entries , null , 4 ) : '' , callback ) ;
2017-04-07 18:45:14 +02:00
} ) . catch ( function ( error ) {
2019-02-06 15:47:56 +01:00
if ( error ) console . error ( error ) ;
callback ( ) ;
2017-04-07 18:45:14 +02:00
} ) ;
2016-01-22 17:37:41 -08:00
} ) ;
}
2016-01-25 16:03:12 -08:00
2019-02-06 14:58:45 -08:00
function checkMailStatus ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
debug ( 'checking mail status' ) ;
2019-02-28 16:46:30 -08:00
mail . checkConfiguration ( function ( error , message ) {
2019-02-06 14:58:45 -08:00
if ( error ) return callback ( error ) ;
2019-03-07 14:49:38 -08:00
notifications . alert ( notifications . ALERT _MAIL _STATUS , 'Email is not configured properly' , message , callback ) ;
2019-02-06 14:58:45 -08:00
} ) ;
}
2019-02-19 09:19:56 -08:00
function checkRebootRequired ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
debug ( 'checking if reboot required' ) ;
isRebootRequired ( function ( error , rebootRequired ) {
if ( error ) return callback ( error ) ;
2019-03-07 14:49:38 -08:00
notifications . alert ( notifications . ALERT _REBOOT , 'Reboot Required' , rebootRequired ? 'To finish security updates, a [reboot](/#/system) is necessary.' : '' , callback ) ;
2019-02-19 09:19:56 -08:00
} ) ;
}
2018-06-11 20:09:38 +02:00
function getLogs ( unit , options , callback ) {
assert . strictEqual ( typeof unit , 'string' ) ;
2017-04-18 20:32:57 -07:00
assert ( options && typeof options === 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2019-01-08 12:10:53 -08:00
assert . strictEqual ( typeof options . lines , 'number' ) ;
assert . strictEqual ( typeof options . format , 'string' ) ;
assert . strictEqual ( typeof options . follow , 'boolean' ) ;
2017-04-18 15:15:35 -07:00
2019-01-08 12:10:53 -08:00
var lines = options . lines === - 1 ? '+1' : options . lines ,
format = options . format || 'json' ,
follow = options . follow ;
2017-04-18 15:15:35 -07:00
2018-06-11 20:09:38 +02:00
debug ( 'Getting logs for %s as %s' , unit , format ) ;
2018-11-16 13:05:11 +01:00
let args = [ '--lines=' + lines ] ;
if ( follow ) args . push ( '--follow' ) ;
2017-04-18 15:15:35 -07:00
2018-11-16 13:05:11 +01:00
// need to handle box.log without subdir
if ( unit === 'box' ) args . push ( path . join ( paths . LOG _DIR , 'box.log' ) ) ;
2019-03-01 15:45:44 -08:00
else if ( unit . startsWith ( 'crash-' ) ) args . push ( path . join ( paths . CRASH _LOG _DIR , unit . slice ( 6 ) + '.log' ) ) ;
else return callback ( new CloudronError ( CloudronError . BAD _FIELD , 'No such unit' ) ) ;
2017-04-18 15:15:35 -07:00
2018-11-16 13:05:11 +01:00
var cp = spawn ( '/usr/bin/tail' , args ) ;
2017-04-18 20:32:57 -07:00
2018-11-16 13:05:11 +01:00
var transformStream = split ( function mapper ( line ) {
if ( format !== 'json' ) return line + '\n' ;
2017-04-18 15:15:35 -07:00
2018-11-16 13:05:11 +01:00
var data = line . split ( ' ' ) ; // logs are <ISOtimestamp> <msg>
var timestamp = ( new Date ( data [ 0 ] ) ) . getTime ( ) ;
if ( isNaN ( timestamp ) ) timestamp = 0 ;
return JSON . stringify ( {
realtimeTimestamp : timestamp * 1000 ,
message : line . slice ( data [ 0 ] . length + 1 ) ,
source : unit
} ) + '\n' ;
} ) ;
2017-04-18 15:15:35 -07:00
transformStream . close = cp . kill . bind ( cp , 'SIGKILL' ) ; // closing stream kills the child process
cp . stdout . pipe ( transformStream ) ;
return callback ( null , transformStream ) ;
}
2018-11-11 09:29:11 -08:00
2018-12-15 15:27:16 -08:00
function prepareDashboardDomain ( domain , auditSource , callback ) {
2018-12-14 09:57:28 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2018-12-15 15:27:16 -08:00
debug ( ` prepareDashboardDomain: ${ domain } ` ) ;
2018-12-14 09:57:28 -08:00
2019-03-18 19:13:44 -07:00
domains . get ( domain , function ( error , domainObject ) {
if ( error && error . reason === DomainsError . NOT _FOUND ) return callback ( new CloudronError ( CloudronError . BAD _FIELD , 'No such domain' ) ) ;
if ( error ) return callback ( new CloudronError ( CloudronError . INTERNAL _ERROR , error ) ) ;
const fqdn = domains . fqdn ( constants . ADMIN _LOCATION , domainObject ) ;
apps . getAll ( function ( error , result ) {
if ( error ) return callback ( new CloudronError ( CloudronError . INTERNAL _ERROR , error ) ) ;
const conflict = result . filter ( app => app . fqdn === fqdn ) ;
if ( conflict . length ) return callback ( new CloudronError ( CloudronError . BAD _STATE , 'Dashboard location conflicts with an existing app' ) ) ;
let task = tasks . startTask ( tasks . TASK _PREPARE _DASHBOARD _DOMAIN , [ domain , auditSource ] ) ;
task . on ( 'error' , ( error ) => callback ( new CloudronError ( CloudronError . INTERNAL _ERROR , error ) ) ) ;
task . on ( 'start' , ( taskId ) => callback ( null , taskId ) ) ;
} ) ;
} ) ;
2018-12-14 09:57:28 -08:00
}
2019-02-26 19:43:18 -08:00
// call this only pre activation since it won't start mail server
2019-02-04 20:24:28 -08:00
function setDashboardDomain ( domain , auditSource , callback ) {
2018-12-07 16:15:21 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
2019-02-04 20:24:28 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2018-12-07 16:15:21 -08:00
assert . strictEqual ( typeof callback , 'function' ) ;
2018-12-08 18:18:45 -08:00
debug ( ` setDashboardDomain: ${ domain } ` ) ;
2018-12-07 16:15:21 -08:00
2018-12-14 09:57:28 -08:00
domains . get ( domain , function ( error , domainObject ) {
2018-12-07 16:15:21 -08:00
if ( error && error . reason === DomainsError . NOT _FOUND ) return callback ( new CloudronError ( CloudronError . BAD _FIELD , 'No such domain' ) ) ;
if ( error ) return callback ( new CloudronError ( CloudronError . INTERNAL _ERROR , error ) ) ;
2019-01-16 21:36:48 -08:00
reverseProxy . writeAdminConfig ( domain , function ( error ) {
if ( error ) return callback ( new CloudronError ( CloudronError . INTERNAL _ERROR , error ) ) ;
2018-12-14 09:57:28 -08:00
2019-01-16 21:36:48 -08:00
const fqdn = domains . fqdn ( constants . ADMIN _LOCATION , domainObject ) ;
2018-12-07 16:15:21 -08:00
2019-01-16 21:36:48 -08:00
config . setAdminDomain ( domain ) ;
config . setAdminFqdn ( fqdn ) ;
2018-12-07 16:15:21 -08:00
2019-01-16 21:36:48 -08:00
clients . addDefaultClients ( config . adminOrigin ( ) , function ( error ) {
if ( error ) return callback ( new CloudronError ( CloudronError . INTERNAL _ERROR , error ) ) ;
2019-02-04 20:24:28 -08:00
eventlog . add ( eventlog . ACTION _DASHBOARD _DOMAIN _UPDATE , auditSource , { domain : domain , fqdn : fqdn } ) ;
2019-01-16 21:36:48 -08:00
callback ( null ) ;
} ) ;
2018-12-07 16:15:21 -08:00
} ) ;
} ) ;
}
2018-12-10 20:20:53 -08:00
2019-02-26 19:43:18 -08:00
// call this only post activation because it will restart mail server
function setDashboardAndMailDomain ( domain , auditSource , callback ) {
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
debug ( ` setDashboardAndMailDomain: ${ domain } ` ) ;
setDashboardDomain ( domain , auditSource , function ( error ) {
if ( error ) return callback ( error ) ;
mail . onMailFqdnChanged ( NOOP _CALLBACK ) ; // this will update dns and re-configure mail server
callback ( null ) ;
} ) ;
}
2018-12-10 20:20:53 -08:00
function renewCerts ( options , auditSource , callback ) {
2018-12-11 12:00:47 +01:00
assert . strictEqual ( typeof options , 'object' ) ;
2018-12-10 20:20:53 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2018-12-11 09:22:13 -08:00
let task = tasks . startTask ( tasks . TASK _RENEW _CERTS , [ options , auditSource ] ) ;
2018-12-10 20:20:53 -08:00
task . on ( 'error' , ( error ) => callback ( new CloudronError ( CloudronError . INTERNAL _ERROR , error ) ) ) ;
task . on ( 'start' , ( taskId ) => callback ( null , taskId ) ) ;
}