Compare commits

..

76 Commits

Author SHA1 Message Date
Girish Ramakrishnan 7f45e1db06 send new login location to user email 2021-11-17 11:53:03 -08:00
Girish Ramakrishnan 2ab2255115 fix dhparam generation
it cannot be created in default config creation time since it is
already run pre-VM snapshot time
2021-11-17 11:48:06 -08:00
Girish Ramakrishnan 515b1db9d0 Fix tests 2021-11-17 11:35:44 -08:00
Girish Ramakrishnan a7fe7b0aa3 boxerror: add acme error code 2021-11-17 10:54:26 -08:00
Girish Ramakrishnan 89389258d7 pass correct auditSource when raising notifications
this fixes the bug where automatic app update notification were not
raised.
2021-11-17 10:42:53 -08:00
Girish Ramakrishnan 1aacf65372 apps: pass the auditSource to addTask()
this is required for the notification logic to know what caused the
task (cron or manual, for example)
2021-11-17 10:38:02 -08:00
Girish Ramakrishnan 7ffcfc5206 auditSource: add PLATFORM 2021-11-17 10:33:28 -08:00
Girish Ramakrishnan 5ab2d9da8a notifications: remove dead code 2021-11-17 10:26:47 -08:00
Girish Ramakrishnan cd302a7621 add missing await 2021-11-17 09:38:01 -08:00
Girish Ramakrishnan 1c8e699a71 generate dhparams per server
this way we don't need to save/restore it from the database.
2021-11-16 23:03:16 -08:00
Girish Ramakrishnan c4db0d746d acme: if account key was revoked, generate new account key
the plan was to migrate only specific keys but this allows us the
flexibility to revoke keys after the release (since we have not
gotten response from DO about access to old 1-click images so far).
2021-11-16 22:57:40 -08:00
Girish Ramakrishnan b7c5c99301 move turn secret generation 2021-11-16 22:37:42 -08:00
Girish Ramakrishnan 132c1872f4 sftp: move key generation to sftp code 2021-11-16 21:52:39 -08:00
Girish Ramakrishnan 0f04933dbf backups: fix issue where mail backups were not cleaned up 2021-11-16 19:52:51 -08:00
Girish Ramakrishnan 6d864d3621 ensure we have atleast 1GB before making an update 2021-11-16 18:20:40 -08:00
Girish Ramakrishnan b6ee1fb662 mail: add non-tls ports for recvmail addon 2021-11-16 17:21:34 -08:00
Girish Ramakrishnan 649cd896fc throw error and not return 2021-11-16 14:46:58 -08:00
Girish Ramakrishnan 39be267805 restore: secrets must be copied over after downloading box backup 2021-11-16 11:14:41 -08:00
Girish Ramakrishnan f6356b2dff speed up dhparam creation 2021-11-16 09:53:43 -08:00
Johannes Zellner 48574ce350 Add missing await 2021-11-16 18:48:13 +01:00
Girish Ramakrishnan 40a3145d92 Add more bad account keys and fix fresh cloudron migration 2021-11-16 00:56:59 -08:00
Girish Ramakrishnan f42430b7c4 regenerate acme key of DO 1-click image
https://community.letsencrypt.org/t/receiving-expiration-emails-for-dozens-of-domains/165441
2021-11-16 00:25:59 -08:00
Girish Ramakrishnan 178d93033f 7.0.4 changes 2021-11-15 23:51:06 -08:00
Girish Ramakrishnan 01a1803625 provision: delay initialization of secrets until provision time
when we create the DO 1-click image, the key also gets snapshotted.

https://community.letsencrypt.org/t/receiving-expiration-emails-for-dozens-of-domains/165441
2021-11-15 23:33:54 -08:00
Girish Ramakrishnan 42eef42cf3 Add to changes 2021-11-15 13:58:59 -08:00
Girish Ramakrishnan 9c096b18e1 demo: limit to 20 apps 2021-11-15 13:55:29 -08:00
Girish Ramakrishnan aa3ee2e180 cloudron-support: add option to reset account
new cli option --reset-appstore-account
2021-11-15 10:06:18 -08:00
Girish Ramakrishnan fdefc780b4 docker: hardcode the bridge gateway IP
on some environments like ESXi, the gateway gets the dynamic IP 172.18.0.2.
we have hardcoded 172.18.0.1 in many places in the code

https://forum.cloudron.io/topic/5987/install-cloudron-7-0-3-on-ubuntu-20-04-3-esxi
2021-11-12 09:04:03 -08:00
Johannes Zellner 3826ae64c6 Ensure the main login route is rate-limited 2021-11-12 11:14:21 +01:00
Johannes Zellner dcdafda124 Remove deprecated developer/login route 2021-11-12 11:12:15 +01:00
Girish Ramakrishnan fc2cc25861 Update manifest-format (httpPaths) 2021-11-09 21:56:52 -08:00
Girish Ramakrishnan 68db4524f1 remove unused httpPaths from manifest 2021-11-09 21:50:33 -08:00
Girish Ramakrishnan 48b75accdd 7.0.4 changes 2021-11-09 09:31:58 -08:00
Johannes Zellner 0313a60f44 Fix newline stripping when passing the tmp file as path
This fixes the issue where the input data gets too large for the
commandline argument buffer
2021-11-09 16:05:36 +01:00
Girish Ramakrishnan 9897b5d18a appstore: fix crash if account already registered 2021-11-08 10:45:57 -08:00
Girish Ramakrishnan e4cc431d35 Do not nuke all the logrotate configs on update
this was added many releases ago to migrate to new logrotate configs.
looks like I forgot to remove this.

https://forum.cloudron.io/topic/4381/safe-to-truncate-home-yellowtent-platformdata-logs-when-large-disk-consumer
2021-11-04 09:41:33 -07:00
Girish Ramakrishnan 535a755e74 7.1.0 changes 2021-11-03 15:08:48 -07:00
Johannes Zellner 2ae77a5ab7 Provide dashboardOrigin to proxy auth for stylesheet sourcing 2021-11-03 22:12:30 +01:00
Johannes Zellner e36d7665fa The profile based password reset does not return a resetLink 2021-11-03 22:03:08 +01:00
Girish Ramakrishnan 786b627bad add 7.0.3 changes 2021-11-03 12:21:12 -07:00
Girish Ramakrishnan c7ddbea8ed restore: download mail backup in restore phase
if we download it in the platform start phase, there is no way to
give feedback to the user. so it's best to show the restore UI and
not redirect to the dashboard.
2021-11-03 12:10:40 -07:00
Girish Ramakrishnan af2a8ba07f add retry to platform.start instead
this is because it holds a lock and cannot be re-tried

