Compare commits
166 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d7f0c3ebe | ||
|
|
a66bc7192d | ||
|
|
ff550e897a | ||
|
|
10034fcbba | ||
|
|
36f8ce453f | ||
|
|
c2e40acb2c | ||
|
|
82b1bb668d | ||
|
|
935a8258a6 | ||
|
|
fa483e5806 | ||
|
|
e0c9658cb9 | ||
|
|
0266a46b32 | ||
|
|
e7294f2950 | ||
|
|
c9f325e75d | ||
|
|
0fa353c2e2 | ||
|
|
c7da090882 | ||
|
|
ee609c8ef0 | ||
|
|
6891ce2bc8 | ||
|
|
94f5adba04 | ||
|
|
b8f843993a | ||
|
|
f9add21899 | ||
|
|
1277da8bfe | ||
|
|
55650fb734 | ||
|
|
d2f4b68c9f | ||
|
|
a76731a991 | ||
|
|
536b8166ce | ||
|
|
d43106b0af | ||
|
|
3688371ce8 | ||
|
|
6d66eb7759 | ||
|
|
8502bf4bfa | ||
|
|
d8225ad653 | ||
|
|
cfb68a0511 | ||
|
|
76677e0aea | ||
|
|
515ee891d3 | ||
|
|
3aea1f3c9d | ||
|
|
8d944f9a4a | ||
|
|
331c8ae247 | ||
|
|
c71a429f61 | ||
|
|
3bad9e523c | ||
|
|
dfa61f1b2d | ||
|
|
6331fa5ced | ||
|
|
707b03b8c8 | ||
|
|
f2f93ed141 | ||
|
|
37e16c7a4c | ||
|
|
41b0c3242e | ||
|
|
48ed051edf | ||
|
|
502642fd25 | ||
|
|
4abe6a7a00 | ||
|
|
3f8fa64b98 | ||
|
|
527ff1b1fb | ||
|
|
804467dce2 | ||
|
|
4d7f308821 | ||
|
|
a5b8418845 | ||
|
|
93d428b8c5 | ||
|
|
7c424ad60c | ||
|
|
5b29a8680d | ||
|
|
8f57c44837 | ||
|
|
b23939127b | ||
|
|
3196322063 | ||
|
|
54c96d98d1 | ||
|
|
f5f92fbb03 | ||
|
|
be0876603c | ||
|
|
7c1ef143f9 | ||
|
|
6d128595e7 | ||
|
|
2f55abfc60 | ||
|
|
f93044ac3b | ||
|
|
7ed422a3c1 | ||
|
|
823b3b8aa8 | ||
|
|
9a701560f4 | ||
|
|
9800154d01 | ||
|
|
4b3f18ccdb | ||
|
|
840d78b2f4 | ||
|
|
b409fd775d | ||
|
|
dbcfb20fab | ||
|
|
12a5965740 | ||
|
|
006ab75433 | ||
|
|
c72ea91743 | ||
|
|
f39ce20580 | ||
|
|
b5c59e6b7d | ||
|
|
b0ecdcc8b6 | ||
|
|
8e1560f412 | ||
|
|
df927eae74 | ||
|
|
30aea047e3 | ||
|
|
cbcadaa449 | ||
|
|
9f4226093b | ||
|
|
fca0e897b2 | ||
|
|
2f729b56fa | ||
|
|
d9f3f64c76 | ||
|
|
e8fa909c2f | ||
|
|
44f6636653 | ||
|
|
148a0d0fc6 | ||
|
|
632ba69663 | ||
|
|
b2465dd2ee | ||
|
|
e56b87766b | ||
|
|
f7ca2e416a | ||
|
|
002f68b0a1 | ||
|
|
aa31be5c5a | ||
|
|
6c0b7017bd | ||
|
|
581774e001 | ||
|
|
3847a6616e | ||
|
|
48fbe28355 | ||
|
|
e3ee5bc1d5 | ||
|
|
a2da9bea58 | ||
|
|
e4512e12c5 | ||
|
|
114f48fb17 | ||
|
|
289e018160 | ||
|
|
cb6699eeed | ||
|
|
802011bb7e | ||
|
|
6cd8e769be | ||
|
|
9f6f67d331 | ||
|
|
161a8fe2bf | ||
|
|
b9c9839bb7 | ||
|
|
76edbee48c | ||
|
|
4142d7a050 | ||
|
|
a0306c69e1 | ||
|
|
31823f6282 | ||
|
|
9b4fffde29 | ||
|
|
cce03e250d | ||
|
|
9b32cad946 | ||
|
|
2877a1057e | ||
|
|
e2debe3c39 | ||
|
|
f54ab11f18 | ||
|
|
b560e281d0 | ||
|
|
3bb4ef5727 | ||
|
|
900c008d20 | ||
|
|
c1183a09a8 | ||
|
|
e04b7b55b0 | ||
|
|
329cc80933 | ||
|
|
a13f0706b4 | ||
|
|
55811de4b8 | ||
|
|
ab456f179e | ||
|
|
f9d5bcd352 | ||
|
|
6a337884b5 | ||
|
|
f953d115da | ||
|
|
88e8fc840f | ||
|
|
d1818e31b0 | ||
|
|
3f4bf647e8 | ||
|
|
725a7e6dec | ||
|
|
e08b210001 | ||
|
|
ec08ccb996 | ||
|
|
b47a146c2b | ||
|
|
14dff27d45 | ||
|
|
305a3c94d0 | ||
|
|
218739a6b5 | ||
|
|
390e69c01c | ||
|
|
4ef274acf0 | ||
|
|
8267279779 | ||
|
|
6d971b9235 | ||
|
|
98dc160886 | ||
|
|
a869c88b43 | ||
|
|
0b86070fe9 | ||
|
|
5c9b6736f0 | ||
|
|
fd4057df94 | ||
|
|
1b1945e1f5 | ||
|
|
ebb053b900 | ||
|
|
3381d9b595 | ||
|
|
d7a11ef394 | ||
|
|
9d40cffabe | ||
|
|
de44c63557 | ||
|
|
ac25477cd7 | ||
|
|
59b86aa090 | ||
|
|
6abd48d480 | ||
|
|
72fc6b8c5a | ||
|
|
fcce4a6853 | ||
|
|
a3b1a2c781 | ||
|
|
a838a1706f | ||
|
|
a24c9fbafb |
@@ -23,6 +23,7 @@
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
]
|
||||
],
|
||||
"no-console": "off"
|
||||
}
|
||||
}
|
||||
30
CHANGES
30
CHANGES
@@ -1464,3 +1464,33 @@
|
||||
* Add support for hyphenated subdomains
|
||||
* Add domain, mail events to eventlog
|
||||
|
||||
[3.4.0]
|
||||
* Improve error page
|
||||
* Add system view to manage addons and view their status
|
||||
* Fix iconset regression for account and Cloudron name edits
|
||||
* Add server reboot button and warn if reboot is required for security updates
|
||||
* Backup and update tasks are now cancelable
|
||||
* Move graphite away from port 3000 (reserved by ESXi)
|
||||
* Flexible mailbox management
|
||||
* Automatic updates can be toggled per app
|
||||
|
||||
[3.4.1]
|
||||
* Improve error page
|
||||
* Add system view to manage addons and view their status
|
||||
* Fix iconset regression for account and Cloudron name edits
|
||||
* Add server reboot button and warn if reboot is required for security updates
|
||||
* Backup and update tasks are now cancelable
|
||||
* Move graphite away from port 3000 (reserved by ESXi)
|
||||
* Flexible mailbox management
|
||||
* Automatic updates can be toggled per app
|
||||
|
||||
[3.4.2]
|
||||
* Improve error page
|
||||
* Add system view to manage addons and view their status
|
||||
* Fix iconset regression for account and Cloudron name edits
|
||||
* Add server reboot button and warn if reboot is required for security updates
|
||||
* Backup and update tasks are now cancelable
|
||||
* Move graphite away from port 3000 (reserved by ESXi)
|
||||
* Flexible mailbox management
|
||||
* Automatic updates can be toggled per app
|
||||
|
||||
|
||||
14
box.js
14
box.js
@@ -2,12 +2,16 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
require('supererror')({ splatchError: true });
|
||||
// prefix all output with a timestamp
|
||||
// debug() already prefixes and uses process.stderr NOT console.*
|
||||
['log', 'info', 'warn', 'debug', 'error'].forEach(function (log) {
|
||||
var orig = console[log];
|
||||
console[log] = function () {
|
||||
orig.apply(console, [new Date().toISOString()].concat(Array.prototype.slice.call(arguments)));
|
||||
};
|
||||
});
|
||||
|
||||
// remove timestamp from debug() based output
|
||||
require('debug').formatArgs = function formatArgs(args) {
|
||||
args[0] = this.namespace + ' ' + args[0];
|
||||
};
|
||||
require('supererror')({ splatchError: true });
|
||||
|
||||
let async = require('async'),
|
||||
config = require('./src/config.js'),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
var cmd = "CREATE TABLE groups(" +
|
||||
var cmd = "CREATE TABLE userGroups(" +
|
||||
"id VARCHAR(128) NOT NULL UNIQUE," +
|
||||
"name VARCHAR(128) NOT NULL UNIQUE," +
|
||||
"PRIMARY KEY(id))";
|
||||
@@ -13,7 +13,7 @@ exports.up = function(db, callback) {
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('DROP TABLE groups', function (error) {
|
||||
db.runSql('DROP TABLE userGroups', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ exports.up = function(db, callback) {
|
||||
var cmd = "CREATE TABLE IF NOT EXISTS groupMembers(" +
|
||||
"groupId VARCHAR(128) NOT NULL," +
|
||||
"userId VARCHAR(128) NOT NULL," +
|
||||
"FOREIGN KEY(groupId) REFERENCES groups(id)," +
|
||||
"FOREIGN KEY(groupId) REFERENCES userGroups(id)," +
|
||||
"FOREIGN KEY(userId) REFERENCES users(id));";
|
||||
|
||||
db.runSql(cmd, function (error) {
|
||||
|
||||
@@ -7,7 +7,7 @@ var ADMIN_GROUP_ID = 'admin'; // see constants.js
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
db.runSql.bind(db, 'INSERT INTO groups (id, name) VALUES (?, ?)', [ ADMIN_GROUP_ID, 'admin' ]),
|
||||
db.runSql.bind(db, 'INSERT INTO userGroups (id, name) VALUES (?, ?)', [ ADMIN_GROUP_ID, 'admin' ]),
|
||||
function migrateAdminFlag(done) {
|
||||
db.all('SELECT * FROM users WHERE admin=1', function (error, results) {
|
||||
if (error) return done(error);
|
||||
|
||||
@@ -10,7 +10,7 @@ exports.up = function(db, callback) {
|
||||
function addGroupMailboxes(done) {
|
||||
console.log('Importing group mailboxes');
|
||||
|
||||
db.all('SELECT id, name FROM groups', function (error, results) {
|
||||
db.all('SELECT id, name FROM userGroups', function (error, results) {
|
||||
if (error) return done(error);
|
||||
|
||||
async.eachSeries(results, function (g, next) {
|
||||
|
||||
@@ -16,7 +16,7 @@ exports.up = function(db, callback) {
|
||||
db.runSql.bind(db, 'ALTER TABLE clients CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
|
||||
db.runSql.bind(db, 'ALTER TABLE eventlog CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
|
||||
db.runSql.bind(db, 'ALTER TABLE groupMembers CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
|
||||
db.runSql.bind(db, 'ALTER TABLE groups CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
|
||||
db.runSql.bind(db, 'ALTER TABLE userGroups CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
|
||||
db.runSql.bind(db, 'ALTER TABLE mailboxes CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
|
||||
db.runSql.bind(db, 'ALTER TABLE migrations CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
|
||||
db.runSql.bind(db, 'ALTER TABLE settings CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
|
||||
|
||||
@@ -29,7 +29,7 @@ exports.up = function(db, callback) {
|
||||
|
||||
// this will be finally created once we have a domain when we create the owner in user.js
|
||||
const ADMIN_GROUP_ID = 'admin'; // see constants.js
|
||||
db.runSql('DELETE FROM groups WHERE id = ?', [ ADMIN_GROUP_ID ], function (error) {
|
||||
db.runSql('DELETE FROM userGroups WHERE id = ?', [ ADMIN_GROUP_ID ], function (error) {
|
||||
if (error) return done(error);
|
||||
|
||||
db.runSql('DELETE FROM mailboxes WHERE ownerId = ?', [ ADMIN_GROUP_ID ], done);
|
||||
|
||||
@@ -19,8 +19,8 @@ exports.up = function(db, callback) {
|
||||
},
|
||||
function getGroups(done) {
|
||||
db.all('SELECT id, name, GROUP_CONCAT(groupMembers.userId) AS userIds ' +
|
||||
' FROM groups LEFT OUTER JOIN groupMembers ON groups.id = groupMembers.groupId ' +
|
||||
' GROUP BY groups.id', [ ], function (error, results) {
|
||||
' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' +
|
||||
' GROUP BY userGroups.id', [ ], function (error, results) {
|
||||
if (error) return done(error);
|
||||
|
||||
results.forEach(function (result) {
|
||||
|
||||
@@ -18,7 +18,7 @@ exports.up = function(db, callback) {
|
||||
|
||||
async.series([
|
||||
db.runSql.bind(db, 'DELETE FROM groupMembers WHERE groupId=?', [ 'admin' ]),
|
||||
db.runSql.bind(db, 'DELETE FROM groups WHERE id=?', [ 'admin' ])
|
||||
db.runSql.bind(db, 'DELETE FROM userGroups WHERE id=?', [ 'admin' ])
|
||||
], callback);
|
||||
});
|
||||
});
|
||||
|
||||
27
migrations/20181116191032-tasks-add-table.js
Normal file
27
migrations/20181116191032-tasks-add-table.js
Normal file
@@ -0,0 +1,27 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
var cmd = 'CREATE TABLE tasks(' +
|
||||
'id int NOT NULL AUTO_INCREMENT,' +
|
||||
'type VARCHAR(32) NOT NULL,' +
|
||||
'argsJson TEXT,' +
|
||||
'percent INTEGER DEFAULT 0,' +
|
||||
'message TEXT,' +
|
||||
'errorMessage TEXT,' +
|
||||
'result TEXT,' +
|
||||
'creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,' +
|
||||
'ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,' +
|
||||
'PRIMARY KEY (id))';
|
||||
|
||||
db.runSql(cmd, function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('DROP TABLE tasks', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
17
migrations/20181203111504-rename-groups-to-userGroups.js
Normal file
17
migrations/20181203111504-rename-groups-to-userGroups.js
Normal file
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('SELECT 1 FROM groups LIMIT 1', function (error) {
|
||||
if (error) return callback(); // groups table does not exist
|
||||
|
||||
db.runSql('RENAME TABLE groups TO userGroups', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
// this is a one way renaming since the previous migration steps have been already updated to match the new name
|
||||
callback();
|
||||
};
|
||||
17
migrations/20181203122115-ensure-default-timestamps.js
Normal file
17
migrations/20181203122115-ensure-default-timestamps.js
Normal file
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE apps MODIFY creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'),
|
||||
db.runSql.bind(db, 'ALTER TABLE apps MODIFY updateTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'),
|
||||
db.runSql.bind(db, 'ALTER TABLE eventlog MODIFY creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'),
|
||||
db.runSql.bind(db, 'ALTER TABLE backups MODIFY creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'),
|
||||
db.runSql.bind(db, 'ALTER TABLE mailboxes MODIFY creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'),
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
28
migrations/20181207042802-apps-add-mailboxName.js
Normal file
28
migrations/20181207042802-apps-add-mailboxName.js
Normal file
@@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE apps ADD COLUMN mailboxName VARCHAR(128)'),
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
|
||||
function migrateMailboxNames(done) {
|
||||
db.all('SELECT * FROM mailboxes', function (error, mailboxes) {
|
||||
if (error) return done(error);
|
||||
|
||||
async.eachSeries(mailboxes, function (mailbox, iteratorDone) {
|
||||
if (mailbox.ownerType !== 'app') return iteratorDone();
|
||||
|
||||
db.runSql('UPDATE apps SET mailboxName = ? WHERE id = ?', [ mailbox.name, mailbox.ownerId ], iteratorDone);
|
||||
}, done);
|
||||
});
|
||||
},
|
||||
|
||||
db.runSql.bind(db, 'COMMIT')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
28
migrations/20181207042803-mailboxes-remove-ownerType.js
Normal file
28
migrations/20181207042803-mailboxes-remove-ownerType.js
Normal file
@@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
|
||||
function migrateMailboxNames(done) {
|
||||
db.all('SELECT * FROM mailboxes', function (error, mailboxes) {
|
||||
if (error) return done(error);
|
||||
|
||||
async.eachSeries(mailboxes, function (mailbox, iteratorDone) {
|
||||
if (mailbox.ownerType !== 'app') return iteratorDone();
|
||||
|
||||
db.runSql('DELETE FROM mailboxes WHERE name = ?', [ mailbox.name ], iteratorDone);
|
||||
}, done);
|
||||
});
|
||||
},
|
||||
|
||||
db.runSql.bind(db, 'COMMIT'),
|
||||
db.runSql.bind(db, 'ALTER TABLE mailboxes DROP COLUMN ownerType')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
16
migrations/20181207165827-apps-add-enableAutomaticUpdate.js
Normal file
16
migrations/20181207165827-apps-add-enableAutomaticUpdate.js
Normal file
@@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN enableAutomaticUpdate BOOLEAN DEFAULT 1', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN enableAutomaticUpdate', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -29,7 +29,7 @@ CREATE TABLE IF NOT EXISTS users(
|
||||
|
||||
PRIMARY KEY(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS groups(
|
||||
CREATE TABLE IF NOT EXISTS userGroups(
|
||||
id VARCHAR(128) NOT NULL UNIQUE,
|
||||
name VARCHAR(254) NOT NULL UNIQUE,
|
||||
PRIMARY KEY(id));
|
||||
@@ -37,7 +37,7 @@ CREATE TABLE IF NOT EXISTS groups(
|
||||
CREATE TABLE IF NOT EXISTS groupMembers(
|
||||
groupId VARCHAR(128) NOT NULL,
|
||||
userId VARCHAR(128) NOT NULL,
|
||||
FOREIGN KEY(groupId) REFERENCES groups(id),
|
||||
FOREIGN KEY(groupId) REFERENCES userGroups(id),
|
||||
FOREIGN KEY(userId) REFERENCES users(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tokens(
|
||||
@@ -71,8 +71,8 @@ CREATE TABLE IF NOT EXISTS apps(
|
||||
location VARCHAR(128) NOT NULL,
|
||||
domain VARCHAR(128) NOT NULL,
|
||||
accessRestrictionJson TEXT, // { users: [ ], groups: [ ] }
|
||||
creationTime TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app was installed
|
||||
updateTime TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the last app update was done
|
||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app was installed
|
||||
updateTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the last app update was done
|
||||
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, // when this db record was updated (useful for UI caching)
|
||||
memoryLimit BIGINT DEFAULT 0,
|
||||
xFrameOptions VARCHAR(512),
|
||||
@@ -80,6 +80,8 @@ CREATE TABLE IF NOT EXISTS apps(
|
||||
debugModeJson TEXT, // options for development mode
|
||||
robotsTxt TEXT,
|
||||
enableBackup BOOLEAN DEFAULT 1, // misnomer: controls automatic daily backups
|
||||
enableAutomaticUpdate BOOLEAN DEFAULT 1,
|
||||
mailboxName VARCHAR(128), // mailbox of this app
|
||||
|
||||
// the following fields do not belong here, they can be removed when we use a queue for apptask
|
||||
restoreConfigJson VARCHAR(256), // used to pass backupId to restore from to apptask
|
||||
@@ -126,7 +128,7 @@ CREATE TABLE IF NOT EXISTS appEnvVars(
|
||||
|
||||
CREATE TABLE IF NOT EXISTS backups(
|
||||
id VARCHAR(128) NOT NULL,
|
||||
creationTime TIMESTAMP,
|
||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
version VARCHAR(128) NOT NULL, /* app version or box version */
|
||||
type VARCHAR(16) NOT NULL, /* 'box' or 'app' */
|
||||
dependsOn TEXT, /* comma separate list of objects this backup depends on */
|
||||
@@ -141,7 +143,7 @@ CREATE TABLE IF NOT EXISTS eventlog(
|
||||
action VARCHAR(128) NOT NULL,
|
||||
source TEXT, /* { userId, username, ip }. userId can be null for cron,sysadmin */
|
||||
data TEXT, /* free flowing json based on action */
|
||||
createdAt TIMESTAMP(2) NOT NULL,
|
||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
PRIMARY KEY (id));
|
||||
|
||||
@@ -173,15 +175,17 @@ CREATE TABLE IF NOT EXISTS mail(
|
||||
/* Future fields:
|
||||
* accessRestriction - to determine who can access it. So this has foreign keys
|
||||
* quota - per mailbox quota
|
||||
|
||||
NOTE: this table exists only real mailboxes. And has unique constraint to handle
|
||||
conflict with aliases and mailbox names
|
||||
*/
|
||||
CREATE TABLE IF NOT EXISTS mailboxes(
|
||||
name VARCHAR(128) NOT NULL,
|
||||
type VARCHAR(16) NOT NULL, /* 'mailbox', 'alias', 'list' */
|
||||
ownerId VARCHAR(128) NOT NULL, /* app id or user id or group id */
|
||||
ownerType VARCHAR(16) NOT NULL, /* 'app' or 'user' or 'group' */
|
||||
ownerId VARCHAR(128) NOT NULL, /* user id */
|
||||
aliasTarget VARCHAR(128), /* the target name type is an alias */
|
||||
membersJson TEXT, /* members of a group */
|
||||
creationTime TIMESTAMP,
|
||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
domain VARCHAR(128),
|
||||
|
||||
FOREIGN KEY(domain) REFERENCES mail(domain),
|
||||
@@ -195,6 +199,18 @@ CREATE TABLE IF NOT EXISTS subdomains(
|
||||
|
||||
FOREIGN KEY(domain) REFERENCES domains(domain),
|
||||
FOREIGN KEY(appId) REFERENCES apps(id),
|
||||
UNIQUE (subdomain, domain))
|
||||
UNIQUE (subdomain, domain));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tasks(
|
||||
id int NOT NULL AUTO_INCREMENT,
|
||||
type VARCHAR(32) NOT NULL,
|
||||
percent INTEGER DEFAULT 0,
|
||||
message TEXT,
|
||||
errorMessage TEXT,
|
||||
result TEXT,
|
||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id));
|
||||
|
||||
CHARACTER SET utf8 COLLATE utf8_bin;
|
||||
|
||||
|
||||
3113
package-lock.json
generated
3113
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -27,7 +27,7 @@
|
||||
"connect-timeout": "^1.9.0",
|
||||
"cookie-parser": "^1.3.5",
|
||||
"cookie-session": "^1.3.2",
|
||||
"cron": "^1.3.0",
|
||||
"cron": "^1.5.1",
|
||||
"csurf": "^1.6.6",
|
||||
"db-migrate": "^0.11.1",
|
||||
"db-migrate-mysql": "^1.1.10",
|
||||
@@ -92,7 +92,7 @@
|
||||
"scripts": {
|
||||
"migrate_local": "DATABASE_URL=mysql://root:@localhost/box node_modules/.bin/db-migrate up",
|
||||
"migrate_test": "BOX_ENV=test DATABASE_URL=mysql://root:@localhost/boxtest node_modules/.bin/db-migrate up",
|
||||
"test": "npm run migrate_test && src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- --exit -R spec ./src/test ./src/routes/test",
|
||||
"test": "npm run migrate_test && src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- --no-timeouts --exit -R spec ./src/test ./src/routes/test",
|
||||
"postmerge": "/bin/true",
|
||||
"precommit": "/bin/true",
|
||||
"prepush": "npm test",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
|
||||
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400 --http1.1"
|
||||
|
||||
ip=""
|
||||
dns_config=""
|
||||
|
||||
@@ -94,7 +94,7 @@ echo "Running cloudron-setup with args : $@" > "${LOG_FILE}"
|
||||
|
||||
# validate arguments in the absence of data
|
||||
if [[ -z "${provider}" ]]; then
|
||||
echo "--provider is required (azure, digitalocean, ec2, exoscale, gce, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
|
||||
echo "--provider is required (azure, digitalocean, ec2, exoscale, gce, hetzner, lightsail, linode, netcup, ovh, rosehosting, scaleway, vultr or generic)"
|
||||
exit 1
|
||||
elif [[ \
|
||||
"${provider}" != "ami" && \
|
||||
@@ -110,13 +110,14 @@ elif [[ \
|
||||
"${provider}" != "hetzner" && \
|
||||
"${provider}" != "lightsail" && \
|
||||
"${provider}" != "linode" && \
|
||||
"${provider}" != "netcup" && \
|
||||
"${provider}" != "ovh" && \
|
||||
"${provider}" != "rosehosting" && \
|
||||
"${provider}" != "scaleway" && \
|
||||
"${provider}" != "vultr" && \
|
||||
"${provider}" != "generic" \
|
||||
]]; then
|
||||
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
|
||||
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, lightsail, linode, netcup, ovh, rosehosting, scaleway, vultr or generic"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -137,6 +138,12 @@ echo " Join us at https://forum.cloudron.io for any questions."
|
||||
echo ""
|
||||
|
||||
if [[ "${initBaseImage}" == "true" ]]; then
|
||||
echo "=> Ensure required apt sources"
|
||||
if ! add-apt-repository universe &>> "${LOG_FILE}"; then
|
||||
echo "Could not add required apt sources (for nginx-full). See ${LOG_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=> Updating apt and installing script dependencies"
|
||||
if ! apt-get update &>> "${LOG_FILE}"; then
|
||||
echo "Could not update package repositories. See ${LOG_FILE}"
|
||||
@@ -231,10 +238,15 @@ while true; do
|
||||
sleep 10
|
||||
done
|
||||
|
||||
echo -e "\n\n${GREEN}Visit https://<IP> and accept the self-signed certificate to finish setup.${DONE}"
|
||||
echo -e "\n\n${GREEN}Visit https://<IP> and accept the self-signed certificate to finish setup.${DONE}\n"
|
||||
|
||||
if [[ "${rebootServer}" == "true" ]]; then
|
||||
echo -e "\n${RED}Rebooting this server now to let changes take effect.${DONE}\n"
|
||||
systemctl stop mysql # sometimes mysql ends up having corrupt privilege tables
|
||||
systemctl reboot
|
||||
systemctl stop box mysql # sometimes mysql ends up having corrupt privilege tables
|
||||
|
||||
read -p "This server has to rebooted to apply all the settings. Reboot now ? [Y/n] " yn
|
||||
yn=${yn:-y}
|
||||
case $yn in
|
||||
[Yy]* ) systemctl reboot;;
|
||||
* ) exit;;
|
||||
esac
|
||||
fi
|
||||
|
||||
@@ -7,6 +7,13 @@ PASTEBIN="https://paste.cloudron.io"
|
||||
OUT="/tmp/cloudron-support.log"
|
||||
LINE="\n========================================================\n"
|
||||
CLOUDRON_SUPPORT_PUBLIC_KEY="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQVilclYAIu+ioDp/sgzzFz6YU0hPcRYY7ze/LiF/lC7uQqK062O54BFXTvQ3ehtFZCx3bNckjlT2e6gB8Qq07OM66De4/S/g+HJW4TReY2ppSPMVNag0TNGxDzVH8pPHOysAm33LqT2b6L/wEXwC6zWFXhOhHjcMqXvi8Ejaj20H1HVVcf/j8qs5Thkp9nAaFTgQTPu8pgwD8wDeYX1hc9d0PYGesTADvo6HF4hLEoEnefLw7PaStEbzk2fD3j7/g5r5HcgQQXBe74xYZ/1gWOX2pFNuRYOBSEIrNfJEjFJsqk3NR1+ZoMGK7j+AZBR4k0xbrmncQLcQzl6MMDzkp support@cloudron.io"
|
||||
HELP_MESSAGE="
|
||||
This script collects diagnostic information to help debug server related issues
|
||||
|
||||
Options:
|
||||
--enable-ssh Enable SSH access for the Cloudron support team
|
||||
--help Show this message
|
||||
"
|
||||
|
||||
# We require root
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
@@ -14,6 +21,20 @@ if [[ ${EUID} -ne 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
enableSSH="false"
|
||||
|
||||
args=$(getopt -o "" -l "help,enable-ssh" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--help) echo -e "${HELP_MESSAGE}"; exit 0;;
|
||||
--enable-ssh) enableSSH="true"; shift;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
# check if at least 10mb root partition space is available
|
||||
if [[ "`df --output="avail" / | sed -n 2p`" -lt "10240" ]]; then
|
||||
echo "No more space left on /"
|
||||
@@ -66,6 +87,9 @@ df -h &>> $OUT
|
||||
echo -e $LINE"System daemon status"$LINE >> $OUT
|
||||
systemctl status --lines=100 cloudron.target box mysql unbound cloudron-syslog nginx collectd docker &>> $OUT
|
||||
|
||||
echo -e $LINE"Box logs"$LINE >> $OUT
|
||||
tail -n 100 /home/yellowtent/platformdata/logs/box.log &>> $OUT
|
||||
|
||||
echo -e $LINE"Firewall chains"$LINE >> $OUT
|
||||
iptables -L &>> $OUT
|
||||
|
||||
@@ -76,12 +100,14 @@ echo -n "Uploading information..."
|
||||
paste_key=$(curl -X POST ${PASTEBIN}/documents --silent -d "$(cat $OUT)" | python3 -c "import sys, json; print(json.load(sys.stdin)['key'])")
|
||||
echo "Done"
|
||||
|
||||
echo -n "Enabling ssh access for the Cloudron support team..."
|
||||
mkdir -p "${ssh_folder}"
|
||||
echo "${CLOUDRON_SUPPORT_PUBLIC_KEY}" >> ${authorized_key_file}
|
||||
chown -R ${ssh_user} "${ssh_folder}"
|
||||
chmod 600 "${authorized_key_file}"
|
||||
echo "Done"
|
||||
if [[ "${enableSSH}" == "true" ]]; then
|
||||
echo -n "Enabling ssh access for the Cloudron support team..."
|
||||
mkdir -p "${ssh_folder}"
|
||||
echo "${CLOUDRON_SUPPORT_PUBLIC_KEY}" >> ${authorized_key_file}
|
||||
chown -R ${ssh_user} "${ssh_folder}"
|
||||
chmod 600 "${authorized_key_file}"
|
||||
echo "Done"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Please email the following link to support@cloudron.io"
|
||||
|
||||
@@ -86,8 +86,14 @@ images=$(node -e "var i = require('${box_src_tmp_dir}/src/infra_version.js'); co
|
||||
|
||||
echo -e "\tPulling docker images: ${images}"
|
||||
for image in ${images}; do
|
||||
docker pull "${image}" # this pulls the image using the sha256
|
||||
docker pull "${image%@sha256:*}" # this will tag the image for readability
|
||||
if ! docker pull "${image}"; then # this pulls the image using the sha256
|
||||
echo "==> installer: Could not pull ${image}"
|
||||
exit 5
|
||||
fi
|
||||
if ! docker pull "${image%@sha256:*}"; then # this will tag the image for readability
|
||||
echo "==> installer: Could not pull ${image%@sha256:*}"
|
||||
exit 6
|
||||
fi
|
||||
done
|
||||
|
||||
echo "==> installer: update cloudron-syslog"
|
||||
|
||||
@@ -16,7 +16,7 @@ readonly BOX_DATA_DIR="${HOME_DIR}/boxdata" # box data
|
||||
readonly CONFIG_DIR="${HOME_DIR}/configs"
|
||||
|
||||
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly get_config="$(realpath ${script_dir}/../node_modules/.bin/json) -f /etc/cloudron/cloudron.conf"
|
||||
readonly json="$(realpath ${script_dir}/../node_modules/.bin/json)"
|
||||
|
||||
echo "==> Configuring docker"
|
||||
cp "${script_dir}/start/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
|
||||
@@ -58,7 +58,7 @@ mkdir -p "${PLATFORM_DATA_DIR}/collectd/collectd.conf.d"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/logrotate.d"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/acme"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/backup"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/logs/backup"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/logs/backup" "${PLATFORM_DATA_DIR}/logs/updater" "${PLATFORM_DATA_DIR}/logs/tasks"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/update"
|
||||
|
||||
mkdir -p "${BOX_DATA_DIR}/appicons"
|
||||
@@ -91,12 +91,9 @@ setfacl -n -m u:${USER}:r /var/log/journal/*/system.journal
|
||||
echo "==> Creating config directory"
|
||||
mkdir -p "${CONFIG_DIR}"
|
||||
|
||||
# migration for cloudron.conf file. Can be removed after 3.3
|
||||
if [[ ! -d /etc/cloudron ]]; then
|
||||
echo "==> Migrating existing cloudron.conf to new location"
|
||||
mkdir -p /etc/cloudron
|
||||
cp "${CONFIG_DIR}/cloudron.conf" /etc/cloudron/cloudron.conf
|
||||
fi
|
||||
# remove old cloudron.conf. Can be removed after 3.4
|
||||
rm -f "${CONFIG_DIR}/cloudron.conf"
|
||||
$json -f /etc/cloudron/cloudron.conf -I -e "delete this.version" # remove the version field
|
||||
chown -R "${USER}" /etc/cloudron
|
||||
|
||||
echo "==> Setting up unbound"
|
||||
@@ -142,8 +139,8 @@ echo "==> Configuring logrotate"
|
||||
if ! grep -q "^include ${PLATFORM_DATA_DIR}/logrotate.d" /etc/logrotate.conf; then
|
||||
echo -e "\ninclude ${PLATFORM_DATA_DIR}/logrotate.d\n" >> /etc/logrotate.conf
|
||||
fi
|
||||
cp "${script_dir}/start/app-logrotate" "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate"
|
||||
chown root:root "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate"
|
||||
cp "${script_dir}/start/box-logrotate" "${script_dir}/start/app-logrotate" "${PLATFORM_DATA_DIR}/logrotate.d/"
|
||||
chown root:root "${PLATFORM_DATA_DIR}/logrotate.d/box-logrotate" "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate"
|
||||
|
||||
echo "==> Adding motd message for admins"
|
||||
cp "${script_dir}/start/cloudron-motd" /etc/update-motd.d/92-cloudron
|
||||
|
||||
9
setup/start/box-logrotate
Normal file
9
setup/start/box-logrotate
Normal file
@@ -0,0 +1,9 @@
|
||||
# logrotate config for box logs
|
||||
|
||||
/home/yellowtent/platformdata/logs/box.log {
|
||||
rotate 10
|
||||
size 10M
|
||||
# we never compress so we can simply tail the files
|
||||
nocompress
|
||||
copytruncate
|
||||
}
|
||||
@@ -162,7 +162,7 @@ server {
|
||||
# graphite paths (uncomment block below and visit /graphite/index.html)
|
||||
# remember to comment out the CSP policy as well to access the graphite dashboard
|
||||
# location ~ ^/(graphite|content|metrics|dashboard|render|browser|composer)/ {
|
||||
# proxy_pass http://127.0.0.1:8000;
|
||||
# proxy_pass http://127.0.0.1:8417;
|
||||
# client_max_body_size 1m;
|
||||
# }
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ Defaults !syslog
|
||||
Defaults!/home/yellowtent/box/src/scripts/rmvolume.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmvolume.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/rmaddon.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmaddon.sh
|
||||
Defaults!/home/yellowtent/box/src/scripts/rmaddondir.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmaddondir.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/reloadnginx.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reloadnginx.sh
|
||||
@@ -31,9 +31,15 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/authorized_keys
|
||||
Defaults!/home/yellowtent/box/src/scripts/configurelogrotate.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/configurelogrotate.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/backuptask.js env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD:SETENV: /home/yellowtent/box/src/backuptask.js
|
||||
Defaults!/home/yellowtent/box/src/scripts/backupupload.js env_keep="HOME BOX_ENV"
|
||||
Defaults!/home/yellowtent/box/src/scripts/backupupload.js closefrom_override
|
||||
yellowtent ALL=(root) NOPASSWD:SETENV: /home/yellowtent/box/src/scripts/backupupload.js
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/restart.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restart.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/restartdocker.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartdocker.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/restartunbound.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartunbound.sh
|
||||
|
||||
@@ -12,7 +12,8 @@ Wants=cloudron-resize-fs.service
|
||||
Type=idle
|
||||
WorkingDirectory=/home/yellowtent/box
|
||||
Restart=always
|
||||
ExecStart=/usr/bin/node --max_old_space_size=150 /home/yellowtent/box/box.js
|
||||
; Systemd does not append logs when logging to files, we spawn a shell first and exec to replace it after setting up the pipes
|
||||
ExecStart=/bin/sh -c 'echo "Logging to /home/yellowtent/platformdata/logs/box.log"; exec /usr/bin/node --max_old_space_size=150 /home/yellowtent/box/box.js >> /home/yellowtent/platformdata/logs/box.log 2>&1'
|
||||
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
|
||||
; kill apptask processes as well
|
||||
KillMode=control-group
|
||||
|
||||
649
src/addons.js
649
src/addons.js
@@ -1,8 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
startAddons: startAddons,
|
||||
updateAddonConfig: updateAddonConfig,
|
||||
AddonsError: AddonsError,
|
||||
|
||||
getServices: getServices,
|
||||
getService: getService,
|
||||
configureService: configureService,
|
||||
getServiceLogs: getServiceLogs,
|
||||
restartService: restartService,
|
||||
|
||||
startServices: startServices,
|
||||
updateServiceConfig: updateServiceConfig,
|
||||
|
||||
setupAddons: setupAddons,
|
||||
teardownAddons: teardownAddons,
|
||||
@@ -16,7 +24,11 @@ exports = module.exports = {
|
||||
|
||||
// exported for testing
|
||||
_setupOauth: setupOauth,
|
||||
_teardownOauth: teardownOauth
|
||||
_teardownOauth: teardownOauth,
|
||||
|
||||
SERVICE_STATUS_STARTING: 'starting', // container up, waiting for healthcheck
|
||||
SERVICE_STATUS_ACTIVE: 'active',
|
||||
SERVICE_STATUS_STOPPED: 'stopped'
|
||||
};
|
||||
|
||||
var accesscontrol = require('./accesscontrol.js'),
|
||||
@@ -31,11 +43,11 @@ var accesscontrol = require('./accesscontrol.js'),
|
||||
debug = require('debug')('box:addons'),
|
||||
docker = require('./docker.js'),
|
||||
dockerConnection = docker.connection,
|
||||
DockerError = docker.DockerError,
|
||||
fs = require('fs'),
|
||||
hat = require('./hat.js'),
|
||||
infra = require('./infra_version.js'),
|
||||
mail = require('./mail.js'),
|
||||
mailboxdb = require('./mailboxdb.js'),
|
||||
once = require('once'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
@@ -43,13 +55,41 @@ var accesscontrol = require('./accesscontrol.js'),
|
||||
rimraf = require('rimraf'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
settings = require('./settings.js'),
|
||||
shell = require('./shell.js'),
|
||||
spawn = require('child_process').spawn,
|
||||
split = require('split'),
|
||||
request = require('request'),
|
||||
util = require('util');
|
||||
|
||||
// http://dustinsenos.com/articles/customErrorsInNode
|
||||
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
|
||||
function AddonsError(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(AddonsError, Error);
|
||||
AddonsError.INTERNAL_ERROR = 'Internal Error';
|
||||
AddonsError.NOT_FOUND = 'Not Found';
|
||||
AddonsError.NOT_ACTIVE = 'Not Active';
|
||||
|
||||
const NOOP = function (app, options, callback) { return callback(); };
|
||||
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
const RMADDON_CMD = path.join(__dirname, 'scripts/rmaddon.sh');
|
||||
const RMADDONDIR_CMD = path.join(__dirname, 'scripts/rmaddondir.sh');
|
||||
|
||||
// setup can be called multiple times for the same app (configure crash restart) and existing data must not be lost
|
||||
// teardown is destructive. app data stored with the addon is lost
|
||||
@@ -59,84 +99,117 @@ var KNOWN_ADDONS = {
|
||||
teardown: teardownEmail,
|
||||
backup: NOOP,
|
||||
restore: setupEmail,
|
||||
clear: NOOP
|
||||
clear: NOOP,
|
||||
},
|
||||
ldap: {
|
||||
setup: setupLdap,
|
||||
teardown: teardownLdap,
|
||||
backup: NOOP,
|
||||
restore: setupLdap,
|
||||
clear: NOOP
|
||||
clear: NOOP,
|
||||
},
|
||||
localstorage: {
|
||||
setup: setupLocalStorage, // docker creates the directory for us
|
||||
teardown: teardownLocalStorage,
|
||||
backup: NOOP, // no backup because it's already inside app data
|
||||
restore: NOOP,
|
||||
clear: clearLocalStorage
|
||||
clear: clearLocalStorage,
|
||||
},
|
||||
mongodb: {
|
||||
setup: setupMongoDb,
|
||||
teardown: teardownMongoDb,
|
||||
backup: backupMongoDb,
|
||||
restore: restoreMongoDb,
|
||||
clear: clearMongodb
|
||||
clear: clearMongodb,
|
||||
},
|
||||
mysql: {
|
||||
setup: setupMySql,
|
||||
teardown: teardownMySql,
|
||||
backup: backupMySql,
|
||||
restore: restoreMySql,
|
||||
clear: clearMySql
|
||||
clear: clearMySql,
|
||||
},
|
||||
oauth: {
|
||||
setup: setupOauth,
|
||||
teardown: teardownOauth,
|
||||
backup: NOOP,
|
||||
restore: setupOauth,
|
||||
clear: NOOP
|
||||
clear: NOOP,
|
||||
},
|
||||
postgresql: {
|
||||
setup: setupPostgreSql,
|
||||
teardown: teardownPostgreSql,
|
||||
backup: backupPostgreSql,
|
||||
restore: restorePostgreSql,
|
||||
clear: clearPostgreSql
|
||||
clear: clearPostgreSql,
|
||||
},
|
||||
recvmail: {
|
||||
setup: setupRecvMail,
|
||||
teardown: teardownRecvMail,
|
||||
backup: NOOP,
|
||||
restore: setupRecvMail,
|
||||
clear: NOOP
|
||||
clear: NOOP,
|
||||
},
|
||||
redis: {
|
||||
setup: setupRedis,
|
||||
teardown: teardownRedis,
|
||||
backup: backupRedis,
|
||||
restore: restoreRedis,
|
||||
clear: clearRedis
|
||||
clear: clearRedis,
|
||||
},
|
||||
sendmail: {
|
||||
setup: setupSendMail,
|
||||
teardown: teardownSendMail,
|
||||
backup: NOOP,
|
||||
restore: setupSendMail,
|
||||
clear: NOOP
|
||||
clear: NOOP,
|
||||
},
|
||||
scheduler: {
|
||||
setup: NOOP,
|
||||
teardown: NOOP,
|
||||
backup: NOOP,
|
||||
restore: NOOP,
|
||||
clear: NOOP
|
||||
clear: NOOP,
|
||||
},
|
||||
docker: {
|
||||
setup: NOOP,
|
||||
teardown: NOOP,
|
||||
backup: NOOP,
|
||||
restore: NOOP,
|
||||
clear: NOOP
|
||||
clear: NOOP,
|
||||
}
|
||||
};
|
||||
|
||||
const KNOWN_SERVICES = {
|
||||
mail: {
|
||||
status: containerStatus.bind(null, 'mail', 'CLOUDRON_MAIL_TOKEN'),
|
||||
restart: restartContainer.bind(null, 'mail'),
|
||||
defaultMemoryLimit: Math.max((1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 128, 256) * 1024 * 1024
|
||||
},
|
||||
mongodb: {
|
||||
status: containerStatus.bind(null, 'mongodb', 'CLOUDRON_MONGODB_TOKEN'),
|
||||
restart: restartContainer.bind(null, 'mongodb'),
|
||||
defaultMemoryLimit: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 200 * 1024 * 1024
|
||||
},
|
||||
mysql: {
|
||||
status: containerStatus.bind(null, 'mysql', 'CLOUDRON_MYSQL_TOKEN'),
|
||||
restart: restartContainer.bind(null, 'mysql'),
|
||||
defaultMemoryLimit: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256 * 1024 * 1024
|
||||
},
|
||||
postgresql: {
|
||||
status: containerStatus.bind(null, 'postgresql', 'CLOUDRON_POSTGRESQL_TOKEN'),
|
||||
restart: restartContainer.bind(null, 'postgresql'),
|
||||
defaultMemoryLimit: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256 * 1024 * 1024
|
||||
},
|
||||
docker: {
|
||||
status: statusDocker,
|
||||
restart: restartDocker,
|
||||
defaultMemoryLimit: 0
|
||||
},
|
||||
unbound: {
|
||||
status: statusUnbound,
|
||||
restart: restartUnbound,
|
||||
defaultMemoryLimit: 0
|
||||
}
|
||||
};
|
||||
|
||||
@@ -170,41 +243,233 @@ function dumpPath(addon, appId) {
|
||||
}
|
||||
}
|
||||
|
||||
function getAddonDetails(containerName, tokenEnvName, callback) {
|
||||
function restartContainer(serviceName, callback) {
|
||||
assert.strictEqual(typeof serviceName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
assert(KNOWN_SERVICES[serviceName], `Unknown service ${serviceName}`);
|
||||
|
||||
docker.stopContainer(serviceName, function (error) {
|
||||
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
|
||||
|
||||
docker.startContainer(serviceName, function (error) {
|
||||
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function containerStatus(addonName, addonTokenName, callback) {
|
||||
assert.strictEqual(typeof addonName, 'string');
|
||||
assert.strictEqual(typeof addonTokenName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getServiceDetails(addonName, addonTokenName, function (error, addonDetails) {
|
||||
if (error && error.reason === AddonsError.NOT_ACTIVE) return callback(null, { status: exports.SERVICE_STATUS_STOPPED });
|
||||
if (error) return callback(error);
|
||||
|
||||
request.get(`https://${addonDetails.ip}:3000/healthcheck?access_token=${addonDetails.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
|
||||
if (error) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for ${addonName}: ${error.message}` });
|
||||
if (response.statusCode !== 200 || !response.body.status) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for ${addonName}. Status code: ${response.statusCode} message: ${response.body.message}` });
|
||||
|
||||
docker.memoryUsage(addonName, function (error, result) {
|
||||
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
|
||||
|
||||
var tmp = {
|
||||
status: addonDetails.state.Running ? exports.SERVICE_STATUS_ACTIVE : exports.SERVICE_STATUS_STOPPED,
|
||||
memoryUsed: result.memory_stats.usage,
|
||||
memoryPercent: parseInt(100 * result.memory_stats.usage / result.memory_stats.limit)
|
||||
};
|
||||
|
||||
callback(null, tmp);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getServices(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let services = Object.keys(KNOWN_SERVICES);
|
||||
|
||||
callback(null, services);
|
||||
}
|
||||
|
||||
function getService(serviceName, callback) {
|
||||
assert.strictEqual(typeof serviceName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!KNOWN_SERVICES[serviceName]) return callback(new AddonsError(AddonsError.NOT_FOUND));
|
||||
|
||||
var tmp = {
|
||||
name: serviceName,
|
||||
status: null,
|
||||
config: {
|
||||
// If a property is not set then we cannot change it through the api, see below
|
||||
// memory: 0,
|
||||
// memorySwap: 0
|
||||
}
|
||||
};
|
||||
|
||||
settings.getPlatformConfig(function (error, platformConfig) {
|
||||
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (platformConfig[serviceName] && platformConfig[serviceName].memory && platformConfig[serviceName].memorySwap) {
|
||||
tmp.config.memory = platformConfig[serviceName].memory;
|
||||
tmp.config.memorySwap = platformConfig[serviceName].memorySwap;
|
||||
} else if (KNOWN_SERVICES[serviceName].defaultMemoryLimit) {
|
||||
tmp.config.memory = KNOWN_SERVICES[serviceName].defaultMemoryLimit;
|
||||
tmp.config.memorySwap = tmp.config.memory * 2;
|
||||
}
|
||||
|
||||
KNOWN_SERVICES[serviceName].status(function (error, result) {
|
||||
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
|
||||
|
||||
tmp.status = result.status;
|
||||
tmp.memoryUsed = result.memoryUsed;
|
||||
tmp.memoryPercent = result.memoryPercent;
|
||||
tmp.error = result.error || null;
|
||||
|
||||
callback(null, tmp);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function configureService(serviceName, data, callback) {
|
||||
assert.strictEqual(typeof serviceName, 'string');
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!KNOWN_SERVICES[serviceName]) return callback(new AddonsError(AddonsError.NOT_FOUND));
|
||||
|
||||
settings.getPlatformConfig(function (error, platformConfig) {
|
||||
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (!platformConfig[serviceName]) platformConfig[serviceName] = {};
|
||||
|
||||
// if not specified we clear the entry and use defaults
|
||||
if (!data.memory || !data.memorySwap) {
|
||||
delete platformConfig[serviceName];
|
||||
} else {
|
||||
platformConfig[serviceName].memory = data.memory;
|
||||
platformConfig[serviceName].memorySwap = data.memorySwap;
|
||||
}
|
||||
|
||||
settings.setPlatformConfig(platformConfig, function (error) {
|
||||
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getServiceLogs(serviceName, options, callback) {
|
||||
assert.strictEqual(typeof serviceName, 'string');
|
||||
assert(options && typeof options === 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!KNOWN_SERVICES[serviceName]) return callback(new AddonsError(AddonsError.NOT_FOUND));
|
||||
|
||||
debug(`Getting logs for ${serviceName}`);
|
||||
|
||||
var lines = options.lines || 100,
|
||||
format = options.format || 'json',
|
||||
follow = !!options.follow;
|
||||
|
||||
assert.strictEqual(typeof lines, 'number');
|
||||
assert.strictEqual(typeof format, 'string');
|
||||
|
||||
var cmd;
|
||||
var args = [ '--lines=' + lines ];
|
||||
|
||||
// docker and unbound use journald
|
||||
if (serviceName === 'docker' || serviceName === 'unbound') {
|
||||
cmd = 'journalctl';
|
||||
|
||||
args.push(`--unit=${serviceName}`);
|
||||
args.push('--no-pager');
|
||||
args.push('--output=short-iso');
|
||||
|
||||
if (follow) args.push('--follow');
|
||||
} else {
|
||||
cmd = '/usr/bin/tail';
|
||||
|
||||
if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs
|
||||
args.push(path.join(paths.LOG_DIR, serviceName, 'app.log'));
|
||||
}
|
||||
|
||||
var cp = spawn(cmd, args);
|
||||
|
||||
var transformStream = split(function mapper(line) {
|
||||
if (format !== 'json') return line + '\n';
|
||||
|
||||
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
|
||||
var timestamp = (new Date(data[0])).getTime();
|
||||
if (isNaN(timestamp)) timestamp = 0;
|
||||
var message = line.slice(data[0].length+1);
|
||||
|
||||
// ignore faulty empty logs
|
||||
if (!timestamp && !message) return;
|
||||
|
||||
return JSON.stringify({
|
||||
realtimeTimestamp: timestamp * 1000,
|
||||
message: message,
|
||||
source: serviceName
|
||||
}) + '\n';
|
||||
});
|
||||
|
||||
transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process
|
||||
|
||||
cp.stdout.pipe(transformStream);
|
||||
|
||||
callback(null, transformStream);
|
||||
}
|
||||
|
||||
function restartService(serviceName, callback) {
|
||||
assert.strictEqual(typeof serviceName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!KNOWN_SERVICES[serviceName]) return callback(new AddonsError(AddonsError.NOT_FOUND));
|
||||
|
||||
KNOWN_SERVICES[serviceName].restart(callback);
|
||||
}
|
||||
|
||||
function getServiceDetails(containerName, tokenEnvName, callback) {
|
||||
assert.strictEqual(typeof containerName, 'string');
|
||||
assert.strictEqual(typeof tokenEnvName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var container = dockerConnection.getContainer(containerName);
|
||||
container.inspect(function (error, result) {
|
||||
if (error) return callback(new Error(`Error inspecting ${containerName} container: ` + error));
|
||||
docker.inspect(containerName, function (error, result) {
|
||||
if (error && error.reason === DockerError.NOT_FOUND) return callback(new AddonsError(AddonsError.NOT_ACTIVE, error));
|
||||
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
|
||||
|
||||
const ip = safe.query(result, 'NetworkSettings.Networks.cloudron.IPAddress', null);
|
||||
if (!ip) return callback(new Error(`Error getting ${containerName} container ip`));
|
||||
if (!ip) return callback(new AddonsError(AddonsError.NOT_ACTIVE, `Error getting ${containerName} container ip`));
|
||||
|
||||
// extract the cloudron token for auth
|
||||
const env = safe.query(result, 'Config.Env', null);
|
||||
if (!env) return callback(new Error(`Error getting ${containerName} env`));
|
||||
if (!env) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, `Error getting ${containerName} env`));
|
||||
const tmp = env.find(function (e) { return e.indexOf(tokenEnvName) === 0; });
|
||||
if (!tmp) return callback(new Error(`Error getting ${containerName} cloudron token env var`));
|
||||
if (!tmp) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, `Error getting ${containerName} cloudron token env var`));
|
||||
const token = tmp.slice(tokenEnvName.length + 1); // +1 for the = sign
|
||||
if (!token) return callback(new Error(`Error getting ${containerName} cloudron token`));
|
||||
if (!token) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, `Error getting ${containerName} cloudron token`));
|
||||
|
||||
callback(null, { ip: ip, token: token });
|
||||
callback(null, { ip: ip, token: token, state: result.State });
|
||||
});
|
||||
}
|
||||
|
||||
function waitForAddon(containerName, tokenEnvName, callback) {
|
||||
function waitForService(containerName, tokenEnvName, callback) {
|
||||
assert.strictEqual(typeof containerName, 'string');
|
||||
assert.strictEqual(typeof tokenEnvName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`Waiting for ${containerName}`);
|
||||
|
||||
getAddonDetails(containerName, tokenEnvName, function (error, result) {
|
||||
getServiceDetails(containerName, tokenEnvName, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
|
||||
async.retry({ times: 10, interval: 15000 }, function (retryCallback) {
|
||||
request.get(`https://${result.ip}:3000/healthcheck?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
|
||||
if (error) return retryCallback(new Error(`Error waiting for ${containerName}: ${error.message}`));
|
||||
if (response.statusCode !== 200 || !response.body.status) return retryCallback(new Error(`Error waiting for ${containerName}. Status code: ${response.statusCode} message: ${response.body.message}`));
|
||||
@@ -343,37 +608,29 @@ function importDatabase(addon, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function updateAddonConfig(platformConfig, callback) {
|
||||
function updateServiceConfig(platformConfig, callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
// TODO: maybe derive these defaults based on how many apps are using them
|
||||
const defaultMemoryLimits = {
|
||||
mysql: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256 * 1024 * 1024,
|
||||
mongodb: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 200 * 1024 * 1024,
|
||||
postgresql: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256 * 1024 * 1024,
|
||||
mail: Math.max((1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 128, 256) * 1024 * 1024
|
||||
};
|
||||
|
||||
debug('updateAddonConfig: %j', platformConfig);
|
||||
debug('updateServiceConfig: %j', platformConfig);
|
||||
|
||||
// TODO: this should possibly also rollback memory to default
|
||||
async.eachSeries([ 'mysql', 'postgresql', 'mail', 'mongodb' ], function iterator(containerName, iteratorCallback) {
|
||||
const containerConfig = platformConfig[containerName];
|
||||
async.eachSeries([ 'mysql', 'postgresql', 'mail', 'mongodb' ], function iterator(serviceName, iteratorCallback) {
|
||||
const containerConfig = platformConfig[serviceName];
|
||||
let memory, memorySwap;
|
||||
if (containerConfig && containerConfig.memory && containerConfig.memorySwap) {
|
||||
memory = containerConfig.memory;
|
||||
memorySwap = containerConfig.memorySwap;
|
||||
} else {
|
||||
memory = defaultMemoryLimits[containerName];
|
||||
memory = KNOWN_SERVICES[serviceName].defaultMemoryLimit;
|
||||
memorySwap = memory * 2;
|
||||
}
|
||||
|
||||
const args = `update --memory ${memory} --memory-swap ${memorySwap} ${containerName}`.split(' ');
|
||||
shell.exec(`update${containerName}`, '/usr/bin/docker', args, { }, iteratorCallback);
|
||||
const args = `update --memory ${memory} --memory-swap ${memorySwap} ${serviceName}`.split(' ');
|
||||
shell.spawn(`update${serviceName}`, '/usr/bin/docker', args, { }, iteratorCallback);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function startAddons(existingInfra, callback) {
|
||||
function startServices(existingInfra, callback) {
|
||||
assert.strictEqual(typeof existingInfra, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -381,7 +638,7 @@ function startAddons(existingInfra, callback) {
|
||||
|
||||
// always start addons on any infra change, regardless of minor or major update
|
||||
if (existingInfra.version !== infra.version) {
|
||||
debug(`startAddons: ${existingInfra.version} -> ${infra.version}. starting all addons`);
|
||||
debug(`startServices: ${existingInfra.version} -> ${infra.version}. starting all services`);
|
||||
startFuncs.push(
|
||||
startMysql.bind(null, existingInfra),
|
||||
startPostgresql.bind(null, existingInfra),
|
||||
@@ -397,7 +654,7 @@ function startAddons(existingInfra, callback) {
|
||||
if (infra.images.mail.tag !== existingInfra.images.mail.tag) startFuncs.push(mail.startMail);
|
||||
if (infra.images.redis.tag !== existingInfra.images.redis.tag) startFuncs.push(startRedis.bind(null, existingInfra));
|
||||
|
||||
debug('startAddons: existing infra. incremental addon create %j', startFuncs.map(function (f) { return f.name; }));
|
||||
debug('startServices: existing infra. incremental service create %j', startFuncs.map(function (f) { return f.name; }));
|
||||
}
|
||||
|
||||
async.series(startFuncs, callback);
|
||||
@@ -621,23 +878,17 @@ function setupSendMail(app, options, callback) {
|
||||
|
||||
var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length
|
||||
|
||||
mailboxdb.getByOwnerId(app.id, function (error, results) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
|
||||
|
||||
var env = [
|
||||
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_SMTP_PORT', value: '2525' },
|
||||
{ name: 'MAIL_SMTPS_PORT', value: '2465' },
|
||||
{ name: 'MAIL_SMTP_USERNAME', value: mailbox.name + '@' + app.domain },
|
||||
{ name: 'MAIL_SMTP_PASSWORD', value: password },
|
||||
{ name: 'MAIL_FROM', value: mailbox.name + '@' + app.domain },
|
||||
{ name: 'MAIL_DOMAIN', value: app.domain }
|
||||
];
|
||||
debugApp(app, 'Setting sendmail addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
|
||||
});
|
||||
var env = [
|
||||
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_SMTP_PORT', value: '2525' },
|
||||
{ name: 'MAIL_SMTPS_PORT', value: '2465' },
|
||||
{ name: 'MAIL_SMTP_USERNAME', value: app.mailboxName + '@' + app.domain },
|
||||
{ name: 'MAIL_SMTP_PASSWORD', value: password },
|
||||
{ name: 'MAIL_FROM', value: app.mailboxName + '@' + app.domain },
|
||||
{ name: 'MAIL_DOMAIN', value: app.domain }
|
||||
];
|
||||
debugApp(app, 'Setting sendmail addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -663,23 +914,17 @@ function setupRecvMail(app, options, callback) {
|
||||
|
||||
var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length
|
||||
|
||||
mailboxdb.getByOwnerId(app.id, function (error, results) {
|
||||
if (error) return callback(error);
|
||||
var env = [
|
||||
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_IMAP_PORT', value: '9993' },
|
||||
{ name: 'MAIL_IMAP_USERNAME', value: app.mailboxName + '@' + app.domain },
|
||||
{ name: 'MAIL_IMAP_PASSWORD', value: password },
|
||||
{ name: 'MAIL_TO', value: app.mailboxName + '@' + app.domain },
|
||||
{ name: 'MAIL_DOMAIN', value: app.domain }
|
||||
];
|
||||
|
||||
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
|
||||
|
||||
var env = [
|
||||
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_IMAP_PORT', value: '9993' },
|
||||
{ name: 'MAIL_IMAP_USERNAME', value: mailbox.name + '@' + app.domain },
|
||||
{ name: 'MAIL_IMAP_PASSWORD', value: password },
|
||||
{ name: 'MAIL_TO', value: mailbox.name + '@' + app.domain },
|
||||
{ name: 'MAIL_DOMAIN', value: app.domain }
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting sendmail addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'recvmail', env, callback);
|
||||
});
|
||||
debugApp(app, 'Setting sendmail addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'recvmail', env, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -713,36 +958,40 @@ function startMysql(existingInfra, callback) {
|
||||
|
||||
const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.mysql.tag, tag);
|
||||
|
||||
if (upgrading) {
|
||||
debug('startMysql: mysql will be upgraded');
|
||||
shell.sudoSync('startMysql', `${RMADDON_CMD} mysql`);
|
||||
}
|
||||
if (upgrading) debug('startMysql: mysql will be upgraded');
|
||||
const upgradeFunc = upgrading ? shell.sudo.bind(null, 'startMysql', [ RMADDONDIR_CMD, 'mysql' ], {}) : (next) => next();
|
||||
|
||||
const cmd = `docker run --restart=always -d --name="mysql" \
|
||||
--net cloudron \
|
||||
--net-alias mysql \
|
||||
--log-driver syslog \
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag=mysql \
|
||||
-m ${memoryLimit}m \
|
||||
--memory-swap ${memoryLimit * 2}m \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
-e CLOUDRON_MYSQL_TOKEN=${cloudronToken} \
|
||||
-e CLOUDRON_MYSQL_ROOT_HOST=172.18.0.1 \
|
||||
-e CLOUDRON_MYSQL_ROOT_PASSWORD=${rootPassword} \
|
||||
-v "${dataDir}/mysql:/var/lib/mysql" \
|
||||
--label isCloudronManaged=true \
|
||||
--read-only -v /tmp -v /run "${tag}"`;
|
||||
|
||||
shell.execSync('startMysql', cmd);
|
||||
|
||||
waitForAddon('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error) {
|
||||
upgradeFunc(function (error) {
|
||||
if (error) return callback(error);
|
||||
if (!upgrading) return callback(null);
|
||||
|
||||
importDatabase('mysql', callback);
|
||||
const cmd = `docker run --restart=always -d --name="mysql" \
|
||||
--net cloudron \
|
||||
--net-alias mysql \
|
||||
--log-driver syslog \
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag=mysql \
|
||||
-m ${memoryLimit}m \
|
||||
--memory-swap ${memoryLimit * 2}m \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
-e CLOUDRON_MYSQL_TOKEN=${cloudronToken} \
|
||||
-e CLOUDRON_MYSQL_ROOT_HOST=172.18.0.1 \
|
||||
-e CLOUDRON_MYSQL_ROOT_PASSWORD=${rootPassword} \
|
||||
-v "${dataDir}/mysql:/var/lib/mysql" \
|
||||
--label isCloudronManaged=true \
|
||||
--read-only -v /tmp -v /run "${tag}"`;
|
||||
|
||||
shell.exec('startMysql', cmd, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
waitForService('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error) {
|
||||
if (error) return callback(error);
|
||||
if (!upgrading) return callback(null);
|
||||
|
||||
importDatabase('mysql', callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -765,7 +1014,7 @@ function setupMySql(app, options, callback) {
|
||||
password: error ? hat(4 * 48) : existingPassword // see box#362 for password length
|
||||
};
|
||||
|
||||
getAddonDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
|
||||
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
request.post(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) {
|
||||
@@ -802,7 +1051,7 @@ function clearMySql(app, options, callback) {
|
||||
|
||||
const database = mysqlDatabaseName(app.id);
|
||||
|
||||
getAddonDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
|
||||
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
request.post(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}/clear?access_token=${result.token}`, { rejectUnauthorized: false }, function (error, response) {
|
||||
@@ -821,7 +1070,7 @@ function teardownMySql(app, options, callback) {
|
||||
const database = mysqlDatabaseName(app.id);
|
||||
const username = database;
|
||||
|
||||
getAddonDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
|
||||
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
request.delete(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}?access_token=${result.token}&username=${username}`, { rejectUnauthorized: false }, function (error, response) {
|
||||
@@ -868,7 +1117,7 @@ function backupMySql(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Backing up mysql');
|
||||
|
||||
getAddonDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
|
||||
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const url = `https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}/backup?access_token=${result.token}`;
|
||||
@@ -887,7 +1136,7 @@ function restoreMySql(app, options, callback) {
|
||||
|
||||
callback = once(callback); // protect from multiple returns with streams
|
||||
|
||||
getAddonDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
|
||||
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var input = fs.createReadStream(dumpPath('mysql', app.id));
|
||||
@@ -921,35 +1170,39 @@ function startPostgresql(existingInfra, callback) {
|
||||
|
||||
const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.postgresql.tag, tag);
|
||||
|
||||
if (upgrading) {
|
||||
debug('startPostgresql: postgresql will be upgraded');
|
||||
shell.sudoSync('startPostgresql', `${RMADDON_CMD} postgresql`);
|
||||
}
|
||||
if (upgrading) debug('startPostgresql: postgresql will be upgraded');
|
||||
const upgradeFunc = upgrading ? shell.sudo.bind(null, 'startPostgresql', [ RMADDONDIR_CMD, 'postgresql' ], {}) : (next) => next();
|
||||
|
||||
const cmd = `docker run --restart=always -d --name="postgresql" \
|
||||
--net cloudron \
|
||||
--net-alias postgresql \
|
||||
--log-driver syslog \
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag=postgresql \
|
||||
-m ${memoryLimit}m \
|
||||
--memory-swap ${memoryLimit * 2}m \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
-e CLOUDRON_POSTGRESQL_ROOT_PASSWORD="${rootPassword}" \
|
||||
-e CLOUDRON_POSTGRESQL_TOKEN="${cloudronToken}" \
|
||||
-v "${dataDir}/postgresql:/var/lib/postgresql" \
|
||||
--label isCloudronManaged=true \
|
||||
--read-only -v /tmp -v /run "${tag}"`;
|
||||
|
||||
shell.execSync('startPostgresql', cmd);
|
||||
|
||||
waitForAddon('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error) {
|
||||
upgradeFunc(function (error) {
|
||||
if (error) return callback(error);
|
||||
if (!upgrading) return callback(null);
|
||||
|
||||
importDatabase('postgresql', callback);
|
||||
const cmd = `docker run --restart=always -d --name="postgresql" \
|
||||
--net cloudron \
|
||||
--net-alias postgresql \
|
||||
--log-driver syslog \
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag=postgresql \
|
||||
-m ${memoryLimit}m \
|
||||
--memory-swap ${memoryLimit * 2}m \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
-e CLOUDRON_POSTGRESQL_ROOT_PASSWORD="${rootPassword}" \
|
||||
-e CLOUDRON_POSTGRESQL_TOKEN="${cloudronToken}" \
|
||||
-v "${dataDir}/postgresql:/var/lib/postgresql" \
|
||||
--label isCloudronManaged=true \
|
||||
--read-only -v /tmp -v /run "${tag}"`;
|
||||
|
||||
shell.exec('startPostgresql', cmd, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
waitForService('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error) {
|
||||
if (error) return callback(error);
|
||||
if (!upgrading) return callback(null);
|
||||
|
||||
importDatabase('postgresql', callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -971,7 +1224,7 @@ function setupPostgreSql(app, options, callback) {
|
||||
password: error ? hat(4 * 128) : existingPassword
|
||||
};
|
||||
|
||||
getAddonDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
|
||||
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
request.post(`https://${result.ip}:3000/databases?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) {
|
||||
@@ -1003,7 +1256,7 @@ function clearPostgreSql(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Clearing postgresql');
|
||||
|
||||
getAddonDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
|
||||
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
request.post(`https://${result.ip}:3000/databases/${database}/clear?access_token=${result.token}&username=${username}`, { rejectUnauthorized: false }, function (error, response) {
|
||||
@@ -1022,7 +1275,7 @@ function teardownPostgreSql(app, options, callback) {
|
||||
|
||||
const { database, username } = postgreSqlNames(app.id);
|
||||
|
||||
getAddonDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
|
||||
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
request.delete(`https://${result.ip}:3000/databases/${database}?access_token=${result.token}&username=${username}`, { rejectUnauthorized: false }, function (error, response) {
|
||||
@@ -1043,7 +1296,7 @@ function backupPostgreSql(app, options, callback) {
|
||||
|
||||
const { database } = postgreSqlNames(app.id);
|
||||
|
||||
getAddonDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
|
||||
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const url = `https://${result.ip}:3000/databases/${database}/backup?access_token=${result.token}`;
|
||||
@@ -1062,7 +1315,7 @@ function restorePostgreSql(app, options, callback) {
|
||||
|
||||
callback = once(callback); // protect from multiple returns with streams
|
||||
|
||||
getAddonDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
|
||||
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var input = fs.createReadStream(dumpPath('postgresql', app.id));
|
||||
@@ -1091,35 +1344,39 @@ function startMongodb(existingInfra, callback) {
|
||||
|
||||
const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.mongodb.tag, tag);
|
||||
|
||||
if (upgrading) {
|
||||
debug('startMongodb: mongodb will be upgraded');
|
||||
shell.sudoSync('startMongodb', `${RMADDON_CMD} mongodb`);
|
||||
}
|
||||
if (upgrading) debug('startMongodb: mongodb will be upgraded');
|
||||
const upgradeFunc = upgrading ? shell.sudo.bind(null, 'startMongodb', [ RMADDONDIR_CMD, 'mongodb' ], {}) : (next) => next();
|
||||
|
||||
const cmd = `docker run --restart=always -d --name="mongodb" \
|
||||
--net cloudron \
|
||||
--net-alias mongodb \
|
||||
--log-driver syslog \
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag=mongodb \
|
||||
-m ${memoryLimit}m \
|
||||
--memory-swap ${memoryLimit * 2}m \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
-e CLOUDRON_MONGODB_ROOT_PASSWORD="${rootPassword}" \
|
||||
-e CLOUDRON_MONGODB_TOKEN="${cloudronToken}" \
|
||||
-v "${dataDir}/mongodb:/var/lib/mongodb" \
|
||||
--label isCloudronManaged=true \
|
||||
--read-only -v /tmp -v /run "${tag}"`;
|
||||
|
||||
shell.execSync('startMongodb', cmd);
|
||||
|
||||
waitForAddon('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error) {
|
||||
upgradeFunc(function (error) {
|
||||
if (error) return callback(error);
|
||||
if (!upgrading) return callback(null);
|
||||
|
||||
importDatabase('mongodb', callback);
|
||||
const cmd = `docker run --restart=always -d --name="mongodb" \
|
||||
--net cloudron \
|
||||
--net-alias mongodb \
|
||||
--log-driver syslog \
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag=mongodb \
|
||||
-m ${memoryLimit}m \
|
||||
--memory-swap ${memoryLimit * 2}m \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
-e CLOUDRON_MONGODB_ROOT_PASSWORD="${rootPassword}" \
|
||||
-e CLOUDRON_MONGODB_TOKEN="${cloudronToken}" \
|
||||
-v "${dataDir}/mongodb:/var/lib/mongodb" \
|
||||
--label isCloudronManaged=true \
|
||||
--read-only -v /tmp -v /run "${tag}"`;
|
||||
|
||||
shell.exec('startMongodb', cmd, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
waitForService('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error) {
|
||||
if (error) return callback(error);
|
||||
if (!upgrading) return callback(null);
|
||||
|
||||
importDatabase('mongodb', callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1139,7 +1396,7 @@ function setupMongoDb(app, options, callback) {
|
||||
password: error ? hat(4 * 128) : existingPassword
|
||||
};
|
||||
|
||||
getAddonDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
||||
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
request.post(`https://${result.ip}:3000/databases?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) {
|
||||
@@ -1169,7 +1426,7 @@ function clearMongodb(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Clearing mongodb');
|
||||
|
||||
getAddonDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
||||
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
request.post(`https://${result.ip}:3000/databases/${app.id}/clear?access_token=${result.token}`, { rejectUnauthorized: false }, function (error, response) {
|
||||
@@ -1188,7 +1445,7 @@ function teardownMongoDb(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Tearing down mongodb');
|
||||
|
||||
getAddonDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
||||
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
request.delete(`https://${result.ip}:3000/databases/${app.id}?access_token=${result.token}`, { rejectUnauthorized: false }, function (error, response) {
|
||||
@@ -1207,7 +1464,7 @@ function backupMongoDb(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Backing up mongodb');
|
||||
|
||||
getAddonDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
||||
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const url = `https://${result.ip}:3000/databases/${app.id}/backup?access_token=${result.token}`;
|
||||
@@ -1224,7 +1481,7 @@ function restoreMongoDb(app, options, callback) {
|
||||
|
||||
debugApp(app, 'restoreMongoDb');
|
||||
|
||||
getAddonDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
||||
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const readStream = fs.createReadStream(dumpPath('mongodb', app.id));
|
||||
@@ -1313,9 +1570,9 @@ function setupRedis(app, options, callback) {
|
||||
];
|
||||
|
||||
async.series([
|
||||
shell.execSync.bind(null, 'startRedis', cmd),
|
||||
shell.exec.bind(null, 'startRedis', cmd),
|
||||
appdb.setAddonConfig.bind(null, app.id, 'redis', env),
|
||||
waitForAddon.bind(null, 'redis-' + app.id, 'CLOUDRON_REDIS_TOKEN')
|
||||
waitForService.bind(null, 'redis-' + app.id, 'CLOUDRON_REDIS_TOKEN')
|
||||
], function (error) {
|
||||
if (error) debug('Error setting up redis: ', error);
|
||||
callback(error);
|
||||
@@ -1331,7 +1588,7 @@ function clearRedis(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Clearing redis');
|
||||
|
||||
getAddonDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
|
||||
getServiceDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
request.post(`https://${result.ip}:3000/clear?access_token=${result.token}`, { rejectUnauthorized: false }, function (error, response) {
|
||||
@@ -1358,7 +1615,7 @@ function teardownRedis(app, options, callback) {
|
||||
container.remove(removeOptions, function (error) {
|
||||
if (error && error.statusCode !== 404) return callback(new Error('Error removing container:' + error));
|
||||
|
||||
shell.sudo('removeVolume', [ RMADDON_CMD, 'redis', app.id ], function (error) {
|
||||
shell.sudo('removeVolume', [ RMADDONDIR_CMD, 'redis', app.id ], {}, function (error) {
|
||||
if (error) return callback(new Error('Error removing redis data:' + error));
|
||||
|
||||
rimraf(path.join(paths.LOG_DIR, `redis-${app.id}`), function (error) {
|
||||
@@ -1377,7 +1634,7 @@ function backupRedis(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Backing up redis');
|
||||
|
||||
getAddonDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
|
||||
getServiceDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const url = `https://${result.ip}:3000/backup?access_token=${result.token}`;
|
||||
@@ -1392,7 +1649,7 @@ function restoreRedis(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Restoring redis');
|
||||
|
||||
getAddonDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
|
||||
getServiceDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
let input;
|
||||
@@ -1414,3 +1671,35 @@ function restoreRedis(app, options, callback) {
|
||||
input.pipe(restoreReq);
|
||||
});
|
||||
}
|
||||
|
||||
function statusDocker(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
docker.ping(function (error) {
|
||||
callback(null, { status: error ? exports.SERVICE_STATUS_STOPPED: exports.SERVICE_STATUS_ACTIVE });
|
||||
});
|
||||
}
|
||||
|
||||
function restartDocker(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
shell.sudo('restartdocker', [ path.join(__dirname, 'scripts/restartdocker.sh') ], {}, NOOP_CALLBACK);
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function statusUnbound(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
shell.exec('statusUnbound', 'systemctl is-active unbound', function (error) {
|
||||
callback(null, { status: error ? exports.SERVICE_STATUS_STOPPED : exports.SERVICE_STATUS_ACTIVE });
|
||||
});
|
||||
}
|
||||
|
||||
function restartUnbound(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
shell.sudo('restartunbound', [ path.join(__dirname, 'scripts/restartunbound.sh') ], {}, NOOP_CALLBACK);
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
45
src/appdb.js
45
src/appdb.js
@@ -18,6 +18,7 @@ exports = module.exports = {
|
||||
getAddonConfigByName: getAddonConfigByName,
|
||||
unsetAddonConfig: unsetAddonConfig,
|
||||
unsetAddonConfigByAppId: unsetAddonConfigByAppId,
|
||||
getAppIdByAddonConfigValue: getAppIdByAddonConfigValue,
|
||||
|
||||
setHealth: setHealth,
|
||||
setInstallationCommand: setInstallationCommand,
|
||||
@@ -61,7 +62,6 @@ var assert = require('assert'),
|
||||
async = require('async'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror'),
|
||||
mailboxdb = require('./mailboxdb.js'),
|
||||
safe = require('safetydance'),
|
||||
util = require('util');
|
||||
|
||||
@@ -69,7 +69,7 @@ var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationSta
|
||||
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'subdomains.subdomain AS location', 'subdomains.domain',
|
||||
'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit',
|
||||
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
|
||||
'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.ts' ].join(',');
|
||||
'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.mailboxName', 'apps.enableAutomaticUpdate', 'apps.ts' ].join(',');
|
||||
|
||||
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(',');
|
||||
|
||||
@@ -120,6 +120,7 @@ function postProcess(result) {
|
||||
|
||||
result.sso = !!result.sso; // make it bool
|
||||
result.enableBackup = !!result.enableBackup; // make it bool
|
||||
result.enableAutomaticUpdate = !!result.enableAutomaticUpdate; // make it bool
|
||||
|
||||
assert(result.debugModeJson === null || typeof result.debugModeJson === 'string');
|
||||
result.debugMode = safe.JSON.parse(result.debugModeJson);
|
||||
@@ -276,13 +277,14 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
|
||||
var robotsTxt = 'robotsTxt' in data ? data.robotsTxt : null;
|
||||
var debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null;
|
||||
var env = data.env || {};
|
||||
const mailboxName = data.mailboxName || null;
|
||||
|
||||
var queries = [];
|
||||
|
||||
queries.push({
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId) ' +
|
||||
' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId ]
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId, mailboxName) ' +
|
||||
' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId, mailboxName ]
|
||||
});
|
||||
|
||||
queries.push({
|
||||
@@ -304,14 +306,6 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
|
||||
});
|
||||
});
|
||||
|
||||
// only allocate a mailbox if mailboxName is set
|
||||
if (data.mailboxName) {
|
||||
queries.push({
|
||||
query: 'INSERT INTO mailboxes (name, type, domain, ownerId, ownerType) VALUES (?, ?, ?, ?, ?)',
|
||||
args: [ data.mailboxName, mailboxdb.TYPE_MAILBOX, domain, id, mailboxdb.OWNER_TYPE_APP ]
|
||||
});
|
||||
}
|
||||
|
||||
if (data.alternateDomains) {
|
||||
data.alternateDomains.forEach(function (d) {
|
||||
queries.push({
|
||||
@@ -376,7 +370,6 @@ function del(id, callback) {
|
||||
|
||||
var queries = [
|
||||
{ query: 'DELETE FROM subdomains WHERE appId = ?', args: [ id ] },
|
||||
{ query: 'DELETE FROM mailboxes WHERE ownerId=?', args: [ id ] },
|
||||
{ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] },
|
||||
{ query: 'DELETE FROM appEnvVars WHERE appId = ?', args: [ id ] },
|
||||
{ query: 'DELETE FROM apps WHERE id = ?', args: [ id ] }
|
||||
@@ -384,7 +377,7 @@ function del(id, callback) {
|
||||
|
||||
database.transaction(queries, function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (results[4].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
if (results[3].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -442,12 +435,8 @@ function updateWithConstraints(id, app, constraints, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
if ('location' in app) {
|
||||
queries.push({ query: 'UPDATE subdomains SET subdomain = ? WHERE appId = ? AND type = ?', args: [ app.location, id, exports.SUBDOMAIN_TYPE_PRIMARY ]});
|
||||
}
|
||||
|
||||
if ('domain' in app) {
|
||||
queries.push({ query: 'UPDATE subdomains SET domain = ? WHERE appId = ? AND type = ?', args: [ app.domain, id, exports.SUBDOMAIN_TYPE_PRIMARY ]});
|
||||
if ('location' in app && 'domain' in app) { // must be updated together as they are unique together
|
||||
queries.push({ query: 'UPDATE subdomains SET subdomain = ?, domain = ? WHERE appId = ? AND type = ?', args: [ app.location, app.domain, id, exports.SUBDOMAIN_TYPE_PRIMARY ]});
|
||||
}
|
||||
|
||||
if ('alternateDomains' in app) {
|
||||
@@ -621,6 +610,20 @@ function getAddonConfigByAppId(appId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getAppIdByAddonConfigValue(addonId, name, value, callback) {
|
||||
assert.strictEqual(typeof addonId, 'string');
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT appId FROM appAddonConfigs WHERE addonId = ? AND name = ? AND value = ?', [ addonId, name, value ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null, results[0].appId);
|
||||
});
|
||||
}
|
||||
|
||||
function getAddonConfigByName(appId, addonId, name, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof addonId, 'string');
|
||||
|
||||
115
src/apps.js
115
src/apps.js
@@ -72,7 +72,6 @@ var appdb = require('./appdb.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
fs = require('fs'),
|
||||
mail = require('./mail.js'),
|
||||
mailboxdb = require('./mailboxdb.js'),
|
||||
manifestFormat = require('cloudron-manifestformat'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
@@ -154,7 +153,8 @@ function validatePortBindings(portBindings, manifest) {
|
||||
config.get('ldapPort'), /* ldap server (lo) */
|
||||
3306, /* mysql (lo) */
|
||||
4190, /* managesieve */
|
||||
8000, /* graphite (lo) */
|
||||
8000, /* ESXi monitoring */
|
||||
8417, /* graphite (lo) */
|
||||
];
|
||||
|
||||
if (!portBindings) return null;
|
||||
@@ -346,7 +346,7 @@ function removeInternalFields(app) {
|
||||
'location', 'domain', 'fqdn', 'mailboxName',
|
||||
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'xFrameOptions',
|
||||
'sso', 'debugMode', 'robotsTxt', 'enableBackup', 'creationTime', 'updateTime', 'ts',
|
||||
'alternateDomains', 'ownerId', 'env');
|
||||
'alternateDomains', 'ownerId', 'env', 'enableAutomaticUpdate');
|
||||
}
|
||||
|
||||
function removeRestrictedFields(app) {
|
||||
@@ -399,13 +399,7 @@ function get(appId, callback) {
|
||||
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
|
||||
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
|
||||
|
||||
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (!error) app.mailboxName = mailboxes[0].name;
|
||||
|
||||
callback(null, app);
|
||||
});
|
||||
callback(null, app);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -439,13 +433,7 @@ function getByIpAddress(ip, callback) {
|
||||
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
|
||||
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
|
||||
|
||||
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (!error) app.mailboxName = mailboxes[0].name;
|
||||
|
||||
callback(null, app);
|
||||
});
|
||||
callback(null, app);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -471,13 +459,7 @@ function getAll(callback) {
|
||||
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
|
||||
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
|
||||
|
||||
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return iteratorDone(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (!error) app.mailboxName = mailboxes[0].name;
|
||||
|
||||
iteratorDone(null, app);
|
||||
});
|
||||
iteratorDone(null, app);
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -543,11 +525,13 @@ function install(data, user, auditSource, callback) {
|
||||
debugMode = data.debugMode || null,
|
||||
robotsTxt = data.robotsTxt || null,
|
||||
enableBackup = 'enableBackup' in data ? data.enableBackup : true,
|
||||
enableAutomaticUpdate = 'enableAutomaticUpdate' in data ? data.enableAutomaticUpdate : true,
|
||||
backupId = data.backupId || null,
|
||||
backupFormat = data.backupFormat || 'tgz',
|
||||
ownerId = data.ownerId,
|
||||
alternateDomains = data.alternateDomains || [],
|
||||
env = data.env || {};
|
||||
env = data.env || {},
|
||||
mailboxName = data.mailboxName || '';
|
||||
|
||||
assert(data.appStoreId || data.manifest); // atleast one of them is required
|
||||
|
||||
@@ -588,6 +572,13 @@ function install(data, user, auditSource, callback) {
|
||||
error = validateEnv(env);
|
||||
if (error) return callback(error);
|
||||
|
||||
if (mailboxName) {
|
||||
error = mail.validateName(mailboxName);
|
||||
if (error) return callback(error);
|
||||
} else {
|
||||
mailboxName = mailboxNameForLocation(location, manifest);
|
||||
}
|
||||
|
||||
var appId = uuid.v4();
|
||||
|
||||
if (icon) {
|
||||
@@ -621,9 +612,10 @@ function install(data, user, auditSource, callback) {
|
||||
xFrameOptions: xFrameOptions,
|
||||
sso: sso,
|
||||
debugMode: debugMode,
|
||||
mailboxName: mailboxNameForLocation(location, manifest),
|
||||
mailboxName: mailboxName,
|
||||
restoreConfig: backupId ? { backupId: backupId, backupFormat: backupFormat } : null,
|
||||
enableBackup: enableBackup,
|
||||
enableAutomaticUpdate: enableAutomaticUpdate,
|
||||
robotsTxt: robotsTxt,
|
||||
alternateDomains: alternateDomains,
|
||||
env: env
|
||||
@@ -682,12 +674,14 @@ function configure(appId, data, user, auditSource, callback) {
|
||||
get(appId, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
|
||||
let domain, location, portBindings, values = { }, mailboxName;
|
||||
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;
|
||||
let domain, location, portBindings, values = { };
|
||||
if ('location' in data && 'domain' in data) {
|
||||
location = values.location = data.location.toLowerCase();
|
||||
domain = values.domain = data.domain.toLowerCase();
|
||||
} else {
|
||||
location = app.location;
|
||||
domain = app.domain;
|
||||
}
|
||||
|
||||
if ('accessRestriction' in data) {
|
||||
values.accessRestriction = data.accessRestriction;
|
||||
@@ -729,15 +723,15 @@ function configure(appId, data, user, auditSource, callback) {
|
||||
}
|
||||
|
||||
if ('mailboxName' in data) {
|
||||
if (data.mailboxName === '') { // special case to reset back to .app
|
||||
mailboxName = mailboxNameForLocation(location, app.manifest);
|
||||
} else {
|
||||
if (data.mailboxName) {
|
||||
error = mail.validateName(data.mailboxName);
|
||||
if (error) return callback(error);
|
||||
mailboxName = data.mailboxName;
|
||||
values.mailboxName = data.mailboxName;
|
||||
} else {
|
||||
values.mailboxName = mailboxNameForLocation(location, app.manifest);
|
||||
}
|
||||
} else { // keep existing name or follow the new location
|
||||
mailboxName = app.mailboxName.endsWith('.app') ? mailboxNameForLocation(location, app.manifest) : app.mailboxName;
|
||||
values.mailboxName = app.mailboxName.endsWith('.app') ? mailboxNameForLocation(location, app.manifest) : app.mailboxName;
|
||||
}
|
||||
|
||||
if ('alternateDomains' in data) {
|
||||
@@ -773,34 +767,26 @@ function configure(appId, data, user, auditSource, callback) {
|
||||
}
|
||||
|
||||
if ('enableBackup' in data) values.enableBackup = data.enableBackup;
|
||||
if ('enableAutomaticUpdate' in data) values.enableAutomaticUpdate = data.enableAutomaticUpdate;
|
||||
|
||||
values.oldConfig = getAppConfig(app);
|
||||
|
||||
debug('Will configure app with id:%s values:%j', appId, values);
|
||||
|
||||
// make the mailbox name follow the apps new location, if the user did not set it explicitly
|
||||
mailboxdb.updateName(app.mailboxName /* old */, values.oldConfig.domain, mailboxName, domain, function (error) {
|
||||
if (mailboxName.endsWith('.app')) error = null; // ignore internal mailbox conflict errors since we want to show location conflict errors in the UI
|
||||
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new AppsError(AppsError.ALREADY_EXISTS, 'This mailbox is already taken'));
|
||||
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));
|
||||
|
||||
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));
|
||||
taskmanager.restartAppTask(appId);
|
||||
|
||||
// fetch fresh app object for eventlog
|
||||
get(appId, function (error, result) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
taskmanager.restartAppTask(appId);
|
||||
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: result });
|
||||
|
||||
// fetch fresh app object for eventlog
|
||||
get(appId, function (error, result) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: result });
|
||||
|
||||
callback(null);
|
||||
});
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -987,7 +973,8 @@ function clone(appId, data, user, auditSource, callback) {
|
||||
domain = data.domain.toLowerCase(),
|
||||
portBindings = data.portBindings || null,
|
||||
backupId = data.backupId,
|
||||
ownerId = data.ownerId;
|
||||
ownerId = data.ownerId,
|
||||
mailboxName = data.mailboxName || '';
|
||||
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
@@ -1005,13 +992,22 @@ function clone(appId, data, user, auditSource, callback) {
|
||||
|
||||
if (!backupInfo.manifest) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore config'));
|
||||
|
||||
const manifest = backupInfo.manifest;
|
||||
|
||||
// re-validate because this new box version may not accept old configs
|
||||
error = checkManifestConstraints(backupInfo.manifest);
|
||||
error = checkManifestConstraints(manifest);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validatePortBindings(portBindings, backupInfo.manifest);
|
||||
error = validatePortBindings(portBindings, manifest);
|
||||
if (error) return callback(error);
|
||||
|
||||
if (mailboxName) {
|
||||
error = mail.validateName(mailboxName);
|
||||
if (error) return callback(error);
|
||||
} else {
|
||||
mailboxName = mailboxNameForLocation(location, manifest);
|
||||
}
|
||||
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'No such domain'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
|
||||
@@ -1020,7 +1016,7 @@ function clone(appId, data, user, auditSource, callback) {
|
||||
error = domains.validateHostname(location, domainObject);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message));
|
||||
|
||||
var newAppId = uuid.v4(), manifest = backupInfo.manifest;
|
||||
var newAppId = uuid.v4();
|
||||
|
||||
var data = {
|
||||
installationState: appdb.ISTATE_PENDING_CLONE,
|
||||
@@ -1029,7 +1025,7 @@ function clone(appId, data, user, auditSource, callback) {
|
||||
xFrameOptions: app.xFrameOptions,
|
||||
restoreConfig: { backupId: backupId, backupFormat: backupInfo.format },
|
||||
sso: !!app.sso,
|
||||
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app',
|
||||
mailboxName: mailboxName,
|
||||
enableBackup: app.enableBackup,
|
||||
robotsTxt: app.robotsTxt,
|
||||
env: app.env
|
||||
@@ -1220,6 +1216,7 @@ function autoupdateApps(updateInfo, auditSource, callback) { // updateInfo is {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
function canAutoupdateApp(app, newManifest) {
|
||||
if (!app.enableAutomaticUpdate) return new Error('Automatic update disabled');
|
||||
if ((semver.major(app.manifest.version) !== 0) && (semver.major(app.manifest.version) !== semver.major(newManifest.version))) return new Error('Major version change'); // major changes are blocking
|
||||
|
||||
const newTcpPorts = newManifest.tcpPorts || { };
|
||||
|
||||
@@ -55,6 +55,8 @@ var COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd.config.ejs', {
|
||||
LOGROTATE_CONFIG_EJS = fs.readFileSync(__dirname + '/logrotate.ejs', { encoding: 'utf8' }),
|
||||
CONFIGURE_LOGROTATE_CMD = path.join(__dirname, 'scripts/configurelogrotate.sh');
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -210,7 +212,7 @@ function addCollectdProfile(app, callback) {
|
||||
var collectdConf = ejs.render(COLLECTD_CONFIG_EJS, { appId: app.id, containerId: app.containerId });
|
||||
fs.writeFile(path.join(paths.COLLECTD_APPCONFIG_DIR, app.id + '.conf'), collectdConf, function (error) {
|
||||
if (error) return callback(error);
|
||||
shell.sudo('addCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'add', app.id ], callback);
|
||||
shell.sudo('addCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'add', app.id ], {}, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -220,7 +222,7 @@ function removeCollectdProfile(app, callback) {
|
||||
|
||||
fs.unlink(path.join(paths.COLLECTD_APPCONFIG_DIR, app.id + '.conf'), function (error) {
|
||||
if (error && error.code !== 'ENOENT') debugApp(app, 'Error removing collectd profile', error);
|
||||
shell.sudo('removeCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'remove', app.id ], callback);
|
||||
shell.sudo('removeCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'remove', app.id ], {}, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -239,7 +241,7 @@ function addLogrotateConfig(app, callback) {
|
||||
var tmpFilePath = path.join(os.tmpdir(), app.id + '.logrotate');
|
||||
fs.writeFile(tmpFilePath, logrotateConf, function (error) {
|
||||
if (error) return callback(error);
|
||||
shell.sudo('addLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'add', app.id, tmpFilePath ], callback);
|
||||
shell.sudo('addLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'add', app.id, tmpFilePath ], {}, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -248,7 +250,7 @@ function removeLogrotateConfig(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
shell.sudo('removeLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'remove', app.id ], callback);
|
||||
shell.sudo('removeLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'remove', app.id ], {}, callback);
|
||||
}
|
||||
|
||||
function verifyManifest(manifest, callback) {
|
||||
@@ -539,7 +541,7 @@ function install(app, callback) {
|
||||
updateApp.bind(null, app, { installationProgress: '60, Download backup and restoring addons' }),
|
||||
addons.setupAddons.bind(null, app, app.manifest.addons),
|
||||
addons.clearAddons.bind(null, app, app.manifest.addons),
|
||||
backups.restoreApp.bind(null, app, app.manifest.addons, restoreConfig),
|
||||
backups.restoreApp.bind(null, app, app.manifest.addons, restoreConfig, (progress) => updateApp(app, { installationProgress: progress.message }, NOOP_CALLBACK))
|
||||
], next);
|
||||
}
|
||||
},
|
||||
@@ -581,7 +583,7 @@ function backup(app, callback) {
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '10, Backing up' }),
|
||||
backups.backupApp.bind(null, app),
|
||||
backups.backupApp.bind(null, app, (progress) => updateApp(app, { installationProgress: progress.message }, NOOP_CALLBACK)),
|
||||
|
||||
// done!
|
||||
function (callback) {
|
||||
@@ -694,7 +696,7 @@ function update(app, callback) {
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '15, Backing up app' }),
|
||||
backups.backupApp.bind(null, app)
|
||||
backups.backupApp.bind(null, app, (progress) => updateApp(app, { installationProgress: progress.message }, NOOP_CALLBACK))
|
||||
], function (error) {
|
||||
if (error) error.backupError = true;
|
||||
next(error);
|
||||
|
||||
275
src/backups.js
275
src/backups.js
@@ -10,9 +10,9 @@ exports = module.exports = {
|
||||
|
||||
get: get,
|
||||
|
||||
startBackupTask: startBackupTask,
|
||||
ensureBackup: ensureBackup,
|
||||
|
||||
backup: backup,
|
||||
restore: restore,
|
||||
|
||||
backupApp: backupApp,
|
||||
@@ -53,7 +53,6 @@ var addons = require('./addons.js'),
|
||||
once = require('once'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
progress = require('./progress.js'),
|
||||
progressStream = require('progress-stream'),
|
||||
safe = require('safetydance'),
|
||||
shell = require('./shell.js'),
|
||||
@@ -61,12 +60,12 @@ var addons = require('./addons.js'),
|
||||
superagent = require('superagent'),
|
||||
syncer = require('./syncer.js'),
|
||||
tar = require('tar-fs'),
|
||||
tasks = require('./tasks.js'),
|
||||
util = require('util'),
|
||||
zlib = require('zlib');
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
var BACKUPTASK_CMD = path.join(__dirname, 'backuptask.js');
|
||||
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
const BACKUP_UPLOAD_CMD = path.join(__dirname, 'scripts/backupupload.js');
|
||||
|
||||
function debugApp(app) {
|
||||
assert(typeof app === 'object');
|
||||
@@ -181,11 +180,6 @@ function getBackupFilePath(backupConfig, backupId, format) {
|
||||
}
|
||||
}
|
||||
|
||||
function log(detail) {
|
||||
safe.fs.appendFileSync(paths.BACKUP_LOG_FILE, detail + '\n', 'utf8');
|
||||
progress.setDetail(progress.BACKUP, detail);
|
||||
}
|
||||
|
||||
function encryptFilePath(filePath, key) {
|
||||
assert.strictEqual(typeof filePath, 'string');
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
@@ -238,10 +232,6 @@ function createReadStream(sourceFile, key) {
|
||||
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||
});
|
||||
|
||||
ps.on('progress', function(progress) {
|
||||
debug('createReadStream: %s@%s (%s)', Math.round(progress.transferred/1024/1024) + 'M', Math.round(progress.speed/1024/1024) + 'Mbps', sourceFile);
|
||||
});
|
||||
|
||||
if (key !== null) {
|
||||
var encrypt = crypto.createCipher('aes-256-cbc', key);
|
||||
encrypt.on('error', function (error) {
|
||||
@@ -287,7 +277,7 @@ function createTarPackStream(sourceDir, key) {
|
||||
});
|
||||
|
||||
var gzip = zlib.createGzip({});
|
||||
var ps = progressStream({ time: 10000 }); // display a progress every 10 seconds
|
||||
var ps = progressStream({ time: 10000 }); // emit 'pgoress' every 10 seconds
|
||||
|
||||
pack.on('error', function (error) {
|
||||
debug('createTarPackStream: tar stream error.', error);
|
||||
@@ -299,10 +289,6 @@ function createTarPackStream(sourceDir, key) {
|
||||
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||
});
|
||||
|
||||
ps.on('progress', function(progress) {
|
||||
debug('createTarPackStream: %s@%s', Math.round(progress.transferred/1024/1024) + 'M', Math.round(progress.speed/1024/1024) + 'Mbps');
|
||||
});
|
||||
|
||||
if (key !== null) {
|
||||
var encrypt = crypto.createCipher('aes-256-cbc', key);
|
||||
encrypt.on('error', function (error) {
|
||||
@@ -315,17 +301,13 @@ function createTarPackStream(sourceDir, key) {
|
||||
}
|
||||
}
|
||||
|
||||
function sync(backupConfig, backupId, dataDir, callback) {
|
||||
function sync(backupConfig, backupId, dataDir, progressCallback, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof dataDir, 'string');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
function setBackupProgress(message) {
|
||||
debug('%s', message);
|
||||
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, message);
|
||||
}
|
||||
|
||||
syncer.sync(dataDir, function processTask(task, iteratorCallback) {
|
||||
debug('sync: processing task: %j', task);
|
||||
// the empty task.path is special to signify the directory
|
||||
@@ -333,12 +315,12 @@ function sync(backupConfig, backupId, dataDir, callback) {
|
||||
const backupFilePath = path.join(getBackupFilePath(backupConfig, backupId, backupConfig.format), destPath);
|
||||
|
||||
if (task.operation === 'removedir') {
|
||||
setBackupProgress(`Removing directory ${backupFilePath}`);
|
||||
debug(`Removing directory ${backupFilePath}`);
|
||||
return api(backupConfig.provider).removeDir(backupConfig, backupFilePath)
|
||||
.on('progress', setBackupProgress)
|
||||
.on('progress', (message) => progressCallback({ message }))
|
||||
.on('done', iteratorCallback);
|
||||
} else if (task.operation === 'remove') {
|
||||
setBackupProgress(`Removing ${backupFilePath}`);
|
||||
debug(`Removing ${backupFilePath}`);
|
||||
return api(backupConfig.provider).remove(backupConfig, backupFilePath, iteratorCallback);
|
||||
}
|
||||
|
||||
@@ -347,16 +329,19 @@ function sync(backupConfig, backupId, dataDir, callback) {
|
||||
retryCallback = once(retryCallback); // protect again upload() erroring much later after read stream error
|
||||
|
||||
++retryCount;
|
||||
debug(`${task.operation} ${task.path} try ${retryCount}`);
|
||||
progressCallback({ message: `${task.operation} ${task.path} try ${retryCount}` });
|
||||
if (task.operation === 'add') {
|
||||
setBackupProgress(`Adding ${task.path} position ${task.position} try ${retryCount}`);
|
||||
debug(`Adding ${task.path} position ${task.position} try ${retryCount}`);
|
||||
var stream = createReadStream(path.join(dataDir, task.path), backupConfig.key || null);
|
||||
stream.on('error', function (error) {
|
||||
setBackupProgress(`read stream error for ${task.path}: ${error.message}`);
|
||||
debug(`read stream error for ${task.path}: ${error.message}`);
|
||||
retryCallback();
|
||||
}); // ignore error if file disappears
|
||||
stream.on('progress', function(progress) {
|
||||
progressCallback({ message: `Uploading ${task.path}: ${Math.round(progress.transferred/1024/1024)}M@${Math.round(progress.speed/1024/1024)}` });
|
||||
});
|
||||
api(backupConfig.provider).upload(backupConfig, backupFilePath, stream, function (error) {
|
||||
setBackupProgress(error ? `Error uploading ${task.path} try ${retryCount}: ${error.message}` : `Uploaded ${task.path}`);
|
||||
debug(error ? `Error uploading ${task.path} try ${retryCount}: ${error.message}` : `Uploaded ${task.path}`);
|
||||
retryCallback(error);
|
||||
});
|
||||
}
|
||||
@@ -388,14 +373,15 @@ function saveFsMetadata(appDataDir, callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
// this function is called via backuptask (since it needs root to traverse app's directory)
|
||||
function upload(backupId, format, dataDir, callback) {
|
||||
// this function is called via backupupload (since it needs root to traverse app's directory)
|
||||
function upload(backupId, format, dataDir, progressCallback, callback) {
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof format, 'string');
|
||||
assert.strictEqual(typeof dataDir, 'string');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('upload: id %s format %s dataDir %s', backupId, format, dataDir);
|
||||
debug(`upload: id ${backupId} format ${format} dataDir ${dataDir}`);
|
||||
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
@@ -405,6 +391,9 @@ function upload(backupId, format, dataDir, callback) {
|
||||
retryCallback = once(retryCallback); // protect again upload() erroring much later after tar stream error
|
||||
|
||||
var tarStream = createTarPackStream(dataDir, backupConfig.key || null);
|
||||
tarStream.on('progress', function(progress) {
|
||||
progressCallback({ message: `Uploading ${Math.round(progress.transferred/1024/1024)}M@${Math.round(progress.speed/1024/1024)}Mbps` });
|
||||
});
|
||||
tarStream.on('error', retryCallback); // already returns BackupsError
|
||||
|
||||
api(backupConfig.provider).upload(backupConfig, getBackupFilePath(backupConfig, backupId, format), tarStream, retryCallback);
|
||||
@@ -412,7 +401,7 @@ function upload(backupId, format, dataDir, callback) {
|
||||
} else {
|
||||
async.series([
|
||||
saveFsMetadata.bind(null, dataDir),
|
||||
sync.bind(null, backupConfig, backupId, dataDir)
|
||||
sync.bind(null, backupConfig, backupId, dataDir, progressCallback)
|
||||
], callback);
|
||||
}
|
||||
});
|
||||
@@ -435,10 +424,6 @@ function tarExtract(inStream, destination, key, callback) {
|
||||
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||
});
|
||||
|
||||
ps.on('progress', function(progress) {
|
||||
debug('tarExtract: %s@%s', Math.round(progress.transferred/1024/1024) + 'M', Math.round(progress.speed/1024/1024) + 'Mbps');
|
||||
});
|
||||
|
||||
gunzip.on('error', function (error) {
|
||||
debug('tarExtract: gunzip stream error.', error);
|
||||
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||
@@ -464,13 +449,15 @@ function tarExtract(inStream, destination, key, callback) {
|
||||
} else {
|
||||
inStream.pipe(ps).pipe(gunzip).pipe(extract);
|
||||
}
|
||||
|
||||
return ps;
|
||||
}
|
||||
|
||||
function restoreFsMetadata(appDataDir, callback) {
|
||||
assert.strictEqual(typeof appDataDir, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
log('Recreating empty directories');
|
||||
debug(`Recreating empty directories in ${appDataDir}`);
|
||||
|
||||
var metadataJson = safe.fs.readFileSync(path.join(appDataDir, 'fsmetadata.json'), 'utf8');
|
||||
if (metadataJson === null) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error loading fsmetadata.txt:' + safe.error.message));
|
||||
@@ -492,10 +479,11 @@ function restoreFsMetadata(appDataDir, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function downloadDir(backupConfig, backupFilePath, destDir, callback) {
|
||||
function downloadDir(backupConfig, backupFilePath, destDir, progressCallback, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof backupFilePath, 'string');
|
||||
assert.strictEqual(typeof destDir, 'string');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`downloadDir: ${backupFilePath} to ${destDir}`);
|
||||
@@ -519,7 +507,7 @@ function downloadDir(backupConfig, backupFilePath, destDir, callback) {
|
||||
let destStream = createWriteStream(destFilePath, backupConfig.key || null);
|
||||
destStream.on('error', callback);
|
||||
|
||||
debug(`downloadDir: Copying ${entry.fullPath} to ${destFilePath}`);
|
||||
progressCallback({ message: `Downloading ${entry.fullPath} to ${destFilePath}` });
|
||||
|
||||
sourceStream.pipe(destStream, { end: true }).on('finish', callback);
|
||||
});
|
||||
@@ -531,25 +519,27 @@ function downloadDir(backupConfig, backupFilePath, destDir, callback) {
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function download(backupConfig, backupId, format, dataDir, callback) {
|
||||
function download(backupConfig, backupId, format, dataDir, progressCallback, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof format, 'string');
|
||||
assert.strictEqual(typeof dataDir, 'string');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
safe.fs.unlinkSync(paths.BACKUP_LOG_FILE); // start fresh log file
|
||||
|
||||
log(`Downloading ${backupId} of format ${format} to ${dataDir}`);
|
||||
debug(`download - Downloading ${backupId} of format ${format} to ${dataDir}`);
|
||||
|
||||
if (format === 'tgz') {
|
||||
api(backupConfig.provider).download(backupConfig, getBackupFilePath(backupConfig, backupId, format), function (error, sourceStream) {
|
||||
if (error) return callback(error);
|
||||
|
||||
tarExtract(sourceStream, dataDir, backupConfig.key || null, callback);
|
||||
let ps = tarExtract(sourceStream, dataDir, backupConfig.key || null, callback);
|
||||
ps.on('progress', function (progress) {
|
||||
progressCallback({ message: `Downloading ${Math.round(progress.transferred/1024/1024)}M@${Math.round(progress.speed/1024/1024)}Mbps` });
|
||||
});
|
||||
});
|
||||
} else {
|
||||
downloadDir(backupConfig, getBackupFilePath(backupConfig, backupId, format), dataDir, function (error) {
|
||||
downloadDir(backupConfig, getBackupFilePath(backupConfig, backupId, format), dataDir, progressCallback, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
restoreFsMetadata(dataDir, callback);
|
||||
@@ -557,12 +547,13 @@ function download(backupConfig, backupId, format, dataDir, callback) {
|
||||
}
|
||||
}
|
||||
|
||||
function restore(backupConfig, backupId, callback) {
|
||||
function restore(backupConfig, backupId, progressCallback, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
download(backupConfig, backupId, backupConfig.format, paths.BOX_DATA_DIR, function (error) {
|
||||
download(backupConfig, backupId, backupConfig.format, paths.BOX_DATA_DIR, progressCallback, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('restore: download completed, importing database');
|
||||
@@ -577,10 +568,11 @@ function restore(backupConfig, backupId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function restoreApp(app, addonsToRestore, restoreConfig, callback) {
|
||||
function restoreApp(app, addonsToRestore, restoreConfig, progressCallback, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof addonsToRestore, 'object');
|
||||
assert.strictEqual(typeof restoreConfig, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id));
|
||||
@@ -591,7 +583,7 @@ function restoreApp(app, addonsToRestore, restoreConfig, callback) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
async.series([
|
||||
download.bind(null, backupConfig, restoreConfig.backupId, restoreConfig.backupFormat, appDataDir),
|
||||
download.bind(null, backupConfig, restoreConfig.backupId, restoreConfig.backupFormat, appDataDir, progressCallback),
|
||||
addons.restoreAddons.bind(null, app, addonsToRestore)
|
||||
], function (error) {
|
||||
debug('restoreApp: time: %s', (new Date() - startTime)/1000);
|
||||
@@ -601,44 +593,27 @@ function restoreApp(app, addonsToRestore, restoreConfig, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function runBackupTask(backupId, format, dataDir, callback) {
|
||||
function runBackupUpload(backupId, format, dataDir, progressCallback, callback) {
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof format, 'string');
|
||||
assert.strictEqual(typeof dataDir, 'string');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var killTimerId = null, progressTimerId = null;
|
||||
|
||||
var logStream = fs.createWriteStream(paths.BACKUP_LOG_FILE, { flags: 'a' });
|
||||
var cp = shell.sudo(`backup-${backupId}`, [ BACKUPTASK_CMD, backupId, format, dataDir ], { env: process.env, logStream: logStream }, function (error) {
|
||||
clearTimeout(killTimerId);
|
||||
clearInterval(progressTimerId);
|
||||
|
||||
cp = null;
|
||||
let result = '';
|
||||
|
||||
shell.sudo(`backup-${backupId}`, [ BACKUP_UPLOAD_CMD, backupId, format, dataDir ], { preserveEnv: true, ipc: true }, function (error) {
|
||||
if (error && (error.code === null /* signal */ || (error.code !== 0 && error.code !== 50))) { // backuptask crashed
|
||||
return callback(new BackupsError(BackupsError.INTERNAL_ERROR, 'Backuptask crashed'));
|
||||
} else if (error && error.code === 50) { // exited with error
|
||||
var result = safe.fs.readFileSync(paths.BACKUP_RESULT_FILE, 'utf8') || safe.error.message;
|
||||
return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, result));
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
|
||||
progressTimerId = setInterval(function () {
|
||||
var result = safe.fs.readFileSync(paths.BACKUP_RESULT_FILE, 'utf8');
|
||||
if (result) progress.setDetail(progress.BACKUP, result);
|
||||
}, 1000); // every second
|
||||
|
||||
killTimerId = setTimeout(function () {
|
||||
debug('runBackupTask: backup task taking too long. killing');
|
||||
cp.kill();
|
||||
}, 4 * 60 * 60 * 1000); // 4 hours
|
||||
|
||||
logStream.on('error', function (error) {
|
||||
debug('runBackupTask: error in logging stream', error);
|
||||
cp.kill();
|
||||
}).on('message', function (message) {
|
||||
if (!message.result) return progressCallback(message);
|
||||
debug(`runBackupUpload: result - ${message}`);
|
||||
result = message.result;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -664,10 +639,11 @@ function setSnapshotInfo(id, info, callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
function snapshotBox(callback) {
|
||||
function snapshotBox(progressCallback, callback) {
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
log('Snapshotting box');
|
||||
progressCallback({ message: 'Snapshotting box' });
|
||||
|
||||
database.exportToFile(`${paths.BOX_DATA_DIR}/box.mysqldump`, function (error) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
@@ -676,16 +652,17 @@ function snapshotBox(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function uploadBoxSnapshot(backupConfig, callback) {
|
||||
function uploadBoxSnapshot(backupConfig, progressCallback, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var startTime = new Date();
|
||||
|
||||
snapshotBox(function (error) {
|
||||
snapshotBox(progressCallback, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
runBackupTask('snapshot/box', backupConfig.format, paths.BOX_DATA_DIR, function (error) {
|
||||
runBackupUpload('snapshot/box', backupConfig.format, paths.BOX_DATA_DIR, progressCallback, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('uploadBoxSnapshot: time: %s secs', (new Date() - startTime)/1000);
|
||||
@@ -723,10 +700,11 @@ function backupDone(apiConfig, backupId, appBackupIds, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function rotateBoxBackup(backupConfig, timestamp, appBackupIds, callback) {
|
||||
function rotateBoxBackup(backupConfig, timestamp, appBackupIds, progressCallback, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof timestamp, 'string');
|
||||
assert(Array.isArray(appBackupIds));
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var snapshotInfo = getSnapshotInfo('box');
|
||||
@@ -736,13 +714,13 @@ function rotateBoxBackup(backupConfig, timestamp, appBackupIds, callback) {
|
||||
var backupId = util.format('%s/box_%s_v%s', timestamp, snapshotTime, config.version());
|
||||
const format = backupConfig.format;
|
||||
|
||||
log(`Rotating box backup to id ${backupId}`);
|
||||
debug(`Rotating box backup to id ${backupId}`);
|
||||
|
||||
backupdb.add({ id: backupId, version: config.version(), type: backupdb.BACKUP_TYPE_BOX, dependsOn: appBackupIds, manifest: null, format: format }, function (error) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
var copy = api(backupConfig.provider).copy(backupConfig, getBackupFilePath(backupConfig, 'snapshot/box', format), getBackupFilePath(backupConfig, backupId, format));
|
||||
copy.on('progress', log);
|
||||
copy.on('progress', (message) => progressCallback({ message }));
|
||||
copy.on('done', function (copyBackupError) {
|
||||
const state = copyBackupError ? backupdb.BACKUP_STATE_ERROR : backupdb.BACKUP_STATE_NORMAL;
|
||||
|
||||
@@ -750,7 +728,7 @@ function rotateBoxBackup(backupConfig, timestamp, appBackupIds, callback) {
|
||||
if (copyBackupError) return callback(copyBackupError);
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
log(`Rotated box backup successfully as id ${backupId}`);
|
||||
debug(`Rotated box backup successfully as id ${backupId}`);
|
||||
|
||||
backupDone(backupConfig, backupId, appBackupIds, function (error) {
|
||||
if (error) return callback(error);
|
||||
@@ -762,18 +740,19 @@ function rotateBoxBackup(backupConfig, timestamp, appBackupIds, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function backupBoxWithAppBackupIds(appBackupIds, timestamp, callback) {
|
||||
function backupBoxWithAppBackupIds(appBackupIds, timestamp, progressCallback, callback) {
|
||||
assert(Array.isArray(appBackupIds));
|
||||
assert.strictEqual(typeof timestamp, 'string');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
uploadBoxSnapshot(backupConfig, function (error) {
|
||||
uploadBoxSnapshot(backupConfig, progressCallback, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
rotateBoxBackup(backupConfig, timestamp, appBackupIds, callback);
|
||||
rotateBoxBackup(backupConfig, timestamp, appBackupIds, progressCallback, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -787,11 +766,12 @@ function canBackupApp(app) {
|
||||
app.installationState === appdb.ISTATE_PENDING_UPDATE; // called from apptask
|
||||
}
|
||||
|
||||
function snapshotApp(app, callback) {
|
||||
function snapshotApp(app, progressCallback, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
log(`Snapshotting app ${app.id}`);
|
||||
progressCallback({ message: `Snapshotting app ${app.id}` });
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/config.json'), JSON.stringify(apps.getAppConfig(app)))) {
|
||||
return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error creating config.json: ' + safe.error.message));
|
||||
@@ -804,10 +784,11 @@ function snapshotApp(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function rotateAppBackup(backupConfig, app, timestamp, callback) {
|
||||
function rotateAppBackup(backupConfig, app, timestamp, progressCallback, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof timestamp, 'string');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var snapshotInfo = getSnapshotInfo(app.id);
|
||||
@@ -818,13 +799,13 @@ function rotateAppBackup(backupConfig, app, timestamp, callback) {
|
||||
var backupId = util.format('%s/app_%s_%s_v%s', timestamp, app.id, snapshotTime, manifest.version);
|
||||
const format = backupConfig.format;
|
||||
|
||||
log(`Rotating app backup of ${app.id} to id ${backupId}`);
|
||||
debug(`Rotating app backup of ${app.id} to id ${backupId}`);
|
||||
|
||||
backupdb.add({ id: backupId, version: manifest.version, type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ], manifest: manifest, format: format }, function (error) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
var copy = api(backupConfig.provider).copy(backupConfig, getBackupFilePath(backupConfig, `snapshot/app_${app.id}`, format), getBackupFilePath(backupConfig, backupId, format));
|
||||
copy.on('progress', log);
|
||||
copy.on('progress', (message) => progressCallback({ message }));
|
||||
copy.on('done', function (copyBackupError) {
|
||||
const state = copyBackupError ? backupdb.BACKUP_STATE_ERROR : backupdb.BACKUP_STATE_NORMAL;
|
||||
|
||||
@@ -832,7 +813,7 @@ function rotateAppBackup(backupConfig, app, timestamp, callback) {
|
||||
if (copyBackupError) return callback(copyBackupError);
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
log(`Rotated app backup of ${app.id} successfully to id ${backupId}`);
|
||||
debug(`Rotated app backup of ${app.id} successfully to id ${backupId}`);
|
||||
|
||||
callback(null, backupId);
|
||||
});
|
||||
@@ -840,21 +821,22 @@ function rotateAppBackup(backupConfig, app, timestamp, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function uploadAppSnapshot(backupConfig, app, callback) {
|
||||
function uploadAppSnapshot(backupConfig, app, progressCallback, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!canBackupApp(app)) return callback(); // nothing to do
|
||||
|
||||
var startTime = new Date();
|
||||
|
||||
snapshotApp(app, function (error) {
|
||||
snapshotApp(app, progressCallback, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var backupId = util.format('snapshot/app_%s', app.id);
|
||||
var appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id));
|
||||
runBackupTask(backupId, backupConfig.format, appDataDir, function (error) {
|
||||
runBackupUpload(backupId, backupConfig.format, appDataDir, progressCallback, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debugApp(app, 'uploadAppSnapshot: %s done time: %s secs', backupId, (new Date() - startTime)/1000);
|
||||
@@ -864,9 +846,10 @@ function uploadAppSnapshot(backupConfig, app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function backupAppWithTimestamp(app, timestamp, callback) {
|
||||
function backupAppWithTimestamp(app, timestamp, progressCallback, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof timestamp, 'string');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!canBackupApp(app)) return callback(); // nothing to do
|
||||
@@ -874,110 +857,88 @@ function backupAppWithTimestamp(app, timestamp, callback) {
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
uploadAppSnapshot(backupConfig, app, function (error) {
|
||||
uploadAppSnapshot(backupConfig, app, progressCallback, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
rotateAppBackup(backupConfig, app, timestamp, callback);
|
||||
rotateAppBackup(backupConfig, app, timestamp, progressCallback, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function backupApp(app, callback) {
|
||||
function backupApp(app, progressCallback, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
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.fqdn);
|
||||
debug(`backupApp - Backing up ${app.fqdn} with timestamp ${timestamp}`);
|
||||
|
||||
backupAppWithTimestamp(app, timestamp, function (error) {
|
||||
progress.set(progress.BACKUP, 100, error ? error.message : '');
|
||||
|
||||
callback(error);
|
||||
});
|
||||
backupAppWithTimestamp(app, timestamp, progressCallback, callback);
|
||||
}
|
||||
|
||||
// this function expects you to have a lock
|
||||
function backupBoxAndApps(auditSource, callback) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
// this function expects you to have a lock. Unlike other progressCallback this also has a progress field
|
||||
function backupBoxAndApps(progressCallback, callback) {
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var timestamp = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
|
||||
safe.fs.unlinkSync(paths.BACKUP_LOG_FILE); // start fresh log file
|
||||
|
||||
eventlog.add(eventlog.ACTION_BACKUP_START, auditSource, { });
|
||||
|
||||
apps.getAll(function (error, allApps) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
var processed = 1;
|
||||
var step = 100/(allApps.length+2);
|
||||
let percent = 1;
|
||||
let step = 100/(allApps.length+2);
|
||||
|
||||
async.mapSeries(allApps, function iterator(app, iteratorCallback) {
|
||||
progress.set(progress.BACKUP, step * processed, 'Backing up ' + app.fqdn);
|
||||
|
||||
++processed;
|
||||
progressCallback({ percent: percent, message: `Backing up ${app.fqdn}` });
|
||||
percent += step;
|
||||
|
||||
if (!app.enableBackup) {
|
||||
progress.set(progress.BACKUP, step * processed, 'Skipped backup ' + app.fqdn);
|
||||
debug(`Skipped backup ${app.fqdn}`);
|
||||
return iteratorCallback(null, null); // nothing to backup
|
||||
}
|
||||
|
||||
backupAppWithTimestamp(app, timestamp, function (error, backupId) {
|
||||
backupAppWithTimestamp(app, timestamp, (progress) => progressCallback({ percent: percent, message: progress.message }), function (error, backupId) {
|
||||
if (error && error.reason !== BackupsError.BAD_STATE) {
|
||||
debugApp(app, 'Unable to backup', error);
|
||||
return iteratorCallback(error);
|
||||
}
|
||||
|
||||
progress.set(progress.BACKUP, step * processed, 'Backed up ' + app.fqdn);
|
||||
debugApp(app, 'Backed up');
|
||||
|
||||
iteratorCallback(null, backupId || null); // clear backupId if is in BAD_STATE and never backed up
|
||||
});
|
||||
}, function appsBackedUp(error, backupIds) {
|
||||
if (error) {
|
||||
progress.set(progress.BACKUP, 100, error.message);
|
||||
return callback(error);
|
||||
}
|
||||
if (error) return callback(error);
|
||||
|
||||
backupIds = backupIds.filter(function (id) { return id !== null; }); // remove apps in bad state that were never backed up
|
||||
|
||||
progress.set(progress.BACKUP, step * processed, 'Backing up system data');
|
||||
progressCallback({ percent: percent, message: 'Backing up system data' });
|
||||
percent += step;
|
||||
|
||||
backupBoxWithAppBackupIds(backupIds, timestamp, function (error, backupId) {
|
||||
progress.set(progress.BACKUP, 100, error ? error.message : '');
|
||||
|
||||
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { errorMessage: error ? error.message : null, backupId: backupId, timestamp: timestamp });
|
||||
|
||||
callback(error, backupId);
|
||||
});
|
||||
backupBoxWithAppBackupIds(backupIds, timestamp, (progress) => progressCallback({ percent: percent, message: progress.message }), callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function backup(auditSource, callback) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = locker.lock(locker.OP_FULL_BACKUP);
|
||||
if (error) return callback(new BackupsError(BackupsError.BAD_STATE, error.message));
|
||||
|
||||
var startTime = new Date();
|
||||
progress.set(progress.BACKUP, 0, 'Starting'); // ensure tools can 'wait' on progress
|
||||
|
||||
backupBoxAndApps(auditSource, function (error) { // start the backup operation in the background
|
||||
if (error) {
|
||||
debug('backup failed.', error);
|
||||
mailer.backupFailed(error);
|
||||
}
|
||||
function startBackupTask(auditSource, callback) {
|
||||
let error = locker.lock(locker.OP_FULL_BACKUP);
|
||||
if (error) return callback(error);
|
||||
|
||||
let task = tasks.startTask(tasks.TASK_BACKUP, [], auditSource);
|
||||
task.on('error', (error) => callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)));
|
||||
task.on('start', (taskId) => {
|
||||
eventlog.add(eventlog.ACTION_BACKUP_START, auditSource, { taskId });
|
||||
callback(null, taskId);
|
||||
});
|
||||
task.on('finish', (error, result) => {
|
||||
locker.unlock(locker.OP_FULL_BACKUP);
|
||||
|
||||
debug('backup took %s seconds', (new Date() - startTime)/1000);
|
||||
});
|
||||
if (error) mailer.backupFailed(error);
|
||||
|
||||
callback(null);
|
||||
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { errorMessage: error ? error.message : null, backupId: result });
|
||||
});
|
||||
}
|
||||
|
||||
function ensureBackup(auditSource, callback) {
|
||||
@@ -999,7 +960,7 @@ function ensureBackup(auditSource, callback) {
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
backup(auditSource, callback);
|
||||
startBackupTask(auditSource, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
138
src/caas.js
138
src/caas.js
@@ -4,29 +4,18 @@ exports = module.exports = {
|
||||
verifySetupToken: verifySetupToken,
|
||||
setupDone: setupDone,
|
||||
|
||||
changePlan: changePlan,
|
||||
upgrade: upgrade,
|
||||
sendHeartbeat: sendHeartbeat,
|
||||
getBoxAndUserDetails: getBoxAndUserDetails,
|
||||
setPtrRecord: setPtrRecord,
|
||||
|
||||
CaasError: CaasError
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
backups = require('./backups.js'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:caas'),
|
||||
locker = require('./locker.js'),
|
||||
path = require('path'),
|
||||
progress = require('./progress.js'),
|
||||
settings = require('./settings.js'),
|
||||
shell = require('./shell.js'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
const RETIRE_CMD = path.join(__dirname, 'scripts/retire.sh');
|
||||
util = require('util');
|
||||
|
||||
function CaasError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
@@ -53,20 +42,6 @@ CaasError.INVALID_TOKEN = 'Invalid Token';
|
||||
CaasError.INTERNAL_ERROR = 'Internal Error';
|
||||
CaasError.EXTERNAL_ERROR = 'External Error';
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
function retire(reason, info, callback) {
|
||||
assert(reason === 'migrate' || reason === 'upgrade');
|
||||
info = info || { };
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
var data = {
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
adminFqdn: config.adminFqdn()
|
||||
};
|
||||
shell.sudo('retire', [ RETIRE_CMD, reason, JSON.stringify(info), JSON.stringify(data) ], callback);
|
||||
}
|
||||
|
||||
function getCaasConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -117,96 +92,6 @@ function setupDone(setupToken, callback) {
|
||||
});
|
||||
});
|
||||
}
|
||||
function doMigrate(options, caasConfig, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof caasConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = locker.lock(locker.OP_MIGRATE);
|
||||
if (error) return callback(new CaasError(CaasError.BAD_STATE, error.message));
|
||||
|
||||
function unlock(error) {
|
||||
debug('Failed to migrate', error);
|
||||
locker.unlock(locker.OP_MIGRATE);
|
||||
progress.set(progress.MIGRATE, -1, 'Backup failed: ' + error.message);
|
||||
}
|
||||
|
||||
progress.set(progress.MIGRATE, 10, 'Backing up for migration');
|
||||
|
||||
// initiate the migration in the background
|
||||
backups.backupBoxAndApps({ userId: null, username: 'migrator' }, function (error) {
|
||||
if (error) return unlock(error);
|
||||
|
||||
debug('migrate: domain: %s size %s region %s', options.domain, options.size, options.region);
|
||||
|
||||
superagent
|
||||
.post(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId + '/migrate')
|
||||
.query({ token: caasConfig.token })
|
||||
.send(options)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return unlock(error); // network error
|
||||
if (result.statusCode === 409) return unlock(new CaasError(CaasError.BAD_STATE));
|
||||
if (result.statusCode === 404) return unlock(new CaasError(CaasError.NOT_FOUND));
|
||||
if (result.statusCode !== 202) return unlock(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
|
||||
|
||||
progress.set(progress.MIGRATE, 10, 'Migrating');
|
||||
|
||||
retire('migrate', _.pick(options, 'domain', 'size', 'region'));
|
||||
});
|
||||
});
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function changePlan(options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (config.isDemo()) return callback(new CaasError(CaasError.BAD_FIELD, 'Not allowed in demo mode'));
|
||||
|
||||
getCaasConfig(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
doMigrate(options, result, callback);
|
||||
});
|
||||
}
|
||||
|
||||
// this function expects a lock
|
||||
function upgrade(boxUpdateInfo, callback) {
|
||||
assert(boxUpdateInfo !== null && typeof boxUpdateInfo === 'object');
|
||||
|
||||
function upgradeError(e) {
|
||||
progress.set(progress.UPDATE, -1, e.message);
|
||||
callback(e);
|
||||
}
|
||||
|
||||
progress.set(progress.UPDATE, 5, 'Backing up for upgrade');
|
||||
|
||||
backups.backupBoxAndApps({ userId: null, username: 'upgrader' }, function (error) {
|
||||
if (error) return upgradeError(error);
|
||||
|
||||
getCaasConfig(function (error, result) {
|
||||
if (error) return upgradeError(error);
|
||||
|
||||
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + result.boxId + '/upgrade')
|
||||
.query({ token: result.token })
|
||||
.send({ version: boxUpdateInfo.version })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return upgradeError(new Error('Network error making upgrade request: ' + error));
|
||||
if (result.statusCode !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
|
||||
|
||||
progress.set(progress.UPDATE, 10, 'Updating base system');
|
||||
|
||||
// no need to unlock since this is the last thing we ever do on this box
|
||||
callback();
|
||||
|
||||
retire('upgrade');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sendHeartbeat() {
|
||||
assert(config.provider() === 'caas', 'Heartbeat is only sent for managed cloudrons');
|
||||
@@ -223,27 +108,6 @@ function sendHeartbeat() {
|
||||
});
|
||||
}
|
||||
|
||||
function getBoxAndUserDetails(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (config.provider() !== 'caas') return callback(null, {});
|
||||
|
||||
getCaasConfig(function (error, caasConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
superagent
|
||||
.get(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId)
|
||||
.query({ token: caasConfig.token })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, 'Cannot reach appstore'));
|
||||
if (result.statusCode !== 200) return callback(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||
|
||||
return callback(null, result.body);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setPtrRecord(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -1,481 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
crypto = require('crypto'),
|
||||
debug = require('debug')('box:cert/acme1'),
|
||||
execSync = require('safetydance').child_process.execSync,
|
||||
fs = require('fs'),
|
||||
parseLinks = require('parse-links'),
|
||||
path = require('path'),
|
||||
paths = require('../paths.js'),
|
||||
safe = require('safetydance'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
var CA_PROD = 'https://acme-v01.api.letsencrypt.org',
|
||||
CA_STAGING = 'https://acme-staging.api.letsencrypt.org',
|
||||
LE_AGREEMENT = 'https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf';
|
||||
|
||||
exports = module.exports = {
|
||||
getCertificate: getCertificate,
|
||||
|
||||
// testing
|
||||
_name: 'acme'
|
||||
};
|
||||
|
||||
function Acme1Error(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(Acme1Error, Error);
|
||||
Acme1Error.INTERNAL_ERROR = 'Internal Error';
|
||||
Acme1Error.EXTERNAL_ERROR = 'External Error';
|
||||
Acme1Error.ALREADY_EXISTS = 'Already Exists';
|
||||
Acme1Error.NOT_COMPLETED = 'Not Completed';
|
||||
Acme1Error.FORBIDDEN = 'Forbidden';
|
||||
|
||||
// http://jose.readthedocs.org/en/latest/
|
||||
// https://www.ietf.org/proceedings/92/slides/slides-92-acme-1.pdf
|
||||
// https://community.letsencrypt.org/t/list-of-client-implementations/2103
|
||||
|
||||
function Acme1(options) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
|
||||
this.caOrigin = options.prod ? CA_PROD : CA_STAGING;
|
||||
this.accountKeyPem = null; // Buffer
|
||||
this.email = options.email;
|
||||
}
|
||||
|
||||
Acme1.prototype.getNonce = function (callback) {
|
||||
superagent.get(this.caOrigin + '/directory').timeout(30 * 1000).end(function (error, response) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (response.statusCode !== 200) return callback(new Error('Invalid response code when fetching nonce : ' + response.statusCode));
|
||||
|
||||
return callback(null, response.headers['Replay-Nonce'.toLowerCase()]);
|
||||
});
|
||||
};
|
||||
|
||||
// urlsafe base64 encoding (jose)
|
||||
function urlBase64Encode(string) {
|
||||
return string.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
||||
}
|
||||
|
||||
function b64(str) {
|
||||
var buf = util.isBuffer(str) ? str : new Buffer(str);
|
||||
return urlBase64Encode(buf.toString('base64'));
|
||||
}
|
||||
|
||||
function getModulus(pem) {
|
||||
assert(util.isBuffer(pem));
|
||||
|
||||
var stdout = execSync('openssl rsa -modulus -noout', { input: pem, encoding: 'utf8' });
|
||||
if (!stdout) return null;
|
||||
var match = stdout.match(/Modulus=([0-9a-fA-F]+)$/m);
|
||||
if (!match) return null;
|
||||
return Buffer.from(match[1], 'hex');
|
||||
}
|
||||
|
||||
Acme1.prototype.sendSignedRequest = function (url, payload, callback) {
|
||||
assert.strictEqual(typeof url, 'string');
|
||||
assert.strictEqual(typeof payload, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
assert(util.isBuffer(this.accountKeyPem));
|
||||
|
||||
var that = this;
|
||||
var header = {
|
||||
alg: 'RS256',
|
||||
jwk: {
|
||||
e: b64(Buffer.from([0x01, 0x00, 0x01])), // exponent - 65537
|
||||
kty: 'RSA',
|
||||
n: b64(getModulus(this.accountKeyPem))
|
||||
}
|
||||
};
|
||||
|
||||
var payload64 = b64(payload);
|
||||
|
||||
this.getNonce(function (error, nonce) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('sendSignedRequest: using nonce %s for url %s', nonce, url);
|
||||
|
||||
var protected64 = b64(JSON.stringify(_.extend({ }, header, { nonce: nonce })));
|
||||
|
||||
var signer = crypto.createSign('RSA-SHA256');
|
||||
signer.update(protected64 + '.' + payload64, 'utf8');
|
||||
var signature64 = urlBase64Encode(signer.sign(that.accountKeyPem, 'base64'));
|
||||
|
||||
var data = {
|
||||
header: header,
|
||||
protected: protected64,
|
||||
payload: payload64,
|
||||
signature: signature64
|
||||
};
|
||||
|
||||
superagent.post(url).set('Content-Type', 'application/x-www-form-urlencoded').send(JSON.stringify(data)).timeout(30 * 1000).end(function (error, res) {
|
||||
if (error && !error.response) return callback(error); // network errors
|
||||
|
||||
callback(null, res);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Acme1.prototype.updateContact = function (registrationUri, callback) {
|
||||
assert.strictEqual(typeof registrationUri, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('updateContact: %s %s', registrationUri, this.email);
|
||||
|
||||
// https://github.com/ietf-wg-acme/acme/issues/30
|
||||
var payload = {
|
||||
resource: 'reg',
|
||||
contact: [ 'mailto:' + this.email ],
|
||||
agreement: LE_AGREEMENT
|
||||
};
|
||||
|
||||
var that = this;
|
||||
this.sendSignedRequest(registrationUri, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
|
||||
if (result.statusCode !== 202) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to update contact. Expecting 202, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
debug('updateContact: contact of user updated to %s', that.email);
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
Acme1.prototype.registerUser = function (callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var payload = {
|
||||
resource: 'new-reg',
|
||||
contact: [ 'mailto:' + this.email ],
|
||||
agreement: LE_AGREEMENT
|
||||
};
|
||||
|
||||
debug('registerUser: %s', this.email);
|
||||
|
||||
var that = this;
|
||||
this.sendSignedRequest(this.caOrigin + '/acme/new-reg', JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
|
||||
if (result.statusCode === 409) return that.updateContact(result.headers.location, callback); // already exists
|
||||
if (result.statusCode !== 201) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
debug('registerUser: registered user %s', that.email);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
};
|
||||
|
||||
Acme1.prototype.registerDomain = function (domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var payload = {
|
||||
resource: 'new-authz',
|
||||
identifier: {
|
||||
type: 'dns',
|
||||
value: domain
|
||||
}
|
||||
};
|
||||
|
||||
debug('registerDomain: %s', domain);
|
||||
|
||||
this.sendSignedRequest(this.caOrigin + '/acme/new-authz', JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when registering domain: ' + error.message));
|
||||
if (result.statusCode === 403) return callback(new Acme1Error(Acme1Error.FORBIDDEN, result.body.detail));
|
||||
if (result.statusCode !== 201) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
debug('registerDomain: registered %s', domain);
|
||||
|
||||
callback(null, result.body);
|
||||
});
|
||||
};
|
||||
|
||||
Acme1.prototype.prepareHttpChallenge = function (challenge, callback) {
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('prepareHttpChallenge: preparing for challenge %j', challenge);
|
||||
|
||||
var token = challenge.token;
|
||||
|
||||
assert(util.isBuffer(this.accountKeyPem));
|
||||
|
||||
var jwk = {
|
||||
e: b64(Buffer.from([0x01, 0x00, 0x01])), // Exponent - 65537
|
||||
kty: 'RSA',
|
||||
n: b64(getModulus(this.accountKeyPem))
|
||||
};
|
||||
|
||||
var shasum = crypto.createHash('sha256');
|
||||
shasum.update(JSON.stringify(jwk));
|
||||
var thumbprint = urlBase64Encode(shasum.digest('base64'));
|
||||
var keyAuthorization = token + '.' + thumbprint;
|
||||
|
||||
debug('prepareHttpChallenge: writing %s to %s', keyAuthorization, path.join(paths.ACME_CHALLENGES_DIR, token));
|
||||
|
||||
fs.writeFile(path.join(paths.ACME_CHALLENGES_DIR, token), token + '.' + thumbprint, function (error) {
|
||||
if (error) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, error));
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
Acme1.prototype.notifyChallengeReady = function (challenge, callback) {
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('notifyChallengeReady: %s was met', challenge.uri);
|
||||
|
||||
var keyAuthorization = fs.readFileSync(path.join(paths.ACME_CHALLENGES_DIR, challenge.token), 'utf8');
|
||||
|
||||
var payload = {
|
||||
resource: 'challenge',
|
||||
keyAuthorization: keyAuthorization
|
||||
};
|
||||
|
||||
this.sendSignedRequest(challenge.uri, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when notifying challenge: ' + error.message));
|
||||
if (result.statusCode !== 202) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 202, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
Acme1.prototype.waitForChallenge = function (challenge, callback) {
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('waitingForChallenge: %j', challenge);
|
||||
|
||||
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
|
||||
debug('waitingForChallenge: getting status');
|
||||
|
||||
superagent.get(challenge.uri).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) {
|
||||
debug('waitForChallenge: network error getting uri %s', challenge.uri);
|
||||
return retryCallback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, error.message)); // network error
|
||||
}
|
||||
if (result.statusCode !== 202) {
|
||||
debug('waitForChallenge: invalid response code getting uri %s', result.statusCode);
|
||||
return retryCallback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
|
||||
}
|
||||
|
||||
debug('waitForChallenge: status is "%s %j', result.body.status, result.body);
|
||||
|
||||
if (result.body.status === 'pending') return retryCallback(new Acme1Error(Acme1Error.NOT_COMPLETED));
|
||||
else if (result.body.status === 'valid') return retryCallback();
|
||||
else return retryCallback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Unexpected status: ' + result.body.status));
|
||||
});
|
||||
}, function retryFinished(error) {
|
||||
// async.retry will pass 'undefined' as second arg making it unusable with async.waterfall()
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
// https://community.letsencrypt.org/t/public-beta-rate-limits/4772 for rate limits
|
||||
Acme1.prototype.signCertificate = function (domain, csrDer, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert(util.isBuffer(csrDer));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var outdir = paths.APP_CERTS_DIR;
|
||||
|
||||
var payload = {
|
||||
resource: 'new-cert',
|
||||
csr: b64(csrDer)
|
||||
};
|
||||
|
||||
debug('signCertificate: sending new-cert request');
|
||||
|
||||
this.sendSignedRequest(this.caOrigin + '/acme/new-cert', JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when signing certificate: ' + error.message));
|
||||
// 429 means we reached the cert limit for this domain
|
||||
if (result.statusCode !== 201) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
var certUrl = result.headers.location;
|
||||
|
||||
if (!certUrl) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Missing location in downloadCertificate'));
|
||||
|
||||
safe.fs.writeFileSync(path.join(outdir, domain + '.url'), certUrl, 'utf8'); // maybe use for renewal
|
||||
|
||||
return callback(null, result.headers.location);
|
||||
});
|
||||
};
|
||||
|
||||
Acme1.prototype.createKeyAndCsr = function (domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var outdir = paths.APP_CERTS_DIR;
|
||||
var csrFile = path.join(outdir, domain + '.csr');
|
||||
var privateKeyFile = path.join(outdir, domain + '.key');
|
||||
|
||||
if (safe.fs.existsSync(privateKeyFile)) {
|
||||
// in some old releases, csr file was corrupt. so always regenerate it
|
||||
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
|
||||
} else {
|
||||
var key = execSync('openssl genrsa 4096');
|
||||
if (!key) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
|
||||
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
|
||||
|
||||
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
|
||||
}
|
||||
|
||||
var csrDer = execSync(util.format('openssl req -new -key %s -outform DER -subj /CN=%s', privateKeyFile, domain));
|
||||
if (!csrDer) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
|
||||
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error)); // bookkeeping
|
||||
|
||||
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFile);
|
||||
|
||||
callback(null, csrDer);
|
||||
};
|
||||
|
||||
// TODO: download the chain in a loop following 'up' header
|
||||
Acme1.prototype.downloadChain = function (linkHeader, callback) {
|
||||
if (!linkHeader) return new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Empty link header when downloading certificate chain');
|
||||
|
||||
debug('downloadChain: linkHeader %s', linkHeader);
|
||||
|
||||
var linkInfo = parseLinks(linkHeader);
|
||||
if (!linkInfo || !linkInfo.up) return new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Failed to parse link header when downloading certificate chain');
|
||||
|
||||
var intermediateCertUrl = linkInfo.up.startsWith('https://') ? linkInfo.up : (this.caOrigin + linkInfo.up);
|
||||
|
||||
debug('downloadChain: downloading from %s', intermediateCertUrl);
|
||||
|
||||
superagent.get(intermediateCertUrl).buffer().parse(function (res, done) {
|
||||
var data = [ ];
|
||||
res.on('data', function(chunk) { data.push(chunk); });
|
||||
res.on('end', function () { res.text = Buffer.concat(data); done(); });
|
||||
}).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when downloading certificate'));
|
||||
if (result.statusCode !== 200) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
var chainDer = result.text;
|
||||
var chainPem = execSync('openssl x509 -inform DER -outform PEM', { input: chainDer }); // this is really just base64 encoding with header
|
||||
if (!chainPem) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
|
||||
|
||||
callback(null, chainPem);
|
||||
});
|
||||
};
|
||||
|
||||
Acme1.prototype.downloadCertificate = function (domain, certUrl, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof certUrl, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var outdir = paths.APP_CERTS_DIR;
|
||||
var that = this;
|
||||
|
||||
superagent.get(certUrl).buffer().parse(function (res, done) {
|
||||
var data = [ ];
|
||||
res.on('data', function(chunk) { data.push(chunk); });
|
||||
res.on('end', function () { res.text = Buffer.concat(data); done(); });
|
||||
}).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when downloading certificate'));
|
||||
if (result.statusCode === 202) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, 'Retry not implemented yet'));
|
||||
if (result.statusCode !== 200) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
var certificateDer = result.text;
|
||||
|
||||
safe.fs.writeFileSync(path.join(outdir, domain + '.der'), certificateDer);
|
||||
debug('downloadCertificate: cert der file for %s saved', domain);
|
||||
|
||||
var certificatePem = execSync('openssl x509 -inform DER -outform PEM', { input: certificateDer }); // this is really just base64 encoding with header
|
||||
if (!certificatePem) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
|
||||
|
||||
that.downloadChain(result.header['link'], function (error, chainPem) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var certificateFile = path.join(outdir, domain + '.cert');
|
||||
var fullChainPem = Buffer.concat([certificatePem, chainPem]);
|
||||
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
|
||||
|
||||
debug('downloadCertificate: cert file for %s saved at %s', domain, certificateFile);
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Acme1.prototype.acmeFlow = function (domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!fs.existsSync(paths.ACME_ACCOUNT_KEY_FILE)) {
|
||||
debug('getCertificate: generating acme account key on first run');
|
||||
this.accountKeyPem = safe.child_process.execSync('openssl genrsa 4096');
|
||||
if (!this.accountKeyPem) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
|
||||
|
||||
safe.fs.writeFileSync(paths.ACME_ACCOUNT_KEY_FILE, this.accountKeyPem);
|
||||
} else {
|
||||
debug('getCertificate: using existing acme account key');
|
||||
this.accountKeyPem = fs.readFileSync(paths.ACME_ACCOUNT_KEY_FILE);
|
||||
}
|
||||
|
||||
var that = this;
|
||||
this.registerUser(function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
that.registerDomain(domain, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('acmeFlow: challenges: %j', result);
|
||||
|
||||
var httpChallenges = result.challenges.filter(function(x) { return x.type === 'http-01'; });
|
||||
if (httpChallenges.length === 0) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'no http challenges'));
|
||||
var challenge = httpChallenges[0];
|
||||
|
||||
async.waterfall([
|
||||
that.prepareHttpChallenge.bind(that, challenge),
|
||||
that.notifyChallengeReady.bind(that, challenge),
|
||||
that.waitForChallenge.bind(that, challenge),
|
||||
that.createKeyAndCsr.bind(that, domain),
|
||||
that.signCertificate.bind(that, domain),
|
||||
that.downloadCertificate.bind(that, domain)
|
||||
], callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Acme1.prototype.getCertificate = function (hostname, domain, callback) {
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('getCertificate: start acme flow for %s from %s', hostname, this.caOrigin);
|
||||
this.acmeFlow(hostname, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var outdir = paths.APP_CERTS_DIR;
|
||||
callback(null, path.join(outdir, hostname + '.cert'), path.join(outdir, hostname + '.key'));
|
||||
});
|
||||
};
|
||||
|
||||
function getCertificate(hostname, domain, options, callback) {
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var acme = new Acme1(options || { });
|
||||
acme.getCertificate(hostname, domain, callback);
|
||||
}
|
||||
@@ -5,7 +5,6 @@ var assert = require('assert'),
|
||||
crypto = require('crypto'),
|
||||
debug = require('debug')('box:cert/acme2'),
|
||||
domains = require('../domains.js'),
|
||||
execSync = require('safetydance').child_process.execSync,
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
paths = require('../paths.js'),
|
||||
@@ -88,7 +87,7 @@ function b64(str) {
|
||||
function getModulus(pem) {
|
||||
assert(util.isBuffer(pem));
|
||||
|
||||
var stdout = execSync('openssl rsa -modulus -noout', { input: pem, encoding: 'utf8' });
|
||||
var stdout = safe.child_process.execSync('openssl rsa -modulus -noout', { input: pem, encoding: 'utf8' });
|
||||
if (!stdout) return null;
|
||||
var match = stdout.match(/Modulus=([0-9a-fA-F]+)$/m);
|
||||
if (!match) return null;
|
||||
@@ -351,14 +350,14 @@ Acme2.prototype.createKeyAndCsr = function (hostname, callback) {
|
||||
// in some old releases, csr file was corrupt. so always regenerate it
|
||||
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
|
||||
} else {
|
||||
var key = execSync('openssl genrsa 4096');
|
||||
var key = safe.child_process.execSync('openssl genrsa 4096');
|
||||
if (!key) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
|
||||
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
|
||||
|
||||
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
|
||||
}
|
||||
|
||||
var csrDer = execSync(`openssl req -new -key ${privateKeyFile} -outform DER -subj /CN=${hostname}`);
|
||||
var csrDer = safe.child_process.execSync(`openssl req -new -key ${privateKeyFile} -outform DER -subj /CN=${hostname}`);
|
||||
if (!csrDer) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
|
||||
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error)); // bookkeeping
|
||||
|
||||
|
||||
110
src/cloudron.js
110
src/cloudron.js
@@ -11,9 +11,12 @@ exports = module.exports = {
|
||||
getStatus: getStatus,
|
||||
|
||||
reboot: reboot,
|
||||
isRebootRequired: isRebootRequired,
|
||||
|
||||
onActivated: onActivated,
|
||||
|
||||
setDashboardDomain: setDashboardDomain,
|
||||
renewCerts: renewCerts,
|
||||
|
||||
checkDiskSpace: checkDiskSpace,
|
||||
|
||||
@@ -23,24 +26,26 @@ exports = module.exports = {
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
clients = require('./clients.js'),
|
||||
config = require('./config.js'),
|
||||
cron = require('./cron.js'),
|
||||
debug = require('debug')('box:cloudron'),
|
||||
domains = require('./domains.js'),
|
||||
DomainsError = require('./domains.js').DomainsError,
|
||||
df = require('@sindresorhus/df'),
|
||||
fs = require('fs'),
|
||||
mailer = require('./mailer.js'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
platform = require('./platform.js'),
|
||||
progress = require('./progress.js'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
shell = require('./shell.js'),
|
||||
spawn = require('child_process').spawn,
|
||||
split = require('split'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
tasks = require('./tasks.js'),
|
||||
users = require('./users.js'),
|
||||
util = require('util');
|
||||
|
||||
@@ -82,8 +87,6 @@ CloudronError.INTERNAL_ERROR = 'Internal Error';
|
||||
CloudronError.EXTERNAL_ERROR = 'External Error';
|
||||
CloudronError.BAD_STATE = 'Bad state';
|
||||
CloudronError.ALREADY_UPTODATE = 'No Update Available';
|
||||
CloudronError.NOT_FOUND = 'Not found';
|
||||
CloudronError.SELF_UPGRADE_NOT_SUPPORTED = 'Self upgrade not supported';
|
||||
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -171,7 +174,6 @@ function getConfig(callback) {
|
||||
adminFqdn: config.adminFqdn(),
|
||||
mailFqdn: config.mailFqdn(),
|
||||
version: config.version(),
|
||||
progress: progress.getAll(),
|
||||
isDemo: config.isDemo(),
|
||||
edition: config.edition(),
|
||||
memory: os.totalmem(),
|
||||
@@ -182,7 +184,14 @@ function getConfig(callback) {
|
||||
}
|
||||
|
||||
function reboot(callback) {
|
||||
shell.sudo('reboot', [ REBOOT_CMD ], callback);
|
||||
shell.sudo('reboot', [ REBOOT_CMD ], {}, callback);
|
||||
}
|
||||
|
||||
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'));
|
||||
}
|
||||
|
||||
function checkDiskSpace(callback) {
|
||||
@@ -244,49 +253,28 @@ function getLogs(unit, options, callback) {
|
||||
|
||||
debug('Getting logs for %s as %s', unit, format);
|
||||
|
||||
var cp, transformStream;
|
||||
if (unit === 'box') {
|
||||
let args = [ '--no-pager', `--lines=${lines}` ];
|
||||
if (format === 'short') args.push('--output=short', '-a'); else args.push('--output=json');
|
||||
if (follow) args.push('--follow');
|
||||
args.push('--unit=box');
|
||||
args.push('--unit=cloudron-updater');
|
||||
cp = spawn('/bin/journalctl', args);
|
||||
let args = [ '--lines=' + lines ];
|
||||
if (follow) args.push('--follow');
|
||||
|
||||
transformStream = split(function mapper(line) {
|
||||
if (format !== 'json') return line + '\n';
|
||||
// need to handle box.log without subdir
|
||||
if (unit === 'box') args.push(path.join(paths.LOG_DIR, 'box.log'));
|
||||
else args.push(path.join(paths.LOG_DIR, unit, 'app.log'));
|
||||
|
||||
var obj = safe.JSON.parse(line);
|
||||
if (!obj) return undefined;
|
||||
var cp = spawn('/usr/bin/tail', args);
|
||||
|
||||
return JSON.stringify({
|
||||
realtimeTimestamp: obj.__REALTIME_TIMESTAMP,
|
||||
monotonicTimestamp: obj.__MONOTONIC_TIMESTAMP,
|
||||
message: obj.MESSAGE,
|
||||
source: obj.SYSLOG_IDENTIFIER || ''
|
||||
}) + '\n';
|
||||
});
|
||||
} else { // mail, mongodb, mysql, postgresql, backup
|
||||
let args = [ '--lines=' + lines ];
|
||||
if (follow) args.push('--follow');
|
||||
args.push(path.join(paths.LOG_DIR, unit, 'app.log'));
|
||||
var transformStream = split(function mapper(line) {
|
||||
if (format !== 'json') return line + '\n';
|
||||
|
||||
cp = spawn('/usr/bin/tail', args);
|
||||
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
|
||||
var timestamp = (new Date(data[0])).getTime();
|
||||
if (isNaN(timestamp)) timestamp = 0;
|
||||
|
||||
transformStream = split(function mapper(line) {
|
||||
if (format !== 'json') return line + '\n';
|
||||
|
||||
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';
|
||||
});
|
||||
}
|
||||
return JSON.stringify({
|
||||
realtimeTimestamp: timestamp * 1000,
|
||||
message: line.slice(data[0].length+1),
|
||||
source: unit
|
||||
}) + '\n';
|
||||
});
|
||||
|
||||
transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process
|
||||
|
||||
@@ -365,3 +353,37 @@ function getStatus(callback) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setDashboardDomain(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`setDashboardDomain: ${domain}`);
|
||||
|
||||
domains.get(domain, function (error, result) {
|
||||
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));
|
||||
|
||||
config.setAdminDomain(result.domain);
|
||||
config.setAdminLocation('my');
|
||||
config.setAdminFqdn('my' + (result.config.hyphenatedSubdomains ? '-' : '.') + result.domain);
|
||||
|
||||
clients.addDefaultClients(config.adminOrigin(), function (error) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
|
||||
configureWebadmin(NOOP_CALLBACK); // ## trigger as task
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renewCerts(options, auditSource, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let task = tasks.startTask(tasks.TASK_RENEW_CERTS, [ options, auditSource ]);
|
||||
task.on('error', (error) => callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)));
|
||||
task.on('start', (taskId) => callback(null, taskId));
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ function recreateJobs(tz) {
|
||||
if (gJobs.certificateRenew) gJobs.certificateRenew.stop();
|
||||
gJobs.certificateRenew = new CronJob({
|
||||
cronTime: '00 00 */12 * * *', // every 12 hours
|
||||
onTick: reverseProxy.renewAll.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
|
||||
onTick: cloudron.renewCerts.bind(null, {}, AUDIT_SOURCE, NOOP_CALLBACK),
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
DockerError: DockerError,
|
||||
|
||||
connection: connectionInstance(),
|
||||
setRegistryConfig: setRegistryConfig,
|
||||
|
||||
ping: ping,
|
||||
|
||||
downloadImage: downloadImage,
|
||||
createContainer: createContainer,
|
||||
startContainer: startContainer,
|
||||
@@ -18,24 +22,26 @@ exports = module.exports = {
|
||||
getContainerIdByIp: getContainerIdByIp,
|
||||
inspect: inspect,
|
||||
inspectByName: inspect,
|
||||
memoryUsage: memoryUsage,
|
||||
execContainer: execContainer,
|
||||
createVolume: createVolume,
|
||||
removeVolume: removeVolume,
|
||||
clearVolume: clearVolume
|
||||
};
|
||||
|
||||
function connectionInstance() {
|
||||
// timeout is optional
|
||||
function connectionInstance(timeout) {
|
||||
var Docker = require('dockerode');
|
||||
var docker;
|
||||
|
||||
if (process.env.BOX_ENV === 'test') {
|
||||
// test code runs a docker proxy on this port
|
||||
docker = new Docker({ host: 'http://localhost', port: 5687 });
|
||||
docker = new Docker({ host: 'http://localhost', port: 5687, timeout: timeout });
|
||||
|
||||
// proxy code uses this to route to the real docker
|
||||
docker.options = { socketPath: '/var/run/docker.sock' };
|
||||
} else {
|
||||
docker = new Docker({ socketPath: '/var/run/docker.sock' });
|
||||
docker = new Docker({ socketPath: '/var/run/docker.sock', timeout: timeout });
|
||||
}
|
||||
|
||||
return docker;
|
||||
@@ -80,6 +86,7 @@ function DockerError(reason, errorOrMessage) {
|
||||
}
|
||||
util.inherits(DockerError, Error);
|
||||
DockerError.INTERNAL_ERROR = 'Internal Error';
|
||||
DockerError.NOT_FOUND = 'Not found';
|
||||
DockerError.BAD_FIELD = 'Bad field';
|
||||
|
||||
function debugApp(app, args) {
|
||||
@@ -104,12 +111,26 @@ function setRegistryConfig(auth, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function ping(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// do not let the request linger
|
||||
var docker = connectionInstance(1000);
|
||||
|
||||
docker.ping(function (error, result) {
|
||||
if (error) return callback(new DockerError(DockerError.INTERNAL_ERROR, error));
|
||||
if (result !== 'OK') return callback(new DockerError(DockerError.INTERNAL_ERROR, 'Unable to ping the docker daemon'));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function pullImage(manifest, callback) {
|
||||
var docker = exports.connection;
|
||||
|
||||
// Use docker CLI here to support downloading of private repos. for dockerode, we have to use
|
||||
// https://github.com/apocas/dockerode#pull-from-private-repos
|
||||
shell.exec('pullImage', '/usr/bin/docker', [ 'pull', manifest.dockerImage ], { }, function (error) {
|
||||
shell.spawn('pullImage', '/usr/bin/docker', [ 'pull', manifest.dockerImage ], {}, function (error) {
|
||||
if (error) {
|
||||
debug(`pullImage: Error pulling image ${manifest.dockerImage} of ${manifest.id}: ${error.message}`);
|
||||
return callback(new Error('Failed to pull image'));
|
||||
@@ -161,8 +182,10 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
var manifest = app.manifest;
|
||||
var exposedPorts = {}, dockerPortBindings = { };
|
||||
var domain = app.fqdn;
|
||||
// TODO: these should all have the CLOUDRON_ prefix
|
||||
var stdEnv = [
|
||||
'CLOUDRON=1',
|
||||
'CLOUDRON_PROXY_IP=172.18.0.1',
|
||||
'WEBADMIN_ORIGIN=' + config.adminOrigin(),
|
||||
'API_ORIGIN=' + config.adminOrigin(),
|
||||
'APP_ORIGIN=https://' + domain,
|
||||
@@ -204,9 +227,6 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
// if required, we can make this a manifest and runtime argument later
|
||||
if (!isAppContainer) memoryLimit *= 2;
|
||||
|
||||
// apparmor is disabled on few servers
|
||||
var enableSecurityOpt = config.CLOUDRON && safe(function () { return child_process.spawnSync('aa-enabled').status === 0; }, false);
|
||||
|
||||
addons.getEnvironment(app, function (error, addonEnv) {
|
||||
if (error) return callback(new Error('Error getting addon environment : ' + error));
|
||||
|
||||
@@ -257,7 +277,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
NetworkMode: 'cloudron',
|
||||
Dns: ['172.18.0.1'], // use internal dns
|
||||
DnsSearch: ['.'], // use internal dns
|
||||
SecurityOpt: enableSecurityOpt ? [ 'apparmor=docker-cloudron-app' ] : null // profile available only on cloudron
|
||||
SecurityOpt: [ 'apparmor=docker-cloudron-app' ]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -445,7 +465,23 @@ function inspect(containerId, callback) {
|
||||
var container = exports.connection.getContainer(containerId);
|
||||
|
||||
container.inspect(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
if (error && error.statusCode === 404) return callback(new DockerError(DockerError.NOT_FOUND));
|
||||
if (error) return callback(new DockerError(DockerError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function memoryUsage(containerId, callback) {
|
||||
assert.strictEqual(typeof containerId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var container = exports.connection.getContainer(containerId);
|
||||
|
||||
container.stats({ stream: false }, function (error, result) {
|
||||
if (error && error.statusCode === 404) return callback(new DockerError(DockerError.NOT_FOUND));
|
||||
if (error) return callback(new DockerError(DockerError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
@@ -522,7 +558,7 @@ function clearVolume(app, name, subdir, callback) {
|
||||
assert.strictEqual(typeof subdir, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
shell.sudo('removeVolume', [ RMVOLUME_CMD, app.id, subdir ], callback);
|
||||
shell.sudo('removeVolume', [ RMVOLUME_CMD, app.id, subdir ], {}, callback);
|
||||
}
|
||||
|
||||
function removeVolume(app, name, subdir, callback) {
|
||||
@@ -540,6 +576,6 @@ function removeVolume(app, name, subdir, callback) {
|
||||
callback(error);
|
||||
}
|
||||
|
||||
shell.sudo('removeVolume', [ RMVOLUME_CMD, app.id, subdir ], callback);
|
||||
shell.sudo('removeVolume', [ RMVOLUME_CMD, app.id, subdir ], {}, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,8 +8,7 @@ exports = module.exports = {
|
||||
getAll: getAll,
|
||||
update: update,
|
||||
del: del,
|
||||
|
||||
_clear: clear
|
||||
clear: clear
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
|
||||
@@ -6,12 +6,10 @@ module.exports = exports = {
|
||||
getAll: getAll,
|
||||
update: update,
|
||||
del: del,
|
||||
clear: clear,
|
||||
isLocked: isLocked,
|
||||
|
||||
renewCerts: renewCerts,
|
||||
|
||||
fqdn: fqdn,
|
||||
setAdmin: setAdmin,
|
||||
|
||||
getDnsRecords: getDnsRecords,
|
||||
upsertDnsRecords: upsertDnsRecords,
|
||||
@@ -35,26 +33,20 @@ module.exports = exports = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
caas = require('./caas.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:domains'),
|
||||
domaindb = require('./domaindb.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
path = require('path'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
ReverseProxyError = reverseProxy.ReverseProxyError,
|
||||
safe = require('safetydance'),
|
||||
shell = require('./shell.js'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
tld = require('tldjs'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
var RESTART_CMD = path.join(__dirname, 'scripts/restart.sh');
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
function DomainsError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
@@ -369,6 +361,16 @@ function del(domain, auditSource, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function clear(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
domaindb.clear(function (error) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
// returns the 'name' that needs to be inserted into zone
|
||||
function getName(domain, subdomain, type) {
|
||||
// hack for supporting special caas domains. if we want to remove this, we have to fix the appstore domain API first
|
||||
@@ -468,31 +470,6 @@ function waitForDnsRecord(subdomain, domain, type, value, options, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function setAdmin(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('setAdmin domain:%s', domain);
|
||||
|
||||
get(domain, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var setPtrRecord = config.provider() === 'caas' ? caas.setPtrRecord : function (d, next) { next(); };
|
||||
|
||||
setPtrRecord(domain, function (error) {
|
||||
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Error setting PTR record:' + error.message));
|
||||
|
||||
config.setAdminDomain(result.domain);
|
||||
config.setAdminLocation('my');
|
||||
config.setAdminFqdn('my' + (result.config.hyphenatedSubdomains ? '-' : '.') + result.domain);
|
||||
|
||||
callback();
|
||||
|
||||
shell.sudo('restart', [ RESTART_CMD ], NOOP_CALLBACK);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// removes all fields that are strictly private and should never be returned by API calls
|
||||
function removePrivateFields(domain) {
|
||||
var result = _.pick(domain, 'domain', 'zoneName', 'provider', 'config', 'tlsConfig', 'fallbackCertificate', 'locked');
|
||||
@@ -517,16 +494,3 @@ function makeWildcard(hostname) {
|
||||
parts[0] = '*';
|
||||
return parts.join('.');
|
||||
}
|
||||
|
||||
function renewCerts(domain, auditSource, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// trigger renewal in the background
|
||||
reverseProxy.renewCerts({ domain: domain }, auditSource, function (error) {
|
||||
debug('renewCerts', error);
|
||||
});
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
@@ -31,12 +31,10 @@ function startGraphite(existingInfra, callback) {
|
||||
--dns-search=. \
|
||||
-p 127.0.0.1:2003:2003 \
|
||||
-p 127.0.0.1:2004:2004 \
|
||||
-p 127.0.0.1:8000:8000 \
|
||||
-p 127.0.0.1:8417:8000 \
|
||||
-v "${dataDir}/graphite:/var/lib/graphite" \
|
||||
--label isCloudronManaged=true \
|
||||
--read-only -v /tmp -v /run "${tag}"`;
|
||||
|
||||
shell.execSync('startGraphite', cmd);
|
||||
|
||||
callback();
|
||||
shell.exec('startGraphite', cmd, callback);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ function get(groupId, callback) {
|
||||
assert.strictEqual(typeof groupId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + GROUPS_FIELDS + ' FROM groups WHERE id = ? ORDER BY name', [ groupId ], function (error, result) {
|
||||
database.query('SELECT ' + GROUPS_FIELDS + ' FROM userGroups WHERE id = ? ORDER BY name', [ groupId ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
@@ -47,9 +47,9 @@ function getWithMembers(groupId, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + GROUPS_FIELDS + ',GROUP_CONCAT(groupMembers.userId) AS userIds ' +
|
||||
' FROM groups LEFT OUTER JOIN groupMembers ON groups.id = groupMembers.groupId ' +
|
||||
' WHERE groups.id = ? ' +
|
||||
' GROUP BY groups.id', [ groupId ], function (error, results) {
|
||||
' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' +
|
||||
' WHERE userGroups.id = ? ' +
|
||||
' GROUP BY userGroups.id', [ groupId ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
@@ -63,7 +63,7 @@ function getWithMembers(groupId, callback) {
|
||||
function getAll(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + GROUPS_FIELDS + ' FROM groups', function (error, results) {
|
||||
database.query('SELECT ' + GROUPS_FIELDS + ' FROM userGroups', function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
@@ -72,8 +72,8 @@ function getAll(callback) {
|
||||
|
||||
function getAllWithMembers(callback) {
|
||||
database.query('SELECT ' + GROUPS_FIELDS + ',GROUP_CONCAT(groupMembers.userId) AS userIds ' +
|
||||
' FROM groups LEFT OUTER JOIN groupMembers ON groups.id = groupMembers.groupId ' +
|
||||
' GROUP BY groups.id', function (error, results) {
|
||||
' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' +
|
||||
' GROUP BY userGroups.id', function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
@@ -88,7 +88,7 @@ function add(id, name, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('INSERT INTO groups (id, name) VALUES (?, ?)', [ id, name ], function (error, result) {
|
||||
database.query('INSERT INTO userGroups (id, name) VALUES (?, ?)', [ id, name ], function (error, result) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
|
||||
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -112,8 +112,8 @@ function update(id, data, callback) {
|
||||
}
|
||||
args.push(id);
|
||||
|
||||
database.query('UPDATE groups SET ' + fields.join(', ') + ' WHERE id = ?', args, function (error, result) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('groups_name') !== -1) return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'name already exists'));
|
||||
database.query('UPDATE userGroups SET ' + fields.join(', ') + ' WHERE id = ?', args, function (error, result) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('userGroups_name') !== -1) return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'name already exists'));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
@@ -128,7 +128,7 @@ function del(id, callback) {
|
||||
// also cleanup the groupMembers table
|
||||
var queries = [];
|
||||
queries.push({ query: 'DELETE FROM groupMembers WHERE groupId = ?', args: [ id ] });
|
||||
queries.push({ query: 'DELETE FROM groups WHERE id = ?', args: [ id ] });
|
||||
queries.push({ query: 'DELETE FROM userGroups WHERE id = ?', args: [ id ] });
|
||||
|
||||
database.transaction(queries, function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
@@ -141,7 +141,7 @@ function del(id, callback) {
|
||||
function count(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT COUNT(*) AS total FROM groups', function (error, result) {
|
||||
database.query('SELECT COUNT(*) AS total FROM userGroups', function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, result[0].total);
|
||||
@@ -152,7 +152,7 @@ function clear(callback) {
|
||||
database.query('DELETE FROM groupMembers', function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
database.query('DELETE FROM groups', function (error) {
|
||||
database.query('DELETE FROM userGroups', function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(error);
|
||||
@@ -266,7 +266,7 @@ function getGroups(userId, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + GROUPS_FIELDS + ' ' +
|
||||
' FROM groups INNER JOIN groupMembers ON groups.id = groupMembers.groupId AND groupMembers.userId = ?', [ userId ], function (error, results) {
|
||||
' FROM userGroups INNER JOIN groupMembers ON userGroups.id = groupMembers.groupId AND groupMembers.userId = ?', [ userId ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
|
||||
@@ -19,7 +19,7 @@ exports = module.exports = {
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.0.2@sha256:6dcee0731dfb9b013ed94d56205eee219040ee806c7e251db3b3886eaa4947ff' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.0.2@sha256:95e006390ddce7db637e1672eb6f3c257d3c2652747424f529b1dee3cbe6728c' },
|
||||
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.0.0@sha256:8a88dd334b62b578530a014ca1a2425a54cb9df1e475f5d3a36806e5cfa22121' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.0.0@sha256:3c0fbb2a042ac471940ac3e9f6ffa900c8a294941fb7de509b2e3309b09fbffd' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.0@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' }
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.0.1@sha256:deee3739011670d45abd8997a8a0b8d3c4cd577a93f235417614dea58338e0f9' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.2@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' }
|
||||
}
|
||||
};
|
||||
|
||||
54
src/ldap.js
54
src/ldap.js
@@ -271,7 +271,6 @@ function mailboxSearch(req, res, next) {
|
||||
cn: `${mailbox.name}@${mailbox.domain}`,
|
||||
uid: `${mailbox.name}@${mailbox.domain}`,
|
||||
mail: `${mailbox.name}@${mailbox.domain}`,
|
||||
ownerType: mailbox.ownerType,
|
||||
displayname: 'Max Mustermann',
|
||||
givenName: 'Max',
|
||||
username: 'mmustermann',
|
||||
@@ -297,9 +296,6 @@ function mailboxSearch(req, res, next) {
|
||||
|
||||
var results = [];
|
||||
|
||||
// only send user mailboxes
|
||||
result = result.filter(function (m) { return m.ownerType === mailboxdb.OWNER_TYPE_USER; });
|
||||
|
||||
// send mailbox objects
|
||||
result.forEach(function (mailbox) {
|
||||
var dn = ldap.parseDN(`cn=${mailbox.name}@${domain},domain=${domain},ou=mailboxes,dc=cloudron`);
|
||||
@@ -311,8 +307,7 @@ function mailboxSearch(req, res, next) {
|
||||
objectcategory: 'mailbox',
|
||||
cn: `${mailbox.name}@${domain}`,
|
||||
uid: `${mailbox.name}@${domain}`,
|
||||
mail: `${mailbox.name}@${domain}`,
|
||||
ownerType: mailbox.ownerType
|
||||
mail: `${mailbox.name}@${domain}`
|
||||
}
|
||||
};
|
||||
|
||||
@@ -464,30 +459,30 @@ function authenticateMailbox(req, res, next) {
|
||||
var parts = email.split('@');
|
||||
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
const addonId = req.dn.rdns[1].attrs.ou.value.toLowerCase(); // 'sendmail' or 'recvmail'
|
||||
|
||||
mail.getDomain(parts[1], function (error, domain) {
|
||||
if (error && error.reason === MailError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (error) return next(new ldap.OperationsError(error.message));
|
||||
|
||||
mail.getDomain(parts[1], function (error, domain) {
|
||||
if (error && error.reason === MailError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (error) return next(new ldap.OperationsError(error.message));
|
||||
if (addonId === 'recvmail' && !domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
if (mailbox.ownerType === mailboxdb.OWNER_TYPE_APP) {
|
||||
var addonId = req.dn.rdns[1].attrs.ou.value.toLowerCase(); // 'sendmail' or 'recvmail'
|
||||
var name;
|
||||
if (addonId === 'sendmail') name = 'MAIL_SMTP_PASSWORD';
|
||||
else if (addonId === 'recvmail') name = 'MAIL_IMAP_PASSWORD';
|
||||
else return next(new ldap.OperationsError('Invalid DN'));
|
||||
let name;
|
||||
if (addonId === 'sendmail') name = 'MAIL_SMTP_PASSWORD';
|
||||
else if (addonId === 'recvmail') name = 'MAIL_IMAP_PASSWORD';
|
||||
else return next(new ldap.OperationsError('Invalid DN'));
|
||||
|
||||
appdb.getAddonConfigByName(mailbox.ownerId, addonId, name, function (error, value) {
|
||||
if (error) return next(new ldap.OperationsError(error.message));
|
||||
if (req.credentials !== value) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
|
||||
// note: with sendmail addon, apps can send mail without a mailbox (unlike users)
|
||||
appdb.getAppIdByAddonConfigValue(addonId, name, req.credentials || '', function (error, appId) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return next(new ldap.OperationsError(error.message));
|
||||
if (appId) { // matched app password
|
||||
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: email }, { appId: appId, addonId: addonId });
|
||||
return res.end();
|
||||
}
|
||||
|
||||
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: name }, { appId: mailbox.ownerId, addonId: addonId });
|
||||
return res.end();
|
||||
});
|
||||
} else if (mailbox.ownerType === mailboxdb.OWNER_TYPE_USER) {
|
||||
if (!domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (error) return next(new ldap.OperationsError(error.message));
|
||||
|
||||
users.verify(mailbox.ownerId, req.credentials || '', function (error, result) {
|
||||
if (error && error.reason === UsersError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
@@ -497,9 +492,7 @@ function authenticateMailbox(req, res, next) {
|
||||
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) });
|
||||
res.end();
|
||||
});
|
||||
} else {
|
||||
return next(new ldap.OperationsError('Unknown ownerType for mailbox'));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -527,9 +520,8 @@ function start(callback) {
|
||||
gServer.search('ou=mailaliases,dc=cloudron', mailAliasSearch);
|
||||
gServer.search('ou=mailinglists,dc=cloudron', mailingListSearch);
|
||||
|
||||
gServer.bind('ou=mailboxes,dc=cloudron', authenticateMailbox);
|
||||
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailbox);
|
||||
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailbox);
|
||||
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailbox); // dovecot
|
||||
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailbox); // haraka
|
||||
|
||||
gServer.compare('cn=users,ou=groups,dc=cloudron', authenticateApp, groupUsersCompare);
|
||||
gServer.compare('cn=admins,ou=groups,dc=cloudron', authenticateApp, groupAdminsCompare);
|
||||
|
||||
@@ -18,7 +18,6 @@ Locker.prototype.OP_BOX_UPDATE = 'box_update';
|
||||
Locker.prototype.OP_PLATFORM_START = 'platform_start';
|
||||
Locker.prototype.OP_FULL_BACKUP = 'full_backup';
|
||||
Locker.prototype.OP_APPTASK = 'apptask';
|
||||
Locker.prototype.OP_MIGRATE = 'migrate';
|
||||
|
||||
Locker.prototype.lock = function (operation) {
|
||||
assert.strictEqual(typeof operation, 'string');
|
||||
|
||||
@@ -22,6 +22,8 @@ function collectLogs(unitName, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var logs = safe.child_process.execSync('sudo ' + COLLECT_LOGS_CMD + ' ' + unitName, { encoding: 'utf8' });
|
||||
if (!logs) return callback(safe.error);
|
||||
|
||||
logs = logs + '\n\n=====================================\n\n';
|
||||
|
||||
callback(null, logs);
|
||||
|
||||
81
src/mail.js
81
src/mail.js
@@ -8,6 +8,7 @@ exports = module.exports = {
|
||||
getDomain: getDomain,
|
||||
addDomain: addDomain,
|
||||
removeDomain: removeDomain,
|
||||
clearDomains: clearDomains,
|
||||
|
||||
setDnsRecords: setDnsRecords,
|
||||
|
||||
@@ -22,11 +23,11 @@ exports = module.exports = {
|
||||
|
||||
sendTestMail: sendTestMail,
|
||||
|
||||
getMailboxes: getMailboxes,
|
||||
listMailboxes: listMailboxes,
|
||||
removeMailboxes: removeMailboxes,
|
||||
getMailbox: getMailbox,
|
||||
addMailbox: addMailbox,
|
||||
updateMailbox: updateMailbox,
|
||||
updateMailboxOwner: updateMailboxOwner,
|
||||
removeMailbox: removeMailbox,
|
||||
|
||||
listAliases: listAliases,
|
||||
@@ -52,13 +53,13 @@ var assert = require('assert'),
|
||||
dns = require('./native-dns.js'),
|
||||
domains = require('./domains.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
hat = require('./hat.js'),
|
||||
infra = require('./infra_version.js'),
|
||||
mailboxdb = require('./mailboxdb.js'),
|
||||
maildb = require('./maildb.js'),
|
||||
mailer = require('./mailer.js'),
|
||||
net = require('net'),
|
||||
nodemailer = require('nodemailer'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
@@ -109,9 +110,6 @@ function validateName(name) {
|
||||
// also need to consider valid LDAP characters here (e.g '+' is reserved)
|
||||
if (/[^a-zA-Z0-9.-]/.test(name)) return new MailError(MailError.BAD_FIELD, 'mailbox name can only contain alphanumerals and dot');
|
||||
|
||||
// app emails are sent using the .app suffix
|
||||
if (name.indexOf('.app') !== -1) return new MailError(MailError.BAD_FIELD, 'mailbox name pattern is reserved for apps');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -555,6 +553,7 @@ function restartMail(callback) {
|
||||
|
||||
const tag = infra.images.mail.tag;
|
||||
const memoryLimit = 4 * 256;
|
||||
const cloudronToken = hat(8 * 128);
|
||||
|
||||
// admin and mail share the same certificate
|
||||
reverseProxy.getCertificate({ fqdn: config.adminFqdn(), domain: config.adminDomain() }, function (error, bundle) {
|
||||
@@ -567,34 +566,35 @@ function restartMail(callback) {
|
||||
if (!safe.child_process.execSync(`cp ${bundle.certFilePath} ${mailCertFilePath}`)) return callback(new Error('Could not create cert file:' + safe.error.message));
|
||||
if (!safe.child_process.execSync(`cp ${bundle.keyFilePath} ${mailKeyFilePath}`)) return callback(new Error('Could not create key file:' + safe.error.message));
|
||||
|
||||
shell.execSync('startMail', 'docker rm -f mail || true');
|
||||
|
||||
createMailConfig(function (error, allowInbound) {
|
||||
shell.exec('startMail', 'docker rm -f mail || true', function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var ports = allowInbound ? '-p 587:2525 -p 993:9993 -p 4190:4190 -p 25:2525' : '';
|
||||
createMailConfig(function (error, allowInbound) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const cmd = `docker run --restart=always -d --name="mail" \
|
||||
--net cloudron \
|
||||
--net-alias mail \
|
||||
--log-driver syslog \
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag=mail \
|
||||
-m ${memoryLimit}m \
|
||||
--memory-swap ${memoryLimit * 2}m \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
-v "${paths.MAIL_DATA_DIR}:/app/data" \
|
||||
-v "${paths.PLATFORM_DATA_DIR}/addons/mail:/etc/mail" \
|
||||
${ports} \
|
||||
-p 127.0.0.1:2020:2020 \
|
||||
--label isCloudronManaged=true \
|
||||
--read-only -v /run -v /tmp ${tag}`;
|
||||
var ports = allowInbound ? '-p 587:2525 -p 993:9993 -p 4190:4190 -p 25:2525' : '';
|
||||
|
||||
shell.execSync('startMail', cmd);
|
||||
const cmd = `docker run --restart=always -d --name="mail" \
|
||||
--net cloudron \
|
||||
--net-alias mail \
|
||||
--log-driver syslog \
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag=mail \
|
||||
-m ${memoryLimit}m \
|
||||
--memory-swap ${memoryLimit * 2}m \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
-e CLOUDRON_MAIL_TOKEN="${cloudronToken}" \
|
||||
-v "${paths.MAIL_DATA_DIR}:/app/data" \
|
||||
-v "${paths.PLATFORM_DATA_DIR}/addons/mail:/etc/mail" \
|
||||
${ports} \
|
||||
-p 127.0.0.1:2020:2020 \
|
||||
--label isCloudronManaged=true \
|
||||
--read-only -v /run -v /tmp ${tag}`;
|
||||
|
||||
callback();
|
||||
shell.exec('startMail', cmd, callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -628,7 +628,7 @@ function getDomain(domain, callback) {
|
||||
function getDomains(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
maildb.getAll(function (error, results) {
|
||||
maildb.list(function (error, results) {
|
||||
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, results);
|
||||
@@ -805,6 +805,16 @@ function removeDomain(domain, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function clearDomains(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
maildb.clear(function (error) {
|
||||
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function setMailFromValidation(domain, enabled, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof enabled, 'boolean');
|
||||
@@ -886,7 +896,7 @@ function sendTestMail(domain, to, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getMailboxes(domain, callback) {
|
||||
function listMailboxes(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -933,7 +943,7 @@ function addMailbox(name, domain, userId, auditSource, callback) {
|
||||
var error = validateName(name);
|
||||
if (error) return callback(error);
|
||||
|
||||
mailboxdb.addMailbox(name, domain, userId, mailboxdb.OWNER_TYPE_USER, function (error) {
|
||||
mailboxdb.addMailbox(name, domain, userId, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, `mailbox ${name} already exists`));
|
||||
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -943,7 +953,7 @@ function addMailbox(name, domain, userId, auditSource, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function updateMailbox(name, domain, userId, callback) {
|
||||
function updateMailboxOwner(name, domain, userId, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
@@ -951,10 +961,7 @@ function updateMailbox(name, domain, userId, callback) {
|
||||
|
||||
name = name.toLowerCase();
|
||||
|
||||
var error = validateName(name);
|
||||
if (error) return callback(error);
|
||||
|
||||
mailboxdb.updateMailbox(name, domain, userId, mailboxdb.OWNER_TYPE_USER, function (error) {
|
||||
mailboxdb.updateMailboxOwner(name, domain, userId, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such mailbox'));
|
||||
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ exports = module.exports = {
|
||||
addMailbox: addMailbox,
|
||||
addGroup: addGroup,
|
||||
|
||||
updateMailbox: updateMailbox,
|
||||
updateMailboxOwner: updateMailboxOwner,
|
||||
updateList: updateList,
|
||||
del: del,
|
||||
|
||||
@@ -29,11 +29,7 @@ exports = module.exports = {
|
||||
|
||||
TYPE_MAILBOX: 'mailbox',
|
||||
TYPE_LIST: 'list',
|
||||
TYPE_ALIAS: 'alias',
|
||||
|
||||
OWNER_TYPE_USER: 'user',
|
||||
OWNER_TYPE_APP: 'app',
|
||||
OWNER_TYPE_GROUP: 'group' // obsolete
|
||||
TYPE_ALIAS: 'alias'
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
@@ -42,7 +38,7 @@ var assert = require('assert'),
|
||||
safe = require('safetydance'),
|
||||
util = require('util');
|
||||
|
||||
var MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'ownerType', 'aliasTarget', 'creationTime', 'membersJson', 'domain' ].join(',');
|
||||
var MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'aliasTarget', 'creationTime', 'membersJson', 'domain' ].join(',');
|
||||
|
||||
function postProcess(data) {
|
||||
data.members = safe.JSON.parse(data.membersJson) || [ ];
|
||||
@@ -51,14 +47,13 @@ function postProcess(data) {
|
||||
return data;
|
||||
}
|
||||
|
||||
function addMailbox(name, domain, ownerId, ownerType, callback) {
|
||||
function addMailbox(name, domain, ownerId, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof ownerId, 'string');
|
||||
assert.strictEqual(typeof ownerType, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, ownerType) VALUES (?, ?, ?, ?, ?)', [ name, exports.TYPE_MAILBOX, domain, ownerId, ownerType ], function (error) {
|
||||
database.query('INSERT INTO mailboxes (name, type, domain, ownerId) VALUES (?, ?, ?, ?)', [ name, exports.TYPE_MAILBOX, domain, ownerId ], function (error) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'mailbox already exists'));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -66,14 +61,13 @@ function addMailbox(name, domain, ownerId, ownerType, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function updateMailbox(name, domain, ownerId, ownerType, callback) {
|
||||
function updateMailboxOwner(name, domain, ownerId, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof ownerId, 'string');
|
||||
assert.strictEqual(typeof ownerType, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('UPDATE mailboxes SET ownerId = ? WHERE name = ? AND domain = ? AND ownerType = ?', [ ownerId, name, domain, ownerType ], function (error, result) {
|
||||
database.query('UPDATE mailboxes SET ownerId = ? WHERE name = ? AND domain = ?', [ ownerId, name, domain ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
@@ -87,8 +81,8 @@ function addGroup(name, domain, members, callback) {
|
||||
assert(Array.isArray(members));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, ownerType, membersJson) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[ name, exports.TYPE_LIST, domain, 'admin', exports.OWNER_TYPE_GROUP, JSON.stringify(members) ], function (error) {
|
||||
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, membersJson) VALUES (?, ?, ?, ?, ?)',
|
||||
[ name, exports.TYPE_LIST, domain, 'admin', JSON.stringify(members) ], function (error) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'mailbox already exists'));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -259,8 +253,8 @@ function setAliasesForName(name, domain, aliases, callback) {
|
||||
// clear existing aliases
|
||||
queries.push({ query: 'DELETE FROM mailboxes WHERE aliasTarget = ? AND domain = ? AND type = ?', args: [ name, domain, exports.TYPE_ALIAS ] });
|
||||
aliases.forEach(function (alias) {
|
||||
queries.push({ query: 'INSERT INTO mailboxes (name, type, domain, aliasTarget, ownerId, ownerType) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
args: [ alias, exports.TYPE_ALIAS, domain, name, results[0].ownerId, results[0].ownerType ] });
|
||||
queries.push({ query: 'INSERT INTO mailboxes (name, type, domain, aliasTarget, ownerId) VALUES (?, ?, ?, ?, ?)',
|
||||
args: [ alias, exports.TYPE_ALIAS, domain, name, results[0].ownerId ] });
|
||||
});
|
||||
|
||||
database.transaction(queries, function (error) {
|
||||
|
||||
@@ -4,10 +4,10 @@ exports = module.exports = {
|
||||
add: add,
|
||||
del: del,
|
||||
get: get,
|
||||
getAll: getAll,
|
||||
list: list,
|
||||
update: update,
|
||||
|
||||
_clear: clear,
|
||||
clear: clear,
|
||||
|
||||
TYPE_USER: 'user',
|
||||
TYPE_APP: 'app',
|
||||
@@ -49,7 +49,8 @@ function add(domain, callback) {
|
||||
function clear(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('TRUNCATE TABLE mail', [], function (error) {
|
||||
// using TRUNCATE makes it fail foreign key check
|
||||
database.query('DELETE FROM mail', [], function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
callback(null);
|
||||
});
|
||||
@@ -81,7 +82,7 @@ function get(domain, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getAll(callback) {
|
||||
function list(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + MAILDB_FIELDS + ' FROM mail ORDER BY domain', function (error, results) {
|
||||
|
||||
@@ -7,7 +7,6 @@ var config = require('./config.js'),
|
||||
exports = module.exports = {
|
||||
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
|
||||
INFRA_VERSION_FILE: path.join(config.baseDir(), 'platformdata/INFRA_VERSION'),
|
||||
BACKUP_RESULT_FILE: path.join(config.baseDir(), 'platformdata/backup/result.txt'),
|
||||
|
||||
OLD_DATA_DIR: path.join(config.baseDir(), 'data'),
|
||||
PLATFORM_DATA_DIR: path.join(config.baseDir(), 'platformdata'),
|
||||
@@ -34,6 +33,9 @@ exports = module.exports = {
|
||||
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'boxdata/updatechecker.json'),
|
||||
|
||||
LOG_DIR: path.join(config.baseDir(), 'platformdata/logs'),
|
||||
TASKS_LOG_DIR: path.join(config.baseDir(), 'platformdata/logs/tasks'),
|
||||
|
||||
// this pattern is for the cloudron logs API route to work
|
||||
BACKUP_LOG_FILE: path.join(config.baseDir(), 'platformdata/logs/backup/app.log'),
|
||||
UPDATER_LOG_FILE: path.join(config.baseDir(), 'platformdata/logs/updater/app.log')
|
||||
};
|
||||
|
||||
@@ -16,7 +16,6 @@ var addons = require('./addons.js'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:platform'),
|
||||
execSync = require('child_process').execSync,
|
||||
fs = require('fs'),
|
||||
graphs = require('./graphs.js'),
|
||||
infra = require('./infra_version.js'),
|
||||
@@ -64,8 +63,7 @@ function start(callback) {
|
||||
// mark app state before we start addons. this gives the db import logic a chance to mark an app as errored
|
||||
startApps.bind(null, existingInfra),
|
||||
graphs.startGraphite.bind(null, existingInfra),
|
||||
addons.startAddons.bind(null, existingInfra),
|
||||
pruneInfraImages,
|
||||
addons.startServices.bind(null, existingInfra),
|
||||
fs.writeFile.bind(fs, paths.INFRA_VERSION_FILE, JSON.stringify(infra, null, 4))
|
||||
], function (error) {
|
||||
if (error) return callback(error);
|
||||
@@ -86,7 +84,9 @@ function onPlatformReady() {
|
||||
debug('onPlatformReady: platform is ready');
|
||||
exports._isReady = true;
|
||||
taskmanager.resumeTasks();
|
||||
|
||||
applyPlatformConfig(NOOP_CALLBACK);
|
||||
pruneInfraImages(NOOP_CALLBACK);
|
||||
}
|
||||
|
||||
function applyPlatformConfig(callback) {
|
||||
@@ -97,8 +97,8 @@ function applyPlatformConfig(callback) {
|
||||
settings.getPlatformConfig(function (error, platformConfig) {
|
||||
if (error) return retryCallback(error);
|
||||
|
||||
addons.updateAddonConfig(platformConfig, function (error) {
|
||||
if (error) debug('Error updating addons. Will rety in 5 minutes', platformConfig, error);
|
||||
addons.updateServiceConfig(platformConfig, function (error) {
|
||||
if (error) debug('Error updating services. Will rety in 5 minutes', platformConfig, error);
|
||||
|
||||
retryCallback(error);
|
||||
});
|
||||
@@ -110,21 +110,22 @@ function pruneInfraImages(callback) {
|
||||
debug('pruneInfraImages: checking existing images');
|
||||
|
||||
// cannot blindly remove all unused images since redis image may not be used
|
||||
let images = infra.baseImages.concat(Object.keys(infra.images).map(function (addon) { return infra.images[addon]; }));
|
||||
const images = infra.baseImages.concat(Object.keys(infra.images).map(function (addon) { return infra.images[addon]; }));
|
||||
|
||||
async.eachSeries(images, function (image, iteratorCallback) {
|
||||
let output = safe.child_process.execSync(`docker images --digests ${image.repo} --format "{{.ID}} {{.Repository}}:{{.Tag}}@{{.Digest}}"`, { encoding: 'utf8' });
|
||||
if (output === null) return iteratorCallback(safe.error);
|
||||
|
||||
for (let image of images) {
|
||||
let output = execSync(`docker images --digests ${image.repo} --format "{{.ID}} {{.Repository}}:{{.Tag}}@{{.Digest}}"`, { encoding: 'utf8' });
|
||||
let lines = output.trim().split('\n');
|
||||
for (let line of lines) {
|
||||
if (!line) continue;
|
||||
let parts = line.split(' '); // [ ID, Repo:Tag@Digest ]
|
||||
if (image.tag === parts[1]) continue; // keep
|
||||
debug(`pruneInfraImages: removing unused image of ${image.repo}: ${line}`);
|
||||
shell.execSync('pruneInfraImages', `docker rmi ${parts[0]}`);
|
||||
}
|
||||
}
|
||||
|
||||
callback();
|
||||
shell.exec('pruneInfraImages', `docker rmi ${parts[0]}`, iteratorCallback);
|
||||
}
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function stopContainers(existingInfra, callback) {
|
||||
@@ -132,8 +133,10 @@ function stopContainers(existingInfra, callback) {
|
||||
if (existingInfra.version !== infra.version) {
|
||||
// TODO: only nuke containers with isCloudronManaged=true
|
||||
debug('stopping all containers for infra upgrade');
|
||||
shell.execSync('stopContainers', 'docker ps -qa | xargs --no-run-if-empty docker stop');
|
||||
shell.execSync('stopContainers', 'docker ps -qa | xargs --no-run-if-empty docker rm -f');
|
||||
async.series([
|
||||
shell.exec.bind(null, 'stopContainers', 'docker ps -qa | xargs --no-run-if-empty docker stop'),
|
||||
shell.exec.bind(null, 'stopContainers', 'docker ps -qa | xargs --no-run-if-empty docker rm -f')
|
||||
], callback);
|
||||
} else {
|
||||
assert(typeof infra.images, 'object');
|
||||
var changedAddons = [ ];
|
||||
@@ -144,11 +147,11 @@ function stopContainers(existingInfra, callback) {
|
||||
debug('stopContainer: stopping addons for incremental infra update: %j', changedAddons);
|
||||
let filterArg = changedAddons.map(function (c) { return `--filter 'name=${c}'`; }).join(' '); // name=c matches *c*. required for redis-{appid}
|
||||
// ignore error if container not found (and fail later) so that this code works across restarts
|
||||
shell.execSync('stopContainers', `docker ps -qa ${filterArg} | xargs --no-run-if-empty docker stop || true`);
|
||||
shell.execSync('stopContainers', `docker ps -qa ${filterArg} | xargs --no-run-if-empty docker rm -f || true`);
|
||||
async.series([
|
||||
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} | xargs --no-run-if-empty docker stop || true`),
|
||||
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} | xargs --no-run-if-empty docker rm -f || true`)
|
||||
], callback);
|
||||
}
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
function startApps(existingInfra, callback) {
|
||||
@@ -165,12 +168,13 @@ function startApps(existingInfra, callback) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleCertChanged(cn) {
|
||||
function handleCertChanged(cn, callback) {
|
||||
assert.strictEqual(typeof cn, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('handleCertChanged', cn);
|
||||
|
||||
if (cn === '*.' + config.adminDomain() || cn === config.adminFqdn()) {
|
||||
mail.startMail(NOOP_CALLBACK);
|
||||
}
|
||||
if (cn === '*.' + config.adminDomain() || cn === config.adminFqdn()) return mail.startMail(callback);
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
set: set,
|
||||
setDetail: setDetail,
|
||||
clear: clear,
|
||||
getAll: getAll,
|
||||
|
||||
UPDATE: 'update',
|
||||
BACKUP: 'backup',
|
||||
MIGRATE: 'migrate'
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
debug = require('debug')('box:progress');
|
||||
|
||||
// if progress.update or progress.backup are object, they will contain 'percent' and 'message' properties
|
||||
// otherwise no such operation is currently ongoing
|
||||
var progress = {
|
||||
update: null,
|
||||
backup: null,
|
||||
migrate: null
|
||||
};
|
||||
|
||||
// We use -1 for percentage to indicate errors
|
||||
function set(tag, percent, message) {
|
||||
assert.strictEqual(typeof tag, 'string');
|
||||
assert.strictEqual(typeof percent, 'number');
|
||||
assert.strictEqual(typeof message, 'string');
|
||||
|
||||
progress[tag] = {
|
||||
percent: percent,
|
||||
message: message,
|
||||
detail: ''
|
||||
};
|
||||
|
||||
debug('%s: %s %s', tag, percent, message);
|
||||
}
|
||||
|
||||
function setDetail(tag, detail) {
|
||||
assert.strictEqual(typeof tag, 'string');
|
||||
assert.strictEqual(typeof detail, 'string');
|
||||
|
||||
if (!progress[tag]) return debug('[%s] %s', tag, detail);
|
||||
|
||||
progress[tag].detail = detail;
|
||||
}
|
||||
|
||||
function clear(tag) {
|
||||
assert.strictEqual(typeof tag, 'string');
|
||||
|
||||
progress[tag] = null;
|
||||
|
||||
debug('clearing %s', tag);
|
||||
}
|
||||
|
||||
function getAll() {
|
||||
return progress;
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
provision: provision,
|
||||
setup: setup,
|
||||
restore: restore,
|
||||
activate: activate,
|
||||
|
||||
SetupError: SetupError
|
||||
ProvisionError: ProvisionError
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
@@ -16,7 +16,7 @@ var assert = require('assert'),
|
||||
constants = require('./constants.js'),
|
||||
clients = require('./clients.js'),
|
||||
cloudron = require('./cloudron.js'),
|
||||
debug = require('debug')('box:setup'),
|
||||
debug = require('debug')('box:provision'),
|
||||
domains = require('./domains.js'),
|
||||
DomainsError = domains.DomainsError,
|
||||
eventlog = require('./eventlog.js'),
|
||||
@@ -37,7 +37,7 @@ var RESTART_CMD = path.join(__dirname, 'scripts/restart.sh');
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
function SetupError(reason, errorOrMessage) {
|
||||
function ProvisionError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
@@ -55,13 +55,13 @@ function SetupError(reason, errorOrMessage) {
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(SetupError, Error);
|
||||
SetupError.BAD_FIELD = 'Field error';
|
||||
SetupError.BAD_STATE = 'Bad State';
|
||||
SetupError.ALREADY_SETUP = 'Already Setup';
|
||||
SetupError.INTERNAL_ERROR = 'Internal Error';
|
||||
SetupError.EXTERNAL_ERROR = 'External Error';
|
||||
SetupError.ALREADY_PROVISIONED = 'Already Provisioned';
|
||||
util.inherits(ProvisionError, Error);
|
||||
ProvisionError.BAD_FIELD = 'Field error';
|
||||
ProvisionError.BAD_STATE = 'Bad State';
|
||||
ProvisionError.ALREADY_SETUP = 'Already Setup';
|
||||
ProvisionError.INTERNAL_ERROR = 'Internal Error';
|
||||
ProvisionError.EXTERNAL_ERROR = 'External Error';
|
||||
ProvisionError.ALREADY_PROVISIONED = 'Already Provisioned';
|
||||
|
||||
function autoprovision(autoconf, callback) {
|
||||
assert.strictEqual(typeof autoconf, 'object');
|
||||
@@ -89,64 +89,72 @@ function autoprovision(autoconf, callback) {
|
||||
return iteratorDone();
|
||||
}
|
||||
}, function (error) {
|
||||
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function provision(dnsConfig, autoconf, auditSource, callback) {
|
||||
function unprovision(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('unprovision');
|
||||
|
||||
config.setAdminDomain('');
|
||||
config.setAdminFqdn('');
|
||||
config.setAdminLocation('my');
|
||||
|
||||
// TODO: also cancel any existing configureWebadmin task
|
||||
async.series([
|
||||
mail.clearDomains,
|
||||
domains.clear
|
||||
], callback);
|
||||
}
|
||||
|
||||
function setup(dnsConfig, autoconf, auditSource, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof autoconf, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (config.adminDomain()) return callback(new SetupError(SetupError.ALREADY_SETUP));
|
||||
users.isActivated(function (error, activated) {
|
||||
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
|
||||
if (activated) return callback(new ProvisionError(ProvisionError.ALREADY_SETUP));
|
||||
|
||||
let webadminStatus = cloudron.getWebadminStatus();
|
||||
unprovision(function (error) {
|
||||
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
|
||||
|
||||
if (webadminStatus.configuring || webadminStatus.restore.active) return callback(new SetupError(SetupError.BAD_STATE, 'Already restoring or configuring'));
|
||||
let webadminStatus = cloudron.getWebadminStatus();
|
||||
|
||||
const domain = dnsConfig.domain.toLowerCase();
|
||||
const zoneName = dnsConfig.zoneName ? dnsConfig.zoneName : (tld.getDomain(domain) || domain);
|
||||
if (webadminStatus.configuring || webadminStatus.restore.active) return callback(new ProvisionError(ProvisionError.BAD_STATE, 'Already restoring or configuring'));
|
||||
|
||||
const adminFqdn = 'my' + (dnsConfig.config.hyphenatedSubdomains ? '-' : '.') + domain;
|
||||
const domain = dnsConfig.domain.toLowerCase();
|
||||
const zoneName = dnsConfig.zoneName ? dnsConfig.zoneName : (tld.getDomain(domain) || domain);
|
||||
|
||||
debug(`provision: Setting up Cloudron with domain ${domain} and zone ${zoneName} using admin fqdn ${adminFqdn}`);
|
||||
const adminFqdn = 'my' + (dnsConfig.config.hyphenatedSubdomains ? '-' : '.') + domain;
|
||||
|
||||
domains.get(domain, function (error, result) {
|
||||
if (error && error.reason !== DomainsError.NOT_FOUND) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
|
||||
debug(`provision: Setting up Cloudron with domain ${domain} and zone ${zoneName} using admin fqdn ${adminFqdn}`);
|
||||
|
||||
if (result) return callback(new SetupError(SetupError.BAD_STATE, 'Domain already exists'));
|
||||
let data = {
|
||||
zoneName: zoneName,
|
||||
provider: dnsConfig.provider,
|
||||
config: dnsConfig.config,
|
||||
fallbackCertificate: dnsConfig.fallbackCertificate || null,
|
||||
tlsConfig: dnsConfig.tlsConfig || { provider: 'letsencrypt-prod' }
|
||||
};
|
||||
|
||||
let data = {
|
||||
zoneName: zoneName,
|
||||
provider: dnsConfig.provider,
|
||||
config: dnsConfig.config,
|
||||
fallbackCertificate: dnsConfig.fallbackCertificate || null,
|
||||
tlsConfig: dnsConfig.tlsConfig || { provider: 'letsencrypt-prod' }
|
||||
};
|
||||
domains.add(domain, data, auditSource, function (error) {
|
||||
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
|
||||
if (error && error.reason === DomainsError.ALREADY_EXISTS) return callback(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
|
||||
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
|
||||
|
||||
async.series([
|
||||
domains.add.bind(null, domain, data, auditSource),
|
||||
mail.addDomain.bind(null, domain)
|
||||
], function (error) {
|
||||
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
|
||||
if (error && error.reason === DomainsError.ALREADY_EXISTS) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
|
||||
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
|
||||
|
||||
config.setAdminDomain(domain); // set fqdn only after dns config is valid, otherwise cannot re-setup if we failed
|
||||
config.setAdminFqdn(adminFqdn);
|
||||
config.setAdminLocation('my');
|
||||
|
||||
eventlog.add(eventlog.ACTION_PROVISION, auditSource, { });
|
||||
|
||||
clients.addDefaultClients(config.adminOrigin(), callback);
|
||||
|
||||
async.series([
|
||||
autoprovision.bind(null, autoconf),
|
||||
cloudron.configureWebadmin
|
||||
], NOOP_CALLBACK);
|
||||
async.series([
|
||||
mail.addDomain.bind(null, domain),
|
||||
cloudron.setDashboardDomain.bind(null, domain), // triggers task to setup my. dns/cert/reverseproxy
|
||||
autoprovision.bind(null, autoconf),
|
||||
eventlog.add.bind(null, eventlog.ACTION_PROVISION, auditSource, { })
|
||||
], callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -190,12 +198,12 @@ function activate(username, password, email, displayName, ip, auditSource, callb
|
||||
setTimeZone(ip, function () { }); // TODO: get this from user. note that timezone is detected based on the browser location and not the cloudron region
|
||||
|
||||
users.createOwner(username, password, email, displayName, auditSource, function (error, userObject) {
|
||||
if (error && error.reason === UsersError.ALREADY_EXISTS) return callback(new SetupError(SetupError.ALREADY_PROVISIONED));
|
||||
if (error && error.reason === UsersError.BAD_FIELD) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
|
||||
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
|
||||
if (error && error.reason === UsersError.ALREADY_EXISTS) return callback(new ProvisionError(ProvisionError.ALREADY_PROVISIONED, 'Already activated'));
|
||||
if (error && error.reason === UsersError.BAD_FIELD) return callback(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
|
||||
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
|
||||
|
||||
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
|
||||
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
|
||||
|
||||
eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { });
|
||||
|
||||
@@ -218,21 +226,21 @@ function restore(backupConfig, backupId, version, autoconf, auditSource, callbac
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!semver.valid(version)) return callback(new SetupError(SetupError.BAD_STATE, 'version is not a valid semver'));
|
||||
if (semver.major(config.version()) !== semver.major(version) || semver.minor(config.version()) !== semver.minor(version)) return callback(new SetupError(SetupError.BAD_STATE, `Run cloudron-setup with --version ${version} to restore from this backup`));
|
||||
if (!semver.valid(version)) return callback(new ProvisionError(ProvisionError.BAD_STATE, 'version is not a valid semver'));
|
||||
if (semver.major(config.version()) !== semver.major(version) || semver.minor(config.version()) !== semver.minor(version)) return callback(new ProvisionError(ProvisionError.BAD_STATE, `Run cloudron-setup with --version ${version} to restore from this backup`));
|
||||
|
||||
let webadminStatus = cloudron.getWebadminStatus();
|
||||
|
||||
if (webadminStatus.configuring || webadminStatus.restore.active) return callback(new SetupError(SetupError.BAD_STATE, 'Already restoring or configuring'));
|
||||
if (webadminStatus.configuring || webadminStatus.restore.active) return callback(new ProvisionError(ProvisionError.BAD_STATE, 'Already restoring or configuring'));
|
||||
|
||||
users.isActivated(function (error, activated) {
|
||||
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
|
||||
if (activated) return callback(new SetupError(SetupError.ALREADY_PROVISIONED, 'Already activated'));
|
||||
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
|
||||
if (activated) return callback(new ProvisionError(ProvisionError.ALREADY_PROVISIONED, 'Already activated'));
|
||||
|
||||
backups.testConfig(backupConfig, function (error) {
|
||||
if (error && error.reason === BackupsError.BAD_FIELD) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
|
||||
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new SetupError(SetupError.EXTERNAL_ERROR, error.message));
|
||||
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
|
||||
if (error && error.reason === BackupsError.BAD_FIELD) return callback(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
|
||||
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new ProvisionError(ProvisionError.EXTERNAL_ERROR, error.message));
|
||||
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
|
||||
|
||||
debug(`restore: restoring from ${backupId} from provider ${backupConfig.provider} with format ${backupConfig.format}`);
|
||||
|
||||
@@ -242,14 +250,14 @@ function restore(backupConfig, backupId, version, autoconf, auditSource, callbac
|
||||
callback(null); // do no block
|
||||
|
||||
async.series([
|
||||
backups.restore.bind(null, backupConfig, backupId),
|
||||
backups.restore.bind(null, backupConfig, backupId, (progress) => debug(`restore: ${progress}`)),
|
||||
eventlog.add.bind(null, eventlog.ACTION_RESTORE, auditSource, { backupId }),
|
||||
autoprovision.bind(null, autoconf),
|
||||
// currently, our suggested restore flow is after a dnsSetup. The dnSetup creates DKIM keys and updates the DNS
|
||||
// for this reason, we have to re-setup DNS after a restore so it has DKIm from the backup
|
||||
// Once we have a 100% IP based restore, we can skip this
|
||||
mail.setDnsRecords.bind(null, config.adminDomain()),
|
||||
shell.sudo.bind(null, 'restart', [ RESTART_CMD ])
|
||||
shell.sudo.bind(null, 'restart', [ RESTART_CMD ], {})
|
||||
], function (error) {
|
||||
debug('restore:', error);
|
||||
if (error) webadminStatus.restore.error = error.message;
|
||||
@@ -114,6 +114,8 @@ function isExpiringSync(certFilePath, hours) {
|
||||
|
||||
var result = safe.child_process.spawnSync('/usr/bin/openssl', [ 'x509', '-checkend', String(60 * 60 * hours), '-in', certFilePath ]);
|
||||
|
||||
if (!result) return 3; // some error
|
||||
|
||||
debug('isExpiringSync: %s %s %s', certFilePath, result.stdout.toString('utf8').trim(), result.status);
|
||||
|
||||
return result.status === 1; // 1 - expired 0 - not expired
|
||||
@@ -130,19 +132,21 @@ function providerMatchesSync(domainObject, certFilePath, apiOptions) {
|
||||
if (apiOptions.fallback) return certFilePath.includes('.host.cert');
|
||||
|
||||
const subjectAndIssuer = safe.child_process.execSync(`/usr/bin/openssl x509 -noout -subject -issuer -in "${certFilePath}"`, { encoding: 'utf8' });
|
||||
if (!subjectAndIssuer) return false; // something bad happenned
|
||||
|
||||
const subject = subjectAndIssuer.match(/^subject=(.*)$/m)[1];
|
||||
const domain = subject.substr(subject.indexOf('=') + 1).trim(); // subject can be /CN=, CN=, CN = and other forms
|
||||
const issuer = subjectAndIssuer.match(/^issuer=(.*)$/m)[1];
|
||||
const isWildcardCert = subject.includes('*');
|
||||
const isWildcardCert = domain.includes('*');
|
||||
const isLetsEncryptProd = issuer.includes('Let\'s Encrypt Authority');
|
||||
|
||||
const issuerMismatch = (apiOptions.prod && !isLetsEncryptProd) || (!apiOptions.prod && isLetsEncryptProd);
|
||||
// bare domain is not part of wildcard SAN
|
||||
const wildcardMismatch = (subject !== domainObject.domain) && (apiOptions.wildcard && !isWildcardCert) || (!apiOptions.wildcard && isWildcardCert);
|
||||
const wildcardMismatch = (domain !== domainObject.domain) && (apiOptions.wildcard && !isWildcardCert) || (!apiOptions.wildcard && isWildcardCert);
|
||||
|
||||
const mismatch = issuerMismatch || wildcardMismatch;
|
||||
|
||||
debug(`providerMatchesSync: ${certFilePath} subject=${subject} issuer=${issuer} wildcard=${isWildcardCert}/${apiOptions.wildcard} prod=${isLetsEncryptProd}/${apiOptions.prod} match=${!mismatch}`);
|
||||
debug(`providerMatchesSync: ${certFilePath} subject=${subject} domain=${domain} issuer=${issuer} wildcard=${isWildcardCert}/${apiOptions.wildcard} prod=${isLetsEncryptProd}/${apiOptions.prod} match=${!mismatch}`);
|
||||
|
||||
return !mismatch;
|
||||
}
|
||||
@@ -164,13 +168,17 @@ function validateCertificate(location, domainObject, certificate) {
|
||||
const fqdn = domains.fqdn(location, domainObject);
|
||||
|
||||
var result = safe.child_process.execSync(`openssl x509 -noout -checkhost "${fqdn}"`, { encoding: 'utf8', input: cert });
|
||||
if (!result) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Unable to get certificate subject.');
|
||||
if (result === null) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Unable to get certificate subject:' + safe.error.message);
|
||||
|
||||
if (result.indexOf('does match certificate') === -1) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, `Certificate is not valid for this domain. Expecting ${fqdn}`);
|
||||
|
||||
// http://httpd.apache.org/docs/2.0/ssl/ssl_faq.html#verify
|
||||
var certModulus = safe.child_process.execSync('openssl x509 -noout -modulus', { encoding: 'utf8', input: cert });
|
||||
if (certModulus === null) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, `Unable to get cert modulus: ${safe.error.message}`);
|
||||
|
||||
var keyModulus = safe.child_process.execSync('openssl rsa -noout -modulus', { encoding: 'utf8', input: key });
|
||||
if (keyModulus === null) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, `Unable to get key modulus: ${safe.error.message}`);
|
||||
|
||||
if (certModulus !== keyModulus) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Key does not match the certificate.');
|
||||
|
||||
// check expiration
|
||||
@@ -183,7 +191,7 @@ function validateCertificate(location, domainObject, certificate) {
|
||||
function reload(callback) {
|
||||
if (process.env.BOX_ENV === 'test') return callback();
|
||||
|
||||
shell.sudo('reload', [ RELOAD_NGINX_CMD ], callback);
|
||||
shell.sudo('reload', [ RELOAD_NGINX_CMD ], {}, callback);
|
||||
}
|
||||
|
||||
function generateFallbackCertificateSync(domainObject) {
|
||||
@@ -234,12 +242,14 @@ function setFallbackCertificate(domain, fallback, callback) {
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.key`), fallback.key)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
|
||||
}
|
||||
|
||||
platform.handleCertChanged('*.' + domain);
|
||||
|
||||
reload(function (error) {
|
||||
platform.handleCertChanged('*.' + domain, function (error) {
|
||||
if (error) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null);
|
||||
reload(function (error) {
|
||||
if (error) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -497,9 +507,10 @@ function unconfigureApp(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function renewCerts(options, auditSource, callback) {
|
||||
function renewCerts(options, auditSource, progressCallback, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
apps.getAll(function (error, allApps) {
|
||||
@@ -522,7 +533,11 @@ function renewCerts(options, auditSource, callback) {
|
||||
|
||||
if (options.domain) appDomains = appDomains.filter(function (appDomain) { return appDomain.domain === options.domain; });
|
||||
|
||||
let progress = 1;
|
||||
async.eachSeries(appDomains, function (appDomain, iteratorCallback) {
|
||||
progressCallback({ percent: progress, message: `Renewing certs of ${appDomain.fqdn}` });
|
||||
progress += Math.round(100/appDomains.length);
|
||||
|
||||
ensureCertificate(appDomain.fqdn, appDomain.domain, auditSource, function (error, bundle) {
|
||||
if (error) return iteratorCallback(error); // this can happen if cloudron is not setup yet
|
||||
|
||||
@@ -537,17 +552,15 @@ function renewCerts(options, auditSource, callback) {
|
||||
if (appDomain.type === 'webadmin') configureFunc = writeAdminConfig.bind(null, bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn());
|
||||
else if (appDomain.type === 'main') configureFunc = writeAppConfig.bind(null, appDomain.app, bundle);
|
||||
else if (appDomain.type === 'alternate') configureFunc = writeAppRedirectConfig.bind(null, appDomain.app, appDomain.fqdn, bundle);
|
||||
else return callback(new Error(`Unknown domain type for ${appDomain.fqdn}. This should never happen`));
|
||||
else return iteratorCallback(new Error(`Unknown domain type for ${appDomain.fqdn}. This should never happen`));
|
||||
|
||||
configureFunc(function (ignoredError) {
|
||||
if (ignoredError) debug('renewAll: error reconfiguring app', ignoredError);
|
||||
|
||||
platform.handleCertChanged(appDomain.fqdn);
|
||||
|
||||
iteratorCallback(); // move to next domain
|
||||
platform.handleCertChanged(appDomain.fqdn, iteratorCallback);
|
||||
});
|
||||
});
|
||||
});
|
||||
}, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -578,8 +591,10 @@ function configureDefaultServer(callback) {
|
||||
debug('configureDefaultServer: create new cert');
|
||||
|
||||
var cn = 'cloudron-' + (new Date()).toISOString(); // randomize date a bit to keep firefox happy
|
||||
var certCommand = util.format('openssl req -x509 -newkey rsa:2048 -keyout %s -out %s -days 3650 -subj /CN=%s -nodes', keyFilePath, certFilePath, cn);
|
||||
safe.child_process.execSync(certCommand);
|
||||
if (!safe.child_process.execSync(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=${cn} -nodes`)) {
|
||||
debug(`configureDefaultServer: could not generate certificate: ${safe.error.message}`);
|
||||
return callback(safe.error);
|
||||
}
|
||||
}
|
||||
|
||||
writeAdminConfig({ certFilePath, keyFilePath }, constants.NGINX_DEFAULT_CONFIG_FILE_NAME, '', function (error) {
|
||||
|
||||
@@ -134,6 +134,7 @@ function installApp(req, res, next) {
|
||||
|
||||
if ('sso' in data && typeof data.sso !== 'boolean') return next(new HttpError(400, 'sso must be a boolean'));
|
||||
if ('enableBackup' in data && typeof data.enableBackup !== 'boolean') return next(new HttpError(400, 'enableBackup must be a boolean'));
|
||||
if ('enableAutomaticUpdate' in data && typeof data.enableAutomaticUpdate !== 'boolean') return next(new HttpError(400, 'enableAutomaticUpdate must be a boolean'));
|
||||
|
||||
if (('debugMode' in data) && typeof data.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object'));
|
||||
|
||||
@@ -174,6 +175,10 @@ function configureApp(req, res, next) {
|
||||
|
||||
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'));
|
||||
// domain, location must both be provided since they are unique together
|
||||
if ('location' in data && !('domain' in data)) return next(new HttpError(400, 'domain must be provided'));
|
||||
if (!('location' in data) && 'domain' in data) return next(new HttpError(400, 'location must be provided'));
|
||||
|
||||
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'));
|
||||
|
||||
@@ -187,6 +192,7 @@ function configureApp(req, res, next) {
|
||||
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
|
||||
|
||||
if ('enableBackup' in data && typeof data.enableBackup !== 'boolean') return next(new HttpError(400, 'enableBackup must be a boolean'));
|
||||
if ('enableAutomaticUpdate' in data && typeof data.enableAutomaticUpdate !== 'boolean') return next(new HttpError(400, 'enableAutomaticUpdate must be a boolean'));
|
||||
|
||||
if (('debugMode' in data) && typeof data.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object'));
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
get: get,
|
||||
create: create
|
||||
list: list,
|
||||
startBackup: startBackup
|
||||
};
|
||||
|
||||
var backupdb = require('../backupdb.js'),
|
||||
@@ -16,7 +16,7 @@ function auditSource(req) {
|
||||
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
|
||||
}
|
||||
|
||||
function get(req, res, next) {
|
||||
function list(req, res, next) {
|
||||
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
|
||||
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
|
||||
|
||||
@@ -31,13 +31,11 @@ function get(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function create(req, res, next) {
|
||||
// note that cloudron.backup only waits for backup initiation and not for backup to complete
|
||||
// backup progress can be checked up ny polling the progress api call
|
||||
backups.backup(auditSource(req), function (error) {
|
||||
function startBackup(req, res, next) {
|
||||
backups.startBackupTask(auditSource(req), function (error, taskId) {
|
||||
if (error && error.reason === BackupsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
next(new HttpSuccess(202, { taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
getConfig: getConfig,
|
||||
changePlan: changePlan
|
||||
};
|
||||
|
||||
var caas = require('../caas.js'),
|
||||
CaasError = require('../caas.js').CaasError,
|
||||
config = require('../config.js'),
|
||||
debug = require('debug')('box:routes/cloudron'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
_ = require('underscore');
|
||||
|
||||
function getConfig(req, res, next) {
|
||||
if (config.provider() !== 'caas') return next(new HttpError(422, 'Cannot use this API with this provider'));
|
||||
|
||||
caas.getBoxAndUserDetails(function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
// the result is { box: { region, size, plan }, user: { billing, currency } }
|
||||
next(new HttpSuccess(200, {
|
||||
region: result.box.region,
|
||||
size: result.box.size,
|
||||
billing: !!result.user.billing,
|
||||
plan: result.box.plan,
|
||||
currency: result.user.currency
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
function changePlan(req, res, next) {
|
||||
if (config.provider() !== 'caas') return next(new HttpError(422, 'Cannot use this API with this provider'));
|
||||
|
||||
if ('size' in req.body && typeof req.body.size !== 'string') return next(new HttpError(400, 'size must be string'));
|
||||
if ('region' in req.body && typeof req.body.region !== 'string') return next(new HttpError(400, 'region must be string'));
|
||||
|
||||
if ('domain' in req.body) {
|
||||
if (typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be string'));
|
||||
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider must be string'));
|
||||
}
|
||||
|
||||
if ('zoneName' in req.body && typeof req.body.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be string'));
|
||||
|
||||
debug('Migration requested domain:%s size:%s region:%s', req.body.domain, req.body.size, req.body.region);
|
||||
|
||||
var options = _.pick(req.body, 'domain', 'size', 'region');
|
||||
if (Object.keys(options).length === 0) return next(new HttpError(400, 'no migrate option provided'));
|
||||
|
||||
if (options.domain) options.domain = options.domain.toLowerCase();
|
||||
|
||||
caas.changePlan(req.body, function (error) { // pass req.body because 'domain' can have arbitrary options
|
||||
if (error && error.reason === CaasError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === CaasError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
exports = module.exports = {
|
||||
reboot: reboot,
|
||||
getProgress: getProgress,
|
||||
isRebootRequired: isRebootRequired,
|
||||
getConfig: getConfig,
|
||||
getDisks: getDisks,
|
||||
getUpdateInfo: getUpdateInfo,
|
||||
@@ -12,6 +12,8 @@ exports = module.exports = {
|
||||
getLogs: getLogs,
|
||||
getLogStream: getLogStream,
|
||||
getStatus: getStatus,
|
||||
setDashboardDomain: setDashboardDomain,
|
||||
renewCerts: renewCerts
|
||||
};
|
||||
|
||||
var appstore = require('../appstore.js'),
|
||||
@@ -22,7 +24,6 @@ var appstore = require('../appstore.js'),
|
||||
CloudronError = cloudron.CloudronError,
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
progress = require('../progress.js'),
|
||||
updater = require('../updater.js'),
|
||||
updateChecker = require('../updatechecker.js'),
|
||||
UpdaterError = require('../updater.js').UpdaterError,
|
||||
@@ -33,15 +34,19 @@ function auditSource(req) {
|
||||
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
|
||||
}
|
||||
|
||||
function getProgress(req, res, next) {
|
||||
return next(new HttpSuccess(200, progress.getAll()));
|
||||
}
|
||||
|
||||
function reboot(req, res, next) {
|
||||
// Finish the request, to let the appstore know we triggered the restore it
|
||||
// Finish the request, to let the appstore know we triggered the reboot
|
||||
next(new HttpSuccess(202, {}));
|
||||
|
||||
cloudron.reboot(function () { });
|
||||
cloudron.reboot(function () {});
|
||||
}
|
||||
|
||||
function isRebootRequired(req, res, next) {
|
||||
cloudron.isRebootRequired(function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, { rebootRequired: result }));
|
||||
});
|
||||
}
|
||||
|
||||
function getConfig(req, res, next) {
|
||||
@@ -61,13 +66,12 @@ function getDisks(req, res, next) {
|
||||
|
||||
function update(req, res, next) {
|
||||
// this only initiates the update, progress can be checked via the progress route
|
||||
updater.updateToLatest(auditSource(req), function (error) {
|
||||
updater.updateToLatest(auditSource(req), function (error, taskId) {
|
||||
if (error && error.reason === UpdaterError.ALREADY_UPTODATE) return next(new HttpError(422, error.message));
|
||||
if (error && error.reason === UpdaterError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === UpdaterError.SELF_UPGRADE_NOT_SUPPORTED) return next(new HttpError(412, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
next(new HttpSuccess(202, { taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -171,6 +175,17 @@ function getLogStream(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function setDashboardDomain(req, res, next) {
|
||||
if (!req.body.domain || typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
|
||||
|
||||
cloudron.setDashboardDomain(req.body.domain, function (error) {
|
||||
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(404, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(204, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function getStatus(req, res, next) {
|
||||
cloudron.getStatus(function (error, status) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
@@ -178,3 +193,12 @@ function getStatus(req, res, next) {
|
||||
next(new HttpSuccess(200, status));
|
||||
});
|
||||
}
|
||||
|
||||
function renewCerts(req, res, next) {
|
||||
cloudron.renewCerts({ domain: req.body.domain || null }, auditSource(req), function (error, taskId) {
|
||||
if (error && error.reason === CloudronError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ exports = module.exports = {
|
||||
update: update,
|
||||
del: del,
|
||||
|
||||
renewCerts: renewCerts,
|
||||
|
||||
verifyDomainLock: verifyDomainLock
|
||||
};
|
||||
|
||||
@@ -154,16 +152,3 @@ function del(req, res, next) {
|
||||
next(new HttpSuccess(204));
|
||||
});
|
||||
}
|
||||
|
||||
function renewCerts(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.domain, 'string');
|
||||
|
||||
domains.renewCerts(req.params.domain, auditSource(req), function (error) {
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ exports = module.exports = {
|
||||
var middleware = require('../middleware/index.js'),
|
||||
url = require('url');
|
||||
|
||||
var graphiteProxy = middleware.proxy(url.parse('http://127.0.0.1:8000'));
|
||||
var graphiteProxy = middleware.proxy(url.parse('http://127.0.0.1:8417'));
|
||||
|
||||
function getGraphs(req, res, next) {
|
||||
var parsedUrl = url.parse(req.url, true /* parseQueryString */);
|
||||
|
||||
@@ -4,7 +4,6 @@ exports = module.exports = {
|
||||
accesscontrol: require('./accesscontrol.js'),
|
||||
apps: require('./apps.js'),
|
||||
backups: require('./backups.js'),
|
||||
caas: require('./caas.js'),
|
||||
clients: require('./clients.js'),
|
||||
cloudron: require('./cloudron.js'),
|
||||
developer: require('./developer.js'),
|
||||
@@ -15,9 +14,11 @@ exports = module.exports = {
|
||||
oauth2: require('./oauth2.js'),
|
||||
mail: require('./mail.js'),
|
||||
profile: require('./profile.js'),
|
||||
setup: require('./setup.js'),
|
||||
sysadmin: require('./sysadmin.js'),
|
||||
provision: require('./provision.js'),
|
||||
services: require('./services.js'),
|
||||
settings: require('./settings.js'),
|
||||
sysadmin: require('./sysadmin.js'),
|
||||
ssh: require('./ssh.js'),
|
||||
tasks: require('./tasks.js'),
|
||||
users: require('./users.js')
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ exports = module.exports = {
|
||||
|
||||
sendTestMail: sendTestMail,
|
||||
|
||||
getMailboxes: getMailboxes,
|
||||
listMailboxes: listMailboxes,
|
||||
getMailbox: getMailbox,
|
||||
addMailbox: addMailbox,
|
||||
updateMailbox: updateMailbox,
|
||||
@@ -214,10 +214,10 @@ function sendTestMail(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function getMailboxes(req, res, next) {
|
||||
function listMailboxes(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.domain, 'string');
|
||||
|
||||
mail.getMailboxes(req.params.domain, function (error, result) {
|
||||
mail.listMailboxes(req.params.domain, function (error, result) {
|
||||
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
@@ -259,7 +259,7 @@ function updateMailbox(req, res, next) {
|
||||
|
||||
if (typeof req.body.userId !== 'string') return next(new HttpError(400, 'userId must be a string'));
|
||||
|
||||
mail.updateMailbox(req.params.name, req.params.domain, req.body.userId, function (error) {
|
||||
mail.updateMailboxOwner(req.params.name, req.params.domain, req.body.userId, function (error) {
|
||||
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
exports = module.exports = {
|
||||
providerTokenAuth: providerTokenAuth,
|
||||
setupTokenAuth: setupTokenAuth,
|
||||
provision: provision,
|
||||
setup: setup,
|
||||
activate: activate,
|
||||
restore: restore
|
||||
};
|
||||
@@ -15,8 +15,8 @@ var assert = require('assert'),
|
||||
debug = require('debug')('box:routes/setup'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
setup = require('../setup.js'),
|
||||
SetupError = require('../setup.js').SetupError,
|
||||
provision = require('../provision.js'),
|
||||
ProvisionError = require('../provision.js').ProvisionError,
|
||||
superagent = require('superagent');
|
||||
|
||||
function auditSource(req) {
|
||||
@@ -61,7 +61,7 @@ function setupTokenAuth(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function provision(req, res, next) {
|
||||
function setup(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (!req.body.dnsConfig || typeof req.body.dnsConfig !== 'object') return next(new HttpError(400, 'dnsConfig is required'));
|
||||
@@ -83,10 +83,10 @@ function provision(req, res, next) {
|
||||
// it can take sometime to setup DNS, register cloudron
|
||||
req.clearTimeout();
|
||||
|
||||
setup.provision(dnsConfig, req.body.autoconf || {}, auditSource(req), function (error) {
|
||||
if (error && error.reason === SetupError.ALREADY_SETUP) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === SetupError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === SetupError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
provision.setup(dnsConfig, req.body.autoconf || {}, auditSource(req), function (error) {
|
||||
if (error && error.reason === ProvisionError.ALREADY_SETUP) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === ProvisionError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === ProvisionError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200));
|
||||
@@ -109,9 +109,9 @@ function activate(req, res, next) {
|
||||
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
||||
debug('activate: username:%s ip:%s', username, ip);
|
||||
|
||||
setup.activate(username, password, email, displayName, ip, auditSource(req), function (error, info) {
|
||||
if (error && error.reason === SetupError.ALREADY_PROVISIONED) return next(new HttpError(409, 'Already setup'));
|
||||
if (error && error.reason === SetupError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
provision.activate(username, password, email, displayName, ip, auditSource(req), function (error, info) {
|
||||
if (error && error.reason === ProvisionError.ALREADY_PROVISIONED) return next(new HttpError(409, 'Already setup'));
|
||||
if (error && error.reason === ProvisionError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
// only in caas case do we have to notify the api server about activation
|
||||
@@ -146,11 +146,11 @@ function restore(req, res, next) {
|
||||
// TODO: validate subfields of these objects
|
||||
if (req.body.autoconf && typeof req.body.autoconf !== 'object') return next(new HttpError(400, 'autoconf must be an object'));
|
||||
|
||||
setup.restore(backupConfig, req.body.backupId, req.body.version, req.body.autoconf || {}, auditSource(req), function (error) {
|
||||
if (error && error.reason === SetupError.ALREADY_SETUP) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === SetupError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === SetupError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === SetupError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
|
||||
provision.restore(backupConfig, req.body.backupId, req.body.version, req.body.autoconf || {}, auditSource(req), function (error) {
|
||||
if (error && error.reason === ProvisionError.ALREADY_SETUP) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === ProvisionError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === ProvisionError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === ProvisionError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200));
|
||||
134
src/routes/services.js
Normal file
134
src/routes/services.js
Normal file
@@ -0,0 +1,134 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
getAll: getAll,
|
||||
get: get,
|
||||
configure: configure,
|
||||
getLogs: getLogs,
|
||||
getLogStream: getLogStream,
|
||||
restart: restart
|
||||
};
|
||||
|
||||
var addons = require('../addons.js'),
|
||||
AddonsError = addons.AddonsError,
|
||||
assert = require('assert'),
|
||||
debug = require('debug')('box:routes/addons'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess;
|
||||
|
||||
function getAll(req, res, next) {
|
||||
addons.getServices(function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, { services: result }));
|
||||
});
|
||||
}
|
||||
|
||||
function get(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.service, 'string');
|
||||
|
||||
addons.getService(req.params.service, function (error, result) {
|
||||
if (error && error.reason === AddonsError.NOT_FOUND) return next(new HttpError(404, 'No such service'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, { service: result }));
|
||||
});
|
||||
}
|
||||
|
||||
function configure(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.service, 'string');
|
||||
|
||||
if (typeof req.body.memory !== 'number') return next(new HttpError(400, 'memory must be a number'));
|
||||
|
||||
const data = {
|
||||
memory: req.body.memory,
|
||||
memorySwap: req.body.memory * 2
|
||||
};
|
||||
|
||||
addons.configureService(req.params.service, data, function (error) {
|
||||
if (error && error.reason === AddonsError.NOT_FOUND) return next(new HttpError(404, 'No such service'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function getLogs(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.service, 'string');
|
||||
|
||||
var lines = req.query.lines ? parseInt(req.query.lines, 10) : 100;
|
||||
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number'));
|
||||
|
||||
debug(`Getting logs of service ${req.params.service}`);
|
||||
|
||||
var options = {
|
||||
lines: lines,
|
||||
follow: false,
|
||||
format: req.query.format
|
||||
};
|
||||
|
||||
addons.getServiceLogs(req.params.service, options, function (error, logStream) {
|
||||
if (error && error.reason === AddonsError.NOT_FOUND) return next(new HttpError(404, 'No such service'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/x-logs',
|
||||
'Content-Disposition': 'attachment; filename="log.txt"',
|
||||
'Cache-Control': 'no-cache',
|
||||
'X-Accel-Buffering': 'no' // disable nginx buffering
|
||||
});
|
||||
logStream.pipe(res);
|
||||
});
|
||||
}
|
||||
|
||||
// this route is for streaming logs
|
||||
function getLogStream(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.service, 'string');
|
||||
|
||||
debug(`Getting logstream of service ${req.params.service}`);
|
||||
|
||||
var lines = req.query.lines ? parseInt(req.query.lines, 10) : -10; // we ignore last-event-id
|
||||
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
|
||||
|
||||
function sse(id, data) { return 'id: ' + id + '\ndata: ' + data + '\n\n'; }
|
||||
|
||||
if (req.headers.accept !== 'text/event-stream') return next(new HttpError(400, 'This API call requires EventStream'));
|
||||
|
||||
var options = {
|
||||
lines: lines,
|
||||
follow: true
|
||||
};
|
||||
|
||||
addons.getServiceLogs(req.params.service, options, function (error, logStream) {
|
||||
if (error && error.reason === AddonsError.NOT_FOUND) return next(new HttpError(404, 'No such service'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
'X-Accel-Buffering': 'no', // disable nginx buffering
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
});
|
||||
res.write('retry: 3000\n');
|
||||
res.on('close', logStream.close);
|
||||
logStream.on('data', function (data) {
|
||||
var obj = JSON.parse(data);
|
||||
res.write(sse(obj.monotonicTimestamp, JSON.stringify(obj))); // send timestamp as id
|
||||
});
|
||||
logStream.on('end', res.end.bind(res));
|
||||
logStream.on('error', res.end.bind(res, null));
|
||||
});
|
||||
}
|
||||
|
||||
function restart(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.service, 'string');
|
||||
|
||||
debug(`Restarting service ${req.params.service}`);
|
||||
|
||||
addons.restartService(req.params.service, function (error) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
@@ -26,11 +26,11 @@ function backup(req, res, next) {
|
||||
// note that cloudron.backup only waits for backup initiation and not for backup to complete
|
||||
// backup progress can be checked up ny polling the progress api call
|
||||
var auditSource = { userId: null, username: 'sysadmin' };
|
||||
backups.backup(auditSource, function (error) {
|
||||
backups.startBackupTask(auditSource, function (error, taskId) {
|
||||
if (error && error.reason === BackupsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
next(new HttpSuccess(202, { taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -39,13 +39,12 @@ function update(req, res, next) {
|
||||
|
||||
// this only initiates the update, progress can be checked via the progress route
|
||||
var auditSource = { userId: null, username: 'sysadmin' };
|
||||
updater.updateToLatest(auditSource, function (error) {
|
||||
updater.updateToLatest(auditSource, function (error, taskId) {
|
||||
if (error && error.reason === UpdaterError.ALREADY_UPTODATE) return next(new HttpError(422, error.message));
|
||||
if (error && error.reason === UpdaterError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === UpdaterError.SELF_UPGRADE_NOT_SUPPORTED) return next(new HttpError(412, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
next(new HttpSuccess(202, { taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
121
src/routes/tasks.js
Normal file
121
src/routes/tasks.js
Normal file
@@ -0,0 +1,121 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
get: get,
|
||||
stopTask: stopTask,
|
||||
list: list,
|
||||
|
||||
getLogs: getLogs,
|
||||
getLogStream: getLogStream
|
||||
};
|
||||
|
||||
let assert = require('assert'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
TaskError = require('../tasks.js').TaskError,
|
||||
tasks = require('../tasks.js');
|
||||
|
||||
function stopTask(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.taskId, 'string');
|
||||
|
||||
tasks.stopTask(req.params.taskId, function (error) {
|
||||
if (error && error.reason === TaskError.NOT_FOUND) return next(new HttpError(404, 'No such task'));
|
||||
if (error && error.reason === TaskError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(204, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function get(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.taskId, 'string');
|
||||
|
||||
tasks.get(req.params.taskId, function (error, task) {
|
||||
if (error && error.reason === TaskError.NOT_FOUND) return next(new HttpError(404, 'No such task'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, tasks.removePrivateFields(task)));
|
||||
});
|
||||
}
|
||||
|
||||
function list(req, res, next) {
|
||||
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
|
||||
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
|
||||
|
||||
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
|
||||
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
|
||||
|
||||
if (req.query.type && typeof req.query.type !== 'string') return next(new HttpError(400, 'type must be a string'));
|
||||
|
||||
tasks.listByTypePaged(req.query.type || null, page, perPage, function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
result = result.map(tasks.removePrivateFields);
|
||||
|
||||
next(new HttpSuccess(200, { tasks: result }));
|
||||
});
|
||||
}
|
||||
|
||||
function getLogs(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.taskId, 'string');
|
||||
|
||||
var lines = req.query.lines ? parseInt(req.query.lines, 10) : 100;
|
||||
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number'));
|
||||
|
||||
var options = {
|
||||
lines: lines,
|
||||
follow: false,
|
||||
format: req.query.format
|
||||
};
|
||||
|
||||
tasks.getLogs(req.params.taskId, options, function (error, logStream) {
|
||||
if (error && error.reason === TaskError.NOT_FOUND) return next(new HttpError(404, 'No such task'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/x-logs',
|
||||
'Content-Disposition': 'attachment; filename="log.txt"',
|
||||
'Cache-Control': 'no-cache',
|
||||
'X-Accel-Buffering': 'no' // disable nginx buffering
|
||||
});
|
||||
logStream.pipe(res);
|
||||
});
|
||||
}
|
||||
|
||||
// this route is for streaming logs
|
||||
function getLogStream(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.taskId, 'string');
|
||||
|
||||
var lines = req.query.lines ? parseInt(req.query.lines, 10) : -10; // we ignore last-event-id
|
||||
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
|
||||
|
||||
function sse(id, data) { return 'id: ' + id + '\ndata: ' + data + '\n\n'; }
|
||||
|
||||
if (req.headers.accept !== 'text/event-stream') return next(new HttpError(400, 'This API call requires EventStream'));
|
||||
|
||||
var options = {
|
||||
lines: lines,
|
||||
follow: true
|
||||
};
|
||||
|
||||
tasks.getLogs(req.params.taskId, options, function (error, logStream) {
|
||||
if (error && error.reason === TaskError.NOT_FOUND) return next(new HttpError(404, 'No such task'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
'X-Accel-Buffering': 'no', // disable nginx buffering
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
});
|
||||
res.write('retry: 3000\n');
|
||||
res.on('close', logStream.close);
|
||||
logStream.on('data', function (data) {
|
||||
var obj = JSON.parse(data);
|
||||
res.write(sse(obj.monotonicTimestamp, JSON.stringify(obj))); // send timestamp as id
|
||||
});
|
||||
logStream.on('end', res.end.bind(res));
|
||||
logStream.on('error', res.end.bind(res, null));
|
||||
});
|
||||
}
|
||||
@@ -264,8 +264,6 @@ function stopBox(done) {
|
||||
}
|
||||
|
||||
describe('App API', function () {
|
||||
this.timeout(100000);
|
||||
|
||||
before(startBox);
|
||||
after(stopBox);
|
||||
|
||||
@@ -618,11 +616,7 @@ describe('App API', function () {
|
||||
});
|
||||
|
||||
describe('App installation', function () {
|
||||
this.timeout(100000);
|
||||
|
||||
var apiHockInstance = hock.createHock({ throwOnUnmatched: false });
|
||||
var apiHockServer;
|
||||
|
||||
var validCert1, validKey1;
|
||||
|
||||
before(function (done) {
|
||||
@@ -820,7 +814,6 @@ describe('App installation', function () {
|
||||
});
|
||||
|
||||
it('installation - app can check addons', function (done) {
|
||||
this.timeout(120000);
|
||||
console.log('This test can take a while as it waits for scheduler addon to tick 3');
|
||||
checkAddons(appEntry, done);
|
||||
});
|
||||
@@ -949,7 +942,6 @@ describe('App installation', function () {
|
||||
});
|
||||
|
||||
it('installation - app can check addons', function (done) {
|
||||
this.timeout(120000);
|
||||
console.log('This test can take a while as it waits for scheduler addon to tick 2');
|
||||
checkAddons(appEntry, done);
|
||||
});
|
||||
@@ -1076,7 +1068,6 @@ describe('App installation', function () {
|
||||
});
|
||||
|
||||
it('installation - app can check addons', function (done) {
|
||||
this.timeout(120000);
|
||||
console.log('This test can take a while as it waits for scheduler addon to tick 4');
|
||||
checkAddons(appEntry, done);
|
||||
});
|
||||
|
||||
@@ -145,29 +145,6 @@ describe('Caas', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('get config', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
it('succeeds (admin)', function (done) {
|
||||
var scope = nock(config.apiServerOrigin())
|
||||
.get('/api/v1/boxes/BOX_ID?token=ACCESS_TOKEN2')
|
||||
.reply(200, { box: { region: 'sfo', size: '1gb' }, user: { }});
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/caas/config')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.size).to.eql('1gb');
|
||||
expect(result.body.region).to.eql('sfo');
|
||||
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Backups API', function () {
|
||||
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/BOX_ID/awscredentials?token=BACKUP_TOKEN')
|
||||
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey' } }, { 'Content-Type': 'application/json' });
|
||||
@@ -191,169 +168,5 @@ describe('Caas', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
xdescribe('migrate', function () {
|
||||
before(function (done) {
|
||||
async.series([
|
||||
setup,
|
||||
|
||||
function (callback) {
|
||||
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/BOX_ID/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/BOX_ID/setup/done?setupToken=somesetuptoken').reply(201, {});
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
], done);
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
locker.unlock(locker._operation); // migrate never unlocks
|
||||
cleanup(done);
|
||||
});
|
||||
|
||||
it('fails without token', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 'small', region: 'sfo'})
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails without password', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 'small', region: 'sfo'})
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds without size', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ region: 'sfo', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(202);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with wrong size type', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 4, region: 'sfo', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds without region', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 'small', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(202);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with wrong region type', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 'small', region: 4, password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails when in wrong state', function (done) {
|
||||
var scope2 = nock(config.apiServerOrigin())
|
||||
.post('/api/v1/boxes/BOX_ID/awscredentials?token=BACKUP_TOKEN')
|
||||
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
|
||||
|
||||
var scope3 = nock(config.apiServerOrigin())
|
||||
.post('/api/v1/boxes/BOX_ID/backupDone?token=APPSTORE_TOKEN', function (body) {
|
||||
return body.boxVersion && body.restoreKey && !body.appId && !body.appVersion && body.appBackupIds.length === 0;
|
||||
})
|
||||
.reply(200, { id: 'someid' });
|
||||
|
||||
var scope1 = nock(config.apiServerOrigin())
|
||||
.post('/api/v1/boxes/BOX_ID/migrate?token=APPSTORE_TOKEN', function (body) {
|
||||
return body.size && body.region && body.restoreKey;
|
||||
}).reply(409, {});
|
||||
|
||||
injectShellMock();
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 'small', region: 'sfo', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(202);
|
||||
|
||||
function checkAppstoreServerCalled() {
|
||||
if (scope1.isDone() && scope2.isDone() && scope3.isDone()) {
|
||||
restoreShellMock();
|
||||
return done();
|
||||
}
|
||||
|
||||
setTimeout(checkAppstoreServerCalled, 100);
|
||||
}
|
||||
|
||||
checkAppstoreServerCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/BOX_ID/migrate?token=APPSTORE_TOKEN', function (body) {
|
||||
return body.size && body.region && body.restoreKey;
|
||||
}).reply(202, {});
|
||||
|
||||
var scope2 = nock(config.apiServerOrigin())
|
||||
.post('/api/v1/boxes/BOX_ID/backupDone?token=APPSTORE_TOKEN', function (body) {
|
||||
return body.boxVersion && body.restoreKey && !body.appId && !body.appVersion && body.appBackupIds.length === 0;
|
||||
})
|
||||
.reply(200, { id: 'someid' });
|
||||
|
||||
var scope3 = nock(config.apiServerOrigin())
|
||||
.post('/api/v1/boxes/BOX_ID/awscredentials?token=BACKUP_TOKEN')
|
||||
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
|
||||
|
||||
injectShellMock();
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 'small', region: 'sfo', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(202);
|
||||
|
||||
function checkAppstoreServerCalled() {
|
||||
if (scope1.isDone() && scope2.isDone() && scope3.isDone()) {
|
||||
restoreShellMock();
|
||||
return done();
|
||||
}
|
||||
|
||||
setTimeout(checkAppstoreServerCalled, 100);
|
||||
}
|
||||
|
||||
checkAppstoreServerCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -483,8 +483,6 @@ describe('Clients', function () {
|
||||
});
|
||||
|
||||
describe('delete tokens by client', function () {
|
||||
this.timeout(5000);
|
||||
|
||||
before(setup2);
|
||||
after(cleanup);
|
||||
|
||||
|
||||
@@ -191,7 +191,6 @@ describe('Cloudron', function () {
|
||||
expect(result.body.apiServerOrigin).to.eql('http://localhost:6060');
|
||||
expect(result.body.webServerOrigin).to.eql(null);
|
||||
expect(result.body.adminFqdn).to.eql(config.adminFqdn());
|
||||
expect(result.body.progress).to.be.an('object');
|
||||
expect(result.body.version).to.eql(config.version());
|
||||
expect(result.body.memory).to.eql(os.totalmem());
|
||||
expect(result.body.cloudronName).to.be.a('string');
|
||||
|
||||
@@ -37,8 +37,6 @@ function cleanup(done) {
|
||||
}
|
||||
|
||||
describe('Developer API', function () {
|
||||
this.timeout(20000);
|
||||
|
||||
describe('login', function () {
|
||||
before(function (done) {
|
||||
async.series([
|
||||
|
||||
@@ -43,8 +43,6 @@ var DOMAIN_1 = {
|
||||
};
|
||||
|
||||
describe('Domains API', function () {
|
||||
this.timeout(10000);
|
||||
|
||||
before(function (done) {
|
||||
config._reset();
|
||||
config.setFqdn(DOMAIN);
|
||||
|
||||
@@ -78,8 +78,6 @@ function cleanup(done) {
|
||||
}
|
||||
|
||||
describe('Eventlog API', function () {
|
||||
this.timeout(10000);
|
||||
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
|
||||
@@ -105,8 +105,6 @@ function cleanup(done) {
|
||||
}
|
||||
|
||||
describe('Mail API', function () {
|
||||
this.timeout(10000);
|
||||
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
@@ -231,8 +229,6 @@ describe('Mail API', function () {
|
||||
var dnsAnswerQueue = [];
|
||||
var dkimDomain, spfDomain, mxDomain, dmarcDomain;
|
||||
|
||||
this.timeout(10000);
|
||||
|
||||
before(function (done) {
|
||||
var dns = require('../../native-dns.js');
|
||||
|
||||
@@ -765,7 +761,6 @@ describe('Mail API', function () {
|
||||
expect(res.body.mailbox).to.be.an('object');
|
||||
expect(res.body.mailbox.name).to.equal(MAILBOX_NAME);
|
||||
expect(res.body.mailbox.ownerId).to.equal(userId);
|
||||
expect(res.body.mailbox.ownerType).to.equal('user');
|
||||
expect(res.body.mailbox.aliasTarget).to.equal(null);
|
||||
expect(res.body.mailbox.domain).to.equal(DOMAIN_0.domain);
|
||||
done();
|
||||
@@ -781,7 +776,6 @@ describe('Mail API', function () {
|
||||
expect(res.body.mailboxes[0]).to.be.an('object');
|
||||
expect(res.body.mailboxes[0].name).to.equal(MAILBOX_NAME);
|
||||
expect(res.body.mailboxes[0].ownerId).to.equal(userId);
|
||||
expect(res.body.mailboxes[0].ownerType).to.equal('user');
|
||||
expect(res.body.mailboxes[0].aliasTarget).to.equal(null);
|
||||
expect(res.body.mailboxes[0].domain).to.equal(DOMAIN_0.domain);
|
||||
done();
|
||||
@@ -914,12 +908,10 @@ describe('Mail API', function () {
|
||||
expect(res.body.aliases.length).to.eql(2);
|
||||
expect(res.body.aliases[0].name).to.equal('hello');
|
||||
expect(res.body.aliases[0].ownerId).to.equal(userId);
|
||||
expect(res.body.aliases[0].ownerType).to.equal('user');
|
||||
expect(res.body.aliases[0].aliasTarget).to.equal(MAILBOX_NAME);
|
||||
expect(res.body.aliases[0].domain).to.equal(DOMAIN_0.domain);
|
||||
expect(res.body.aliases[1].name).to.equal('there');
|
||||
expect(res.body.aliases[1].ownerId).to.equal(userId);
|
||||
expect(res.body.aliases[1].ownerType).to.equal('user');
|
||||
expect(res.body.aliases[1].aliasTarget).to.equal(MAILBOX_NAME);
|
||||
expect(res.body.aliases[1].domain).to.equal(DOMAIN_0.domain);
|
||||
done();
|
||||
@@ -1031,7 +1023,6 @@ describe('Mail API', function () {
|
||||
expect(res.body.list).to.be.an('object');
|
||||
expect(res.body.list.name).to.equal(LIST_NAME);
|
||||
expect(res.body.list.ownerId).to.equal('admin');
|
||||
expect(res.body.list.ownerType).to.equal('group');
|
||||
expect(res.body.list.aliasTarget).to.equal(null);
|
||||
expect(res.body.list.domain).to.equal(DOMAIN_0.domain);
|
||||
expect(res.body.list.members).to.eql([ 'admin2', 'superadmin' ]);
|
||||
@@ -1048,7 +1039,6 @@ describe('Mail API', function () {
|
||||
expect(res.body.lists.length).to.equal(1);
|
||||
expect(res.body.lists[0].name).to.equal(LIST_NAME);
|
||||
expect(res.body.lists[0].ownerId).to.equal('admin');
|
||||
expect(res.body.lists[0].ownerType).to.equal('group');
|
||||
expect(res.body.lists[0].aliasTarget).to.equal(null);
|
||||
expect(res.body.lists[0].domain).to.equal(DOMAIN_0.domain);
|
||||
expect(res.body.lists[0].members).to.eql([ 'admin2', 'superadmin' ]);
|
||||
|
||||
@@ -24,8 +24,6 @@ const EMAIL_0_NEW_FALLBACK = 'stupIDfallback@me.com';
|
||||
const DISPLAY_NAME_0_NEW = 'New Name';
|
||||
|
||||
describe('Profile API', function () {
|
||||
this.timeout(5000);
|
||||
|
||||
var user_0 = null;
|
||||
var token_0;
|
||||
|
||||
|
||||
@@ -137,12 +137,12 @@ describe('REST API', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('dns setup twice fails', function (done) {
|
||||
it('dns setup twice succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
|
||||
.send({ dnsConfig: { provider: 'noop', domain: DOMAIN, DOMAIN, config: {} } })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(409);
|
||||
expect(result.statusCode).to.eql(200);
|
||||
|
||||
done();
|
||||
});
|
||||
@@ -247,6 +247,17 @@ describe('REST API', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('dns setup after activation fails', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
|
||||
.send({ dnsConfig: { provider: 'noop', domain: DOMAIN, DOMAIN, config: {} } })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(409);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not crash with invalid JSON', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
|
||||
@@ -61,8 +61,6 @@ function cleanup(done) {
|
||||
}
|
||||
|
||||
describe('SSH API', function () {
|
||||
this.timeout(10000);
|
||||
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
|
||||
@@ -65,29 +65,27 @@ function cleanup(done) {
|
||||
}
|
||||
|
||||
describe('Internal API', function () {
|
||||
this.timeout(5000);
|
||||
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
describe('backup', function () {
|
||||
it('succeeds', function (done) {
|
||||
superagent.post(config.sysadminOrigin() + '/api/v1/backup')
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(202);
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(202);
|
||||
|
||||
function checkBackupStartEvent() {
|
||||
eventlog.getAllPaged([ eventlog.ACTION_BACKUP_START ], '', 1, 100, function (error, result) {
|
||||
expect(error).to.equal(null);
|
||||
function checkBackupStartEvent() {
|
||||
eventlog.getAllPaged([ eventlog.ACTION_BACKUP_START ], '', 1, 100, function (error, result) {
|
||||
expect(error).to.equal(null);
|
||||
|
||||
if (result.length === 0) return setTimeout(checkBackupStartEvent, 1000);
|
||||
if (result.length === 0) return setTimeout(checkBackupStartEvent, 1000);
|
||||
|
||||
done();
|
||||
});
|
||||
}
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
checkBackupStartEvent();
|
||||
});
|
||||
checkBackupStartEvent();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
138
src/routes/test/tasks-test.js
Normal file
138
src/routes/test/tasks-test.js
Normal file
@@ -0,0 +1,138 @@
|
||||
'use strict';
|
||||
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
var async = require('async'),
|
||||
config = require('../../config.js'),
|
||||
database = require('../../database.js'),
|
||||
expect = require('expect.js'),
|
||||
server = require('../../server.js'),
|
||||
superagent = require('superagent'),
|
||||
tasks = require('../../tasks.js');
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
|
||||
var token = null;
|
||||
|
||||
function setup(done) {
|
||||
config._reset();
|
||||
config.setFqdn('example-tasks-test.com');
|
||||
config.setAdminFqdn('my.example-tasks-test.com');
|
||||
|
||||
async.series([
|
||||
server.start.bind(null),
|
||||
database._clear.bind(null),
|
||||
|
||||
function createAdmin(callback) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(201);
|
||||
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
], done);
|
||||
}
|
||||
|
||||
function cleanup(done) {
|
||||
database._clear(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
|
||||
server.stop(done);
|
||||
});
|
||||
}
|
||||
|
||||
describe('Tasks API', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
it('can get task', function (done) {
|
||||
let taskId = null;
|
||||
let task = tasks.startTask(tasks._TASK_IDENTITY, [ 'ping' ]);
|
||||
task.on('error', done);
|
||||
task.on('start', (tid) => { taskId = tid; });
|
||||
|
||||
task.on('finish', function () {
|
||||
superagent.get(SERVER_URL + '/api/v1/tasks/' + taskId)
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.percent).to.be(100);
|
||||
expect(res.body.args).to.be(undefined);
|
||||
expect(res.body.active).to.be(false); // finished
|
||||
expect(res.body.result).to.be('ping');
|
||||
expect(res.body.errorMessage).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can get logs', function (done) {
|
||||
let taskId = null;
|
||||
let task = tasks.startTask(tasks._TASK_CRASH, [ 'ping' ]);
|
||||
task.on('error', done);
|
||||
task.on('start', (tid) => { taskId = tid; });
|
||||
|
||||
task.on('finish', function () {
|
||||
superagent.get(SERVER_URL + '/api/v1/tasks/' + taskId + '/logs')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot stop inactive task', function (done) {
|
||||
let taskId = null;
|
||||
let task = tasks.startTask(tasks._TASK_IDENTITY, [ 'ping' ]);
|
||||
task.on('error', done);
|
||||
task.on('start', (tid) => { taskId = tid; });
|
||||
|
||||
task.on('finish', function () {
|
||||
superagent.post(SERVER_URL + '/api/v1/tasks/' + taskId + '/stop')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(409);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('can stop task', function (done) {
|
||||
let taskId = null;
|
||||
let task = tasks.startTask(tasks._TASK_SLEEP, [ 10000 ]);
|
||||
task.on('error', done);
|
||||
task.on('start', (tid) => {
|
||||
taskId = tid;
|
||||
superagent.post(SERVER_URL + '/api/v1/tasks/' + taskId + '/stop')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(204);
|
||||
});
|
||||
});
|
||||
task.on('finish', () => {
|
||||
superagent.get(SERVER_URL + '/api/v1/tasks/' + taskId)
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.percent).to.be(100);
|
||||
expect(res.body.active).to.be(false); // finished
|
||||
expect(res.body.result).to.be(null);
|
||||
expect(res.body.errorMessage).to.contain('signal SIGTERM');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -82,8 +82,6 @@ function checkMails(number, done) {
|
||||
}
|
||||
|
||||
describe('Users API', function () {
|
||||
this.timeout(5000);
|
||||
|
||||
var user_0, user_1, user_2, user_4;
|
||||
var token = null, userToken = null;
|
||||
var token_1 = tokendb.generateToken();
|
||||
|
||||
@@ -9,17 +9,10 @@ if (process.argv[2] === '--check') return console.log('OK');
|
||||
|
||||
require('supererror')({ splatchError: true });
|
||||
|
||||
// remove timestamp from debug() based output
|
||||
require('debug').formatArgs = function formatArgs(args) {
|
||||
args[0] = this.namespace + ' ' + args[0];
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
backups = require('./backups.js'),
|
||||
database = require('./database.js'),
|
||||
debug = require('debug')('box:backuptask'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance');
|
||||
backups = require('../backups.js'),
|
||||
database = require('../database.js'),
|
||||
debug = require('debug')('box:backupupload');
|
||||
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -38,17 +31,21 @@ process.on('SIGTERM', function () {
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// this can happen when the backup task is terminated (not box code)
|
||||
process.on('disconnect', function () {
|
||||
debug('parent process died');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
initialize(function (error) {
|
||||
if (error) throw error;
|
||||
|
||||
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, '');
|
||||
|
||||
backups.upload(backupId, format, dataDir, function resultHandler(error) {
|
||||
backups.upload(backupId, format, dataDir, (progress) => process.send(progress), function resultHandler(error) {
|
||||
if (error) debug('upload completed with error', error);
|
||||
|
||||
debug('upload completed');
|
||||
|
||||
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, error ? error.message : '');
|
||||
process.send({ result: error ? error.message : '' });
|
||||
|
||||
// https://nodejs.org/api/process.html are exit codes used by node. apps.js uses the value below
|
||||
// to check apptask crashes
|
||||
18
src/scripts/restartdocker.sh
Executable file
18
src/scripts/restartdocker.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $# == 1 && "$1" == "--check" ]]; then
|
||||
echo "OK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${BOX_ENV}" == "cloudron" ]]; then
|
||||
systemctl restart docker
|
||||
fi
|
||||
|
||||
19
src/scripts/restartunbound.sh
Executable file
19
src/scripts/restartunbound.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $# == 1 && "$1" == "--check" ]]; then
|
||||
echo "OK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${BOX_ENV}" == "cloudron" ]]; then
|
||||
unbound-anchor -a /var/lib/unbound/root.key
|
||||
systemctl restart unbound
|
||||
fi
|
||||
|
||||
@@ -30,21 +30,32 @@ if systemctl reset-failed "${UPDATER_SERVICE}"; then
|
||||
echo "=> service has failed earlier"
|
||||
fi
|
||||
|
||||
echo "=> Run installer.sh as cloudron-updater.service"
|
||||
if ! systemd-run --unit "${UPDATER_SERVICE}" ${installer_path}; then
|
||||
echo "Failed to install cloudron. See ${LOG_FILE} for details"
|
||||
# StandardError will follow StandardOutput in default inherit mode. https://www.freedesktop.org/software/systemd/man/systemd.exec.html
|
||||
echo "=> Run installer.sh as ${UPDATER_SERVICE}."
|
||||
if [[ "$(systemd --version | head -n1)" != "systemd 22"* ]]; then
|
||||
readonly DATETIME=`date '+%Y-%m-%d_%H-%M-%S'`
|
||||
readonly LOG_FILE="/home/yellowtent/platformdata/logs/updater/cloudron-updater-${DATETIME}.log"
|
||||
|
||||
update_service_options="-p StandardOutput=file:${LOG_FILE}"
|
||||
echo "=> starting service (ubuntu 18.04) ${UPDATER_SERVICE}. see logs at ${LOG_FILE}"
|
||||
else
|
||||
update_service_options=""
|
||||
echo "=> starting service (ubuntu 16.04) ${UPDATER_SERVICE}. see logs using journalctl -u ${UPDATER_SERVICE}"
|
||||
fi
|
||||
|
||||
if ! systemd-run --unit "${UPDATER_SERVICE}" $update_service_options ${installer_path}; then
|
||||
echo "Failed to install cloudron. See log for details"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=> service ${UPDATER_SERVICE} started."
|
||||
echo "=> See logs with journalctl -u ${UPDATER_SERVICE} -f"
|
||||
|
||||
while true; do
|
||||
if systemctl is-failed "${UPDATER_SERVICE}"; then
|
||||
if systemctl is-failed "${UPDATER_SERVICE}" >/dev/null 2>&1; then
|
||||
echo "=> ${UPDATER_SERVICE} has failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "${UPDATER_SERVICE} is still active. will check in 5 seconds"
|
||||
|
||||
sleep 5
|
||||
# this loop will stop once the update process stopped the box unit and thus terminating this child process
|
||||
done
|
||||
|
||||
@@ -108,12 +108,11 @@ function initializeExpressSync() {
|
||||
var csrf = routes.oauth2.csrf();
|
||||
|
||||
// public routes
|
||||
router.post('/api/v1/cloudron/setup', routes.setup.providerTokenAuth, routes.setup.provision); // only available until no-domain
|
||||
router.post('/api/v1/cloudron/restore', routes.setup.restore); // only available until activated
|
||||
router.post('/api/v1/cloudron/activate', routes.setup.setupTokenAuth, routes.setup.activate);
|
||||
router.post('/api/v1/cloudron/setup', routes.provision.providerTokenAuth, routes.provision.setup); // only available until no-domain
|
||||
router.post('/api/v1/cloudron/restore', routes.provision.restore); // only available until activated
|
||||
router.post('/api/v1/cloudron/activate', routes.provision.setupTokenAuth, routes.provision.activate);
|
||||
router.get ('/api/v1/cloudron/status', routes.cloudron.getStatus);
|
||||
|
||||
router.get ('/api/v1/cloudron/progress', routes.cloudron.getProgress);
|
||||
router.get ('/api/v1/cloudron/avatar', routes.settings.getCloudronAvatar); // this is a public alias for /api/v1/settings/cloudron_avatar
|
||||
|
||||
// developer routes
|
||||
@@ -122,7 +121,10 @@ function initializeExpressSync() {
|
||||
// cloudron routes
|
||||
router.get ('/api/v1/cloudron/update', cloudronScope, routes.cloudron.getUpdateInfo);
|
||||
router.post('/api/v1/cloudron/update', cloudronScope, routes.cloudron.update);
|
||||
router.post('/api/v1/cloudron/set_dashboard_domain', cloudronScope, routes.cloudron.setDashboardDomain);
|
||||
router.post('/api/v1/cloudron/renew_certs', cloudronScope, routes.cloudron.renewCerts);
|
||||
router.post('/api/v1/cloudron/check_for_updates', cloudronScope, routes.cloudron.checkForUpdates);
|
||||
router.get ('/api/v1/cloudron/reboot', cloudronScope, routes.cloudron.isRebootRequired);
|
||||
router.post('/api/v1/cloudron/reboot', cloudronScope, routes.cloudron.reboot);
|
||||
router.get ('/api/v1/cloudron/graphs', cloudronScope, routes.graphs.getGraphs);
|
||||
router.get ('/api/v1/cloudron/disks', cloudronScope, routes.cloudron.getDisks);
|
||||
@@ -134,6 +136,17 @@ function initializeExpressSync() {
|
||||
router.del ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, isUnmanaged, routes.ssh.delAuthorizedKey);
|
||||
router.get ('/api/v1/cloudron/eventlog', cloudronScope, routes.eventlog.get);
|
||||
|
||||
// tasks
|
||||
router.get ('/api/v1/tasks', settingsScope, routes.tasks.list);
|
||||
router.get ('/api/v1/tasks/:taskId', settingsScope, routes.tasks.get);
|
||||
router.get ('/api/v1/tasks/:taskId/logs', cloudronScope, routes.tasks.getLogs);
|
||||
router.get ('/api/v1/tasks/:taskId/logstream', cloudronScope, routes.tasks.getLogStream);
|
||||
router.post('/api/v1/tasks/:taskId/stop', settingsScope, routes.tasks.stopTask);
|
||||
|
||||
// backups
|
||||
router.get ('/api/v1/backups', settingsScope, routes.backups.list);
|
||||
router.post('/api/v1/backups', settingsScope, routes.backups.startBackup);
|
||||
|
||||
// config route (for dashboard)
|
||||
router.get ('/api/v1/config', profileScope, routes.cloudron.getConfig);
|
||||
|
||||
@@ -252,7 +265,7 @@ function initializeExpressSync() {
|
||||
router.post('/api/v1/mail/:domain/enable', mailScope, routes.mail.setMailEnabled);
|
||||
router.post('/api/v1/mail/:domain/dns', mailScope, routes.mail.setDnsRecords);
|
||||
router.post('/api/v1/mail/:domain/send_test_mail', mailScope, routes.mail.sendTestMail);
|
||||
router.get ('/api/v1/mail/:domain/mailboxes', mailScope, routes.mail.getMailboxes);
|
||||
router.get ('/api/v1/mail/:domain/mailboxes', mailScope, routes.mail.listMailboxes);
|
||||
router.get ('/api/v1/mail/:domain/mailboxes/:name', mailScope, routes.mail.getMailbox);
|
||||
router.post('/api/v1/mail/:domain/mailboxes', mailScope, routes.mail.addMailbox);
|
||||
router.post('/api/v1/mail/:domain/mailboxes/:name', mailScope, routes.mail.updateMailbox);
|
||||
@@ -269,21 +282,20 @@ function initializeExpressSync() {
|
||||
// feedback
|
||||
router.post('/api/v1/feedback', cloudronScope, isUnmanaged, routes.cloudron.feedback);
|
||||
|
||||
// backup routes
|
||||
router.get ('/api/v1/backups', settingsScope, routes.backups.get);
|
||||
router.post('/api/v1/backups', settingsScope, routes.backups.create);
|
||||
|
||||
// domain routes
|
||||
router.post('/api/v1/domains', domainsManageScope, routes.domains.add);
|
||||
router.get ('/api/v1/domains', domainsReadScope, routes.domains.getAll);
|
||||
router.get ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.domains.get); // this is manage scope because it returns non-restricted fields
|
||||
router.put ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.domains.update);
|
||||
router.post ('/api/v1/domains/:domain/renew_certs', domainsManageScope, verifyDomainLock, routes.domains.renewCerts);
|
||||
router.del ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.users.verifyPassword, routes.domains.del);
|
||||
|
||||
// caas routes
|
||||
router.get('/api/v1/caas/config', cloudronScope, routes.caas.getConfig);
|
||||
router.post('/api/v1/caas/change_plan', cloudronScope, routes.users.verifyPassword, routes.caas.changePlan);
|
||||
// addon routes
|
||||
router.get ('/api/v1/services', cloudronScope, routes.services.getAll);
|
||||
router.get ('/api/v1/services/:service', cloudronScope, routes.services.get);
|
||||
router.post('/api/v1/services/:service', cloudronScope, routes.services.configure);
|
||||
router.get ('/api/v1/services/:service/logs', cloudronScope, routes.services.getLogs);
|
||||
router.get ('/api/v1/services/:service/logstream', cloudronScope, routes.services.getLogStream);
|
||||
router.post('/api/v1/services/:service/restart', cloudronScope, routes.services.restart);
|
||||
|
||||
// disable server socket "idle" timeout. we use the timeout middleware to handle timeouts on a route level
|
||||
// we rely on nginx for timeouts on the TCP level (see client_header_timeout)
|
||||
|
||||
@@ -86,6 +86,7 @@ var gDefaults = (function () {
|
||||
provider: 'filesystem',
|
||||
key: '',
|
||||
backupFolder: '/var/backups',
|
||||
format: 'tgz',
|
||||
retentionSecs: 2 * 24 * 60 * 60, // 2 days
|
||||
intervalSecs: 24 * 60 * 60 // ~1 day
|
||||
};
|
||||
@@ -393,7 +394,7 @@ function setPlatformConfig(platformConfig, callback) {
|
||||
settingsdb.set(exports.PLATFORM_CONFIG_KEY, JSON.stringify(platformConfig), function (error) {
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
addons.updateAddonConfig(platformConfig, callback);
|
||||
addons.updateServiceConfig(platformConfig, callback);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
63
src/shell.js
63
src/shell.js
@@ -1,10 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
spawn: spawn,
|
||||
exec: exec,
|
||||
execSync: execSync,
|
||||
sudo: sudo,
|
||||
sudoSync: sudoSync
|
||||
sudo: sudo
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
@@ -15,21 +14,22 @@ var assert = require('assert'),
|
||||
|
||||
var SUDO = '/usr/bin/sudo';
|
||||
|
||||
function execSync(tag, cmd, callback) {
|
||||
function exec(tag, cmd, callback) {
|
||||
assert.strictEqual(typeof tag, 'string');
|
||||
assert.strictEqual(typeof cmd, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(cmd);
|
||||
try {
|
||||
child_process.execSync(cmd, { stdio: 'inherit' });
|
||||
} catch (e) {
|
||||
if (callback) return callback(e);
|
||||
throw e;
|
||||
}
|
||||
if (callback) callback();
|
||||
debug(`${tag} exec: ${cmd}`);
|
||||
|
||||
child_process.exec(cmd, function (error, stdout, stderr) {
|
||||
debug(`${tag} (stdout): %s`, stdout.toString('utf8'));
|
||||
debug(`${tag} (stderr): %s`, stderr.toString('utf8'));
|
||||
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
|
||||
function exec(tag, file, args, options, callback) {
|
||||
function spawn(tag, file, args, options, callback) {
|
||||
assert.strictEqual(typeof tag, 'string');
|
||||
assert.strictEqual(typeof file, 'string');
|
||||
assert(util.isArray(args));
|
||||
@@ -38,7 +38,9 @@ function exec(tag, file, args, options, callback) {
|
||||
|
||||
callback = once(callback); // exit may or may not be called after an 'error'
|
||||
|
||||
debug(tag + ' execFile: %s %s', file, args.join(' '));
|
||||
debug(tag + ' spawn: %s %s', file, args.join(' '));
|
||||
|
||||
if (options.ipc) options.stdio = ['pipe', 'pipe', 'pipe', 'ipc'];
|
||||
|
||||
var cp = child_process.spawn(file, args, options);
|
||||
if (options.logStream) {
|
||||
@@ -50,7 +52,7 @@ function exec(tag, file, args, options, callback) {
|
||||
});
|
||||
|
||||
cp.stderr.on('data', function (data) {
|
||||
debug(tag + ' (stderr): %s', data.toString('utf8'));
|
||||
debug(tag + ' (stdout): %s', data.toString('utf8'));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -75,33 +77,14 @@ function exec(tag, file, args, options, callback) {
|
||||
function sudo(tag, args, options, callback) {
|
||||
assert.strictEqual(typeof tag, 'string');
|
||||
assert(util.isArray(args));
|
||||
|
||||
if (typeof options === 'function') {
|
||||
callback = options;
|
||||
options = { };
|
||||
}
|
||||
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// -S makes sudo read stdin for password. -E preserves environment
|
||||
var cp = exec(tag, SUDO, [ options.env ? '-SE' : '-S' ].concat(args), options, callback);
|
||||
let sudoArgs = [ '-S' ]; // -S makes sudo read stdin for password
|
||||
if (options.preserveEnv) sudoArgs.push('-E'); // -E preserves environment
|
||||
if (options.ipc) sudoArgs.push('--close-from=4'); // keep the ipc open. requires closefrom_override in sudoers file
|
||||
|
||||
var cp = spawn(tag, SUDO, sudoArgs.concat(args), options, callback);
|
||||
cp.stdin.end();
|
||||
return cp;
|
||||
}
|
||||
|
||||
function sudoSync(tag, cmd, callback) {
|
||||
assert.strictEqual(typeof tag, 'string');
|
||||
assert.strictEqual(typeof cmd, 'string');
|
||||
|
||||
// -S makes sudo read stdin for password
|
||||
cmd = 'sudo -S ' + cmd;
|
||||
debug(cmd);
|
||||
|
||||
try {
|
||||
child_process.execSync(cmd, { stdio: 'inherit' });
|
||||
} catch (e) {
|
||||
if (callback) return callback(e);
|
||||
throw e;
|
||||
}
|
||||
if (callback) callback();
|
||||
}
|
||||
|
||||
100
src/ssh.js
100
src/ssh.js
@@ -14,7 +14,6 @@ exports = module.exports = {
|
||||
var assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:ssh'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
safe = require('safetydance'),
|
||||
shell = require('./shell.js'),
|
||||
@@ -56,61 +55,72 @@ function clear(callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
function saveKeys(keys) {
|
||||
function saveKeys(keys, callback) {
|
||||
assert(Array.isArray(keys));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!safe.fs.writeFileSync(AUTHORIZED_KEYS_TMP_FILEPATH, keys.map(function (k) { return k.key; }).join('\n'))) {
|
||||
debug('Error writing to temporary file', safe.error);
|
||||
return false;
|
||||
return callback(safe.error);
|
||||
}
|
||||
|
||||
try {
|
||||
// 600 = rw-------
|
||||
fs.chmodSync(AUTHORIZED_KEYS_TMP_FILEPATH, '600');
|
||||
} catch (e) {
|
||||
debug('Failed to adjust permissions of %s %j', AUTHORIZED_KEYS_TMP_FILEPATH, e);
|
||||
return false;
|
||||
if (!safe.fs.chmodSync(AUTHORIZED_KEYS_TMP_FILEPATH, '600')) { // 600 = rw-------
|
||||
debug('Failed to adjust permissions of %s %s', AUTHORIZED_KEYS_TMP_FILEPATH, safe.error);
|
||||
return callback(safe.error);
|
||||
}
|
||||
|
||||
var user = config.TEST ? process.env.USER : ((config.provider() === 'ec2' || config.provider() === 'lightsail' || config.provider() === 'ami') ? 'ubuntu' : 'root');
|
||||
shell.sudoSync('authorized_keys', util.format('%s %s %s %s', AUTHORIZED_KEYS_CMD, user, AUTHORIZED_KEYS_TMP_FILEPATH, AUTHORIZED_KEYS_FILEPATH));
|
||||
shell.sudo('authorized_keys', [ AUTHORIZED_KEYS_CMD, user, AUTHORIZED_KEYS_TMP_FILEPATH, AUTHORIZED_KEYS_FILEPATH ], {}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
return true;
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function getKeys() {
|
||||
shell.sudoSync('authorized_keys', util.format('%s %s %s %s', AUTHORIZED_KEYS_CMD, process.env.USER, AUTHORIZED_KEYS_FILEPATH, AUTHORIZED_KEYS_TMP_FILEPATH));
|
||||
function getKeys(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var content = safe.fs.readFileSync(AUTHORIZED_KEYS_TMP_FILEPATH, 'utf8');
|
||||
if (!content) return [];
|
||||
shell.sudo('authorized_keys', [ AUTHORIZED_KEYS_CMD, process.env.USER, AUTHORIZED_KEYS_FILEPATH, AUTHORIZED_KEYS_TMP_FILEPATH ], {}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var keys = content.split('\n')
|
||||
.filter(function (k) { return !!k.trim(); })
|
||||
.map(function (k) { return { identifier: k.split(' ')[2], key: k }; })
|
||||
.filter(function (k) { return k.identifier && k.key; });
|
||||
var content = safe.fs.readFileSync(AUTHORIZED_KEYS_TMP_FILEPATH, 'utf8');
|
||||
if (!content) return callback(null, []);
|
||||
|
||||
safe.fs.unlinkSync(AUTHORIZED_KEYS_TMP_FILEPATH);
|
||||
var keys = content.split('\n')
|
||||
.filter(function (k) { return !!k.trim(); })
|
||||
.map(function (k) { return { identifier: k.split(' ')[2], key: k }; })
|
||||
.filter(function (k) { return k.identifier && k.key; });
|
||||
|
||||
return keys;
|
||||
safe.fs.unlinkSync(AUTHORIZED_KEYS_TMP_FILEPATH);
|
||||
|
||||
return callback(null, keys);
|
||||
});
|
||||
}
|
||||
|
||||
function getAuthorizedKeys(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
return callback(null, getKeys().sort(function (a, b) { return a.identifier.localeCompare(b.identifier); }));
|
||||
getKeys(function (error, keys) {
|
||||
if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, keys.sort(function (a, b) { return a.identifier.localeCompare(b.identifier); }));
|
||||
});
|
||||
}
|
||||
|
||||
function getAuthorizedKey(identifier, callback) {
|
||||
assert.strictEqual(typeof identifier, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var keys = getKeys();
|
||||
if (keys.length === 0) return callback(new SshError(SshError.NOT_FOUND));
|
||||
getKeys(function (error, keys) {
|
||||
if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error));
|
||||
|
||||
var key = keys.find(function (k) { return k.identifier === identifier; });
|
||||
if (!key) return callback(new SshError(SshError.NOT_FOUND));
|
||||
if (keys.length === 0) return callback(new SshError(SshError.NOT_FOUND));
|
||||
|
||||
callback(null, key);
|
||||
var key = keys.find(function (k) { return k.identifier === identifier; });
|
||||
if (!key) return callback(new SshError(SshError.NOT_FOUND));
|
||||
|
||||
callback(null, key);
|
||||
});
|
||||
}
|
||||
|
||||
function addAuthorizedKey(key, callback) {
|
||||
@@ -124,28 +134,38 @@ function addAuthorizedKey(key, callback) {
|
||||
|
||||
var identifier = tmp[2];
|
||||
|
||||
var keys = getKeys();
|
||||
var index = keys.findIndex(function (k) { return k.identifier === identifier; });
|
||||
if (index !== -1) keys[index] = { identifier: identifier, key: key };
|
||||
else keys.push({ identifier: identifier, key: key });
|
||||
getKeys(function (error, keys) {
|
||||
if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error));
|
||||
|
||||
if (!saveKeys(keys)) return callback(new SshError(SshError.INTERNAL_ERROR));
|
||||
var index = keys.findIndex(function (k) { return k.identifier === identifier; });
|
||||
if (index !== -1) keys[index] = { identifier: identifier, key: key };
|
||||
else keys.push({ identifier: identifier, key: key });
|
||||
|
||||
callback();
|
||||
saveKeys(keys, function (error) {
|
||||
if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function delAuthorizedKey(identifier, callback) {
|
||||
assert.strictEqual(typeof identifier, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var keys = getKeys();
|
||||
var index = keys.findIndex(function (k) { return k.identifier === identifier; });
|
||||
if (index === -1) return callback(new SshError(SshError.NOT_FOUND));
|
||||
getKeys(function (error, keys) {
|
||||
if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error));
|
||||
|
||||
// now remove the key
|
||||
keys.splice(index, 1);
|
||||
let index = keys.findIndex(function (k) { return k.identifier === identifier; });
|
||||
if (index === -1) return callback(new SshError(SshError.NOT_FOUND));
|
||||
|
||||
if (!saveKeys(keys)) return callback(new SshError(SshError.INTERNAL_ERROR));
|
||||
// now remove the key
|
||||
keys.splice(index, 1);
|
||||
|
||||
callback();
|
||||
saveKeys(keys, function (error) {
|
||||
if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ function copy(apiConfig, oldFilePath, newFilePath) {
|
||||
|
||||
// this will hardlink backups saving space
|
||||
var cpOptions = apiConfig.noHardlinks ? '-a' : '-al';
|
||||
shell.exec('copy', '/bin/cp', [ cpOptions, oldFilePath, newFilePath ], { }, function (error) {
|
||||
shell.spawn('copy', '/bin/cp', [ cpOptions, oldFilePath, newFilePath ], { }, function (error) {
|
||||
if (error) return events.emit('done', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||
|
||||
events.emit('done', null);
|
||||
@@ -155,7 +155,7 @@ function removeDir(apiConfig, pathPrefix) {
|
||||
|
||||
events.emit('progress', `removeDir: ${pathPrefix}`);
|
||||
|
||||
shell.exec('removeDir', '/bin/rm', [ '-rf', pathPrefix ], { }, function (error) {
|
||||
shell.spawn('removeDir', '/bin/rm', [ '-rf', pathPrefix ], { }, function (error) {
|
||||
if (error) return events.emit('done', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||
|
||||
events.emit('done', null);
|
||||
|
||||
115
src/taskdb.js
Normal file
115
src/taskdb.js
Normal file
@@ -0,0 +1,115 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
get: get,
|
||||
add: add,
|
||||
update: update,
|
||||
del: del,
|
||||
listByTypePaged: listByTypePaged
|
||||
};
|
||||
|
||||
let assert = require('assert'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror'),
|
||||
safe = require('safetydance');
|
||||
|
||||
const TASKS_FIELDS = [ 'id', 'type', 'argsJson', 'percent', 'message', 'errorMessage', 'creationTime', 'result', 'ts' ];
|
||||
|
||||
function postProcess(result) {
|
||||
assert.strictEqual(typeof result, 'object');
|
||||
|
||||
assert(result.argsJson === null || typeof result.argsJson === 'string');
|
||||
result.args = safe.JSON.parse(result.argsJson) || [];
|
||||
delete result.argsJson;
|
||||
|
||||
result.id = String(result.id);
|
||||
}
|
||||
|
||||
function add(task, callback) {
|
||||
assert.strictEqual(typeof task, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const query = 'INSERT INTO tasks (type, argsJson, percent, message) VALUES (?, ?, ?, ?)';
|
||||
const args = [ task.type, JSON.stringify(task.args), task.percent, task.message ];
|
||||
|
||||
database.query(query, args, function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, String(result.insertId));
|
||||
});
|
||||
}
|
||||
|
||||
function update(id, data, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let args = [ ];
|
||||
let fields = [ ];
|
||||
for (let k in data) {
|
||||
fields.push(k + ' = ?');
|
||||
args.push(data[k]);
|
||||
}
|
||||
args.push(id);
|
||||
|
||||
database.query('UPDATE tasks SET ' + fields.join(', ') + ' WHERE id = ?', args, function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function get(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + TASKS_FIELDS + ' FROM tasks WHERE id = ?', [ id ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
postProcess(result[0]);
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function del(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM tasks WHERE id = ?', [ id ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function listByTypePaged(type, page, perPage, callback) {
|
||||
assert(typeof type === 'string' || type === null);
|
||||
assert.strictEqual(typeof page, 'number');
|
||||
assert.strictEqual(typeof perPage, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var data = [];
|
||||
var query = 'SELECT ' + TASKS_FIELDS + ' FROM tasks';
|
||||
|
||||
if (type) {
|
||||
query += ' WHERE TYPE=?';
|
||||
data.push(type);
|
||||
}
|
||||
|
||||
query += ' ORDER BY creationTime DESC LIMIT ?,?';
|
||||
|
||||
data.push((page-1)*perPage);
|
||||
data.push(perPage);
|
||||
|
||||
database.query(query, data, function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
results.forEach(postProcess);
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
223
src/tasks.js
Normal file
223
src/tasks.js
Normal file
@@ -0,0 +1,223 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
get: get,
|
||||
update: update,
|
||||
listByTypePaged: listByTypePaged,
|
||||
|
||||
getLogs: getLogs,
|
||||
|
||||
startTask: startTask,
|
||||
stopTask: stopTask,
|
||||
|
||||
removePrivateFields: removePrivateFields,
|
||||
|
||||
TaskError: TaskError,
|
||||
|
||||
// task types. if you add a task here, fill up the function table in taskworker
|
||||
TASK_BACKUP: 'backup',
|
||||
TASK_UPDATE: 'update',
|
||||
TASK_RENEW_CERTS: 'renewcerts',
|
||||
|
||||
// testing
|
||||
_TASK_IDENTITY: '_identity',
|
||||
_TASK_CRASH: '_crash',
|
||||
_TASK_ERROR: '_error',
|
||||
_TASK_SLEEP: '_sleep'
|
||||
};
|
||||
|
||||
let assert = require('assert'),
|
||||
child_process = require('child_process'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:tasks'),
|
||||
EventEmitter = require('events'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
spawn = require('child_process').spawn,
|
||||
split = require('split'),
|
||||
taskdb = require('./taskdb.js'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
let gTasks = {}; // indexed by task id
|
||||
|
||||
function TaskError(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(TaskError, Error);
|
||||
TaskError.INTERNAL_ERROR = 'Internal Error';
|
||||
TaskError.BAD_STATE = 'Bad State';
|
||||
TaskError.NOT_FOUND = 'Not Found';
|
||||
|
||||
function get(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
taskdb.get(id, function (error, task) {
|
||||
if (error && error.reason == DatabaseError.NOT_FOUND) return callback(new TaskError(TaskError.NOT_FOUND));
|
||||
if (error) return callback(new TaskError(TaskError.INTERNAL_ERROR, error));
|
||||
|
||||
task.active = !!gTasks[id];
|
||||
|
||||
callback(null, task);
|
||||
});
|
||||
}
|
||||
|
||||
function update(id, task, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof task, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`${id}: ${JSON.stringify(task)}`);
|
||||
|
||||
taskdb.update(id, task, function (error) {
|
||||
if (error && error.reason == DatabaseError.NOT_FOUND) return callback(new TaskError(TaskError.NOT_FOUND));
|
||||
if (error) return callback(new TaskError(TaskError.INTERNAL_ERROR, error));
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function startTask(type, args) {
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(Array.isArray(args));
|
||||
|
||||
let events = new EventEmitter();
|
||||
|
||||
taskdb.add({ type: type, percent: 0, message: 'Starting', args: args }, function (error, taskId) {
|
||||
if (error) return events.emit('error', new TaskError(TaskError.INTERNAL_ERROR, error));
|
||||
|
||||
const logFile = `${paths.TASKS_LOG_DIR}/${taskId}.log`;
|
||||
let fd = safe.fs.openSync(logFile, 'a'); // will autoclose
|
||||
if (!fd) {
|
||||
debug(`startTask: unable to get log filedescriptor ${safe.error.message}`);
|
||||
return events.emit('error', new TaskError(TaskError.INTERNAL_ERROR, error.message));
|
||||
}
|
||||
|
||||
debug(`startTask - starting task ${type}. logs at ${logFile} id ${taskId}`);
|
||||
|
||||
gTasks[taskId] = child_process.fork(`${__dirname}/taskworker.js`, [ taskId ], { stdio: [ 'pipe', fd, fd, 'ipc' ]}); // fork requires ipc
|
||||
gTasks[taskId].once('exit', function (code, signal) {
|
||||
debug(`startTask: ${taskId} completed with code ${code} and signal ${signal}`);
|
||||
|
||||
get(taskId, function (error, task) {
|
||||
if (!error && task.percent !== 100) { // task crashed or was killed by us (code 50)
|
||||
error = code === 0 ? new Error(`${taskId} task stopped`) : new Error(`${taskId} task crashed with code ${code} and signal ${signal}`);
|
||||
update(taskId, { percent: 100, errorMessage: error.message }, NOOP_CALLBACK);
|
||||
} else if (!error && task.errorMessage) {
|
||||
error = new Error(task.errorMessage);
|
||||
} else if (!task) { // db got cleared in tests
|
||||
error = new Error(`No such task ${taskId}`);
|
||||
}
|
||||
|
||||
gTasks[taskId] = null;
|
||||
|
||||
events.emit('finish', error, task ? task.result : null);
|
||||
|
||||
debug(`startTask: ${taskId} done`);
|
||||
});
|
||||
});
|
||||
|
||||
events.emit('start', taskId);
|
||||
});
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
function stopTask(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!gTasks[id]) return callback(new TaskError(TaskError.BAD_STATE, 'task is not active'));
|
||||
|
||||
debug(`stopTask: stopping task ${id}`);
|
||||
|
||||
gTasks[id].kill('SIGTERM'); // this will end up calling the 'exit' signal handler
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function listByTypePaged(type, page, perPage, callback) {
|
||||
assert(typeof type === 'string' || type === null);
|
||||
assert.strictEqual(typeof page, 'number');
|
||||
assert.strictEqual(typeof perPage, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
taskdb.listByTypePaged(type, page, perPage, function (error, tasks) {
|
||||
if (error) return callback(new TaskError(TaskError.INTERNAL_ERROR, error));
|
||||
|
||||
tasks.forEach((task) => { task.active = !!gTasks[task.id]; });
|
||||
|
||||
callback(null, tasks);
|
||||
});
|
||||
}
|
||||
|
||||
function getLogs(taskId, options, callback) {
|
||||
assert.strictEqual(typeof taskId, 'string');
|
||||
assert(options && typeof options === 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`Getting logs for ${taskId}`);
|
||||
|
||||
var lines = options.lines || 100,
|
||||
format = options.format || 'json',
|
||||
follow = !!options.follow;
|
||||
|
||||
assert.strictEqual(typeof lines, 'number');
|
||||
assert.strictEqual(typeof format, 'string');
|
||||
|
||||
let cmd = '/usr/bin/tail';
|
||||
var args = [ '--lines=' + lines ];
|
||||
|
||||
if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs
|
||||
args.push(`${paths.TASKS_LOG_DIR}/${taskId}.log`);
|
||||
|
||||
var cp = spawn(cmd, args);
|
||||
|
||||
var transformStream = split(function mapper(line) {
|
||||
if (format !== 'json') return line + '\n';
|
||||
|
||||
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
|
||||
var timestamp = (new Date(data[0])).getTime();
|
||||
if (isNaN(timestamp)) timestamp = 0;
|
||||
var message = line.slice(data[0].length+1);
|
||||
|
||||
// ignore faulty empty logs
|
||||
if (!timestamp && !message) return;
|
||||
|
||||
return JSON.stringify({
|
||||
realtimeTimestamp: timestamp * 1000,
|
||||
message: message,
|
||||
source: taskId
|
||||
}) + '\n';
|
||||
});
|
||||
|
||||
transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process
|
||||
|
||||
cp.stdout.pipe(transformStream);
|
||||
|
||||
callback(null, transformStream);
|
||||
}
|
||||
|
||||
// removes all fields that are strictly private and should never be returned by API calls
|
||||
function removePrivateFields(task) {
|
||||
var result = _.pick(task, 'id', 'type', 'percent', 'message', 'errorMessage', 'active', 'creationTime', 'result', 'ts');
|
||||
return result;
|
||||
}
|
||||
51
src/taskworker.js
Executable file
51
src/taskworker.js
Executable file
@@ -0,0 +1,51 @@
|
||||
'use strict';
|
||||
|
||||
require('supererror')({ splatchError: true });
|
||||
|
||||
var assert = require('assert'),
|
||||
backups = require('./backups.js'),
|
||||
database = require('./database.js'),
|
||||
debug = require('debug')('box:taskworker'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
tasks = require('./tasks.js'),
|
||||
updater = require('./updater.js');
|
||||
|
||||
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
const TASKS = { // indexed by task type
|
||||
backup: backups.backupBoxAndApps,
|
||||
update: updater.update,
|
||||
renewcerts: reverseProxy.renewCerts,
|
||||
|
||||
_identity: (arg, progressCallback, callback) => callback(null, arg),
|
||||
_error: (arg, progressCallback, callback) => callback(new Error(`Failed for arg: ${arg}`)),
|
||||
_crash: (arg) => { throw new Error(`Crashing for arg: ${arg}`); },
|
||||
_sleep: (arg) => setTimeout(process.exit, arg)
|
||||
};
|
||||
|
||||
process.on('SIGTERM', function () {
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
assert.strictEqual(process.argv.length, 3, 'Pass the taskid as argument');
|
||||
const taskId = process.argv[2];
|
||||
|
||||
// Main process starts here
|
||||
debug(`Staring task ${taskId}`);
|
||||
|
||||
database.initialize(function (error) {
|
||||
if (error) return process.exit(50);
|
||||
|
||||
tasks.get(taskId, function (error, task) {
|
||||
if (error) return process.exit(50);
|
||||
|
||||
const progressCallback = (progress) => tasks.update(taskId, progress, NOOP_CALLBACK);
|
||||
const resultCallback = (error, result) => {
|
||||
const progress = { percent: 100, result: result || null, errorMessage: error ? error.message : null };
|
||||
|
||||
tasks.update(taskId, progress, () => process.exit(error ? 50 : 0));
|
||||
};
|
||||
|
||||
TASKS[task.type].apply(null, task.args.concat(progressCallback).concat(resultCallback));
|
||||
});
|
||||
});
|
||||
@@ -16,73 +16,31 @@ var async = require('async'),
|
||||
fs = require('fs'),
|
||||
os = require('os'),
|
||||
mkdirp = require('mkdirp'),
|
||||
readdirp = require('readdirp'),
|
||||
path = require('path'),
|
||||
progress = require('../progress.js'),
|
||||
rimraf = require('rimraf'),
|
||||
settings = require('../settings.js'),
|
||||
SettingsError = require('../settings.js').SettingsError;
|
||||
|
||||
function compareDirectories(one, two, callback) {
|
||||
readdirp({ root: one }, function (error, treeOne) {
|
||||
if (error) return callback(error);
|
||||
|
||||
readdirp({ root: two }, function (error, treeTwo) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var mismatch = [];
|
||||
|
||||
function compareDirs(a, b) {
|
||||
a.forEach(function (tmpA) {
|
||||
var found = b.find(function (tmpB) {
|
||||
return tmpA.path === tmpB.path;
|
||||
});
|
||||
|
||||
if (!found) mismatch.push(tmpA);
|
||||
});
|
||||
}
|
||||
|
||||
function compareFiles(a, b) {
|
||||
a.forEach(function (tmpA) {
|
||||
var found = b.find(function (tmpB) {
|
||||
// TODO check file or symbolic link
|
||||
return tmpA.path === tmpB.path && tmpA.mode === tmpB.mode;
|
||||
});
|
||||
|
||||
if (!found) mismatch.push(tmpA);
|
||||
});
|
||||
}
|
||||
|
||||
compareDirs(treeOne.directories, treeTwo.directories);
|
||||
compareDirs(treeTwo.directories, treeOne.directories);
|
||||
compareFiles(treeOne.files, treeTwo.files);
|
||||
compareFiles(treeTwo.files, treeOne.files);
|
||||
|
||||
if (mismatch.length) {
|
||||
console.error('Files not found in both: %j', mismatch);
|
||||
return callback(new Error('file mismatch'));
|
||||
}
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
SettingsError = require('../settings.js').SettingsError,
|
||||
tasks = require('../tasks.js');
|
||||
|
||||
function createBackup(callback) {
|
||||
backups.backup({ username: 'test' }, function (error) { // this call does not wait for the backup!
|
||||
backups.startBackupTask({ username: 'test' }, function (error, taskId) { // this call does not wait for the backup!
|
||||
if (error) return callback(error);
|
||||
|
||||
function waitForBackup() {
|
||||
var p = progress.getAll();
|
||||
if (p.backup.percent !== 100) return setTimeout(waitForBackup, 1000);
|
||||
|
||||
if (p.backup.message) return callback(new Error('backup failed:' + p.backup.message));
|
||||
|
||||
backups.getByStatePaged(backupdb.BACKUP_STATE_NORMAL, 1, 1, function (error, result) {
|
||||
tasks.get(taskId, function (error, p) {
|
||||
if (error) return callback(error);
|
||||
if (result.length !== 1) return callback(new Error('result is not of length 1'));
|
||||
|
||||
callback(null, result[0]);
|
||||
if (p.percent !== 100) return setTimeout(waitForBackup, 1000);
|
||||
|
||||
if (p.errorMessage) return callback(new Error('backup failed:' + p));
|
||||
if (!p.result) return callback(new Error('backup has no result:' + p));
|
||||
|
||||
backups.getByStatePaged(backupdb.BACKUP_STATE_NORMAL, 1, 1, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
if (result.length !== 1) return callback(new Error('result is not of length 1'));
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -115,8 +73,6 @@ describe('backups', function () {
|
||||
});
|
||||
|
||||
describe('cleanup', function () {
|
||||
this.timeout(20000);
|
||||
|
||||
var BACKUP_0 = {
|
||||
id: 'backup-box-0',
|
||||
version: '1.0.0',
|
||||
@@ -305,7 +261,6 @@ describe('backups', function () {
|
||||
|
||||
after(function (done) {
|
||||
rimraf.sync(gBackupConfig.backupFolder);
|
||||
progress.clear(progress.BACKUP);
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -329,8 +284,6 @@ describe('backups', function () {
|
||||
});
|
||||
|
||||
it('can backup', function (done) {
|
||||
this.timeout(6000);
|
||||
|
||||
createBackup(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(fs.statSync(path.join(gBackupConfig.backupFolder, 'snapshot/box.tar.gz')).nlink).to.be(2); // hard linked to a rotated backup
|
||||
@@ -343,8 +296,6 @@ describe('backups', function () {
|
||||
});
|
||||
|
||||
it('can take another backup', function (done) {
|
||||
this.timeout(6000);
|
||||
|
||||
createBackup(function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(fs.statSync(path.join(gBackupConfig.backupFolder, 'snapshot/box.tar.gz')).nlink).to.be(2); // hard linked to a rotated backup
|
||||
|
||||
@@ -10,14 +10,17 @@ sudo -k || sudo --reset-timestamp
|
||||
|
||||
# checks if all scripts are sudo access
|
||||
scripts=("${SOURCE_DIR}/src/scripts/rmvolume.sh" \
|
||||
"${SOURCE_DIR}/src/scripts/rmaddon.sh" \
|
||||
"${SOURCE_DIR}/src/scripts/rmaddondir.sh" \
|
||||
"${SOURCE_DIR}/src/scripts/reloadnginx.sh" \
|
||||
"${SOURCE_DIR}/src/scripts/reboot.sh" \
|
||||
"${SOURCE_DIR}/src/scripts/restart.sh" \
|
||||
"${SOURCE_DIR}/src/scripts/restartdocker.sh" \
|
||||
"${SOURCE_DIR}/src/scripts/restartunbound.sh" \
|
||||
"${SOURCE_DIR}/src/scripts/update.sh" \
|
||||
"${SOURCE_DIR}/src/scripts/collectlogs.sh" \
|
||||
"${SOURCE_DIR}/src/scripts/configurecollectd.sh" \
|
||||
"${SOURCE_DIR}/src/scripts/authorized_keys.sh" \
|
||||
"${SOURCE_DIR}/src/backuptask.js" \
|
||||
"${SOURCE_DIR}/src/scripts/backupupload.js" \
|
||||
"${SOURCE_DIR}/src/scripts/configurelogrotate.sh")
|
||||
|
||||
for script in "${scripts[@]}"; do
|
||||
|
||||
@@ -21,6 +21,7 @@ var appdb = require('../appdb.js'),
|
||||
mailboxdb = require('../mailboxdb.js'),
|
||||
maildb = require('../maildb.js'),
|
||||
settingsdb = require('../settingsdb.js'),
|
||||
taskdb = require('../taskdb.js'),
|
||||
tokendb = require('../tokendb.js'),
|
||||
userdb = require('../userdb.js'),
|
||||
_ = require('underscore');
|
||||
@@ -98,8 +99,6 @@ const TEST_DOMAIN = {
|
||||
};
|
||||
|
||||
describe('database', function () {
|
||||
this.timeout(5000);
|
||||
|
||||
before(function (done) {
|
||||
config._reset();
|
||||
config.setFqdn(TEST_DOMAIN.domain);
|
||||
@@ -234,7 +233,9 @@ describe('database', function () {
|
||||
robotsTxt: null,
|
||||
enableBackup: true,
|
||||
ownerId: USER_0.id,
|
||||
env: {}
|
||||
env: {},
|
||||
mailboxName: 'talktome',
|
||||
enableAutomaticUpdate: true
|
||||
};
|
||||
|
||||
it('cannot delete referenced domain', function (done) {
|
||||
@@ -755,7 +756,9 @@ describe('database', function () {
|
||||
alternateDomains: [],
|
||||
env: {
|
||||
'CUSTOM_KEY': 'CUSTOM_VALUE'
|
||||
}
|
||||
},
|
||||
mailboxName: 'talktome',
|
||||
enableAutomaticUpdate: true
|
||||
};
|
||||
|
||||
var APP_1 = {
|
||||
@@ -783,7 +786,9 @@ describe('database', function () {
|
||||
enableBackup: true,
|
||||
ownerId: USER_0.id,
|
||||
alternateDomains: [],
|
||||
env: {}
|
||||
env: {},
|
||||
mailboxName: 'callme',
|
||||
enableAutomaticUpdate: true
|
||||
};
|
||||
|
||||
before(function (done) {
|
||||
@@ -871,6 +876,7 @@ describe('database', function () {
|
||||
var data = {
|
||||
installationState: APP_0.installationState,
|
||||
location: APP_0.location,
|
||||
domain: APP_0.domain,
|
||||
manifest: APP_0.manifest,
|
||||
accessRestriction: APP_0.accessRestriction,
|
||||
httpPort: APP_0.httpPort,
|
||||
@@ -1094,6 +1100,82 @@ describe('database', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('tasks', function () {
|
||||
let taskId;
|
||||
|
||||
let TASK = {
|
||||
type: 'tasktype',
|
||||
args: { x: 1 },
|
||||
percent: 0,
|
||||
message: 'starting task'
|
||||
};
|
||||
|
||||
it('add succeeds', function (done) {
|
||||
taskdb.add(TASK, function (error, id) {
|
||||
expect(error).to.be(null);
|
||||
expect(id).to.be.ok();
|
||||
taskId = id;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get succeeds', function (done) {
|
||||
taskdb.get(taskId, function (error, task) {
|
||||
expect(error).to.be(null);
|
||||
expect(_.pick(task, Object.keys(TASK))).to.eql(TASK);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('update succeeds', function (done) {
|
||||
TASK.percent = 34;
|
||||
TASK.message = 'almost ther';
|
||||
taskdb.update(taskId, { percent: TASK.percent, message: TASK.message }, function (error) {
|
||||
expect(error).to.be(null);
|
||||
taskdb.get(taskId, function (error, task) {
|
||||
expect(_.pick(task, Object.keys(TASK))).to.eql(TASK);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('list succeeds - does not exist', function (done) {
|
||||
taskdb.listByTypePaged('randomtask', 1, 1, function (error, tasks) {
|
||||
expect(error).to.be(null);
|
||||
expect(tasks.length).to.be(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('list succeeds - by type', function (done) {
|
||||
taskdb.listByTypePaged(TASK.type, 1, 1, function (error, tasks) {
|
||||
expect(error).to.be(null);
|
||||
expect(tasks.length).to.be(1);
|
||||
expect(_.pick(tasks[0], Object.keys(TASK))).to.eql(TASK);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('list succeeds - all', function (done) {
|
||||
taskdb.listByTypePaged(null, 1, 1, function (error, tasks) {
|
||||
expect(error).to.be(null);
|
||||
expect(tasks.length).to.be(1);
|
||||
expect(_.pick(tasks[0], Object.keys(TASK))).to.eql(TASK);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('del succeeds', function (done) {
|
||||
taskdb.del(taskId, function (error) {
|
||||
expect(error).to.be(null);
|
||||
taskdb.get(taskId, function (error) {
|
||||
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('client', function () {
|
||||
var CLIENT_0 = {
|
||||
id: 'cid-0',
|
||||
@@ -1634,21 +1716,21 @@ describe('database', function () {
|
||||
});
|
||||
|
||||
it('add user mailbox succeeds', function (done) {
|
||||
mailboxdb.addMailbox('girish', DOMAIN_0.domain, 'uid-0', mailboxdb.OWNER_TYPE_USER, function (error) {
|
||||
mailboxdb.addMailbox('girish', DOMAIN_0.domain, 'uid-0', function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot add dup entry', function (done) {
|
||||
mailboxdb.addMailbox('girish', DOMAIN_0.domain, 'uid-1', mailboxdb.OWNER_TYPE_APP, function (error) {
|
||||
mailboxdb.addMailbox('girish', DOMAIN_0.domain, 'uid-1', function (error) {
|
||||
expect(error.reason).to.be(DatabaseError.ALREADY_EXISTS);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('add app mailbox succeeds', function (done) {
|
||||
mailboxdb.addMailbox('support', DOMAIN_0.domain, 'osticket', mailboxdb.OWNER_TYPE_APP, function (error) {
|
||||
mailboxdb.addMailbox('support', DOMAIN_0.domain, 'osticket', function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
@@ -1671,7 +1753,6 @@ describe('database', function () {
|
||||
expect(error).to.be(null);
|
||||
expect(mailboxes.length).to.be(2);
|
||||
expect(mailboxes[0].name).to.be('girish');
|
||||
expect(mailboxes[0].ownerType).to.be(mailboxdb.OWNER_TYPE_USER);
|
||||
expect(mailboxes[1].name).to.be('support');
|
||||
|
||||
done();
|
||||
@@ -1817,7 +1898,7 @@ describe('database', function () {
|
||||
});
|
||||
|
||||
it('can get all domains', function (done) {
|
||||
maildb.getAll(function (error, result) {
|
||||
maildb.list(function (error, result) {
|
||||
expect(error).to.equal(null);
|
||||
expect(result).to.be.an(Array);
|
||||
expect(result[0]).to.be.an('object');
|
||||
|
||||
@@ -42,7 +42,6 @@ describe('Dockerproxy', function () {
|
||||
});
|
||||
|
||||
// uncomment this to run the proxy for manual testing
|
||||
// this.timeout(1000000);
|
||||
// it('wait', function (done) {} );
|
||||
|
||||
it('can get info', function (done) {
|
||||
|
||||
@@ -79,7 +79,8 @@ var APP_0 = {
|
||||
restoreConfig: null,
|
||||
oldConfig: null,
|
||||
memoryLimit: 4294967296,
|
||||
ownerId: null
|
||||
ownerId: null,
|
||||
mailboxName: 'some-location-0.app'
|
||||
};
|
||||
|
||||
var dockerProxy;
|
||||
@@ -112,7 +113,7 @@ function setup(done) {
|
||||
appdb.update.bind(null, APP_0.id, { containerId: APP_0.containerId }),
|
||||
appdb.setAddonConfig.bind(null, APP_0.id, 'sendmail', [{ name: 'MAIL_SMTP_PASSWORD', value : 'sendmailpassword' }]),
|
||||
appdb.setAddonConfig.bind(null, APP_0.id, 'recvmail', [{ name: 'MAIL_IMAP_PASSWORD', value : 'recvmailpassword' }]),
|
||||
mailboxdb.addMailbox.bind(null, APP_0.location + '.app', APP_0.domain, APP_0.id, mailboxdb.OWNER_TYPE_APP),
|
||||
mailboxdb.addMailbox.bind(null, APP_0.location + '.app', APP_0.domain, APP_0.id),
|
||||
|
||||
function (callback) {
|
||||
users.create(USER_1.username, USER_1.password, USER_1.email, USER_0.displayName, { invitor: USER_0 }, AUDIT_SOURCE, function (error, result) {
|
||||
@@ -202,8 +203,6 @@ function cleanup(done) {
|
||||
}
|
||||
|
||||
describe('Ldap', function () {
|
||||
this.timeout(10000);
|
||||
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
@@ -814,7 +813,7 @@ describe('Ldap', function () {
|
||||
|
||||
describe('search mailbox', function () {
|
||||
before(function (done) {
|
||||
mailboxdb.addMailbox(USER_0.username.toLowerCase(), DOMAIN_0.domain, USER_0.id, mailboxdb.OWNER_TYPE_USER, done);
|
||||
mailboxdb.addMailbox(USER_0.username.toLowerCase(), DOMAIN_0.domain, USER_0.id, done);
|
||||
});
|
||||
|
||||
it('get specific mailbox by email', function (done) {
|
||||
@@ -925,7 +924,7 @@ describe('Ldap', function () {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
client.bind('cn=' + USER_0.username + '@example.com,ou=sendmail,dc=cloudron', USER_0.password + 'nope', function (error) {
|
||||
expect(error).to.be.a(ldap.NoSuchObjectError);
|
||||
expect(error).to.be.a(ldap.InvalidCredentialsError);
|
||||
client.unbind(done);
|
||||
});
|
||||
});
|
||||
@@ -993,7 +992,7 @@ describe('Ldap', function () {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
client.bind('cn=' + APP_0.location + '.app@example.com,ou=sendmail,dc=cloudron', 'nope', function (error) {
|
||||
expect(error).to.be.a(ldap.InvalidCredentialsError);
|
||||
expect(error).to.be.a(ldap.NoSuchObjectError);
|
||||
client.unbind(done);
|
||||
});
|
||||
});
|
||||
@@ -1084,7 +1083,7 @@ describe('Ldap', function () {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
client.bind('cn=' + APP_0.location + '.app@example.com,ou=recvmail,dc=cloudron', 'nope', function (error) {
|
||||
expect(error).to.be.a(ldap.InvalidCredentialsError);
|
||||
expect(error).to.be.a(ldap.NoSuchObjectError);
|
||||
client.unbind(done);
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user