appstore and support: async'ify

This commit is contained in:
Girish Ramakrishnan
2021-08-18 15:54:53 -07:00
parent 200018a022
commit 03e22170da
16 changed files with 458 additions and 633 deletions
+8 -4
View File
@@ -595,9 +595,9 @@ function downloadManifest(appStoreId, manifest, callback) {
superagent.get(url).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Network error downloading manifest:' + error.message));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.NOT_FOUND, util.format('Failed to get app info from store.', result.statusCode, result.text)));
if (result.status !== 200) return callback(new BoxError(BoxError.NOT_FOUND, util.format('Failed to get app info from store.', result.status, result.text)));
if (!result.body.manifest || typeof result.body.manifest !== 'object') return callback(new BoxError(BoxError.NOT_FOUND, util.format('Missing manifest. Failed to get app info from store.', result.statusCode, result.text)));
if (!result.body.manifest || typeof result.body.manifest !== 'object') return callback(new BoxError(BoxError.NOT_FOUND, util.format('Missing manifest. Failed to get app info from store.', result.status, result.text)));
callback(null, parts[0], result.body.manifest);
});
@@ -1611,7 +1611,9 @@ function purchaseApp(data, callback) {
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
appstore.purchaseApp(data, function (error) {
const purchaseApp = util.callbackify(appstore.purchaseApp);
purchaseApp(data, function (error) {
if (!error) return callback();
// if purchase failed, rollback the appdb record
@@ -1733,7 +1735,9 @@ function uninstall(app, auditSource, callback) {
let error = checkAppState(app, exports.ISTATE_PENDING_UNINSTALL);
if (error) return callback(error);
appstore.unpurchaseApp(appId, { appstoreId: app.appStoreId, manifestId: app.manifest.id || 'customapp' }, function (error) {
const unpurchaseApp = util.callbackify(appstore.unpurchaseApp);
unpurchaseApp(appId, { appstoreId: app.appStoreId, manifestId: app.manifest.id || 'customapp' }, function (error) {
if (error) return callback(error);
const task = {
+250 -314
View File
@@ -13,7 +13,7 @@ exports = module.exports = {
purchaseApp,
unpurchaseApp,
getUserToken,
createUserToken,
getSubscription,
isFreePlan,
@@ -23,9 +23,8 @@ exports = module.exports = {
createTicket
};
var apps = require('./apps.js'),
const apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
debug = require('debug')('box:appstore'),
@@ -74,99 +73,89 @@ function isAppAllowed(appstoreId, listingConfig) {
return true;
}
function getCloudronToken(callback) {
assert.strictEqual(typeof callback, 'function');
settings.getCloudronToken(function (error, token) {
if (error) return callback(error);
if (!token) return callback(new BoxError(BoxError.LICENSE_ERROR, 'Missing token'));
callback(null, token);
});
async function getCloudronToken() {
const token = await settings.getCloudronToken();
if (!token) throw new BoxError(BoxError.LICENSE_ERROR, 'Missing token');
return token;
}
function login(email, password, totpToken, callback) {
async function login(email, password, totpToken) {
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof totpToken, 'string');
assert.strictEqual(typeof callback, 'function');
var data = {
email: email,
password: password,
totpToken: totpToken
};
const data = { email, password, totpToken };
const url = settings.apiServerOrigin() + '/api/v1/login';
superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `login status code: ${result.statusCode}`));
const [error, response] = await safe(superagent.post(url)
.send(data)
.timeout(30 * 1000)
.ok(() => true));
callback(null, result.body); // { userId, accessToken }
});
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message);
if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS);
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `login status code: ${response.status}`);
return response.body; // { userId, accessToken }
}
function registerUser(email, password, callback) {
async function registerUser(email, password) {
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof callback, 'function');
var data = {
email: email,
password: password,
};
const data = { email, password };
const url = settings.apiServerOrigin() + '/api/v1/register_user';
superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 409) return callback(new BoxError(BoxError.ALREADY_EXISTS, error.message));
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `register status code: ${result.statusCode}`));
const [error, response] = await safe(superagent.post(url)
.send(data)
.timeout(30 * 1000)
.ok(() => true));
callback(null);
});
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message);
if (response.status === 409) throw new BoxError(BoxError.ALREADY_EXISTS, error.message);
if (response.status !== 201) throw new BoxError(BoxError.EXTERNAL_ERROR, `register status code: ${response.status}`);
}
function getUserToken(callback) {
assert.strictEqual(typeof callback, 'function');
async function createUserToken() {
if (settings.isDemo()) throw new BoxError(BoxError.BAD_FIELD, 'Not allowed in demo mode');
if (settings.isDemo()) return callback(new BoxError(BoxError.BAD_FIELD, 'Not allowed in demo mode'));
const token = await getCloudronToken();
const url = `${settings.apiServerOrigin()}/api/v1/user_token`;
getCloudronToken(function (error, token) {
if (error) return callback(error);
const [error, response] = await safe(superagent.post(url)
.send({})
.query({ accessToken: token })
.timeout(30 * 1000)
.ok(() => true));
const url = `${settings.apiServerOrigin()}/api/v1/user_token`;
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message);
if (response.status !== 201) throw new BoxError(BoxError.EXTERNAL_ERROR, `getUserToken status code: ${response.status}`);
superagent.post(url).send({}).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `getUserToken status code: ${result.status}`));
callback(null, result.body.accessToken);
});
});
return response.body.accessToken;
}
function getSubscription(callback) {
assert.strictEqual(typeof callback, 'function');
async function getSubscription() {
const token = await getCloudronToken();
getCloudronToken(function (error, token) {
if (error) return callback(error);
const url = settings.apiServerOrigin() + '/api/v1/subscription';
const url = settings.apiServerOrigin() + '/api/v1/subscription';
superagent.get(url).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR));
if (result.statusCode === 502) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Stripe error: ${error.message}`));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Unknown error: ${error.message}`));
const [error, response] = await safe(superagent.get(url)
.query({ accessToken: token })
.timeout(30 * 1000)
.ok(() => true));
// update the features cache
gFeatures = result.body.features;
safe.fs.writeFileSync(paths.FEATURES_INFO_FILE, JSON.stringify(gFeatures), 'utf8');
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message);
if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS);
if (response.status === 422) throw new BoxError(BoxError.LICENSE_ERROR);
if (response.status === 502) throw new BoxError(BoxError.EXTERNAL_ERROR, `Stripe error: ${error.message}`);
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Unknown error: ${error.message}`);
callback(null, result.body);
});
});
// update the features cache
gFeatures = response.body.features;
safe.fs.writeFileSync(paths.FEATURES_INFO_FILE, JSON.stringify(gFeatures), 'utf8');
return response.body;
}
function isFreePlan(subscription) {
@@ -174,225 +163,203 @@ function isFreePlan(subscription) {
}
// See app.js install it will create a db record first but remove it again if appstore purchase fails
function purchaseApp(data, callback) {
async function purchaseApp(data) {
assert.strictEqual(typeof data, 'object'); // { appstoreId, manifestId, appId }
assert(data.appstoreId || data.manifestId);
assert.strictEqual(typeof data.appId, 'string');
assert.strictEqual(typeof callback, 'function');
getCloudronToken(function (error, token) {
if (error) return callback(error);
const token = await getCloudronToken();
const url = `${settings.apiServerOrigin()}/api/v1/cloudronapps`;
const url = `${settings.apiServerOrigin()}/api/v1/cloudronapps`;
const [error, response] = await safe(superagent.post(url)
.send(data)
.query({ accessToken: token })
.timeout(30 * 1000)
.ok(() => true));
superagent.post(url).send(data).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND)); // appstoreId does not exist
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 402) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
// 200 if already purchased, 201 is newly purchased
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
callback(null);
});
});
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message);
if (response.status === 404) throw new BoxError(BoxError.NOT_FOUND); // appstoreId does not exist
if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS);
if (response.status === 402) throw new BoxError(BoxError.LICENSE_ERROR, response.body.message);
if (response.status === 422) throw new BoxError(BoxError.LICENSE_ERROR, response.body.message);
// 200 if already purchased, 201 is newly purchased
if (response.status !== 201 && response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', response.status, response.body));
}
function unpurchaseApp(appId, data, callback) {
async function unpurchaseApp(appId, data) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof data, 'object'); // { appstoreId, manifestId }
assert(data.appstoreId || data.manifestId);
assert.strictEqual(typeof callback, 'function');
getCloudronToken(function (error, token) {
if (error) return callback(error);
const token = await getCloudronToken();
const url = `${settings.apiServerOrigin()}/api/v1/cloudronapps/${appId}`;
const url = `${settings.apiServerOrigin()}/api/v1/cloudronapps/${appId}`;
let [error, response] = await safe(superagent.get(url)
.query({ accessToken: token })
.timeout(30 * 1000)
.ok(() => true));
superagent.get(url).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 404) return callback(null); // was never purchased
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message);
if (response.status === 404) return; // was never purchased
if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS);
if (response.status === 422) throw new BoxError(BoxError.LICENSE_ERROR, response.body.message);
if (response.status !== 201 && response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', response.status, response.body));
superagent.del(url).send(data).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error));
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode !== 204) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
[error, response] = await safe(superagent.del(url)
.send(data)
.query({ accessToken: token })
.timeout(30 * 1000)
.ok(() => true));
callback(null);
});
});
});
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message);
if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS);
if (response.status !== 204) throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', response.status, response.body));
}
function getBoxUpdate(options, callback) {
async function getBoxUpdate(options) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
getCloudronToken(function (error, token) {
if (error) return callback(error);
const token = await getCloudronToken();
const url = `${settings.apiServerOrigin()}/api/v1/boxupdate`;
const url = `${settings.apiServerOrigin()}/api/v1/boxupdate`;
const query = {
accessToken: token,
boxVersion: constants.VERSION,
automatic: options.automatic
};
const query = {
accessToken: token,
boxVersion: constants.VERSION,
automatic: options.automatic
};
const [error, response] = await safe(superagent.get(url)
.query(query)
.timeout(30 * 1000)
.ok(() => true));
superagent.get(url).query(query).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
if (result.statusCode === 204) return callback(null, null); // no update
if (result.statusCode !== 200 || !result.body) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message);
if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS);
if (response.status === 422) throw new BoxError(BoxError.LICENSE_ERROR, response.body.message);
if (response.status === 204) return; // no update
if (response.status !== 200 || !response.body) throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', response.status, response.text));
var updateInfo = result.body;
const updateInfo = response.body;
if (!semver.valid(updateInfo.version) || semver.gt(constants.VERSION, updateInfo.version)) {
return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Update version invalid or is a downgrade: %s %s', result.statusCode, result.text)));
}
if (!semver.valid(updateInfo.version) || semver.gt(constants.VERSION, updateInfo.version)) {
throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Update version invalid or is a downgrade: %s %s', response.status, response.text));
}
// updateInfo: { version, changelog, sourceTarballUrl, sourceTarballSigUrl, boxVersionsUrl, boxVersionsSigUrl }
if (!updateInfo.version || typeof updateInfo.version !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad version): %s %s', result.statusCode, result.text)));
if (!updateInfo.changelog || !Array.isArray(updateInfo.changelog)) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad version): %s %s', result.statusCode, result.text)));
if (!updateInfo.sourceTarballUrl || typeof updateInfo.sourceTarballUrl !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad sourceTarballUrl): %s %s', result.statusCode, result.text)));
if (!updateInfo.sourceTarballSigUrl || typeof updateInfo.sourceTarballSigUrl !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad sourceTarballSigUrl): %s %s', result.statusCode, result.text)));
if (!updateInfo.boxVersionsUrl || typeof updateInfo.boxVersionsUrl !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad boxVersionsUrl): %s %s', result.statusCode, result.text)));
if (!updateInfo.boxVersionsSigUrl || typeof updateInfo.boxVersionsSigUrl !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad boxVersionsSigUrl): %s %s', result.statusCode, result.text)));
// updateInfo: { version, changelog, sourceTarballUrl, sourceTarballSigUrl, boxVersionsUrl, boxVersionsSigUrl }
if (!updateInfo.version || typeof updateInfo.version !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad version): %s %s', response.status, response.text));
if (!updateInfo.changelog || !Array.isArray(updateInfo.changelog)) throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad version): %s %s', response.status, response.text));
if (!updateInfo.sourceTarballUrl || typeof updateInfo.sourceTarballUrl !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad sourceTarballUrl): %s %s', response.status, response.text));
if (!updateInfo.sourceTarballSigUrl || typeof updateInfo.sourceTarballSigUrl !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad sourceTarballSigUrl): %s %s', response.status, response.text));
if (!updateInfo.boxVersionsUrl || typeof updateInfo.boxVersionsUrl !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad boxVersionsUrl): %s %s', response.status, response.text));
if (!updateInfo.boxVersionsSigUrl || typeof updateInfo.boxVersionsSigUrl !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad boxVersionsSigUrl): %s %s', response.status, response.text));
callback(null, updateInfo);
});
});
return updateInfo;
}
function getAppUpdate(app, options, callback) {
async function getAppUpdate(app, options) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
getCloudronToken(function (error, token) {
if (error) return callback(error);
const token = await getCloudronToken();
const url = `${settings.apiServerOrigin()}/api/v1/appupdate`;
const query = {
accessToken: token,
boxVersion: constants.VERSION,
appId: app.appStoreId,
appVersion: app.manifest.version,
automatic: options.automatic
};
const url = `${settings.apiServerOrigin()}/api/v1/appupdate`;
const query = {
accessToken: token,
boxVersion: constants.VERSION,
appId: app.appStoreId,
appVersion: app.manifest.version,
automatic: options.automatic
};
const [error, response] = await safe(superagent.get(url)
.query(query)
.timeout(30 * 1000)
.ok(() => true));
superagent.get(url).query(query).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error));
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
if (result.statusCode === 204) return callback(null); // no update
if (result.statusCode !== 200 || !result.body) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS);
if (response.status === 422) throw new BoxError(BoxError.LICENSE_ERROR, response.body.message);
if (response.status === 204) return; // no update
if (response.status !== 200 || !response.body) throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', response.status, response.text));
const updateInfo = result.body;
const updateInfo = response.body;
// for the appstore, x.y.z is the same as x.y.z-0 but in semver, x.y.z > x.y.z-0
const curAppVersion = semver.prerelease(app.manifest.version) ? app.manifest.version : `${app.manifest.version}-0`;
// for the appstore, x.y.z is the same as x.y.z-0 but in semver, x.y.z > x.y.z-0
const curAppVersion = semver.prerelease(app.manifest.version) ? app.manifest.version : `${app.manifest.version}-0`;
// do some sanity checks
if (!safe.query(updateInfo, 'manifest.version') || semver.gt(curAppVersion, safe.query(updateInfo, 'manifest.version'))) {
debug('Skipping malformed update of app %s version: %s. got %j', app.id, curAppVersion, updateInfo);
return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Malformed update: %s %s', result.statusCode, result.text)));
}
// do some sanity checks
if (!safe.query(updateInfo, 'manifest.version') || semver.gt(curAppVersion, safe.query(updateInfo, 'manifest.version'))) {
debug('Skipping malformed update of app %s version: %s. got %j', app.id, curAppVersion, updateInfo);
throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Malformed update: %s %s', response.status, response.text));
}
updateInfo.unstable = !!updateInfo.unstable;
updateInfo.unstable = !!updateInfo.unstable;
// { id, creationDate, manifest, unstable }
callback(null, updateInfo);
});
});
// { id, creationDate, manifest, unstable }
return updateInfo;
}
function registerCloudron(data, callback) {
async function registerCloudron(data) {
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
const url = `${settings.apiServerOrigin()}/api/v1/register_cloudron`;
superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Unable to register cloudron: ${result.statusCode} ${error.message}`));
const [error, response] = await safe(superagent.post(url)
.send(data)
.timeout(30 * 1000)
.ok(() => true));
// cloudronId, token, licenseKey
if (!result.body.cloudronId) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no cloudron id'));
if (!result.body.cloudronToken) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no token'));
if (!result.body.licenseKey) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no license'));
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message);
if (response.status !== 201) throw new BoxError(BoxError.EXTERNAL_ERROR, `Unable to register cloudron: ${response.statusCode} ${error.message}`);
async.series([
settings.setCloudronId.bind(null, result.body.cloudronId),
settings.setCloudronToken.bind(null, result.body.cloudronToken),
settings.setLicenseKey.bind(null, result.body.licenseKey),
], function (error) {
if (error) return callback(error);
// cloudronId, token, licenseKey
if (!response.body.cloudronId) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no cloudron id');
if (!response.body.cloudronToken) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no token');
if (!response.body.licenseKey) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no license');
debug(`registerCloudron: Cloudron registered with id ${result.body.cloudronId}`);
await settings.setCloudronId(response.body.cloudronId);
await settings.setCloudronToken(response.body.cloudronToken);
await settings.setLicenseKey(response.body.licenseKey);
callback();
});
});
debug(`registerCloudron: Cloudron registered with id ${response.body.cloudronId}`);
}
function updateCloudron(data, callback) {
async function updateCloudron(data) {
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
getCloudronToken(function (error, token) {
if (error && error.reason === BoxError.LICENSE_ERROR) return callback(null); // missing token. not registered yet
if (error) return callback(error);
const token = await getCloudronToken();
const url = `${settings.apiServerOrigin()}/api/v1/update_cloudron`;
const query = {
accessToken: token
};
const url = `${settings.apiServerOrigin()}/api/v1/update_cloudron`;
const query = {
accessToken: token
};
const [error, response] = await safe(superagent.post(url)
.query(query)
.send(data)
.timeout(30 * 1000)
.ok(() => true));
superagent.post(url).query(query).send(data).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error));
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS);
if (response.status === 422) throw new BoxError(BoxError.LICENSE_ERROR, response.body.message);
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', response.status, response.text));
debug(`updateCloudron: Cloudron updated with data ${JSON.stringify(data)}`);
callback();
});
});
debug(`updateCloudron: Cloudron updated with data ${JSON.stringify(data)}`);
}
function registerWithLoginCredentials(options, callback) {
async function registerWithLoginCredentials(options) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
function maybeSignup(done) {
if (!options.signup) return done();
const token = await getCloudronToken();
if (token) throw new BoxError(BoxError.CONFLICT, 'Cloudron is already registered');
registerUser(options.email, options.password, done);
}
getCloudronToken(function (error, token) {
if (token) return callback(new BoxError(BoxError.CONFLICT, 'Cloudron is already registered'));
maybeSignup(function (error) {
if (error) return callback(error);
login(options.email, options.password, options.totpToken || '', function (error, result) {
if (error) return callback(error);
registerCloudron({ domain: settings.dashboardDomain(), accessToken: result.accessToken, version: constants.VERSION }, callback);
});
});
});
if (!options.signup) return;
await registerUser(options.email, options.password);
const result = await login(options.email, options.password, options.totpToken || '');
await registerCloudron({ domain: settings.dashboardDomain(), accessToken: result.accessToken, version: constants.VERSION });
}
function createTicket(info, auditSource, callback) {
async function createTicket(info, auditSource) {
assert.strictEqual(typeof info, 'object');
assert.strictEqual(typeof info.email, 'string');
assert.strictEqual(typeof info.displayName, 'string');
@@ -400,127 +367,96 @@ function createTicket(info, auditSource, callback) {
assert.strictEqual(typeof info.subject, 'string');
assert.strictEqual(typeof info.description, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
function collectAppInfoIfNeeded(callback) {
if (!info.appId) return callback();
apps.get(info.appId, callback);
const token = await getCloudronToken();
if (info.enableSshSupport) {
await safe(support.enableRemoteSupport(true, auditSource));
}
function enableSshIfNeeded(callback) {
if (!info.enableSshSupport) return callback();
info.app = info.appId ? await util.promisify(apps.get)(info.appId) : null;
info.supportEmail = constants.SUPPORT_EMAIL; // destination address for tickets
support.enableRemoteSupport(true, auditSource, function (error) {
// ensure we can at least get the ticket through
if (error) debug('Unable to enable SSH support.', error);
const request = superagent.post(`${settings.apiServerOrigin()}/api/v1/ticket`)
.query({ accessToken: token })
.timeout(30 * 1000)
.ok(() => true);
callback();
// either send as JSON through body or as multipart, depending on attachments
if (info.app) {
request.field('infoJSON', JSON.stringify(info));
apps.getLocalLogfilePaths(info.app).forEach(function (filePath) {
const logs = safe.child_process.execSync(`tail --lines=1000 ${filePath}`);
if (logs) request.attach(path.basename(filePath), logs, path.basename(filePath));
});
} else {
request.send(info);
}
getCloudronToken(function (error, token) {
if (error) return callback(error);
const [error, response] = await safe(request);
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message);
if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS);
if (response.status === 422) throw new BoxError(BoxError.LICENSE_ERROR, response.body.message);
if (response.status !== 201) throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', response.status, response.text));
enableSshIfNeeded(function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_SUPPORT_TICKET, auditSource, info);
collectAppInfoIfNeeded(function (error, app) {
if (error) return callback(error);
if (app) info.app = app;
info.supportEmail = constants.SUPPORT_EMAIL; // destination address for tickets
var req = superagent.post(`${settings.apiServerOrigin()}/api/v1/ticket`)
.query({ accessToken: token })
.timeout(30 * 1000);
// either send as JSON through body or as multipart, depending on attachments
if (info.app) {
req.field('infoJSON', JSON.stringify(info));
apps.getLocalLogfilePaths(info.app).forEach(function (filePath) {
var logs = safe.child_process.execSync(`tail --lines=1000 ${filePath}`);
if (logs) req.attach(path.basename(filePath), logs, path.basename(filePath));
});
} else {
req.send(info);
}
req.end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
eventlog.add(eventlog.ACTION_SUPPORT_TICKET, auditSource, info);
callback(null, { message: `An email was sent to ${constants.SUPPORT_EMAIL}. We will get back shortly!` });
});
});
});
});
return { message: `An email was sent to ${constants.SUPPORT_EMAIL}. We will get back shortly!` };
}
function getApps(callback) {
assert.strictEqual(typeof callback, 'function');
async function getApps() {
const token = await getCloudronToken();
getCloudronToken(async function (error, token) {
if (error) return callback(error);
const unstable = await settings.getUnstableAppsConfig();
const [settingsError, unstable] = await settings.getUnstableAppsConfig();
if (settingsError) return callback(settingsError);
const url = `${settings.apiServerOrigin()}/api/v1/apps`;
const url = `${settings.apiServerOrigin()}/api/v1/apps`;
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION, unstable: unstable }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App listing failed. %s %j', result.status, result.body)));
if (!result.body.apps) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
const [error, response] = await safe(superagent.get(url)
.query({ accessToken: token, boxVersion: constants.VERSION, unstable: unstable })
.timeout(30 * 1000)
.ok(() => true));
settings.getAppstoreListingConfig(function (error, listingConfig) {
if (error) return callback(error);
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message);
if (response.status === 403 || response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS);
if (response.status === 422) throw new BoxError(BoxError.LICENSE_ERROR, response.body.message);
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('App listing failed. %s %j', response.status, response.body));
if (!response.body.apps) throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', response.status, response.text));
const filteredApps = result.body.apps.filter(app => isAppAllowed(app.id, listingConfig));
callback(null, filteredApps);
});
});
});
const listingConfig = await settings.getAppstoreListingConfig();
const filteredApps = response.body.apps.filter(app => isAppAllowed(app.id, listingConfig));
return filteredApps;
}
function getAppVersion(appId, version, callback) {
async function getAppVersion(appId, version) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof version, 'string');
assert.strictEqual(typeof callback, 'function');
settings.getAppstoreListingConfig(function (error, listingConfig) {
if (error) return callback(error);
const listingConfig = await settings.getAppstoreListingConfig();
if (!isAppAllowed(appId, listingConfig)) return callback(new BoxError(BoxError.FEATURE_DISABLED));
if (!isAppAllowed(appId, listingConfig)) throw new BoxError(BoxError.FEATURE_DISABLED);
getCloudronToken(function (error, token) {
if (error) return callback(error);
const token = await getCloudronToken();
let url = `${settings.apiServerOrigin()}/api/v1/apps/${appId}`;
if (version !== 'latest') url += `/versions/${version}`;
let url = `${settings.apiServerOrigin()}/api/v1/apps/${appId}`;
if (version !== 'latest') url += `/versions/${version}`;
superagent.get(url).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App fetch failed. %s %j', result.status, result.body)));
const [error, response] = await safe(superagent.get(url)
.query({ accessToken: token })
.timeout(30 * 1000)
.ok(() => true));
callback(null, result.body);
});
});
});
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message);
if (response.status === 403 || response.statusCode === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS);
if (response.status === 404) throw new BoxError(BoxError.NOT_FOUND);
if (response.status === 422) throw new BoxError(BoxError.LICENSE_ERROR, response.body.message);
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('App fetch failed. %s %j', response.status, response.body));
return response.body;
}
function getApp(appId, callback) {
async function getApp(appId) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
getAppVersion(appId, 'latest', callback);
return await getAppVersion(appId, 'latest');
}
+2 -2
View File
@@ -314,10 +314,10 @@ function setDashboardDomain(domain, auditSource, callback) {
const fqdn = dns.fqdn(constants.DASHBOARD_LOCATION, domainObject);
settings.setDashboardLocation(domain, fqdn, function (error) {
settings.setDashboardLocation(domain, fqdn, async function (error) {
if (error) return callback(error);
appstore.updateCloudron({ domain }, NOOP_CALLBACK);
await safe(appstore.updateCloudron({ domain }));
eventlog.add(eventlog.ACTION_DASHBOARD_DOMAIN_UPDATE, auditSource, { domain, fqdn });
+1 -1
View File
@@ -81,7 +81,7 @@ async function startJobs() {
// this is run separately from the update itself so that the user can disable automatic updates but can still get a notification
gJobs.updateCheckerJob = new CronJob({
cronTime: `${randomTick} ${randomTick} 1,5,9,13,17,21,23 * * *`,
onTick: () => updateChecker.checkForUpdates({ automatic: true }, NOOP_CALLBACK),
onTick: async () => await updateChecker.checkForUpdates({ automatic: true }),
start: true
});
+6 -8
View File
@@ -35,17 +35,15 @@ const MAIL_TEMPLATES_DIR = path.join(__dirname, 'mail_templates');
function getMailConfig(callback) {
assert.strictEqual(typeof callback, 'function');
settings.getCloudronName(function (error, cloudronName) {
settings.getCloudronName(async function (error, cloudronName) {
if (error) debug('Error getting cloudron name: ', error);
settings.getSupportConfig(function (error, supportConfig) {
if (error) debug('Error getting support config: ', error);
const supportConfig = await settings.getSupportConfig();
callback(null, {
cloudronName: cloudronName || '',
notificationFrom: `"${cloudronName}" <no-reply@${settings.dashboardDomain()}>`,
supportEmail: supportConfig.email
});
callback(null, {
cloudronName: cloudronName || '',
notificationFrom: `"${cloudronName}" <no-reply@${settings.dashboardDomain()}>`,
supportEmail: supportConfig.email
});
});
}
+27 -32
View File
@@ -10,50 +10,47 @@ exports = module.exports = {
getSubscription
};
var appstore = require('../appstore.js'),
const appstore = require('../appstore.js'),
assert = require('assert'),
BoxError = require('../boxerror.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
HttpSuccess = require('connect-lastmile').HttpSuccess,
safe = require('safetydance');
function getApps(req, res, next) {
appstore.getApps(function (error, apps) {
if (error) return next(BoxError.toHttpError(error));
async function getApps(req, res, next) {
const [error, apps] = await safe(appstore.getApps());
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { apps }));
});
next(new HttpSuccess(200, { apps }));
}
function getApp(req, res, next) {
async function getApp(req, res, next) {
assert.strictEqual(typeof req.params.appstoreId, 'string');
appstore.getApp(req.params.appstoreId, function (error, app) {
if (error) return next(BoxError.toHttpError(error));
const [error, app] = await safe(appstore.getApp(req.params.appstoreId));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, app));
});
next(new HttpSuccess(200, app));
}
function getAppVersion(req, res, next) {
async function getAppVersion(req, res, next) {
assert.strictEqual(typeof req.params.appstoreId, 'string');
assert.strictEqual(typeof req.params.versionId, 'string');
appstore.getAppVersion(req.params.appstoreId, req.params.versionId, function (error, manifest) {
if (error) return next(BoxError.toHttpError(error));
const [error, manifest] = await safe(appstore.getAppVersion(req.params.appstoreId, req.params.versionId));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, manifest));
});
next(new HttpSuccess(200, manifest));
}
function createUserToken(req, res, next) {
appstore.getUserToken(function (error, result) {
if (error) return next(BoxError.toHttpError(error));
async function createUserToken(req, res, next) {
const [error, accessToken] = await safe(appstore.createUserToken());
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201, { accessToken: result }));
});
next(new HttpSuccess(201, { accessToken }));
}
function registerCloudron(req, res, next) {
async function registerCloudron(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.email !== 'string' || !req.body.email) return next(new HttpError(400, 'email must be string'));
@@ -61,19 +58,17 @@ function registerCloudron(req, res, next) {
if ('totpToken' in req.body && typeof req.body.totpToken !== 'string') return next(new HttpError(400, 'totpToken must be string'));
if (typeof req.body.signup !== 'boolean') return next(new HttpError(400, 'signup must be a boolean'));
appstore.registerWithLoginCredentials(req.body, function (error) {
if (error) return next(BoxError.toHttpError(error));
const [error] = await safe(appstore.registerWithLoginCredentials(req.body));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201, {}));
});
next(new HttpSuccess(201, {}));
}
function getSubscription(req, res, next) {
async function getSubscription(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
appstore.getSubscription(function (error, result) {
if (error) return next(BoxError.toHttpError(error));
const [error, result] = await safe(appstore.getSubscription());
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, result)); // { email, cloudronId, cloudronCreatedAt, plan, current_period_end, canceled_at, cancel_at, status, features }
});
next(new HttpSuccess(200, result)); // { email, cloudronId, cloudronCreatedAt, plan, current_period_end, canceled_at, cancel_at, status, features }
}
+8 -10
View File
@@ -55,7 +55,7 @@ function getCloudronName(req, res, next) {
});
}
function setAppstoreListingConfig(req, res, next) {
async function setAppstoreListingConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
const listingConfig = _.pick(req.body, 'whitelist', 'blacklist');
@@ -73,19 +73,17 @@ function setAppstoreListingConfig(req, res, next) {
if (!listingConfig.blacklist.every(id => typeof id === 'string')) return next(new HttpError(400, 'blacklist must be array of strings'));
}
settings.setAppstoreListingConfig(listingConfig, function (error) {
if (error) return next(BoxError.toHttpError(error));
const [error] = await safe(settings.setAppstoreListingConfig(listingConfig));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {}));
});
next(new HttpSuccess(202, {}));
}
function getAppstoreListingConfig(req, res, next) {
settings.getAppstoreListingConfig(function (error, listingConfig) {
if (error) return next(BoxError.toHttpError(error));
async function getAppstoreListingConfig(req, res, next) {
const [error, listingConfig] = await safe(settings.getAppstoreListingConfig());
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, listingConfig));
});
next(new HttpSuccess(200, listingConfig));
}
async function setCloudronAvatar(req, res, next) {
+3 -4
View File
@@ -196,13 +196,12 @@ function getUpdateInfo(req, res, next) {
next(new HttpSuccess(200, { update: updateChecker.getUpdateInfo() }));
}
function checkForUpdates(req, res, next) {
async function checkForUpdates(req, res, next) {
// it can take a while sometimes to get all the app updates one by one
req.clearTimeout();
updateChecker.checkForUpdates({ automatic: false }, function () {
next(new HttpSuccess(200, { update: updateChecker.getUpdateInfo() }));
});
await updateChecker.checkForUpdates({ automatic: false });
next(new HttpSuccess(200, { update: updateChecker.getUpdateInfo() }));
}
function getLogs(req, res, next) {
+1 -1
View File
@@ -26,7 +26,7 @@ const assert = require('assert'),
async function authorize(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
const [error, directoryConfig] = await settings.getDirectoryConfig();
const [error, directoryConfig] = await safe(settings.getDirectoryConfig());
if (error) return next(BoxError.toHttpError(error));
if (directoryConfig.lockUserProfiles) return next(new HttpError(403, 'admin has disallowed users from editing profiles'));
+4 -5
View File
@@ -58,12 +58,11 @@ function setTimeZone(req, res, next) {
});
}
function getSupportConfig(req, res, next) {
settings.getSupportConfig(function (error, supportConfig) {
if (error) return next(BoxError.toHttpError(error));
async function getSupportConfig(req, res, next) {
const [error, supportConfig] = await settings.getSupportConfig();
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, supportConfig));
});
next(new HttpSuccess(200, supportConfig));
}
function getBackupConfig(req, res, next) {
+27 -32
View File
@@ -10,27 +10,27 @@ exports = module.exports = {
canEnableRemoteSupport
};
var appstore = require('../appstore.js'),
const appstore = require('../appstore.js'),
assert = require('assert'),
auditSource = require('../auditsource.js'),
constants = require('../constants.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
safe = require('safetydance'),
settings = require('../settings.js'),
support = require('../support.js'),
_ = require('underscore');
function canCreateTicket(req, res, next) {
settings.getSupportConfig(function (error, supportConfig) {
if (error) return next(new HttpError(503, error.message));
async function canCreateTicket(req, res, next) {
const [error, supportConfig] = await safe(settings.getSupportConfig());
if (error) return next(new HttpError(503, error.message));
if (!supportConfig.submitTickets) return next(new HttpError(405, 'feature disabled by admin'));
if (!supportConfig.submitTickets) return next(new HttpError(405, 'feature disabled by admin'));
next();
});
next();
}
function createTicket(req, res, next) {
async function createTicket(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
const VALID_TYPES = [ 'feedback', 'ticket', 'app_missing', 'app_error', 'upgrade_request', 'email_error' ];
@@ -43,44 +43,39 @@ function createTicket(req, res, next) {
if (req.body.altEmail && typeof req.body.altEmail !== 'string') return next(new HttpError(400, 'altEmail must be string'));
if (req.body.enableSshSupport && typeof req.body.enableSshSupport !== 'boolean') return next(new HttpError(400, 'enableSshSupport must be a boolean'));
settings.getSupportConfig(function (error, supportConfig) {
if (error) return next(new HttpError(503, `Error getting support config: ${error.message}`));
if (supportConfig.email !== constants.SUPPORT_EMAIL) return next(new HttpError(503, 'Sending to non-cloudron email not implemented yet'));
const [error, supportConfig] = await safe(settings.getSupportConfig());
if (error) return next(new HttpError(503, `Error getting support config: ${error.message}`));
if (supportConfig.email !== constants.SUPPORT_EMAIL) return next(new HttpError(503, 'Sending to non-cloudron email not implemented yet'));
appstore.createTicket(_.extend({ }, req.body, { email: req.user.email, displayName: req.user.displayName }), auditSource.fromRequest(req), function (error, result) {
if (error) return next(new HttpError(503, `Error contacting cloudron.io: ${error.message}. Please email ${constants.SUPPORT_EMAIL}`));
const [ticketError, result] = await safe(appstore.createTicket(_.extend({ }, req.body, { email: req.user.email, displayName: req.user.displayName }), auditSource.fromRequest(req)));
if (ticketError) return next(new HttpError(503, `Error contacting cloudron.io: ${error.message}. Please email ${constants.SUPPORT_EMAIL}`));
next(new HttpSuccess(201, result));
});
});
next(new HttpSuccess(201, result));
}
function canEnableRemoteSupport(req, res, next) {
settings.getSupportConfig(function (error, supportConfig) {
if (error) return next(new HttpError(503, error.message));
async function canEnableRemoteSupport(req, res, next) {
const [error, supportConfig] = await safe(settings.getSupportConfig());
if (error) return next(new HttpError(503, error.message));
if (!supportConfig.remoteSupport) return next(new HttpError(405, 'feature disabled by admin'));
if (!supportConfig.remoteSupport) return next(new HttpError(405, 'feature disabled by admin'));
next();
});
next();
}
function enableRemoteSupport(req, res, next) {
async function enableRemoteSupport(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enabled is required'));
support.enableRemoteSupport(req.body.enable, auditSource.fromRequest(req), function (error) {
if (error) return next(new HttpError(503, 'Error enabling remote support. Try running "cloudron-support --enable-ssh" on the server'));
const [error] = await safe(support.enableRemoteSupport(req.body.enable, auditSource.fromRequest(req)));
if (error) return next(new HttpError(503, 'Error enabling remote support. Try running "cloudron-support --enable-ssh" on the server'));
next(new HttpSuccess(202, {}));
});
next(new HttpSuccess(202, {}));
}
function getRemoteSupport(req, res, next) {
support.getRemoteSupport(function (error, status) {
if (error) return next(new HttpError(500, error));
async function getRemoteSupport(req, res, next) {
const [error, enabled] = await safe(support.getRemoteSupport());
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, status));
});
next(new HttpSuccess(200, { enabled }));
}
+33 -80
View File
@@ -384,7 +384,7 @@ function setDynamicDnsConfig(enabled, callback) {
async function getUnstableAppsConfig() {
const result = await get(exports.UNSTABLE_APPS_KEY);
if (result === null) gDefaults[exports.UNSTABLE_APPS_KEY];
if (result === null) return gDefaults[exports.UNSTABLE_APPS_KEY];
return !!result; // db holds string values only
}
@@ -650,28 +650,18 @@ async function setDirectoryConfig(directoryConfig) {
notifyChange(exports.DIRECTORY_CONFIG_KEY, directoryConfig);
}
function getAppstoreListingConfig(callback) {
assert.strictEqual(typeof callback, 'function');
async function getAppstoreListingConfig() {
const value = await get(exports.APPSTORE_LISTING_CONFIG_KEY);
if (value === null) return gDefaults[exports.APPSTORE_LISTING_CONFIG_KEY];
settingsdb.get(exports.APPSTORE_LISTING_CONFIG_KEY, function (error, value) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.APPSTORE_LISTING_CONFIG_KEY]);
if (error) return callback(error);
callback(null, JSON.parse(value));
});
return JSON.parse(value);
}
function setAppstoreListingConfig(listingConfig, callback) {
async function setAppstoreListingConfig(listingConfig) {
assert.strictEqual(typeof listingConfig, 'object');
assert.strictEqual(typeof callback, 'function');
settingsdb.set(exports.APPSTORE_LISTING_CONFIG_KEY, JSON.stringify(listingConfig), function (error) {
if (error) return callback(error);
notifyChange(exports.APPSTORE_LISTING_CONFIG_KEY, listingConfig);
callback(null);
});
await set(exports.APPSTORE_LISTING_CONFIG_KEY, JSON.stringify(listingConfig));
notifyChange(exports.APPSTORE_LISTING_CONFIG_KEY, listingConfig);
}
async function getFirewallBlocklist() {
@@ -688,39 +678,24 @@ async function setFirewallBlocklist(blocklist) {
await setBlob(exports.FIREWALL_BLOCKLIST_KEY, Buffer.from(blocklist));
}
function getSupportConfig(callback) {
assert.strictEqual(typeof callback, 'function');
async function getSupportConfig() {
const value = await get(exports.SUPPORT_CONFIG_KEY);
if (value === null) return gDefaults[exports.SUPPORT_CONFIG_KEY];
settingsdb.get(exports.SUPPORT_CONFIG_KEY, function (error, value) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.SUPPORT_CONFIG_KEY]);
if (error) return callback(error);
callback(null, JSON.parse(value));
});
return JSON.parse(value);
}
function getLicenseKey(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.LICENSE_KEY, function (error, value) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.LICENSE_KEY]);
if (error) return callback(error);
callback(null, value);
});
async function getLicenseKey() {
const value = get(exports.LICENSE_KEY);
if (value === null) return gDefaults[exports.LICENSE_KEY];
return value;
}
function setLicenseKey(licenseKey, callback) {
async function setLicenseKey(licenseKey) {
assert.strictEqual(typeof licenseKey, 'string');
assert.strictEqual(typeof callback, 'function');
settingsdb.set(exports.LICENSE_KEY, licenseKey, function (error) {
if (error) return callback(error);
notifyChange(exports.LICENSE_KEY, licenseKey);
callback(null);
});
await set(exports.LICENSE_KEY, licenseKey);
notifyChange(exports.LICENSE_KEY, licenseKey);
}
function getLanguage(callback) {
@@ -753,52 +728,30 @@ function setLanguage(language, callback) {
});
}
function getCloudronId(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.CLOUDRON_ID_KEY, function (error, value) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.CLOUDRON_ID_KEY]);
if (error) return callback(error);
callback(null, value);
});
async function getCloudronId() {
const value = await get(exports.CLOUDRON_ID_KEY);
if (value === null) return gDefaults[exports.CLOUDRON_ID_KEY];
return value;
}
function setCloudronId(cid, callback) {
async function setCloudronId(cid) {
assert.strictEqual(typeof cid, 'string');
assert.strictEqual(typeof callback, 'function');
settingsdb.set(exports.CLOUDRON_ID_KEY, cid, function (error) {
if (error) return callback(error);
notifyChange(exports.CLOUDRON_ID_KEY, cid);
callback(null);
});
await set(exports.CLOUDRON_ID_KEY, cid);
notifyChange(exports.CLOUDRON_ID_KEY, cid);
}
function getCloudronToken(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.CLOUDRON_TOKEN_KEY, function (error, value) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.CLOUDRON_TOKEN_KEY]);
if (error) return callback(error);
callback(null, value);
});
async function getCloudronToken() {
const value = await get(exports.CLOUDRON_TOKEN_KEY);
if (value === null) return gDefaults[exports.CLOUDRON_TOKEN_KEY];
return value;
}
function setCloudronToken(token, callback) {
async function setCloudronToken(token) {
assert.strictEqual(typeof token, 'string');
assert.strictEqual(typeof callback, 'function');
settingsdb.set(exports.CLOUDRON_TOKEN_KEY, token, function (error) {
if (error) return callback(error);
notifyChange(exports.CLOUDRON_TOKEN_KEY, token);
callback(null);
});
await set(exports.CLOUDRON_TOKEN_KEY, token);
notifyChange(exports.CLOUDRON_TOKEN_KEY, token);
}
async function list() {
+3 -1
View File
@@ -53,6 +53,7 @@ function spawn(tag, file, args, options, callback) {
debug(tag + ' spawn: %s %s', file, args.join(' ').replace(/\n/g, '\\n'));
const cp = child_process.spawn(file, args, options);
let stdoutResult = '';
if (options.logStream) {
cp.stdout.pipe(options.logStream);
@@ -60,6 +61,7 @@ function spawn(tag, file, args, options, callback) {
} else {
cp.stdout.on('data', function (data) {
debug(tag + ' (stdout): %s', data.toString('utf8'));
stdoutResult += data.toString('utf8');
});
cp.stderr.on('data', function (data) {
@@ -69,7 +71,7 @@ function spawn(tag, file, args, options, callback) {
cp.on('exit', function (code, signal) {
if (code || signal) debug(tag + ' code: %s, signal: %s', code, signal);
if (code === 0) return callback(null);
if (code === 0) return callback(null, stdoutResult);
let e = new BoxError(BoxError.SPAWN_ERROR, `${tag} exited with code ${code} signal ${signal}`);
e.code = code;
+8 -21
View File
@@ -11,7 +11,6 @@ const assert = require('assert'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
eventlog = require('./eventlog.js'),
once = require('once'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
@@ -30,38 +29,26 @@ function sshInfo() {
filePath = '/home/ubuntu/.ssh/authorized_keys';
user = 'ubuntu';
} else {
filePath = '/root/.ssh/authorized_keys';
user = 'root';
}
return { filePath, user };
}
function getRemoteSupport(callback) {
assert.strictEqual(typeof callback, 'function');
async function getRemoteSupport() {
const [error, stdoutResult] = await safe(shell.promises.sudo('support', [ AUTHORIZED_KEYS_CMD, 'is-enabled', sshInfo().filePath ], {}));
if (error) throw new BoxError(BoxError.FS_ERROR, error);
callback = once(callback); // exit may or may not be called after an 'error'
let result = '';
let cp = shell.sudo('support', [ AUTHORIZED_KEYS_CMD, 'is-enabled', sshInfo().filePath ], {}, function (error) {
if (error) callback(new BoxError(BoxError.FS_ERROR, error));
callback(null, { enabled: result.trim() === 'true' });
});
cp.stdout.on('data', (data) => result = result + data.toString('utf8'));
return stdoutResult.trim() === 'true';
}
function enableRemoteSupport(enable, auditSource, callback) {
async function enableRemoteSupport(enable, auditSource) {
assert.strictEqual(typeof enable, 'boolean');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
const si = sshInfo();
shell.sudo('support', [ AUTHORIZED_KEYS_CMD, enable ? 'enable' : 'disable', si.filePath, si.user ], {}, function (error) {
if (error) callback(new BoxError(BoxError.FS_ERROR, error));
const [error] = await safe(shell.promises.sudo('support', [ AUTHORIZED_KEYS_CMD, enable ? 'enable' : 'disable', si.filePath, si.user ], {}));
if (error) throw new BoxError(BoxError.FS_ERROR, error);
eventlog.add(eventlog.ACTION_SUPPORT_SSH, auditSource, { enable });
callback();
});
await eventlog.add(eventlog.ACTION_SUPPORT_SSH, auditSource, { enable });
}
+35 -56
View File
@@ -30,54 +30,45 @@ describe('updatechecker', function () {
settings.setAutoupdatePattern(constants.AUTOUPDATE_PATTERN_NEVER, done);
});
it('no updates', function (done) {
it('no updates', async function () {
nock.cleanAll();
var scope = nock(mockApiServerOrigin)
const scope = nock(mockApiServerOrigin)
.get('/api/v1/boxupdate')
.query({ boxVersion: constants.VERSION, accessToken: appstoreToken, automatic: false })
.reply(204, { } );
updatechecker.checkForUpdates({ automatic: false }, function (error) {
expect(!error).to.be.ok();
expect(updatechecker.getUpdateInfo().box).to.not.be.ok();
expect(scope.isDone()).to.be.ok();
done();
});
await updatechecker.checkForUpdates({ automatic: false });
expect(updatechecker.getUpdateInfo().box).to.not.be.ok();
expect(scope.isDone()).to.be.ok();
});
it('new version', function (done) {
it('new version', async function () {
nock.cleanAll();
var scope = nock(mockApiServerOrigin)
const scope = nock(mockApiServerOrigin)
.get('/api/v1/boxupdate')
.query({ boxVersion: constants.VERSION, accessToken: appstoreToken, automatic: false })
.reply(200, { version: UPDATE_VERSION, changelog: [''], sourceTarballUrl: 'box.tar.gz', sourceTarballSigUrl: 'box.tar.gz.sig', boxVersionsUrl: 'box.versions', boxVersionsSigUrl: 'box.versions.sig' } );
updatechecker.checkForUpdates({ automatic: false }, function (error) {
expect(!error).to.be.ok();
expect(updatechecker.getUpdateInfo().box.version).to.be(UPDATE_VERSION);
expect(updatechecker.getUpdateInfo().box.sourceTarballUrl).to.be('box.tar.gz');
expect(scope.isDone()).to.be.ok();
done();
});
await updatechecker.checkForUpdates({ automatic: false });
expect(updatechecker.getUpdateInfo().box.version).to.be(UPDATE_VERSION);
expect(updatechecker.getUpdateInfo().box.sourceTarballUrl).to.be('box.tar.gz');
expect(scope.isDone()).to.be.ok();
});
it('bad response offers whatever was last valid', function (done) {
it('bad response offers whatever was last valid', async function () {
nock.cleanAll();
var scope = nock(mockApiServerOrigin)
const scope = nock(mockApiServerOrigin)
.get('/api/v1/boxupdate')
.query({ boxVersion: constants.VERSION, accessToken: appstoreToken, automatic: false })
.reply(404, { version: '2.0.0-pre.0', changelog: [''], sourceTarballUrl: 'box-pre.tar.gz' } );
updatechecker.checkForUpdates({ automatic: false }, function (error) {
expect(error).to.be.ok();
expect(updatechecker.getUpdateInfo().box.version).to.be(UPDATE_VERSION);
expect(updatechecker.getUpdateInfo().box.sourceTarballUrl).to.be('box.tar.gz');
expect(scope.isDone()).to.be.ok();
done();
});
await updatechecker.checkForUpdates({ automatic: false });
expect(updatechecker.getUpdateInfo().box.version).to.be(UPDATE_VERSION);
expect(updatechecker.getUpdateInfo().box.sourceTarballUrl).to.be('box.tar.gz');
expect(scope.isDone()).to.be.ok();
});
});
@@ -88,62 +79,50 @@ describe('updatechecker', function () {
settings.setAutoupdatePattern(constants.AUTOUPDATE_PATTERN_NEVER, done);
});
it('no updates', function (done) {
it('no updates', async function () {
nock.cleanAll();
var scope = nock(mockApiServerOrigin)
const scope = nock(mockApiServerOrigin)
.get('/api/v1/appupdate')
.query({ boxVersion: constants.VERSION, accessToken: appstoreToken, appId: app.appStoreId, appVersion: app.manifest.version, automatic: false })
.reply(204, { } );
updatechecker._checkAppUpdates({ automatic: false }, function (error) {
expect(!error).to.be.ok();
expect(updatechecker.getUpdateInfo()).to.eql({});
expect(scope.isDone()).to.be.ok();
done();
});
await updatechecker._checkAppUpdates({ automatic: false });
expect(updatechecker.getUpdateInfo()).to.eql({});
expect(scope.isDone()).to.be.ok();
});
it('bad response', function (done) {
it('bad response', async function () {
nock.cleanAll();
var scope = nock(mockApiServerOrigin)
const scope = nock(mockApiServerOrigin)
.get('/api/v1/appupdate')
.query({ boxVersion: constants.VERSION, accessToken: appstoreToken, appId: app.appStoreId, appVersion: app.manifest.version, automatic: false })
.reply(500, { update: { manifest: { version: '1.0.0', changelog: '* some changes' } } } );
updatechecker._checkAppUpdates({ automatic: false }, function (error) {
expect(!error).to.be.ok();
expect(updatechecker.getUpdateInfo()).to.eql({});
expect(scope.isDone()).to.be.ok();
done();
});
await updatechecker._checkAppUpdates({ automatic: false });
expect(updatechecker.getUpdateInfo()).to.eql({});
expect(scope.isDone()).to.be.ok();
});
it('offers new version', function (done) {
it('offers new version', async function () {
nock.cleanAll();
var scope = nock(mockApiServerOrigin)
const scope = nock(mockApiServerOrigin)
.get('/api/v1/appupdate')
.query({ boxVersion: constants.VERSION, accessToken: appstoreToken, appId: app.appStoreId, appVersion: app.manifest.version, automatic: false })
.reply(200, { manifest: { version: '2.0.0', changelog: '* some changes' } } );
updatechecker._checkAppUpdates({ automatic: false }, function (error) {
expect(!error).to.be.ok();
expect(updatechecker.getUpdateInfo()).to.eql({ 'appid': { manifest: { version: '2.0.0', changelog: '* some changes' }, unstable: false } });
expect(scope.isDone()).to.be.ok();
done();
});
await updatechecker._checkAppUpdates({ automatic: false });
expect(updatechecker.getUpdateInfo()).to.eql({ 'appid': { manifest: { version: '2.0.0', changelog: '* some changes' }, unstable: false } });
expect(scope.isDone()).to.be.ok();
});
it('does not offer old version', function (done) {
it('does not offer old version', async function () {
nock.cleanAll();
updatechecker._checkAppUpdates({ automatic: false }, function (error) {
expect(!error).to.be.ok();
expect(updatechecker.getUpdateInfo()).to.eql({ });
done();
});
await updatechecker._checkAppUpdates({ automatic: false });
expect(updatechecker.getUpdateInfo()).to.eql({ });
});
});
});
+42 -62
View File
@@ -11,11 +11,11 @@ exports = module.exports = {
const apps = require('./apps.js'),
appstore = require('./appstore.js'),
assert = require('assert'),
async = require('async'),
debug = require('debug')('box:updatechecker'),
notifications = require('./notifications.js'),
paths = require('./paths.js'),
safe = require('safetydance');
safe = require('safetydance'),
util = require('util');
function setUpdateInfo(state) {
// appid -> update info { creationDate, manifest }
@@ -31,95 +31,75 @@ function getUpdateInfo() {
return state;
}
function checkAppUpdates(options, callback) {
async function checkAppUpdates(options) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debug('checkAppUpdates: checking for updates');
let state = getUpdateInfo();
let newState = { }; // create new state so that old app ids are removed
apps.getAll(function (error, result) {
if (error) return callback(error);
const appsGetAllAsync = util.promisify(apps.getAll);
async.eachSeries(result, function (app, iteratorDone) {
if (app.appStoreId === '') return iteratorDone(); // appStoreId can be '' for dev apps
const result = await appsGetAllAsync();
appstore.getAppUpdate(app, options, function (error, updateInfo) {
if (error) {
debug('checkAppUpdates: Error getting app update info for %s', app.id, error);
return iteratorDone(); // continue to next
}
for (const app of result) {
if (app.appStoreId === '') continue; // appStoreId can be '' for dev apps
if (!updateInfo) return iteratorDone(); // skip if no next version is found
const [error, updateInfo] = await safe(appstore.getAppUpdate(app, options));
if (error) {
debug('checkAppUpdates: Error getting app update info for %s', app.id, error);
continue; // continue to next
}
newState[app.id] = updateInfo;
if (!updateInfo) continue; // skip if no next version is found
newState[app.id] = updateInfo;
}
iteratorDone();
});
}, function () {
if ('box' in state) newState.box = state.box; // preserve the latest box state information
setUpdateInfo(newState);
callback();
});
});
if ('box' in state) newState.box = state.box; // preserve the latest box state information
setUpdateInfo(newState);
}
function checkBoxUpdates(options, callback) {
async function checkBoxUpdates(options) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debug('checkBoxUpdates: checking for updates');
appstore.getBoxUpdate(options, async function (error, updateInfo) {
if (error) return callback(error);
const updateInfo = await appstore.getBoxUpdate(options);
let state = getUpdateInfo();
let state = getUpdateInfo();
if (!updateInfo) { // no update
if ('box' in state) {
delete state.box;
setUpdateInfo(state);
}
debug('checkBoxUpdates: no updates');
return callback(null);
if (!updateInfo) { // no update
if ('box' in state) {
delete state.box;
setUpdateInfo(state);
}
debug('checkBoxUpdates: no updates');
return;
}
if (state.box && state.box.version === updateInfo.version) {
debug(`checkBoxUpdates: Skipping notification of box update ${updateInfo.version} as user was already notified`);
return callback(null);
}
if (state.box && state.box.version === updateInfo.version) {
debug(`checkBoxUpdates: Skipping notification of box update ${updateInfo.version} as user was already notified`);
return;
}
debug(`checkBoxUpdates: ${updateInfo.version} is available`);
debug(`checkBoxUpdates: ${updateInfo.version} is available`);
const changelog = updateInfo.changelog.map((m) => `* ${m}\n`).join('');
const changelog = updateInfo.changelog.map((m) => `* ${m}\n`).join('');
const message = `Changelog:\n${changelog}\n\nGo to the settings view to update.\n\n`;
const message = `Changelog:\n${changelog}\n\nGo to the settings view to update.\n\n`;
await notifications.alert(notifications.ALERT_BOX_UPDATE, `Cloudron v${updateInfo.version} is available`, message);
[error] = await safe(notifications.alert(notifications.ALERT_BOX_UPDATE, `Cloudron v${updateInfo.version} is available`, message));
if (error) return callback(error);
state.box = updateInfo;
setUpdateInfo(state);
callback(null);
});
state.box = updateInfo;
setUpdateInfo(state);
}
function checkForUpdates(options, callback) {
async function checkForUpdates(options) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
checkBoxUpdates(options, function (boxError) {
if (boxError) debug('checkForUpdates: error checking for box updates:', boxError);
const [boxError] = await safe(checkBoxUpdates(options));
if (boxError) debug('checkForUpdates: error checking for box updates:', boxError);
checkAppUpdates(options, function (appError) {
if (appError) debug('checkForUpdates: error checking for app updates:', appError);
callback(boxError || appError || null);
});
});
const [appError] = await safe(checkAppUpdates(options));
if (appError) debug('checkForUpdates: error checking for app updates:', appError);
}