See also 0c0aeeae4c which tried to
make it for all startup tasks
2021-11-02 23:35:53 -07:00
Girish Ramakrishnan 4ffe03553a database: sqlMessage can be undefined for connection errors 2021-11-02 23:23:59 -07:00
Girish Ramakrishnan f505fdd5cb remove the space 2021-11-02 18:07:45 -07:00
Girish Ramakrishnan ce4f5c0ad6 backups: print the app index/total 2021-11-02 18:07:19 -07:00
Girish Ramakrishnan de2c596394 backups: typo
this resulted in incomplete backups when there is an app with backups disabled
2021-11-02 18:00:04 -07:00
Girish Ramakrishnan 6cb041bcb2 Print readable sizes in the log 2021-11-02 17:51:27 -07:00
Girish Ramakrishnan 0c0aeeae4c retry startup tasks on database error
https://forum.cloudron.io/topic/5909/cloudron-7-0-1-gitlab-stuck-after-update
2021-11-02 14:05:51 -07:00
Girish Ramakrishnan 8bfb3d6b6d mail: save message-id in eventlog 2021-11-02 01:42:07 -07:00
Girish Ramakrishnan f803754e08 mail: fix eventlog search 2021-11-02 01:00:28 -07:00
Girish Ramakrishnan 09cfce79fb mail: fix direction field in eventlog of deferred mails 2021-11-02 00:48:01 -07:00
Girish Ramakrishnan 6479e333de pop3: fix crash when authenticating non-existent mailbox 2021-11-01 19:54:39 -07:00
Girish Ramakrishnan 28d1d5e960 ldap: make mailbox app passwords work with sogo 2021-11-01 19:17:30 -07:00
Girish Ramakrishnan 15d8f4e89c ldap: remove legacy sogo search route 2021-11-01 17:08:23 -07:00
Girish Ramakrishnan 8fdbd7bd5f 7.0.3 changes 2021-11-01 16:17:35 -07:00
Girish Ramakrishnan 7b5ed0b2a1 support: set filePath when user is root 2021-11-01 12:20:47 -07:00
Girish Ramakrishnan b69c5f62c0 Add to changes 2021-10-28 10:27:32 -07:00
Johannes Zellner 63f6f065ba Add and fixup invite link related tests 2021-10-28 11:18:31 +02:00
Johannes Zellner 92f0f56fae do not strictly require fallbackEmail on user creation but provide a fallback 2021-10-28 10:29:02 +02:00
Johannes Zellner cb8aa15e62 Do not allow setting ghost password for user without username 2021-10-27 23:36:44 +02:00
Johannes Zellner 4356d673bc Fix wrong assert and minor typos 2021-10-27 22:31:54 +02:00
Girish Ramakrishnan 5ece159fba sftp: fix crash when creating directory 2021-10-27 13:17:23 -07:00
Johannes Zellner b59776bf9b fail getting invite link or sending invite if invate was already used 2021-10-27 21:25:43 +02:00
Johannes Zellner 475795a107 Invite is now also separate 2021-10-27 19:58:06 +02:00
Johannes Zellner 9a80049d36 Add two distinct password reset routes 2021-10-27 19:12:18 +02:00
Johannes Zellner daf212468f fallbackEmail is now independent from email 2021-10-26 22:50:02 +02:00
Girish Ramakrishnan 2f510c2625 capitalize sql keywords 2021-10-26 11:19:30 -07:00
Girish Ramakrishnan 7a977fa76b 7.0.2 changes 2021-10-26 11:17:57 -07:00
Girish Ramakrishnan f5e025c213 mail: mailbox listing does not return pop3 status 2021-10-26 11:11:07 -07:00
Girish Ramakrishnan 971b73f853 move the bind inside 2021-10-26 11:03:54 -07:00
Girish Ramakrishnan 0103b21724 bump default backup memory limit to 800 2021-10-26 11:03:54 -07:00
Johannes Zellner cef5c1e78c Use normal bind() 2021-10-26 18:47:51 +02:00
Johannes Zellner 50ff6b99e0 More external ldap fixes after the test tests the correct thing 2021-10-26 18:04:25 +02:00
Johannes Zellner 26dbd50cf2 Ensure we don't crash if mount status does not include some strings 2021-10-26 14:54:56 +02:00
Johannes Zellner 84884b969e Fix external ldap bind
See "Losing context" https://masteringjs.io/tutorials/node/promisify
2021-10-26 11:55:58 +02:00
Girish Ramakrishnan 62174c5328 proxyauth: only log failed requests by default 2021-10-25 09:41:12 -07:00
50 changed files with 616 additions and 421 deletions
+32
View File
@@ -2367,3 +2367,35 @@
[7.0.1]
* Fix matrix wellKnown client migration
[7.0.2]
* mail: POP3 flag was not returned correctly
* external ldap: fix crash preventing users from logging in
* volumes: ensure we don't crash if mount status is unexpected
* backups: set default backup memory limit to 800
* users: allow admins to specify password recovery email
* retry startup tasks on database error
[7.0.3]
* support: fix remoe support not working for 'root' user
* Fix cog icon on app grid item hover for darkmode
* Disable password reset and impersonate button for self user instead of hiding them
* pop3: fix crash with auth of non-existent mailbox
* mail: fix direction field in eventlog of deferred mails
* mail: fix eventlog search
* mail: save message-id in eventlog
* backups: fix issue which resulted in incomplete backups when an app has backups disabled
* restore: do not redirect until mail data has been restored
* proxyauth: set viewport meta tag in login view
[7.0.4]
* Add password reveal button to login pages
* appstore: fix crash if account already registered
* Do not nuke all the logrotate configs on update
* Remove unused httpPaths from manifest
* cloudron-support: add option to reset cloudron.io account
* Fix flicker in login page
* Fix LE account key re-use issue in DO 1-click image
* mail: add non-tls ports for recvmail addon
* backups: fix issue where mail backups where not cleaned up
* notifications: fix automatic app update notifications
@@ -0,0 +1,9 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('DELETE FROM blobs WHERE id=?', [ 'dhparams' ], callback);
};
exports.down = function(db, callback) {
callback();
};
+31 -31
View File
@@ -246,7 +246,7 @@
},
"amdefine": {
"version": "1.0.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
"integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
"dev": true
},
@@ -320,7 +320,7 @@
},
"assert-plus": {
"version": "1.0.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
},
"async": {
@@ -391,7 +391,7 @@
},
"backoff": {
"version": "2.5.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz",
"integrity": "sha1-9hbtqdPktmuMp/ynn2lXIsX44m8=",
"requires": {
"precond": "0.2"
@@ -526,7 +526,7 @@
},
"buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
},
"buffer-fill": {
@@ -628,9 +628,9 @@
}
},
"cloudron-manifestformat": {
"version": "5.10.2",
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-5.10.2.tgz",
"integrity": "sha512-RU+uwwdPDXJ+zoORBVCkA6+mDEgyYlqtHjZWnvomdcemfHnxsE54A1r7+spm2Tz3tS1aVUQN7Vd5l1PD21PUzg==",
"version": "5.11.0",
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-5.11.0.tgz",
"integrity": "sha512-F+bOvZFNvmQQxpXYXNWtjkjEVpVvO7ZJlWj1RlDB9zyP31Uq1P9RdFBbvEih3PRx4NovKcENT28Rd59wCwWqpw==",
"requires": {
"cron": "^1.8.2",
"java-packagename-regex": "^1.0.0",
@@ -649,7 +649,7 @@
},
"code-point-at": {
"version": "1.1.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"dev": true
},
@@ -694,7 +694,7 @@
},
"concat-map": {
"version": "0.0.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"configstore": {
@@ -869,7 +869,7 @@
},
"core-util-is": {
"version": "1.0.2",
"resolved": false,
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"cron": {
@@ -902,7 +902,7 @@
},
"dashdash": {
"version": "1.14.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
"integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
"requires": {
"assert-plus": "^1.0.0"
@@ -1114,7 +1114,7 @@
},
"decamelize": {
"version": "1.2.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
},
"decamelize-keys": {
@@ -1374,7 +1374,7 @@
},
"ent": {
"version": "2.2.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz",
"integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0="
},
"env-paths": {
@@ -1441,7 +1441,7 @@
},
"expect.js": {
"version": "0.3.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/expect.js/-/expect.js-0.3.1.tgz",
"integrity": "sha1-sKWaDS7/VDdUTr8M6qYBWEHQm1s=",
"dev": true
},
@@ -2144,7 +2144,7 @@
},
"inflight": {
"version": "1.0.6",
"resolved": false,
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"requires": {
"once": "^1.3.0",
@@ -2168,7 +2168,7 @@
},
"is-arrayish": {
"version": "0.2.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
"dev": true
},
@@ -2245,12 +2245,12 @@
},
"isarray": {
"version": "1.0.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"isexe": {
"version": "2.0.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
},
"isstream": {
@@ -2723,14 +2723,14 @@
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
"resolved": false,
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
},
"minimist-options": {
@@ -3403,7 +3403,7 @@
},
"number-is-nan": {
"version": "1.0.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
"dev": true
},
@@ -3514,7 +3514,7 @@
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"path-key": {
@@ -3549,7 +3549,7 @@
},
"precond": {
"version": "0.2.3",
"resolved": false,
"resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz",
"integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw="
},
"pretty-bytes": {
@@ -3941,7 +3941,7 @@
},
"require-directory": {
"version": "2.1.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
},
"require-main-filename": {
@@ -4153,7 +4153,7 @@
},
"set-blocking": {
"version": "2.0.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
},
"setprototypeof": {
@@ -4176,7 +4176,7 @@
},
"signal-exit": {
"version": "3.0.2",
"resolved": false,
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
},
"smtp-connection": {
@@ -4395,7 +4395,7 @@
},
"stubs": {
"version": "3.0.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz",
"integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls="
},
"superagent": {
@@ -4794,7 +4794,7 @@
},
"util-deprecate": {
"version": "1.0.2",
"resolved": false,
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"utile": {
@@ -4865,7 +4865,7 @@
},
"verror": {
"version": "1.10.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
"integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
"requires": {
"assert-plus": "^1.0.0",
@@ -4888,7 +4888,7 @@
},
"which-module": {
"version": "2.0.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
},
"wide-align": {
@@ -4975,7 +4975,7 @@
},
"wrappy": {
"version": "1.0.2",
"resolved": false,
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"write-file-atomic": {
+1 -1
View File
@@ -18,7 +18,7 @@
"aws-sdk": "^2.936.0",
"basic-auth": "^2.0.1",
"body-parser": "^1.19.0",
"cloudron-manifestformat": "^5.10.2",
"cloudron-manifestformat": "^5.11.0",
"connect": "^3.7.0",
"connect-lastmile": "^2.1.1",
"connect-timeout": "^1.9.0",
+1 -1
View File
@@ -45,7 +45,7 @@ else
fi
# create docker network (while the infra code does this, most tests skip infra setup)
docker network create --subnet=172.18.0.0/16 --ip-range=172.18.0.0/20 cloudron || true
docker network create --subnet=172.18.0.0/16 --ip-range=172.18.0.0/20 --gateway 172.18.0.1 cloudron || true
# create the same mysql server version to test with
OUT=`docker inspect mysql-server` || true
+16 -7
View File
@@ -10,12 +10,13 @@ OUT="/tmp/cloudron-support.log"
LINE="\n========================================================\n"
CLOUDRON_SUPPORT_PUBLIC_KEY="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQVilclYAIu+ioDp/sgzzFz6YU0hPcRYY7ze/LiF/lC7uQqK062O54BFXTvQ3ehtFZCx3bNckjlT2e6gB8Qq07OM66De4/S/g+HJW4TReY2ppSPMVNag0TNGxDzVH8pPHOysAm33LqT2b6L/wEXwC6zWFXhOhHjcMqXvi8Ejaj20H1HVVcf/j8qs5Thkp9nAaFTgQTPu8pgwD8wDeYX1hc9d0PYGesTADvo6HF4hLEoEnefLw7PaStEbzk2fD3j7/g5r5HcgQQXBe74xYZ/1gWOX2pFNuRYOBSEIrNfJEjFJsqk3NR1+ZoMGK7j+AZBR4k0xbrmncQLcQzl6MMDzkp support@cloudron.io"
HELP_MESSAGE="
This script collects diagnostic information to help debug server related issues
This script collects diagnostic information to help debug server related issues.
Options:
--owner-login Login as owner
--enable-ssh Enable SSH access for the Cloudron support team
--help Show this message
--owner-login Login as owner
--enable-ssh Enable SSH access for the Cloudron support team
--reset-appstore-account Reset associated cloudron.io account
--help Show this message
"
# We require root
@@ -26,7 +27,7 @@ fi
enableSSH="false"
args=$(getopt -o "" -l "help,enable-ssh,admin-login,owner-login" -n "$0" -- "$@")
args=$(getopt -o "" -l "help,enable-ssh,admin-login,owner-login,reset-appstore-account" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
@@ -44,6 +45,15 @@ while true; do
echo "Login at https://${dashboard_domain} as ${admin_username} / ${admin_password} . This password may only be used once."
exit 0
;;
--reset-appstore-account)
echo -e "This will reset the Cloudron.io account associated with this Cloudron. Once reset, you can re-login with a different account in the Cloudron Dashboard. See https://docs.cloudron.io/appstore/#change-account for more information.\n"
read -e -p "Reset the Cloudron.io account? [y/N] " choice
[[ "$choice" != [Yy]* ]] && exit 1
mysql -uroot -ppassword -e "DELETE FROM box.settings WHERE name='cloudron_token';" 2>/dev/null
dashboard_domain=$(mysql -NB -uroot -ppassword -e "SELECT value FROM box.settings WHERE name='admin_fqdn'" 2>/dev/null)
echo "Account reset. Please re-login at https://${dashboard_domain}/#/appstore"
exit 0
;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
@@ -141,8 +151,7 @@ if [[ "${enableSSH}" == "true" ]]; then
fi
echo -n "Uploading information..."
# for some reason not using $(cat $OUT) will not contain newlines!?
paste_key=$(curl -X POST ${PASTEBIN}/documents --silent -d "$(cat $OUT)" | python3 -c "import sys, json; print(json.load(sys.stdin)['key'])")
paste_key=$(curl -X POST ${PASTEBIN}/documents --silent --data-binary "@$OUT" | python3 -c "import sys, json; print(json.load(sys.stdin)['key'])")
echo "Done"
echo ""
+1 -2
View File
@@ -38,7 +38,7 @@ systemctl restart apparmor
usermod ${USER} -a -G docker
# unbound (which starts after box code) relies on this interface to exist. dockerproxy also relies on this.
docker network create --subnet=172.18.0.0/16 --ip-range=172.18.0.0/20 cloudron || true
docker network create --subnet=172.18.0.0/16 --ip-range=172.18.0.0/20 --gateway 172.18.0.1 cloudron || true
mkdir -p "${BOX_DATA_DIR}"
mkdir -p "${APPS_DATA_DIR}"
@@ -146,7 +146,6 @@ log "Configuring logrotate"
if ! grep -q "^include ${PLATFORM_DATA_DIR}/logrotate.d" /etc/logrotate.conf; then
echo -e "\ninclude ${PLATFORM_DATA_DIR}/logrotate.d\n" >> /etc/logrotate.conf
fi
rm -f "${PLATFORM_DATA_DIR}/logrotate.d/"*
cp "${script_dir}/start/logrotate/"* "${PLATFORM_DATA_DIR}/logrotate.d/"
# logrotate files have to be owned by root, this is here to fixup existing installations where we were resetting the owner to yellowtent
+50 -29
View File
@@ -9,6 +9,7 @@ exports = module.exports = {
};
const assert = require('assert'),
blobs = require('./blobs.js'),
BoxError = require('./boxerror.js'),
crypto = require('crypto'),
debug = require('debug')('box:cert/acme2'),
@@ -31,7 +32,7 @@ const CA_PROD_DIRECTORY_URL = 'https://acme-v02.api.letsencrypt.org/directory',
function Acme2(options) {
assert.strictEqual(typeof options, 'object');
this.accountKeyPem = options.accountKeyPem; // Buffer
this.accountKeyPem = null; // Buffer .
this.email = options.email;
this.keyId = null;
this.caDirectory = options.prod ? CA_PROD_DIRECTORY_URL : CA_STAGING_DIRECTORY_URL;
@@ -87,10 +88,10 @@ Acme2.prototype.sendSignedRequest = async function (url, payload) {
let [error, response] = await safe(superagent.get(this.directory.newNonce).timeout(30000).ok(() => true));
if (error) throw new BoxError(BoxError.NETWORK_ERROR, `Network error sending signed request: ${error.message}`);
if (response.status !== 204) throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid response code when fetching nonce : ${response.status}`);
if (response.status !== 204) throw new BoxError(BoxError.ACME_ERROR, `Invalid response code when fetching nonce : ${response.status}`);
const nonce = response.headers['Replay-Nonce'.toLowerCase()];
if (!nonce) throw new BoxError(BoxError.EXTERNAL_ERROR, 'No nonce in response');
if (!nonce) throw new BoxError(BoxError.ACME_ERROR, 'No nonce in response');
debug('sendSignedRequest: using nonce %s for url %s', nonce, url);
@@ -128,23 +129,43 @@ Acme2.prototype.updateContact = async function (registrationUri) {
};
const result = await this.sendSignedRequest(registrationUri, JSON.stringify(payload));
if (result.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Failed to update contact. Expecting 200, got ${result.status} ${JSON.stringify(result.body)}`);
if (result.status !== 200) throw new BoxError(BoxError.ACME_ERROR, `Failed to update contact. Expecting 200, got ${result.status} ${JSON.stringify(result.body)}`);
debug(`updateContact: contact of user updated to ${this.email}`);
};
Acme2.prototype.registerUser = async function () {
async function generateAccountKey() {
const acmeAccountKey = safe.child_process.execSync('openssl genrsa 4096');
if (!acmeAccountKey) throw new BoxError(BoxError.OPENSSL_ERROR, `Could not generate acme account key: ${safe.error.message}`);
return acmeAccountKey;
}
Acme2.prototype.ensureAccount = async function () {
const payload = {
termsOfServiceAgreed: true
};
debug('registerUser: registering user');
debug('ensureAccount: registering user');
this.accountKeyPem = await blobs.get(blobs.ACME_ACCOUNT_KEY);
if (!this.accountKeyPem) {
debug('ensureAccount: generating new account keys');
this.accountKeyPem = await generateAccountKey();
await blobs.set(blobs.ACME_ACCOUNT_KEY, this.accountKeyPem);
}
let result = await this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload));
if (result.status === 403 && result.body.type === 'urn:ietf:params:acme:error:unauthorized') {
debug(`ensureAccount: key was revoked. ${result.status} ${JSON.stringify(result.body)}. generating new account key`);
this.accountKeyPem = await generateAccountKey();
await blobs.set(blobs.ACME_ACCOUNT_KEY, this.accountKeyPem);
result = await this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload));
}
const result = await this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload));
// 200 if already exists. 201 for new accounts
if (result.status !== 200 && result.status !== 201) return new BoxError(BoxError.EXTERNAL_ERROR, `Failed to register new account. Expecting 200 or 201, got ${result.status} ${JSON.stringify(result.body)}`);
if (result.status !== 200 && result.status !== 201) throw new BoxError(BoxError.ACME_ERROR, `Failed to register new account. Expecting 200 or 201, got ${result.status} ${JSON.stringify(result.body)}`);
debug(`registerUser: user registered keyid: ${result.headers.location}`);
debug(`ensureAccount: user registered keyid: ${result.headers.location}`);
this.keyId = result.headers.location;
@@ -165,15 +186,15 @@ Acme2.prototype.newOrder = async function (domain) {
const result = await this.sendSignedRequest(this.directory.newOrder, JSON.stringify(payload));
if (result.status === 403) throw new BoxError(BoxError.ACCESS_DENIED, `Forbidden sending new order: ${result.body.detail}`);
if (result.status !== 201) throw new BoxError(BoxError.EXTERNAL_ERROR, `Failed to send new order. Expecting 201, got ${result.statusCode} ${JSON.stringify(result.body)}`);
if (result.status !== 201) throw new BoxError(BoxError.ACME_ERROR, `Failed to send new order. Expecting 201, got ${result.statusCode} ${JSON.stringify(result.body)}`);
debug('newOrder: created order %s %j', domain, result.body);
const order = result.body, orderUrl = result.headers.location;
if (!Array.isArray(order.authorizations)) throw new BoxError(BoxError.EXTERNAL_ERROR, 'invalid authorizations in order');
if (typeof order.finalize !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, 'invalid finalize in order');
if (typeof orderUrl !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, 'invalid order location in order header');
if (!Array.isArray(order.authorizations)) throw new BoxError(BoxError.ACME_ERROR, 'invalid authorizations in order');
if (typeof order.finalize !== 'string') throw new BoxError(BoxError.ACME_ERROR, 'invalid finalize in order');
if (typeof orderUrl !== 'string') throw new BoxError(BoxError.ACME_ERROR, 'invalid order location in order header');
return { order, orderUrl };
};
@@ -189,14 +210,14 @@ Acme2.prototype.waitForOrder = async function (orderUrl) {
const result = await this.postAsGet(orderUrl);
if (result.status !== 200) {
debug(`waitForOrder: invalid response code getting uri ${result.status}`);
throw new BoxError(BoxError.EXTERNAL_ERROR, `Bad response code: ${result.status}`);
throw new BoxError(BoxError.ACME_ERROR, `Bad response when waiting for order. code: ${result.status}`);
}
debug('waitForOrder: status is "%s %j', result.body.status, result.body);
if (result.body.status === 'pending' || result.body.status === 'processing') throw new BoxError(BoxError.TRY_AGAIN, `Request is in ${result.body.status} state`);
if (result.body.status === 'pending' || result.body.status === 'processing') throw new BoxError(BoxError.ACME_ERROR, `Request is in ${result.body.status} state`);
else if (result.body.status === 'valid' && result.body.certificate) return result.body.certificate;
else throw new BoxError(BoxError.EXTERNAL_ERROR, `Unexpected status or invalid response: ${JSON.stringify(result.body)}`);
else throw new BoxError(BoxError.ACME_ERROR, `Unexpected status or invalid response when waiting for order: ${JSON.stringify(result.body)}`);
});
};
@@ -228,7 +249,7 @@ Acme2.prototype.notifyChallengeReady = async function (challenge) {
};
const result = await this.sendSignedRequest(challenge.url, JSON.stringify(payload));
if (result.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Failed to notify challenge. Expecting 200, got ${result.statusCode} ${JSON.stringify(result.body)}`);
if (result.status !== 200) throw new BoxError(BoxError.ACME_ERROR, `Failed to notify challenge. Expecting 200, got ${result.statusCode} ${JSON.stringify(result.body)}`);
};
Acme2.prototype.waitForChallenge = async function (challenge) {
@@ -242,14 +263,14 @@ Acme2.prototype.waitForChallenge = async function (challenge) {
const result = await this.postAsGet(challenge.url);
if (result.status !== 200) {
debug(`waitForChallenge: invalid response code getting uri ${result.status}`);
throw new BoxError(BoxError.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode);
throw new BoxError(BoxError.ACME_ERROR, `Bad response code when waiting for challenge : ${result.status}`);
}
debug(`waitForChallenge: status is "${result.body.status}" "${JSON.stringify(result.body)}"`);
if (result.body.status === 'pending') throw new BoxError(BoxError.TRY_AGAIN);
if (result.body.status === 'pending') throw new BoxError(BoxError.ACME_ERROR, 'Challenge is in pending state');
else if (result.body.status === 'valid') return;
else throw new BoxError(BoxError.EXTERNAL_ERROR, `Unexpected status: ${result.body.status}`);
else throw new BoxError(BoxError.ACME_ERROR, `Unexpected status when waiting for challenge: ${result.body.status}`);
});
};
@@ -267,7 +288,7 @@ Acme2.prototype.signCertificate = async function (domain, finalizationUrl, csrDe
const result = await this.sendSignedRequest(finalizationUrl, JSON.stringify(payload));
// 429 means we reached the cert limit for this domain
if (result.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Failed to sign certificate. Expecting 200, got ${result.status} ${JSON.stringify(result.body)}`);
if (result.status !== 200) throw new BoxError(BoxError.ACME_ERROR, `Failed to sign certificate. Expecting 200, got ${result.status} ${JSON.stringify(result.body)}`);
};
Acme2.prototype.createKeyAndCsr = async function (hostname, keyFilePath, csrFilePath) {
@@ -318,8 +339,8 @@ Acme2.prototype.downloadCertificate = async function (hostname, certUrl, certFil
debug('downloadCertificate: downloading certificate');
const result = await this.postAsGet(certUrl);
if (result.statusCode === 202) throw new BoxError(BoxError.TRY_AGAIN, 'Retry downloading certificate');
if (result.statusCode !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Failed to get cert. Expecting 200, got ${result.statusCode} ${JSON.stringify(result.body)}`);
if (result.statusCode === 202) throw new BoxError(BoxError.ACME_ERROR, 'Retry downloading certificate');
if (result.statusCode !== 200) throw new BoxError(BoxError.ACME_ERROR, `Failed to get cert. Expecting 200, got ${result.statusCode} ${JSON.stringify(result.body)}`);
const fullChainPem = result.body; // buffer
@@ -337,7 +358,7 @@ Acme2.prototype.prepareHttpChallenge = async function (hostname, domain, authori
debug('prepareHttpChallenge: challenges: %j', authorization);
let httpChallenges = authorization.challenges.filter(function(x) { return x.type === 'http-01'; });
if (httpChallenges.length === 0) throw new BoxError(BoxError.EXTERNAL_ERROR, 'no http challenges');
if (httpChallenges.length === 0) throw new BoxError(BoxError.ACME_ERROR, 'no http challenges');
let challenge = httpChallenges[0];
debug('prepareHttpChallenge: preparing for challenge %j', challenge);
@@ -386,7 +407,7 @@ Acme2.prototype.prepareDnsChallenge = async function (hostname, domain, authoriz
debug('prepareDnsChallenge: challenges: %j', authorization);
const dnsChallenges = authorization.challenges.filter(function(x) { return x.type === 'dns-01'; });
if (dnsChallenges.length === 0) throw new BoxError(BoxError.EXTERNAL_ERROR, 'no dns challenges');
if (dnsChallenges.length === 0) throw new BoxError(BoxError.ACME_ERROR, 'no dns challenges');
const challenge = dnsChallenges[0];
const keyAuthorization = this.getKeyAuthorization(challenge.token);
@@ -431,7 +452,7 @@ Acme2.prototype.prepareChallenge = async function (hostname, domain, authorizati
debug(`prepareChallenge: http: ${this.performHttpAuthorization}`);
const response = await this.postAsGet(authorizationUrl);
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid response code getting authorization : ${response.status}`);
if (response.status !== 200) throw new BoxError(BoxError.ACME_ERROR, `Invalid response code getting authorization : ${response.status}`);
const authorization = response.body;
@@ -464,7 +485,7 @@ Acme2.prototype.acmeFlow = async function (hostname, domain, paths) {
const { certFilePath, keyFilePath, csrFilePath, acmeChallengesDir } = paths;
await this.registerUser();
await this.ensureAccount();
const { order, orderUrl } = await this.newOrder(hostname);
for (let i = 0; i < order.authorizations.length; i++) {
@@ -491,11 +512,11 @@ Acme2.prototype.loadDirectory = async function () {
await promiseRetry({ times: 3, interval: 20000 }, async () => {
const response = await superagent.get(this.caDirectory).timeout(30000).ok(() => true);
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid response code when fetching directory : ${response.status}`);
if (response.status !== 200) throw new BoxError(BoxError.ACME_ERROR, `Invalid response code when fetching directory : ${response.status}`);
if (typeof response.body.newNonce !== 'string' ||
typeof response.body.newOrder !== 'string' ||
typeof response.body.newAccount !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid response body : ${response.body}`);
typeof response.body.newAccount !== 'string') throw new BoxError(BoxError.ACME_ERROR, `Invalid response body : ${response.body}`);
this.directory = response.body;
});
+58 -38
View File
@@ -141,7 +141,6 @@ exports = module.exports = {
const appstore = require('./appstore.js'),
appTaskManager = require('./apptaskmanager.js'),
assert = require('assert'),
AuditSource = require('./auditsource.js'),
backups = require('./backups.js'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
@@ -1036,7 +1035,13 @@ function mailboxNameForLocation(location, manifest) {
return 'noreply.app';
}
async function onTaskFinished(appId, installationState, taskId, error) {
async function onTaskFinished(error, appId, installationState, taskId, auditSource) {
assert(!error || typeof error === 'object');
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof installationState, 'string');
assert.strictEqual(typeof taskId, 'string');
assert.strictEqual(typeof auditSource, 'object');
const success = !error;
const errorMessage = error?.message || null;
@@ -1046,25 +1051,26 @@ async function onTaskFinished(appId, installationState, taskId, error) {
switch (installationState) {
case exports.ISTATE_PENDING_DATA_DIR_MIGRATION:
if (success) await safe(services.rebuildService('sftp', AuditSource.APPTASK), { debug });
if (success) await safe(services.rebuildService('sftp', auditSource), { debug });
break;
case exports.ISTATE_PENDING_UPDATE: {
const fromManifest = success ? task.args[1].updateConfig.manifest : app.manifest;
const toManifest = success ? app.manifest : task.args[1].updateConfig.manifest;
await eventlog.add(eventlog.ACTION_APP_UPDATE_FINISH, AuditSource.APPTASK, { app, toManifest, fromManifest, success, errorMessage });
await eventlog.add(eventlog.ACTION_APP_UPDATE_FINISH, auditSource, { app, toManifest, fromManifest, success, errorMessage });
break;
}
case exports.ISTATE_PENDING_BACKUP:
await eventlog.add(eventlog.ACTION_APP_BACKUP_FINISH, AuditSource.APPTASK, { app, success, errorMessage, backupId: task.result });
await eventlog.add(eventlog.ACTION_APP_BACKUP_FINISH, auditSource, { app, success, errorMessage, backupId: task.result });
break;
}
}
async function scheduleTask(appId, installationState, taskId) {
async function scheduleTask(appId, installationState, taskId, auditSource) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof installationState, 'string');
assert.strictEqual(typeof taskId, 'string');
assert.strictEqual(typeof auditSource, 'object');
const backupConfig = await settings.getBackupConfig();
@@ -1093,14 +1099,15 @@ async function scheduleTask(appId, installationState, taskId) {
await safe(update(appId, { taskId: null }), { debug });
}
await safe(onTaskFinished(appId, installationState, taskId, error), { debug }); // ignore error
await safe(onTaskFinished(error, appId, installationState, taskId, auditSource), { debug }); // ignore error
});
}
async function addTask(appId, installationState, task) {
async function addTask(appId, installationState, task, auditSource) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof installationState, 'string');
assert.strictEqual(typeof task, 'object'); // { args, values }
assert.strictEqual(typeof auditSource, 'object');
const { args, values } = task;
// TODO: match the SQL logic to match checkAppState. this means checking the error.installationState and installationState. Unfortunately, former is JSON right now
@@ -1114,7 +1121,7 @@ async function addTask(appId, installationState, task) {
if (updateError && updateError.reason === BoxError.NOT_FOUND) throw new BoxError(BoxError.BAD_STATE, 'Another task is scheduled for this app'); // could be because app went away OR a taskId exists
if (updateError) throw updateError;
if (scheduleNow) await safe(scheduleTask(appId, installationState, taskId), { debug }); // ignore error
if (scheduleNow) await safe(scheduleTask(appId, installationState, taskId, auditSource), { debug }); // ignore error
return taskId;
}
@@ -1162,6 +1169,11 @@ async function validateLocations(locations) {
return domainObjectMap;
}
async function getCount() {
const result = await database.query('SELECT COUNT(*) AS total FROM apps');
return result[0].total;
}
async function install(data, auditSource) {
assert(data && typeof data === 'object');
assert.strictEqual(typeof auditSource, 'object');
@@ -1237,6 +1249,8 @@ async function install(data, auditSource) {
const domainObjectMap = await validateLocations(locations);
if (settings.isDemo() && (await getCount() >= constants.DEMO_APP_LIMIT)) throw new BoxError(BoxError.BAD_STATE, 'Too many installed apps, please uninstall a few and try again');
const appId = uuid.v4();
debug('Will install app with id : ' + appId);
@@ -1272,7 +1286,7 @@ async function install(data, auditSource) {
requiredState: app.installationState
};
const taskId = await addTask(appId, app.installationState, task);
const taskId = await addTask(appId, app.installationState, task, auditSource);
const newApp = _.extend({}, _.omit(app, 'icon'), { appStoreId, manifest, location, domain, portBindings });
newApp.fqdn = dns.fqdn(newApp.location, domainObjectMap[newApp.domain]);
@@ -1380,7 +1394,7 @@ async function setMemoryLimit(app, memoryLimit, auditSource) {
args: {},
values: { memoryLimit }
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_RESIZE, task);
const taskId = await addTask(appId, exports.ISTATE_PENDING_RESIZE, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, memoryLimit, taskId });
@@ -1403,7 +1417,7 @@ async function setCpuShares(app, cpuShares, auditSource) {
args: {},
values: { cpuShares }
};
const taskId = await safe(addTask(appId, exports.ISTATE_PENDING_RESIZE, task));
const taskId = await safe(addTask(appId, exports.ISTATE_PENDING_RESIZE, task, auditSource));
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, cpuShares, taskId });
@@ -1423,7 +1437,7 @@ async function setMounts(app, mounts, auditSource) {
args: {},
values: { mounts }
};
const [taskError, taskId] = await safe(addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task));
const [taskError, taskId] = await safe(addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task, auditSource));
if (taskError && taskError.reason === BoxError.ALREADY_EXISTS) throw new BoxError(BoxError.CONFLICT, 'Duplicate mount points');
if (taskError) throw taskError;
@@ -1448,7 +1462,7 @@ async function setEnvironment(app, env, auditSource) {
args: {},
values: { env }
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task);
const taskId = await addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, env, taskId });
@@ -1471,7 +1485,7 @@ async function setDebugMode(app, debugMode, auditSource) {
args: {},
values: { debugMode }
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_DEBUG, task);
const taskId = await addTask(appId, exports.ISTATE_PENDING_DEBUG, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, debugMode, taskId });
@@ -1513,7 +1527,7 @@ async function setMailbox(app, data, auditSource) {
args: {},
values: { enableMailbox, mailboxName, mailboxDomain }
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task);
const taskId = await addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, mailboxName, mailboxDomain, taskId });
@@ -1548,7 +1562,7 @@ async function setInbox(app, data, auditSource) {
args: {},
values: { enableInbox, inboxName, inboxDomain }
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task);
const taskId = await addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, enableInbox, inboxName, inboxDomain, taskId });
@@ -1668,7 +1682,7 @@ async function setLocation(app, data, auditSource) {
},
values
};
let [taskError, taskId] = await safe(addTask(appId, exports.ISTATE_PENDING_LOCATION_CHANGE, task));
let [taskError, taskId] = await safe(addTask(appId, exports.ISTATE_PENDING_LOCATION_CHANGE, task, auditSource));
if (taskError && taskError.reason === BoxError.ALREADY_EXISTS) taskError = getDuplicateErrorDetails(error.message, locations, domainObjectMap, data.portBindings);
if (taskError) throw taskError;
@@ -1697,7 +1711,7 @@ async function setDataDir(app, dataDir, auditSource) {
args: { newDataDir: dataDir },
values: {}
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_DATA_DIR_MIGRATION, task);
const taskId = await addTask(appId, exports.ISTATE_PENDING_DATA_DIR_MIGRATION, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, dataDir, taskId });
@@ -1771,7 +1785,7 @@ async function updateApp(app, data, auditSource) {
args: { updateConfig },
values
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_UPDATE, task);
const taskId = await addTask(appId, exports.ISTATE_PENDING_UPDATE, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId, app, skipBackup, toManifest: manifest, fromManifest: app.manifest, force: data.force, taskId });
@@ -1890,7 +1904,7 @@ async function repair(app, data, auditSource) {
}
}
const taskId = await addTask(appId, errorState, task);
const taskId = await addTask(appId, errorState, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_REPAIR, auditSource, { app, taskId });
@@ -1937,7 +1951,7 @@ async function restore(app, backupId, auditSource) {
values
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_RESTORE, task);
const taskId = await addTask(appId, exports.ISTATE_PENDING_RESTORE, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { app: app, backupId: backupInfo.id, fromManifest: app.manifest, toManifest: backupInfo.manifest, taskId });
@@ -2003,7 +2017,7 @@ async function importApp(app, data, auditSource) {
},
values: {}
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_IMPORT, task);
const taskId = await addTask(appId, exports.ISTATE_PENDING_IMPORT, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_IMPORT, auditSource, { app: app, backupId, fromManifest: app.manifest, toManifest: app.manifest, taskId });
@@ -2025,7 +2039,7 @@ async function exportApp(app, data, auditSource) {
values: {}
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_BACKUP, task);
const taskId = await addTask(appId, exports.ISTATE_PENDING_BACKUP, task, auditSource);
return { taskId };
}
@@ -2117,7 +2131,7 @@ async function clone(app, data, user, auditSource) {
values: {},
requiredState: exports.ISTATE_PENDING_CLONE
};
const taskId = await addTask(newAppId, exports.ISTATE_PENDING_CLONE, task);
const taskId = await addTask(newAppId, exports.ISTATE_PENDING_CLONE, task, auditSource);
const newApp = _.extend({}, _.omit(obj, 'icon'), { appStoreId, manifest, location, domain, portBindings });
newApp.fqdn = dns.fqdn(newApp.location, domainObjectMap[newApp.domain]);
@@ -2144,7 +2158,7 @@ async function uninstall(app, auditSource) {
values: {},
requiredState: null // can run in any state, as long as no task is active
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_UNINSTALL, task);
const taskId = await addTask(appId, exports.ISTATE_PENDING_UNINSTALL, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId, app, taskId });
return { taskId };
@@ -2162,7 +2176,7 @@ async function start(app, auditSource) {
args: {},
values: { runState: exports.RSTATE_RUNNING }
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_START, task);
const taskId = await addTask(appId, exports.ISTATE_PENDING_START, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_START, auditSource, { appId, app, taskId });
return { taskId };
}
@@ -2179,7 +2193,7 @@ async function stop(app, auditSource) {
args: {},
values: { runState: exports.RSTATE_STOPPED }
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_STOP, task);
const taskId = await addTask(appId, exports.ISTATE_PENDING_STOP, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_STOP, auditSource, { appId, app, taskId });
@@ -2198,7 +2212,7 @@ async function restart(app, auditSource) {
args: {},
values: { runState: exports.RSTATE_RUNNING }
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_RESTART, task);
const taskId = await addTask(appId, exports.ISTATE_PENDING_RESTART, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_RESTART, auditSource, { appId, app, taskId });
@@ -2333,7 +2347,7 @@ async function backup(app, auditSource) {
args: {},
values: {}
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_BACKUP, task);
const taskId = await addTask(appId, exports.ISTATE_PENDING_BACKUP, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_BACKUP, auditSource, { app, appId, taskId });
return { taskId };
@@ -2347,8 +2361,9 @@ async function listBackups(app, page, perPage) {
return await backups.getByIdentifierAndStatePaged(app.id, backups.BACKUP_STATE_NORMAL, page, perPage);
}
async function restoreInstalledApps(options) {
async function restoreInstalledApps(options, auditSource) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof auditSource, 'object');
let apps = await list();
apps = apps.filter(app => app.installationState !== exports.ISTATE_ERROR); // remove errored apps. let them be 'repaired' by hand
@@ -2376,13 +2391,15 @@ async function restoreInstalledApps(options) {
debug(`restoreInstalledApps: marking ${app.fqdn} for restore using restore config ${JSON.stringify(restoreConfig)}`);
const [addTaskError, taskId] = await safe(addTask(app.id, installationState, task));
const [addTaskError, taskId] = await safe(addTask(app.id, installationState, task, auditSource));
if (addTaskError) debug(`restoreInstalledApps: error marking ${app.fqdn} for restore: ${JSON.stringify(addTaskError)}`);
else debug(`restoreInstalledApps: marked ${app.id} for restore with taskId ${taskId}`);
}
}
async function configureInstalledApps() {
async function configureInstalledApps(auditSource) {
assert.strictEqual(typeof auditSource, 'object');
let apps = await list();
apps = apps.filter(app => app.installationState !== exports.ISTATE_ERROR); // remove errored apps. let them be 'repaired' by hand
apps = apps.filter(app => app.installationState !== exports.ISTATE_PENDING_CONFIGURE); // safeguard against tasks being created non-stop if we crash on startup
@@ -2397,14 +2414,15 @@ async function configureInstalledApps() {
requireNullTaskId: false // ignore existing stale taskId
};
const [addTaskError, taskId] = await safe(addTask(app.id, exports.ISTATE_PENDING_CONFIGURE, task));
const [addTaskError, taskId] = await safe(addTask(app.id, exports.ISTATE_PENDING_CONFIGURE, task, auditSource));
if (addTaskError) debug(`configureInstalledApps: error marking ${app.fqdn} for configure: ${JSON.stringify(addTaskError)}`);
else debug(`configureInstalledApps: marked ${app.id} for re-configure with taskId ${taskId}`);
}
}
async function restartAppsUsingAddons(changedAddons) {
async function restartAppsUsingAddons(changedAddons, auditSource) {
assert(Array.isArray(changedAddons));
assert.strictEqual(typeof auditSource, 'object');
let apps = await list();
apps = apps.filter(app => app.manifest.addons && _.intersection(Object.keys(app.manifest.addons), changedAddons).length !== 0);
@@ -2424,14 +2442,16 @@ async function restartAppsUsingAddons(changedAddons) {
const [stopError] = await safe(docker.stopContainers(app.id));
if (stopError) debug(`restartAppsUsingAddons: error stopping ${app.fqdn}`, stopError);
const [addTaskError, taskId] = await safe(addTask(app.id, exports.ISTATE_PENDING_RESTART, task));
const [addTaskError, taskId] = await safe(addTask(app.id, exports.ISTATE_PENDING_RESTART, task, auditSource));
if (addTaskError) debug(`restartAppsUsingAddons: error marking ${app.fqdn} for restart: ${JSON.stringify(addTaskError)}`);
else debug(`restartAppsUsingAddons: marked ${app.id} for restart with taskId ${taskId}`);
}
}
// auto-restart app tasks after a crash
async function schedulePendingTasks() {
async function schedulePendingTasks(auditSource) {
assert.strictEqual(typeof auditSource, 'object');
debug('schedulePendingTasks: scheduling app tasks');
const result = await list();
@@ -2441,7 +2461,7 @@ async function schedulePendingTasks() {
debug(`schedulePendingTasks: schedule task for ${app.fqdn} ${app.id}: state=${app.installationState},taskId=${app.taskId}`);
await safe(scheduleTask(app.id, app.installationState, app.taskId), { debug }); // ignore error
await safe(scheduleTask(app.id, app.installationState, app.taskId, auditSource), { debug }); // ignore error
}
}
+1 -1
View File
@@ -107,7 +107,7 @@ async function registerUser(email, password) {
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 === 409) throw new BoxError(BoxError.ALREADY_EXISTS, 'account already exists');
if (response.status !== 201) throw new BoxError(BoxError.EXTERNAL_ERROR, `register status code: ${response.status}`);
}
+1 -2
View File
@@ -565,7 +565,6 @@ async function update(app, args, progressCallback) {
// app does not want these addons anymore
// FIXME: this does not handle option changes (like multipleDatabases)
const unusedAddons = _.omit(app.manifest.addons, Object.keys(updateConfig.manifest.addons));
const httpPathsChanged = app.manifest.httpPaths !== updateConfig.manifest.httpPaths;
const httpPortChanged = app.manifest.httpPort !== updateConfig.manifest.httpPort;
const proxyAuthChanged = !_.isEqual(safe.query(app.manifest, 'addons.proxyAuth'), safe.query(updateConfig.manifest, 'addons.proxyAuth'));
@@ -630,7 +629,7 @@ async function update(app, args, progressCallback) {
await startApp(app);
await progressCallback({ percent: 90, message: 'Configuring reverse proxy' });
if (httpPathsChanged || proxyAuthChanged || httpPortChanged) {
if (proxyAuthChanged || httpPortChanged) {
await reverseProxy.configureApp(app, AuditSource.APPTASK);
}
+1
View File
@@ -19,5 +19,6 @@ AuditSource.HEALTH_MONITOR = new AuditSource('healthmonitor');
AuditSource.EXTERNAL_LDAP_TASK = new AuditSource('externalldap');
AuditSource.EXTERNAL_LDAP_AUTO_CREATE = new AuditSource('externalldap');
AuditSource.APPTASK = new AuditSource('apptask');
AuditSource.PLATFORM = new AuditSource('platform');
exports = module.exports = AuditSource;
+27 -1
View File
@@ -152,6 +152,29 @@ async function cleanupAppBackups(backupConfig, referencedAppBackupIds, progressC
return removedAppBackupIds;
}
async function cleanupMailBackups(backupConfig, referencedAppBackupIds, progressCallback) {
assert.strictEqual(typeof backupConfig, 'object');
assert(Array.isArray(referencedAppBackupIds));
assert.strictEqual(typeof progressCallback, 'function');
let removedMailBackupIds = [];
const mailBackups = await backups.getByTypePaged(backups.BACKUP_TYPE_MAIL, 1, 1000);
applyBackupRetentionPolicy(mailBackups, _.extend({ keepLatest: true }, backupConfig.retentionPolicy), referencedAppBackupIds);
for (const mailBackup of mailBackups) {
if (mailBackup.keepReason) continue;
await progressCallback({ message: `Removing mail backup ${mailBackup.id}`});
removedMailBackupIds.push(mailBackup.id);
await cleanupBackup(backupConfig, mailBackup, progressCallback); // never errors
}
debug('cleanupMailBackups: done');
return removedMailBackupIds;
}
async function cleanupBoxBackups(backupConfig, progressCallback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof progressCallback, 'function');
@@ -267,6 +290,9 @@ async function run(progressCallback) {
await progressCallback({ percent: 10, message: 'Cleaning box backups' });
const { removedBoxBackupIds, referencedAppBackupIds } = await cleanupBoxBackups(backupConfig, progressCallback);
await progressCallback({ percent: 20, message: 'Cleaning mail backups' });
const removedMailBackupIds = await cleanupMailBackups(backupConfig, referencedAppBackupIds, progressCallback);
await progressCallback({ percent: 40, message: 'Cleaning app backups' });
const removedAppBackupIds = await cleanupAppBackups(backupConfig, referencedAppBackupIds, progressCallback);
@@ -276,5 +302,5 @@ async function run(progressCallback) {
await progressCallback({ percent: 90, message: 'Cleaning snapshots' });
await cleanupSnapshots(backupConfig);
return { removedBoxBackupIds, removedAppBackupIds, missingBackupIds };
return { removedBoxBackupIds, removedMailBackupIds, removedAppBackupIds, missingBackupIds };
}
+3 -2
View File
@@ -193,7 +193,7 @@ async function startBackupTask(auditSource) {
const backupConfig = await settings.getBackupConfig();
const memoryLimit = 'memoryLimit' in backupConfig ? Math.max(backupConfig.memoryLimit/1024/1024, 400) : 400;
const memoryLimit = 'memoryLimit' in backupConfig ? Math.max(backupConfig.memoryLimit/1024/1024, 800) : 800;
const taskId = await tasks.add(tasks.TASK_BACKUP, [ { /* options */ } ]);
@@ -268,11 +268,12 @@ async function startCleanupTask(auditSource) {
const taskId = await tasks.add(tasks.TASK_CLEAN_BACKUPS, []);
tasks.startTask(taskId, {}, (error, result) => { // result is { removedBoxBackupIds, removedAppBackupIds, missingBackupIds }
tasks.startTask(taskId, {}, (error, result) => { // result is { removedBoxBackupIds, removedAppBackupIds, removedMailBackupIds, missingBackupIds }
eventlog.add(eventlog.ACTION_BACKUP_CLEANUP_FINISH, auditSource, {
taskId,
errorMessage: error ? error.message : null,
removedBoxBackupIds: result ? result.removedBoxBackupIds : [],
removedMailBackupIds: result ? result.removedMailBackupIds : [],
removedAppBackupIds: result ? result.removedAppBackupIds : [],
missingBackupIds: result ? result.missingBackupIds : []
});
+6 -6
View File
@@ -1017,10 +1017,9 @@ async function downloadMail(restoreConfig, progressCallback) {
const dataLayout = new DataLayout(mailDataDir, []);
const startTime = new Date();
const backupConfig = restoreConfig.backupConfig || await settings.getBackupConfig();
const downloadAsync = util.promisify(download);
await downloadAsync(backupConfig, restoreConfig.backupId, restoreConfig.backupFormat, dataLayout, progressCallback);
await downloadAsync(restoreConfig.backupConfig, restoreConfig.backupId, restoreConfig.backupFormat, dataLayout, progressCallback);
debug('downloadMail: time: %s', (new Date() - startTime)/1000);
}
@@ -1037,13 +1036,14 @@ async function fullBackup(options, progressCallback) {
let step = 100/(allApps.length+3);
const appBackupIds = [];
for (const app of allApps) {
progressCallback({ percent: percent, message: `Backing up ${app.fqdn}` });
for (let i = 0; i < allApps.length; i++) {
const app = allApps[i];
progressCallback({ percent: percent, message: `Backing up ${app.fqdn} (${i+1}/${allApps.length})` });
percent += step;
if (!app.enableBackup) {
debug(`fullBackup: skipped backup ${app.fqdn}`);
return; // nothing to backup
debug(`fullBackup: skipped backup ${app.fqdn} (${i+1}/${allApps.length})`);
continue; // nothing to backup
}
const startTime = new Date();
+1 -58
View File
@@ -7,11 +7,8 @@ exports = module.exports = {
set,
del,
initSecrets,
ACME_ACCOUNT_KEY: 'acme_account_key',
ADDON_TURN_SECRET: 'addon_turn_secret',
DHPARAMS: 'dhparams',
SFTP_PUBLIC_KEY: 'sftp_public_key',
SFTP_PRIVATE_KEY: 'sftp_private_key',
@@ -21,13 +18,7 @@ exports = module.exports = {
};
const assert = require('assert'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
crypto = require('crypto'),
database = require('./database.js'),
debug = require('debug')('box:blobs'),
paths = require('./paths.js'),
safe = require('safetydance');
database = require('./database.js');
const BLOBS_FIELDS = [ 'id', 'value' ].join(',');
@@ -53,51 +44,3 @@ async function del(id) {
async function clear() {
await database.query('DELETE FROM blobs');
}
async function initSecrets() {
let acmeAccountKey = await get(exports.ACME_ACCOUNT_KEY);
if (!acmeAccountKey) {
acmeAccountKey = safe.child_process.execSync('openssl genrsa 4096');
if (!acmeAccountKey) throw new BoxError(BoxError.OPENSSL_ERROR, `Could not generate acme account key: ${safe.error.message}`);
await set(exports.ACME_ACCOUNT_KEY, acmeAccountKey);
}
let turnSecret = await get(exports.ADDON_TURN_SECRET);
if (!turnSecret) {
turnSecret = 'a' + crypto.randomBytes(15).toString('hex'); // prefix with a to ensure string starts with a letter
await set(exports.ADDON_TURN_SECRET, Buffer.from(turnSecret));
}
// TODO maybe skip this in tests if possible again
let dhparams = await get(exports.DHPARAMS);
if (!dhparams) {
debug('initSecrets: generating dhparams.pem. this takes forever');
if (constants.TEST) dhparams = safe.fs.readFileSync('/tmp/dhparams.pem');
if (!dhparams) dhparams = safe.child_process.execSync('openssl dhparam 2048');
if (!dhparams) throw new BoxError(BoxError.OPENSSL_ERROR, safe.error);
if (constants.TEST) safe.fs.writeFileSync('/tmp/dhparams.pem', dhparams);
if (!safe.fs.writeFileSync(paths.DHPARAMS_FILE, dhparams)) throw new BoxError(BoxError.FS_ERROR, `Could not save dhparams.pem: ${safe.error.message}`);
await set(exports.DHPARAMS, dhparams);
} else if (!safe.fs.existsSync(paths.DHPARAMS_FILE)) {
if (!safe.fs.writeFileSync(paths.DHPARAMS_FILE, dhparams)) throw new BoxError(BoxError.FS_ERROR, `Could not save dhparams.pem: ${safe.error.message}`);
}
let sftpPrivateKey = await get(exports.SFTP_PRIVATE_KEY);
let sftpPublicKey = await get(exports.SFTP_PUBLIC_KEY);
if (!sftpPrivateKey || !sftpPublicKey) {
debug('initSecrets: generate sftp keys');
if (constants.TEST) {
safe.fs.unlinkSync(paths.SFTP_PUBLIC_KEY_FILE);
safe.fs.unlinkSync(paths.SFTP_PRIVATE_KEY_FILE);
}
if (!safe.child_process.execSync(`ssh-keygen -m PEM -t rsa -f "${paths.SFTP_KEYS_DIR}/ssh_host_rsa_key" -q -N ""`)) throw new BoxError(BoxError.OPENSSL_ERROR, `Could not generate sftp ssh keys: ${safe.error.message}`);
sftpPublicKey = safe.fs.readFileSync(paths.SFTP_PUBLIC_KEY_FILE);
await set(exports.SFTP_PUBLIC_KEY, sftpPublicKey);
sftpPrivateKey = safe.fs.readFileSync(paths.SFTP_PRIVATE_KEY_FILE);
await set(exports.SFTP_PRIVATE_KEY, sftpPrivateKey);
} else if (!safe.fs.existsSync(paths.SFTP_PUBLIC_KEY_FILE) || !safe.fs.existsSync(paths.SFTP_PRIVATE_KEY_FILE)) {
if (!safe.fs.writeFileSync(paths.SFTP_PUBLIC_KEY_FILE, sftpPublicKey)) throw new BoxError(BoxError.FS_ERROR, `Could not save sftp public key: ${safe.error.message}`);
if (!safe.fs.writeFileSync(paths.SFTP_PRIVATE_KEY_FILE, sftpPrivateKey)) throw new BoxError(BoxError.FS_ERROR, `Could not save sftp private key: ${safe.error.message}`);
}
}
+2
View File
@@ -33,6 +33,7 @@ function BoxError(reason, errorOrMessage, override) {
}
util.inherits(BoxError, Error);
BoxError.ACCESS_DENIED = 'Access Denied';
BoxError.ACME_ERROR = 'Acme Error';
BoxError.ADDONS_ERROR = 'Addons Error';
BoxError.ALREADY_EXISTS = 'Already Exists';
BoxError.BAD_FIELD = 'Bad Field';
@@ -91,6 +92,7 @@ BoxError.toHttpError = function (error) {
case BoxError.INVALID_CREDENTIALS:
return new HttpError(412, error);
case BoxError.EXTERNAL_ERROR:
case BoxError.ACME_ERROR:
case BoxError.NETWORK_ERROR:
case BoxError.FS_ERROR:
case BoxError.MOUNT_ERROR:
+1 -21
View File
@@ -10,7 +10,6 @@ exports = module.exports = {
isRebootRequired,
onActivated,
onRestored,
setupDnsAndCert,
@@ -28,7 +27,6 @@ const apps = require('./apps.js'),
assert = require('assert'),
AuditSource = require('./auditsource.js'),
backups = require('./backups.js'),
backuptask = require('./backuptask.js'),
BoxError = require('./boxerror.js'),
branding = require('./branding.js'),
constants = require('./constants.js'),
@@ -58,7 +56,7 @@ const apps = require('./apps.js'),
const REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh');
async function initialize() {
safe(runStartupTasks(), { debug });
safe(runStartupTasks(), { debug }); // background
await notifyUpdate();
}
@@ -85,24 +83,6 @@ async function onActivated(options) {
await reverseProxy.writeDefaultConfig({ activated :true });
}
async function onRestored(options) {
assert.strictEqual(typeof options, 'object');
debug('onRestored: downloading mail');
const [error, results] = await safe(backups.getByIdentifierAndStatePaged(backups.BACKUP_IDENTIFIER_MAIL, backups.BACKUP_STATE_NORMAL, 1, 1));
if (error || results.length == 0) {
debug('startMail: could not find backup to restore', error, results);
} else {
debug(`startMail: downloading backup ${results[0].id}`);
const restoreConfig = { backupId: results[0].id, backupFormat: results[0].format };
// have to wait for download before starting mail container because we download as non-root user
await backuptask.downloadMail(restoreConfig, (progress) => debug(`startMail: ${progress.message}`));
}
await onActivated(options);
}
async function notifyUpdate() {
const version = safe.fs.readFileSync(paths.VERSION_FILE, 'utf8');
if (version === constants.VERSION) return;
+2 -1
View File
@@ -46,6 +46,7 @@ exports = module.exports = {
'io.github.sickchill.cloudronapp',
'to.couchpota.cloudronapp'
],
DEMO_APP_LIMIT: 20,
AUTOUPDATE_PATTERN_NEVER: 'never',
@@ -63,6 +64,6 @@ exports = module.exports = {
FOOTER: '&copy; %YEAR% &nbsp; [Cloudron](https://cloudron.io) &nbsp; &nbsp; &nbsp; [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)',
VERSION: process.env.BOX_ENV === 'cloudron' ? fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim() : '6.3.0-test'
VERSION: process.env.BOX_ENV === 'cloudron' ? fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim() : '7.0.0-test'
};
+2 -3
View File
@@ -15,7 +15,6 @@ exports = module.exports = {
const assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
child_process = require('child_process'),
constants = require('./constants.js'),
debug = require('debug')('box:database'),
mysql = require('mysql'),
@@ -89,7 +88,7 @@ async function query() {
let args = Array.prototype.slice.call(arguments);
args.push(function queryCallback(error, result) {
if (error) return reject(new BoxError(BoxError.DATABASE_ERROR, error, { code: error.code, sqlMessage: error.sqlMessage }));
if (error) return reject(new BoxError(BoxError.DATABASE_ERROR, error, { code: error.code, sqlMessage: error.sqlMessage || null }));
resolve(result);
});
@@ -107,7 +106,7 @@ async function transaction(queries) {
const releaseConnection = (error) => {
connection.release();
reject(new BoxError(BoxError.DATABASE_ERROR, error, { code: error.code, sqlMessage: error.sqlMessage }));
reject(new BoxError(BoxError.DATABASE_ERROR, error, { code: error.code, sqlMessage: error.sqlMessage || null }));
};
connection.beginTransaction(function (error) {
+6 -6
View File
@@ -254,9 +254,8 @@ async function search(identifier) {
return users;
}
async function maybeCreateUser(identifier, password) {
async function maybeCreateUser(identifier) {
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof password, 'string');
const externalLdapConfig = await settings.getExternalLdapConfig();
if (externalLdapConfig.provider === 'noop') throw new BoxError(BoxError.BAD_STATE, 'not enabled');
@@ -269,13 +268,14 @@ async function maybeCreateUser(identifier, password) {
const user = translateUser(externalLdapConfig, ldapUsers[0]);
if (!validUserRequirements(user)) throw new BoxError(BoxError.BAD_FIELD);
const [error] = await safe(users.add(user.email, { username: user.username, password: null, displayName: user.displayName, source: 'ldap' }, AuditSource.EXTERNAL_LDAP_AUTO_CREATE));
const [error, userId] = await safe(users.add(user.email, { username: user.username, password: null, displayName: user.displayName, source: 'ldap' }, AuditSource.EXTERNAL_LDAP_AUTO_CREATE));
if (error) {
debug(`maybeCreateUser: failed to auto create user ${user.username}`, error);
throw error;
}
return user;
// fetch the full record
return await users.get(userId);
}
async function verifyPassword(user, password) {
@@ -291,7 +291,7 @@ async function verifyPassword(user, password) {
const client = await getClient(externalLdapConfig, { bind: false });
const [error] = await safe(util.promisify(client.bind)(ldapUsers[0].dn, password));
const [error] = await safe(util.promisify(client.bind.bind(client))(ldapUsers[0].dn, password));
client.unbind();
if (error instanceof ldap.InvalidCredentialsError) throw new BoxError(BoxError.INVALID_CREDENTIALS);
if (error) throw new BoxError(BoxError.EXTERNAL_ERROR, error);
@@ -337,7 +337,7 @@ async function syncUsers(externalLdapConfig, progressCallback) {
debug(`syncUsers: [adding user] username=${ldapUser.username} email=${ldapUser.email} displayName=${ldapUser.displayName}`);
const [userAddError] = await safe(users.add(ldapUser.email, { username: ldapUser.username, password: null, displayName: ldapUser.displayName, source: 'ldap' }, AuditSource.EXTERNAL_LDAP_TASK));
if (userAddError) debug('syncUsers: Failed to create user', ldapUser, userAddError.message);
if (userAddError) debug('syncUsers: Failed to create user', ldapUser, userAddError);
} else if (user.source !== 'ldap') {
debug(`syncUsers: [mapping user] username=${ldapUser.username} email=${ldapUser.email} displayName=${ldapUser.displayName}`);
+2 -2
View File
@@ -20,8 +20,8 @@ exports = module.exports = {
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:4.1.1@sha256:86e4e2f4fd43809efca7c9cb1def4d7608cf36cb9ea27052f9b64da4481db43a' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:4.0.2@sha256:9df297ccc3370f38c54f8d614e214e082b363777cd1c6c9522e29663cc8f5362' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:3.0.4@sha256:5c60de75d078ae609da5565f32dcd91030f45907e945756cc976ff207b8c6199' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:3.4.0@sha256:5f9795cad3634c177f789019c12f8d53a6481de3cc627fb5a4866ce085006507' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:3.5.0@sha256:e05d328ea1afa94e31e2eae9b035ff2edde8b90cae902ca49e06053b5bdb5fde' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:3.0.1@sha256:bed9f6b5d06fe2c5289e895e806cfa5b74ad62993d705be55d4554a67d128029' },
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:3.4.1@sha256:13e066fcd52230f23244c16fdd2f7aa447a91e98ff703269f48b1afe3b393e31' }
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:3.4.2@sha256:810306478c3dac7caa7497e5f6381cc7ce2f68aafda849a4945d39a67cc04bc1' }
}
};
+27 -87
View File
@@ -278,40 +278,6 @@ async function mailboxSearch(req, res, next) {
} else {
res.end();
}
} else if (req.dn.rdns[0].attrs.domain) { // legacy ldap mailbox search for old sogo
const domain = req.dn.rdns[0].attrs.domain.value.toLowerCase();
let [error, mailboxes] = await safe(mail.listMailboxes(domain, 1, 1000));
if (error) return next(new ldap.OperationsError(error.toString()));
mailboxes = mailboxes.filter(m => m.active);
let results = [];
// send mailbox objects
mailboxes.forEach(function (mailbox) {
var dn = ldap.parseDN(`cn=${mailbox.name}@${domain},domain=${domain},ou=mailboxes,dc=cloudron`);
var obj = {
dn: dn.toString(),
attributes: {
objectclass: ['mailbox'],
objectcategory: 'mailbox',
cn: `${mailbox.name}@${domain}`,
uid: `${mailbox.name}@${domain}`,
mail: `${mailbox.name}@${domain}`
}
};
// ensure all filter values are also lowercase
var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null);
if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString()));
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) {
results.push(obj);
}
});
finalSend(results, req, res, next);
} else { // new sogo
let [error, mailboxes] = await safe(mail.listAllMailboxes(1, 1000));
if (error) return next(new ldap.OperationsError(error.toString()));
@@ -500,36 +466,6 @@ async function verifyMailboxPassword(mailbox, password) {
return verifiedUser;
}
async function authenticateUserMailbox(req, res, next) {
debug('user mailbox auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
const email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
const parts = email.split('@');
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
const [error, domain] = await safe(mail.getDomain(parts[1]));
if (error) return next(new ldap.OperationsError(error.message));
if (!domain) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (!domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
const [getMailboxError, mailbox] = await safe(mail.getMailbox(parts[0], parts[1]));
if (getMailboxError) return next(new ldap.OperationsError(getMailboxError.message));
if (!mailbox) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (!mailbox.active) return next(new ldap.NoSuchObjectError(req.dn.toString()));
const [verifyError, result] = await safe(verifyMailboxPassword(mailbox, req.credentials || ''));
if (verifyError && verifyError.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (verifyError && verifyError.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
if (verifyError) return next(new ldap.OperationsError(verifyError.message));
eventlog.upsertLoginEvent(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) });
res.end();
}
async function authenticateSftp(req, res, next) {
debug('sftp auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
@@ -607,46 +543,43 @@ async function verifyAppMailboxPassword(serviceId, username, password) {
if (!result.some(r => r.name.endsWith(`${pattern}_USERNAME`) && r.value === username)) throw new BoxError(BoxError.INVALID_CREDENTIALS);
}
async function authenticateMail(req, res, next) {
debug('authenticateMail: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
async function authenticateService(serviceId, dn, req, res, next) {
debug(`authenticateService: ${req.dn.toString()} (from ${req.connection.ldap.id})`);
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (!dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(dn.toString()));
const email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
const email = dn.rdns[0].attrs.cn.value.toLowerCase();
const parts = email.split('@');
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(dn.toString()));
const knownServices = [ 'msa', 'imap', 'pop3', 'sieve' ];
const serviceId = req.dn.rdns[1].attrs.ou.value.toLowerCase();
const knownServices = [ 'msa', 'imap', 'pop3', 'sieve', 'sogo' ];
if (!knownServices.includes(serviceId)) return next(new ldap.OperationsError('Invalid DN. Unknown service'));
const [error, domain] = await safe(mail.getDomain(parts[1]));
if (error) return next(new ldap.OperationsError(error.message));
if (!domain) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (!domain) return next(new ldap.NoSuchObjectError(dn.toString()));
const serviceNeedsMailbox = serviceId === 'imap' || serviceId === 'sieve' || serviceId === 'pop3';
if (serviceNeedsMailbox && !domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
const serviceNeedsMailbox = serviceId === 'imap' || serviceId === 'sieve' || serviceId === 'pop3' || serviceId === 'sogo';
if (serviceNeedsMailbox && !domain.enabled) return next(new ldap.NoSuchObjectError(dn.toString()));
const [getMailboxError, mailbox] = await safe(mail.getMailbox(parts[0], parts[1]));
if (getMailboxError) return next(new ldap.OperationsError(getMailboxError.message));
if (serviceId === 'pop3' && !mailbox.enablePop3) return next(new ldap.OperationsError('POP3 is not enabled'));
const [appPasswordError] = await safe(verifyAppMailboxPassword(serviceId, email, req.credentials || ''));
if (!appPasswordError) { // validated as app
if (serviceNeedsMailbox && (!mailbox || !mailbox.active)) return next(new ldap.NoSuchObjectError(req.dn.toString()));
return res.end();
if (serviceNeedsMailbox) {
if (!mailbox || !mailbox.active) return next(new ldap.NoSuchObjectError(dn.toString()));
if (serviceId === 'pop3' && !mailbox.enablePop3) return next(new ldap.OperationsError('POP3 is not enabled'));
}
if (appPasswordError && appPasswordError.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
if (appPasswordError && appPasswordError.reason !== BoxError.NOT_FOUND) return next(new ldap.OperationsError(appPasswordError.message));
const [appPasswordError] = await safe(verifyAppMailboxPassword(serviceId, email, req.credentials || ''));
if (!appPasswordError) return res.end(); // validated as app
// user password check requires an active mailbox
if (!mailbox) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (!mailbox.active) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (appPasswordError.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(dn.toString()));
if (appPasswordError.reason !== BoxError.NOT_FOUND) return next(new ldap.OperationsError(appPasswordError.message));
if (!mailbox || !mailbox.active) return next(new ldap.NoSuchObjectError(dn.toString())); // user auth requires active mailbox
const [verifyError, result] = await safe(verifyMailboxPassword(mailbox, req.credentials || ''));
if (verifyError && verifyError.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (verifyError && verifyError.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
if (verifyError && verifyError.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(dn.toString()));
if (verifyError && verifyError.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(dn.toString()));
if (verifyError) return next(new ldap.OperationsError(verifyError.message));
eventlog.upsertLoginEvent(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) });
@@ -654,6 +587,11 @@ async function authenticateMail(req, res, next) {
res.end();
}
async function authenticateMail(req, res, next) {
if (!req.dn.rdns[1].attrs.ou) return next(new ldap.NoSuchObjectError(req.dn.toString()));
await authenticateService(req.dn.rdns[1].attrs.ou.value.toLowerCase(), req.dn, req, res, next);
}
async function start() {
const logger = {
trace: NOOP,
@@ -676,7 +614,9 @@ async function start() {
// http://www.ietf.org/proceedings/43/I-D/draft-srivastava-ldap-mail-00.txt
gServer.search('ou=mailboxes,dc=cloudron', mailboxSearch); // haraka (address translation), dovecot (LMTP), sogo (mailbox search)
gServer.bind('ou=mailboxes,dc=cloudron', authenticateUserMailbox); // apps like sogo can use domain=${domain} to authenticate a mailbox
gServer.bind('ou=mailboxes,dc=cloudron', async function (req, res, next) { // used for sogo only. this route happens only at sogo login time. after that it will use imap ldap route
await authenticateService('sogo', req.dn, req, res, next);
});
gServer.search('ou=mailaliases,dc=cloudron', mailAliasSearch); // haraka
gServer.search('ou=mailinglists,dc=cloudron', mailingListSearch); // haraka
+3 -2
View File
@@ -106,6 +106,7 @@ const assert = require('assert'),
const DNS_OPTIONS = { timeout: 5000 };
const REMOVE_MAILBOX_CMD = path.join(__dirname, 'scripts/rmmailbox.sh');
// if you add a field here, listMailboxes has to be updated
const MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'ownerType', 'aliasName', 'aliasDomain', 'creationTime', 'membersJson', 'membersOnly', 'domain', 'active', 'enablePop3' ].join(',');
const MAILDB_FIELDS = [ 'domain', 'enabled', 'mailFromValidation', 'catchAllJson', 'relayJson', 'dkimKeyJson', 'dkimSelector', 'bannerJson' ].join(',');
@@ -1080,7 +1081,7 @@ async function listMailboxes(domain, search, page, perPage) {
const escapedSearch = mysql.escape('%' + search + '%'); // this also quotes the string
const searchQuery = search ? ` HAVING (name LIKE ${escapedSearch} OR aliasNames LIKE ${escapedSearch} OR aliasDomains LIKE ${escapedSearch})` : ''; // having instead of where because of aggregated columns use
const query = 'SELECT m1.name AS name, m1.domain AS domain, m1.ownerId AS ownerId, m1.ownerType as ownerType, m1.active as active, JSON_ARRAYAGG(m2.name) AS aliasNames, JSON_ARRAYAGG(m2.domain) AS aliasDomains '
const query = 'SELECT m1.name AS name, m1.domain AS domain, m1.ownerId AS ownerId, m1.ownerType as ownerType, m1.active as active, JSON_ARRAYAGG(m2.name) AS aliasNames, JSON_ARRAYAGG(m2.domain) AS aliasDomains, m1.enablePop3 AS enablePop3 '
+ ` FROM (SELECT * FROM mailboxes WHERE type='${exports.TYPE_MAILBOX}') AS m1`
+ ` LEFT JOIN (SELECT * FROM mailboxes WHERE type='${exports.TYPE_ALIAS}') AS m2`
+ ' ON m1.name=m2.aliasName AND m1.domain=m2.aliasDomain AND m1.ownerId=m2.ownerId'
@@ -1101,7 +1102,7 @@ async function listAllMailboxes(page, perPage) {
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
const query = 'SELECT m1.name AS name, m1.domain AS domain, m1.ownerId AS ownerId, m1.ownerType as ownerType, m1.active as active, JSON_ARRAYAGG(m2.name) AS aliasNames, JSON_ARRAYAGG(m2.domain) AS aliasDomains '
const query = 'SELECT m1.name AS name, m1.domain AS domain, m1.ownerId AS ownerId, m1.ownerType as ownerType, m1.active as active, JSON_ARRAYAGG(m2.name) AS aliasNames, JSON_ARRAYAGG(m2.domain) AS aliasDomains, m1.enablePop3 AS enablePop3 '
+ ` FROM (SELECT * FROM mailboxes WHERE type='${exports.TYPE_MAILBOX}') AS m1`
+ ` LEFT JOIN (SELECT * FROM mailboxes WHERE type='${exports.TYPE_ALIAS}') AS m2`
+ ' ON m1.name=m2.aliasName AND m1.domain=m2.aliasDomain AND m1.ownerId=m2.ownerId'
+9 -6
View File
@@ -61,7 +61,7 @@ async function sendMail(mailOptions) {
}
}));
const transportSendMail = util.promisify(transport.sendMail).bind(transport);
const transportSendMail = util.promisify(transport.sendMail.bind(transport));
const [error] = await safe(transportSendMail(mailOptions));
if (error) throw new BoxError(BoxError.EXTERNAL_ERROR, error);
debug(`Email "${mailOptions.subject}" sent to ${mailOptions.to}`);
@@ -89,9 +89,10 @@ function render(templateFile, params, translationAssets) {
return content;
}
async function sendInvite(user, invitor, inviteLink) {
async function sendInvite(user, invitor, email, inviteLink) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof invitor, 'object');
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof inviteLink, 'string');
const mailConfig = await getMailConfig();
@@ -108,7 +109,7 @@ async function sendInvite(user, invitor, inviteLink) {
const mailOptions = {
from: mailConfig.notificationFrom,
to: user.fallbackEmail,
to: email,
subject: ejs.render(translation.translate('{{ welcomeEmail.subject }}', translationAssets.translations || {}, translationAssets.fallback || {}), { cloudron: mailConfig.cloudronName }),
text: render('welcome_user-text.ejs', templateData, translationAssets),
html: render('welcome_user-html.ejs', templateData, translationAssets)
@@ -143,7 +144,7 @@ async function sendNewLoginLocation(user, loginLocation) {
const mailOptions = {
from: mailConfig.notificationFrom,
to: user.fallbackEmail,
to: user.email,
subject: ejs.render(translation.translate('{{ newLoginEmail.subject }}', translationAssets.translations || {}, translationAssets.fallback || {}), { cloudron: mailConfig.cloudronName }),
text: render('new_login_location-text.ejs', templateData, translationAssets),
html: render('new_login_location-html.ejs', templateData, translationAssets)
@@ -152,8 +153,10 @@ async function sendNewLoginLocation(user, loginLocation) {
await sendMail(mailOptions);
}
async function passwordReset(user, resetLink) {
async function passwordReset(user, email, resetLink) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof resetLink, 'string');
const mailConfig = await getMailConfig();
const translationAssets = await translation.getTranslations();
@@ -167,7 +170,7 @@ async function passwordReset(user, resetLink) {
const mailOptions = {
from: mailConfig.notificationFrom,
to: user.fallbackEmail,
to: email,
subject: ejs.render(translation.translate('{{ passwordResetEmail.subject }}', translationAssets.translations || {}, translationAssets.fallback || {}), { cloudron: mailConfig.cloudronName }),
text: render('password_reset-text.ejs', templateData, translationAssets),
html: render('password_reset-html.ejs', templateData, translationAssets)
+1 -1
View File
@@ -150,7 +150,7 @@ async function getStatus(mountType, hostPath) {
let start = -1, end = -1; // start and end of error message block
for (let idx = lines.length - 1; idx >= 0; idx--) { // reverse
const line = lines[idx];
const match = line['SYSLOG_IDENTIFIER'] === 'mount' || line['_EXE'].includes('mount') || line['_COMM'].includes('mount');
const match = line['SYSLOG_IDENTIFIER'] === 'mount' || (line['_EXE'] && line['_EXE'].includes('mount')) || (line['_COMM'] && line['_COMM'].includes('mount'));
if (match) {
if (end === -1) end = idx;
start = idx;
+4 -8
View File
@@ -87,7 +87,10 @@ server {
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256;
ssl_prefer_server_ciphers off;
<% if (endpoint !== 'ip' && endpoint !== 'setup') { -%>
# dhparams is generated only after dns setup
ssl_dhparam /home/yellowtent/platformdata/dhparams.pem;
<% } -%>
add_header Strict-Transport-Security "max-age=63072000";
<% if ( ocsp ) { -%>
@@ -214,7 +217,7 @@ server {
client_max_body_size 1m;
}
location ~ ^/api/v1/(developer|session)/login$ {
location ~ ^/api/v1/cloudron/login$ {
proxy_pass http://127.0.0.1:3000;
client_max_body_size 1m;
limit_req zone=admin_login burst=5;
@@ -301,13 +304,6 @@ server {
}
<% } %>
<% Object.keys(httpPaths).forEach(function (path) { -%>
location "<%= path %>" {
# the trailing / will replace part of the original URI matched by the location.
proxy_pass http://<%= ip %>:<%= httpPaths[path] %>/;
}
<% }); %>
<% } else if ( endpoint === 'redirect' ) { %>
location / {
# redirect everything to the app. this is temporary because there is no way
-3
View File
@@ -246,9 +246,6 @@ async function onEvent(id, action, source, data) {
assert.strictEqual(typeof source, 'object');
assert.strictEqual(typeof data, 'object');
// external ldap syncer does not generate notifications - FIXME username might be an issue here
if (source.username === AuditSource.EXTERNAL_LDAP_TASK.username) return;
switch (action) {
case eventlog.ACTION_APP_OOM:
return await oomEvent(id, data.containerId, data.app, data.addonName, data.event);
+27 -14
View File
@@ -10,7 +10,10 @@ exports = module.exports = {
const apps = require('./apps.js'),
assert = require('assert'),
AuditSource = require('./auditsource.js'),
BoxError = require('./boxerror.js'),
debug = require('debug')('box:platform'),
delay = require('delay'),
fs = require('fs'),
infra = require('./infra_version.js'),
locker = require('./locker.js'),
@@ -37,9 +40,7 @@ async function start(options) {
// short-circuit for the restart case
if (_.isEqual(infra, existingInfra)) {
debug('platform is uptodate at version %s', infra.version);
onPlatformReady(false /* !infraChanged */);
await onPlatformReady(false /* !infraChanged */);
return;
}
@@ -48,15 +49,27 @@ async function start(options) {
const error = locker.lock(locker.OP_PLATFORM_START);
if (error) throw error;
if (existingInfra.version !== infra.version) await removeAllContainers();
if (existingInfra.version === 'none') await volumes.mountAll(); // when restoring, mount all volumes
await markApps(existingInfra, options); // mark app state before we start addons. this gives the db import logic a chance to mark an app as errored
await services.startServices(existingInfra);
await fs.promises.writeFile(paths.INFRA_VERSION_FILE, JSON.stringify(infra, null, 4));
for (let attempt = 0; attempt < 5; attempt++) {
try {
if (existingInfra.version !== infra.version) await removeAllContainers();
if (existingInfra.version === 'none') await volumes.mountAll(); // when restoring, mount all volumes
await markApps(existingInfra, options); // mark app state before we start addons. this gives the db import logic a chance to mark an app as errored
await services.startServices(existingInfra);
await fs.promises.writeFile(paths.INFRA_VERSION_FILE, JSON.stringify(infra, null, 4));
break;
} catch (error) {
// for some reason, mysql arbitrary restarts making startup tasks fail. this makes the box update stuck
// LOST is when existing connection breaks. REFUSED is when new connection cannot connect at all
const retry = error.reason === BoxError.DATABASE_ERROR && (error.code === 'PROTOCOL_CONNECTION_LOST' || error.code === 'ECONNREFUSED');
debug(`Failed to start services. retry=${retry} (attempt ${attempt}): ${error.message}`);
if (!retry) break;
await delay(10000);
}
}
locker.unlock(locker.OP_PLATFORM_START);
onPlatformReady(true /* infraChanged */); // background
await onPlatformReady(true /* infraChanged */);
}
async function stopAllTasks() {
@@ -67,9 +80,9 @@ async function onPlatformReady(infraChanged) {
debug(`onPlatformReady: platform is ready. infra changed: ${infraChanged}`);
exports._isReady = true;
if (infraChanged) await pruneInfraImages();
if (infraChanged) await safe(pruneInfraImages(), { debug }); // ignore error
await apps.schedulePendingTasks();
await apps.schedulePendingTasks(AuditSource.PLATFORM);
}
async function pruneInfraImages() {
@@ -111,11 +124,11 @@ async function markApps(existingInfra, options) {
if (existingInfra.version === 'none') { // cloudron is being restored from backup
debug('markApps: restoring installed apps');
await apps.restoreInstalledApps(options);
await apps.restoreInstalledApps(options, AuditSource.PLATFORM);
} else if (existingInfra.version !== infra.version) {
debug('markApps: reconfiguring installed apps');
reverseProxy.removeAppConfigs(); // should we change the cert location, nginx will not start
await apps.configureInstalledApps();
await apps.configureInstalledApps(AuditSource.PLATFORM);
} else {
let changedAddons = [];
if (infra.images.mysql.tag !== existingInfra.images.mysql.tag) changedAddons.push('mysql');
@@ -126,7 +139,7 @@ async function markApps(existingInfra, options) {
if (changedAddons.length) {
// restart apps if docker image changes since the IP changes and any "persistent" connections fail
debug(`markApps: changedAddons: ${JSON.stringify(changedAddons)}`);
await apps.restartAppsUsingAddons(changedAddons);
await apps.restartAppsUsingAddons(changedAddons, AuditSource.PLATFORM);
} else {
debug('markApps: apps are already uptodate');
}
+21 -2
View File
@@ -17,6 +17,7 @@ const assert = require('assert'),
debug = require('debug')('box:provision'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
mail = require('./mail.js'),
mounts = require('./mounts.js'),
reverseProxy = require('./reverseproxy.js'),
@@ -24,6 +25,7 @@ const assert = require('assert'),
semver = require('semver'),
settings = require('./settings.js'),
sysinfo = require('./sysinfo.js'),
paths = require('./paths.js'),
users = require('./users.js'),
tld = require('tldjs'),
tokens = require('./tokens.js'),
@@ -49,6 +51,14 @@ function setProgress(task, message, callback) {
if (callback) callback();
}
async function ensureDhparams() {
if (fs.existsSync(paths.DHPARAMS_FILE)) return;
debug('ensureDhparams: generating dhparams');
const dhparams = safe.child_process.execSync('openssl dhparam -dsaparam 2048');
if (!dhparams) throw new BoxError(BoxError.OPENSSL_ERROR, safe.error);
if (!safe.fs.writeFileSync(paths.DHPARAMS_FILE, dhparams)) throw new BoxError(BoxError.FS_ERROR, `Could not save dhparams.pem: ${safe.error.message}`);
}
async function unprovision() {
// TODO: also cancel any existing configureWebadmin task
await settings.setDashboardLocation('', '');
@@ -62,6 +72,7 @@ async function setupTask(domain, auditSource) {
try {
await cloudron.setupDnsAndCert(constants.DASHBOARD_LOCATION, domain, auditSource, (progress) => setProgress('setup', progress.message));
await ensureDhparams();
await cloudron.setDashboardDomain(domain, auditSource);
setProgress('setup', 'Done'),
await eventlog.add(eventlog.ACTION_PROVISION, auditSource, {});
@@ -150,8 +161,16 @@ async function restoreTask(backupConfig, backupId, sysinfoConfig, options, audit
assert.strictEqual(typeof auditSource, 'object');
try {
setProgress('restore', 'Downloading backup');
setProgress('restore', 'Downloading box backup');
await backuptask.restore(backupConfig, backupId, (progress) => setProgress('restore', progress.message));
setProgress('restore', 'Downloading mail backup');
const mailBackups = await backups.getByIdentifierAndStatePaged(backups.BACKUP_IDENTIFIER_MAIL, backups.BACKUP_STATE_NORMAL, 1, 1);
if (mailBackups.length === 0) throw new BoxError(BoxError.NOT_FOUND, 'mail backup not found');
const mailRestoreConfig = { backupConfig, backupId: mailBackups[0].id, backupFormat: mailBackups[0].format };
await backuptask.downloadMail(mailRestoreConfig, (progress) => setProgress('restore', progress.message));
await ensureDhparams();
await settings.setSysinfoConfig(sysinfoConfig);
await reverseProxy.restoreFallbackCertificates();
@@ -161,7 +180,7 @@ async function restoreTask(backupConfig, backupId, sysinfoConfig, options, audit
await settings.setBackupCredentials(backupConfig); // update just the credentials and not the policy and flags
await eventlog.add(eventlog.ACTION_RESTORE, auditSource, { backupId });
setImmediate(() => safe(cloudron.onRestored(options), { debug }));
setImmediate(() => safe(cloudron.onActivated(options), { debug }));
} catch (error) {
gProvisionStatus.restore.errorMessage = error ? error.message : '';
}
+21 -2
View File
@@ -24,6 +24,7 @@ const apps = require('./apps.js'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
speakeasy = require('speakeasy'),
translation = require('./translation.js'),
users = require('./users.js'),
@@ -89,9 +90,10 @@ async function loginPage(req, res, next) {
if (iconError || !iconBuffer) return next(new HttpError(500, 'Icon rendering error'));
const icon = 'data:image/png;base64,' + iconBuffer.toString('base64');
const dashboardOrigin = settings.dashboardOrigin();
try {
finalContent = ejs.render(translatedContent, { title, icon });
finalContent = ejs.render(translatedContent, { title, icon, dashboardOrigin });
} catch (e) {
debug('Error rendering proxyauth-login.ejs', e);
return next(new HttpError(500, 'Login template error'));
@@ -209,7 +211,24 @@ function initializeAuthwallExpressSync() {
const json = middleware.json({ strict: true, limit: QUERY_LIMIT }); // application/json
if (process.env.BOX_ENV !== 'test') app.use(middleware.morgan('proxyauth :method :url :status :response-time ms - :res[content-length]', { immediate: false }));
if (process.env.BOX_ENV !== 'test') {
app.use(middleware.morgan(function (tokens, req, res) {
return [
'proxyauth',
tokens.method(req, res),
tokens.url(req, res),
tokens.status(req, res),
res.errorBody ? res.errorBody.status : '', // attached by connect-lastmile. can be missing when router errors like 404
res.errorBody ? res.errorBody.message : '', // attached by connect-lastmile. can be missing when router errors like 404
tokens['response-time'](req, res), 'ms', '-',
tokens.res(req, res, 'content-length')
].join(' ');
}, {
immediate: false,
// only log failed requests by default
skip: function (req, res) { return res.statusCode < 400; }
}));
}
const router = new express.Router();
router.del = router.delete; // amend router.del for readability further on
+3 -10
View File
@@ -51,8 +51,7 @@ const acme2 = require('./acme2.js'),
shell = require('./shell.js'),
sysinfo = require('./sysinfo.js'),
users = require('./users.js'),
util = require('util'),
_ = require('underscore');
util = require('util');
const NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/nginxconfig.ejs', { encoding: 'utf8' });
const RESTART_SERVICE_CMD = path.join(__dirname, 'scripts/restartservice.sh');
@@ -82,11 +81,6 @@ async function getAcmeApi(domainObject) {
const [error, owner] = await safe(users.getOwner());
apiOptions.email = (error || !owner) ? 'webmaster@cloudron.io' : owner.email; // can error if not activated yet
const accountKeyPem = await blobs.get(blobs.ACME_ACCOUNT_KEY);
if (!accountKeyPem) throw new BoxError(BoxError.NOT_FOUND, 'acme account key not found');
apiOptions.accountKeyPem = accountKeyPem;
return { acmeApi, apiOptions };
}
@@ -412,7 +406,7 @@ async function ensureCertificate(vhost, domain, auditSource) {
debug(`ensureCertificate: ${vhost} cert does not exist`);
}
debug('ensureCertificate: getting certificate for %s with options %j', vhost, _.omit(apiOptions, 'accountKeyPem'));
debug('ensureCertificate: getting certificate for %s with options %j', vhost, apiOptions);
const acmePaths = getAcmeCertificatePathSync(vhost, domainObject);
let [error] = await safe(acmeApi.getCertificate(vhost, domain, acmePaths, apiOptions));
@@ -496,7 +490,7 @@ async function writeAppNginxConfig(app, fqdn, bundle) {
hasIPv6: sysinfo.hasIPv6(),
ip: app.containerIp,
port: app.manifest.httpPort,
endpoint: endpoint,
endpoint,
certFilePath: bundle.certFilePath,
keyFilePath: bundle.keyFilePath,
robotsTxtQuoted,
@@ -507,7 +501,6 @@ async function writeAppNginxConfig(app, fqdn, bundle) {
id: app.id,
location: nginxLocation(safe.query(app.manifest, 'addons.proxyAuth.path') || '/')
},
httpPaths: app.manifest.httpPaths || {},
ocsp: await isOcspEnabled(bundle.certFilePath)
};
const nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
+2 -2
View File
@@ -31,9 +31,9 @@ async function passwordAuth(req, res, next) {
let [error, user] = await safe(verifyFunc(username, password, users.AP_WEBADMIN));
if (error && error.reason === BoxError.NOT_FOUND) {
[error, user] = await safe(externalLdap.maybeCreateUser(username.toLowerCase(), password));
[error, user] = await safe(externalLdap.maybeCreateUser(username.toLowerCase()));
if (error) return next(new HttpError(401, 'Unauthorized'));
[error] = await safe(externalLdap.verifyPassword(user));
[error] = await safe(externalLdap.verifyPassword(user, password));
if (error) return next(new HttpError(401, 'Unauthorized'));
}
if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new HttpError(401, 'Unauthorized'));
+2 -2
View File
@@ -81,10 +81,10 @@ async function logout(req, res) {
async function passwordResetRequest(req, res, next) {
if (!req.body.identifier || typeof req.body.identifier !== 'string') return next(new HttpError(401, 'A identifier must be non-empty string'));
const [error, result] = await safe(users.sendPasswordResetByIdentifier(req.body.identifier, AuditSource.fromRequest(req)));
const [error] = await safe(users.sendPasswordResetByIdentifier(req.body.identifier, AuditSource.fromRequest(req)));
if (error && error.reason !== BoxError.NOT_FOUND) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { resetLink: result }));
next(new HttpSuccess(202, {}));
}
async function passwordReset(req, res, next) {
+2
View File
@@ -81,6 +81,8 @@ async function setup() {
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
const token = await tokens.add({ identifier: user.id, clientId: 'test-client-id', expires: Date.now() + (60 * 60 * 1000), name: 'fromtest' });
user.token = token.accessToken;
await settings._set(settings.CLOUDRON_TOKEN_KEY, exports.appstoreToken); // appstore token
}
async function cleanup() {
+1 -1
View File
@@ -49,7 +49,7 @@ describe('Profile API', function () {
expect(response.statusCode).to.equal(200);
expect(response.body.username).to.equal(owner.username.toLowerCase());
expect(response.body.email).to.equal(owner.email.toLowerCase());
expect(response.body.fallbackEmail).to.equal(owner.email.toLowerCase());
expect(response.body.fallbackEmail).to.equal('');
expect(response.body.displayName).to.be.a('string');
expect(response.body.password).to.not.be.ok();
expect(response.body.salt).to.not.be.ok();
+21 -3
View File
@@ -76,6 +76,15 @@ describe('Users API', function () {
expect(response.statusCode).to.equal(400);
});
it('cannot create user with non email fallbackEmail', async function () {
const response = await superagent.post(`${serverUrl}/api/v1/users`)
.query({ access_token: owner.token })
.send({ username: user2.username, email: user2.email, fallbackEmail: 'notanemail' })
.ok(() => true);
expect(response.statusCode).to.equal(400);
});
it('create second user succeeds', async function () {
const response = await superagent.post(`${serverUrl}/api/v1/users`)
.query({ access_token: owner.token })
@@ -176,11 +185,11 @@ describe('Users API', function () {
it('sending succeeds', async function () {
common.clearMailQueue();
const response = await superagent.post(`${serverUrl}/api/v1/users/${user.id}/send_invite`)
const response = await superagent.post(`${serverUrl}/api/v1/users/${user.id}/send_invite_email`)
.query({ access_token: owner.token })
.send({});
.send({ email: user.email });
expect(response.statusCode).to.equal(200);
expect(response.statusCode).to.equal(202);
await common.checkMails(1);
});
});
@@ -299,6 +308,15 @@ describe('Users API', function () {
expect(response.statusCode).to.equal(400);
});
it('change fallbackEmail fails due to invalid email', async function () {
const response = await superagent.post(`${serverUrl}/api/v1/users/${user2.id}`)
.query({ access_token: owner.token })
.send({ fallbackEmail: 'newemail@cloudron' })
.ok(() => true);
expect(response.statusCode).to.equal(400);
});
it('change user succeeds without email nor displayName', async function () {
const response = await superagent.post(`${serverUrl}/api/v1/users/${user2.id}`)
.query({ access_token: owner.token })
+56 -13
View File
@@ -8,11 +8,16 @@ exports = module.exports = {
del,
setPassword,
verifyPassword,
sendInvite,
setGroups,
setGhost,
makeOwner,
getPasswordResetLink,
sendPasswordResetEmail,
getInviteLink,
sendInviteEmail,
disableTwoFactorAuthentication,
load
@@ -43,6 +48,7 @@ async function add(req, res, next) {
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
if ('username' in req.body && typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be string'));
if ('fallbackEmail' in req.body && typeof req.body.fallbackEmail !== 'string') return next(new HttpError(400, 'fallbackEmail must be string'));
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be string'));
if ('role' in req.body) {
@@ -54,8 +60,9 @@ async function add(req, res, next) {
const email = req.body.email;
const username = 'username' in req.body ? req.body.username : null;
const displayName = req.body.displayName || '';
const fallbackEmail = req.body.fallbackEmail || '';
const [error, id] = await safe(users.add(email, { username, password, displayName, invitor: req.user, role: req.body.role || users.ROLE_USER }, AuditSource.fromRequest(req)));
const [error, id] = await safe(users.add(email, { username, password, displayName, fallbackEmail, invitor: req.user, role: req.body.role || users.ROLE_USER }, AuditSource.fromRequest(req)));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201, { id }));
@@ -151,17 +158,6 @@ async function disableTwoFactorAuthentication(req, res, next) {
next(new HttpSuccess(200, {}));
}
async function sendInvite(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
if (users.compareRoles(req.user.role, req.resource.role) < 0) return next(new HttpError(403, `role '${req.resource.role}' is required but user has only '${req.user.role}'`));
const [error, inviteLink ] = await safe(users.sendInvite(req.resource, { invitor: req.user }, AuditSource.fromRequest(req)));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { inviteLink }));
}
async function setGroups(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
@@ -214,3 +210,50 @@ async function makeOwner(req, res, next) {
next(new HttpSuccess(204));
}
// This will always return a reset link, if none is set or expired a new one will be created
async function getPasswordResetLink(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
if (users.compareRoles(req.user.role, req.resource.role) < 0) return next(new HttpError(403, `role '${req.resource.role}' is required but user has only '${req.user.role}'`));
let [error, passwordResetLink] = await safe(users.getPasswordResetLink(req.resource, AuditSource.fromRequest(req)));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { passwordResetLink }));
}
async function sendPasswordResetEmail(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
if (!req.body.email || typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be a non-empty string'));
if (users.compareRoles(req.user.role, req.resource.role) < 0) return next(new HttpError(403, `role '${req.resource.role}' is required but user has only '${req.user.role}'`));
let [error] = await safe(users.sendPasswordResetEmail(req.resource, req.body.email, AuditSource.fromRequest(req)));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {}));
}
async function getInviteLink(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
if (users.compareRoles(req.user.role, req.resource.role) < 0) return next(new HttpError(403, `role '${req.resource.role}' is required but user has only '${req.user.role}'`));
let [error, inviteLink] = await safe(users.getInviteLink(req.resource, AuditSource.fromRequest(req)));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { inviteLink }));
}
async function sendInviteEmail(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
if (!req.body.email || typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be a non-empty string'));
if (users.compareRoles(req.user.role, req.resource.role) < 0) return next(new HttpError(403, `role '${req.resource.role}' is required but user has only '${req.user.role}'`));
let [error] = await safe(users.sendInviteEmail(req.resource, req.body.email, AuditSource.fromRequest(req)));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {}));
}
+9 -2
View File
@@ -56,8 +56,15 @@ function dumpMemoryInfo() {
const mu = process.memoryUsage();
const hs = v8.getHeapStatistics();
debug(`process: rss: ${mu.rss} heapTotal: ${mu.heapTotal} heapUsed: ${mu.heapUsed} external: ${mu.external}`);
debug(`v8 heap : used ${hs.used_heap_size} total: ${hs.total_heap_size} max: ${hs.heap_size_limit}`);
function h(bytes) { // human readable
const i = Math.floor(Math.log(bytes) / Math.log(1024)),
sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
return (bytes / Math.pow(1024, i)).toFixed(2) * 1 + '' + sizes[i];
}
debug(`process: rss: ${h(mu.rss)} heapUsed: ${h(mu.heapUsed)} heapTotal: ${h(mu.heapTotal)} external: ${h(mu.external)}`);
debug(`v8 heap: used ${h(hs.used_heap_size)} total: ${h(hs.total_heap_size)} max: ${h(hs.heap_size_limit)}`);
}
initialize(function (error) {
+4 -6
View File
@@ -6,7 +6,6 @@ exports = module.exports = {
};
const assert = require('assert'),
blobs = require('./blobs.js'),
cloudron = require('./cloudron.js'),
constants = require('./constants.js'),
database = require('./database.js'),
@@ -103,9 +102,6 @@ function initializeExpressSync() {
router.post('/api/v1/cloudron/password_reset', json, routes.cloudron.passwordReset);
router.post('/api/v1/cloudron/setup_account', json, routes.cloudron.setupAccount);
// developer routes
router.post('/api/v1/developer/login', json, password, routes.cloudron.login); // DEPRECATED we should use the regular /api/v1/cloudron/login
// cloudron routes
router.get ('/api/v1/cloudron/update', token, authorizeAdmin, routes.cloudron.getUpdateInfo);
router.post('/api/v1/cloudron/update', json, token, authorizeAdmin, routes.cloudron.update);
@@ -179,8 +175,11 @@ function initializeExpressSync() {
router.post('/api/v1/users/:userId/ghost', json, token, authorizeAdmin, routes.users.load, routes.users.setGhost);
router.put ('/api/v1/users/:userId/groups', json, token, authorizeUserManager, routes.users.load, routes.users.setGroups);
router.post('/api/v1/users/:userId/make_owner', json, token, authorizeOwner, routes.users.load, routes.users.makeOwner);
router.post('/api/v1/users/:userId/send_invite', json, token, authorizeUserManager, routes.users.load, routes.users.sendInvite);
router.post('/api/v1/users/:userId/twofactorauthentication_disable', json, token, authorizeUserManager, routes.users.load, routes.users.disableTwoFactorAuthentication);
router.get ('/api/v1/users/:userId/password_reset_link', json, token, authorizeUserManager, routes.users.load, routes.users.getPasswordResetLink);
router.post('/api/v1/users/:userId/send_password_reset_email', json, token, authorizeUserManager, routes.users.load, routes.users.sendPasswordResetEmail);
router.get ('/api/v1/users/:userId/invite_link', json, token, authorizeUserManager, routes.users.load, routes.users.getInviteLink);
router.post('/api/v1/users/:userId/send_invite_email', json, token, authorizeUserManager, routes.users.load, routes.users.sendInviteEmail);
// Group management
router.get ('/api/v1/groups', token, authorizeUserManager, routes.groups.list);
@@ -380,7 +379,6 @@ async function start() {
await database.initialize();
await settings.initCache(); // pre-load very often used settings
await blobs.initSecrets();
await cloudron.initialize();
await util.promisify(gHttpServer.listen.bind(gHttpServer))(constants.PORT, '127.0.0.1');
await safe(eventlog.add(eventlog.ACTION_START, { userId: null, username: 'boot' }, { version: constants.VERSION })); // can fail if db down
+11 -5
View File
@@ -919,8 +919,12 @@ async function startTurn(existingInfra) {
const memory = system.getMemoryAllocation(memoryLimit);
const realm = settings.dashboardFqdn();
const turnSecret = await blobs.get(blobs.ADDON_TURN_SECRET);
if (!turnSecret) throw new BoxError(BoxError.ADDONS_ERROR, 'Turn secret is missing');
let turnSecret = await blobs.get(blobs.ADDON_TURN_SECRET);
if (!turnSecret) {
debug('startTurn: generting turn secret');
turnSecret = 'a' + crypto.randomBytes(15).toString('hex'); // prefix with a to ensure string starts with a letter
await blobs.set(blobs.ADDON_TURN_SECRET, Buffer.from(turnSecret));
}
const readOnly = !serviceConfig.recoveryMode ? '--read-only' : '';
const cmd = serviceConfig.recoveryMode ? '/bin/bash -c \'echo "Debug mode. Sleeping" && sleep infinity\'' : '';
@@ -1080,8 +1084,10 @@ async function setupRecvMail(app, options) {
const env = [
{ name: `${envPrefix}MAIL_IMAP_SERVER`, value: 'mail' },
{ name: `${envPrefix}MAIL_IMAP_PORT`, value: '9993' },
{ name: `${envPrefix}MAIL_POP3_PORT`, value: '9995' },
{ name: `${envPrefix}MAIL_IMAP_PORT`, value: '9393' },
{ name: `${envPrefix}MAIL_IMAPS_PORT`, value: '9993' },
{ name: `${envPrefix}MAIL_POP3_PORT`, value: '9595' },
{ name: `${envPrefix}MAIL_POP3S_PORT`, value: '9995' },
{ name: `${envPrefix}MAIL_IMAP_USERNAME`, value: app.inboxName + '@' + app.inboxDomain },
{ name: `${envPrefix}MAIL_IMAP_PASSWORD`, value: password },
{ name: `${envPrefix}MAIL_TO`, value: app.inboxName + '@' + app.inboxDomain },
@@ -1400,7 +1406,7 @@ async function setupPostgreSql(app, options) {
];
debug('Setting postgresql addon config to %j', env);
addonConfigs.set(app.id, 'postgresql', env);
await addonConfigs.set(app.id, 'postgresql', env);
}
async function clearPostgreSql(app, options) {
+20
View File
@@ -9,6 +9,7 @@ exports = module.exports = {
const apps = require('./apps.js'),
assert = require('assert'),
blobs = require('./blobs.js'),
BoxError = require('./boxerror.js'),
debug = require('debug')('box:sftp'),
docker = require('./docker.js'),
@@ -22,6 +23,23 @@ const apps = require('./apps.js'),
system = require('./system.js'),
volumes = require('./volumes.js');
async function ensureKeys() {
let sftpPrivateKey = await blobs.get(blobs.SFTP_PRIVATE_KEY);
let sftpPublicKey = await blobs.get(blobs.SFTP_PUBLIC_KEY);
if (!sftpPrivateKey || !sftpPublicKey) {
debug('ensureSecrets: generating new sftp keys');
if (!safe.child_process.execSync(`ssh-keygen -m PEM -t rsa -f "${paths.SFTP_KEYS_DIR}/ssh_host_rsa_key" -q -N ""`)) throw new BoxError(BoxError.OPENSSL_ERROR, `Could not generate sftp ssh keys: ${safe.error.message}`);
sftpPublicKey = safe.fs.readFileSync(paths.SFTP_PUBLIC_KEY_FILE);
await blobs.set(blobs.SFTP_PUBLIC_KEY, sftpPublicKey);
sftpPrivateKey = safe.fs.readFileSync(paths.SFTP_PRIVATE_KEY_FILE);
await blobs.set(blobs.SFTP_PRIVATE_KEY, sftpPrivateKey);
}
if (!safe.fs.writeFileSync(paths.SFTP_PUBLIC_KEY_FILE, sftpPublicKey)) throw new BoxError(BoxError.FS_ERROR, `Could not save sftp public key: ${safe.error.message}`);
if (!safe.fs.writeFileSync(paths.SFTP_PRIVATE_KEY_FILE, sftpPrivateKey)) throw new BoxError(BoxError.FS_ERROR, `Could not save sftp private key: ${safe.error.message}`);
}
async function start(existingInfra) {
assert.strictEqual(typeof existingInfra, 'object');
@@ -34,6 +52,8 @@ async function start(existingInfra) {
const memory = system.getMemoryAllocation(memoryLimit);
const cloudronToken = hat(8 * 128);
await ensureKeys();
const resolvedAppDataDir = safe.fs.realpathSync(paths.APPS_DATA_DIR);
if (!resolvedAppDataDir) throw new BoxError(BoxError.FS_ERROR, `Could not resolve apps data dir: ${safe.error.message}`);
+3 -7
View File
@@ -71,18 +71,14 @@ async function checkPreconditions(apiConfig, dataLayout, callback) {
for (let localPath of dataLayout.localPaths()) {
debug(`checkPreconditions: getting disk usage of ${localPath}`);
let result = safe.child_process.execSync(`du -Dsb ${localPath}`, { encoding: 'utf8' });
if (!result) return callback(new BoxError(BoxError.FS_ERROR, safe.error));
if (!result) return callback(new BoxError(BoxError.FS_ERROR, `du error: ${safe.error.message}`));
used += parseInt(result, 10);
}
debug(`checkPreconditions: ${used} bytes`);
let result;
try {
result = await df.file(getBackupPath(apiConfig));
} catch(error) {
return callback(new BoxError(BoxError.FS_ERROR, error));
}
const [error, result] = await safe(df.file(getBackupPath(apiConfig)));
if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error when checking for disk space: ${error.message}`));
// Check filesystem is mounted so we don't write into the actual folder on disk
if (apiConfig.provider === PROVIDER_SSHFS || apiConfig.provider === PROVIDER_CIFS || apiConfig.provider === PROVIDER_NFS || apiConfig.provider === PROVIDER_EXT4) {
+1
View File
@@ -29,6 +29,7 @@ function sshInfo() {
filePath = '/home/ubuntu/.ssh/authorized_keys';
user = 'ubuntu';
} else {
filePath = '/root/.ssh/authorized_keys';
user = 'root';
}
+3 -2
View File
@@ -6,6 +6,7 @@
'use strict';
const apps = require('../apps.js'),
AuditSource = require('../auditsource.js'),
BoxError = require('../boxerror.js'),
common = require('./common.js'),
expect = require('expect.js'),
@@ -284,7 +285,7 @@ describe('Apps', function () {
});
it('can mark apps for reconfigure', async function () {
await apps.configureInstalledApps();
await apps.configureInstalledApps(AuditSource.PLATFORM);
const result = await apps.list();
expect(result[0].installationState).to.be(apps.ISTATE_PENDING_CONFIGURE);
@@ -309,7 +310,7 @@ describe('Apps', function () {
});
it('can mark apps for reconfigure', async function () {
await apps.restoreInstalledApps({});
await apps.restoreInstalledApps({}, AuditSource.PLATFORM);
const result = await apps.list();
expect(result[0].installationState).to.be(apps.ISTATE_PENDING_INSTALL);
+6 -3
View File
@@ -63,7 +63,7 @@ const admin = {
username: 'testadmin',
password: 'secret123',
email: 'admin@me.com',
fallbackEmail: 'admin@me.com',
fallbackEmail: 'admin@external.com',
salt: 'morton',
createdAt: 'sometime back',
resetToken: '',
@@ -80,7 +80,7 @@ const user = {
username: 'user',
password: '123secret',
email: 'user@me.com',
fallbackEmail: 'user@me.com',
fallbackEmail: 'user@external.com',
role: 'user',
salt: 'morton',
createdAt: 'sometime back',
@@ -165,7 +165,6 @@ async function databaseSetup() {
await settings._setApiServerOrigin(exports.mockApiServerOrigin);
await settings.setDashboardLocation(exports.dashboardDomain, exports.dashboardFqdn);
await settings.initCache();
await blobs.initSecrets();
}
async function domainSetup() {
@@ -208,5 +207,9 @@ function clearMailQueue() {
async function checkMails(number) {
await delay(1000);
expect(mailer._mailQueue.length).to.equal(number);
const emails = mailer._mailQueue;
clearMailQueue();
// return for further investigation
return emails;
}
+2 -6
View File
@@ -535,16 +535,12 @@ describe('External LDAP', function () {
});
it('succeeds for known user with correct password', async function () {
const newUser = {
gLdapUsers.push({
username: 'autologinuser2',
displayName: 'Auto Login2',
email: 'auto2@login.com',
password: LDAP_SHARED_PASSWORD
};
gLdapUsers.push(newUser);
await users.add(newUser.email, newUser, auditSource);
});
const response = await superagent.post(`${serverUrl}/api/v1/cloudron/login`)
.send({ username: 'autologinuser2', password: LDAP_SHARED_PASSWORD })
+37 -5
View File
@@ -83,6 +83,12 @@ describe('User', function () {
expect(error.reason).to.equal(BoxError.BAD_FIELD);
});
it('fails because fallbackEmail is not an email', async function () {
const user = Object.assign({}, admin, { fallbackEmail: 'notanemail' });
const [error] = await safe(users.add(user.email, user, auditSource));
expect(error.reason).to.equal(BoxError.BAD_FIELD);
});
it('can add user', async function () {
const id = await users.add(admin.email, admin, auditSource);
admin.id = id;
@@ -403,7 +409,7 @@ describe('User', function () {
const result = await users.get(admin.id);
expect(result.id).to.equal(admin.id);
expect(result.email).to.equal(admin.email.toLowerCase());
expect(result.fallbackEmail).to.equal(admin.email.toLowerCase());
expect(result.fallbackEmail).to.equal(admin.fallbackEmail.toLowerCase());
expect(result.username).to.equal(admin.username.toLowerCase());
expect(result.displayName).to.equal(admin.displayName);
});
@@ -487,11 +493,37 @@ describe('User', function () {
describe('invite', function () {
before(createOwner);
it('send invite', async function () {
let user;
it('get link fails as alreayd been used', async function () {
const [error] = await safe(users.getInviteLink(admin, auditSource));
expect(error.reason).to.be(BoxError.BAD_STATE);
});
it('can get link', async function () {
const userId = await users.add('some@mail.com', { username: 'someoneinvited', displayName: 'some one', password: 'unsafe1234' }, auditSource);
user = await users.get(userId);
const inviteLink = await users.getInviteLink(user, auditSource);
expect(inviteLink).to.be.a('string');
expect(inviteLink).to.contain(user.inviteToken);
});
it('cannot send mail for already active user', async function () {
const [error] = await safe(users.sendInviteEmail(admin, 'admin@mail.com', auditSource));
expect(error.reason).to.be(BoxError.BAD_STATE);
});
it('cannot send mail with empty receipient', async function () {
const [error] = await safe(users.sendInviteEmail(user, '', auditSource));
expect(error.reason).to.be(BoxError.BAD_FIELD);
});
it('can send mail', async function () {
await clearMailQueue();
const inviteLink = await users.sendInvite(admin, {}, auditSource);
expect(inviteLink).to.be.ok();
await checkMails(1);
await users.sendInviteEmail(user, 'custom@mail.com', auditSource);
const emails = await checkMails(1);
expect(emails[0].to).to.equal('custom@mail.com');
});
});
+3 -1
View File
@@ -148,9 +148,11 @@ async function update(boxUpdateInfo, options, progressCallback) {
progressCallback({ percent: 10, message: 'Backing up' });
await backuptask.fullBackup({ preserveSecs: 3*7*24*60*60 }, (progress) => progressCallback({ percent: 10+progress.percent*70/100, message: progress.message }));
await checkFreeDiskSpace(1024*1024*1024); // check again in case backup is in same disk
}
debug('updating box %s', boxUpdateInfo.sourceTarballUrl);
debug(`Updating box with ${boxUpdateInfo.sourceTarballUrl}`);
progressCallback({ percent: 70, message: 'Installing update' });
+63 -16
View File
@@ -29,14 +29,18 @@ exports = module.exports = {
del,
sendInvite,
setTwoFactorAuthenticationSecret,
enableTwoFactorAuthentication,
disableTwoFactorAuthentication,
sendPasswordResetByIdentifier,
getPasswordResetLink,
sendPasswordResetEmail,
getInviteLink,
sendInviteEmail,
notifyLoginLocation,
setupAccount,
@@ -180,8 +184,10 @@ async function add(email, data, auditSource) {
assert(data.username === null || typeof data.username === 'string');
assert(data.password === null || typeof data.password === 'string');
assert.strictEqual(typeof data.displayName, 'string');
if ('fallbackEmail' in data) assert.strictEqual(typeof data.fallbackEmail, 'string');
let { username, password, displayName } = data;
let fallbackEmail = data.fallbackEmail || '';
const source = data.source || ''; // empty is local user
const role = data.role || exports.ROLE_USER;
@@ -204,6 +210,12 @@ async function add(email, data, auditSource) {
error = validateEmail(email);
if (error) throw error;
fallbackEmail = fallbackEmail.toLowerCase();
if (fallbackEmail) {
let error = validateEmail(fallbackEmail);
if (error) throw error;
}
error = validateDisplayName(displayName);
if (error) throw error;
@@ -222,7 +234,7 @@ async function add(email, data, auditSource) {
id: 'uid-' + uuid.v4(),
username: username,
email: email,
fallbackEmail: email,
fallbackEmail: fallbackEmail,
password: Buffer.from(derivedKey, 'binary').toString('hex'),
salt: salt.toString('hex'),
resetToken: '',
@@ -256,6 +268,8 @@ async function setGhost(user, password, expiresAt) {
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof expiresAt, 'number');
if (!user.username) throw new BoxError(BoxError.BAD_STATE, 'user has no username yet');
expiresAt = expiresAt || (Date.now() + DEFAULT_GHOST_LIFETIME);
debug(`setGhost: ${user.username} expiresAt ${expiresAt}`);
@@ -616,11 +630,40 @@ async function sendPasswordResetByIdentifier(identifier, auditSource) {
await update(user, { resetToken,resetTokenCreationTime }, auditSource);
const resetLink = `${settings.dashboardOrigin()}/login.html?resetToken=${user.resetToken}`;
await mailer.passwordReset(user, resetLink);
await mailer.passwordReset(user, user.fallbackEmail || user.email, resetLink);
}
async function getPasswordResetLink(user, auditSource) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof auditSource, 'object');
let resetToken = user.resetToken;
let resetTokenCreationTime = user.resetTokenCreationTime || 0;
if (!resetToken || (Date.now() - resetTokenCreationTime > 7 * 24 * 60 * 60 * 1000)) {
resetToken = hat(256);
resetTokenCreationTime = new Date();
await update(user, { resetToken, resetTokenCreationTime }, auditSource);
}
const resetLink = `${settings.dashboardOrigin()}/login.html?resetToken=${resetToken}`;
return resetLink;
}
async function sendPasswordResetEmail(user, email, auditSource) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof auditSource, 'object');
const error = validateEmail(email);
if (error) throw error;
const resetLink = await getPasswordResetLink(user, auditSource);
await mailer.passwordReset(user, email, resetLink);
}
async function notifyLoginLocation(user, ip, userAgent, auditSource) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof ip, 'string');
@@ -701,21 +744,15 @@ async function createOwner(email, username, password, displayName, auditSource)
const activated = await isActivated();
if (activated) throw new BoxError(BoxError.ALREADY_EXISTS, 'Cloudron already activated');
return await add(email, { username, password, displayName, role: exports.ROLE_OWNER }, auditSource);
return await add(email, { username, password, fallbackEmail: '', displayName, role: exports.ROLE_OWNER }, auditSource);
}
async function sendInvite(user, options, auditSource) {
async function getInviteLink(user, auditSource) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof auditSource, 'object');
if (user.source) throw new BoxError(BoxError.CONFLICT, 'User is from an external directory');
// not sure if this can ever be the case
if (!user.inviteToken) {
const inviteToken = hat(256);
user.inviteToken = inviteToken;
await update(user, { inviteToken }, auditSource);
}
if (!user.inviteToken) throw new BoxError(BoxError.BAD_STATE, 'User already used invite link');
const directoryConfig = await settings.getDirectoryConfig();
let inviteLink = `${settings.dashboardOrigin()}/setupaccount.html?inviteToken=${user.inviteToken}&email=${encodeURIComponent(user.email)}`;
@@ -724,11 +761,21 @@ async function sendInvite(user, options, auditSource) {
if (user.displayName) inviteLink += `&displayName=${encodeURIComponent(user.displayName)}`;
if (directoryConfig.lockUserProfiles) inviteLink += '&profileLocked=true';
await mailer.sendInvite(user, options.invitor || null, inviteLink);
return inviteLink;
}
async function sendInviteEmail(user, email, auditSource) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof auditSource, 'object');
const error = validateEmail(email);
if (error) throw error;
const inviteLink = await getInviteLink(user, auditSource);
await mailer.sendInvite(user, null /* invitor */, email, inviteLink);
}
async function setupAccount(user, data, auditSource) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof data, 'object');