diff --git a/src/addons.js b/src/addons.js index 32232759f..bf18455ef 100644 --- a/src/addons.js +++ b/src/addons.js @@ -248,7 +248,7 @@ function setupOauth(app, options, callback) { if (!app.sso) return callback(null); var appId = app.id; - var redirectURI = 'https://' + (app.altDomain || config.appFqdn(app.location)); + var redirectURI = 'https://' + (app.altDomain || config.appFqdn(app)); var scope = 'profile'; clients.delByAppIdAndType(appId, clients.TYPE_OAUTH, function (error) { // remove existing creds diff --git a/src/apps.js b/src/apps.js index c4173fe84..2130c314c 100644 --- a/src/apps.js +++ b/src/apps.js @@ -119,10 +119,10 @@ AppsError.BAD_CERTIFICATE = 'Invalid certificate'; // 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(hostname) { - assert.strictEqual(typeof hostname, 'string'); +function validateHostname(location) { + assert.strictEqual(typeof location, 'string'); - if (!hostname) return new AppsError(AppsError.BAD_FIELD, 'location cannot be empty'); + if (!location) return new AppsError(AppsError.BAD_FIELD, 'location cannot be empty'); const RESERVED_LOCATIONS = [ config.adminFqdn(), @@ -135,12 +135,12 @@ function validateHostname(hostname) { if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new AppsError(AppsError.BAD_FIELD, location + ' is reserved'); // workaround https://github.com/oncletom/tld.js/issues/73 - var tmp = hostname.replace('_', '-'); + var tmp = location.replace('_', '-'); if (!tld.isValid(tmp)) return new AppsError(AppsError.BAD_FIELD, 'location is not a valid domain name'); // TODO the real limitation is 253 but our db only has VARCHAR(128) here - if (hostname.length > 128) return new AppsError(AppsError.BAD_FIELD, 'location length exceeds 128 characters'); - // if (hostname.length > 253) return new AppsError(AppsError.BAD_FIELD, 'FQDN length exceeds 253 characters'); + if (location.length > 128) return new AppsError(AppsError.BAD_FIELD, 'location length exceeds 128 characters'); + // if (location.length > 253) return new AppsError(AppsError.BAD_FIELD, 'FQDN length exceeds 253 characters'); return null; } @@ -351,8 +351,8 @@ function get(appId, callback) { if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); app.iconUrl = getIconUrlSync(app); - app.fqdn = app.altDomain || config.appFqdn(app.location); - app.cnameTarget = app.altDomain ? config.appFqdn(app.location) : null; + app.fqdn = app.altDomain || config.appFqdn(app); + app.cnameTarget = app.altDomain ? config.appFqdn(app) : null; callback(null, app); }); @@ -370,8 +370,8 @@ function getByIpAddress(ip, callback) { if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); app.iconUrl = getIconUrlSync(app); - app.fqdn = app.altDomain || config.appFqdn(app.location); - app.cnameTarget = app.altDomain ? config.appFqdn(app.location) : null; + app.fqdn = app.altDomain || config.appFqdn(app); + app.cnameTarget = app.altDomain ? config.appFqdn(app) : null; callback(null, app); }); @@ -386,8 +386,8 @@ function getAll(callback) { apps.forEach(function (app) { app.iconUrl = getIconUrlSync(app); - app.fqdn = app.altDomain || config.appFqdn(app.location); - app.cnameTarget = app.altDomain ? config.appFqdn(app.location) : null; + app.fqdn = app.altDomain || config.appFqdn(app); + app.cnameTarget = app.altDomain ? config.appFqdn(app) : null; }); callback(null, apps); @@ -433,6 +433,7 @@ function install(data, auditSource, callback) { assert.strictEqual(typeof callback, 'function'); var location = data.location.toLowerCase(), + domain = data.domain.toLowerCase(), portBindings = data.portBindings || null, accessRestriction = data.accessRestriction || null, icon = data.icon || null, @@ -459,7 +460,7 @@ function install(data, auditSource, callback) { error = checkManifestConstraints(manifest); if (error) return callback(error); - error = validateHostname(location); + error = validateHostname(config.appFqdn({ domain: domain, location: location })); if (error) return callback(error); error = validatePortBindings(portBindings, manifest.tcpPorts); @@ -499,7 +500,7 @@ function install(data, auditSource, callback) { } } - error = certificates.validateCertificate(cert, key, config.appFqdn(location)); + error = certificates.validateCertificate(cert, key, config.appFqdn({ domain: domain, location: location })); if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message)); debug('Will install app with id : ' + appId); @@ -523,19 +524,19 @@ function install(data, auditSource, callback) { robotsTxt: robotsTxt }; - appdb.add(appId, appStoreId, manifest, location, portBindings, data, function (error) { + appdb.add(appId, appStoreId, manifest, location, domain, portBindings, data, function (error) { if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error)); if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); // 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)); + if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn({ domain: domain, location: 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({ domain: domain, location: location }) + '.user.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message)); } taskmanager.restartAppTask(appId); - eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, location: location, manifest: manifest, backupId: backupId }); + eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, location: location, domain: domain, manifest: manifest, backupId: backupId }); callback(null, { id : appId }); }); @@ -553,14 +554,15 @@ function configure(appId, data, auditSource, callback) { 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)); - var location, portBindings, values = { }; - if ('location' in data) { - location = values.location = data.location.toLowerCase(); - error = validateHostname(values.location); - if (error) return callback(error); - } else { - location = app.location; - } + var domain, location, portBindings, values = { }; + if ('location' in data) location = values.location = data.location.toLowerCase(); + else location = app.location; + + if ('domain' in data) domain = values.domain = data.domain.toLowerCase(); + else domain = app.domain; + + error = validateHostname(config.appFqdn({ domain: domain, location: location })); + if (error) return callback(error); if ('accessRestriction' in data) { values.accessRestriction = data.accessRestriction; @@ -608,14 +610,14 @@ function configure(appId, data, auditSource, callback) { // save cert to boxdata/certs. TODO: move this to apptask when we have a real task queue if ('cert' in data && 'key' in data) { if (data.cert && data.key) { - error = certificates.validateCertificate(data.cert, data.key, config.appFqdn(location)); + error = certificates.validateCertificate(data.cert, data.key, config.appFqdn({ domain: domain, location: location })); if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message)); - 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)); + if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn({ domain: domain, location: 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({ domain: domain, location: location }) + '.user.key'), data.key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message)); } else { // remove existing cert/key - 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); + if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, config.appFqdn({ domain: domain, location: location }) + '.user.cert'))) debug('Error removing cert: ' + safe.error.message); + if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, config.appFqdn({ domain: domain, location: location }) + '.user.key'))) debug('Error removing key: ' + safe.error.message); } } @@ -826,11 +828,13 @@ function clone(appId, data, auditSource, callback) { debug('Will clone app with id:%s', appId); var location = data.location.toLowerCase(), + domain = data.domain.toLowerCase(), portBindings = data.portBindings || null, backupId = data.backupId; assert.strictEqual(typeof backupId, 'string'); assert.strictEqual(typeof location, 'string'); + assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof portBindings, 'object'); appdb.get(appId, function (error, app) { @@ -848,7 +852,7 @@ function clone(appId, data, auditSource, callback) { error = checkManifestConstraints(backupInfo.manifest); if (error) return callback(error); - error = validateHostname(location); + error = validateHostname(config.appFqdn({ domain: domain, location: location })); if (error) return callback(error); error = validatePortBindings(portBindings, backupInfo.manifest.tcpPorts); @@ -856,7 +860,7 @@ function clone(appId, data, auditSource, callback) { var newAppId = uuid.v4(), appStoreId = app.appStoreId, manifest = backupInfo.manifest; - appstore.purchase(newAppId, appStoreId, function (error) { + appstore.purchase(newAppId, app.appStoreId, function (error) { 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)); @@ -872,7 +876,7 @@ function clone(appId, data, auditSource, callback) { mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app' }; - appdb.add(newAppId, appStoreId, manifest, location, portBindings, data, function (error) { + appdb.add(newAppId, app.appStoreId, manifest, location, domain, portBindings, data, function (error) { if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error)); if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); diff --git a/src/backups.js b/src/backups.js index f49192e35..7cf8dab1d 100644 --- a/src/backups.js +++ b/src/backups.js @@ -68,7 +68,7 @@ var BACKUPTASK_CMD = path.join(__dirname, 'backuptask.js'); function debugApp(app) { assert(!app || typeof app === 'object'); - var prefix = app ? app.location : '(no app)'; + var prefix = app ? config.appFqdn(app) : '(no app)'; debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1))); } @@ -704,7 +704,7 @@ function backupApp(app, callback) { const timestamp = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,''); safe.fs.unlinkSync(paths.BACKUP_LOG_FILE); // start fresh log file - progress.set(progress.BACKUP, 10, 'Backing up ' + (app.altDomain || config.appFqdn(app.location))); + progress.set(progress.BACKUP, 10, 'Backing up ' + (app.altDomain || app.location)); backupAppWithTimestamp(app, timestamp, function (error) { progress.set(progress.BACKUP, 100, error ? error.message : ''); @@ -731,12 +731,12 @@ function backupBoxAndApps(auditSource, callback) { var step = 100/(allApps.length+2); async.mapSeries(allApps, function iterator(app, iteratorCallback) { - progress.set(progress.BACKUP, step * processed, 'Backing up ' + (app.altDomain || config.appFqdn(app.location))); + progress.set(progress.BACKUP, step * processed, 'Backing up ' + (app.altDomain || config.appFqdn(app))); ++processed; if (!app.enableBackup) { - progress.set(progress.BACKUP, step * processed, 'Skipped backup ' + (app.altDomain || config.appFqdn(app.location))); + progress.set(progress.BACKUP, step * processed, 'Skipped backup ' + (app.altDomain || config.appFqdn(app))); return iteratorCallback(null, null); // nothing to backup } @@ -746,7 +746,7 @@ function backupBoxAndApps(auditSource, callback) { return iteratorCallback(error); } - progress.set(progress.BACKUP, step * processed, 'Backed up ' + (app.altDomain || config.appFqdn(app.location))); + progress.set(progress.BACKUP, step * processed, 'Backed up ' + (app.altDomain || config.appFqdn(app))); iteratorCallback(null, backupId || null); // clear backupId if is in BAD_STATE and never backed up }); diff --git a/src/certificates.js b/src/certificates.js index c3ae8a69f..20c042d05 100644 --- a/src/certificates.js +++ b/src/certificates.js @@ -181,7 +181,7 @@ function renewAll(auditSource, callback) { var expiringApps = [ ]; for (var i = 0; i < allApps.length; i++) { - var appDomain = allApps[i].altDomain || config.appFqdn(allApps[i].location); + var appDomain = allApps[i].altDomain || config.appFqdn(allApps[i]); var certFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.user.cert'); var keyFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.user.key'); @@ -205,10 +205,10 @@ function renewAll(auditSource, callback) { } } - debug('renewAll: %j needs to be renewed', expiringApps.map(function (a) { return a.altDomain || config.appFqdn(a.location); })); + debug('renewAll: %j needs to be renewed', expiringApps.map(function (app) { return app.altDomain || config.appFqdn(app); })); async.eachSeries(expiringApps, function iterator(app, iteratorCallback) { - var domain = app.altDomain || config.appFqdn(app.location); + var domain = app.altDomain || config.appFqdn(app); getApi(app, function (error, api, apiOptions) { if (error) return callback(error); @@ -394,7 +394,7 @@ function ensureCertificate(app, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof callback, 'function'); - var domain = app.altDomain || config.appFqdn(app.location); + var domain = app.altDomain || config.appFqdn(app); var certFilePath = path.join(paths.APP_CERTS_DIR, domain + '.user.cert'); var keyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.user.key'); diff --git a/src/cloudron.js b/src/cloudron.js index dd37537b4..595ecfb8a 100644 --- a/src/cloudron.js +++ b/src/cloudron.js @@ -237,7 +237,7 @@ function configureWebadmin(callback) { function configureNginx(error) { debug('configureNginx: dns update:%j', error); - certificates.ensureCertificate({ location: config.adminLocation() }, function (error, certFilePath, keyFilePath) { + certificates.ensureCertificate({ domain: config.fqdn(), location: config.adminLocation() }, function (error, certFilePath, keyFilePath) { if (error) return done(error); gWebadminStatus.tls = true; diff --git a/src/config.js b/src/config.js index 307ebfd25..841ec822d 100644 --- a/src/config.js +++ b/src/config.js @@ -189,11 +189,15 @@ function zoneName() { } // keep this in sync with start.sh admin.conf generation code -function appFqdn(location) { - assert.strictEqual(typeof location, 'string'); +function appFqdn(app) { + assert.strictEqual(typeof app, 'object'); + assert.strictEqual(typeof app.location, 'string'); + assert.strictEqual(typeof app.domain, 'string'); - if (location === '') return fqdn(); - return isCustomDomain() ? location + '.' + fqdn() : location + '-' + fqdn(); + if (app.location === '') return app.domain; + + // caas still has subdomains with a dash + return app.location + (isCustomDomain() ? '.' : '-') + app.domain; } function mailLocation() { @@ -201,7 +205,7 @@ function mailLocation() { } function mailFqdn() { - return appFqdn(mailLocation()); + return appFqdn({ domain: fqdn(), location: mailLocation() }); } function adminLocation() { @@ -209,11 +213,11 @@ function adminLocation() { } function adminFqdn() { - return appFqdn(adminLocation()); + return appFqdn({ domain: fqdn(), location: adminLocation() }); } function adminOrigin() { - return 'https://' + appFqdn(adminLocation()); + return 'https://' + adminFqdn(); } function internalAdminOrigin() { diff --git a/src/docker.js b/src/docker.js index 54afd42a3..e415aba5c 100644 --- a/src/docker.js +++ b/src/docker.js @@ -129,7 +129,7 @@ function createSubcontainer(app, name, cmd, options, callback) { var manifest = app.manifest; var exposedPorts = {}, dockerPortBindings = { }; - var domain = app.altDomain || config.appFqdn(app.location); + var domain = app.altDomain || config.appFqdn(app); var stdEnv = [ 'CLOUDRON=1', 'WEBADMIN_ORIGIN=' + config.adminOrigin(), diff --git a/src/nginx.js b/src/nginx.js index 7e8e88bb2..878c6d178 100644 --- a/src/nginx.js +++ b/src/nginx.js @@ -55,7 +55,7 @@ function configureApp(app, certFilePath, keyFilePath, callback) { var sourceDir = path.resolve(__dirname, '..'); var endpoint = 'app'; - var vhost = app.altDomain || config.appFqdn(app.location); + var vhost = app.altDomain || config.appFqdn(app); var data = { sourceDir: sourceDir, @@ -86,7 +86,7 @@ function unconfigureApp(app, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof callback, 'function'); - var vhost = app.altDomain || config.appFqdn(app.location); + var vhost = app.altDomain || config.appFqdn(app); var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf'); if (!safe.fs.unlinkSync(nginxConfigFilename)) { diff --git a/src/routes/apps.js b/src/routes/apps.js index bd7dba386..c31632c14 100644 --- a/src/routes/apps.js +++ b/src/routes/apps.js @@ -116,6 +116,7 @@ function installApp(req, res, next) { // required if (typeof data.location !== 'string') return next(new HttpError(400, 'location is required')); + if (typeof data.domain !== 'string') return next(new HttpError(400, 'domain is required')); if (typeof data.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction is required')); // optional @@ -168,6 +169,7 @@ function configureApp(req, res, next) { var data = req.body; if ('location' in data && typeof data.location !== 'string') return next(new HttpError(400, 'location must be string')); + if ('domain' in data && typeof data.domain !== 'string') return next(new HttpError(400, 'domain must be string')); if ('portBindings' in data && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object')); if ('accessRestriction' in data && typeof data.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction must be an object')); @@ -235,6 +237,7 @@ function cloneApp(req, res, next) { if (typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be a string')); if (typeof data.location !== 'string') return next(new HttpError(400, 'location is required')); + if (typeof data.domain !== 'string') return next(new HttpError(400, 'domain is required')); if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object')); apps.clone(req.params.id, data, auditSource(req), function (error, result) {