Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aaf266d272 | |||
| 0750db9aae | |||
| 316976d295 | |||
| 593b5d945b | |||
| 88f0240757 | |||
| f5c2f8849d | |||
| 5c4a8f7803 | |||
| 5b8fdad5cb | |||
| fe819f95ec | |||
| be6728f8cb | |||
| 24d3a81bc8 | |||
| 268c7b5bcf | |||
| 64716a2de5 | |||
| d2c8457ab1 | |||
| 667cb84af7 | |||
| df8653cdd5 | |||
| 32f677ca0d | |||
| 6f5408f0d6 | |||
| 23c04fb10b | |||
| 0c5d6b1045 | |||
| 33f30decd1 | |||
| 9595b63939 | |||
| b9695b09cd | |||
| 5a0f7df377 | |||
| 2e54be3df8 | |||
| 6625610aca | |||
| 5c9abfe97a | |||
| e06f3d4180 | |||
| e3cc12da4f | |||
| 3d80821203 | |||
| d9bfcc7c8a | |||
| 8bd9a6c109 | |||
| d89db24bfc | |||
| 352b5ca736 |
@@ -0,0 +1,16 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps MODIFY manifestJson TEXT', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps MODIFY manifestJson VARCHAR(2048)', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE apps MODIFY accessRestrictionJson TEXT'),
|
||||
db.runSql.bind(db, 'ALTER TABLE apps MODIFY lastBackupConfigJson TEXT'),
|
||||
db.runSql.bind(db, 'ALTER TABLE apps MODIFY oldConfigJson TEXT')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE apps MODIFY accessRestrictionJson VARCHAR(2048)'),
|
||||
db.runSql.bind(db, 'ALTER TABLE apps MODIFY lastBackupConfigJson VARCHAR(2048)'),
|
||||
db.runSql.bind(db, 'ALTER TABLE apps MODIFY oldConfigJson VARCHAR(2048)')
|
||||
], callback);
|
||||
};
|
||||
@@ -45,18 +45,18 @@ CREATE TABLE IF NOT EXISTS apps(
|
||||
runState VARCHAR(512),
|
||||
health VARCHAR(128),
|
||||
containerId VARCHAR(128),
|
||||
manifestJson VARCHAR(2048),
|
||||
manifestJson TEXT,
|
||||
httpPort INTEGER, // this is the nginx proxy port and not manifest.httpPort
|
||||
location VARCHAR(128) NOT NULL UNIQUE,
|
||||
dnsRecordId VARCHAR(512),
|
||||
accessRestrictionJson VARCHAR(2048),
|
||||
accessRestrictionJson TEXT,
|
||||
oauthProxy BOOLEAN DEFAULT 0,
|
||||
createdAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
lastBackupId VARCHAR(128),
|
||||
lastBackupConfigJson VARCHAR(2048), // used for appstore and non-appstore installs. it's here so it's easy to do REST validation
|
||||
lastBackupConfigJson TEXT, // used for appstore and non-appstore installs. it's here so it's easy to do REST validation
|
||||
|
||||
oldConfigJson VARCHAR(2048), // used to pass old config for apptask
|
||||
oldConfigJson TEXT, // used to pass old config for apptask
|
||||
|
||||
PRIMARY KEY(id));
|
||||
|
||||
|
||||
Generated
+1554
-2094
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -58,7 +58,7 @@
|
||||
"serve-favicon": "^2.2.0",
|
||||
"split": "^1.0.0",
|
||||
"superagent": "~0.21.0",
|
||||
"supererror": "^0.7.0",
|
||||
"supererror": "^0.7.1",
|
||||
"tail-stream": "https://registry.npmjs.org/tail-stream/-/tail-stream-0.2.1.tgz",
|
||||
"underscore": "^1.7.0",
|
||||
"valid-url": "^1.0.9",
|
||||
|
||||
+8
-8
@@ -3,17 +3,17 @@
|
||||
# If you change the infra version, be sure to put a warning
|
||||
# in the change log
|
||||
|
||||
INFRA_VERSION=20
|
||||
INFRA_VERSION=21
|
||||
|
||||
# WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
|
||||
# These constants are used in the installer script as well
|
||||
BASE_IMAGE=cloudron/base:0.7.0
|
||||
MYSQL_IMAGE=cloudron/mysql:0.7.0
|
||||
POSTGRESQL_IMAGE=cloudron/postgresql:0.7.0
|
||||
MONGODB_IMAGE=cloudron/mongodb:0.7.0
|
||||
REDIS_IMAGE=cloudron/redis:0.7.0 # if you change this, fix src/addons.js as well
|
||||
MAIL_IMAGE=cloudron/mail:0.8.0
|
||||
GRAPHITE_IMAGE=cloudron/graphite:0.7.0
|
||||
BASE_IMAGE=cloudron/base:0.8.0
|
||||
MYSQL_IMAGE=cloudron/mysql:0.8.0
|
||||
POSTGRESQL_IMAGE=cloudron/postgresql:0.8.0
|
||||
MONGODB_IMAGE=cloudron/mongodb:0.8.0
|
||||
REDIS_IMAGE=cloudron/redis:0.8.0 # if you change this, fix src/addons.js as well
|
||||
MAIL_IMAGE=cloudron/mail:0.9.0
|
||||
GRAPHITE_IMAGE=cloudron/graphite:0.8.0
|
||||
|
||||
MYSQL_REPO=cloudron/mysql
|
||||
POSTGRESQL_REPO=cloudron/postgresql
|
||||
|
||||
+2
-1
@@ -48,6 +48,7 @@ mkdir -p "${DATA_DIR}/mongodb"
|
||||
mkdir -p "${DATA_DIR}/snapshots"
|
||||
mkdir -p "${DATA_DIR}/addons"
|
||||
mkdir -p "${DATA_DIR}/collectd/collectd.conf.d"
|
||||
mkdir -p "${DATA_DIR}/acme"
|
||||
|
||||
# bookkeep the version as part of data
|
||||
echo "{ \"version\": \"${arg_version}\", \"boxVersionsUrl\": \"${arg_box_versions_url}\" }" > "${DATA_DIR}/box/version"
|
||||
@@ -125,7 +126,7 @@ else
|
||||
fi
|
||||
|
||||
set_progress "33" "Changing ownership"
|
||||
chown "${USER}:${USER}" -R "${DATA_DIR}/box" "${DATA_DIR}/nginx" "${DATA_DIR}/collectd" "${DATA_DIR}/addons"
|
||||
chown "${USER}:${USER}" -R "${DATA_DIR}/box" "${DATA_DIR}/nginx" "${DATA_DIR}/collectd" "${DATA_DIR}/addons" "${DATA_DIR}/acme"
|
||||
chown "${USER}:${USER}" "${DATA_DIR}"
|
||||
|
||||
set_progress "40" "Setting up infra"
|
||||
|
||||
@@ -37,7 +37,8 @@ server {
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
|
||||
error_page 500 502 503 504 @appstatus;
|
||||
# only serve up the status page if we get proxy gateway errors
|
||||
error_page 502 503 504 @appstatus;
|
||||
location @appstatus {
|
||||
return 307 <%= adminOrigin %>/appstatus.html?referrer=https://$host$request_uri;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,11 @@ http {
|
||||
deny all;
|
||||
}
|
||||
|
||||
# acme challenges
|
||||
location /.well-known/acme-challenge/ {
|
||||
alias /home/yellowtent/data/acme/;
|
||||
}
|
||||
|
||||
location / {
|
||||
# redirect everything to HTTPS
|
||||
return 301 https://$host$request_uri;
|
||||
|
||||
+334
@@ -0,0 +1,334 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
crypto = require('crypto'),
|
||||
debug = require('debug')('acme'),
|
||||
execSync = require('child_process').execSync,
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
superagent = require('superagent'),
|
||||
urlBase64Encode = require('url-base64-node').escape,
|
||||
ursa = require('ursa'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
var CA_STAGING = 'https://acme-v01.api.letsencrypt.org',
|
||||
CA_STAGING = 'https://acme-staging.api.letsencrypt.org/',
|
||||
LE_AGREEMENT = 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf';
|
||||
|
||||
exports = module.exports = {
|
||||
getCertificate: getCertificate
|
||||
};
|
||||
|
||||
function AcmeError(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(AcmeError, Error);
|
||||
AcmeError.INTERNAL_ERROR = 'Internal Error';
|
||||
AcmeError.EXTERNAL_ERROR = 'External Error';
|
||||
AcmeError.ALREADY_EXISTS = 'Already Exists';
|
||||
AcmeError.NOT_COMPLETED = 'Not Completed';
|
||||
AcmeError.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 getNonce(callback) {
|
||||
superagent.get(CA_STAGING + '/directory', function (error, response) {
|
||||
if (error) 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 b64(str) {
|
||||
var buf = util.isBuffer(str) ? str : new Buffer(str);
|
||||
return urlBase64Encode(buf.toString('base64'));
|
||||
}
|
||||
|
||||
function sendSignedRequest(url, privateKeyPem, payload, callback) {
|
||||
assert.strictEqual(typeof url, 'string');
|
||||
assert(util.isBuffer(privateKeyPem));
|
||||
assert.strictEqual(typeof payload, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var privateKey = ursa.createPrivateKey(privateKeyPem);
|
||||
|
||||
var header = {
|
||||
alg: 'RS256',
|
||||
jwk: {
|
||||
e: b64(privateKey.getExponent()),
|
||||
kty: 'RSA',
|
||||
n: b64(privateKey.getModulus())
|
||||
}
|
||||
};
|
||||
|
||||
var payload64 = b64(payload);
|
||||
|
||||
getNonce(function (error, nonce) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('Using nonce %s', nonce);
|
||||
|
||||
var protected64 = b64(JSON.stringify(_.extend({ }, header, { nonce: nonce })));
|
||||
|
||||
var signer = ursa.createSigner('sha256');
|
||||
signer.update(protected64 + '.' + payload64, 'utf8');
|
||||
var signature64 = urlBase64Encode(signer.sign(privateKey, '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)).buffer().end(function (error, res) {
|
||||
if (error && !error.response) return callback(error); // network errors
|
||||
|
||||
callback(null, res);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function registerUser(privateKeyPem, email, callback) {
|
||||
assert(util.isBuffer(privateKeyPem));
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var payload = {
|
||||
resource: 'new-reg',
|
||||
contact: [ 'mailto:' + email ],
|
||||
agreement: LE_AGREEMENT
|
||||
};
|
||||
|
||||
debug('registerUser: %s', email);
|
||||
|
||||
sendSignedRequest(CA_STAGING + '/acme/new-reg', privateKeyPem, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
|
||||
if (result.statusCode === 409) return callback(new AcmeError(AcmeError.ALREADY_EXISTS, result.body.detail));
|
||||
if (result.statusCode !== 201) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
debug('registerUser: registered user %s', email);
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function registerDomain(privateKeyPem, domain, callback) {
|
||||
assert(util.isBuffer(privateKeyPem));
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var payload = {
|
||||
resource: 'new-authz',
|
||||
identifier: {
|
||||
type: 'dns',
|
||||
value: domain
|
||||
}
|
||||
};
|
||||
|
||||
debug('registerDomain: %s', domain);
|
||||
|
||||
sendSignedRequest(CA_STAGING + '/acme/new-authz', privateKeyPem, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when registering domain: ' + error.message));
|
||||
if (result.statusCode === 403) return callback(new AcmeError(AcmeError.FORBIDDEN, result.body.detail));
|
||||
if (result.statusCode !== 201) return callback(new AcmeError(AcmeError.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);
|
||||
});
|
||||
}
|
||||
|
||||
function prepareHttpChallenge(privateKeyPem, challenge, callback) {
|
||||
assert(util.isBuffer(privateKeyPem));
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('prepareHttpChallenge: preparing for challenge %j', challenge);
|
||||
|
||||
var token = challenge.token;
|
||||
|
||||
var privateKey = ursa.createPrivateKey(privateKeyPem);
|
||||
|
||||
var jwk = {
|
||||
e: b64(privateKey.getExponent()),
|
||||
kty: 'RSA',
|
||||
n: b64(privateKey.getModulus())
|
||||
};
|
||||
|
||||
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 AcmeError(AcmeError.INTERNAL_ERROR, error));
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function notifyChallengeReady(privateKeyPem, challenge, callback) {
|
||||
assert(util.isBuffer(privateKeyPem));
|
||||
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
|
||||
};
|
||||
|
||||
sendSignedRequest(challenge.uri, privateKeyPem, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when notifying challenge: ' + error.message));
|
||||
if (result.statusCode !== 202) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 202, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function waitForChallenge(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).end(function (error, result) {
|
||||
if (error && !error.response) {
|
||||
debug('waitForChallenge: network error getting uri %s', challenge.uri);
|
||||
return retryCallback(new AcmeError(AcmeError.EXTERNAL_ERROR, error.message)); // network error
|
||||
}
|
||||
if (result.statusCode !== 202) {
|
||||
debug('waitForChallenge: invalid response code getting uri %s', result.statusCode);
|
||||
return retryCallback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
|
||||
}
|
||||
|
||||
debug('waitForChallenge: status is "%s"', result.body.status);
|
||||
|
||||
if (result.body.status === 'pending') return retryCallback(new AcmeError(AcmeError.NOT_COMPLETED));
|
||||
else if (result.body.status === 'valid') return retryCallback();
|
||||
else return retryCallback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Unexpected status: ' + result.body.status));
|
||||
});
|
||||
}, callback);
|
||||
}
|
||||
|
||||
// https://community.letsencrypt.org/t/public-beta-rate-limits/4772 for rate limits
|
||||
function signCertificate(privateKeyPem, certificateDer, callback) {
|
||||
assert(util.isBuffer(privateKeyPem));
|
||||
assert(util.isBuffer(certificateDer));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var payload = {
|
||||
resource: 'new-cert',
|
||||
csr: b64(certificateDer)
|
||||
};
|
||||
|
||||
debug('signCertificate: signing %s', payload.csr);
|
||||
|
||||
sendSignedRequest(CA_STAGING + '/acme/new-cert', privateKeyPem, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when signing certificate: ' + error.message));
|
||||
if (result.statusCode !== 201) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
// TODO: result.body can be empty in which case it has to be polled for from this location
|
||||
debug('signCertificate: certificate is available at ', result.headers['location']);
|
||||
|
||||
callback(null, result.text);
|
||||
});
|
||||
}
|
||||
|
||||
function acmeFlow(domain, email, privateKeyPem, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert(util.isBuffer(privateKeyPem));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
registerUser(privateKeyPem, email, function (error) {
|
||||
if (error && error.reason !== AcmeError.ALREADY_EXISTS) return callback(error);
|
||||
|
||||
registerDomain(privateKeyPem, domain, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('getCertificate: challenges: %j', result);
|
||||
|
||||
var httpChallenges = result.challenges.filter(function(x) { return x.type === 'http-01'; });
|
||||
if (httpChallenges.length === 0) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'no http challenges'));
|
||||
var challenge = httpChallenges[0];
|
||||
|
||||
prepareHttpChallenge(privateKeyPem, challenge, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
notifyChallengeReady(privateKeyPem, challenge, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
waitForChallenge(challenge, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var serverKey = execSync('openssl genrsa 4096');
|
||||
var certificateDer = execSync(util.format('openssl req -nodes -outform DER -subj /CN=%s', domain), { stdio: [ serverKey, null, null ] });
|
||||
|
||||
signCertificate(privateKeyPem, certificateDer, function (error, certificateDer) {
|
||||
if (error) return callback(error);
|
||||
var certificatePem = execSync('openssl x509 -inform DER -outform PEM', { stdio: [ certificateDer, null, null ] });
|
||||
|
||||
callback(null, serverKey, certificatePem);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getCertificate(domain, callback) {
|
||||
var email = 'admin@' + config.fqdn();
|
||||
var privateKeyPem;
|
||||
|
||||
if (!fs.existsSync(paths.ACME_ACCOUNT_KEY_FILE)) {
|
||||
debug('getCertificate: generating acme account key on first run');
|
||||
privateKeyPem = execSync('openssl genrsa 4096');
|
||||
fs.writeFileSync(paths.ACME_ACCOUNT_KEY_FILE, privateKeyPem);
|
||||
} else {
|
||||
privateKeyPem = fs.readFileSync(paths.ACME_ACCOUNT_KEY_FILE);
|
||||
}
|
||||
|
||||
acmeFlow(domain, email, privateKeyPem, callback);
|
||||
}
|
||||
|
||||
getCertificate('foobar.girish.in', function (error, key, cert) {
|
||||
console.dir(error);
|
||||
console.dir(key);
|
||||
console.dir(cert);
|
||||
});
|
||||
+6
-6
@@ -325,10 +325,10 @@ function setupSimpleAuth(app, options, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var env = [
|
||||
'SIMPLE_AUTH_SERVER=172.17.42.1',
|
||||
'SIMPLE_AUTH_SERVER=172.17.0.1',
|
||||
'SIMPLE_AUTH_PORT=' + config.get('simpleAuthPort'),
|
||||
'SIMPLE_AUTH_URL=http://172.17.42.1:' + config.get('simpleAuthPort'), // obsolete, remove
|
||||
'SIMPLE_AUTH_ORIGIN=http://172.17.42.1:' + config.get('simpleAuthPort'),
|
||||
'SIMPLE_AUTH_URL=http://172.17.0.1:' + config.get('simpleAuthPort'), // obsolete, remove
|
||||
'SIMPLE_AUTH_ORIGIN=http://172.17.0.1:' + config.get('simpleAuthPort'),
|
||||
'SIMPLE_AUTH_CLIENT_ID=' + id
|
||||
];
|
||||
|
||||
@@ -359,9 +359,9 @@ function setupLdap(app, options, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var env = [
|
||||
'LDAP_SERVER=172.17.42.1',
|
||||
'LDAP_SERVER=172.17.0.1',
|
||||
'LDAP_PORT=' + config.get('ldapPort'),
|
||||
'LDAP_URL=ldap://172.17.42.1:' + config.get('ldapPort'),
|
||||
'LDAP_URL=ldap://172.17.0.1:' + config.get('ldapPort'),
|
||||
'LDAP_USERS_BASE_DN=ou=users,dc=cloudron',
|
||||
'LDAP_GROUPS_BASE_DN=ou=groups,dc=cloudron',
|
||||
'LDAP_BIND_DN=cn='+ app.id + ',ou=apps,dc=cloudron',
|
||||
@@ -778,7 +778,7 @@ function setupRedis(app, options, callback) {
|
||||
name: 'redis-' + app.id,
|
||||
Hostname: 'redis-' + app.location,
|
||||
Tty: true,
|
||||
Image: 'cloudron/redis:0.7.0', // if you change this, fix setup/INFRA_VERSION as well
|
||||
Image: 'cloudron/redis:0.8.0', // if you change this, fix setup/INFRA_VERSION as well
|
||||
Cmd: null,
|
||||
Volumes: {
|
||||
'/tmp': {},
|
||||
|
||||
@@ -110,7 +110,7 @@ function processApps(callback) {
|
||||
async.each(apps, checkAppHealth, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
var alive =apps
|
||||
var alive = apps
|
||||
.filter(function (a) { return a.installationState === appdb.ISTATE_INSTALLED && a.runState === appdb.RSTATE_RUNNING && a.health === appdb.HEALTH_HEALTHY; })
|
||||
.map(function (a) { return a.location; }).join(', ');
|
||||
|
||||
|
||||
+2
-3
@@ -176,7 +176,7 @@ function validatePortBindings(portBindings, tcpPorts) {
|
||||
if (!Number.isInteger(portBindings[env])) return new Error(portBindings[env] + ' is not an integer');
|
||||
if (portBindings[env] <= 0 || portBindings[env] > 65535) return new Error(portBindings[env] + ' is out of range');
|
||||
|
||||
if (RESERVED_PORTS.indexOf(portBindings[env]) !== -1) return new AppsError(AppsError.PORT_RESERVED, + portBindings[env]);
|
||||
if (RESERVED_PORTS.indexOf(portBindings[env]) !== -1) return new AppsError(AppsError.PORT_RESERVED, String(portBindings[env]));
|
||||
}
|
||||
|
||||
// it is OK if there is no 1-1 mapping between values in manifest.tcpPorts and portBindings. missing values implies
|
||||
@@ -293,6 +293,7 @@ function purchase(appStoreId, callback) {
|
||||
superagent.post(url).query({ token: config.token() }).end(function (error, res) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
if (res.status === 402) return callback(new AppsError(AppsError.BILLING_REQUIRED));
|
||||
if (res.status === 404) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
if (res.status !== 201 && res.status !== 200) return callback(new Error(util.format('App purchase failed. %s %j', res.status, res.body)));
|
||||
|
||||
callback(null);
|
||||
@@ -496,8 +497,6 @@ function getLogs(appId, lines, follow, callback) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (app.installationState !== appdb.ISTATE_INSTALLED) return callback(new AppsError(AppsError.BAD_STATE, util.format('App is in %s state.', app.installationState)));
|
||||
|
||||
var args = [ '--output=json', '--no-pager', '--lines=' + lines ];
|
||||
if (follow) args.push('--follow');
|
||||
args = args.concat(appLogFilter(app));
|
||||
|
||||
+1
-1
@@ -660,7 +660,7 @@ function stopApp(app, callback) {
|
||||
docker.stopContainers(app.id, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
updateApp(app, { runState: appdb.RSTATE_STOPPED }, callback);
|
||||
updateApp(app, { runState: appdb.RSTATE_STOPPED, health: null }, callback);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+7
-6
@@ -413,10 +413,11 @@ function addDnsRecords() {
|
||||
|
||||
debug('addDnsRecords: will update %j', records);
|
||||
|
||||
async.eachSeries(records, function (record, iteratorCallback) {
|
||||
async.mapSeries(records, function (record, iteratorCallback) {
|
||||
subdomains.update(record.subdomain, record.type, record.values, iteratorCallback);
|
||||
}, function (error) {
|
||||
}, function (error, changeIds) {
|
||||
if (error) debug('addDnsRecords: failed to update : %s. will retry', error);
|
||||
else debug('addDnsRecords: records %j added with changeIds %j', records, changeIds);
|
||||
|
||||
retryCallback(error);
|
||||
});
|
||||
@@ -491,7 +492,7 @@ function update(boxUpdateInfo, callback) {
|
||||
debug('Starting upgrade');
|
||||
doUpgrade(boxUpdateInfo, function (error) {
|
||||
if (error) {
|
||||
debug('Upgrade failed with error: %s', error);
|
||||
console.error('Upgrade failed with error:', error);
|
||||
locker.unlock(locker.OP_BOX_UPDATE);
|
||||
}
|
||||
});
|
||||
@@ -499,7 +500,7 @@ function update(boxUpdateInfo, callback) {
|
||||
debug('Starting update');
|
||||
doUpdate(boxUpdateInfo, function (error) {
|
||||
if (error) {
|
||||
debug('Update failed with error: %s', error);
|
||||
console.error('Update failed with error:', error);
|
||||
locker.unlock(locker.OP_BOX_UPDATE);
|
||||
}
|
||||
});
|
||||
@@ -699,13 +700,13 @@ function backupBoxAndApps(callback) {
|
||||
++processed;
|
||||
|
||||
apps.backupApp(app, app.manifest.addons, function (error, backupId) {
|
||||
progress.set(progress.BACKUP, step * processed, 'Backed up app at ' + app.location);
|
||||
|
||||
if (error && error.reason !== AppsError.BAD_STATE) {
|
||||
debugApp(app, 'Unable to backup', error);
|
||||
return iteratorCallback(error);
|
||||
}
|
||||
|
||||
progress.set(progress.BACKUP, step * processed, 'Backed up app at ' + app.location);
|
||||
|
||||
iteratorCallback(null, backupId || null); // clear backupId if is in BAD_STATE and never backed up
|
||||
});
|
||||
}, function appsBackedUp(error, backupIds) {
|
||||
|
||||
@@ -126,6 +126,8 @@ function clear(callback) {
|
||||
function beginTransaction(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (gConnectionPool === null) return callback(new Error('No database connection pool.'));
|
||||
|
||||
gConnectionPool.getConnection(function (error, connection) {
|
||||
if (error) return callback(error);
|
||||
|
||||
|
||||
+7
-2
@@ -154,6 +154,9 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
}
|
||||
|
||||
var memoryLimit = manifest.memoryLimit || 1024 * 1024 * 200; // 200mb by default
|
||||
// for subcontainers, this should ideally be false. but docker does not allow network sharing if the app container is not running
|
||||
// this means cloudron exec does not work
|
||||
var isolatedNetworkNs = true;
|
||||
|
||||
addons.getEnvironment(app, function (error, addonEnv) {
|
||||
if (error) return callback(new Error('Error getting addon environment : ' + error));
|
||||
@@ -161,7 +164,8 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
var containerOptions = {
|
||||
name: name, // used for filtering logs
|
||||
// do _not_ set hostname to app fqdn. doing so sets up the dns name to look up the internal docker ip. this makes curl from within container fail
|
||||
Hostname: semver.gte(targetBoxVersion(app.manifest), '0.0.77') ? app.location : config.appFqdn(app.location),
|
||||
// for subcontainers, this should not be set because we already share the network namespace with app container
|
||||
Hostname: isolatedNetworkNs ? (semver.gte(targetBoxVersion(app.manifest), '0.0.77') ? app.location : config.appFqdn(app.location)) : null,
|
||||
Tty: isAppContainer,
|
||||
Image: app.manifest.dockerImage,
|
||||
Cmd: cmd,
|
||||
@@ -183,13 +187,14 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
PortBindings: isAppContainer ? dockerPortBindings : { },
|
||||
PublishAllPorts: false,
|
||||
ReadonlyRootfs: semver.gte(targetBoxVersion(app.manifest), '0.0.66'), // see also Volumes in startContainer
|
||||
Links: addons.getLinksSync(app, app.manifest.addons),
|
||||
RestartPolicy: {
|
||||
"Name": isAppContainer ? "always" : "no",
|
||||
"MaximumRetryCount": 0
|
||||
},
|
||||
CpuShares: 512, // relative to 1024 for system processes
|
||||
VolumesFrom: isAppContainer ? null : [ app.containerId + ":rw" ],
|
||||
NetworkMode: isolatedNetworkNs ? 'default' : ('container:' + app.containerId), // share network namespace with parent
|
||||
Links: isolatedNetworkNs ? addons.getLinksSync(app, app.manifest.addons) : null, // links is redundant with --net=container
|
||||
SecurityOpt: config.CLOUDRON ? [ "apparmor:docker-cloudron-app" ] : null // profile available only on cloudron
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,8 +5,7 @@ Dear Admin,
|
||||
The application titled '<%= title %>' that you installed at <%= appFqdn %>
|
||||
is not responding.
|
||||
|
||||
This is most likely a problem in the application. Please report this issue to
|
||||
support@cloudron.io (by forwarding this email).
|
||||
This is most likely a problem in the application.
|
||||
|
||||
You are receiving this email because you are an Admin of the Cloudron at <%= fqdn %>.
|
||||
|
||||
|
||||
+1
-1
@@ -284,7 +284,7 @@ function appDied(app) {
|
||||
|
||||
var mailOptions = {
|
||||
from: config.get('adminEmail'),
|
||||
to: adminEmails.join(', '),
|
||||
to: adminEmails.concat('support@cloudron.io').join(', '),
|
||||
subject: util.format('App %s is down', app.location),
|
||||
text: render('app_down.ejs', { fqdn: config.fqdn(), title: app.manifest.title, appFqdn: config.appFqdn(app.location), format: 'text' })
|
||||
};
|
||||
|
||||
@@ -35,9 +35,9 @@ args.forEach(function (arg) {
|
||||
});
|
||||
|
||||
if (code && redirectURI) {
|
||||
window.location.href = redirectURI + '?code=' + code + (state ? '&state=' + state : '');
|
||||
window.location.href = redirectURI + (redirectURI.indexOf('?') !== -1 ? '&' : '?') + 'code=' + code + (state ? '&state=' + state : '');
|
||||
} else if (redirectURI && accessToken) {
|
||||
window.location.href = redirectURI + '?token=' + accessToken + (state ? '&state=' + state : '');
|
||||
window.location.href = redirectURI + (redirectURI.indexOf('?') !== -1 ? '&' : '?') + 'token=' + accessToken + (state ? '&state=' + state : '');
|
||||
} else {
|
||||
window.location.href = '/api/v1/session/login';
|
||||
}
|
||||
|
||||
+4
-1
@@ -28,5 +28,8 @@ exports = module.exports = {
|
||||
CLOUDRON_AVATAR_FILE: path.join(config.baseDir(), 'data/box/avatar.png'),
|
||||
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
|
||||
|
||||
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'data/box/updatechecker.json')
|
||||
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'data/box/updatechecker.json'),
|
||||
|
||||
ACME_CHALLENGES_DIR: path.join(config.baseDir(), 'data/acme'),
|
||||
ACME_ACCOUNT_KEY_FILE: path.join(config.baseDir(), 'data/box/acme.key')
|
||||
};
|
||||
|
||||
@@ -133,6 +133,7 @@ function installApp(req, res, next) {
|
||||
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
|
||||
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === AppsError.BILLING_REQUIRED) return next(new HttpError(402, 'Billing required'));
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_CERTIFICATE) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === AppsError.USER_REQUIRED) return next(new HttpError(400, 'accessRestriction must specify one user'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
@@ -359,7 +359,7 @@ var authorization = [
|
||||
var redirectPath = url.parse(redirectURI).path;
|
||||
var redirectOrigin = client.redirectURI;
|
||||
|
||||
callback(null, client, '/api/v1/session/callback?redirectURI=' + url.resolve(redirectOrigin, redirectPath));
|
||||
callback(null, client, '/api/v1/session/callback?redirectURI=' + encodeURIComponent(url.resolve(redirectOrigin, redirectPath)));
|
||||
});
|
||||
}),
|
||||
function (req, res, next) {
|
||||
|
||||
@@ -182,6 +182,8 @@ function doTask(appId, taskName, callback) {
|
||||
|
||||
// NOTE: if you change container name here, fix addons.js to return correct container names
|
||||
docker.createSubcontainer(app, app.id + '-' + taskName, [ '/bin/sh', '-c', gState[appId].schedulerConfig[taskName].command ], { } /* options */, function (error, container) {
|
||||
if (error) return callback(error);
|
||||
|
||||
appState.containerIds[taskName] = container.id;
|
||||
|
||||
saveState(gState);
|
||||
|
||||
Reference in New Issue
Block a user