2015-07-20 00:09:47 -07:00
'use strict' ;
exports = module . exports = {
AppsError : AppsError ,
2015-10-15 15:06:34 +02:00
hasAccessTo : hasAccessTo ,
2015-07-20 00:09:47 -07:00
get : get ,
2016-02-18 15:43:46 +01:00
getByIpAddress : getByIpAddress ,
2015-07-20 00:09:47 -07:00
getAll : getAll ,
2016-02-25 11:28:29 +01:00
getAllByUser : getAllByUser ,
2015-07-20 00:09:47 -07:00
install : install ,
configure : configure ,
uninstall : uninstall ,
restore : restore ,
2016-06-17 17:12:55 -05:00
clone : clone ,
2015-07-20 00:09:47 -07:00
update : update ,
backup : backup ,
2016-01-19 13:35:18 +01:00
listBackups : listBackups ,
2015-07-20 00:09:47 -07:00
getLogs : getLogs ,
start : start ,
stop : stop ,
exec : exec ,
checkManifestConstraints : checkManifestConstraints ,
2016-06-02 18:49:56 -07:00
updateApps : updateApps ,
2015-07-20 00:09:47 -07:00
2016-05-24 10:33:10 -07:00
restoreInstalledApps : restoreInstalledApps ,
configureInstalledApps : configureInstalledApps ,
2016-06-13 13:50:06 -07:00
getAppConfig : getAppConfig ,
2015-07-20 00:09:47 -07:00
// exported for testing
_validateHostname : validateHostname ,
2015-10-15 12:26:48 +02:00
_validatePortBindings : validatePortBindings ,
2015-10-28 14:35:39 +01:00
_validateAccessRestriction : validateAccessRestriction
2015-07-20 00:09:47 -07:00
} ;
var addons = require ( './addons.js' ) ,
appdb = require ( './appdb.js' ) ,
2017-04-13 00:42:44 -07:00
appstore = require ( './appstore.js' ) ,
2017-04-13 01:11:20 -07:00
AppstoreError = require ( './appstore.js' ) . AppstoreError ,
2015-07-20 00:09:47 -07:00
assert = require ( 'assert' ) ,
async = require ( 'async' ) ,
backups = require ( './backups.js' ) ,
2016-06-13 18:11:11 -07:00
BackupsError = backups . BackupsError ,
2015-12-11 12:24:52 -08:00
certificates = require ( './certificates.js' ) ,
2015-07-20 00:09:47 -07:00
config = require ( './config.js' ) ,
constants = require ( './constants.js' ) ,
DatabaseError = require ( './databaseerror.js' ) ,
debug = require ( 'debug' ) ( 'box:apps' ) ,
2015-11-10 12:47:48 -08:00
docker = require ( './docker.js' ) ,
2016-05-01 21:37:08 -07:00
eventlog = require ( './eventlog.js' ) ,
2015-07-20 00:09:47 -07:00
fs = require ( 'fs' ) ,
2016-02-09 13:03:52 -08:00
groups = require ( './groups.js' ) ,
2016-09-23 17:23:45 -07:00
mailboxdb = require ( './mailboxdb.js' ) ,
2015-07-20 00:09:47 -07:00
manifestFormat = require ( 'cloudron-manifestformat' ) ,
path = require ( 'path' ) ,
paths = require ( './paths.js' ) ,
safe = require ( 'safetydance' ) ,
semver = require ( 'semver' ) ,
2015-11-02 11:20:50 -08:00
spawn = require ( 'child_process' ) . spawn ,
2015-07-20 00:09:47 -07:00
split = require ( 'split' ) ,
superagent = require ( 'superagent' ) ,
taskmanager = require ( './taskmanager.js' ) ,
2016-11-07 15:14:06 +01:00
updateChecker = require ( './updatechecker.js' ) ,
2016-07-14 14:23:08 +02:00
url = require ( 'url' ) ,
2015-07-20 00:09:47 -07:00
util = require ( 'util' ) ,
2016-06-04 13:40:43 -07:00
uuid = require ( 'node-uuid' ) ,
2015-10-28 14:35:39 +01:00
validator = require ( 'validator' ) ;
2015-07-20 00:09:47 -07:00
// http://dustinsenos.com/articles/customErrorsInNode
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
function AppsError ( 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 ;
2016-08-05 14:00:53 +02:00
if ( typeof errorOrMessage === 'undefined' ) {
2015-07-20 00:09:47 -07:00
this . message = reason ;
} else if ( typeof errorOrMessage === 'string' ) {
this . message = errorOrMessage ;
} else {
this . message = 'Internal error' ;
this . nestedError = errorOrMessage ;
}
}
util . inherits ( AppsError , Error ) ;
AppsError . INTERNAL _ERROR = 'Internal Error' ;
AppsError . EXTERNAL _ERROR = 'External Error' ;
AppsError . ALREADY _EXISTS = 'Already Exists' ;
AppsError . NOT _FOUND = 'Not Found' ;
AppsError . BAD _FIELD = 'Bad Field' ;
AppsError . BAD _STATE = 'Bad State' ;
AppsError . PORT _RESERVED = 'Port Reserved' ;
AppsError . PORT _CONFLICT = 'Port Conflict' ;
AppsError . BILLING _REQUIRED = 'Billing Required' ;
2015-10-15 15:18:40 +02:00
AppsError . ACCESS _DENIED = 'Access denied' ;
2015-10-27 16:36:09 +01:00
AppsError . BAD _CERTIFICATE = 'Invalid certificate' ;
2015-07-20 00:09:47 -07:00
// Hostname validation comes from RFC 1123 (section 2.1)
// Domain name validation comes from RFC 2181 (Name syntax)
// https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
// We are validating the validity of the location-fqdn as host name
function validateHostname ( location , fqdn ) {
2016-05-11 15:04:22 -07:00
var RESERVED _LOCATIONS = [ constants . ADMIN _LOCATION , constants . API _LOCATION , constants . SMTP _LOCATION , constants . IMAP _LOCATION , constants . MAIL _LOCATION , constants . POSTMAN _LOCATION ] ;
2015-07-20 00:09:47 -07:00
2016-06-03 22:14:08 -07:00
if ( RESERVED _LOCATIONS . indexOf ( location ) !== - 1 ) return new AppsError ( AppsError . BAD _FIELD , location + ' is reserved' ) ;
2015-07-20 00:09:47 -07:00
if ( location === '' ) return null ; // bare location
2016-06-03 22:14:08 -07:00
if ( ( location . length + 1 /*+ hyphen */ + fqdn . indexOf ( '.' ) ) > 63 ) return new AppsError ( AppsError . BAD _FIELD , 'Hostname length cannot be greater than 63' ) ;
if ( location . match ( /^[A-Za-z0-9-]+$/ ) === null ) return new AppsError ( AppsError . BAD _FIELD , 'Hostname can only contain alphanumerics and hyphen' ) ;
if ( location [ 0 ] === '-' || location [ location . length - 1 ] === '-' ) return new AppsError ( AppsError . BAD _FIELD , 'Hostname cannot start or end with hyphen' ) ;
if ( location . length + 1 /* hyphen */ + fqdn . length > 253 ) return new AppsError ( AppsError . BAD _FIELD , 'FQDN length exceeds 253 characters' ) ;
2015-07-20 00:09:47 -07:00
return null ;
}
// validate the port bindings
function validatePortBindings ( portBindings , tcpPorts ) {
2017-01-29 13:01:09 -08:00
assert . strictEqual ( typeof portBindings , 'object' ) ;
2015-07-20 00:09:47 -07:00
// keep the public ports in sync with firewall rules in scripts/initializeBaseUbuntuImage.sh
// these ports are reserved even if we listen only on 127.0.0.1 because we setup HostIp to be 127.0.0.1
// for custom tcp ports
var RESERVED _PORTS = [
2017-01-29 12:38:54 -08:00
22 , /* ssh */
2015-07-20 00:09:47 -07:00
25 , /* smtp */
53 , /* dns */
80 , /* http */
2016-05-05 15:00:07 -07:00
143 , /* imap */
2017-01-29 12:38:54 -08:00
202 , /* caas ssh */
2015-07-20 00:09:47 -07:00
443 , /* https */
2016-05-05 15:00:07 -07:00
465 , /* smtps */
587 , /* submission */
993 , /* imaps */
2015-07-20 00:09:47 -07:00
2003 , /* graphite (lo) */
2004 , /* graphite (lo) */
2020 , /* install server */
config . get ( 'port' ) , /* app server (lo) */
2016-04-15 12:33:54 -07:00
config . get ( 'sysadminPort' ) , /* sysadmin app server (lo) */
2016-05-24 00:52:35 -07:00
config . get ( 'smtpPort' ) , /* internal smtp port (lo) */
2015-09-16 10:12:59 -07:00
config . get ( 'ldapPort' ) , /* ldap server (lo) */
2015-07-20 00:09:47 -07:00
3306 , /* mysql (lo) */
2016-05-13 18:48:05 -07:00
4190 , /* managesieve */
2015-07-20 00:09:47 -07:00
8000 /* graphite (lo) */
] ;
if ( ! portBindings ) return null ;
var env ;
for ( env in portBindings ) {
if ( ! /^[a-zA-Z0-9_]+$/ . test ( env ) ) return new AppsError ( AppsError . BAD _FIELD , env + ' is not valid environment variable' ) ;
2016-06-03 22:15:02 -07:00
if ( ! Number . isInteger ( portBindings [ env ] ) ) return new AppsError ( AppsError . BAD _FIELD , portBindings [ env ] + ' is not an integer' ) ;
2015-11-25 13:49:20 +01:00
if ( RESERVED _PORTS . indexOf ( portBindings [ env ] ) !== - 1 ) return new AppsError ( AppsError . PORT _RESERVED , String ( portBindings [ env ] ) ) ;
2017-01-29 12:43:07 -08:00
if ( portBindings [ env ] <= 1023 || portBindings [ env ] > 65535 ) return new AppsError ( AppsError . BAD _FIELD , portBindings [ env ] + ' is not in permitted range' ) ;
2015-07-20 00:09:47 -07:00
}
// it is OK if there is no 1-1 mapping between values in manifest.tcpPorts and portBindings. missing values implies
// that the user wants the service disabled
tcpPorts = tcpPorts || { } ;
for ( env in portBindings ) {
if ( ! ( env in tcpPorts ) ) return new AppsError ( AppsError . BAD _FIELD , 'Invalid portBindings ' + env ) ;
}
return null ;
}
2015-10-15 12:26:48 +02:00
function validateAccessRestriction ( accessRestriction ) {
2015-10-16 15:11:54 +02:00
assert . strictEqual ( typeof accessRestriction , 'object' ) ;
2015-10-15 12:26:48 +02:00
2015-10-16 15:11:54 +02:00
if ( accessRestriction === null ) return null ;
2015-10-15 12:26:48 +02:00
2016-02-09 13:03:52 -08:00
if ( accessRestriction . users ) {
2016-06-03 22:16:55 -07:00
if ( ! Array . isArray ( accessRestriction . users ) ) return new AppsError ( AppsError . BAD _FIELD , 'users array property required' ) ;
if ( ! accessRestriction . users . every ( function ( e ) { return typeof e === 'string' ; } ) ) return new AppsError ( AppsError . BAD _FIELD , 'All users have to be strings' ) ;
2016-02-09 13:03:52 -08:00
}
if ( accessRestriction . groups ) {
2016-06-03 22:16:55 -07:00
if ( ! Array . isArray ( accessRestriction . groups ) ) return new AppsError ( AppsError . BAD _FIELD , 'groups array property required' ) ;
if ( ! accessRestriction . groups . every ( function ( e ) { return typeof e === 'string' ; } ) ) return new AppsError ( AppsError . BAD _FIELD , 'All groups have to be strings' ) ;
2016-02-09 13:03:52 -08:00
}
2016-06-04 13:20:10 -07:00
// TODO: maybe validate if the users and groups actually exist
2015-10-15 12:26:48 +02:00
return null ;
}
2016-02-11 17:39:15 +01:00
function validateMemoryLimit ( manifest , memoryLimit ) {
assert . strictEqual ( typeof manifest , 'object' ) ;
assert . strictEqual ( typeof memoryLimit , 'number' ) ;
2016-02-14 12:13:49 +01:00
var min = manifest . memoryLimit || constants . DEFAULT _MEMORY _LIMIT ;
2016-02-11 17:39:15 +01:00
var max = ( 4096 * 1024 * 1024 ) ;
// allow 0, which indicates that it is not set, the one from the manifest will be choosen but we don't commit any user value
// this is needed so an app update can change the value in the manifest, and if not set by the user, the new value should be used
if ( memoryLimit === 0 ) return null ;
2017-01-19 15:02:12 -08:00
// a special value that indicates unlimited memory
if ( memoryLimit === - 1 ) return null ;
2016-06-03 22:16:55 -07:00
if ( memoryLimit < min ) return new AppsError ( AppsError . BAD _FIELD , 'memoryLimit too small' ) ;
if ( memoryLimit > max ) return new AppsError ( AppsError . BAD _FIELD , 'memoryLimit too large' ) ;
2016-02-11 17:39:15 +01:00
return null ;
}
2016-07-14 14:23:08 +02:00
// https://tools.ietf.org/html/rfc7034
function validateXFrameOptions ( xFrameOptions ) {
assert . strictEqual ( typeof xFrameOptions , 'string' ) ;
if ( xFrameOptions === 'DENY' ) return null ;
if ( xFrameOptions === 'SAMEORIGIN' ) return null ;
var parts = xFrameOptions . split ( ' ' ) ;
if ( parts . length !== 2 || parts [ 0 ] !== 'ALLOW-FROM' ) return new AppsError ( AppsError . BAD _FIELD , 'xFrameOptions must be "DENY", "SAMEORIGIN" or "ALLOW-FROM uri"' ) ;
var uri = url . parse ( parts [ 1 ] ) ;
return ( uri . protocol === 'http:' || uri . protocol === 'https:' ) ? null : new AppsError ( AppsError . BAD _FIELD , 'xFrameOptions ALLOW-FROM uri must be a valid http[s] uri' ) ;
}
2017-01-20 05:48:25 -08:00
function validateDebugMode ( debugMode ) {
assert . strictEqual ( typeof debugMode , 'object' ) ;
if ( debugMode === null ) return null ;
if ( 'cmd' in debugMode && debugMode . cmd !== null && ! Array . isArray ( debugMode . cmd ) ) return new AppsError ( AppsError . BAD _FIELD , 'debugMode.cmd must be an array or null' ) ;
if ( 'readonlyRootfs' in debugMode && typeof debugMode . readonlyRootfs !== 'boolean' ) return new AppsError ( AppsError . BAD _FIELD , 'debugMode.readonlyRootfs must be a boolean' ) ;
return null ;
}
2015-07-20 00:09:47 -07:00
function getDuplicateErrorDetails ( location , portBindings , error ) {
assert . strictEqual ( typeof location , 'string' ) ;
assert . strictEqual ( typeof portBindings , 'object' ) ;
assert . strictEqual ( error . reason , DatabaseError . ALREADY _EXISTS ) ;
var match = error . message . match ( /ER_DUP_ENTRY: Duplicate entry '(.*)' for key/ ) ;
if ( ! match ) {
2017-02-07 10:48:51 -08:00
debug ( 'Unexpected SQL error message.' , error ) ;
2015-07-20 00:09:47 -07:00
return new AppsError ( AppsError . INTERNAL _ERROR ) ;
}
// check if the location conflicts
if ( match [ 1 ] === location ) return new AppsError ( AppsError . ALREADY _EXISTS ) ;
// check if any of the port bindings conflict
for ( var env in portBindings ) {
if ( portBindings [ env ] === parseInt ( match [ 1 ] ) ) return new AppsError ( AppsError . PORT _CONFLICT , match [ 1 ] ) ;
}
return new AppsError ( AppsError . ALREADY _EXISTS ) ;
}
2016-06-13 13:48:53 -07:00
function getAppConfig ( app ) {
return {
manifest : app . manifest ,
location : app . location ,
accessRestriction : app . accessRestriction ,
portBindings : app . portBindings ,
memoryLimit : app . memoryLimit ,
2016-07-15 11:08:11 +02:00
xFrameOptions : app . xFrameOptions || 'SAMEORIGIN' ,
2016-06-13 13:48:53 -07:00
altDomain : app . altDomain
} ;
}
2015-07-20 00:09:47 -07:00
function getIconUrlSync ( app ) {
2017-01-24 10:13:25 -08:00
var iconPath = paths . APP _ICONS _DIR + '/' + app . id + '.png' ;
2015-07-20 00:09:47 -07:00
return fs . existsSync ( iconPath ) ? '/api/v1/apps/' + app . id + '/icon' : null ;
}
2016-02-09 12:48:21 -08:00
function hasAccessTo ( app , user , callback ) {
2015-10-15 15:06:34 +02:00
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof user , 'object' ) ;
2016-02-09 12:48:21 -08:00
assert . strictEqual ( typeof callback , 'function' ) ;
2015-10-15 15:06:34 +02:00
2016-02-09 12:48:21 -08:00
if ( app . accessRestriction === null ) return callback ( null , true ) ;
2016-02-09 13:03:52 -08:00
// check user access
if ( app . accessRestriction . users . some ( function ( e ) { return e === user . id ; } ) ) return callback ( null , true ) ;
// check group access
if ( ! app . accessRestriction . groups ) return callback ( null , false ) ;
async . some ( app . accessRestriction . groups , function ( groupId , iteratorDone ) {
2017-02-16 20:11:09 -08:00
groups . isMember ( groupId , user . id , iteratorDone ) ;
} , function ( error , result ) {
callback ( null , ! error && result ) ;
2016-02-09 13:03:52 -08:00
} ) ;
2015-10-15 15:06:34 +02:00
}
2015-07-20 00:09:47 -07:00
function get ( appId , callback ) {
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
appdb . get ( appId , function ( error , app ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . NOT _FOUND , 'No such app' ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
app . iconUrl = getIconUrlSync ( app ) ;
2016-04-22 23:46:45 -07:00
app . fqdn = app . altDomain || config . appFqdn ( app . location ) ;
2017-03-09 15:11:25 +01:00
app . cnameTarget = app . altDomain ? config . appFqdn ( app . location ) : null ;
2015-07-20 00:09:47 -07:00
callback ( null , app ) ;
} ) ;
}
2016-02-18 15:43:46 +01:00
function getByIpAddress ( ip , callback ) {
assert . strictEqual ( typeof ip , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
docker . getContainerIdByIp ( ip , function ( error , containerId ) {
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
appdb . getByContainerId ( containerId , function ( error , app ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . NOT _FOUND , 'No such app' ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
app . iconUrl = getIconUrlSync ( app ) ;
2016-04-22 23:46:45 -07:00
app . fqdn = app . altDomain || config . appFqdn ( app . location ) ;
2017-03-09 15:11:25 +01:00
app . cnameTarget = app . altDomain ? config . appFqdn ( app . location ) : null ;
2016-02-18 15:43:46 +01:00
callback ( null , app ) ;
} ) ;
} ) ;
}
2015-07-20 00:09:47 -07:00
function getAll ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
appdb . getAll ( function ( error , apps ) {
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
apps . forEach ( function ( app ) {
app . iconUrl = getIconUrlSync ( app ) ;
2016-04-22 23:46:45 -07:00
app . fqdn = app . altDomain || config . appFqdn ( app . location ) ;
2017-03-09 15:11:25 +01:00
app . cnameTarget = app . altDomain ? config . appFqdn ( app . location ) : null ;
2015-07-20 00:09:47 -07:00
} ) ;
callback ( null , apps ) ;
} ) ;
}
2016-02-25 11:28:29 +01:00
function getAllByUser ( user , callback ) {
assert . strictEqual ( typeof user , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
getAll ( function ( error , result ) {
if ( error ) return callback ( error ) ;
2017-02-16 20:11:09 -08:00
async . filter ( result , function ( app , iteratorDone ) {
hasAccessTo ( app , user , iteratorDone ) ;
} , callback ) ;
2016-02-25 11:28:29 +01:00
} ) ;
}
2016-06-04 01:07:43 -07:00
function downloadManifest ( appStoreId , manifest , callback ) {
2016-06-07 15:36:45 -07:00
if ( ! appStoreId && ! manifest ) return callback ( new AppsError ( AppsError . BAD _FIELD , 'Neither manifest nor appStoreId provided' ) ) ;
2016-06-04 01:07:43 -07:00
if ( ! appStoreId ) return callback ( null , '' , manifest ) ;
var parts = appStoreId . split ( '@' ) ;
var url = config . apiServerOrigin ( ) + '/api/v1/apps/' + parts [ 0 ] + ( parts [ 1 ] ? '/versions/' + parts [ 1 ] : '' ) ;
debug ( 'downloading manifest from %s' , url ) ;
2016-09-12 12:53:51 -07:00
superagent . get ( url ) . timeout ( 30 * 1000 ) . end ( function ( error , result ) {
2016-06-04 01:07:43 -07:00
if ( error && ! error . response ) return callback ( new AppsError ( AppsError . EXTERNAL _ERROR , 'Network error downloading manifest:' + error . message ) ) ;
2016-06-04 13:03:09 -07:00
if ( result . statusCode !== 200 ) return callback ( new AppsError ( AppsError . BAD _FIELD , util . format ( 'Failed to get app info from store.' , result . statusCode , result . text ) ) ) ;
2016-06-04 01:07:43 -07:00
callback ( null , parts [ 0 ] , result . body . manifest ) ;
} ) ;
}
2016-06-04 13:40:43 -07:00
function install ( data , auditSource , callback ) {
2016-06-03 23:22:38 -07:00
assert ( data && typeof data === 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2016-06-04 01:07:43 -07:00
var location = data . location . toLowerCase ( ) ,
2016-06-03 23:22:38 -07:00
portBindings = data . portBindings || null ,
accessRestriction = data . accessRestriction || null ,
icon = data . icon || null ,
cert = data . cert || null ,
key = data . key || null ,
memoryLimit = data . memoryLimit || 0 ,
2016-07-14 14:23:08 +02:00
altDomain = data . altDomain || null ,
2016-09-06 21:21:56 -07:00
xFrameOptions = data . xFrameOptions || 'SAMEORIGIN' ,
2017-01-19 11:20:24 -08:00
sso = 'sso' in data ? data . sso : null ,
2017-04-11 12:49:21 -07:00
debugMode = data . debugMode || null ,
backupId = data . backupId || null ;
2016-06-03 23:22:38 -07:00
2016-06-04 01:07:43 -07:00
assert ( data . appStoreId || data . manifest ) ; // atleast one of them is required
2015-07-20 00:09:47 -07:00
2016-06-04 01:07:43 -07:00
downloadManifest ( data . appStoreId , data . manifest , function ( error , appStoreId , manifest ) {
if ( error ) return callback ( error ) ;
2015-07-20 00:09:47 -07:00
2016-06-04 01:07:43 -07:00
error = manifestFormat . parse ( manifest ) ;
if ( error ) return callback ( new AppsError ( AppsError . BAD _FIELD , 'Manifest error: ' + error . message ) ) ;
2015-07-20 00:09:47 -07:00
2016-06-04 01:07:43 -07:00
error = checkManifestConstraints ( manifest ) ;
if ( error ) return callback ( error ) ;
2015-07-20 00:09:47 -07:00
2016-06-04 01:07:43 -07:00
error = validateHostname ( location , config . fqdn ( ) ) ;
if ( error ) return callback ( error ) ;
2015-07-20 00:09:47 -07:00
2016-06-04 01:07:43 -07:00
error = validatePortBindings ( portBindings , manifest . tcpPorts ) ;
if ( error ) return callback ( error ) ;
2015-07-20 00:09:47 -07:00
2016-06-04 01:07:43 -07:00
error = validateAccessRestriction ( accessRestriction ) ;
if ( error ) return callback ( error ) ;
2016-02-05 15:07:27 +01:00
2016-06-04 01:07:43 -07:00
error = validateMemoryLimit ( manifest , memoryLimit ) ;
if ( error ) return callback ( error ) ;
2016-02-11 18:14:16 +01:00
2016-07-14 14:23:08 +02:00
error = validateXFrameOptions ( xFrameOptions ) ;
if ( error ) return callback ( error ) ;
2017-01-20 05:48:25 -08:00
error = validateDebugMode ( debugMode ) ;
if ( error ) return callback ( error ) ;
2016-11-19 21:37:39 +05:30
if ( 'sso' in data && ! ( 'optionalSso' in manifest ) ) return callback ( new AppsError ( AppsError . BAD _FIELD , 'sso can only be specified for apps with optionalSso' ) ) ;
2016-12-09 18:43:26 -08:00
// if sso was unspecified, enable it by default if possible
2017-03-19 00:36:05 -07:00
if ( sso === null ) sso = ! ! manifest . addons [ 'ldap' ] || ! ! manifest . addons [ 'oauth' ] ;
2016-11-11 10:55:44 +05:30
2017-04-03 16:54:06 -07:00
if ( altDomain !== null && ! validator . isFQDN ( altDomain ) ) return callback ( new AppsError ( AppsError . BAD _FIELD , 'Invalid external domain' ) ) ;
2015-10-16 20:01:45 +02:00
2016-07-09 12:25:00 -07:00
var appId = uuid . v4 ( ) ;
2016-06-04 01:07:43 -07:00
if ( icon ) {
if ( ! validator . isBase64 ( icon ) ) return callback ( new AppsError ( AppsError . BAD _FIELD , 'icon is not base64' ) ) ;
2017-01-24 10:13:25 -08:00
if ( ! safe . fs . writeFileSync ( path . join ( paths . APP _ICONS _DIR , appId + '.png' ) , new Buffer ( icon , 'base64' ) ) ) {
2016-06-04 01:07:43 -07:00
return callback ( new AppsError ( AppsError . INTERNAL _ERROR , 'Error saving icon:' + safe . error . message ) ) ;
}
2015-07-20 00:09:47 -07:00
}
2016-06-04 01:07:43 -07:00
error = certificates . validateCertificate ( cert , key , config . appFqdn ( location ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . BAD _CERTIFICATE , error . message ) ) ;
2015-10-28 22:09:19 +01:00
2016-06-04 01:07:43 -07:00
debug ( 'Will install app with id : ' + appId ) ;
2015-07-20 00:09:47 -07:00
2017-04-13 00:42:44 -07:00
appstore . purchase ( appId , appStoreId , function ( error ) {
2017-04-13 01:11:20 -07:00
if ( error && error . reason === AppstoreError . NOT _FOUND ) return callback ( new AppsError ( AppsError . NOT _FOUND ) ) ;
if ( error && error . reason === AppstoreError . BILLING _REQUIRED ) return callback ( new AppsError ( AppsError . BILLING _REQUIRED , error . message ) ) ;
if ( error && error . reason === AppstoreError . EXTERNAL _ERROR ) return callback ( new AppsError ( AppsError . EXTERNAL _ERROR , error . message ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
2015-07-20 00:09:47 -07:00
2016-06-17 16:43:35 -05:00
var data = {
accessRestriction : accessRestriction ,
memoryLimit : memoryLimit ,
2016-07-14 14:23:08 +02:00
altDomain : altDomain ,
2016-09-06 21:21:56 -07:00
xFrameOptions : xFrameOptions ,
2017-01-19 11:20:24 -08:00
sso : sso ,
2017-02-08 23:50:26 +01:00
debugMode : debugMode ,
2017-04-11 12:49:21 -07:00
mailboxName : ( location ? location : manifest . title . toLowerCase ( ) . replace ( /[^a-zA-Z0-9]/g , '' ) ) + '.app' ,
lastBackupId : backupId
2016-06-17 16:43:35 -05:00
} ;
2017-02-08 23:50:26 +01:00
appdb . add ( appId , appStoreId , manifest , location , portBindings , data , function ( error ) {
if ( error && error . reason === DatabaseError . ALREADY _EXISTS ) return callback ( getDuplicateErrorDetails ( location , portBindings , error ) ) ;
2016-06-04 01:07:43 -07:00
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
2015-07-20 00:09:47 -07:00
2017-02-08 23:50:26 +01:00
// save cert to boxdata/certs
if ( cert && key ) {
if ( ! safe . fs . writeFileSync ( path . join ( paths . APP _CERTS _DIR , config . appFqdn ( location ) + '.user.cert' ) , cert ) ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , 'Error saving cert: ' + safe . error . message ) ) ;
if ( ! safe . fs . writeFileSync ( path . join ( paths . APP _CERTS _DIR , config . appFqdn ( location ) + '.user.key' ) , key ) ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , 'Error saving key: ' + safe . error . message ) ) ;
}
2015-07-20 00:09:47 -07:00
2017-02-08 23:50:26 +01:00
taskmanager . restartAppTask ( appId ) ;
2016-05-01 21:37:08 -07:00
2017-04-11 12:49:21 -07:00
eventlog . add ( eventlog . ACTION _APP _INSTALL , auditSource , { appId : appId , location : location , manifest : manifest , backupId : backupId } ) ;
2016-09-23 17:23:45 -07:00
2017-02-08 23:50:26 +01:00
callback ( null , { id : appId } ) ;
2016-06-04 01:07:43 -07:00
} ) ;
2015-07-20 00:09:47 -07:00
} ) ;
} ) ;
}
2016-06-04 16:32:27 -07:00
function configure ( appId , data , auditSource , callback ) {
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof appId , 'string' ) ;
2016-06-04 16:32:27 -07:00
assert ( data && typeof data === 'object' ) ;
2016-05-01 21:37:08 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2016-06-04 18:07:02 -07:00
appdb . get ( appId , function ( error , app ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . NOT _FOUND , 'No such app' ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
2015-07-20 00:09:47 -07:00
2016-06-04 18:07:02 -07:00
var location , portBindings , values = { } ;
if ( 'location' in data ) {
location = values . location = data . location . toLowerCase ( ) ;
error = validateHostname ( values . location , config . fqdn ( ) ) ;
if ( error ) return callback ( error ) ;
} else {
location = app . location ;
}
2015-07-20 00:09:47 -07:00
2016-06-04 18:07:02 -07:00
if ( 'accessRestriction' in data ) {
values . accessRestriction = data . accessRestriction ;
error = validateAccessRestriction ( values . accessRestriction ) ;
if ( error ) return callback ( error ) ;
}
2015-10-27 16:36:09 +01:00
2016-06-04 18:07:02 -07:00
if ( 'altDomain' in data ) {
values . altDomain = data . altDomain ;
2017-04-03 16:54:06 -07:00
if ( values . altDomain !== null && ! validator . isFQDN ( values . altDomain ) ) return callback ( new AppsError ( AppsError . BAD _FIELD , 'Invalid external domain' ) ) ;
2016-06-04 18:07:02 -07:00
}
2016-04-19 00:22:56 -07:00
2016-06-04 18:07:02 -07:00
if ( 'portBindings' in data ) {
portBindings = values . portBindings = data . portBindings ;
error = validatePortBindings ( values . portBindings , app . manifest . tcpPorts ) ;
if ( error ) return callback ( error ) ;
} else {
portBindings = app . portBindings ;
}
2015-07-20 00:09:47 -07:00
2016-06-04 18:07:02 -07:00
if ( 'memoryLimit' in data ) {
values . memoryLimit = data . memoryLimit ;
error = validateMemoryLimit ( app . manifest , values . memoryLimit ) ;
if ( error ) return callback ( error ) ;
}
2015-07-20 00:09:47 -07:00
2016-07-14 15:16:05 +02:00
if ( 'xFrameOptions' in data ) {
values . xFrameOptions = data . xFrameOptions ;
error = validateXFrameOptions ( values . xFrameOptions ) ;
if ( error ) return callback ( error ) ;
}
2017-01-20 05:48:25 -08:00
if ( 'debugMode' in data ) {
values . debugMode = data . debugMode ;
error = validateDebugMode ( values . debugMode ) ;
if ( error ) return callback ( error ) ;
2017-01-19 15:48:41 -08:00
}
2017-01-24 10:09:05 -08:00
// save cert to boxdata/certs. TODO: move this to apptask when we have a real task queue
2016-06-04 18:07:02 -07:00
if ( 'cert' in data && 'key' in data ) {
2016-06-04 18:30:05 -07:00
if ( data . cert && data . key ) {
error = certificates . validateCertificate ( data . cert , data . key , config . appFqdn ( location ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . BAD _CERTIFICATE , error . message ) ) ;
2016-09-12 01:21:51 -07:00
if ( ! safe . fs . writeFileSync ( path . join ( paths . APP _CERTS _DIR , config . appFqdn ( location ) + '.user.cert' ) , data . cert ) ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , 'Error saving cert: ' + safe . error . message ) ) ;
if ( ! safe . fs . writeFileSync ( path . join ( paths . APP _CERTS _DIR , config . appFqdn ( location ) + '.user.key' ) , data . key ) ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , 'Error saving key: ' + safe . error . message ) ) ;
2016-06-04 18:30:05 -07:00
} else { // remove existing cert/key
2016-09-12 01:21:51 -07:00
if ( ! safe . fs . unlinkSync ( path . join ( paths . APP _CERTS _DIR , config . appFqdn ( location ) + '.user.cert' ) ) ) debug ( 'Error removing cert: ' + safe . error . message ) ;
if ( ! safe . fs . unlinkSync ( path . join ( paths . APP _CERTS _DIR , config . appFqdn ( location ) + '.user.key' ) ) ) debug ( 'Error removing key: ' + safe . error . message ) ;
2016-06-04 18:30:05 -07:00
}
2015-10-28 12:24:59 +01:00
}
2016-06-13 13:48:53 -07:00
values . oldConfig = getAppConfig ( app ) ;
2015-07-20 00:09:47 -07:00
debug ( 'Will configure app with id:%s values:%j' , appId , values ) ;
2016-12-15 16:41:16 +01:00
var oldName = ( app . location ? app . location : app . manifest . title . toLowerCase ( ) . replace ( /[^a-zA-Z0-9]/g , '' ) ) + '.app' ;
var newName = ( location ? location : app . manifest . title . toLowerCase ( ) . replace ( /[^a-zA-Z0-9]/g , '' ) ) + '.app' ;
mailboxdb . updateName ( oldName , newName , function ( error ) {
if ( error && error . reason === DatabaseError . ALREADY _EXISTS ) return callback ( new AppsError ( AppsError . ALREADY _EXISTS , 'This mailbox is already taken' ) ) ;
2015-07-20 00:09:47 -07:00
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . BAD _STATE ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
2016-12-15 16:41:16 +01:00
appdb . setInstallationCommand ( appId , appdb . ISTATE _PENDING _CONFIGURE , values , function ( error ) {
if ( error && error . reason === DatabaseError . ALREADY _EXISTS ) return callback ( getDuplicateErrorDetails ( location , portBindings , error ) ) ;
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . BAD _STATE ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
2015-07-20 00:09:47 -07:00
2016-12-15 16:41:16 +01:00
taskmanager . restartAppTask ( appId ) ;
2016-05-01 21:37:08 -07:00
2016-12-15 16:41:16 +01:00
eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId : appId } ) ;
callback ( null ) ;
} ) ;
2015-07-20 00:09:47 -07:00
} ) ;
} ) ;
}
2016-06-04 19:06:16 -07:00
function update ( appId , data , auditSource , callback ) {
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof appId , 'string' ) ;
2016-06-04 19:06:16 -07:00
assert ( data && typeof data === 'object' ) ;
2016-05-01 21:37:08 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
debug ( 'Will update app with id:%s' , appId ) ;
2016-06-04 19:19:00 -07:00
downloadManifest ( data . appStoreId , data . manifest , function ( error , appStoreId , manifest ) {
if ( error ) return callback ( error ) ;
2016-06-04 19:06:16 -07:00
2016-06-04 19:19:00 -07:00
var values = { } ;
2015-07-20 00:09:47 -07:00
2016-06-04 19:19:00 -07:00
error = manifestFormat . parse ( manifest ) ;
if ( error ) return callback ( new AppsError ( AppsError . BAD _FIELD , 'Manifest error:' + error . message ) ) ;
2015-07-20 00:09:47 -07:00
2016-06-04 19:19:00 -07:00
error = checkManifestConstraints ( manifest ) ;
if ( error ) return callback ( error ) ;
2015-07-20 00:09:47 -07:00
2016-06-04 19:19:00 -07:00
values . manifest = manifest ;
2015-07-20 00:09:47 -07:00
2016-06-04 19:19:00 -07:00
if ( 'portBindings' in data ) {
values . portBindings = data . portBindings ;
error = validatePortBindings ( data . portBindings , values . manifest . tcpPorts ) ;
if ( error ) return callback ( error ) ;
2015-07-20 00:09:47 -07:00
}
2016-06-04 19:19:00 -07:00
if ( 'icon' in data ) {
if ( data . icon ) {
if ( ! validator . isBase64 ( data . icon ) ) return callback ( new AppsError ( AppsError . BAD _FIELD , 'icon is not base64' ) ) ;
2016-03-25 11:35:47 -07:00
2017-01-24 10:13:25 -08:00
if ( ! safe . fs . writeFileSync ( path . join ( paths . APP _ICONS _DIR , appId + '.png' ) , new Buffer ( data . icon , 'base64' ) ) ) {
2016-06-04 19:19:00 -07:00
return callback ( new AppsError ( AppsError . INTERNAL _ERROR , 'Error saving icon:' + safe . error . message ) ) ;
}
} else {
2017-01-24 10:13:25 -08:00
safe . fs . unlinkSync ( path . join ( paths . APP _ICONS _DIR , appId + '.png' ) ) ;
2016-06-04 19:19:00 -07:00
}
2016-03-25 11:35:47 -07:00
}
2016-06-04 19:19:00 -07:00
appdb . get ( appId , function ( error , app ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . NOT _FOUND , 'No such app' ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
// prevent user from installing a app with different manifest id over an existing app
// this allows cloudron install -f --app <appid> for an app installed from the appStore
if ( app . manifest . id !== values . manifest . id ) {
if ( ! data . force ) return callback ( new AppsError ( AppsError . BAD _FIELD , 'manifest id does not match. force to override' ) ) ;
2017-01-19 15:48:41 -08:00
// clear appStoreId so that this app does not get updates anymore
2016-06-04 19:19:00 -07:00
values . appStoreId = '' ;
}
2016-02-11 18:14:16 +01:00
2017-01-20 05:48:25 -08:00
// do not update apps in debug mode
if ( app . debugMode && ! data . force ) return callback ( new AppsError ( AppsError . BAD _STATE , 'debug mode enabled. force to override' ) ) ;
2017-01-19 11:20:24 -08:00
2016-06-04 19:19:00 -07:00
// Ensure we update the memory limit in case the new app requires more memory as a minimum
2017-01-19 15:02:12 -08:00
// 0 and -1 are special values for memory limit indicating unset and unlimited
if ( app . memoryLimit > 0 && values . manifest . memoryLimit && app . memoryLimit < values . manifest . memoryLimit ) {
2016-06-04 19:19:00 -07:00
values . memoryLimit = values . manifest . memoryLimit ;
}
2016-02-14 12:10:22 +01:00
2016-06-13 13:48:53 -07:00
values . oldConfig = getAppConfig ( app ) ;
2015-07-20 00:09:47 -07:00
2016-06-04 19:19:00 -07:00
appdb . setInstallationCommand ( appId , data . force ? appdb . ISTATE _PENDING _FORCE _UPDATE : appdb . ISTATE _PENDING _UPDATE , values , function ( error ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . BAD _STATE ) ) ; // might be a bad guess
if ( error && error . reason === DatabaseError . ALREADY _EXISTS ) return callback ( getDuplicateErrorDetails ( '' /* location cannot conflict */ , values . portBindings , error ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
2015-07-20 00:09:47 -07:00
2016-06-04 19:19:00 -07:00
taskmanager . restartAppTask ( appId ) ;
2015-07-20 00:09:47 -07:00
2016-06-18 13:24:27 -05:00
eventlog . add ( eventlog . ACTION _APP _UPDATE , auditSource , { appId : appId , toManifest : manifest , fromManifest : app . manifest , force : data . force } ) ;
2016-05-01 21:37:08 -07:00
2016-11-07 15:14:06 +01:00
// clear update indicator, if update fails, it will come back through the update checker
updateChecker . resetAppUpdateInfo ( appId ) ;
2016-06-04 19:19:00 -07:00
callback ( null ) ;
} ) ;
2015-07-20 00:09:47 -07:00
} ) ;
} ) ;
}
2015-11-02 11:20:50 -08:00
function appLogFilter ( app ) {
2015-11-02 14:23:02 -08:00
var names = [ app . id ] . concat ( addons . getContainerNamesSync ( app , app . manifest . addons ) ) ;
2015-11-02 11:20:50 -08:00
return names . map ( function ( name ) { return 'CONTAINER_NAME=' + name ; } ) ;
}
function getLogs ( appId , lines , follow , callback ) {
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof appId , 'string' ) ;
2015-11-02 11:20:50 -08:00
assert . strictEqual ( typeof lines , 'number' ) ;
assert . strictEqual ( typeof follow , 'boolean' ) ;
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
debug ( 'Getting logs for %s' , appId ) ;
2015-11-02 11:20:50 -08:00
2015-07-20 00:09:47 -07:00
appdb . get ( appId , function ( error , app ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . NOT _FOUND ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
2015-11-02 11:20:50 -08:00
var args = [ '--output=json' , '--no-pager' , '--lines=' + lines ] ;
if ( follow ) args . push ( '--follow' ) ;
args = args . concat ( appLogFilter ( app ) ) ;
2015-07-20 00:09:47 -07:00
2015-11-02 11:20:50 -08:00
var cp = spawn ( '/bin/journalctl' , args ) ;
2015-07-20 00:09:47 -07:00
2015-11-02 11:20:50 -08:00
var transformStream = split ( function mapper ( line ) {
var obj = safe . JSON . parse ( line ) ;
if ( ! obj ) return undefined ;
2015-07-20 00:09:47 -07:00
2015-11-02 11:20:50 -08:00
var source = obj . CONTAINER _NAME . slice ( app . id . length + 1 ) ;
2015-11-02 14:26:15 -08:00
return JSON . stringify ( {
realtimeTimestamp : obj . _ _REALTIME _TIMESTAMP ,
monotonicTimestamp : obj . _ _MONOTONIC _TIMESTAMP ,
message : obj . MESSAGE ,
source : source || 'main'
2015-11-10 11:31:05 +01:00
} ) + '\n' ;
2015-11-02 11:20:50 -08:00
} ) ;
2015-07-20 00:09:47 -07:00
2015-11-02 11:20:50 -08:00
transformStream . close = cp . kill . bind ( cp , 'SIGKILL' ) ; // closing stream kills the child process
2015-07-20 00:09:47 -07:00
2015-11-02 11:20:50 -08:00
cp . stdout . pipe ( transformStream ) ;
2015-07-20 00:09:47 -07:00
2015-11-02 11:20:50 -08:00
return callback ( null , transformStream ) ;
2015-07-20 00:09:47 -07:00
} ) ;
}
2016-06-13 10:08:58 -07:00
function restore ( appId , data , auditSource , callback ) {
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof appId , 'string' ) ;
2016-06-13 10:08:58 -07:00
assert . strictEqual ( typeof data , 'object' ) ;
2016-05-01 21:37:08 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
debug ( 'Will restore app with id:%s' , appId ) ;
appdb . get ( appId , function ( error , app ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . NOT _FOUND ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
2016-06-13 13:44:49 -07:00
// for empty or null backupId, use existing manifest to mimic a reinstall
2016-06-13 18:04:22 -07:00
var func = data . backupId ? backups . getRestoreConfig . bind ( null , data . backupId ) : function ( next ) { return next ( null , { manifest : app . manifest } ) ; } ;
2016-06-13 13:44:49 -07:00
func ( function ( error , restoreConfig ) {
2016-10-10 13:21:45 +02:00
if ( error && error . reason === BackupsError . NOT _FOUND ) return callback ( new AppsError ( AppsError . EXTERNAL _ERROR , error . message ) ) ;
2016-06-13 18:11:11 -07:00
if ( error && error . reason === BackupsError . EXTERNAL _ERROR ) return callback ( new AppsError ( AppsError . EXTERNAL _ERROR , error . message ) ) ;
2016-06-13 13:44:49 -07:00
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
2016-06-13 18:11:11 -07:00
2016-06-13 13:44:49 -07:00
if ( ! restoreConfig ) callback ( new AppsError ( AppsError . EXTERNAL _ERROR , 'Could not get restore config' ) ) ;
2015-08-19 10:54:39 -07:00
2016-06-13 13:44:49 -07:00
// re-validate because this new box version may not accept old configs
error = checkManifestConstraints ( restoreConfig . manifest ) ;
2015-08-19 10:54:39 -07:00
if ( error ) return callback ( error ) ;
2016-06-13 13:44:49 -07:00
var values = {
lastBackupId : data . backupId || null , // when null, apptask simply reinstalls
2015-08-19 10:54:39 -07:00
manifest : restoreConfig . manifest ,
2016-06-13 13:48:53 -07:00
oldConfig : getAppConfig ( app )
2015-08-19 10:54:39 -07:00
} ;
2015-07-20 00:09:47 -07:00
2016-06-13 13:44:49 -07:00
appdb . setInstallationCommand ( appId , appdb . ISTATE _PENDING _RESTORE , values , function ( error ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . BAD _STATE ) ) ; // might be a bad guess
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
2015-07-20 00:09:47 -07:00
2016-06-13 13:44:49 -07:00
taskmanager . restartAppTask ( appId ) ;
2015-07-20 00:09:47 -07:00
2016-06-13 13:44:49 -07:00
eventlog . add ( eventlog . ACTION _APP _RESTORE , auditSource , { appId : appId } ) ;
2016-05-01 21:37:08 -07:00
2016-06-13 13:44:49 -07:00
callback ( null ) ;
} ) ;
2015-07-20 00:09:47 -07:00
} ) ;
} ) ;
}
2016-06-17 17:12:55 -05:00
function clone ( appId , data , auditSource , callback ) {
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof data , 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
debug ( 'Will clone app with id:%s' , appId ) ;
var location = data . location . toLowerCase ( ) ,
portBindings = data . portBindings || null ,
backupId = data . backupId ;
assert . strictEqual ( typeof backupId , 'string' ) ;
assert . strictEqual ( typeof location , 'string' ) ;
assert . strictEqual ( typeof portBindings , 'object' ) ;
appdb . get ( appId , function ( error , app ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . NOT _FOUND ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
backups . getRestoreConfig ( backupId , function ( error , restoreConfig ) {
if ( error && error . reason === BackupsError . EXTERNAL _ERROR ) return callback ( new AppsError ( AppsError . EXTERNAL _ERROR , error . message ) ) ;
2017-04-07 15:29:42 -07:00
if ( error && error . reason === BackupsError . NOT _FOUND ) return callback ( new AppsError ( AppsError . EXTERNAL _ERROR , error . message ) ) ;
2016-06-17 17:12:55 -05:00
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
if ( ! restoreConfig ) callback ( new AppsError ( AppsError . EXTERNAL _ERROR , 'Could not get restore config' ) ) ;
// re-validate because this new box version may not accept old configs
error = checkManifestConstraints ( restoreConfig . manifest ) ;
if ( error ) return callback ( error ) ;
error = validateHostname ( location , config . fqdn ( ) ) ;
if ( error ) return callback ( error ) ;
error = validatePortBindings ( portBindings , restoreConfig . manifest . tcpPorts ) ;
if ( error ) return callback ( error ) ;
var newAppId = uuid . v4 ( ) , appStoreId = app . appStoreId , manifest = restoreConfig . manifest ;
2017-04-13 00:42:44 -07:00
appstore . purchase ( newAppId , appStoreId , function ( error ) {
2017-04-13 01:11:20 -07:00
if ( error && error . reason === AppstoreError . NOT _FOUND ) return callback ( new AppsError ( AppsError . NOT _FOUND ) ) ;
if ( error && error . reason === AppstoreError . BILLING _REQUIRED ) return callback ( new AppsError ( AppsError . BILLING _REQUIRED , error . message ) ) ;
if ( error && error . reason === AppstoreError . EXTERNAL _ERROR ) return callback ( new AppsError ( AppsError . EXTERNAL _ERROR , error . message ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
2016-06-17 17:12:55 -05:00
var data = {
installationState : appdb . ISTATE _PENDING _CLONE ,
memoryLimit : app . memoryLimit ,
2016-06-17 17:53:27 -05:00
accessRestriction : app . accessRestriction ,
2016-07-14 15:16:05 +02:00
xFrameOptions : app . xFrameOptions ,
2016-11-28 12:45:32 +01:00
lastBackupId : backupId ,
2017-02-08 23:50:26 +01:00
sso : ! ! app . sso ,
2017-02-13 15:15:07 -08:00
mailboxName : ( location ? location : manifest . title . toLowerCase ( ) . replace ( /[^a-zA-Z0-9]/g , '' ) ) + '.app'
2016-06-17 17:12:55 -05:00
} ;
2017-02-08 23:50:26 +01:00
appdb . add ( newAppId , appStoreId , manifest , location , portBindings , data , function ( error ) {
if ( error && error . reason === DatabaseError . ALREADY _EXISTS ) return callback ( getDuplicateErrorDetails ( location , portBindings , error ) ) ;
2016-06-17 17:12:55 -05:00
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
2017-02-08 23:50:26 +01:00
taskmanager . restartAppTask ( newAppId ) ;
2016-06-17 17:12:55 -05:00
2017-02-08 23:50:26 +01:00
eventlog . add ( eventlog . ACTION _APP _CLONE , auditSource , { appId : newAppId , oldAppId : appId , backupId : backupId , location : location , manifest : manifest } ) ;
2016-06-17 17:12:55 -05:00
2017-02-08 23:50:26 +01:00
callback ( null , { id : newAppId } ) ;
2016-06-17 17:12:55 -05:00
} ) ;
} ) ;
} ) ;
} ) ;
}
2016-05-01 21:37:08 -07:00
function uninstall ( appId , auditSource , callback ) {
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof appId , 'string' ) ;
2016-05-01 21:37:08 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
debug ( 'Will uninstall app with id:%s' , appId ) ;
2016-08-04 15:53:55 +02:00
get ( appId , function ( error , result ) {
2016-08-04 09:38:00 +02:00
if ( error ) return callback ( error ) ;
2015-07-20 00:09:47 -07:00
2017-04-13 00:42:44 -07:00
appstore . unpurchase ( appId , result . appStoreId , function ( error ) {
2017-04-13 01:11:20 -07:00
if ( error && error . reason === AppstoreError . NOT _FOUND ) return callback ( new AppsError ( AppsError . NOT _FOUND ) ) ;
if ( error && error . reason === AppstoreError . BILLING _REQUIRED ) return callback ( new AppsError ( AppsError . BILLING _REQUIRED , error . message ) ) ;
if ( error && error . reason === AppstoreError . EXTERNAL _ERROR ) return callback ( new AppsError ( AppsError . EXTERNAL _ERROR , error . message ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
2016-08-04 15:53:55 +02:00
2017-02-13 15:19:17 -08:00
taskmanager . stopAppTask ( appId , function ( ) {
appdb . setInstallationCommand ( appId , appdb . ISTATE _PENDING _UNINSTALL , function ( error ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . NOT _FOUND , 'No such app' ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
2016-08-04 09:38:00 +02:00
2017-02-13 15:19:17 -08:00
eventlog . add ( eventlog . ACTION _APP _UNINSTALL , auditSource , { appId : appId } ) ;
2016-05-01 21:37:08 -07:00
2017-02-13 15:19:17 -08:00
taskmanager . startAppTask ( appId , callback ) ;
2016-08-04 15:53:55 +02:00
} ) ;
2016-08-04 09:38:00 +02:00
} ) ;
2016-02-09 11:41:59 -08:00
} ) ;
2015-07-20 00:09:47 -07:00
} ) ;
}
function start ( appId , callback ) {
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
debug ( 'Will start app with id:%s' , appId ) ;
appdb . setRunCommand ( appId , appdb . RSTATE _PENDING _START , function ( error ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . BAD _STATE ) ) ; // might be a bad guess
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
taskmanager . restartAppTask ( appId ) ;
callback ( null ) ;
} ) ;
}
function stop ( appId , callback ) {
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
debug ( 'Will stop app with id:%s' , appId ) ;
appdb . setRunCommand ( appId , appdb . RSTATE _PENDING _STOP , function ( error ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . BAD _STATE ) ) ; // might be a bad guess
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
taskmanager . restartAppTask ( appId ) ;
callback ( null ) ;
} ) ;
}
function checkManifestConstraints ( manifest ) {
2016-06-13 18:02:57 -07:00
assert ( manifest && typeof manifest === 'object' ) ;
2016-06-03 22:19:09 -07:00
if ( ! manifest . dockerImage ) return new AppsError ( AppsError . BAD _FIELD , 'Missing dockerImage' ) ; // dockerImage is optional in manifest
2015-08-19 11:08:45 -07:00
2015-07-20 00:09:47 -07:00
if ( semver . valid ( manifest . maxBoxVersion ) && semver . gt ( config . version ( ) , manifest . maxBoxVersion ) ) {
2016-06-03 22:19:09 -07:00
return new AppsError ( AppsError . BAD _FIELD , 'Box version exceeds Apps maxBoxVersion' ) ;
2015-07-20 00:09:47 -07:00
}
if ( semver . valid ( manifest . minBoxVersion ) && semver . gt ( manifest . minBoxVersion , config . version ( ) ) ) {
2016-06-03 22:19:09 -07:00
return new AppsError ( AppsError . BAD _FIELD , 'minBoxVersion exceeds Box version' ) ;
2015-07-20 00:09:47 -07:00
}
return null ;
}
function exec ( appId , options , callback ) {
assert . strictEqual ( typeof appId , 'string' ) ;
assert ( options && typeof options === 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
var cmd = options . cmd || [ '/bin/bash' ] ;
assert ( util . isArray ( cmd ) && cmd . length > 0 ) ;
appdb . get ( appId , function ( error , app ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . NOT _FOUND , 'No such app' ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
2016-02-12 12:32:51 -08:00
if ( app . installationState !== appdb . ISTATE _INSTALLED || app . runState !== appdb . RSTATE _RUNNING ) {
return callback ( new AppsError ( AppsError . BAD _STATE , 'App not installed or running' ) ) ;
}
2016-01-15 15:15:24 -08:00
var container = docker . connection . getContainer ( app . containerId ) ;
2016-05-22 00:13:56 -07:00
var execOptions = {
2015-07-20 00:09:47 -07:00
AttachStdin : true ,
AttachStdout : true ,
2016-01-18 21:36:05 -08:00
AttachStderr : true ,
2016-05-22 00:13:56 -07:00
// A pseudo tty is a terminal which processes can detect (for example, disable colored output)
// Creating a pseudo terminal also assigns a terminal driver which detects control sequences
// When passing binary data, tty must be disabled. In addition, the stdout/stderr becomes a single
// unified stream because of the nature of a tty (see https://github.com/docker/docker/issues/19696)
2016-01-18 11:16:06 -08:00
Tty : options . tty ,
2016-01-15 15:15:24 -08:00
Cmd : cmd
2015-07-20 00:09:47 -07:00
} ;
2016-01-15 15:15:24 -08:00
container . exec ( execOptions , function ( error , exec ) {
2015-07-20 00:09:47 -07:00
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
2016-01-15 15:15:24 -08:00
var startOptions = {
Detach : false ,
2016-01-18 11:16:06 -08:00
Tty : options . tty ,
2016-05-22 00:13:56 -07:00
// hijacking upgrades the docker connection from http to tcp. because of this upgrade,
// we can work with half-close connections (not defined in http). this way, the client
// can properly signal that stdin is EOF by closing it's side of the socket. In http,
// the whole connection will be dropped when stdin get EOF.
// https://github.com/apocas/dockerode/commit/b4ae8a03707fad5de893f302e4972c1e758592fe
hijack : true ,
stream : true ,
stdin : true ,
stdout : true ,
stderr : true
2016-01-15 15:15:24 -08:00
} ;
2016-05-22 00:13:56 -07:00
exec . start ( startOptions , function ( error , stream /* in hijacked mode, this is a net.socket */ ) {
2015-07-20 00:09:47 -07:00
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
2016-01-15 15:15:24 -08:00
if ( options . rows && options . columns ) {
exec . resize ( { h : options . rows , w : options . columns } , function ( error ) { if ( error ) debug ( 'Error resizing console' , error ) ; } ) ;
}
2015-11-10 21:56:17 -08:00
2016-01-15 15:15:24 -08:00
return callback ( null , stream ) ;
2015-07-20 00:09:47 -07:00
} ) ;
} ) ;
} ) ;
}
2016-06-02 18:49:56 -07:00
function updateApps ( updateInfo , auditSource , callback ) { // updateInfo is { appId -> { manifest } }
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof updateInfo , 'object' ) ;
2016-06-02 18:49:56 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
function canAutoupdateApp ( app , newManifest ) {
2016-06-18 14:51:49 -05:00
var newTcpPorts = newManifest . tcpPorts || { } ;
2016-06-08 08:44:58 -07:00
var oldTcpPorts = app . manifest . tcpPorts || { } ;
2015-09-10 11:39:03 -07:00
var portBindings = app . portBindings ; // this is never null
2015-07-20 00:09:47 -07:00
2016-06-08 08:44:58 -07:00
for ( var env in newTcpPorts ) {
if ( ! ( env in oldTcpPorts ) ) return new Error ( env + ' is required from user' ) ;
}
2015-07-20 00:09:47 -07:00
2016-06-08 08:44:58 -07:00
for ( env in portBindings ) {
if ( ! ( env in newTcpPorts ) ) return new Error ( env + ' was in use but new update removes it' ) ;
2015-09-10 11:39:03 -07:00
}
2016-06-08 08:44:58 -07:00
// it's fine if one or more (unused) keys got removed
2015-09-10 11:39:03 -07:00
return null ;
2015-07-20 00:09:47 -07:00
}
if ( ! updateInfo ) return callback ( null ) ;
async . eachSeries ( Object . keys ( updateInfo ) , function iterator ( appId , iteratorDone ) {
get ( appId , function ( error , app ) {
2015-09-10 11:39:03 -07:00
if ( error ) {
debug ( 'Cannot autoupdate app %s : %s' , appId , error . message ) ;
return iteratorDone ( ) ;
}
error = canAutoupdateApp ( app , updateInfo [ appId ] . manifest ) ;
if ( error ) {
debug ( 'app %s requires manual update. %s' , appId , error . message ) ;
2015-07-20 00:09:47 -07:00
return iteratorDone ( ) ;
}
2016-06-04 19:06:16 -07:00
var data = {
2016-06-18 13:24:27 -05:00
manifest : updateInfo [ appId ] . manifest ,
force : false
2016-06-04 19:06:16 -07:00
} ;
update ( appId , data , auditSource , function ( error ) {
2015-09-10 11:39:03 -07:00
if ( error ) debug ( 'Error initiating autoupdate of %s. %s' , appId , error . message ) ;
2015-07-20 00:09:47 -07:00
iteratorDone ( null ) ;
} ) ;
} ) ;
} , callback ) ;
}
function backup ( appId , callback ) {
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2016-01-17 16:05:47 +01:00
appdb . exists ( appId , function ( error , exists ) {
2015-07-20 00:09:47 -07:00
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
2016-01-17 16:05:47 +01:00
if ( ! exists ) return callback ( new AppsError ( AppsError . NOT _FOUND ) ) ;
2015-07-20 00:09:47 -07:00
appdb . setInstallationCommand ( appId , appdb . ISTATE _PENDING _BACKUP , function ( error ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . BAD _STATE ) ) ; // might be a bad guess
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
taskmanager . restartAppTask ( appId ) ;
callback ( null ) ;
} ) ;
} ) ;
}
2016-01-19 13:35:18 +01:00
2016-03-08 08:57:28 -08:00
function listBackups ( page , perPage , appId , callback ) {
assert ( typeof page === 'number' && page > 0 ) ;
assert ( typeof perPage === 'number' && perPage > 0 ) ;
2016-01-19 13:35:18 +01:00
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
appdb . exists ( appId , function ( error , exists ) {
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
if ( ! exists ) return callback ( new AppsError ( AppsError . NOT _FOUND ) ) ;
2016-03-08 08:57:28 -08:00
backups . getByAppIdPaged ( page , perPage , appId , function ( error , results ) {
2016-01-19 13:35:18 +01:00
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
2016-03-08 08:57:28 -08:00
callback ( null , results ) ;
2016-01-19 13:35:18 +01:00
} ) ;
} ) ;
}
2016-05-24 10:33:10 -07:00
function restoreInstalledApps ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
appdb . getAll ( function ( error , apps ) {
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
async . map ( apps , function ( app , iteratorDone ) {
debug ( 'marking %s for restore' , app . location || app . id ) ;
2016-06-16 06:38:47 -07:00
appdb . setInstallationCommand ( app . id , appdb . ISTATE _PENDING _RESTORE , { oldConfig : null } , function ( error ) {
if ( error ) debug ( 'did not mark %s for restore' , app . location || app . id , error ) ;
iteratorDone ( ) ; // always succeed
} ) ;
2016-05-24 10:33:10 -07:00
} , callback ) ;
} ) ;
}
function configureInstalledApps ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
appdb . getAll ( function ( error , apps ) {
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
async . map ( apps , function ( app , iteratorDone ) {
debug ( 'marking %s for reconfigure' , app . location || app . id ) ;
2016-06-16 06:38:47 -07:00
appdb . setInstallationCommand ( app . id , appdb . ISTATE _PENDING _CONFIGURE , { oldConfig : null } , function ( error ) {
if ( error ) debug ( 'did not mark %s for reconfigure' , app . location || app . id , error ) ;
iteratorDone ( ) ; // always succeed
} ) ;
2016-05-24 10:33:10 -07:00
} , callback ) ;
} ) ;
}