Compare commits

..

407 Commits

Author SHA1 Message Date
Girish Ramakrishnan a00b7281a7 Fixup changelog 2020-07-17 10:43:22 -07:00
Girish Ramakrishnan ddeee0c970 Add note that links expire in 24 hours 2020-07-16 15:17:51 -07:00
Johannes Zellner 8aad71efd0 Add more feature flags 2020-07-16 18:14:25 +02:00
Johannes Zellner 2028f6b984 Do not reassign ubunt_codename in base image init 2020-07-16 16:42:15 +02:00
Girish Ramakrishnan bff4999d27 mail: add mailbox count route 2020-07-15 15:48:30 -07:00
Johannes Zellner d429015f83 Add more 3.4.0 changes 2020-07-15 14:57:06 +02:00
Johannes Zellner e2628e2d43 Use latest filemanager addon
Fixes dot- and json-files
2020-07-14 17:16:41 +02:00
Girish Ramakrishnan 05dcbee7e3 backups: add b2 provider
part of #508
2020-07-13 14:52:35 -07:00
Johannes Zellner a81919262e Use addon with chown functionality 2020-07-13 18:48:42 +02:00
Girish Ramakrishnan b14b5f141b Hide nginx version 2020-07-13 09:27:57 -07:00
Girish Ramakrishnan 1259d11173 Add back provider field into getStatus 2020-07-13 08:46:05 -07:00
Johannes Zellner 0a7b132be8 Remove or increase timeouts for filemanager 2020-07-13 17:05:22 +02:00
Girish Ramakrishnan ed9210eede Add mandatory 2FA flag
part of #716
2020-07-10 10:25:04 -07:00
Girish Ramakrishnan 9ee6aa54c6 avatar is not part of the profile lock
this is because avatar is not exposed via LDAP anyways. it's purely
a personal dashboard thing.
2020-07-10 09:43:42 -07:00
Girish Ramakrishnan 7cfc455cd3 make tests pass again
also disable column statistics on ubuntu 20
2020-07-10 09:33:35 -07:00
Johannes Zellner a481ceac8c Allow larger file uploads for filemanager 2020-07-10 18:23:55 +02:00
Girish Ramakrishnan 8c7eff4e24 user: add routes to set/clear avatar 2020-07-10 07:23:38 -07:00
Girish Ramakrishnan c6c584ff74 user: move avatar handling into model code 2020-07-10 07:01:15 -07:00
Johannes Zellner ba50eb121d Use new sftp addon 2020-07-10 14:13:16 +02:00
Johannes Zellner aa8ebbd7ea Add filemanager proxy routes 2020-07-10 14:10:52 +02:00
Girish Ramakrishnan 64bc9c6dbe disable profile view for all users to avoid confusion 2020-07-09 21:54:09 -07:00
Girish Ramakrishnan bba9963b7c Add directoryConfig feature flag
Fixes #704
2020-07-09 21:51:22 -07:00
Girish Ramakrishnan 6ea2aa4a54 return profileLocked in config route
part of #704
2020-07-09 17:28:44 -07:00
Girish Ramakrishnan 3c3f81365b add route to get/set directory config
part of #704
2020-07-09 17:12:07 -07:00
Girish Ramakrishnan 3adeed381b setup account based on directory config
part of #704
2020-07-09 16:39:34 -07:00
Girish Ramakrishnan 0f5b7278b8 add directory config setting
part of #704
2020-07-09 16:02:58 -07:00
Girish Ramakrishnan f94ff49fb9 users: replace modifiedAt with ts 2020-07-09 16:02:49 -07:00
Girish Ramakrishnan d512a9c30d rename function 2020-07-09 16:02:43 -07:00
Girish Ramakrishnan 0c5113ed5b email is never used in account setup 2020-07-09 15:37:35 -07:00
Girish Ramakrishnan 2469f4cdff rename function to sendPasswordResetByIdentifier 2020-07-09 15:37:35 -07:00
Girish Ramakrishnan 9c53bfb7fb Do not show LDAP logs, it spams a lot 2020-07-07 11:16:47 -07:00
Girish Ramakrishnan 8b8144588d list must search members 2020-07-05 11:44:46 -07:00
Girish Ramakrishnan 77553da4c1 mail: add search param for mailbox and mailing list api 2020-07-05 11:23:53 -07:00
Girish Ramakrishnan cbcf943691 mail: parameterize the query 2020-07-05 10:48:08 -07:00
Girish Ramakrishnan 725a19e5b5 mail: Add pagination to lists API
Fixes #712
2020-07-05 10:48:04 -07:00
Girish Ramakrishnan f9115f902a Do not send alive status
we used to do this for managed hosting to track scaling but we don't
need this info anymore
2020-07-03 19:13:27 -07:00
Girish Ramakrishnan e4faf26d74 5.3.4 changes
(cherry picked from commit 77785097c1)
2020-07-03 14:23:20 -07:00
Girish Ramakrishnan 1c96fbb533 Fixes for tests 2020-07-03 13:47:56 -07:00
Girish Ramakrishnan 3dc163c33d database: rework connection logic 2020-07-03 13:14:00 -07:00
Girish Ramakrishnan edae94cf2e Bump max_connection for postgres addon to 200 2020-07-02 15:47:26 -07:00
Girish Ramakrishnan d1ff8e9d6b Fix crash when mysql crashes 2020-07-02 15:10:05 -07:00
Girish Ramakrishnan 70743bd285 database: Fix event emitter warning
the connection object gets reused after release. this means that we keep
attaching the 'error' event and not unlistening.

--trace-warnings can be added to box.service to get the stack trace
2020-07-02 12:00:56 -07:00
Johannes Zellner 493f1505f0 Check also for mountpoint on filesystem with external disk 2020-07-02 19:08:27 +02:00
Girish Ramakrishnan 007e3b5eef Add changes 2020-07-01 14:29:40 -07:00
Johannes Zellner d9bf6c0933 also support uniqueMember property next to member for ldap groups 2020-07-01 17:08:17 +02:00
Johannes Zellner 324344d118 Reusue the single correct ldap.createClient call also in auth 2020-07-01 14:59:26 +02:00
Johannes Zellner 5cb71e9443 No need to return externalLdapConfig in getClient() 2020-07-01 14:52:11 +02:00
Johannes Zellner cca19f00c5 Fallback to mailPrimaryAddress in ldap sync 2020-07-01 14:34:41 +02:00
Girish Ramakrishnan 6648f41f3d nginx: [warn] the "ssl" directive is deprecated, use the "listen ... ssl" directive 2020-06-30 16:00:52 -07:00
Girish Ramakrishnan c1e6b47fd6 Fix sogo aliases
Fixes cloudron/sogo#18
2020-06-30 14:29:50 -07:00
Girish Ramakrishnan 0f103ccce1 Add ping capability (for statping) 2020-06-30 07:40:17 -07:00
Girish Ramakrishnan bc6e652293 5.3.3 changes 2020-06-29 19:52:08 -07:00
Girish Ramakrishnan 85b4f2dbdd print sudo command to check failures 2020-06-29 14:03:34 -07:00
Girish Ramakrishnan d47b83a63b Package lock mystery 2020-06-29 14:03:15 -07:00
Girish Ramakrishnan b2e9fa7e0d aschema: dd servicesConfigJson 2020-06-26 15:48:39 -07:00
Girish Ramakrishnan a9fb444622 Use nginx 1.18 for security fixes 2020-06-26 14:57:53 -07:00
Girish Ramakrishnan 33ba22a021 Put this in 5.3.2 itself 2020-06-26 10:41:32 -07:00
Girish Ramakrishnan 57de0282cd remove provider from trackBeginSetup 2020-06-26 09:55:39 -07:00
Girish Ramakrishnan 8568fd26d8 Fix failing test 2020-06-26 09:48:10 -07:00
Girish Ramakrishnan 84f41e08cf Add mlock capability to manifest (for vault app) 2020-06-26 09:27:35 -07:00
Johannes Zellner a96da20536 TODO is done for filesystem backend moutnpoint check 2020-06-26 17:57:26 +02:00
Johannes Zellner 5199a9342e Add missing ldap client error handling 2020-06-26 17:55:42 +02:00
Girish Ramakrishnan 893ecec0fa redis: Set maxmemory and maxmemory-policy 2020-06-26 08:54:47 -07:00
Girish Ramakrishnan e3da6419f5 Add 5.3.2 changes 2020-06-26 08:48:01 -07:00
Girish Ramakrishnan 0750d2ba50 More changes 2020-06-25 16:48:11 -07:00
Girish Ramakrishnan f1fcb65fbe Do not install sshfs. user will install it if they want
we don't use sshfs anywhere in our code ourselves
2020-06-25 12:21:49 -07:00
Girish Ramakrishnan 215aa65d5a Fix provider usage
* do not send to appstore anymore
* do not set in getStatus/getConfig
* provider is not needed when registering cloudron
2020-06-25 11:20:05 -07:00
Girish Ramakrishnan 85f67c13da remove unused registerWithLicense 2020-06-25 11:11:52 -07:00
Girish Ramakrishnan 6dcc478aeb add to changes 2020-06-25 09:20:37 -07:00
Johannes Zellner 3f2496db6f Support self-signed certs for external ldap/ad 2020-06-25 17:45:59 +02:00
Johannes Zellner 612f79f9e0 Copy over changes for 5.3.1 2020-06-25 14:22:44 +02:00
Johannes Zellner 90fb1cd735 We also need enableBackup property for app listing api 2020-06-25 12:31:00 +02:00
Girish Ramakrishnan 7c24d9c6c6 Give graphite more memory 2020-06-22 09:55:01 -07:00
Johannes Zellner 60f1b2356a Also make nfs storage provider same as cifs and sshfs 2020-06-22 15:51:05 +02:00
Johannes Zellner 0b8f21508f Add more changes 2020-06-22 12:04:34 +02:00
Johannes Zellner ae128c0fa4 If no appstore account is setup restrict features to free plan 2020-06-22 12:02:10 +02:00
Girish Ramakrishnan 1b4ec9ecf9 Update changes 2020-06-18 10:25:45 -07:00
Girish Ramakrishnan b0ce0b61d6 logging: fix crash when router errors 2020-06-18 09:27:09 -07:00
Girish Ramakrishnan e1ffdaddfa Fix timeout issues in postgresql and mysql addon 2020-06-17 16:39:30 -07:00
Johannes Zellner af8344f482 remove unused requires 2020-06-16 14:37:06 +02:00
Johannes Zellner 7dc2596b3b Ensure we support pre 5.3 Cloudron installation 2020-06-16 14:10:14 +02:00
Johannes Zellner 0109956fc2 do not rely on some argument passed through for infraversion base path 2020-06-16 14:09:55 +02:00
Johannes Zellner 945fe3f3ec Do not spam install logs with nodejs tarball contents 2020-06-16 13:58:23 +02:00
Johannes Zellner 9c868135f3 app sso flag is not restricted now 2020-06-16 13:09:06 +02:00
Girish Ramakrishnan 5be288023b update mail container to record separator and spam folder 2020-06-15 13:50:46 -07:00
Girish Ramakrishnan a03f97186c Make mail auth case insensitive 2020-06-15 09:58:55 -07:00
Johannes Zellner 0aab891980 Support nginx logs 2020-06-15 17:30:16 +02:00
Johannes Zellner 5268d3f57d Fix test for systems without swap 2020-06-15 16:06:54 +02:00
Girish Ramakrishnan 129cbb5beb backups: fix cleanup
The various changes are:
* Latest backup is always kept for box and app backups
* If the latest backup is part of the policy, it is not counted twice
* Latest backup comes into action only when all backups are outside the retention policy
* For uninstalled apps, latest backup is not preserved
* This way the latest backup of apps that are not referenced in box backup is preserved.
  (for example, for stopped apps)

fixes #692
2020-06-14 22:06:00 -07:00
Girish Ramakrishnan 2601d2945d Fix backup tests 2020-06-14 14:01:01 -07:00
Girish Ramakrishnan e3829eb24b typo 2020-06-14 14:00:29 -07:00
Girish Ramakrishnan f6cb1a0863 backups: query using identifier instead of type
this allows us to move the enums into backups.js instead of backupdb.js
2020-06-14 12:27:41 -07:00
Girish Ramakrishnan 4f964101a0 add identifier to backups table 2020-06-14 11:39:44 -07:00
Girish Ramakrishnan f6dcba025f auditSource is not used in the worker 2020-06-14 09:09:41 -07:00
Johannes Zellner d6ec65d456 Do not remove alternateDomains to allow apps view filter to work 2020-06-14 13:39:15 +02:00
Girish Ramakrishnan 65d8074a07 Fix failing backup test 2020-06-12 12:58:11 -07:00
Girish Ramakrishnan e3af61ca4a Fix failing test 2020-06-12 12:52:32 -07:00
Girish Ramakrishnan a58f1268f0 mail: Add Auto-Submitted header to NDRs 2020-06-11 19:48:37 -07:00
Girish Ramakrishnan 41eacc4bc5 postgresql: Add unaccent extension 2020-06-11 09:53:53 -07:00
Girish Ramakrishnan aabb9dee13 Fix transaction rollback logic 2020-06-11 09:50:49 -07:00
Girish Ramakrishnan c855d75f35 remove mkdirp use
node 10.12 has { recursive: true }
2020-06-11 08:27:48 -07:00
Girish Ramakrishnan 8f5cdcf439 backups: some logs for debugging 2020-06-10 23:00:23 -07:00
Girish Ramakrishnan 984559427e update manifest format to 5.3.0 2020-06-09 11:35:54 -07:00
Johannes Zellner 89494ced41 Check for sshfs and cifs backup backends, if they are mounted 2020-06-08 17:46:52 +02:00
Johannes Zellner ef764c2393 Merge sshfs.js into filesystem.js 2020-06-08 17:08:26 +02:00
Johannes Zellner 8624e2260d add storage api to make preflight checks
Currently there is only disk space checking but sshfs and cifs need
mount point checking as well
2020-06-08 16:25:05 +02:00
Johannes Zellner aa011f4add add ldap group tests and fixes for the found issues 2020-06-07 13:49:01 +02:00
Girish Ramakrishnan 3df61c9ab8 do not automatically update unstable updates
part of #698
2020-06-05 16:26:23 -07:00
Girish Ramakrishnan a4516776d6 make canAutoupdateApp take updateInfo object
part of #698
2020-06-05 16:06:37 -07:00
Girish Ramakrishnan 54d0ade997 curl uses -s and not -q 2020-06-05 13:50:40 -07:00
Johannes Zellner 3557fcd129 Add sshfs quirks to shared code in filesytstem.js 2020-06-05 13:45:25 +02:00
Johannes Zellner 330b4a613c Retrieve the backupPath from the storage provider itself 2020-06-05 13:27:18 +02:00
Johannes Zellner 7ba3412aae Add some sshfs config tests 2020-06-05 12:43:09 +02:00
Johannes Zellner 6f60495d4d Initial version of sshfs storage backend 2020-06-05 11:39:51 +02:00
Johannes Zellner 0b2eb8fb9e Sync users into groups
This does not yet remove users from groups

Part of #685
2020-06-05 11:28:57 +02:00
Johannes Zellner 48af17e052 Groups are lowercase on Cloudron 2020-06-05 10:13:19 +02:00
Johannes Zellner b7b1055530 Avoid the pyramid 2020-06-05 09:26:52 +02:00
Johannes Zellner e7029c0afd Remove unsused and poorly named groups.getGroups() API 2020-06-05 09:24:00 +02:00
Johannes Zellner cba3674ac0 Stop ldap syncing if we hit some internal error 2020-06-05 09:03:30 +02:00
Girish Ramakrishnan 865a549885 say connected 2020-06-04 11:27:11 -07:00
Girish Ramakrishnan 50dcf827a5 remove console.error use in many places
the backtraces just flood the logs

apphealthtask: remove console.error
remove spurious console.dir
cleanup scheduler error logging
2020-06-04 11:21:56 -07:00
Girish Ramakrishnan f5fb582f83 log status and message in morgan
connect lastmile does not forward final handler to express anymore.
otherwise, express logs using console.error()
https://github.com/expressjs/express/issues/2263
2020-06-04 09:17:58 -07:00
Girish Ramakrishnan dbba502f83 remove message from debug 2020-06-04 09:17:58 -07:00
Girish Ramakrishnan aae49f16a2 database: do no reconnect in query 2020-06-04 09:17:58 -07:00
Girish Ramakrishnan 45d5f8c74d make rollback return an error
fixes #690
2020-06-04 09:17:58 -07:00
Girish Ramakrishnan 6cfd64e536 database: do not crash if connection errors
Part of #690
2020-06-04 09:17:58 -07:00
Girish Ramakrishnan c5cc404b3e do not retry here
Part of #690
2020-06-04 09:17:58 -07:00
Johannes Zellner 42cbcc6ce3 groups.create() now needs source argument 2020-06-04 14:20:05 +02:00
Johannes Zellner 812bdcd462 Fix groups test by ensuring we order by name 2020-06-04 14:03:06 +02:00
Johannes Zellner f275409ee8 Fix cloudron api tests 2020-06-04 13:55:47 +02:00
Johannes Zellner 8994ac3727 Fix backup retention tests 2020-06-04 13:43:25 +02:00
Johannes Zellner 7c5ff5e4d5 Create user groups for ldap groups 2020-06-04 13:26:13 +02:00
Johannes Zellner c5e84d5469 Add source property to userGroups 2020-06-04 13:25:55 +02:00
Johannes Zellner c143450dc6 WIP 2020-06-04 12:59:27 +02:00
Johannes Zellner 07b95c2c4b Add groups.getByName() 2020-06-04 12:48:35 +02:00
Johannes Zellner c30734f7f3 Show in the logs if group sync is disabled 2020-06-04 12:40:28 +02:00
Johannes Zellner 91f506c17b Explicitly enable/disable ldap group sync 2020-06-04 12:28:31 +02:00
Girish Ramakrishnan 7a17695ad5 Retry in 10 seconds to not make things worse
Part of #690
2020-06-03 16:05:48 -07:00
Girish Ramakrishnan f5076c87d4 add to changes 2020-06-03 13:52:53 -07:00
Girish Ramakrishnan a47d6e1f3a cloudron-setup: --provider is dead
Long live --provider

Part of #693
2020-06-03 13:47:30 -07:00
Girish Ramakrishnan f6ff1abb00 cloudron-setup: remove --license arg. unused 2020-06-03 13:16:39 -07:00
Johannes Zellner 386aaf6470 Initial code to fetch LDAP groups during sync 2020-06-03 22:12:38 +02:00
Johannes Zellner 2b3c4cf0ff avatar blob now comes in only via branding api calls 2020-06-02 15:13:50 +02:00
Girish Ramakrishnan b602e921d0 better error message if domains exists 2020-06-01 16:11:02 -07:00
Girish Ramakrishnan 2fc3cdc2a2 remove superfluous debug 2020-06-01 09:40:56 -07:00
Girish Ramakrishnan e2cadbfc30 Fix uniqueness constraint in app passwords table
Fixes #688
2020-05-30 13:25:29 -07:00
Girish Ramakrishnan 3ffa935da7 Revert "part focal support"
This reverts commit 7d36533524.

not ready yet
2020-05-30 10:58:28 -07:00
Girish Ramakrishnan 5f539e331a 5.3.0 changes 2020-05-30 09:45:24 -07:00
Girish Ramakrishnan 356d0fabda Add note that pattern must match dashboard code 2020-05-30 09:44:33 -07:00
Girish Ramakrishnan 122ec75cb6 Fix links 2020-05-29 19:10:42 -07:00
Girish Ramakrishnan a3a48e1a49 poll for updates a bit more often 2020-05-29 13:39:16 -07:00
Girish Ramakrishnan 4ede765e1f typo: memoryLimit -> memory 2020-05-29 13:29:01 -07:00
Girish Ramakrishnan 4fa181b346 re-use the latest backup id for non-backupable apps
for stopped apps, as an example
2020-05-28 14:16:38 -07:00
Johannes Zellner 4f76d91ae9 Add backup_config settings API tests 2020-05-28 21:42:25 +02:00
Girish Ramakrishnan 20d1759fa5 Run update checker on stopped apps, we just don't update them 2020-05-28 12:41:51 -07:00
Girish Ramakrishnan 433e783ede do not allow backup, import, update in stopped state 2020-05-28 12:41:51 -07:00
Johannes Zellner 47f47d916d Fixup tests 2020-05-28 21:05:21 +02:00
Johannes Zellner b31ac7d1fd Revert backup policy fallback and check in rest api
Check is now in proper location at backups.testConfig()
2020-05-28 20:44:44 +02:00
Johannes Zellner ea47fb7305 Properly check for backup policy in testConfig() 2020-05-28 20:44:44 +02:00
Girish Ramakrishnan 82170f8f1b Fix failing test 2020-05-28 11:04:39 -07:00
Girish Ramakrishnan acb2655f58 rename variable (it ensures backup and may not actually backup) 2020-05-28 11:03:49 -07:00
Girish Ramakrishnan b1464517e6 centralize all the cron patterns in one place 2020-05-28 11:01:46 -07:00
Girish Ramakrishnan 151e6351f6 add couple of 5.2 changes 2020-05-28 09:37:57 -07:00
Johannes Zellner 154f768281 Forgot .length 2020-05-28 16:44:45 +02:00
Johannes Zellner 90c857e8fc Further validate retentionPolicy api input 2020-05-28 16:27:07 +02:00
Johannes Zellner 7a3efa2631 Ensure we get a proper retention policy for backups 2020-05-28 16:26:21 +02:00
Girish Ramakrishnan 38cc767f27 move up the backup cron to not overlap auto-updates 2020-05-27 23:04:04 -07:00
Girish Ramakrishnan e1a718c78f remove redundant call to canBackupApp 2020-05-27 22:48:48 -07:00
Girish Ramakrishnan 32a4450e5e 5.2.4 changes
(cherry picked from commit 2dc7342f09)
2020-05-27 22:35:30 -07:00
Girish Ramakrishnan fca3f606d2 Do not backup stopped apps 2020-05-27 21:04:01 -07:00
Girish Ramakrishnan 4a0a934a76 start using vhost style for accessing s3 style storage
if bucket name has a '.', accept self-signed

fixes #680
2020-05-27 17:50:37 -07:00
Girish Ramakrishnan f7c406bec9 s3: bucket name cannot contain _ or capitals or .
we can make it more elaborate, but not sure if it's needed

https://blogs.easydynamics.com/2016/10/24/aws-s3-bucket-name-validation-regex/
2020-05-27 17:01:42 -07:00
Girish Ramakrishnan f4807a6354 update many node modules 2020-05-27 16:52:22 -07:00
Girish Ramakrishnan 0960008b7b 5.2.4 changes
(cherry picked from commit 4267f5ea0a)
2020-05-26 17:07:03 -07:00
Girish Ramakrishnan 04a1aa38b4 Add CIFS as storage provider
part of #686
2020-05-26 15:31:45 -07:00
Girish Ramakrishnan f84622efa1 fs: add create/unlink tests 2020-05-26 15:31:41 -07:00
Girish Ramakrishnan f6c4614275 Do not restart stopped apps
(cherry picked from commit 2e76b8bed9)
2020-05-26 07:54:35 -07:00
Girish Ramakrishnan 7d36533524 part focal support
part of #684
2020-05-25 19:49:15 -07:00
Girish Ramakrishnan 5cd3df4869 better nginx config for higher loads 2020-05-25 15:25:00 -07:00
Girish Ramakrishnan b0480f48f3 Add changes 2020-05-24 20:12:19 -07:00
Girish Ramakrishnan 2e820c343a remove meaningless debug 2020-05-24 20:11:03 -07:00
Girish Ramakrishnan ce927a2247 Set dmode in tar extract 2020-05-24 20:08:17 -07:00
Girish Ramakrishnan ae810d59e9 mail: fix crash in audit logs 2020-05-24 18:50:10 -07:00
Girish Ramakrishnan 1438ee52a1 import: fix crash because encryption is unset 2020-05-24 18:42:04 -07:00
Girish Ramakrishnan de4b3e55fa Use apps.getAll so that app.fqdn is valid 2020-05-24 18:21:35 -07:00
Girish Ramakrishnan d2cd78c5cb more debug() removal 2020-05-24 12:30:48 -07:00
Girish Ramakrishnan d000719fa2 app health monitor is too verbose 2020-05-24 11:43:17 -07:00
Girish Ramakrishnan efea4ed615 more debug() removal 2020-05-24 11:35:31 -07:00
Girish Ramakrishnan 67a931c4b8 Remove verbose logs 2020-05-24 11:33:53 -07:00
Girish Ramakrishnan bdcc5c0629 Mbps -> MBps
Fixes #682
2020-05-23 13:31:23 -07:00
Girish Ramakrishnan d113cfc0ba add comment on how often du value is stored 2020-05-22 20:06:45 -07:00
Girish Ramakrishnan 4a3ab50878 5.2.1 changes 2020-05-22 18:49:28 -07:00
Girish Ramakrishnan b39261c8cf remove extra $ 2020-05-22 16:56:01 -07:00
Girish Ramakrishnan 7efb57c8da restart apps on addon container change
when the IP changes on addon container re-create, the apps don't
detect this (maybe there is some large DNS cache timeout in docker)
2020-05-22 16:45:03 -07:00
Girish Ramakrishnan 90c24cf356 add cleanup policy test 2020-05-21 14:30:21 -07:00
Girish Ramakrishnan 54abada561 backups: add progressCallback to cleanup funcs 2020-05-21 13:46:16 -07:00
Girish Ramakrishnan f1922660be add a new line 2020-05-21 10:57:57 -07:00
Girish Ramakrishnan 795e3c57da Add a header for encrypted backup files
this is required to identify old backups and new backups for decryption
2020-05-20 22:44:26 -07:00
Girish Ramakrishnan 3f201464a5 Fix bug where SRS translation was done on the main domain instead of mailing list domain 2020-05-20 21:55:48 -07:00
Girish Ramakrishnan 8ac0be6bb5 Update postgresql for schema ownership fix 2020-05-20 16:44:32 -07:00
Johannes Zellner 130805e7bd Add changes 2020-05-19 14:59:28 +02:00
Girish Ramakrishnan b8c7357fea redis: if container inactive, return stopped status 2020-05-18 14:43:23 -07:00
Girish Ramakrishnan 819f8e338f stop app now stops it's services as well 2020-05-18 14:33:07 -07:00
Girish Ramakrishnan 9569e46ff8 use docker.restart instead of start/stop since it is atomic 2020-05-18 13:35:42 -07:00
Girish Ramakrishnan b7baab2d0f restore: set encryption to null 2020-05-18 09:07:18 -07:00
Girish Ramakrishnan e2d284797d set HOME explicity when calling migrate script 2020-05-17 21:50:50 -07:00
Girish Ramakrishnan a3ac343fe2 installer: print from and to versions 2020-05-17 21:34:39 -07:00
Girish Ramakrishnan dadde96e41 remove login events from addons
more often then not this just spams the eventlog
2020-05-15 21:40:34 -07:00
Girish Ramakrishnan 99475c51e8 fix encryption of 0-length files 2020-05-15 16:05:12 -07:00
Girish Ramakrishnan cc9b4e26b5 use done event to signal write success (just like in extract) 2020-05-15 15:24:12 -07:00
Girish Ramakrishnan 32f232d3c0 destroy input stream on error 2020-05-15 15:21:24 -07:00
Girish Ramakrishnan 235047ad0b bind to source stream error event immediately
download() is async and the source stream error is missed
2020-05-15 14:54:05 -07:00
Girish Ramakrishnan 228f75de0b better error messages 2020-05-15 14:35:19 -07:00
Girish Ramakrishnan 2f89e7e2b4 drop NET_RAW since this allows packet sniffing
this however breaks ping
2020-05-15 12:47:36 -07:00
Girish Ramakrishnan 437f39deb3 More changes 2020-05-15 09:16:24 -07:00
Girish Ramakrishnan 59582f16c4 skip validation in the route 2020-05-14 21:45:13 -07:00
Girish Ramakrishnan af9e3e38ce apply backup retention policy
part of #441
2020-05-14 21:31:24 -07:00
Girish Ramakrishnan d992702b87 rename to keepWithinSecs
part of #441
2020-05-14 16:45:28 -07:00
Girish Ramakrishnan 6a9fe1128f move retentionSecs inside retentionPolicy
part of #441
2020-05-14 16:33:29 -07:00
Johannes Zellner 573da29a4d Once upon a time where settings worked 2020-05-14 23:35:03 +02:00
Johannes Zellner 00cff1a728 Mention that SECRET_PLACEHOLDER is also used in dashboard client.js 2020-05-14 23:04:08 +02:00
Johannes Zellner 9bdeff0a39 Always use constants.SECRET_PLACEHOLDER 2020-05-14 23:02:02 +02:00
Girish Ramakrishnan a1f263c048 stash the backup password in filesystem for safety
we will add a release note asking the user to nuke it
2020-05-14 12:59:37 -07:00
Girish Ramakrishnan 346eac389c bind ui is hidden for this release 2020-05-14 11:57:12 -07:00
Johannes Zellner f52c16b209 Ensure encryption property on backup config always exists 2020-05-14 20:22:10 +02:00
Girish Ramakrishnan 4faf880aa4 Fix crash with unencrypted backups 2020-05-14 11:18:41 -07:00
Girish Ramakrishnan f417a49b34 Add encryptionVersion to backups
this will identify the old style backups and warn user that a restore
doesn't work anymore
2020-05-13 22:37:02 -07:00
Girish Ramakrishnan 66fd713d12 rename version to packageVersion 2020-05-13 21:55:50 -07:00
Girish Ramakrishnan 2e7630f97e remove stale logs 2020-05-13 19:23:04 -07:00
Girish Ramakrishnan 3f10524532 cleanup cache file to start encrypted rsync backups afresh 2020-05-13 16:35:13 -07:00
Johannes Zellner 51f9826918 Strip quotes for TXT records on name.com
The docs and support claim quotes are needed, but the actual API usage
shows otherwise. We do this to not break users, but ideally name.com
gives a correct and clear answer
2020-05-14 01:03:10 +02:00
Girish Ramakrishnan f5bb76333b do hmac validation on filename iv as well
also, pass encryption object instead of config
2020-05-13 10:11:07 -07:00
Girish Ramakrishnan 4947faa5ca update mail container 2020-05-12 23:19:31 -07:00
Girish Ramakrishnan 101dc3a93c s3: do not retry when testing config 2020-05-12 22:45:01 -07:00
Girish Ramakrishnan bd3ee0fa24 add changes 2020-05-12 22:00:05 -07:00
Girish Ramakrishnan 2c52668a74 remove format validation in provider config 2020-05-12 22:00:01 -07:00
Girish Ramakrishnan 03edd8c96b remove max_old_space_size
we have limited understanding of this option
2020-05-12 20:14:35 -07:00
Girish Ramakrishnan 37dfa41e01 Add hmac to the file data
https://stackoverflow.com/questions/10279403/confused-how-to-use-aes-and-hmac
https://en.wikipedia.org/wiki/Padding_oracle_attack

part of #579
2020-05-12 19:59:06 -07:00
Girish Ramakrishnan ea8a3d798e create encryption keys from password during app import & restore 2020-05-12 15:53:18 -07:00
Girish Ramakrishnan 1df94fd84d backups: generate keys from password
this also removes storage of password from db

part of #579
2020-05-12 15:14:51 -07:00
Girish Ramakrishnan 5af957dc9c add changes
part of #579
2020-05-12 10:56:07 -07:00
Girish Ramakrishnan 21073c627e rename backup key to password
Fixes #579
2020-05-12 10:55:10 -07:00
Girish Ramakrishnan 66cdba9c1a remove chat link in readme 2020-05-12 10:21:21 -07:00
Girish Ramakrishnan 56d3b38ce6 read/write iv in the encrypted files
part of #579
2020-05-11 22:35:25 -07:00
Girish Ramakrishnan 15d0275045 key must atleast be 8 chars
part of #579
2020-05-11 16:11:41 -07:00
Girish Ramakrishnan 991c1a0137 check if manifest property is present in network response 2020-05-11 14:52:55 -07:00
Girish Ramakrishnan 7d549dbbd5 logrotate: add some comments 2020-05-11 14:38:50 -07:00
Johannes Zellner e27c5583bb Apps without dockerImage cannot be auto-updated 2020-05-11 23:20:17 +02:00
Girish Ramakrishnan 650c49637f logrotate: Add turn service logs 2020-05-11 13:14:52 -07:00
Girish Ramakrishnan eb5dcf1c3e typo 2020-05-11 11:58:14 -07:00
Girish Ramakrishnan ed2b61b709 Add to changes 2020-05-10 15:35:06 -07:00
Girish Ramakrishnan 41466a3018 No need to poll every hour for updates! 2020-05-06 18:58:35 -07:00
Girish Ramakrishnan 2e130ef99d Add automatic flag for update checks
The appstore can then known if a user clicked the check for updates
button manually or if it was done by the automatic updater.

We will fix appstore so that updates are always provided for manual checks.
automatic updates will follow our roll out plan.

We do have one issue that the automatic update checker will reset the manual
updates when it runs, but this is OK.
2020-05-06 18:57:59 -07:00
Girish Ramakrishnan a96fb39a82 mail relay: fix delivery event log 2020-05-05 20:34:45 -07:00
Girish Ramakrishnan c9923c8d4b spam: large emails were not scanned 2020-05-05 15:23:27 -07:00
Girish Ramakrishnan 74b0ff338b Disallow cloudtorrent in demo mode 2020-05-04 14:56:10 -07:00
Girish Ramakrishnan dcaccc2d7a add redis status
part of #671
2020-05-03 19:46:07 -07:00
Johannes Zellner d60714e4e6 Use webmaster@ instead of support@ as LetsEncrypt fallback 2020-05-03 11:02:18 +02:00
Girish Ramakrishnan d513d5d887 appstore: Better error messages 2020-05-02 18:30:44 -07:00
Girish Ramakrishnan 386566fd4b Fcf: ix crash when no email provide with global key 2020-05-02 18:06:21 -07:00
Girish Ramakrishnan 3357ca76fe specify the invalid bind name in error message 2020-05-02 11:07:58 -07:00
Girish Ramakrishnan a183ce13ee put the status code in the error message 2020-04-30 09:24:22 -07:00
Girish Ramakrishnan e9d0ed8e1e Add binds support to containers 2020-04-29 22:51:46 -07:00
Girish Ramakrishnan 66f66fd14f docker: clean up volume API 2020-04-29 21:28:49 -07:00
Girish Ramakrishnan b49d30b477 Add OVH Object Storage backend 2020-04-29 12:47:57 -07:00
Girish Ramakrishnan 73d83ec57e Ensure stopped apps are getting backed up 2020-04-29 12:05:01 -07:00
Girish Ramakrishnan efb39fb24b refactor for addon/service/container consistency
addon - app manifest thing. part of app lifecycle
services - implementation of addon (may have containers assoc)
2020-04-28 15:32:02 -07:00
Girish Ramakrishnan 73623f2e92 add serviceConfig to appdb
part of #671
2020-04-28 15:31:58 -07:00
Girish Ramakrishnan fbcc4cfa50 Rename KNOWN_ADDONS to ADDONS 2020-04-27 22:59:35 -07:00
Girish Ramakrishnan 474a3548e0 Rename KNOWN_SERVICES to SERVICES 2020-04-27 22:59:11 -07:00
Girish Ramakrishnan 2cdf68379b Revert "add volume support"
This reverts commit b8bb69f730.

Revert this for now, we will try a simpler non-object volume first
2020-04-27 22:55:43 -07:00
Girish Ramakrishnan cc8509f8eb More 5.2 changes 2020-04-26 22:28:43 -07:00
Girish Ramakrishnan a520c1b1cb Update all docker images to use base image 2.0.0 2020-04-26 17:09:31 -07:00
Girish Ramakrishnan 75fc2cbcfb Update base image 2020-04-25 10:37:08 -07:00
Girish Ramakrishnan b8bb69f730 add volume support
part of #668, #569
2020-04-24 22:09:07 -07:00
Girish Ramakrishnan b46d3e74d6 Fix crash in cloudflare error handling 2020-04-23 12:07:54 -07:00
Girish Ramakrishnan 77a1613107 test: fix alias routes 2020-04-22 18:16:33 -07:00
Girish Ramakrishnan 62fab7b09f mail: allow alternate mx 2020-04-22 17:36:34 -07:00
Johannes Zellner 5d87352b28 backupId cannot be null during restore 2020-04-21 16:00:19 +02:00
Girish Ramakrishnan ff60f5a381 move aliases route under mailbox
since aliases can now span domains

fixes #577
2020-04-20 19:17:55 -07:00
Girish Ramakrishnan 7f666d9369 mail: implement aliases across domains
part of #577
2020-04-20 15:19:48 -07:00
Girish Ramakrishnan 442f16dbd0 more changes 2020-04-18 22:56:38 -07:00
Girish Ramakrishnan 2dcab77ed1 Fix issue where app with oauth addon will not backup or uninstall 2020-04-18 10:08:20 -07:00
Girish Ramakrishnan 13be04a169 Deny non-member email immediately 2020-04-18 02:51:31 -07:00
Girish Ramakrishnan e3767c3a54 remove obsolete isadmin flag 2020-04-18 02:32:17 -07:00
Girish Ramakrishnan ce957c8dd5 update mail container 2020-04-18 02:31:59 -07:00
Girish Ramakrishnan 0606b2994c add membersOnly flag to a mailing list 2020-04-17 17:44:14 -07:00
Girish Ramakrishnan 33acccbaaa only check the p key for dkim
this less-strict DKIM check allows users to set a stronger DKIM key
2020-04-17 12:45:21 -07:00
Girish Ramakrishnan 1e097abe86 Add note on dkim key length 2020-04-17 10:29:14 -07:00
Girish Ramakrishnan e51705c41d acme: request ECC certs 2020-04-17 10:22:01 -07:00
Girish Ramakrishnan 7eafa661fe check .well-known presence upstream
this is required for apps like nextcloud which have caldav/cardav
routes
2020-04-15 16:56:41 -07:00
Girish Ramakrishnan 2fe323e587 remove bogus internal route 2020-04-14 23:11:44 -07:00
Girish Ramakrishnan 4e608d04dc 5.1.4 changes 2020-04-11 18:45:39 -07:00
Girish Ramakrishnan 531d314e25 Show error message if gpg failed 2020-04-11 17:11:55 -07:00
Girish Ramakrishnan 1ab23d2902 fix indexOf value comparison 2020-04-11 14:21:05 -07:00
Girish Ramakrishnan b3496e1354 Add ECDHE-RSA-AES128-SHA256 to cipher list
one of our users had the site reverse proxied. it broke after the
5.1 cipher change and they nailed it down to using this cipher.

https://security.stackexchange.com/questions/72926/is-tls-ecdhe-rsa-with-aes-128-cbc-sha256-a-safe-cipher-suite-to-use
says this is safe

The following prints the cipher suite:

    log_format combined2 '$remote_addr - [$time_local] '
        '$ssl_protocol/$ssl_cipher '
        '"$request" $status $body_bytes_sent $request_time '
        '"$http_referer" "$host" "$http_user_agent"';
2020-04-10 09:49:06 -07:00
Girish Ramakrishnan 2efa0aaca4 serve custom well-known documents via nginx 2020-04-09 00:15:56 -07:00
Girish Ramakrishnan ef9aeb0772 Bump default version for tests 2020-04-08 14:24:58 -07:00
Girish Ramakrishnan 924a0136eb 5.1.3 changes 2020-04-08 13:52:53 -07:00
Girish Ramakrishnan c382fc375e Set the resetTokenCreationTime in invitation links 2020-04-08 13:11:24 -07:00
Girish Ramakrishnan 2544acddfa Fix crash with misconfigured reverse proxy
https://forum.cloudron.io/topic/2288/mastodon-terminal-not-starting
2020-04-08 09:43:43 -07:00
Johannes Zellner 58072892d6 Add 5.1.2 changes 2020-04-08 11:52:32 +02:00
Johannes Zellner 85a897c78c Remove console.log debug leftover 2020-04-08 11:48:12 +02:00
Girish Ramakrishnan 6adf5772d8 update turn config to prevent internal access
https://www.rtcsec.com/2020/04/01-slack-webrtc-turn-compromise/
2020-04-07 15:37:31 -07:00
Girish Ramakrishnan f98e3b1960 more 5.1.1 changes 2020-04-03 10:41:37 -07:00
Johannes Zellner 671a967e35 Add 5.1.1 changes 2020-04-03 13:33:03 +02:00
Johannes Zellner 950ef0074f Add libcurl3-gnutls as explicit dependency 2020-04-03 09:45:03 +02:00
Girish Ramakrishnan 5515324fd4 coturn -> turn in docker repo name 2020-04-02 19:51:14 -07:00
Girish Ramakrishnan e72622ed4f Fix crash during auto-update 2020-04-02 19:47:29 -07:00
Girish Ramakrishnan e821733a58 add note on exposed ports 2020-04-02 18:09:26 -07:00
Girish Ramakrishnan a03c0e4475 mail: disable hostname validation 2020-04-02 15:00:11 -07:00
Girish Ramakrishnan 3203821546 typo 2020-04-02 12:29:20 -07:00
Girish Ramakrishnan 16f3cee5c5 install custom nginx only on xenial
https://nginx.org/en/linux_packages.html#Ubuntu
http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/
2020-04-02 11:54:22 -07:00
Johannes Zellner 57afb46cbd Ensure nginx installation will not overwrite our conf files 2020-04-02 16:57:55 +02:00
Johannes Zellner 91dde5147a add-apt-repository does not call apt-get update 2020-04-02 13:54:39 +02:00
Johannes Zellner d0692f7379 Ensure we have latest nginx 2020-04-02 12:37:02 +02:00
Girish Ramakrishnan e360658c6e More changes 2020-04-01 17:00:01 -07:00
Girish Ramakrishnan e7dc77e6de bump mail container for mailbox size fix 2020-04-01 16:31:07 -07:00
Girish Ramakrishnan e240a8b58f add comment on the struct 2020-04-01 16:26:16 -07:00
Girish Ramakrishnan 38d4f2c27b Add note on what df output is 2020-04-01 15:59:48 -07:00
Girish Ramakrishnan 552e2a036c Use block size instead of apparent size in du
https://stackoverflow.com/questions/5694741/why-is-the-output-of-du-often-so-different-from-du-b

df uses superblock info to get consumed blocks/disk size. du with -b
prints actual file size instead of the disk space used by the files.
2020-04-01 15:24:53 -07:00
Johannes Zellner 2d4b978032 It will be 5.1.0 2020-04-01 22:30:50 +02:00
Johannes Zellner 36e00f0c84 We will release a 5.0.7 patch release first 2020-04-01 22:26:23 +02:00
Johannes Zellner ef64b2b945 Use coturn addon tag 1.0.0 2020-04-01 21:50:21 +02:00
Johannes Zellner f6cd33ae24 Set turn secret for apps 2020-04-01 21:50:09 +02:00
Girish Ramakrishnan dd109f149f mail: fix eventlog db perms 2020-04-01 12:24:54 -07:00
Girish Ramakrishnan 5b62d63463 clear mailbox on update and restore
part of #669
2020-03-31 17:51:27 -07:00
Girish Ramakrishnan 3fec599c0c remove mail domain add/remove API
merge this as a transaction into domains API

fixes #669
2020-03-31 14:48:19 -07:00
Girish Ramakrishnan e30ea9f143 make mailbox domain nullable
for apps that do not use sendmail/recvmail addon, these are now null.
otherwise, there is no way to edit the mailbox in the UI

part of #669
2020-03-31 11:26:19 -07:00
Johannes Zellner 7cb0c31c59 Also restart turn server on dashboard domain change 2020-03-31 14:52:09 +02:00
Johannes Zellner b00a7e3cbb Update turn addon 2020-03-31 10:55:41 +02:00
Johannes Zellner e63446ffa2 Support persistent turn secret 2020-03-31 09:28:57 +02:00
Girish Ramakrishnan 580da19bc2 Less strict dmarc validation
fixes #666
2020-03-30 19:32:25 -07:00
Girish Ramakrishnan 936f456cec make reset tokens only valid for a day
fixes #563

mysql timestamps cannot be null. it will become current timestamp when
set as null
2020-03-30 17:13:31 -07:00
Girish Ramakrishnan 5d6a02f73c mysql: create the my.cnf in run time dir 2020-03-30 16:32:54 -07:00
Girish Ramakrishnan b345195ea9 add missing fields in users table 2020-03-30 16:32:28 -07:00
Girish Ramakrishnan 3e6b66751c typoe in assert 2020-03-30 15:17:34 -07:00
Johannes Zellner f78571e46d Support reserved port ranges 2020-03-30 10:01:52 +02:00
Johannes Zellner f52000958c Update manifest format to 5.1.1 2020-03-30 08:43:28 +02:00
Johannes Zellner 5ac9c6ce02 add turn,stun ports to RESERVED ones
We still need to protect the TURN port range
2020-03-30 08:30:06 +02:00
Johannes Zellner 1110a67483 Add turn addon setup and teardown calls 2020-03-30 08:24:52 +02:00
Girish Ramakrishnan 57bb1280f8 better error message 2020-03-29 20:12:59 -07:00
Girish Ramakrishnan 25c000599f Fix assert (appStoreId is optional) 2020-03-29 19:12:07 -07:00
Girish Ramakrishnan 86f45e2769 Fix failing test 2020-03-29 18:55:44 -07:00
Girish Ramakrishnan 7110240e73 Only a Cloudron owner can install/update/exec apps with the docker addon
this should have been part of f1975d8f2b
2020-03-29 18:52:37 -07:00
Girish Ramakrishnan 1da37b66d8 use resource pattern in apps routes
this makes it easy to implement access control in route handlers
2020-03-29 17:11:10 -07:00
Girish Ramakrishnan f1975d8f2b only owner can install/repair/update/exec docker addon apps 2020-03-29 16:24:04 -07:00
Girish Ramakrishnan f407ce734a restrict the app to bind mount under /app/data only
rest have to be volumes
2020-03-29 13:57:45 -07:00
Girish Ramakrishnan f813cfa8db Listen only on the docker interface 2020-03-29 13:11:16 -07:00
Girish Ramakrishnan d5880cb953 TODO block is obsolete 2020-03-29 13:10:19 -07:00
Girish Ramakrishnan 95da9744c1 Prefix env vars with CLOUDRON_ 2020-03-29 09:35:34 -07:00
Girish Ramakrishnan 85c3e45cde remove oauth addon code 2020-03-29 09:35:34 -07:00
Johannes Zellner 520a396ded Use turn server with certificates 2020-03-29 09:32:48 +02:00
Johannes Zellner 13ad611c96 Remove ssh related settings from the turn container config 2020-03-29 09:32:48 +02:00
Girish Ramakrishnan 85f58d9681 more changes 2020-03-28 23:10:17 -07:00
Johannes Zellner c1de62acef Update coturn 2020-03-29 07:30:42 +02:00
Johannes Zellner 7e47e36773 Fix portrange notation in firewall service 2020-03-29 07:25:36 +02:00
Johannes Zellner 00b6217cab Fix turn tls port 2020-03-29 07:09:17 +02:00
Girish Ramakrishnan acc2b5a1a3 remove unused param 2020-03-28 22:05:43 -07:00
Girish Ramakrishnan b06feaa36b more changes 2020-03-28 17:48:55 -07:00
Johannes Zellner 89cf8a455a Allow turn and stun service ports 2020-03-28 23:33:44 +01:00
Johannes Zellner 710046a94f Add coturn addon service 2020-03-28 22:46:32 +01:00
Johannes Zellner b366b0fa6a Stop container with isCloudronManged labels instead of by network 2020-03-28 22:46:32 +01:00
Girish Ramakrishnan f9e7a8207a cloudron-support: make it --owner-login 2020-03-27 18:58:12 -07:00
Johannes Zellner 6178bf3d4b Update sftp addon 2020-03-27 14:54:35 +01:00
Girish Ramakrishnan f3b979f112 More 5.0.6 changelog 2020-03-26 21:56:18 -07:00
Girish Ramakrishnan 9faae96d61 make app password work with sftp 2020-03-26 21:50:25 -07:00
Girish Ramakrishnan 2135fe5dd0 5.0.6 changelog
(cherry picked from commit 3c1a1f1b81)
2020-03-26 19:32:58 -07:00
Girish Ramakrishnan 007a8d248d make eventlog routes owner only 2020-03-26 18:54:16 -07:00
Girish Ramakrishnan 58d4a3455b email: add type filter to eventlog 2020-03-25 22:05:49 -07:00
Girish Ramakrishnan 8e3c14f245 5.0.5 changes
(cherry picked from commit cc6ddf50b1)
2020-03-25 08:13:38 -07:00
Girish Ramakrishnan 91af2495a6 Make key validation work for ecc certs 2020-03-24 21:20:21 -07:00
Girish Ramakrishnan 7d7df5247b Update cipher suite based on ssl-config recommendation
ssl_prefer_server_ciphers off is the recommendation since the cpihers
are deprecated

https://serverfault.com/questions/997614/setting-ssl-prefer-server-ciphers-directive-in-nginx-config
2020-03-24 19:24:58 -07:00
Girish Ramakrishnan f99450d264 Enable TLSv1.3 and remove TLSv1 and 1.1
IE10 does not have 1.2, so maybe we can risk it

As per Android documentaion TLS 1.2 is fully supported after API level 20/Android 5(Lolipop)

https://discussions.qualys.com/thread/17020-tls-12-support-for-android-devices
https://www.ryandesignstudio.com/what-is-tls/
2020-03-24 14:37:08 -07:00
Girish Ramakrishnan d3eeb5f48a mail: disable host and proto mismatch 2020-03-24 11:50:52 -07:00
Girish Ramakrishnan 1e8a02f91a Make token expiry a year
we now have a UI to invalid all tokens easily, so this should be OK.
2020-03-23 21:51:13 -07:00
Girish Ramakrishnan 97c3bd8b8e mail: incoming mail from dynamic hostnames was rejected 2020-03-23 21:50:36 -07:00
Girish Ramakrishnan 09ce27d74b bump default token expiry to a month 2020-03-21 18:46:38 -07:00
Girish Ramakrishnan 2447e91a9f mail: throttle denied events 2020-03-20 14:04:16 -07:00
Girish Ramakrishnan e6d881b75d Use owner email for LE certs
https://forum.cloudron.io/topic/2244/email-contact-on-let-s-encrypt-ssl-tls-certificates-uses-password-recovery-email-rather-than-primary-email-address
2020-03-20 13:39:58 -07:00
Girish Ramakrishnan 36f963dce8 remove unncessary debug in routes 2020-03-19 17:05:31 -07:00
Girish Ramakrishnan 1b15d28212 eventlog: add start/stop/restart logs 2020-03-19 17:02:55 -07:00
Girish Ramakrishnan 4e0c15e102 use short form syntax 2020-03-19 16:48:31 -07:00
Girish Ramakrishnan c9e40f59de bump the timeout for really slow disks 2020-03-19 13:33:53 -07:00
Girish Ramakrishnan 38cf31885c Make backup configure owner only 2020-03-18 17:23:23 -07:00
Girish Ramakrishnan 4420470242 comcast does not allow port 25 check anymore 2020-03-17 13:55:35 -07:00
Girish Ramakrishnan 9b05786615 appstore: add whitelist/blacklist 2020-03-15 17:20:48 -07:00
Girish Ramakrishnan 725b2c81ee custom.yml is obsolete 2020-03-15 16:50:42 -07:00
Girish Ramakrishnan 661965f2e0 Add branding tests 2020-03-15 16:38:15 -07:00
Girish Ramakrishnan 7e0ef60305 Fix incorrect role comparison 2020-03-15 16:19:22 -07:00
Girish Ramakrishnan 2ac0fe21c6 ghost file depends on base dir 2020-03-15 11:41:39 -07:00
Girish Ramakrishnan b997f2329d make branding route for owner only 2020-03-15 11:39:02 -07:00
Girish Ramakrishnan 23ee758ac9 do not check for updates for stopped apps 2020-03-15 09:48:08 -07:00
Girish Ramakrishnan 9ea12e71f0 linode: dns backend
the dns is very slow - https://github.com/certbot/certbot/pull/6320
takes a good 15 minutes at minimum to propagate

https://certbot-dns-linode.readthedocs.io/en/stable/
https://www.linode.com/community/questions/17296/linode-dns-propagation-time
2020-03-13 11:44:43 -07:00
Girish Ramakrishnan d3594c2dd6 change ownership of ghost file for good measure 2020-03-12 10:30:51 -07:00
Girish Ramakrishnan 6ee4b0da27 Move out ghost file to platformdata
Since /tmp is world writable this might cause privilege escalation

https://forum.cloudron.io/topic/2222/impersonate-user-privilege-escalation
2020-03-12 10:24:21 -07:00
Girish Ramakrishnan 3e66feb514 mail: add mailbox acl 2020-03-10 22:12:15 -07:00
Girish Ramakrishnan cd91a5ef64 5.0.3 changes 2020-03-10 17:18:21 -07:00
Girish Ramakrishnan cf89609633 mail: acl was enabled by mistake 2020-03-10 17:15:23 -07:00
Girish Ramakrishnan 67c24c1282 mail: make spamd_user case insensitive 2020-03-10 12:08:43 -07:00
Girish Ramakrishnan 7d3df3c55f Fix sa usage 2020-03-10 09:22:41 -07:00
Girish Ramakrishnan dfe5cec46f Show the public IP to finish setup 2020-03-09 15:18:39 -07:00
Girish Ramakrishnan 17c881da47 Fix spam training 2020-03-09 13:51:17 -07:00
Girish Ramakrishnan 6e30c4917c Do not wait for dns when re-configured 2020-03-09 12:36:29 -07:00
Girish Ramakrishnan c6d4f0d2f0 mail: fix word boundary regexp 2020-03-07 19:16:10 -08:00
Girish Ramakrishnan b32128bebf Fix quoting in emails 2020-03-07 19:12:39 -08:00
Girish Ramakrishnan a3f3d86908 More spam fixes 2020-03-07 18:52:20 -08:00
135 changed files with 6255 additions and 3762 deletions
+223
View File
@@ -1806,3 +1806,226 @@
* restore: carefully replace backup config
* spam: add default corpus and global db
[5.0.2]
* Show backup disk usage in graphs
* Add per-user app passwords
* Make app not responding page customizable
* Make footer customizable
* Add UI to import backups
* Display timestamps in browser timezone in the UI
* Mail eventlog and usage
* Add user roles - owner, admin, user manager and user
* Setup logrotate configs for collectd since upstream does not set it up
* mail: Add X-Envelope-To and X-Envelope-From headers for incoming mails
* linode: add object storage backend
* restore: carefully replace backup config
* spam: per mailbox bayes db and training
[5.0.3]
* Show backup disk usage in graphs
* Add per-user app passwords
* Make app not responding page customizable
* Make footer customizable
* Add UI to import backups
* Display timestamps in browser timezone in the UI
* Mail eventlog and usage
* Add user roles - owner, admin, user manager and user
* Setup logrotate configs for collectd since upstream does not set it up
* mail: Add X-Envelope-To and X-Envelope-From headers for incoming mails
* linode: add object storage backend
* restore: carefully replace backup config
* spam: per mailbox bayes db and training
[5.0.4]
* Fix potential previlige escalation because of ghost file
* linode: dns backend
* make branding routes owner only
* add branding API
* Add app start/stop/restart events
* Use the primary email for LE account
* make mail eventlog more descriptive
[5.0.5]
* Fix bug where incoming mail from dynamic hostnames was rejected
* Increase token expiry
* Fix bug in tag UI where tag removal did not work
[5.0.6]
* Make mail eventlog only visible to owners
* Make app password work with sftp
[5.1.0]
* Add turn addon
* Fix disk usage display
* Drop support for TLSv1 and TLSv1.1
* Make cert validation work for ECC certs
* Add type filter to mail eventlog
* mail: Fix listing of mailboxes and aliases in the UI
* branding: fix login page title
* Only a Cloudron owner can install/update/exec apps with the docker addon
* security: reset tokens are only valid for a day
* mail: fix eventlog db perms
* Fix various bugs in the disk graphs
[5.1.1]
* Add turn addon
* Fix disk usage display
* Drop support for TLSv1 and TLSv1.1
* Make cert validation work for ECC certs
* Add type filter to mail eventlog
* mail: Fix listing of mailboxes and aliases in the UI
* branding: fix login page title
* Only a Cloudron owner can install/update/exec apps with the docker addon
* security: reset tokens are only valid for a day
* mail: fix eventlog db perms
* Fix various bugs in the disk graphs
* Fix collectd installation
* graphs: sort disk contents by usage
* backups: show apps that are not automatically backed up in backup view
[5.1.2]
* Add turn addon
* Fix disk usage display
* Drop support for TLSv1 and TLSv1.1
* Make cert validation work for ECC certs
* Add type filter to mail eventlog
* mail: Fix listing of mailboxes and aliases in the UI
* branding: fix login page title
* Only a Cloudron owner can install/update/exec apps with the docker addon
* security: reset tokens are only valid for a day
* mail: fix eventlog db perms
* Fix various bugs in the disk graphs
* Fix collectd installation
* graphs: sort disk contents by usage
* backups: show apps that are not automatically backed up in backup view
* turn: deny local address peers https://www.rtcsec.com/2020/04/01-slack-webrtc-turn-compromise/
[5.1.3]
* Fix crash with misconfigured reverse proxy
* Fix issue where invitation links are not working anymore
[5.1.4]
* Add support for custom .well-known documents to be served
* Add ECDHE-RSA-AES128-SHA256 to cipher list
* Fix GPG signature verification
[5.1.5]
* Check for .well-known routes upstream as fallback. This broke nextcloud's caldav/carddav
[5.2.0]
* acme: request ECC certs
* less-strict DKIM check to allow users to set a stronger DKIM key
* Add members only flag to mailing list
* oauth: add backward compat layer for backup and uninstall
* fix bug in disk usage sorting
* mail: aliases can be across domains
* mail: allow an external MX to be set
* Add UI to download backup config as JSON (and import it)
* Ensure stopped apps are getting backed up
* Add OVH Object Storage backend
* Add per-app redis status and configuration to Services
* spam: large emails were not scanned
* mail relay: fix delivery event log
* manual update check always gets the latest updates
* graphs: fix issue where large number of apps would crash the box code (query param limit exceeded)
* backups: fix various security issues in encypted backups (thanks @mehdi)
* graphs: add app graphs
* older encrypted backups cannot be used in this version
* Add backup listing UI
* stopping an app will stop dependent services
* Add new wasabi s3 storage region us-east-2
* mail: Fix bug where SRS translation was done on the main domain instead of mailing list domain
* backups: add retention policy
* Drop `NET_RAW` caps from container preventing sniffing of network traffic
[5.2.1]
* Fix app disk graphs
* restart apps on addon container change
[5.2.2]
* regression: import UI
* Mbps -> MBps
* Remove verbose logs
* Set dmode in tar extract
* mail: fix crash in audit logs
* import: fix crash because encryption is unset
* create redis with the correct label
[5.2.3]
* Do not restart stopped apps
[5.2.4]
* mail: enable/disable incoming mail was showing an error
* Do not trigger backup of stopped apps. Instead, we will just retain it's existing backups
based on retention policy
* remove broken disk graphs
* fix OVH backups
[5.3.0]
* better nginx config for higher loads
* backups: add CIFS storage provider
* backups: add SSHFS storage provider
* backups: add NFS storage provider
* s3: use vhost style
* Fix crash when redis config was set
* Update schedule was unselected in the UI
* cloudron-setup: --provider is now optional
* show warning for unstable updates
* add forumUrl to app manifest
* postgresql: add unaccent extension for peertube
* mail: Add Auto-Submitted header to NDRs
* backups: ensure that the latest backup of installed apps is always preserved
* add nginx logs
* mail: make authentication case insensitive
* Fix timeout issues in postgresql and mysql addon
* Do not count stopped apps for memory use
* LDAP group synchronization
[5.3.1]
* better nginx config for higher loads
* backups: add CIFS storage provider
* backups: add SSHFS storage provider
* backups: add NFS storage provider
* s3: use vhost style
* Fix crash when redis config was set
* Update schedule was unselected in the UI
* cloudron-setup: --provider is now optional
* show warning for unstable updates
* add forumUrl to app manifest
* postgresql: add unaccent extension for peertube
* mail: Add Auto-Submitted header to NDRs
* backups: ensure that the latest backup of installed apps is always preserved
* add nginx logs
* mail: make authentication case insensitive
* Fix timeout issues in postgresql and mysql addon
* Do not count stopped apps for memory use
* LDAP group synchronization
[5.3.2]
* Do not install sshfs package
* 'provider' is not required anymore in various API calls
* redis: Set maxmemory and maxmemory-policy
* Add mlock capability to manifest (for vault app)
[5.3.3]
* Fix issue where some postinstall messages where causing angular to infinite loop
[5.3.4]
* Fix issue in database error handling
[5.4.0]
* Update nginx to 1.18 for various security fixes
* Add ping capability (for statping app)
* Fix bug where aliases were displayed incorrectly in SOGo
* Add univention as LDAP provider
* Bump max_connection for postgres addon to 200
* mail: Add pagination to mailing list API
* Allow admin to lock email and display name of users
* Allow admin to ensure all users have 2FA setup
* ami: fix regression where we didn't send provider as part of get status call
* nginx: hide version
* backups: add b2 provider
* Add filemanager webinterface
* Add darkmode
* Add note that password reset and invite links expire in 24 hours
+1 -11
View File
@@ -48,18 +48,8 @@ the dashboard, database addons, graph container, base image etc. Cloudron also r
on external services such as the App Store for apps to be installed. As such, don't
clone this repo and npm install and expect something to work.
## Documentation
## Support
* [Documentation](https://cloudron.io/documentation/)
## Related repos
The [base image repo](https://git.cloudron.io/cloudron/docker-base-image) is the parent image of all
the containers in the Cloudron.
## Community
* [Chat](https://chat.cloudron.io)
* [Forum](https://forum.cloudron.io/)
* [Support](mailto:support@cloudron.io)
+12 -7
View File
@@ -4,8 +4,7 @@ set -euv -o pipefail
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly arg_provider="${1:-generic}"
readonly arg_infraversionpath="${SOURCE_DIR}/${2:-}"
readonly arg_infraversionpath="${SOURCE_DIR}/../src"
function die {
echo $1
@@ -14,6 +13,9 @@ function die {
export DEBIAN_FRONTEND=noninteractive
readonly ubuntu_codename=$(lsb_release -cs)
readonly ubuntu_version=$(lsb_release -rs)
# hold grub since updating it breaks on some VPS providers. also, dist-upgrade will trigger it
apt-mark hold grub* >/dev/null
apt-get -o Dpkg::Options::="--force-confdef" update -y
@@ -27,8 +29,6 @@ debconf-set-selections <<< 'mysql-server mysql-server/root_password_again passwo
# this enables automatic security upgrades (https://help.ubuntu.com/community/AutomaticSecurityUpdates)
# resolvconf is needed for unbound to work property after disabling systemd-resolved in 18.04
ubuntu_version=$(lsb_release -rs)
ubuntu_codename=$(lsb_release -cs)
gpg_package=$([[ "${ubuntu_version}" == "16.04" ]] && echo "gnupg" || echo "gpg")
apt-get -y install \
acl \
@@ -44,7 +44,6 @@ apt-get -y install \
linux-generic \
logrotate \
mysql-server-5.7 \
nginx-full \
openssh-server \
pwgen \
resolvconf \
@@ -54,6 +53,12 @@ apt-get -y install \
unbound \
xfsprogs
echo "==> installing nginx for xenial for TLSv3 support"
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.18.0-1~${ubuntu_codename}_amd64.deb -o /tmp/nginx.deb
# apt install with install deps (as opposed to dpkg -i)
apt install -y /tmp/nginx.deb
rm /tmp/nginx.deb
# on some providers like scaleway the sudo file is changed and we want to keep the old one
apt-get -o Dpkg::Options::="--force-confold" install -y sudo
@@ -63,7 +68,7 @@ cp /usr/share/unattended-upgrades/20auto-upgrades /etc/apt/apt.conf.d/20auto-upg
echo "==> Installing node.js"
mkdir -p /usr/local/node-10.18.1
curl -sL https://nodejs.org/dist/v10.18.1/node-v10.18.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-10.18.1
curl -sL https://nodejs.org/dist/v10.18.1/node-v10.18.1-linux-x64.tar.gz | tar zxf - --strip-components=1 -C /usr/local/node-10.18.1
ln -sf /usr/local/node-10.18.1/bin/node /usr/bin/node
ln -sf /usr/local/node-10.18.1/bin/npm /usr/bin/npm
apt-get install -y python # Install python which is required for npm rebuild
@@ -111,7 +116,7 @@ for image in ${images}; do
done
echo "==> Install collectd"
if ! apt-get install -y collectd collectd-utils; then
if ! apt-get install -y libcurl3-gnutls collectd collectd-utils; then
# FQDNLookup is true in default debian config. The box code has a custom collectd.conf that fixes this
echo "Failed to install collectd. Presumably because of http://mailman.verplant.org/pipermail/collectd/2015-March/006491.html"
sed -e 's/^FQDNLookup true/FQDNLookup false/' -i /etc/collectd/collectd.conf
@@ -12,8 +12,6 @@ exports.up = function(db, callback) {
db.all('SELECT * FROM users WHERE admin=1', function (error, results) {
if (error) return done(error);
console.dir(results);
async.eachSeries(results, function (r, next) {
db.runSql('INSERT INTO groupMembers (groupId, userId) VALUES (?, ?)', [ ADMIN_GROUP_ID, r.id ], next);
}, done);
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN resetTokenCreationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN resetTokenCreationTime', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,28 @@
'use strict';
let async = require('async');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps MODIFY mailboxDomain VARCHAR(128)', [], function (error) { // make it nullable
if (error) console.error(error);
// clear mailboxName/Domain for apps that do not use mail addons
db.all('SELECT * FROM apps', function (error, apps) {
if (error) return callback(error);
async.eachSeries(apps, function (app, iteratorDone) {
var manifest = JSON.parse(app.manifestJson);
if (manifest.addons['sendmail'] || manifest.addons['recvmail']) return iteratorDone();
db.runSql('UPDATE apps SET mailboxName=?, mailboxDomain=? WHERE id=?', [ null, null, app.id ], iteratorDone);
}, callback);
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps MODIFY manifestJson VARCHAR(128) NOT NULL', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,17 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE mailboxes ADD COLUMN membersOnly BOOLEAN DEFAULT 0', function (error) {
if (error) return callback(error);
callback();
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN membersOnly', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,28 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD COLUMN aliasDomain VARCHAR(128)'),
function setAliasDomain(done) {
db.all('SELECT * FROM mailboxes', function (error, mailboxes) {
async.eachSeries(mailboxes, function (mailbox, iteratorDone) {
if (!mailbox.aliasTarget) return iteratorDone();
db.runSql('UPDATE mailboxes SET aliasDomain=? WHERE name=? AND domain=?', [ mailbox.domain, mailbox.name, mailbox.domain ], iteratorDone);
}, done);
});
},
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD CONSTRAINT mailboxes_aliasDomain_constraint FOREIGN KEY(aliasDomain) REFERENCES mail(domain)'),
db.runSql.bind(db, 'ALTER TABLE mailboxes CHANGE aliasTarget aliasName VARCHAR(128)')
], callback);
};
exports.down = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE mailboxes DROP FOREIGN KEY mailboxes_aliasDomain_constraint'),
db.runSql.bind(db, 'ALTER TABLE mailboxes DROP COLUMN aliasDomain'),
db.runSql.bind(db, 'ALTER TABLE mailboxes CHANGE aliasName aliasTarget VARCHAR(128)')
], callback);
};
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN servicesConfigJson TEXT', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN servicesConfigJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN bindsJson TEXT', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN bindsJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,35 @@
'use strict';
const backups = require('../src/backups.js'),
fs = require('fs');
exports.up = function(db, callback) {
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
if (error || results.length === 0) return callback(error);
var backupConfig = JSON.parse(results[0].value);
if (backupConfig.key) {
backupConfig.encryption = backups.generateEncryptionKeysSync(backupConfig.key);
backups.cleanupCacheFilesSync();
fs.writeFileSync('/home/yellowtent/platformdata/BACKUP_PASSWORD',
'This file contains your Cloudron backup password.\nBefore Cloudron v5.2, this was saved in the database.' +
'From Cloudron 5.2, this password is not required anymore. We generate strong keys based off this password and use those keys to encrypt the backups.\n' +
'This means that the password is only required at decryption/restore time.\n\n' +
'This file can be safely removed and only exists for the off-chance that you do not remember your backup password.\n\n' +
`Password: ${backupConfig.key}\n`,
'utf8');
} else {
backupConfig.encryption = null;
}
delete backupConfig.key;
db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [ JSON.stringify(backupConfig) ], callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE backups CHANGE version packageVersion VARCHAR(128) NOT NULL', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE backups CHANGE packageVersion version VARCHAR(128) NOT NULL', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,24 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE backups ADD COLUMN encryptionVersion INTEGER', function (error) {
if (error) return callback(error);
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
if (error || results.length === 0) return callback(error);
var backupConfig = JSON.parse(results[0].value);
if (!backupConfig.encryption) return callback(null);
// mark old encrypted backups as v1
db.runSql('UPDATE backups SET encryptionVersion=1', callback);
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE backups DROP COLUMN encryptionVersion', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,18 @@
'use strict';
exports.up = function(db, callback) {
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
if (error || results.length === 0) return callback(error);
var backupConfig = JSON.parse(results[0].value);
backupConfig.retentionPolicy = { keepWithinSecs: backupConfig.retentionSecs };
delete backupConfig.retentionSecs;
// mark old encrypted backups as v1
db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [ JSON.stringify(backupConfig) ], callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,18 @@
'use strict';
exports.up = function(db, callback) {
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
if (error || results.length === 0) return callback(error);
var backupConfig = JSON.parse(results[0].value);
if (backupConfig.provider !== 'minio' && backupConfig.provider !== 's3-v4-compat') return callback();
backupConfig.s3ForcePathStyle = true; // usually minio is self-hosted. s3 v4 compat, we don't know
db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [ JSON.stringify(backupConfig) ], callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,17 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
// http://stackoverflow.com/questions/386294/what-is-the-maximum-length-of-a-valid-email-address
async.series([
db.runSql.bind(db, 'ALTER TABLE appPasswords DROP INDEX name'),
db.runSql.bind(db, 'ALTER TABLE appPasswords ADD CONSTRAINT appPasswords_name_userId_identifier UNIQUE (name, userId, identifier)'),
], callback);
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,17 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE userGroups ADD COLUMN source VARCHAR(128) DEFAULT ""', function (error) {
if (error) return callback(error);
callback();
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE userGroups DROP COLUMN source', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,38 @@
'use strict';
const async = require('async');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE backups ADD COLUMN identifier VARCHAR(128)', function (error) {
if (error) return callback(error);
db.all('SELECT * FROM backups', function (error, backups) {
if (error) return callback(error);
async.eachSeries(backups, function (backup, next) {
let identifier = 'unknown';
if (backup.type === 'box') {
identifier = 'box';
} else {
const match = backup.id.match(/app_(.+?)_.+/);
if (match) identifier = match[1];
}
db.runSql('UPDATE backups SET identifier=? WHERE id=?', [ identifier, backup.id ], next);
}, function (error) {
if (error) return callback(error);
db.runSql('ALTER TABLE backups MODIFY COLUMN identifier VARCHAR(128) NOT NULL', callback);
});
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE backups DROP COLUMN identifier', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,16 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN ts TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP', function (error) {
if (error) console.error(error);
db.runSql('ALTER TABLE users DROP COLUMN modifiedAt', callback);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN ts', function (error) {
if (error) console.error(error);
callback(error);
});
};
+18 -7
View File
@@ -21,19 +21,23 @@ CREATE TABLE IF NOT EXISTS users(
password VARCHAR(1024) NOT NULL,
salt VARCHAR(512) NOT NULL,
createdAt VARCHAR(512) NOT NULL,
modifiedAt VARCHAR(512) NOT NULL,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
displayName VARCHAR(512) DEFAULT "",
fallbackEmail VARCHAR(512) DEFAULT "",
twoFactorAuthenticationSecret VARCHAR(128) DEFAULT "",
twoFactorAuthenticationEnabled BOOLEAN DEFAULT false,
source VARCHAR(128) DEFAULT "",
role VARCHAR(32),
resetToken VARCHAR(128) DEFAULT "",
resetTokenCreationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
active BOOLEAN DEFAULT 1,
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS userGroups(
id VARCHAR(128) NOT NULL UNIQUE,
name VARCHAR(254) NOT NULL UNIQUE,
source VARCHAR(128) DEFAULT "",
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS groupMembers(
@@ -76,13 +80,15 @@ CREATE TABLE IF NOT EXISTS apps(
reverseProxyConfigJson TEXT, // { robotsTxt, csp }
enableBackup BOOLEAN DEFAULT 1, // misnomer: controls automatic daily backups
enableAutomaticUpdate BOOLEAN DEFAULT 1,
mailboxName VARCHAR(128), // mailbox of this app. default allocated as '.app'
mailboxDomain VARCHAR(128) NOT NULL, // mailbox domain of this apps
mailboxName VARCHAR(128), // mailbox of this app
mailboxDomain VARCHAR(128), // mailbox domain of this apps
label VARCHAR(128), // display name
tagsJson VARCHAR(2048), // array of tags
dataDir VARCHAR(256) UNIQUE,
taskId INTEGER, // current task
errorJson TEXT,
bindsJson TEXT, // bind mounts
servicesConfigJson TEXT, // app services configuration
FOREIGN KEY(mailboxDomain) REFERENCES domains(domain),
FOREIGN KEY(taskId) REFERENCES tasks(id),
@@ -117,8 +123,10 @@ CREATE TABLE IF NOT EXISTS appEnvVars(
CREATE TABLE IF NOT EXISTS backups(
id VARCHAR(128) NOT NULL,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
version VARCHAR(128) NOT NULL, /* app version or box version */
packageVersion VARCHAR(128) NOT NULL, /* app version or box version */
encryptionVersion INTEGER, /* when null, unencrypted backup */
type VARCHAR(16) NOT NULL, /* 'box' or 'app' */
identifier VARCHAR(128) NOT NULL, /* 'box' or the app id */
dependsOn TEXT, /* comma separate list of objects this backup depends on */
state VARCHAR(16) NOT NULL,
manifestJson TEXT, /* to validate if the app can be installed in this version of box */
@@ -174,12 +182,15 @@ CREATE TABLE IF NOT EXISTS mailboxes(
name VARCHAR(128) NOT NULL,
type VARCHAR(16) NOT NULL, /* 'mailbox', 'alias', 'list' */
ownerId VARCHAR(128) NOT NULL, /* user id */
aliasTarget VARCHAR(128), /* the target name type is an alias */
aliasName VARCHAR(128), /* the target name type is an alias */
aliasDomain VARCHAR(128), /* the target domain */
membersJson TEXT, /* members of a group. fully qualified */
membersOnly BOOLEAN DEFAULT false,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
domain VARCHAR(128),
FOREIGN KEY(domain) REFERENCES mail(domain),
FOREIGN KEY(aliasDomain) REFERENCES mail(domain),
UNIQUE (name, domain));
CREATE TABLE IF NOT EXISTS subdomains(
@@ -211,7 +222,7 @@ CREATE TABLE IF NOT EXISTS notifications(
message TEXT,
acknowledged BOOLEAN DEFAULT false,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY appPasswords_name_appId_identifier (name, userId, identifier),
PRIMARY KEY (id)
);
@@ -223,7 +234,7 @@ CREATE TABLE IF NOT EXISTS appPasswords(
hashedPassword VARCHAR(1024) NOT NULL,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(userId) REFERENCES users(id),
UNIQUE (name, userId),
PRIMARY KEY (id)
);
+364 -300
View File
File diff suppressed because it is too large Load Diff
+21 -20
View File
@@ -18,32 +18,33 @@
"@google-cloud/storage": "^2.5.0",
"@sindresorhus/df": "git+https://github.com/cloudron-io/df.git#type",
"async": "^2.6.3",
"aws-sdk": "^2.610.0",
"aws-sdk": "^2.685.0",
"body-parser": "^1.19.0",
"cloudron-manifestformat": "^4.0.0",
"cloudron-manifestformat": "^5.5.0",
"connect": "^3.7.0",
"connect-lastmile": "^1.2.2",
"connect-lastmile": "^2.0.0",
"connect-timeout": "^1.9.0",
"cookie-session": "^1.4.0",
"cron": "^1.8.2",
"db-migrate": "^0.11.6",
"db-migrate": "^0.11.11",
"db-migrate-mysql": "^1.1.10",
"debug": "^4.1.1",
"dockerode": "^2.5.8",
"ejs": "^2.6.1",
"ejs-cli": "^2.1.1",
"ejs-cli": "^2.2.0",
"express": "^4.17.1",
"js-yaml": "^3.13.1",
"js-yaml": "^3.14.0",
"json": "^9.0.6",
"ldapjs": "^1.0.2",
"lodash": "^4.17.15",
"lodash.chunk": "^4.2.0",
"mime": "^2.4.4",
"moment-timezone": "^0.5.27",
"morgan": "^1.9.1",
"mime": "^2.4.6",
"moment": "^2.26.0",
"moment-timezone": "^0.5.31",
"morgan": "^1.10.0",
"multiparty": "^4.2.1",
"mysql": "^2.18.1",
"nodemailer": "^6.4.2",
"nodemailer": "^6.4.6",
"nodemailer-smtp-transport": "^2.7.4",
"once": "^1.4.0",
"parse-links": "^0.1.0",
@@ -51,34 +52,34 @@
"progress-stream": "^2.0.0",
"proxy-middleware": "^0.15.0",
"qrcode": "^1.4.4",
"readdirp": "^3.3.0",
"request": "^2.88.0",
"readdirp": "^3.4.0",
"request": "^2.88.2",
"rimraf": "^2.6.3",
"s3-block-read-stream": "^0.5.0",
"safetydance": "^1.0.0",
"safetydance": "^1.1.1",
"semver": "^6.1.1",
"showdown": "^1.9.1",
"speakeasy": "^2.0.0",
"split": "^1.0.1",
"superagent": "^5.2.1",
"superagent": "^5.2.2",
"supererror": "^0.7.2",
"tar-fs": "github:cloudron-io/tar-fs#ignore_stat_error",
"tar-stream": "^2.1.0",
"tar-stream": "^2.1.2",
"tldjs": "^2.3.1",
"underscore": "^1.9.2",
"underscore": "^1.10.2",
"uuid": "^3.4.0",
"validator": "^11.0.0",
"ws": "^7.2.1",
"ws": "^7.3.0",
"xml2js": "^0.4.23"
},
"devDependencies": {
"expect.js": "*",
"hock": "^1.3.3",
"js2xmlparser": "^4.0.0",
"hock": "^1.4.1",
"js2xmlparser": "^4.0.1",
"mocha": "^6.1.4",
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
"nock": "^10.0.6",
"node-sass": "^4.12.0",
"node-sass": "^4.14.1",
"recursive-readdir": "^2.2.2"
},
"scripts": {
+10 -59
View File
@@ -41,16 +41,14 @@ if systemctl -q is-active box; then
fi
initBaseImage="true"
# provisioning data
provider=""
provider="generic"
requestedVersion=""
apiServerOrigin="https://api.cloudron.io"
webServerOrigin="https://cloudron.io"
sourceTarballUrl=""
rebootServer="true"
license=""
args=$(getopt -o "" -l "help,skip-baseimage-init,provider:,version:,env:,skip-reboot,license:" -n "$0" -- "$@")
args=$(getopt -o "" -l "help,skip-baseimage-init,provider:,version:,env:,skip-reboot" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
@@ -67,7 +65,6 @@ while true; do
webServerOrigin="https://staging.cloudron.io"
fi
shift 2;;
--license) license="$2"; shift 2;;
--skip-baseimage-init) initBaseImage="false"; shift;;
--skip-reboot) rebootServer="false"; shift;;
--) break;;
@@ -91,48 +88,6 @@ fi
# Can only write after we have confirmed script has root access
echo "Running cloudron-setup with args : $@" > "${LOG_FILE}"
# validate arguments in the absence of data
readonly AVAILABLE_PROVIDERS="azure, caas, cloudscale, contabo, digitalocean, ec2, exoscale, gce, hetzner, interox, lightsail, linode, netcup, ovh, rosehosting, scaleway, skysilk, time4vps, upcloud, vultr or generic"
if [[ -z "${provider}" ]]; then
echo "--provider is required ($AVAILABLE_PROVIDERS)"
exit 1
elif [[ \
"${provider}" != "ami" && \
"${provider}" != "azure" && \
"${provider}" != "azure-image" && \
"${provider}" != "caas" && \
"${provider}" != "cloudscale" && \
"${provider}" != "contabo" && \
"${provider}" != "digitalocean" && \
"${provider}" != "digitalocean-mp" && \
"${provider}" != "ec2" && \
"${provider}" != "exoscale" && \
"${provider}" != "gce" && \
"${provider}" != "hetzner" && \
"${provider}" != "interox" && \
"${provider}" != "interox-image" && \
"${provider}" != "lightsail" && \
"${provider}" != "linode" && \
"${provider}" != "linode-oneclick" && \
"${provider}" != "linode-stackscript" && \
"${provider}" != "netcup" && \
"${provider}" != "netcup-image" && \
"${provider}" != "ovh" && \
"${provider}" != "rosehosting" && \
"${provider}" != "scaleway" && \
"${provider}" != "skysilk" && \
"${provider}" != "skysilk-image" && \
"${provider}" != "time4vps" && \
"${provider}" != "time4vps-image" && \
"${provider}" != "upcloud" && \
"${provider}" != "upcloud-image" && \
"${provider}" != "vultr" && \
"${provider}" != "generic" \
]]; then
echo "--provider must be one of: $AVAILABLE_PROVIDERS"
exit 1
fi
echo ""
echo "##############################################"
echo " Cloudron Setup (${requestedVersion:-latest})"
@@ -151,12 +106,6 @@ if [[ "${initBaseImage}" == "true" ]]; then
exit 1
fi
echo "=> Ensure required apt sources"
if ! add-apt-repository universe &>> "${LOG_FILE}"; then
echo "Could not add required apt sources (for nginx-full). See ${LOG_FILE}"
exit 1
fi
echo "=> Updating apt and installing script dependencies"
if ! apt-get update &>> "${LOG_FILE}"; then
echo "Could not update package repositories. See ${LOG_FILE}"
@@ -196,20 +145,19 @@ fi
if [[ "${initBaseImage}" == "true" ]]; then
echo -n "=> Installing base dependencies and downloading docker images (this takes some time) ..."
if ! /bin/bash "${box_src_tmp_dir}/baseimage/initializeBaseUbuntuImage.sh" "${provider}" "../src" &>> "${LOG_FILE}"; then
# initializeBaseUbuntuImage.sh args (provider, infraversion path) are only to support installation of pre 5.3 Cloudrons
if ! /bin/bash "${box_src_tmp_dir}/baseimage/initializeBaseUbuntuImage.sh" "generic" "../src" &>> "${LOG_FILE}"; then
echo "Init script failed. See ${LOG_FILE} for details"
exit 1
fi
echo ""
fi
# NOTE: this install script only supports 4.2 and above
# The provider flag is still used for marketplace images
echo "=> Installing version ${version} (this takes some time) ..."
mkdir -p /etc/cloudron
echo "${provider}" > /etc/cloudron/PROVIDER
[[ -n "${license}" ]] && echo -n "$license" > /etc/cloudron/LICENSE
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" &>> "${LOG_FILE}"; then
echo "Failed to install cloudron. See ${LOG_FILE} for details"
exit 1
@@ -221,13 +169,16 @@ mysql -uroot -ppassword -e "REPLACE INTO box.settings (name, value) VALUES ('web
echo -n "=> Waiting for cloudron to be ready (this takes some time) ..."
while true; do
echo -n "."
if status=$($curl -q -f "http://localhost:3000/api/v1/cloudron/status" 2>/dev/null); then
if status=$($curl -s -f "http://localhost:3000/api/v1/cloudron/status" 2>/dev/null); then
break # we are up and running
fi
sleep 10
done
echo -e "\n\n${GREEN}Visit https://<IP> and accept the self-signed certificate to finish setup.${DONE}\n"
if ! ip=$(curl -s --fail --connect-timeout 2 --max-time 2 https://api.cloudron.io/api/v1/helper/public_ip | sed -n -e 's/.*"ip": "\(.*\)"/\1/p'); then
ip='<IP>'
fi
echo -e "\n\n${GREEN}Visit https://${ip} and accept the self-signed certificate to finish setup.${DONE}\n"
if [[ "${rebootServer}" == "true" ]]; then
systemctl stop box mysql # sometimes mysql ends up having corrupt privilege tables
+10 -5
View File
@@ -13,7 +13,7 @@ HELP_MESSAGE="
This script collects diagnostic information to help debug server related issues
Options:
--admin-login Login as administrator
--owner-login Login as owner
--enable-ssh Enable SSH access for the Cloudron support team
--help Show this message
"
@@ -26,7 +26,7 @@ fi
enableSSH="false"
args=$(getopt -o "" -l "help,enable-ssh,admin-login" -n "$0" -- "$@")
args=$(getopt -o "" -l "help,enable-ssh,admin-login,owner-login" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
@@ -34,10 +34,15 @@ while true; do
--help) echo -e "${HELP_MESSAGE}"; exit 0;;
--enable-ssh) enableSSH="true"; shift;;
--admin-login)
# fall through
;&
--owner-login)
admin_username=$(mysql -NB -uroot -ppassword -e "SELECT username FROM box.users WHERE role='owner' LIMIT 1" 2>/dev/null)
admin_password=$(pwgen -1s 12)
printf '{"%s":"%s"}\n' "${admin_username}" "${admin_password}" > /tmp/cloudron_ghost.json
echo "Login as ${admin_username} / ${admin_password} . Remove /tmp/cloudron_ghost.json when done."
ghost_file=/home/yellowtent/platformdata/cloudron_ghost.json
printf '{"%s":"%s"}\n' "${admin_username}" "${admin_password}" > "${ghost_file}"
chown yellowtent:yellowtent "${ghost_file}" && chmod o-r,g-r "${ghost_file}"
echo "Login as ${admin_username} / ${admin_password} . Remove ${ghost_file} when done."
exit 0
;;
--) break;;
@@ -107,7 +112,7 @@ if [[ "${enableSSH}" == "true" ]]; then
permit_root_login=$(grep -q ^PermitRootLogin.*yes /etc/ssh/sshd_config && echo "yes" || echo "no")
# support.js uses similar logic
if $(grep -q "ec2\|lightsail\|ami" /etc/cloudron/PROVIDER); then
if [[ -d /home/ubuntu ]]; then
ssh_user="ubuntu"
keys_file="/home/ubuntu/.ssh/authorized_keys"
else
+20 -10
View File
@@ -11,9 +11,8 @@ if [[ ${EUID} -ne 0 ]]; then
exit 1
fi
readonly USER=yellowtent
readonly BOX_SRC_DIR=/home/${USER}/box
readonly BASE_DATA_DIR=/home/${USER}
readonly user=yellowtent
readonly box_src_dir=/home/${user}/box
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -24,6 +23,8 @@ readonly ubuntu_codename=$(lsb_release -cs)
readonly is_update=$(systemctl is-active box && echo "yes" || echo "no")
echo "==> installer: Updating from $(cat $box_src_dir/VERSION) to $(cat $box_src_tmp_dir/VERSION) <=="
echo "==> installer: updating docker"
if [[ $(docker version --format {{.Client.Version}}) != "18.09.2" ]]; then
@@ -56,6 +57,15 @@ if [[ $(docker version --format {{.Client.Version}}) != "18.09.2" ]]; then
rm /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb
fi
readonly nginx_version=$(nginx -v)
if [[ "${nginx_version}" != *"1.18."* ]]; then
echo "==> installer: installing nginx 1.18"
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.18.0-1~${ubuntu_codename}_amd64.deb -o /tmp/nginx.deb
# apt install with install deps (as opposed to dpkg -i)
apt install -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" --force-yes /tmp/nginx.deb
rm /tmp/nginx.deb
fi
echo "==> installer: updating node"
if [[ "$(node --version)" != "v10.18.1" ]]; then
mkdir -p /usr/local/node-10.18.1
@@ -109,22 +119,22 @@ while [[ ! -f "${CLOUDRON_SYSLOG}" || "$(${CLOUDRON_SYSLOG} --version)" != ${CLO
sleep 5
done
if ! id "${USER}" 2>/dev/null; then
useradd "${USER}" -m
if ! id "${user}" 2>/dev/null; then
useradd "${user}" -m
fi
if [[ "${is_update}" == "yes" ]]; then
echo "==> installer: stop cloudron.target service for update"
${BOX_SRC_DIR}/setup/stop.sh
${box_src_dir}/setup/stop.sh
fi
# ensure we are not inside the source directory, which we will remove now
cd /root
echo "==> installer: switching the box code"
rm -rf "${BOX_SRC_DIR}"
mv "${box_src_tmp_dir}" "${BOX_SRC_DIR}"
chown -R "${USER}:${USER}" "${BOX_SRC_DIR}"
rm -rf "${box_src_dir}"
mv "${box_src_tmp_dir}" "${box_src_dir}"
chown -R "${user}:${user}" "${box_src_dir}"
echo "==> installer: calling box setup script"
"${BOX_SRC_DIR}/setup/start.sh"
"${box_src_dir}/setup/start.sh"
+23 -2
View File
@@ -20,6 +20,11 @@ readonly ubuntu_version=$(lsb_release -rs)
cp -f "${script_dir}/../scripts/cloudron-support" /usr/bin/cloudron-support
# this needs to match the cloudron/base:2.0.0 gid
if ! getent group media; then
addgroup --gid 500 --system media
fi
echo "==> Configuring docker"
cp "${script_dir}/start/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
systemctl enable apparmor
@@ -56,6 +61,7 @@ mkdir -p "${BOX_DATA_DIR}/profileicons"
mkdir -p "${BOX_DATA_DIR}/certs"
mkdir -p "${BOX_DATA_DIR}/acme" # acme keys
mkdir -p "${BOX_DATA_DIR}/mail/dkim"
mkdir -p "${BOX_DATA_DIR}/well-known" # .well-known documents
# ensure backups folder exists and is writeable
mkdir -p /var/backups
@@ -79,6 +85,9 @@ systemctl daemon-reload
systemctl restart systemd-journald
setfacl -n -m u:${USER}:r /var/log/journal/*/system.journal
# Give user access to nginx logs (uses adm group)
usermod -a -G adm ${USER}
echo "==> Setting up unbound"
# DO uses Google nameservers by default. This causes RBL queries to fail (host 2.0.0.127.zen.spamhaus.org)
# We do not use dnsmasq because it is not a recursive resolver and defaults to the value in the interfaces file (which is Google DNS!)
@@ -144,8 +153,15 @@ cp "${script_dir}/start/nginx/mime.types" "${PLATFORM_DATA_DIR}/nginx/mime.types
if ! grep -q "^Restart=" /etc/systemd/system/multi-user.target.wants/nginx.service; then
# default nginx service file does not restart on crash
echo -e "\n[Service]\nRestart=always\n" >> /etc/systemd/system/multi-user.target.wants/nginx.service
systemctl daemon-reload
fi
# worker_rlimit_nofile in nginx config can be max this number
mkdir -p /etc/systemd/system/nginx.service.d
if ! grep -q "^LimitNOFILE=" /etc/systemd/system/nginx.service.d/cloudron.conf; then
echo -e "[Service]\nLimitNOFILE=16384\n" > /etc/systemd/system/nginx.service.d/cloudron.conf
fi
systemctl daemon-reload
systemctl start nginx
# restart mysql to make sure it has latest config
@@ -170,9 +186,11 @@ readonly mysql_root_password="password"
mysqladmin -u root -ppassword password password # reset default root password
mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
# set HOME explicity, because it's not set when the installer calls it. this is done because
# paths.js uses this env var and some of the migrate code requires box code
echo "==> Migrating data"
cd "${BOX_SRC_DIR}"
if ! BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up; then
if ! HOME=${HOME_DIR} BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up; then
echo "DB migration failed"
exit 1
fi
@@ -191,6 +209,9 @@ fi
echo "==> Cleaning up stale redis directories"
find "${APPS_DATA_DIR}" -maxdepth 2 -type d -name redis -exec rm -rf {} +
echo "==> Cleaning up old logs"
rm -f /home/yellowtent/platformdata/logs/*/*.log.* || true
echo "==> Changing ownership"
# be careful of what is chown'ed here. subdirs like mysql,redis etc are owned by the containers and will stop working if perms change
chown -R "${USER}" /etc/cloudron
+5
View File
@@ -12,6 +12,11 @@ iptables -t filter -I CLOUDRON -m state --state RELATED,ESTABLISHED -j ACCEPT
# ssh is allowed alternately on port 202
iptables -A CLOUDRON -p tcp -m tcp -m multiport --dports 22,25,80,202,443,587,993,4190 -j ACCEPT
# turn and stun service
iptables -t filter -A CLOUDRON -p tcp -m multiport --dports 3478,5349 -j ACCEPT
iptables -t filter -A CLOUDRON -p udp -m multiport --dports 3478,5349 -j ACCEPT
iptables -t filter -A CLOUDRON -p udp -m multiport --dports 50000:51000 -j ACCEPT
iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-request -j ACCEPT
iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-reply -j ACCEPT
iptables -t filter -A CLOUDRON -p udp --sport 53 -j ACCEPT
+8 -1
View File
@@ -3,10 +3,17 @@
printf "**********************************************************************\n\n"
if [[ -z "$(ls -A /home/yellowtent/boxdata/mail/dkim)" ]]; then
if [[ -f /tmp/.cloudron-motd-cache ]]; then
ip=$(cat /tmp/.cloudron-motd-cache)
elif ! ip=$(curl --fail --connect-timeout 2 --max-time 2 -q https://api.cloudron.io/api/v1/helper/public_ip | sed -n -e 's/.*"ip": "\(.*\)"/\1/p'); then
ip='<IP>'
fi
echo "${ip}" > /tmp/.cloudron-motd-cache
printf "\t\t\tWELCOME TO CLOUDRON\n"
printf "\t\t\t-------------------\n"
printf '\n\e[1;32m%-6s\e[m\n\n' "Visit https://<IP> on your browser and accept the self-signed certificate to finish setup."
printf '\n\e[1;32m%-6s\e[m\n\n' "Visit https://${ip} on your browser and accept the self-signed certificate to finish setup."
printf "Cloudron overview - https://cloudron.io/documentation/ \n"
printf "Cloudron setup - https://cloudron.io/documentation/installation/#setup \n"
else
+4 -1
View File
@@ -3,10 +3,12 @@ import collectd,os,subprocess,sys,re,time
# https://www.programcreek.com/python/example/106897/collectd.register_read
PATHS = [] # { name, dir, exclude }
# there is a pattern in carbon/storage-schemas.conf which stores values every 12h for a year
INTERVAL = 60 * 60 * 12 # twice a day. change values in docker-graphite if you change this
def du(pathinfo):
cmd = 'timeout 1800 du -Dsb "{}"'.format(pathinfo['dir'])
# -B1 makes du print block sizes and not apparent sizes (to match df which also uses block sizes)
cmd = 'timeout 1800 du -DsB1 "{}"'.format(pathinfo['dir'])
if pathinfo['exclude'] != '':
cmd += ' --exclude "{}"'.format(pathinfo['exclude'])
@@ -26,6 +28,7 @@ def parseSize(size):
def dockerSize():
# use --format '{{json .}}' to dump the string. '{{if eq .Type "Images"}}{{.Size}}{{end}}' still creates newlines
# https://godoc.org/github.com/docker/go-units#HumanSize is used. so it's 1000 (KB) and not 1024 (KiB)
cmd = 'timeout 1800 docker system df --format "{{.Size}}" | head -n1'
try:
size = subprocess.check_output(cmd, shell=True).strip().decode('utf-8')
-40
View File
@@ -1,40 +0,0 @@
# add customizations here
# after making changes run "sudo systemctl restart box"
# appstore:
# blacklist:
# - io.wekan.cloudronapp
# - io.cloudron.openvpn
# whitelist:
# org.wordpress.cloudronapp: {}
# chat.rocket.cloudronapp: {}
# com.nextcloud.cloudronapp: {}
#
# backups:
# configurable: true
#
# domains:
# dynamicDns: true
# changeDashboardDomain: true
#
# subscription:
# configurable: true
#
# support:
# email: support@cloudron.io
# remoteSupport: true
#
# ticketFormBody: |
# Use this form to open support tickets. You can also write directly to [support@cloudron.io](mailto:support@cloudron.io).
# * [Knowledge Base & App Docs](https://cloudron.io/documentation/apps/?support_view)
# * [Custom App Packaging & API](https://cloudron.io/developer/packaging/?support_view)
# * [Forum](https://forum.cloudron.io/)
#
# submitTickets: true
#
# alerts:
# email: support@cloudron.io
# notifyCloudronAdmins: false
#
# footer:
# body: '&copy; 2020 [Cloudron](https://cloudron.io) [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)'
+2
View File
@@ -10,6 +10,7 @@
/home/yellowtent/platformdata/logs/redis-*/*.log
/home/yellowtent/platformdata/logs/crash/*.log
/home/yellowtent/platformdata/logs/collectd/*.log
/home/yellowtent/platformdata/logs/turn/*.log
/home/yellowtent/platformdata/logs/updater/*.log {
# only keep one rotated file, we currently do not send that over the api
rotate 1
@@ -17,6 +18,7 @@
missingok
# we never compress so we can simply tail the files
nocompress
# this truncates the original log file and not the rotated one
copytruncate
}
+9 -2
View File
@@ -1,11 +1,18 @@
user www-data;
worker_processes 1;
# detect based on available CPU cores
worker_processes auto;
# this is 4096 by default. See /proc/<PID>/limits and /etc/security/limits.conf
# usually twice the worker_connections (one for uptsream, one for downstream)
# see also LimitNOFILE=16384 in systemd drop-in
worker_rlimit_nofile 8192;
pid /run/nginx.pid;
events {
worker_connections 1024;
# a single worker has these many simultaneous connections max
worker_connections 4096;
}
http {
+2 -2
View File
@@ -13,8 +13,8 @@ Type=idle
WorkingDirectory=/home/yellowtent/box
Restart=always
; Systemd does not append logs when logging to files, we spawn a shell first and exec to replace it after setting up the pipes
ExecStart=/bin/sh -c 'echo "Logging to /home/yellowtent/platformdata/logs/box.log"; exec /usr/bin/node --max_old_space_size=150 /home/yellowtent/box/box.js >> /home/yellowtent/platformdata/logs/box.log 2>&1'
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
ExecStart=/bin/sh -c 'echo "Logging to /home/yellowtent/platformdata/logs/box.log"; exec /usr/bin/node /home/yellowtent/box/box.js >> /home/yellowtent/platformdata/logs/box.log 2>&1'
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box:*,connect-lastmile,-box:ldap" "BOX_ENV=cloudron" "NODE_ENV=production"
; kill apptask processes as well
KillMode=control-group
; Do not kill this process on OOM. Children inherit this score. Do not set it to -1000 so that MemoryMax can keep working
+368 -143
View File
@@ -7,6 +7,9 @@ exports = module.exports = {
getServiceLogs: getServiceLogs,
restartService: restartService,
startAppServices,
stopAppServices,
startServices: startServices,
updateServiceConfig: updateServiceConfig,
@@ -20,11 +23,7 @@ exports = module.exports = {
getMountsSync: getMountsSync,
getContainerNamesSync: getContainerNamesSync,
getServiceDetails: getServiceDetails,
// exported for testing
_setupOauth: setupOauth,
_teardownOauth: teardownOauth,
getContainerDetails: getContainerDetails,
SERVICE_STATUS_STARTING: 'starting', // container up, waiting for healthcheck
SERVICE_STATUS_ACTIVE: 'active',
@@ -66,7 +65,14 @@ const RMADDONDIR_CMD = path.join(__dirname, 'scripts/rmaddondir.sh');
// setup can be called multiple times for the same app (configure crash restart) and existing data must not be lost
// teardown is destructive. app data stored with the addon is lost
var KNOWN_ADDONS = {
var ADDONS = {
turn: {
setup: setupTurn,
teardown: teardownTurn,
backup: NOOP,
restore: NOOP,
clear: NOOP
},
email: {
setup: setupEmail,
teardown: teardownEmail,
@@ -102,13 +108,6 @@ var KNOWN_ADDONS = {
restore: restoreMySql,
clear: clearMySql,
},
oauth: {
setup: setupOauth,
teardown: teardownOauth,
backup: NOOP,
restore: setupOauth,
clear: NOOP,
},
postgresql: {
setup: setupPostgreSql,
teardown: teardownPostgreSql,
@@ -150,10 +149,23 @@ var KNOWN_ADDONS = {
backup: NOOP,
restore: NOOP,
clear: NOOP,
},
oauth: { // kept for backward compatibility. keep teardown for uninstall to work
setup: NOOP,
teardown: teardownOauth,
backup: NOOP,
restore: NOOP,
clear: NOOP,
}
};
const KNOWN_SERVICES = {
// services are actual containers that are running. addons are the concepts requested by app
const SERVICES = {
turn: {
status: statusTurn,
restart: restartContainer.bind(null, 'turn'),
defaultMemoryLimit: 256 * 1024 * 1024
},
mail: {
status: containerStatus.bind(null, 'mail', 'CLOUDRON_MAIL_TOKEN'),
restart: mail.restartMail,
@@ -201,6 +213,16 @@ const KNOWN_SERVICES = {
}
};
const APP_SERVICES = {
redis: {
status: (instance, done) => containerStatus(`redis-${instance}`, 'CLOUDRON_REDIS_TOKEN', done),
start: (instance, done) => docker.startContainer(`redis-${instance}`, done),
stop: (instance, done) => docker.stopContainer(`redis-${instance}`, done),
restart: (instance, done) => restartContainer(`redis-${instance}`, done),
defaultMemoryLimit: 150 * 1024 * 1024
}
};
function debugApp(app /*, args */) {
assert(typeof app === 'object');
@@ -237,6 +259,7 @@ function rebuildService(serviceName, callback) {
// this attempts to recreate the service docker container if they don't exist but platform infra version is unchanged
// passing an infra version of 'none' will not attempt to purge existing data, not sure if this is good or bad
if (serviceName === 'turn') return startTurn({ version: 'none' }, callback);
if (serviceName === 'mongodb') return startMongodb({ version: 'none' }, callback);
if (serviceName === 'postgresql') return startPostgresql({ version: 'none' }, callback);
if (serviceName === 'mysql') return startMysql({ version: 'none' }, callback);
@@ -247,25 +270,22 @@ function rebuildService(serviceName, callback) {
callback();
}
function restartContainer(serviceName, callback) {
assert.strictEqual(typeof serviceName, 'string');
function restartContainer(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
docker.stopContainer(serviceName, function (error) {
docker.restartContainer(name, function (error) {
if (error && error.reason === BoxError.NOT_FOUND) {
callback(null); // callback early since rebuilding takes long
return rebuildService(name, function (error) { if (error) console.error(`Unable to rebuild service ${name}`, error); });
}
if (error) return callback(error);
docker.startContainer(serviceName, function (error) {
if (error && error.reason === BoxError.NOT_FOUND) {
callback(null); // callback early since rebuilding takes long
return rebuildService(serviceName, function (error) { if (error) console.error(`Unable to rebuild service ${serviceName}`, error); });
}
callback(error);
});
callback(error);
});
}
function getServiceDetails(containerName, tokenEnvName, callback) {
function getContainerDetails(containerName, tokenEnvName, callback) {
assert.strictEqual(typeof containerName, 'string');
assert.strictEqual(typeof tokenEnvName, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -288,20 +308,20 @@ function getServiceDetails(containerName, tokenEnvName, callback) {
});
}
function containerStatus(addonName, addonTokenName, callback) {
assert.strictEqual(typeof addonName, 'string');
assert.strictEqual(typeof addonTokenName, 'string');
function containerStatus(containerName, tokenEnvName, callback) {
assert.strictEqual(typeof containerName, 'string');
assert.strictEqual(typeof tokenEnvName, 'string');
assert.strictEqual(typeof callback, 'function');
getServiceDetails(addonName, addonTokenName, function (error, addonDetails) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, { status: exports.SERVICE_STATUS_STOPPED });
getContainerDetails(containerName, tokenEnvName, function (error, addonDetails) {
if (error && (error.reason === BoxError.NOT_FOUND || error.reason === BoxError.INACTIVE)) return callback(null, { status: exports.SERVICE_STATUS_STOPPED });
if (error) return callback(error);
request.get(`https://${addonDetails.ip}:3000/healthcheck?access_token=${addonDetails.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
if (error) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for ${addonName}: ${error.message}` });
if (response.statusCode !== 200 || !response.body.status) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for ${addonName}. Status code: ${response.statusCode} message: ${response.body.message}` });
if (error) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for ${containerName}: ${error.message}` });
if (response.statusCode !== 200 || !response.body.status) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for ${containerName}. Status code: ${response.statusCode} message: ${response.body.message}` });
docker.memoryUsage(addonName, function (error, result) {
docker.memoryUsage(containerName, function (error, result) {
if (error) return callback(error);
var tmp = {
@@ -319,19 +339,59 @@ function containerStatus(addonName, addonTokenName, callback) {
function getServices(callback) {
assert.strictEqual(typeof callback, 'function');
let services = Object.keys(KNOWN_SERVICES);
let services = Object.keys(SERVICES);
callback(null, services);
appdb.getAll(function (error, apps) {
if (error) return callback(error);
for (let app of apps) {
if (app.manifest.addons && app.manifest.addons['redis']) services.push(`redis:${app.id}`);
}
callback(null, services);
});
}
function getService(serviceName, callback) {
assert.strictEqual(typeof serviceName, 'string');
function getServicesConfig(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
if (!KNOWN_SERVICES[serviceName]) return callback(new BoxError(BoxError.NOT_FOUND));
const [name, instance ] = id.split(':');
if (!instance) {
settings.getPlatformConfig(function (error, platformConfig) {
if (error) return callback(error);
callback(null, SERVICES[name], platformConfig);
});
return;
}
appdb.get(instance, function (error, app) {
if (error) return callback(error);
callback(null, APP_SERVICES[name], app.servicesConfig);
});
}
function getService(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
const [name, instance ] = id.split(':');
let containerStatusFunc;
if (instance) {
if (!APP_SERVICES[name]) return callback(new BoxError(BoxError.NOT_FOUND));
containerStatusFunc = APP_SERVICES[name].status.bind(null, instance);
} else if (SERVICES[name]) {
containerStatusFunc = SERVICES[name].status;
} else {
return callback(new BoxError(BoxError.NOT_FOUND));
}
var tmp = {
name: serviceName,
name: name,
status: null,
memoryUsed: 0,
memoryPercent: 0,
@@ -343,60 +403,76 @@ function getService(serviceName, callback) {
}
};
settings.getPlatformConfig(function (error, platformConfig) {
containerStatusFunc(function (error, result) {
if (error) return callback(error);
if (platformConfig[serviceName] && platformConfig[serviceName].memory && platformConfig[serviceName].memorySwap) {
tmp.config.memory = platformConfig[serviceName].memory;
tmp.config.memorySwap = platformConfig[serviceName].memorySwap;
} else if (KNOWN_SERVICES[serviceName].defaultMemoryLimit) {
tmp.config.memory = KNOWN_SERVICES[serviceName].defaultMemoryLimit;
tmp.config.memorySwap = tmp.config.memory * 2;
}
tmp.status = result.status;
tmp.memoryUsed = result.memoryUsed;
tmp.memoryPercent = result.memoryPercent;
tmp.error = result.error || null;
KNOWN_SERVICES[serviceName].status(function (error, result) {
getServicesConfig(id, function (error, service, servicesConfig) {
if (error) return callback(error);
tmp.status = result.status;
tmp.memoryUsed = result.memoryUsed;
tmp.memoryPercent = result.memoryPercent;
tmp.error = result.error || null;
const serviceConfig = servicesConfig[name];
if (serviceConfig && serviceConfig.memory && serviceConfig.memorySwap) {
tmp.config.memory = serviceConfig.memory;
tmp.config.memorySwap = serviceConfig.memorySwap;
} else if (service.defaultMemoryLimit) {
tmp.config.memory = service.defaultMemoryLimit;
tmp.config.memorySwap = tmp.config.memory * 2;
}
callback(null, tmp);
});
});
}
function configureService(serviceName, data, callback) {
assert.strictEqual(typeof serviceName, 'string');
function configureService(id, data, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
if (!KNOWN_SERVICES[serviceName]) return callback(new BoxError(BoxError.NOT_FOUND));
const [name, instance ] = id.split(':');
settings.getPlatformConfig(function (error, platformConfig) {
if (instance) {
if (!APP_SERVICES[name]) return callback(new BoxError(BoxError.NOT_FOUND));
} else if (!SERVICES[name]) {
return callback(new BoxError(BoxError.NOT_FOUND));
}
getServicesConfig(id, function (error, service, servicesConfig) {
if (error) return callback(error);
if (!platformConfig[serviceName]) platformConfig[serviceName] = {};
if (!servicesConfig[name]) servicesConfig[name] = {};
// if not specified we clear the entry and use defaults
if (!data.memory || !data.memorySwap) {
delete platformConfig[serviceName];
delete servicesConfig[name];
} else {
platformConfig[serviceName].memory = data.memory;
platformConfig[serviceName].memorySwap = data.memorySwap;
servicesConfig[name].memory = data.memory;
servicesConfig[name].memorySwap = data.memorySwap;
}
settings.setPlatformConfig(platformConfig, function (error) {
if (error) return callback(error);
if (instance) {
appdb.update(instance, { servicesConfig }, function (error) {
if (error) return callback(error);
callback(null);
});
updateAppServiceConfig(name, instance, servicesConfig, callback);
});
} else {
settings.setPlatformConfig(servicesConfig, function (error) {
if (error) return callback(error);
callback(null);
});
}
});
}
function getServiceLogs(serviceName, options, callback) {
assert.strictEqual(typeof serviceName, 'string');
function getServiceLogs(id, options, callback) {
assert.strictEqual(typeof id, 'string');
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
@@ -404,9 +480,15 @@ function getServiceLogs(serviceName, options, callback) {
assert.strictEqual(typeof options.format, 'string');
assert.strictEqual(typeof options.follow, 'boolean');
if (!KNOWN_SERVICES[serviceName]) return callback(new BoxError(BoxError.NOT_FOUND));
const [name, instance ] = id.split(':');
debug(`Getting logs for ${serviceName}`);
if (instance) {
if (!APP_SERVICES[name]) return callback(new BoxError(BoxError.NOT_FOUND));
} else if (!SERVICES[name]) {
return callback(new BoxError(BoxError.NOT_FOUND));
}
debug(`Getting logs for ${name}`);
var lines = options.lines,
format = options.format || 'json',
@@ -415,21 +497,29 @@ function getServiceLogs(serviceName, options, callback) {
let cmd, args = [];
// docker and unbound use journald
if (serviceName === 'docker' || serviceName === 'unbound') {
if (name === 'docker' || name === 'unbound') {
cmd = 'journalctl';
args.push('--lines=' + (lines === -1 ? 'all' : lines));
args.push(`--unit=${serviceName}`);
args.push(`--unit=${name}`);
args.push('--no-pager');
args.push('--output=short-iso');
if (follow) args.push('--follow');
} else if (name === 'nginx') {
cmd = '/usr/bin/tail';
args.push('--lines=' + (lines === -1 ? '+1' : lines));
if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs
args.push('/var/log/nginx/access.log');
args.push('/var/log/nginx/error.log');
} else {
cmd = '/usr/bin/tail';
args.push('--lines=' + (lines === -1 ? '+1' : lines));
if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs
args.push(path.join(paths.LOG_DIR, serviceName, 'app.log'));
const containerName = APP_SERVICES[name] ? `${name}-${instance}` : name;
args.push(path.join(paths.LOG_DIR, containerName, 'app.log'));
}
var cp = spawn(cmd, args);
@@ -448,7 +538,7 @@ function getServiceLogs(serviceName, options, callback) {
return JSON.stringify({
realtimeTimestamp: timestamp * 1000,
message: message,
source: serviceName
source: name
}) + '\n';
});
@@ -459,23 +549,67 @@ function getServiceLogs(serviceName, options, callback) {
callback(null, transformStream);
}
function restartService(serviceName, callback) {
assert.strictEqual(typeof serviceName, 'string');
function restartService(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
if (!KNOWN_SERVICES[serviceName]) return callback(new BoxError(BoxError.NOT_FOUND));
const [name, instance ] = id.split(':');
KNOWN_SERVICES[serviceName].restart(callback);
if (instance) {
if (!APP_SERVICES[name]) return callback(new BoxError(BoxError.NOT_FOUND));
APP_SERVICES[name].restart(instance, callback);
} else if (SERVICES[name]) {
SERVICES[name].restart(callback);
} else {
return callback(new BoxError(BoxError.NOT_FOUND));
}
}
function waitForService(containerName, tokenEnvName, callback) {
// in the future, we can refcount and lazy start global services
function startAppServices(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
const instance = app.id;
async.eachSeries(Object.keys(app.manifest.addons || {}), function (addon, iteratorDone) {
if (!(addon in APP_SERVICES)) return iteratorDone();
APP_SERVICES[addon].start(instance, function (error) { // assume addons name is service name
// error ignored because we don't want "start app" to error. use can fix it from Services
if (error) debug(`startAppServices: ${addon}:${instance}`, error);
iteratorDone();
});
}, callback);
}
// in the future, we can refcount and stop global services as well
function stopAppServices(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
const instance = app.id;
async.eachSeries(Object.keys(app.manifest.addons || {}), function (addon, iteratorDone) {
if (!(addon in APP_SERVICES)) return iteratorDone();
APP_SERVICES[addon].stop(instance, function (error) { // assume addons name is service name
// error ignored because we don't want "start app" to error. use can fix it from Services
if (error) debug(`stopAppServices: ${addon}:${instance}`, error);
iteratorDone();
});
}, callback);
}
function waitForContainer(containerName, tokenEnvName, callback) {
assert.strictEqual(typeof containerName, 'string');
assert.strictEqual(typeof tokenEnvName, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`Waiting for ${containerName}`);
getServiceDetails(containerName, tokenEnvName, function (error, result) {
getContainerDetails(containerName, tokenEnvName, function (error, result) {
if (error) return callback(error);
async.retry({ times: 10, interval: 15000 }, function (retryCallback) {
@@ -499,11 +633,11 @@ function setupAddons(app, addons, callback) {
debugApp(app, 'setupAddons: Setting up %j', Object.keys(addons));
async.eachSeries(Object.keys(addons), function iterator(addon, iteratorCallback) {
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
if (!(addon in ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
debugApp(app, 'Setting up addon %s with options %j', addon, addons[addon]);
KNOWN_ADDONS[addon].setup(app, addons[addon], iteratorCallback);
ADDONS[addon].setup(app, addons[addon], iteratorCallback);
}, callback);
}
@@ -517,11 +651,11 @@ function teardownAddons(app, addons, callback) {
debugApp(app, 'teardownAddons: Tearing down %j', Object.keys(addons));
async.eachSeries(Object.keys(addons), function iterator(addon, iteratorCallback) {
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
if (!(addon in ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
debugApp(app, 'Tearing down addon %s with options %j', addon, addons[addon]);
KNOWN_ADDONS[addon].teardown(app, addons[addon], iteratorCallback);
ADDONS[addon].teardown(app, addons[addon], iteratorCallback);
}, callback);
}
@@ -537,9 +671,9 @@ function backupAddons(app, addons, callback) {
debugApp(app, 'backupAddons: Backing up %j', Object.keys(addons));
async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) {
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
if (!(addon in ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
KNOWN_ADDONS[addon].backup(app, addons[addon], iteratorCallback);
ADDONS[addon].backup(app, addons[addon], iteratorCallback);
}, callback);
}
@@ -555,9 +689,9 @@ function clearAddons(app, addons, callback) {
debugApp(app, 'clearAddons: clearing %j', Object.keys(addons));
async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) {
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
if (!(addon in ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
KNOWN_ADDONS[addon].clear(app, addons[addon], iteratorCallback);
ADDONS[addon].clear(app, addons[addon], iteratorCallback);
}, callback);
}
@@ -573,9 +707,9 @@ function restoreAddons(app, addons, callback) {
debugApp(app, 'restoreAddons: restoring %j', Object.keys(addons));
async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) {
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
if (!(addon in ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
KNOWN_ADDONS[addon].restore(app, addons[addon], iteratorCallback);
ADDONS[addon].restore(app, addons[addon], iteratorCallback);
}, callback);
}
@@ -584,12 +718,12 @@ function importAppDatabase(app, addon, callback) {
assert.strictEqual(typeof addon, 'string');
assert.strictEqual(typeof callback, 'function');
if (!(addon in KNOWN_ADDONS)) return callback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
if (!(addon in ADDONS)) return callback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
async.series([
KNOWN_ADDONS[addon].setup.bind(null, app, app.manifest.addons[addon]),
KNOWN_ADDONS[addon].clear.bind(null, app, app.manifest.addons[addon]), // clear in case we crashed in a restore
KNOWN_ADDONS[addon].restore.bind(null, app, app.manifest.addons[addon])
ADDONS[addon].setup.bind(null, app, app.manifest.addons[addon]),
ADDONS[addon].clear.bind(null, app, app.manifest.addons[addon]), // clear in case we crashed in a restore
ADDONS[addon].restore.bind(null, app, app.manifest.addons[addon])
], callback);
}
@@ -631,7 +765,7 @@ function updateServiceConfig(platformConfig, callback) {
memory = containerConfig.memory;
memorySwap = containerConfig.memorySwap;
} else {
memory = KNOWN_SERVICES[serviceName].defaultMemoryLimit;
memory = SERVICES[serviceName].defaultMemoryLimit;
memorySwap = memory * 2;
}
@@ -640,6 +774,28 @@ function updateServiceConfig(platformConfig, callback) {
}, callback);
}
function updateAppServiceConfig(name, instance, servicesConfig, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof instance, 'string');
assert.strictEqual(typeof servicesConfig, 'object');
assert.strictEqual(typeof callback, 'function');
debug(`updateAppServiceConfig: ${name}-${instance} ${JSON.stringify(servicesConfig)}`);
const serviceConfig = servicesConfig[name];
let memory, memorySwap;
if (serviceConfig && serviceConfig.memory && serviceConfig.memorySwap) {
memory = serviceConfig.memory;
memorySwap = serviceConfig.memorySwap;
} else {
memory = APP_SERVICES[name].defaultMemoryLimit;
memorySwap = memory * 2;
}
const args = `update --memory ${memory} --memory-swap ${memorySwap} ${name}-${instance}`.split(' ');
shell.spawn(`updateAppServiceConfig${name}`, '/usr/bin/docker', args, { }, callback);
}
function startServices(existingInfra, callback) {
assert.strictEqual(typeof existingInfra, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -650,6 +806,7 @@ function startServices(existingInfra, callback) {
if (existingInfra.version !== infra.version) {
debug(`startServices: ${existingInfra.version} -> ${infra.version}. starting all services`);
startFuncs.push(
startTurn.bind(null, existingInfra),
startMysql.bind(null, existingInfra),
startPostgresql.bind(null, existingInfra),
startMongodb.bind(null, existingInfra),
@@ -658,6 +815,7 @@ function startServices(existingInfra, callback) {
} else {
assert.strictEqual(typeof existingInfra.images, 'object');
if (!existingInfra.images.turn || infra.images.turn.tag !== existingInfra.images.turn.tag) startFuncs.push(startTurn.bind(null, existingInfra));
if (infra.images.mysql.tag !== existingInfra.images.mysql.tag) startFuncs.push(startMysql.bind(null, existingInfra));
if (infra.images.postgresql.tag !== existingInfra.images.postgresql.tag) startFuncs.push(startPostgresql.bind(null, existingInfra));
if (infra.images.mongodb.tag !== existingInfra.images.mongodb.tag) startFuncs.push(startMongodb.bind(null, existingInfra));
@@ -677,7 +835,7 @@ function getEnvironment(app, callback) {
appdb.getAddonConfigByAppId(app.id, function (error, result) {
if (error) return callback(error);
if (app.manifest.addons['docker']) result.push({ name: 'DOCKER_HOST', value: `tcp://172.18.0.1:${constants.DOCKER_PROXY_PORT}` });
if (app.manifest.addons['docker']) result.push({ name: 'CLOUDRON_DOCKER_HOST', value: `tcp://172.18.0.1:${constants.DOCKER_PROXY_PORT}` });
return callback(null, result.map(function (e) { return e.name + '=' + e.value; }));
});
@@ -740,8 +898,8 @@ function setupLocalStorage(app, options, callback) {
// reomve any existing volume in case it's bound with an old dataDir
async.series([
docker.removeVolume.bind(null, app, `${app.id}-localstorage`),
docker.createVolume.bind(null, app, `${app.id}-localstorage`, volumeDataDir)
docker.removeVolume.bind(null, `${app.id}-localstorage`),
docker.createVolume.bind(null, `${app.id}-localstorage`, volumeDataDir, { fqdn: app.fqdn, appId: app.id })
], callback);
}
@@ -752,7 +910,7 @@ function clearLocalStorage(app, options, callback) {
debugApp(app, 'clearLocalStorage');
docker.clearVolume(app, `${app.id}-localstorage`, { removeDirectory: false }, callback);
docker.clearVolume(`${app.id}-localstorage`, { removeDirectory: false }, callback);
}
function teardownLocalStorage(app, options, callback) {
@@ -763,35 +921,42 @@ function teardownLocalStorage(app, options, callback) {
debugApp(app, 'teardownLocalStorage');
async.series([
docker.clearVolume.bind(null, app, `${app.id}-localstorage`, { removeDirectory: true }),
docker.removeVolume.bind(null, app, `${app.id}-localstorage`)
docker.clearVolume.bind(null, `${app.id}-localstorage`, { removeDirectory: true }),
docker.removeVolume.bind(null, `${app.id}-localstorage`)
], callback);
}
function setupOauth(app, options, callback) {
function setupTurn(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'setupOauth');
var turnSecret = safe.fs.readFileSync(paths.ADDON_TURN_SECRET_FILE, 'utf8');
if (!turnSecret) console.error('No turn secret set. Will leave emtpy, but this is a problem!');
if (!app.sso) return callback(null);
const env = [
{ name: 'CLOUDRON_STUN_SERVER', value: settings.adminFqdn() },
{ name: 'CLOUDRON_STUN_PORT', value: '3478' },
{ name: 'CLOUDRON_STUN_TLS_PORT', value: '5349' },
{ name: 'CLOUDRON_TURN_SERVER', value: settings.adminFqdn() },
{ name: 'CLOUDRON_TURN_PORT', value: '3478' },
{ name: 'CLOUDRON_TURN_TLS_PORT', value: '5349' },
{ name: 'CLOUDRON_TURN_SECRET', value: turnSecret }
];
const env = [];
debugApp(app, 'Setting up TURN');
debugApp(app, 'Setting oauth addon config to %j', env);
appdb.setAddonConfig(app.id, 'oauth', env, callback);
appdb.setAddonConfig(app.id, 'turn', env, callback);
}
function teardownOauth(app, options, callback) {
function teardownTurn(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'teardownOauth');
debugApp(app, 'Tearing down TURN');
appdb.unsetAddonConfig(app.id, 'oauth', callback);
appdb.unsetAddonConfig(app.id, 'turn', callback);
}
function setupEmail(app, options, callback) {
@@ -993,7 +1158,7 @@ function startMysql(existingInfra, callback) {
shell.exec('startMysql', cmd, function (error) {
if (error) return callback(error);
waitForService('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error) {
waitForContainer('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error) {
if (error) return callback(error);
if (!upgrading) return callback(null);
@@ -1022,7 +1187,7 @@ function setupMySql(app, options, callback) {
password: error ? hat(4 * 48) : existingPassword // see box#362 for password length
};
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
getContainerDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) {
@@ -1061,7 +1226,7 @@ function clearMySql(app, options, callback) {
const database = mysqlDatabaseName(app.id);
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
getContainerDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}/clear?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
@@ -1081,7 +1246,7 @@ function teardownMySql(app, options, callback) {
const database = mysqlDatabaseName(app.id);
const username = database;
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
getContainerDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
if (error) return callback(error);
request.delete(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}?access_token=${result.token}&username=${username}`, { json: true, rejectUnauthorized: false }, function (error, response) {
@@ -1129,7 +1294,7 @@ function backupMySql(app, options, callback) {
debugApp(app, 'Backing up mysql');
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
getContainerDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
if (error) return callback(error);
const url = `https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}/backup?access_token=${result.token}`;
@@ -1148,7 +1313,7 @@ function restoreMySql(app, options, callback) {
callback = once(callback); // protect from multiple returns with streams
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
getContainerDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
if (error) return callback(error);
var input = fs.createReadStream(dumpPath('mysql', app.id));
@@ -1209,7 +1374,7 @@ function startPostgresql(existingInfra, callback) {
shell.exec('startPostgresql', cmd, function (error) {
if (error) return callback(error);
waitForService('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error) {
waitForContainer('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error) {
if (error) return callback(error);
if (!upgrading) return callback(null);
@@ -1237,7 +1402,7 @@ function setupPostgreSql(app, options, callback) {
password: error ? hat(4 * 128) : existingPassword
};
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
getContainerDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/databases?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) {
@@ -1271,7 +1436,7 @@ function clearPostgreSql(app, options, callback) {
debugApp(app, 'Clearing postgresql');
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
getContainerDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/databases/${database}/clear?access_token=${result.token}&username=${username}`, { json: true, rejectUnauthorized: false }, function (error, response) {
@@ -1290,7 +1455,7 @@ function teardownPostgreSql(app, options, callback) {
const { database, username } = postgreSqlNames(app.id);
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
getContainerDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
if (error) return callback(error);
request.delete(`https://${result.ip}:3000/databases/${database}?access_token=${result.token}&username=${username}`, { json: true, rejectUnauthorized: false }, function (error, response) {
@@ -1311,7 +1476,7 @@ function backupPostgreSql(app, options, callback) {
const { database } = postgreSqlNames(app.id);
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
getContainerDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
if (error) return callback(error);
const url = `https://${result.ip}:3000/databases/${database}/backup?access_token=${result.token}`;
@@ -1330,7 +1495,7 @@ function restorePostgreSql(app, options, callback) {
callback = once(callback); // protect from multiple returns with streams
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
getContainerDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
if (error) return callback(error);
var input = fs.createReadStream(dumpPath('postgresql', app.id));
@@ -1347,6 +1512,43 @@ function restorePostgreSql(app, options, callback) {
});
}
function startTurn(existingInfra, callback) {
assert.strictEqual(typeof existingInfra, 'object');
assert.strictEqual(typeof callback, 'function');
// get and ensure we have a turn secret
var turnSecret = safe.fs.readFileSync(paths.ADDON_TURN_SECRET_FILE, 'utf8');
if (!turnSecret) {
turnSecret = 'a' + crypto.randomBytes(15).toString('hex'); // prefix with a to ensure string starts with a letter
safe.fs.writeFileSync(paths.ADDON_TURN_SECRET_FILE, turnSecret, 'utf8');
}
const tag = infra.images.turn.tag;
const memoryLimit = 256;
const realm = settings.adminFqdn();
if (existingInfra.version === infra.version && existingInfra.images.turn && infra.images.turn.tag === existingInfra.images.turn.tag) return callback();
// this exports 3478/tcp, 5349/tls and 50000-51000/udp
const cmd = `docker run --restart=always -d --name="turn" \
--hostname turn \
--net host \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=turn \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_TURN_SECRET="${turnSecret}" \
-e CLOUDRON_REALM="${realm}" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
shell.exec('startTurn', cmd, callback);
}
function startMongodb(existingInfra, callback) {
assert.strictEqual(typeof existingInfra, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -1386,7 +1588,7 @@ function startMongodb(existingInfra, callback) {
shell.exec('startMongodb', cmd, function (error) {
if (error) return callback(error);
waitForService('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error) {
waitForContainer('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error) {
if (error) return callback(error);
if (!upgrading) return callback(null);
@@ -1413,7 +1615,7 @@ function setupMongoDb(app, options, callback) {
oplog: !!options.oplog
};
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/databases?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) {
@@ -1449,7 +1651,7 @@ function clearMongodb(app, options, callback) {
debugApp(app, 'Clearing mongodb');
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/databases/${app.id}/clear?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
@@ -1468,7 +1670,7 @@ function teardownMongoDb(app, options, callback) {
debugApp(app, 'Tearing down mongodb');
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
if (error) return callback(error);
request.delete(`https://${result.ip}:3000/databases/${app.id}?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
@@ -1487,7 +1689,7 @@ function backupMongoDb(app, options, callback) {
debugApp(app, 'Backing up mongodb');
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
if (error) return callback(error);
const url = `https://${result.ip}:3000/databases/${app.id}/backup?access_token=${result.token}`;
@@ -1504,7 +1706,7 @@ function restoreMongoDb(app, options, callback) {
debugApp(app, 'restoreMongoDb');
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
if (error) return callback(error);
const readStream = fs.createReadStream(dumpPath('mongodb', app.id));
@@ -1528,10 +1730,10 @@ function startRedis(existingInfra, callback) {
const tag = infra.images.redis.tag;
const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.redis.tag, tag);
appdb.getAll(function (error, apps) {
apps.getAll(function (error, allApps) {
if (error) return callback(error);
async.eachSeries(apps, function iterator (app, iteratorCallback) {
async.eachSeries(allApps, function iterator (app, iteratorCallback) {
if (!('redis' in app.manifest.addons)) return iteratorCallback(); // app doesn't use the addon
setupRedis(app, app.manifest.addons.redis, iteratorCallback);
@@ -1560,15 +1762,7 @@ function setupRedis(app, options, callback) {
const redisServiceToken = hat(4 * 48);
// Compute redis memory limit based on app's memory limit (this is arbitrary)
var memoryLimit = app.memoryLimit || app.manifest.memoryLimit || 0;
if (memoryLimit === -1) { // unrestricted (debug mode)
memoryLimit = 0;
} else if (memoryLimit === 0 || memoryLimit <= (2 * 1024 * 1024 * 1024)) { // less than 2G (ram+swap)
memoryLimit = 150 * 1024 * 1024; // 150m
} else {
memoryLimit = 600 * 1024 * 1024; // 600m
}
const memoryLimit = app.servicesConfig['redis'] ? app.servicesConfig['redis'].memory : APP_SERVICES['redis'].defaultMemoryLimit;
const tag = infra.images.redis.tag;
const label = app.fqdn;
@@ -1612,7 +1806,7 @@ function setupRedis(app, options, callback) {
});
},
appdb.setAddonConfig.bind(null, app.id, 'redis', env),
waitForService.bind(null, 'redis-' + app.id, 'CLOUDRON_REDIS_TOKEN')
waitForContainer.bind(null, 'redis-' + app.id, 'CLOUDRON_REDIS_TOKEN')
], function (error) {
if (error) debug('Error setting up redis: ', error);
callback(error);
@@ -1627,7 +1821,7 @@ function clearRedis(app, options, callback) {
debugApp(app, 'Clearing redis');
getServiceDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
getContainerDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/clear?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
@@ -1666,7 +1860,7 @@ function backupRedis(app, options, callback) {
debugApp(app, 'Backing up redis');
getServiceDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
getContainerDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
if (error) return callback(error);
const url = `https://${result.ip}:3000/backup?access_token=${result.token}`;
@@ -1683,7 +1877,7 @@ function restoreRedis(app, options, callback) {
callback = once(callback); // protect from multiple returns with streams
getServiceDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
getContainerDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
if (error) return callback(error);
let input;
@@ -1706,6 +1900,27 @@ function restoreRedis(app, options, callback) {
});
}
function statusTurn(callback) {
assert.strictEqual(typeof callback, 'function');
docker.inspect('turn', function (error, container) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, { status: exports.SERVICE_STATUS_STOPPED });
if (error) return callback(error);
docker.memoryUsage(container.Id, function (error, result) {
if (error) return callback(error);
var tmp = {
status: container.State.Running ? exports.SERVICE_STATUS_ACTIVE : exports.SERVICE_STATUS_STOPPED,
memoryUsed: result.memory_stats.usage,
memoryPercent: parseInt(100 * result.memory_stats.usage / result.memory_stats.limit)
};
callback(null, tmp);
});
});
}
function statusDocker(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -1800,3 +2015,13 @@ function statusGraphite(callback) {
});
});
}
function teardownOauth(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'teardownOauth');
appdb.unsetAddonConfig(app.id, 'oauth', callback);
}
+10 -2
View File
@@ -41,7 +41,7 @@ var assert = require('assert'),
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.errorJson', 'apps.runState',
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'subdomains.subdomain AS location', 'subdomains.domain',
'apps.accessRestrictionJson', 'apps.memoryLimit', 'apps.cpuShares',
'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson',
'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.servicesConfigJson', 'apps.bindsJson',
'apps.sso', 'apps.debugModeJson', 'apps.enableBackup',
'apps.creationTime', 'apps.updateTime', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableAutomaticUpdate',
'apps.dataDir', 'apps.ts', 'apps.healthTime' ].join(',');
@@ -94,6 +94,14 @@ function postProcess(result) {
result.debugMode = safe.JSON.parse(result.debugModeJson);
delete result.debugModeJson;
assert(result.servicesConfigJson === null || typeof result.servicesConfigJson === 'string');
result.servicesConfig = safe.JSON.parse(result.servicesConfigJson) || {};
delete result.servicesConfigJson;
assert(result.bindsJson === null || typeof result.bindsJson === 'string');
result.binds = safe.JSON.parse(result.bindsJson) || {};
delete result.bindsJson;
result.alternateDomains = result.alternateDomains || [];
result.alternateDomains.forEach(function (d) {
delete d.appId;
@@ -427,7 +435,7 @@ function updateWithConstraints(id, app, constraints, callback) {
var fields = [ ], values = [ ];
for (var p in app) {
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig') {
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig' || p === 'servicesConfig' || p === 'binds') {
fields.push(`${p}Json = ?`);
values.push(JSON.stringify(app[p]));
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'env') {
+6 -13
View File
@@ -73,7 +73,6 @@ function checkAppHealth(app, callback) {
assert.strictEqual(typeof callback, 'function');
if (app.installationState !== apps.ISTATE_INSTALLED || app.runState !== apps.RSTATE_RUNNING) {
debugApp(app, 'skipped. istate:%s rstate:%s', app.installationState, app.runState);
return callback(null);
}
@@ -103,10 +102,8 @@ function checkAppHealth(app, callback) {
.timeout(HEALTHCHECK_INTERVAL)
.end(function (error, res) {
if (error && !error.response) {
debugApp(app, 'not alive (network error): %s', error.message);
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
} else if (res.statusCode >= 400) { // 2xx and 3xx are ok
debugApp(app, 'not alive : %s', error || res.status);
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
} else {
setHealth(app, apps.HEALTH_HEALTHY, callback);
@@ -180,18 +177,14 @@ function processDockerEvents(intervalSecs, callback) {
function processApp(callback) {
assert.strictEqual(typeof callback, 'function');
apps.getAll(function (error, result) {
apps.getAll(function (error, allApps) {
if (error) return callback(error);
async.each(result, checkAppHealth, function (error) {
if (error) console.error(error);
async.each(allApps, checkAppHealth, function (error) {
const alive = allApps
.filter(function (a) { return a.installationState === apps.ISTATE_INSTALLED && a.runState === apps.RSTATE_RUNNING && a.health === apps.HEALTH_HEALTHY; });
const alive = result
.filter(function (a) { return a.installationState === apps.ISTATE_INSTALLED && a.runState === apps.RSTATE_RUNNING && a.health === apps.HEALTH_HEALTHY; })
.map(a => a.fqdn)
.join(', ');
debug('apps alive: [%s]', alive);
debug(`app health: ${alive.length} alive / ${allApps.length - alive.length} dead.` + (error ? ` ${error.reason}` : ''));
callback(null);
});
@@ -206,7 +199,7 @@ function run(intervalSecs, callback) {
processApp, // this is first because docker.getEvents seems to get 'stuck' sometimes
processDockerEvents.bind(null, intervalSecs)
], function (error) {
if (error) debug(error);
if (error) debug(`run: could not check app health. ${error.message}`);
callback();
});
+885 -837
View File
File diff suppressed because it is too large Load Diff
+38 -152
View File
@@ -11,7 +11,6 @@ exports = module.exports = {
trackFinishedSetup: trackFinishedSetup,
registerWithLoginCredentials: registerWithLoginCredentials,
registerWithLicense: registerWithLicense,
purchaseApp: purchaseApp,
unpurchaseApp: unpurchaseApp,
@@ -20,8 +19,6 @@ exports = module.exports = {
getSubscription: getSubscription,
isFreePlan: isFreePlan,
sendAliveStatus: sendAliveStatus,
getAppUpdate: getAppUpdate,
getBoxUpdate: getBoxUpdate,
@@ -34,32 +31,26 @@ var apps = require('./apps.js'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
debug = require('debug')('box:appstore'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
groups = require('./groups.js'),
mail = require('./mail.js'),
os = require('os'),
paths = require('./paths.js'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
superagent = require('superagent'),
users = require('./users.js'),
util = require('util');
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
// These are the default options and will be adjusted once a subscription state is obtained
// Keep in sync with appstore/routes/cloudrons.js
let gFeatures = {
userMaxCount: null,
externalLdap: true,
eventLog: true,
privateDockerRegistry: true,
branding: true,
userManager: true,
multiAdmin: true,
support: true
userMaxCount: 5,
domainMaxCount: 1,
externalLdap: false,
privateDockerRegistry: false,
branding: false,
support: false,
directoryConfig: false,
mailboxMaxCount: 5,
emailPremium: false
};
// attempt to load feature cache in case appstore would be down
@@ -127,7 +118,7 @@ function registerUser(email, password, callback) {
const url = settings.apiServerOrigin() + '/api/v1/register_user';
superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 409) return callback(new BoxError(BoxError.ALREADY_EXISTS));
if (result.statusCode === 409) return callback(new BoxError(BoxError.ALREADY_EXISTS, error.message));
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `register status code: ${result.statusCode}`));
callback(null);
@@ -235,112 +226,8 @@ function unpurchaseApp(appId, data, callback) {
});
}
function sendAliveStatus(callback) {
callback = callback || NOOP_CALLBACK;
let allSettings, allDomains, mailDomains, loginEvents, userCount, groupCount;
async.series([
function (callback) {
settings.getAll(function (error, result) {
if (error) return callback(error);
allSettings = result;
callback();
});
},
function (callback) {
domains.getAll(function (error, result) {
if (error) return callback(error);
allDomains = result;
callback();
});
},
function (callback) {
mail.getDomains(function (error, result) {
if (error) return callback(error);
mailDomains = result;
callback();
});
},
function (callback) {
eventlog.getAllPaged([ eventlog.ACTION_USER_LOGIN ], null, 1, 1, function (error, result) {
if (error) return callback(error);
loginEvents = result;
callback();
});
},
function (callback) {
users.count(function (error, result) {
if (error) return callback(error);
userCount = result;
callback();
});
},
function (callback) {
groups.count(function (error, result) {
if (error) return callback(error);
groupCount = result;
callback();
});
}
], function (error) {
if (error) return callback(error);
var backendSettings = {
backupConfig: {
provider: allSettings[settings.BACKUP_CONFIG_KEY].provider,
hardlinks: !allSettings[settings.BACKUP_CONFIG_KEY].noHardlinks
},
domainConfig: {
count: allDomains.length,
domains: Array.from(new Set(allDomains.map(function (d) { return { domain: d.domain, provider: d.provider }; })))
},
mailConfig: {
outboundCount: mailDomains.length,
inboundCount: mailDomains.filter(function (d) { return d.enabled; }).length,
catchAllCount: mailDomains.filter(function (d) { return d.catchAll.length !== 0; }).length,
relayProviders: Array.from(new Set(mailDomains.map(function (d) { return d.relay.provider; })))
},
userCount: userCount,
groupCount: groupCount,
appAutoupdatePattern: allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY],
boxAutoupdatePattern: allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY],
timeZone: allSettings[settings.TIME_ZONE_KEY],
sysinfoProvider: allSettings[settings.SYSINFO_CONFIG_KEY].provider
};
var data = {
version: constants.VERSION,
adminFqdn: settings.adminFqdn(),
provider: settings.provider(),
backendSettings: backendSettings,
machine: {
cpus: os.cpus(),
totalmem: os.totalmem()
},
events: {
lastLogin: loginEvents[0] ? (new Date(loginEvents[0].creationTime).getTime()) : 0
}
};
getCloudronToken(function (error, token) {
if (error) return callback(error);
const url = `${settings.apiServerOrigin()}/api/v1/alive`;
superagent.post(url).send(data).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error));
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Sending alive status failed. %s %j', result.status, result.body)));
callback(null);
});
});
});
}
function getBoxUpdate(callback) {
function getBoxUpdate(options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
getCloudronToken(function (error, token) {
@@ -348,7 +235,13 @@ function getBoxUpdate(callback) {
const url = `${settings.apiServerOrigin()}/api/v1/boxupdate`;
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION }).timeout(10 * 1000).end(function (error, result) {
const query = {
accessToken: token,
boxVersion: constants.VERSION,
automatic: options.automatic
};
superagent.get(url).query(query).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
@@ -374,16 +267,24 @@ function getBoxUpdate(callback) {
});
}
function getAppUpdate(app, callback) {
function getAppUpdate(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
getCloudronToken(function (error, token) {
if (error) return callback(error);
const url = `${settings.apiServerOrigin()}/api/v1/appupdate`;
const query = {
accessToken: token,
boxVersion: constants.VERSION,
appId: app.appStoreId,
appVersion: app.manifest.version,
automatic: options.automatic
};
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION, appId: app.appStoreId, appVersion: app.manifest.version }).timeout(10 * 1000).end(function (error, result) {
superagent.get(url).query(query).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error));
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
@@ -401,7 +302,9 @@ function getAppUpdate(app, callback) {
return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Malformed update: %s %s', result.statusCode, result.text)));
}
// { id, creationDate, manifest }
updateInfo.unstable = !!updateInfo.unstable;
// { id, creationDate, manifest, unstable }
callback(null, updateInfo);
});
});
@@ -415,7 +318,7 @@ function registerCloudron(data, callback) {
superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Unable to register cloudron: ${error.message}`));
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Unable to register cloudron: ${result.statusCode} ${error.message}`));
// cloudronId, token, licenseKey
if (!result.body.cloudronId) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no cloudron id'));
@@ -438,16 +341,14 @@ function registerCloudron(data, callback) {
// This works without a Cloudron token as this Cloudron was not yet registered
let gBeginSetupAlreadyTracked = false;
function trackBeginSetup(provider) {
assert.strictEqual(typeof provider, 'string');
function trackBeginSetup() {
// avoid browser reload double tracking, not perfect since box might restart, but covers most cases and is simple
if (gBeginSetupAlreadyTracked) return;
gBeginSetupAlreadyTracked = true;
const url = `${settings.apiServerOrigin()}/api/v1/helper/setup_begin`;
superagent.post(url).send({ provider }).timeout(30 * 1000).end(function (error, result) {
superagent.post(url).send({}).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return console.error(error.message);
if (result.statusCode !== 200) return console.error(error.message);
});
@@ -465,21 +366,6 @@ function trackFinishedSetup(domain) {
});
}
function registerWithLicense(license, domain, callback) {
assert.strictEqual(typeof license, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
getCloudronToken(function (error, token) {
if (token) return callback(new BoxError(BoxError.CONFLICT));
const provider = settings.provider();
const version = constants.VERSION;
registerCloudron({ license, domain, provider, version }, callback);
});
}
function registerWithLoginCredentials(options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -491,7 +377,7 @@ function registerWithLoginCredentials(options, callback) {
}
getCloudronToken(function (error, token) {
if (token) return callback(new BoxError(BoxError.CONFLICT));
if (token) return callback(new BoxError(BoxError.CONFLICT, 'Cloudron is already registered'));
maybeSignup(function (error) {
if (error) return callback(error);
@@ -499,7 +385,7 @@ function registerWithLoginCredentials(options, callback) {
login(options.email, options.password, options.totpToken || '', function (error, result) {
if (error) return callback(error);
registerCloudron({ domain: settings.adminDomain(), accessToken: result.accessToken, provider: settings.provider(), version: constants.VERSION, purpose: options.purpose || '' }, callback);
registerCloudron({ domain: settings.adminDomain(), accessToken: result.accessToken, version: constants.VERSION, purpose: options.purpose || '' }, callback);
});
});
});
@@ -524,7 +410,7 @@ function createTicket(info, auditSource, callback) {
if (error) return callback(error);
collectAppInfoIfNeeded(function (error, result) {
if (error) console.error('Unable to get app info', error);
if (error) return callback(error);
if (result) info.app = result;
let url = settings.apiServerOrigin() + '/api/v1/ticket';
+10 -8
View File
@@ -37,7 +37,6 @@ var addons = require('./addons.js'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
manifestFormat = require('cloudron-manifestformat'),
mkdirp = require('mkdirp'),
net = require('net'),
os = require('os'),
path = require('path'),
@@ -176,7 +175,7 @@ function createAppDir(app, callback) {
assert.strictEqual(typeof callback, 'function');
const appDir = path.join(paths.APPS_DATA_DIR, app.id);
mkdirp(appDir, function (error) {
fs.mkdir(appDir, { recursive: true }, function (error) {
if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error creating directory: ${error.message}`, { appDir }));
callback(null);
@@ -431,12 +430,12 @@ function waitForDnsPropagation(app, callback) {
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Error getting public IP: ${error.message}`));
domains.waitForDnsRecord(app.location, app.domain, 'A', ip, { interval: 5000, times: 240 }, function (error) {
domains.waitForDnsRecord(app.location, app.domain, 'A', ip, { times: 240 }, function (error) {
if (error) return callback(new BoxError(BoxError.DNS_ERROR, `DNS Record is not synced yet: ${error.message}`, { ip: ip, subdomain: app.location, domain: app.domain }));
// now wait for alternateDomains, if any
async.eachSeries(app.alternateDomains, function (domain, iteratorCallback) {
domains.waitForDnsRecord(domain.subdomain, domain.domain, 'A', ip, { interval: 5000, times: 240 }, function (error) {
domains.waitForDnsRecord(domain.subdomain, domain.domain, 'A', ip, { times: 240 }, function (error) {
if (error) return callback(new BoxError(BoxError.DNS_ERROR, `DNS Record is not synced yet: ${error.message}`, { ip: ip, subdomain: domain.subdomain, domain: domain.domain }));
iteratorCallback();
@@ -776,9 +775,6 @@ function configure(app, args, progressCallback, callback) {
startApp.bind(null, app),
progressCallback.bind(null, { percent: 80, message: 'Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
progressCallback.bind(null, { percent: 90, message: 'Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
@@ -902,7 +898,10 @@ function start(app, args, progressCallback, callback) {
assert.strictEqual(typeof callback, 'function');
async.series([
progressCallback.bind(null, { percent: 20, message: 'Starting container' }),
progressCallback.bind(null, { percent: 10, message: 'Starting app services' }),
addons.startAppServices.bind(null, app),
progressCallback.bind(null, { percent: 35, message: 'Starting container' }),
docker.startContainer.bind(null, app.id),
// stopped apps do not renew certs. currently, we don't do DNS to not overwrite existing user settings
@@ -930,6 +929,9 @@ function stop(app, args, progressCallback, callback) {
progressCallback.bind(null, { percent: 20, message: 'Stopping container' }),
docker.stopContainers.bind(null, app.id),
progressCallback.bind(null, { percent: 50, message: 'Stopping app services' }),
addons.stopAppServices.bind(null, app),
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
], function seriesDone(error) {
+25 -30
View File
@@ -6,27 +6,20 @@ var assert = require('assert'),
safe = require('safetydance'),
util = require('util');
var BACKUPS_FIELDS = [ 'id', 'creationTime', 'version', 'type', 'dependsOn', 'state', 'manifestJson', 'format', 'preserveSecs' ];
var BACKUPS_FIELDS = [ 'id', 'identifier', 'creationTime', 'packageVersion', 'type', 'dependsOn', 'state', 'manifestJson', 'format', 'preserveSecs', 'encryptionVersion' ];
exports = module.exports = {
add: add,
add,
getByTypeAndStatePaged: getByTypeAndStatePaged,
getByTypePaged: getByTypePaged,
getByTypePaged,
getByIdentifierPaged,
getByIdentifierAndStatePaged,
get: get,
del: del,
update: update,
getByAppIdPaged: getByAppIdPaged,
get,
del,
update,
_clear: clear,
BACKUP_TYPE_APP: 'app',
BACKUP_TYPE_BOX: 'box',
BACKUP_STATE_NORMAL: 'normal', // should rename to created to avoid listing in UI?
BACKUP_STATE_CREATING: 'creating',
BACKUP_STATE_ERROR: 'error'
_clear: clear
};
function postProcess(result) {
@@ -38,15 +31,15 @@ function postProcess(result) {
delete result.manifestJson;
}
function getByTypeAndStatePaged(type, state, page, perPage, callback) {
assert(type === exports.BACKUP_TYPE_APP || type === exports.BACKUP_TYPE_BOX);
function getByIdentifierAndStatePaged(identifier, state, page, perPage, callback) {
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof state, 'string');
assert(typeof page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0);
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? AND state = ? ORDER BY creationTime DESC LIMIT ?,?',
[ type, state, (page-1)*perPage, perPage ], function (error, results) {
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE identifier = ? AND state = ? ORDER BY creationTime DESC LIMIT ?,?',
[ identifier, state, (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(function (result) { postProcess(result); });
@@ -56,7 +49,7 @@ function getByTypeAndStatePaged(type, state, page, perPage, callback) {
}
function getByTypePaged(type, page, perPage, callback) {
assert(type === exports.BACKUP_TYPE_APP || type === exports.BACKUP_TYPE_BOX);
assert.strictEqual(typeof type, 'string');
assert(typeof page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0);
assert.strictEqual(typeof callback, 'function');
@@ -71,15 +64,14 @@ function getByTypePaged(type, page, perPage, callback) {
});
}
function getByAppIdPaged(page, perPage, appId, callback) {
function getByIdentifierPaged(identifier, page, perPage, callback) {
assert.strictEqual(typeof identifier, 'string');
assert(typeof page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0);
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
// box versions (0.93.x and below) used to use appbackup_ prefix
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? AND state = ? AND id LIKE ? ORDER BY creationTime DESC LIMIT ?,?',
[ exports.BACKUP_TYPE_APP, exports.BACKUP_STATE_NORMAL, '%app%\\_' + appId + '\\_%', (page-1)*perPage, perPage ], function (error, results) {
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE identifier = ? ORDER BY creationTime DESC LIMIT ?,?',
[ identifier, (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(function (result) { postProcess(result); });
@@ -106,8 +98,11 @@ function get(id, callback) {
function add(id, data, callback) {
assert(data && typeof data === 'object');
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof data.version, 'string');
assert(data.type === exports.BACKUP_TYPE_APP || data.type === exports.BACKUP_TYPE_BOX);
assert(data.encryptionVersion === null || typeof data.encryptionVersion === 'number');
assert.strictEqual(typeof data.packageVersion, 'string');
assert.strictEqual(typeof data.type, 'string');
assert.strictEqual(typeof data.identifier, 'string');
assert.strictEqual(typeof data.state, 'string');
assert(util.isArray(data.dependsOn));
assert.strictEqual(typeof data.manifest, 'object');
assert.strictEqual(typeof data.format, 'string');
@@ -116,8 +111,8 @@ function add(id, data, callback) {
var creationTime = data.creationTime || new Date(); // allow tests to set the time
var manifestJson = JSON.stringify(data.manifest);
database.query('INSERT INTO backups (id, version, type, creationTime, state, dependsOn, manifestJson, format) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[ id, data.version, data.type, creationTime, exports.BACKUP_STATE_NORMAL, data.dependsOn.join(','), manifestJson, data.format ],
database.query('INSERT INTO backups (id, identifier, encryptionVersion, packageVersion, type, creationTime, state, dependsOn, manifestJson, format) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[ id, data.identifier, data.encryptionVersion, data.packageVersion, data.type, creationTime, data.state, data.dependsOn.join(','), manifestJson, data.format ],
function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
+434 -206
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -332,7 +332,7 @@ Acme2.prototype.createKeyAndCsr = function (hostname, callback) {
// in some old releases, csr file was corrupt. so always regenerate it
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
} else {
var key = safe.child_process.execSync('openssl genrsa 4096');
var key = safe.child_process.execSync('openssl ecparam -genkey -name secp384r1'); // openssl ecparam -list_curves
if (!key) return callback(new BoxError(BoxError.OPENSSL_ERROR, safe.error));
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new BoxError(BoxError.FS_ERROR, safe.error));
@@ -452,7 +452,7 @@ Acme2.prototype.prepareDnsChallenge = function (hostname, domain, authorization,
domains.upsertDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
if (error) return callback(error);
domains.waitForDnsRecord(challengeSubdomain, domain, 'TXT', txtValue, { interval: 5000, times: 200 }, function (error) {
domains.waitForDnsRecord(challengeSubdomain, domain, 'TXT', txtValue, { times: 200 }, function (error) {
if (error) return callback(error);
callback(null, challenge);
+6 -3
View File
@@ -21,7 +21,8 @@ exports = module.exports = {
runSystemChecks: runSystemChecks,
};
var apps = require('./apps.js'),
var addons = require('./addons.js'),
apps = require('./apps.js'),
appstore = require('./appstore.js'),
assert = require('assert'),
async = require('async'),
@@ -138,10 +139,11 @@ function getConfig(callback) {
mailFqdn: settings.mailFqdn(),
version: constants.VERSION,
isDemo: settings.isDemo(),
provider: settings.provider(),
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
footer: allSettings[settings.FOOTER_KEY] || constants.FOOTER,
features: appstore.getFeatures()
features: appstore.getFeatures(),
profileLocked: allSettings[settings.DIRECTORY_CONFIG_KEY].lockUserProfiles,
mandatory2FA: allSettings[settings.DIRECTORY_CONFIG_KEY].mandatory2FA
});
});
}
@@ -326,6 +328,7 @@ function setDashboardAndMailDomain(domain, auditSource, callback) {
if (error) return callback(error);
mail.onMailFqdnChanged(NOOP_CALLBACK); // this will update dns and re-configure mail server
addons.restartService('turn', NOOP_CALLBACK); // to update the realm variable
callback(null);
});
+4 -5
View File
@@ -32,17 +32,16 @@ exports = module.exports = {
NGINX_DEFAULT_CONFIG_FILE_NAME: 'default.conf',
GHOST_USER_FILE: '/tmp/cloudron_ghost.json',
DEFAULT_TOKEN_EXPIRATION: 7 * 24 * 60 * 60 * 1000, // 1 week
DEFAULT_TOKEN_EXPIRATION: 365 * 24 * 60 * 60 * 1000, // 1 year
DEFAULT_MEMORY_LIMIT: (256 * 1024 * 1024), // see also client.js
DEMO_USERNAME: 'cloudron',
DEMO_BLACKLISTED_APPS: [ 'com.github.cloudtorrent' ],
AUTOUPDATE_PATTERN_NEVER: 'never',
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8),
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8), // also used in dashboard client.js
CLOUDRON: CLOUDRON,
TEST: TEST,
@@ -51,6 +50,6 @@ exports = module.exports = {
FOOTER: '&copy; 2020 &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() : '4.2.0-test'
VERSION: process.env.BOX_ENV === 'cloudron' ? fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim() : '5.1.1-test'
};
+22 -18
View File
@@ -1,16 +1,27 @@
'use strict';
// IMPORTANT: These patterns are together because they spin tasks which acquire a lock
// If the patterns overlap all the time, then the task may not ever get a chance to run!
// If you change this change dashboard patterns in settings.html
const DEFAULT_CLEANUP_BACKUPS_PATTERN = '00 30 1,3,5,23 * * *',
DEFAULT_BOX_ENSURE_BACKUP_PATTERN_LT_6HOURS = '00 45 1,7,13,19 * * *',
DEFAULT_BOX_ENSURE_BACKUP_PATTERN_GT_6HOURS = '00 45 1,3,5,23 * * *',
DEFAULT_BOX_AUTOUPDATE_PATTERN = '00 00 1,3,5,23 * * *',
DEFAULT_APP_AUTOUPDATE_PATTERN = '00 15 1,3,5,23 * * *';
exports = module.exports = {
startJobs: startJobs,
startJobs,
stopJobs: stopJobs,
stopJobs,
handleSettingsChanged: handleSettingsChanged
handleSettingsChanged,
DEFAULT_BOX_AUTOUPDATE_PATTERN,
DEFAULT_APP_AUTOUPDATE_PATTERN
};
var appHealthMonitor = require('./apphealthmonitor.js'),
apps = require('./apps.js'),
appstore = require('./appstore.js'),
assert = require('assert'),
async = require('async'),
auditSource = require('./auditsource.js'),
@@ -29,7 +40,6 @@ var appHealthMonitor = require('./apphealthmonitor.js'),
updateChecker = require('./updatechecker.js');
var gJobs = {
alive: null, // send periodic stats
appAutoUpdater: null,
boxAutoUpdater: null,
appUpdateChecker: null,
@@ -61,12 +71,6 @@ function startJobs(callback) {
assert.strictEqual(typeof callback, 'function');
const randomMinute = Math.floor(60*Math.random());
gJobs.alive = new CronJob({
cronTime: '00 ' + randomMinute + ' * * * *', // every hour on a random minute
onTick: appstore.sendAliveStatus,
start: true
});
gJobs.systemChecks = new CronJob({
cronTime: '00 30 * * * *', // every 30 minutes. if you change this interval, change the notification messages with correct duration
onTick: () => cloudron.runSystemChecks(NOOP_CALLBACK),
@@ -80,14 +84,14 @@ function startJobs(callback) {
});
gJobs.boxUpdateCheckerJob = new CronJob({
cronTime: '00 ' + randomMinute + ' * * * *', // once an hour
onTick: () => updateChecker.checkBoxUpdates(NOOP_CALLBACK),
cronTime: '00 ' + randomMinute + ' 1,3,5,21,23 * * *', // 5 times
onTick: () => updateChecker.checkBoxUpdates({ automatic: true }, NOOP_CALLBACK),
start: true
});
gJobs.appUpdateChecker = new CronJob({
cronTime: '00 ' + randomMinute + ' * * * *', // once an hour
onTick: () => updateChecker.checkAppUpdates(NOOP_CALLBACK),
cronTime: '00 ' + randomMinute + ' 2,4,6,20,22 * * *', // 5 times
onTick: () => updateChecker.checkAppUpdates({ automatic: true }, NOOP_CALLBACK),
start: true
});
@@ -98,7 +102,7 @@ function startJobs(callback) {
});
gJobs.cleanupBackups = new CronJob({
cronTime: '00 45 1,3,5,23 * * *', // every 6 hours. try not to overlap with ensureBackup job
cronTime: DEFAULT_CLEANUP_BACKUPS_PATTERN,
onTick: backups.startCleanupTask.bind(null, auditSource.CRON, NOOP_CALLBACK),
start: true
});
@@ -177,9 +181,9 @@ function backupConfigChanged(value, tz) {
if (gJobs.backup) gJobs.backup.stop();
let pattern;
if (value.intervalSecs <= 6 * 60 * 60) {
pattern = '00 00 1,7,13,19 * * *'; // no option but to backup in the middle of the day
pattern = DEFAULT_BOX_ENSURE_BACKUP_PATTERN_LT_6HOURS; // no option but to backup in the middle of the day
} else {
pattern = '00 00 1,3,5,23 * * *'; // avoid middle of the day backups
pattern = DEFAULT_BOX_ENSURE_BACKUP_PATTERN_GT_6HOURS; // avoid middle of the day backups. it's 45 to not overlap auto-updates
}
gJobs.backup = new CronJob({
+38 -97
View File
@@ -17,12 +17,12 @@ var assert = require('assert'),
BoxError = require('./boxerror.js'),
child_process = require('child_process'),
constants = require('./constants.js'),
debug = require('debug')('box:database'),
mysql = require('mysql'),
once = require('once'),
util = require('util');
var gConnectionPool = null,
gDefaultConnection = null;
var gConnectionPool = null;
const gDatabase = {
hostname: '127.0.0.1',
@@ -42,59 +42,37 @@ function initialize(callback) {
gDatabase.hostname = require('child_process').execSync('docker inspect -f "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" mysql-server').toString().trim();
}
// https://github.com/mysqljs/mysql#pool-options
gConnectionPool = mysql.createPool({
connectionLimit: 5, // this has to be > 1 since we store one connection as 'default'. the rest for transactions
connectionLimit: 5,
host: gDatabase.hostname,
user: gDatabase.username,
password: gDatabase.password,
port: gDatabase.port,
database: gDatabase.name,
multipleStatements: false,
waitForConnections: true, // getConnection() will wait until a connection is avaiable
ssl: false,
timezone: 'Z' // mysql follows the SYSTEM timezone. on Cloudron, this is UTC
});
gConnectionPool.on('connection', function (connection) {
// connection objects are re-used. so we have to attach to the event here (once) to prevent crash
// note the pool also has an 'acquire' event but that is called whenever we do a getConnection()
connection.on('error', (error) => debug(`Connection ${connection.threadId} error: ${error.message} ${error.code}`));
connection.query('USE ' + gDatabase.name);
connection.query('SET SESSION sql_mode = \'strict_all_tables\'');
});
reconnect(callback);
callback(null);
}
function uninitialize(callback) {
if (gConnectionPool) {
gConnectionPool.end(callback);
gConnectionPool = null;
} else {
callback(null);
}
}
if (!gConnectionPool) return callback(null);
function reconnect(callback) {
callback = callback ? once(callback) : function () {};
gConnectionPool.getConnection(function (error, connection) {
if (error) {
console.error('Unable to reestablish connection to database. Try again in a bit.', error.message);
return setTimeout(reconnect.bind(null, callback), 1000);
}
connection.on('error', function (error) {
// by design, we catch all normal errors by providing callbacks.
// this function should be invoked only when we have no callbacks pending and we have a fatal error
assert(error.fatal, 'Non-fatal error on connection object');
console.error('Unhandled mysql connection error.', error);
// This is most likely an issue an can cause double callbacks from reconnect()
setTimeout(reconnect.bind(null, callback), 1000);
});
gDefaultConnection = connection;
callback(null);
});
gConnectionPool.end(callback);
gConnectionPool = null;
}
function clear(callback) {
@@ -107,80 +85,43 @@ function clear(callback) {
child_process.exec(cmd, callback);
}
function beginTransaction(callback) {
assert.strictEqual(typeof callback, 'function');
if (gConnectionPool === null) return callback(new BoxError(BoxError.DATABASE_ERROR, 'No database connection pool.'));
gConnectionPool.getConnection(function (error, connection) {
if (error) {
console.error('Unable to get connection to database. Try again in a bit.', error.message);
return setTimeout(beginTransaction.bind(null, callback), 1000);
}
connection.beginTransaction(function (error) {
if (error) return callback(error);
return callback(null, connection);
});
});
}
function rollback(connection, callback) {
assert.strictEqual(typeof callback, 'function');
connection.rollback(function (error) {
if (error) console.error(error); // can this happen?
connection.release();
callback(null);
});
}
// FIXME: if commit fails, is it supposed to return an error ?
function commit(connection, callback) {
assert.strictEqual(typeof callback, 'function');
connection.commit(function (error) {
if (error) return rollback(connection, callback);
connection.release();
return callback(null);
});
}
function query() {
var args = Array.prototype.slice.call(arguments);
var callback = args[args.length - 1];
const args = Array.prototype.slice.call(arguments);
const callback = args[args.length - 1];
assert.strictEqual(typeof callback, 'function');
if (gDefaultConnection === null) return callback(new BoxError(BoxError.DATABASE_ERROR, 'No connection to database'));
if (constants.TEST && !gConnectionPool) return callback(new BoxError(BoxError.DATABASE_ERROR, 'database.js not initialized'));
args[args.length -1 ] = function (error, result) {
if (error && error.fatal) {
gDefaultConnection = null;
setTimeout(reconnect, 1000);
}
callback(error, result);
};
gDefaultConnection.query.apply(gDefaultConnection, args);
gConnectionPool.query.apply(gConnectionPool, args); // this is same as getConnection/query/release
}
function transaction(queries, callback) {
assert(util.isArray(queries));
assert.strictEqual(typeof callback, 'function');
beginTransaction(function (error, conn) {
callback = once(callback);
gConnectionPool.getConnection(function (error, connection) {
if (error) return callback(error);
async.mapSeries(queries, function iterator(query, done) {
conn.query(query.query, query.args, done);
}, function seriesDone(error, results) {
if (error) return rollback(conn, callback.bind(null, error));
const releaseConnection = (error) => { connection.release(); callback(error); };
commit(conn, callback.bind(null, null, results));
connection.beginTransaction(function (error) {
if (error) return releaseConnection(error);
async.mapSeries(queries, function iterator(query, done) {
connection.query(query.query, query.args, done);
}, function seriesDone(error, results) {
if (error) return connection.rollback(() => releaseConnection(error));
connection.commit(function (error) {
if (error) return connection.rollback(() => releaseConnection(error));
connection.release();
callback(null, results);
});
});
});
});
}
@@ -203,7 +144,7 @@ function exportToFile(file, callback) {
// latest mysqldump enables column stats by default which is not present in MySQL 5.7 server
// this option must not be set in production cloudrons which still use the old mysqldump
const disableColStats = (constants.TEST && process.env.DESKTOP_SESSION !== 'ubuntu') ? '--column-statistics=0' : '';
const disableColStats = (constants.TEST && require('fs').readFileSync('/etc/lsb-release', 'utf-8').includes('20.04')) ? '--column-statistics=0' : '';
var cmd = `/usr/bin/mysqldump -h "${gDatabase.hostname}" -u root -p${gDatabase.password} ${disableColStats} --single-transaction --routines --triggers ${gDatabase.name} > "${file}"`;
+3 -2
View File
@@ -12,6 +12,7 @@ exports = module.exports = {
var assert = require('assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/caas'),
domains = require('../domains.js'),
settings = require('../settings.js'),
@@ -31,7 +32,7 @@ function getFqdn(location, domain) {
}
function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER;
domainObject.config.token = constants.SECRET_PLACEHOLDER;
// do not return the 'key'. in caas, this is private
delete domainObject.fallbackCertificate.key;
@@ -40,7 +41,7 @@ function removePrivateFields(domainObject) {
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function upsert(domainObject, location, type, values, callback) {
+12 -6
View File
@@ -13,6 +13,7 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/cloudflare'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
@@ -25,12 +26,12 @@ var assert = require('assert'),
var CLOUDFLARE_ENDPOINT = 'https://api.cloudflare.com/client/v4';
function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER;
domainObject.config.token = constants.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function translateRequestError(result, callback) {
@@ -39,9 +40,14 @@ function translateRequestError(result, callback) {
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND, util.format('%s %j', result.statusCode, 'API does not exist')));
if (result.statusCode === 422) return callback(new BoxError(BoxError.BAD_FIELD, result.body.message));
if ((result.statusCode === 400 || result.statusCode === 401 || result.statusCode === 403) && result.body.errors.length > 0) {
let error = result.body.errors[0];
let message = `message: ${error.message} statusCode: ${result.statusCode} code:${error.code}`;
if (result.statusCode === 400 || result.statusCode === 401 || result.statusCode === 403) {
let message = 'Unknown error';
if (typeof result.body.error === 'string') {
message = `message: ${result.body.error} statusCode: ${result.statusCode}`;
} else if (Array.isArray(result.body.errors) && result.body.errors.length > 0) {
let error = result.body.errors[0];
message = `message: ${error.message} statusCode: ${result.statusCode} code:${error.code}`;
}
return callback(new BoxError(BoxError.ACCESS_DENIED, message));
}
@@ -284,7 +290,7 @@ function verifyDnsConfig(domainObject, callback) {
if (dnsConfig.tokenType !== 'GlobalApiKey' && dnsConfig.tokenType !== 'ApiToken') return callback(new BoxError(BoxError.BAD_FIELD, 'tokenType is required', { field: 'tokenType' }));
if (dnsConfig.tokenType === 'GlobalApiKey') {
if ('email' in dnsConfig && typeof dnsConfig.email !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'email must be a non-empty string', { field: 'email' }));
if (typeof dnsConfig.email !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'email must be a non-empty string', { field: 'email' }));
}
const ip = '127.0.0.1';
+3 -2
View File
@@ -13,6 +13,7 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/digitalocean'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
@@ -28,12 +29,12 @@ function formatError(response) {
}
function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER;
domainObject.config.token = constants.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function getInternal(dnsConfig, zoneName, name, type, callback) {
+3 -2
View File
@@ -12,6 +12,7 @@ exports = module.exports = {
var assert = require('assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/gandi'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
@@ -26,12 +27,12 @@ function formatError(response) {
}
function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER;
domainObject.config.token = constants.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function upsert(domainObject, location, type, values, callback) {
+3 -2
View File
@@ -12,6 +12,7 @@ exports = module.exports = {
var assert = require('assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/gcdns'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
@@ -21,12 +22,12 @@ var assert = require('assert'),
_ = require('underscore');
function removePrivateFields(domainObject) {
domainObject.config.credentials.private_key = domains.SECRET_PLACEHOLDER;
domainObject.config.credentials.private_key = constants.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.credentials.private_key === domains.SECRET_PLACEHOLDER && currentConfig.credentials) newConfig.credentials.private_key = currentConfig.credentials.private_key;
if (newConfig.credentials.private_key === constants.SECRET_PLACEHOLDER && currentConfig.credentials) newConfig.credentials.private_key = currentConfig.credentials.private_key;
}
function getDnsCredentials(dnsConfig) {
+3 -2
View File
@@ -12,6 +12,7 @@ exports = module.exports = {
var assert = require('assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/godaddy'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
@@ -32,12 +33,12 @@ function formatError(response) {
}
function removePrivateFields(domainObject) {
domainObject.config.apiSecret = domains.SECRET_PLACEHOLDER;
domainObject.config.apiSecret = constants.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.apiSecret === domains.SECRET_PLACEHOLDER) newConfig.apiSecret = currentConfig.apiSecret;
if (newConfig.apiSecret === constants.SECRET_PLACEHOLDER) newConfig.apiSecret = currentConfig.apiSecret;
}
function upsert(domainObject, location, type, values, callback) {
+2 -2
View File
@@ -21,13 +21,13 @@ var assert = require('assert'),
util = require('util');
function removePrivateFields(domainObject) {
// in-place removal of tokens and api keys with domains.SECRET_PLACEHOLDER
// in-place removal of tokens and api keys with constants.SECRET_PLACEHOLDER
return domainObject;
}
// eslint-disable-next-line no-unused-vars
function injectPrivateFields(newConfig, currentConfig) {
// in-place injection of tokens and api keys which came in with domains.SECRET_PLACEHOLDER
// in-place injection of tokens and api keys which came in with constants.SECRET_PLACEHOLDER
}
function upsert(domainObject, location, type, values, callback) {
+310
View File
@@ -0,0 +1,310 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
let async = require('async'),
assert = require('assert'),
constants = require('../constants.js'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:dns/linode'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
superagent = require('superagent'),
util = require('util'),
waitForDns = require('./waitfordns.js');
const LINODE_ENDPOINT = 'https://api.linode.com/v4';
function formatError(response) {
return util.format('Linode DNS error [%s] %j', response.statusCode, response.body);
}
function removePrivateFields(domainObject) {
domainObject.config.token = constants.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function getZoneId(dnsConfig, zoneName, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof callback, 'function');
// returns 100 at a time
superagent.get(`${LINODE_ENDPOINT}/domains`)
.set('Authorization', 'Bearer ' + dnsConfig.token)
.timeout(30 * 1000)
.retry(5)
.end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
if (!Array.isArray(result.body.data)) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response'));
const zone = result.body.data.find(d => d.domain === zoneName);
if (!zone || !zone.id) return callback(new BoxError(BoxError.NOT_FOUND, 'Zone not found'));
debug(`getZoneId: zone id of ${zoneName} is ${zone.id}`);
callback(null, zone.id);
});
}
function getZoneRecords(dnsConfig, zoneName, name, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`getInternal: getting dns records of ${zoneName} with ${name} and type ${type}`);
getZoneId(dnsConfig, zoneName, function (error, zoneId) {
if (error) return callback(error);
let page = 0, more = false;
let records = [];
async.doWhilst(function (iteratorDone) {
const url = `${LINODE_ENDPOINT}/domains/${zoneId}/records?page=${++page}`;
superagent.get(url)
.set('Authorization', 'Bearer ' + dnsConfig.token)
.timeout(30 * 1000)
.retry(5)
.end(function (error, result) {
if (error && !error.response) return iteratorDone(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 404) return iteratorDone(new BoxError(BoxError.NOT_FOUND, formatError(result)));
if (result.statusCode === 403 || result.statusCode === 401) return iteratorDone(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return iteratorDone(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
records = records.concat(result.body.data.filter(function (record) {
return (record.type === type && record.name === name);
}));
more = result.body.page !== result.body.pages;
iteratorDone();
});
}, function () { return more; }, function (error) {
debug('getZoneRecords:', error, JSON.stringify(records));
if (error) return callback(error);
callback(null, { zoneId, records });
});
});
}
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '';
getZoneRecords(dnsConfig, zoneName, name, type, function (error, { records }) {
if (error) return callback(error);
var tmp = records.map(function (record) { return record.target; });
debug('get: %j', tmp);
return callback(null, tmp);
});
}
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '';
debug('upsert: %s for zone %s of type %s with values %j', name, zoneName, type, values);
getZoneRecords(dnsConfig, zoneName, name, type, function (error, { zoneId, records }) {
if (error) return callback(error);
let i = 0, recordIds = []; // used to track available records to update instead of create
async.eachSeries(values, function (value, iteratorCallback) {
let data = {
type: type,
ttl_sec: 300 // lowest
};
if (type === 'MX') {
data.priority = parseInt(value.split(' ')[0], 10);
data.target = value.split(' ')[1];
} else if (type === 'TXT') {
data.target = value.replace(/^"(.*)"$/, '$1'); // strip any double quotes
} else {
data.target = value;
}
if (i >= records.length) {
data.name = name; // only set for new records
superagent.post(`${LINODE_ENDPOINT}/domains/${zoneId}/records`)
.set('Authorization', 'Bearer ' + dnsConfig.token)
.send(data)
.timeout(30 * 1000)
.retry(5)
.end(function (error, result) {
if (error && !error.response) return iteratorCallback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 400) return iteratorCallback(new BoxError(BoxError.BAD_FIELD, formatError(result)));
if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return iteratorCallback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
recordIds.push(result.body.id);
return iteratorCallback(null);
});
} else {
superagent.put(`${LINODE_ENDPOINT}/domains/${zoneId}/records/${records[i].id}`)
.set('Authorization', 'Bearer ' + dnsConfig.token)
.send(data)
.timeout(30 * 1000)
.retry(5)
.end(function (error, result) {
// increment, as we have consumed the record
++i;
if (error && !error.response) return iteratorCallback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 400) return iteratorCallback(new BoxError(BoxError.BAD_FIELD, formatError(result)));
if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return iteratorCallback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
recordIds.push(result.body.id);
return iteratorCallback(null);
});
}
}, function (error) {
if (error) return callback(error);
debug('upsert: completed with recordIds:%j', recordIds);
callback();
});
});
}
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '';
getZoneRecords(dnsConfig, zoneName, name, type, function (error, { zoneId, records }) {
if (error) return callback(error);
if (records.length === 0) return callback(null);
var tmp = records.filter(function (record) { return values.some(function (value) { return value === record.target; }); });
debug('del: %j', tmp);
if (tmp.length === 0) return callback(null);
// FIXME we only handle the first one currently
superagent.del(`${LINODE_ENDPOINT}/domains/${zoneId}/records/${tmp[0].id}`)
.set('Authorization', 'Bearer ' + dnsConfig.token)
.timeout(30 * 1000)
.retry(5)
.end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 404) return callback(null);
if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
debug('del: done');
return callback(null);
});
});
}
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'token must be a non-empty string', { field: 'token' }));
const ip = '127.0.0.1';
var credentials = {
token: dnsConfig.token
};
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new BoxError(BoxError.BAD_FIELD, 'Unable to resolve nameservers for this domain', { field: 'nameservers' }));
if (error || !nameservers) return callback(new BoxError(BoxError.BAD_FIELD, error ? error.message : 'Unable to get nameservers', { field: 'nameservers' }));
if (nameservers.map(function (n) { return n.toLowerCase(); }).indexOf('ns1.linode.com') === -1) {
debug('verifyDnsConfig: %j does not contains DO NS', nameservers);
return callback(new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to Linode', { field: 'nameservers' }));
}
const location = 'cloudrontestdns';
upsert(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added');
del(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
callback(null, credentials);
});
});
});
}
+3 -2
View File
@@ -12,6 +12,7 @@ exports = module.exports = {
var assert = require('assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/namecheap'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
@@ -25,12 +26,12 @@ var assert = require('assert'),
const ENDPOINT = 'https://api.namecheap.com/xml.response';
function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER;
domainObject.config.token = constants.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function getQuery(dnsConfig, callback) {
+11 -2
View File
@@ -12,6 +12,7 @@ exports = module.exports = {
var assert = require('assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/namecom'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
@@ -27,12 +28,12 @@ function formatError(response) {
}
function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER;
domainObject.config.token = constants.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function addRecord(dnsConfig, zoneName, name, type, values, callback) {
@@ -54,6 +55,10 @@ function addRecord(dnsConfig, zoneName, name, type, values, callback) {
if (type === 'MX') {
data.priority = parseInt(values[0].split(' ')[0], 10);
data.answer = values[0].split(' ')[1];
} else if (type === 'TXT') {
// we have to strip the quoting for some odd reason for name.com! If you change that also change updateRecord
let tmp = values[0];
data.answer = tmp.indexOf('"') === 0 && tmp.lastIndexOf('"') === tmp.length-1 ? tmp.slice(1, tmp.length-1) : tmp;
} else {
data.answer = values[0];
}
@@ -91,6 +96,10 @@ function updateRecord(dnsConfig, zoneName, recordId, name, type, values, callbac
if (type === 'MX') {
data.priority = parseInt(values[0].split(' ')[0], 10);
data.answer = values[0].split(' ')[1];
} else if (type === 'TXT') {
// we have to strip the quoting for some odd reason for name.com! If you change that also change addRecord
let tmp = values[0];
data.answer = tmp.indexOf('"') === 0 && tmp.lastIndexOf('"') === tmp.length-1 ? tmp.slice(1, tmp.length-1) : tmp;
} else {
data.answer = values[0];
}
+3 -2
View File
@@ -13,6 +13,7 @@ exports = module.exports = {
var assert = require('assert'),
AWS = require('aws-sdk'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/route53'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
@@ -21,12 +22,12 @@ var assert = require('assert'),
_ = require('underscore');
function removePrivateFields(domainObject) {
domainObject.config.secretAccessKey = domains.SECRET_PLACEHOLDER;
domainObject.config.secretAccessKey = constants.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.secretAccessKey === domains.SECRET_PLACEHOLDER) newConfig.secretAccessKey = currentConfig.secretAccessKey;
if (newConfig.secretAccessKey === constants.SECRET_PLACEHOLDER) newConfig.secretAccessKey = currentConfig.secretAccessKey;
}
function getDnsCredentials(dnsConfig) {
+31 -43
View File
@@ -6,8 +6,6 @@ exports = module.exports = {
injectPrivateFields: injectPrivateFields,
removePrivateFields: removePrivateFields,
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8),
ping: ping,
info: info,
@@ -55,12 +53,6 @@ const CLEARVOLUME_CMD = path.join(__dirname, 'scripts/clearvolume.sh'),
const DOCKER_SOCKET_PATH = '/var/run/docker.sock';
const gConnection = new Docker({ socketPath: DOCKER_SOCKET_PATH });
function debugApp(app) {
assert(typeof app === 'object');
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function testRegistryConfig(auth, callback) {
assert.strictEqual(typeof auth, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -73,13 +65,13 @@ function testRegistryConfig(auth, callback) {
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.password === exports.SECRET_PLACEHOLDER) newConfig.password = currentConfig.password;
if (newConfig.password === constants.SECRET_PLACEHOLDER) newConfig.password = currentConfig.password;
}
function removePrivateFields(registryConfig) {
assert.strictEqual(typeof registryConfig, 'object');
if (registryConfig.password) registryConfig.password = exports.SECRET_PLACEHOLDER;
if (registryConfig.password) registryConfig.password = constants.SECRET_PLACEHOLDER;
return registryConfig;
}
@@ -188,6 +180,19 @@ function downloadImage(manifest, callback) {
}, callback);
}
function getBindsSync(app) {
assert.strictEqual(typeof app, 'object');
let binds = [];
for (let name of Object.keys(app.binds)) {
const bind = app.binds[name];
binds.push(`${bind.hostPath}:/media/${name}:${bind.readOnly ? 'ro' : 'rw'}`);
}
return binds;
}
function createSubcontainer(app, name, cmd, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof name, 'string');
@@ -277,6 +282,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
},
HostConfig: {
Mounts: addons.getMountsSync(app, app.manifest.addons),
Binds: getBindsSync(app), // ideally, we have to use 'Mounts' but we have to create volumes then
LogConfig: {
Type: 'syslog',
Config: {
@@ -299,7 +305,9 @@ function createSubcontainer(app, name, cmd, options, callback) {
NetworkMode: 'cloudron', // user defined bridge network
Dns: ['172.18.0.1'], // use internal dns
DnsSearch: ['.'], // use internal dns
SecurityOpt: [ 'apparmor=docker-cloudron-app' ]
SecurityOpt: [ 'apparmor=docker-cloudron-app' ],
CapAdd: [],
CapDrop: []
},
NetworkingConfig: {
EndpointsConfig: {
@@ -311,16 +319,14 @@ function createSubcontainer(app, name, cmd, options, callback) {
};
var capabilities = manifest.capabilities || [];
if (capabilities.includes('net_admin')) {
containerOptions.HostConfig.CapAdd = [
'NET_ADMIN'
];
}
// https://docs-stage.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
if (capabilities.includes('net_admin')) containerOptions.HostConfig.CapAdd.push('NET_ADMIN', 'NET_RAW');
if (capabilities.includes('mlock')) containerOptions.HostConfig.CapAdd.push('IPC_LOCK'); // mlock prevents swapping
if (!capabilities.includes('ping')) containerOptions.HostConfig.CapDrop.push('NET_RAW'); // NET_RAW is included by default by Docker
containerOptions = _.extend(containerOptions, options);
debugApp(app, 'Creating container for %s', app.manifest.dockerImage);
gConnection.createContainer(containerOptions, function (error, container) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
@@ -338,7 +344,6 @@ function startContainer(containerId, callback) {
assert.strictEqual(typeof callback, 'function');
var container = gConnection.getContainer(containerId);
debug('Starting container %s', containerId);
container.start(function (error) {
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
@@ -354,7 +359,6 @@ function restartContainer(containerId, callback) {
assert.strictEqual(typeof callback, 'function');
var container = gConnection.getContainer(containerId);
debug('Restarting container %s', containerId);
container.restart(function (error) {
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
@@ -375,7 +379,6 @@ function stopContainer(containerId, callback) {
}
var container = gConnection.getContainer(containerId);
debug('Stopping container %s', containerId);
var options = {
t: 10 // wait for 10 seconds before killing it
@@ -384,13 +387,9 @@ function stopContainer(containerId, callback) {
container.stop(options, function (error) {
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Error stopping container:' + error.message));
debug('Waiting for container ' + containerId);
container.wait(function (error, data) {
container.wait(function (error/*, data */) {
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Error waiting on container:' + error.message));
debug('Container %s stopped with status code [%s]', containerId, data ? String(data.StatusCode) : '');
return callback(null);
});
});
@@ -400,8 +399,6 @@ function deleteContainer(containerId, callback) {
assert(!containerId || typeof containerId === 'string');
assert.strictEqual(typeof callback, 'function');
debug('deleting container %s', containerId);
if (containerId === null) return callback(null);
var container = gConnection.getContainer(containerId);
@@ -428,8 +425,6 @@ function deleteContainers(appId, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debug('deleting containers of %s', appId);
let labels = [ 'appId=' + appId ];
if (options.managedOnly) labels.push('isCloudronManaged=true');
@@ -446,8 +441,6 @@ function stopContainers(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
debug('Stopping containers of %s', appId);
gConnection.listContainers({ all: 1, filters: JSON.stringify({ label: [ 'appId=' + appId ] }) }, function (error, containers) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
@@ -573,10 +566,10 @@ function memoryUsage(containerId, callback) {
});
}
function createVolume(app, name, volumeDataDir, callback) {
assert.strictEqual(typeof app, 'object');
function createVolume(name, volumeDataDir, labels, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof volumeDataDir, 'string');
assert.strictEqual(typeof labels, 'object');
assert.strictEqual(typeof callback, 'function');
const volumeOptions = {
@@ -587,10 +580,7 @@ function createVolume(app, name, volumeDataDir, callback) {
device: volumeDataDir,
o: 'bind'
},
Labels: {
'fqdn': app.fqdn,
'appId': app.id
},
Labels: labels
};
// requires sudo because the path can be outside appsdata
@@ -605,8 +595,7 @@ function createVolume(app, name, volumeDataDir, callback) {
});
}
function clearVolume(app, name, options, callback) {
assert.strictEqual(typeof app, 'object');
function clearVolume(name, options, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -626,14 +615,13 @@ function clearVolume(app, name, options, callback) {
}
// this only removes the volume and not the data
function removeVolume(app, name, callback) {
assert.strictEqual(typeof app, 'object');
function removeVolume(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
let volume = gConnection.getVolume(name);
volume.remove(function (error) {
if (error && error.statusCode !== 404) return callback(new BoxError(BoxError.DOCKER_ERROR, `removeVolume: Error removing volume of ${app.id} ${error.message}`));
if (error && error.statusCode !== 404) return callback(new BoxError(BoxError.DOCKER_ERROR, `removeVolume: Error removing volume: ${error.message}`));
callback();
});
+15 -16
View File
@@ -23,11 +23,6 @@ var apps = require('./apps.js'),
var gHttpServer = null;
function authorizeApp(req, res, next) {
// TODO add here some authorization
// - block apps not using the docker addon
// - block calls regarding platform containers
// - only allow managing and inspection of containers belonging to the app
// make the tests pass for now
if (constants.TEST) {
req.app = { id: 'testappid' };
@@ -64,6 +59,8 @@ function attachDockerRequest(req, res, next) {
dockerResponse.pipe(res, { end: true });
});
req.dockerRequest.on('error', () => {}); // abort() throws
next();
}
@@ -74,22 +71,21 @@ function containersCreate(req, res, next) {
safe.set(req.body, 'Labels', _.extend({ }, safe.query(req.body, 'Labels'), { appId: req.app.id, isCloudronManaged: String(false) })); // overwrite the app id to track containers of an app
safe.set(req.body, 'HostConfig.LogConfig', { Type: 'syslog', Config: { 'tag': req.app.id, 'syslog-address': 'udp://127.0.0.1:2514', 'syslog-format': 'rfc5424' }});
const appDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'data'),
dockerDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'docker');
const appDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'data');
debug('Original volume binds:', req.body.HostConfig.Binds);
debug('Original bind mounts:', req.body.HostConfig.Binds);
let binds = [];
for (let bind of (req.body.HostConfig.Binds || [])) {
if (bind.startsWith(appDataDir)) binds.push(bind); // eclipse will inspect docker to find out the host folders and pass that to child containers
else if (bind.startsWith('/app/data')) binds.push(bind.replace(new RegExp('^/app/data'), appDataDir));
else binds.push(`${dockerDataDir}/${bind}`);
if (!bind.startsWith('/app/data/')) {
req.dockerRequest.abort();
return next(new HttpError(400, 'Binds must be under /app/data/'));
}
binds.push(bind.replace(new RegExp('^/app/data/'), appDataDir + '/'));
}
// cleanup the paths from potential double slashes
binds = binds.map(function (bind) { return bind.replace(/\/+/g, '/'); });
debug('Rewritten volume binds:', binds);
debug('Rewritten bind mounts:', binds);
safe.set(req.body, 'HostConfig.Binds', binds);
let plainBody = JSON.stringify(req.body);
@@ -117,6 +113,9 @@ function start(callback) {
assert(gHttpServer === null, 'Already started');
let json = middleware.json({ strict: true });
// we protect container create as the app/admin can otherwise mount random paths (like the ghost file)
// protected other paths is done by preventing install/exec access of apps using docker addon
let router = new express.Router();
router.post('/:version/containers/create', containersCreate);
@@ -137,7 +136,7 @@ function start(callback) {
.use(middleware.lastMile());
gHttpServer = http.createServer(proxyServer);
gHttpServer.listen(constants.DOCKER_PROXY_PORT, '0.0.0.0', callback);
gHttpServer.listen(constants.DOCKER_PROXY_PORT, '172.18.0.1', callback);
// Overwrite the default 2min request timeout. This is required for large builds for example
gHttpServer.setTimeout(60 * 60 * 1000);
+21 -10
View File
@@ -51,17 +51,22 @@ function getAll(callback) {
});
}
function add(name, domain, callback) {
function add(name, data, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'object');
assert.strictEqual(typeof domain.zoneName, 'string');
assert.strictEqual(typeof domain.provider, 'string');
assert.strictEqual(typeof domain.config, 'object');
assert.strictEqual(typeof domain.tlsConfig, 'object');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof data.zoneName, 'string');
assert.strictEqual(typeof data.provider, 'string');
assert.strictEqual(typeof data.config, 'object');
assert.strictEqual(typeof data.tlsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson) VALUES (?, ?, ?, ?, ?)', [ name, domain.zoneName, domain.provider, JSON.stringify(domain.config), JSON.stringify(domain.tlsConfig) ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, error));
let queries = [
{ query: 'INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson) VALUES (?, ?, ?, ?, ?)', args: [ name, data.zoneName, data.provider, JSON.stringify(data.config), JSON.stringify(data.tlsConfig) ] },
{ query: 'INSERT INTO mail (domain, dkimSelector) VALUES (?, ?)', args: [ name, data.dkimSelector || 'cloudron' ] },
];
database.transaction(queries, function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, 'Domain already exists'));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
@@ -100,7 +105,12 @@ function del(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM domains WHERE domain=?', [ domain ], function (error, result) {
let queries = [
{ query: 'DELETE FROM mail WHERE domain = ?', args: [ domain ] },
{ query: 'DELETE FROM domains WHERE domain = ?', args: [ domain ] },
];
database.transaction(queries, function (error, results) {
if (error && error.code === 'ER_ROW_IS_REFERENCED_2') {
if (error.message.indexOf('apps_mailDomain_constraint') !== -1) return callback(new BoxError(BoxError.CONFLICT, 'Domain is in use by an app or the mailbox of an app. Check the domains of apps and the Email section of each app.'));
if (error.message.indexOf('subdomains') !== -1) return callback(new BoxError(BoxError.CONFLICT, 'Domain is in use by one or more app(s).'));
@@ -108,8 +118,9 @@ function del(domain, callback) {
return callback(new BoxError(BoxError.CONFLICT, error.message));
}
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Domain not found'));
if (results[1].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Domain not found'));
callback(null);
});
+16 -5
View File
@@ -28,9 +28,7 @@ module.exports = exports = {
checkDnsRecords: checkDnsRecords,
prepareDashboardDomain: prepareDashboardDomain,
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8)
prepareDashboardDomain: prepareDashboardDomain
};
var assert = require('assert'),
@@ -40,6 +38,7 @@ var assert = require('assert'),
debug = require('debug')('box:domains'),
domaindb = require('./domaindb.js'),
eventlog = require('./eventlog.js'),
mail = require('./mail.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
@@ -48,6 +47,8 @@ var assert = require('assert'),
util = require('util'),
_ = require('underscore');
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
// choose which subdomain backend we use for test purpose we use route53
function api(provider) {
assert.strictEqual(typeof provider, 'string');
@@ -60,6 +61,7 @@ function api(provider) {
case 'digitalocean': return require('./dns/digitalocean.js');
case 'gandi': return require('./dns/gandi.js');
case 'godaddy': return require('./dns/godaddy.js');
case 'linode': return require('./dns/linode.js');
case 'namecom': return require('./dns/namecom.js');
case 'namecheap': return require('./dns/namecheap.js');
case 'noop': return require('./dns/noop.js');
@@ -170,7 +172,7 @@ function add(domain, data, auditSource, callback) {
assert.strictEqual(typeof data.tlsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
let { zoneName, provider, config, fallbackCertificate, tlsConfig } = data;
let { zoneName, provider, config, fallbackCertificate, tlsConfig, dkimSelector } = data;
if (!tld.isValid(domain)) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid domain', { field: 'domain' }));
if (domain.endsWith('.')) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid domain', { field: 'domain' }));
@@ -193,10 +195,12 @@ function add(domain, data, auditSource, callback) {
let error = validateTlsConfig(tlsConfig, provider);
if (error) return callback(error);
if (!dkimSelector) dkimSelector = 'cloudron-' + settings.adminDomain().replace(/\./g, '');
verifyDnsConfig(config, domain, zoneName, provider, function (error, sanitizedConfig) {
if (error) return callback(error);
domaindb.add(domain, { zoneName: zoneName, provider: provider, config: sanitizedConfig, tlsConfig: tlsConfig }, function (error) {
domaindb.add(domain, { zoneName, provider, config: sanitizedConfig, tlsConfig, dkimSelector }, function (error) {
if (error) return callback(error);
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
@@ -204,6 +208,8 @@ function add(domain, data, auditSource, callback) {
eventlog.add(eventlog.ACTION_DOMAIN_ADD, auditSource, { domain, zoneName, provider });
mail.onDomainAdded(domain, NOOP_CALLBACK);
callback();
});
});
@@ -313,6 +319,8 @@ function del(domain, auditSource, callback) {
eventlog.add(eventlog.ACTION_DOMAIN_REMOVE, auditSource, { domain });
mail.onDomainRemoved(domain, NOOP_CALLBACK);
return callback(null);
});
}
@@ -436,6 +444,9 @@ function waitForDnsRecord(location, domain, type, value, options, callback) {
get(domain, function (error, domainObject) {
if (error) return callback(error);
// linode DNS takes ~15mins
if (!options.interval) options.interval = domainObject.provider === 'linode' ? 20000 : 5000;
api(domainObject.provider).wait(domainObject, location, type, value, options, callback);
});
}
+4 -1
View File
@@ -7,7 +7,7 @@ exports = module.exports = {
getByCreationTime: getByCreationTime,
cleanup: cleanup,
// keep in sync with webadmin index.js filter and CLI tool
// keep in sync with webadmin index.js filter
ACTION_ACTIVATE: 'cloudron.activate',
ACTION_APP_CLONE: 'app.clone',
ACTION_APP_CONFIGURE: 'app.configure',
@@ -21,6 +21,9 @@ exports = module.exports = {
ACTION_APP_OOM: 'app.oom',
ACTION_APP_UP: 'app.up',
ACTION_APP_DOWN: 'app.down',
ACTION_APP_START: 'app.start',
ACTION_APP_STOP: 'app.stop',
ACTION_APP_RESTART: 'app.restart',
ACTION_BACKUP_FINISH: 'backup.finish',
ACTION_BACKUP_START: 'backup.start',
+325 -65
View File
@@ -20,7 +20,9 @@ var assert = require('assert'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
debug = require('debug')('box:externalldap'),
groups = require('./groups.js'),
ldap = require('ldapjs'),
once = require('once'),
settings = require('./settings.js'),
tasks = require('./tasks.js'),
users = require('./users.js');
@@ -40,14 +42,14 @@ function translateUser(ldapConfig, ldapUser) {
return {
username: ldapUser[ldapConfig.usernameField],
email: ldapUser.mail,
email: ldapUser.mail || ldapUser.mailPrimaryAddress,
displayName: ldapUser.cn // user.giveName + ' ' + user.sn
};
}
function validUserRequirements(user) {
if (!user.username || !user.email || !user.displayName) {
debug(`[LDAP user empty username/email/displayName] username=${user.username} email=${user.email} displayName=${user.displayName}`);
debug(`[Invalid LDAP user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
return false;
} else {
return true;
@@ -55,40 +57,92 @@ function validUserRequirements(user) {
}
// performs service bind if required
function getClient(externalLdapConfig, callback) {
function getClient(externalLdapConfig, doBindAuth, callback) {
assert.strictEqual(typeof externalLdapConfig, 'object');
assert.strictEqual(typeof doBindAuth, 'boolean');
assert.strictEqual(typeof callback, 'function');
// ensure we only callback once since we also have to listen to client.error events
callback = once(callback);
// basic validation to not crash
try { ldap.parseDN(externalLdapConfig.baseDn); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid baseDn')); }
try { ldap.parseFilter(externalLdapConfig.filter); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid filter')); }
var config = {
url: externalLdapConfig.url,
tlsOptions: {
rejectUnauthorized: externalLdapConfig.acceptSelfSignedCerts ? false : true
}
};
var client;
try {
client = ldap.createClient({ url: externalLdapConfig.url });
client = ldap.createClient(config);
} catch (e) {
if (e instanceof ldap.ProtocolError) return callback(new BoxError(BoxError.BAD_FIELD, 'url protocol is invalid'));
return callback(new BoxError(BoxError.INTERNAL_ERROR, e));
}
if (!externalLdapConfig.bindDn) return callback(null, client);
// ensure we don't just crash
client.on('error', function (error) {
callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
});
// skip bind auth if none exist or if not wanted
if (!externalLdapConfig.bindDn || !doBindAuth) return callback(null, client);
client.bind(externalLdapConfig.bindDn, externalLdapConfig.bindPassword, function (error) {
if (error instanceof ldap.InvalidCredentialsError) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
callback(null, client, externalLdapConfig);
callback(null, client);
});
}
function ldapGetByDN(externalLdapConfig, dn, callback) {
assert.strictEqual(typeof externalLdapConfig, 'object');
assert.strictEqual(typeof dn, 'string');
assert.strictEqual(typeof callback, 'function');
getClient(externalLdapConfig, true, function (error, client) {
if (error) return callback(error);
let searchOptions = {
paged: true,
scope: 'sub' // We may have to make this configurable
};
debug(`Get object at ${dn}`);
client.search(dn, searchOptions, function (error, result) {
if (error instanceof ldap.NoSuchObjectError) return callback(new BoxError(BoxError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
let ldapObjects = [];
result.on('searchEntry', entry => ldapObjects.push(entry.object));
result.on('error', error => callback(new BoxError(BoxError.EXTERNAL_ERROR, error)));
result.on('end', function (result) {
client.unbind();
if (result.status !== 0) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Server returned status ' + result.status));
if (ldapObjects.length === 0) return callback(new BoxError(BoxError.NOT_FOUND));
callback(null, ldapObjects[0]);
});
});
});
}
// TODO support search by email
function ldapSearch(externalLdapConfig, options, callback) {
function ldapUserSearch(externalLdapConfig, options, callback) {
assert.strictEqual(typeof externalLdapConfig, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
getClient(externalLdapConfig, function (error, client) {
getClient(externalLdapConfig, true, function (error, client) {
if (error) return callback(error);
let searchOptions = {
@@ -124,6 +178,48 @@ function ldapSearch(externalLdapConfig, options, callback) {
});
}
function ldapGroupSearch(externalLdapConfig, options, callback) {
assert.strictEqual(typeof externalLdapConfig, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
getClient(externalLdapConfig, true, function (error, client) {
if (error) return callback(error);
let searchOptions = {
paged: true,
scope: 'sub' // We may have to make this configurable
};
if (externalLdapConfig.groupFilter) searchOptions.filter = ldap.parseFilter(externalLdapConfig.groupFilter);
if (options.filter) { // https://github.com/ldapjs/node-ldapjs/blob/master/docs/filters.md
let extraFilter = ldap.parseFilter(options.filter);
searchOptions.filter = new ldap.AndFilter({ filters: [ extraFilter, searchOptions.filter ] });
}
debug(`Listing groups at ${externalLdapConfig.groupBaseDn} with filter ${searchOptions.filter.toString()}`);
client.search(externalLdapConfig.groupBaseDn, searchOptions, function (error, result) {
if (error instanceof ldap.NoSuchObjectError) return callback(new BoxError(BoxError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
let ldapGroups = [];
result.on('searchEntry', entry => ldapGroups.push(entry.object));
result.on('error', error => callback(new BoxError(BoxError.EXTERNAL_ERROR, error)));
result.on('end', function (result) {
client.unbind();
if (result.status !== 0) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Server returned status ' + result.status));
callback(null, ldapGroups);
});
});
});
}
function testConfig(config, callback) {
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -141,7 +237,20 @@ function testConfig(config, callback) {
if (!config.filter) return callback(new BoxError(BoxError.BAD_FIELD, 'filter must not be empty'));
try { ldap.parseFilter(config.filter); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid filter')); }
getClient(config, function (error, client) {
if ('syncGroups' in config && typeof config.syncGroups !== 'boolean') return callback(new BoxError(BoxError.BAD_FIELD, 'syncGroups must be a boolean'));
if ('acceptSelfSignedCerts' in config && typeof config.acceptSelfSignedCerts !== 'boolean') return callback(new BoxError(BoxError.BAD_FIELD, 'acceptSelfSignedCerts must be a boolean'));
if (config.syncGroups) {
if (!config.groupBaseDn) return callback(new BoxError(BoxError.BAD_FIELD, 'groupBaseDn must not be empty'));
try { ldap.parseDN(config.groupBaseDn); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid groupBaseDn')); }
if (!config.groupFilter) return callback(new BoxError(BoxError.BAD_FIELD, 'groupFilter must not be empty'));
try { ldap.parseFilter(config.groupFilter); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid groupFilter')); }
if (!config.groupnameField || typeof config.groupnameField !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'groupFilter must not be empty'));
}
getClient(config, true, function (error, client) {
if (error) return callback(error);
var opts = {
@@ -167,7 +276,7 @@ function search(identifier, callback) {
if (error) return callback(error);
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
ldapSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${identifier}` }, function (error, ldapUsers) {
ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${identifier}` }, function (error, ldapUsers) {
if (error) return callback(error);
// translate ldap properties to ours
@@ -188,7 +297,7 @@ function createAndVerifyUserIfNotExist(identifier, password, callback) {
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
if (!externalLdapConfig.autoCreate) return callback(new BoxError(BoxError.BAD_STATE, 'auto create not enabled'));
ldapSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${identifier}` }, function (error, ldapUsers) {
ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${identifier}` }, function (error, ldapUsers) {
if (error) return callback(error);
if (ldapUsers.length === 0) return callback(new BoxError(BoxError.NOT_FOUND));
if (ldapUsers.length > 1) return callback(new BoxError(BoxError.CONFLICT));
@@ -220,17 +329,20 @@ function verifyPassword(user, password, callback) {
if (error) return callback(error);
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
ldapSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${user.username}` }, function (error, ldapUsers) {
ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${user.username}` }, function (error, ldapUsers) {
if (error) return callback(error);
if (ldapUsers.length === 0) return callback(new BoxError(BoxError.NOT_FOUND));
if (ldapUsers.length > 1) return callback(new BoxError(BoxError.CONFLICT));
let client = ldap.createClient({ url: externalLdapConfig.url });
client.bind(ldapUsers[0].dn, password, function (error) {
if (error instanceof ldap.InvalidCredentialsError) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
getClient(externalLdapConfig, false, function (error, client) {
if (error) return callback(error);
callback(null, translateUser(externalLdapConfig, ldapUsers[0]));
client.bind(ldapUsers[0].dn, password, function (error) {
if (error instanceof ldap.InvalidCredentialsError) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
callback(null, translateUser(externalLdapConfig, ldapUsers[0]));
});
});
});
});
@@ -255,6 +367,194 @@ function startSyncer(callback) {
});
}
function syncUsers(externalLdapConfig, progressCallback, callback) {
assert.strictEqual(typeof externalLdapConfig, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
ldapUserSearch(externalLdapConfig, {}, function (error, ldapUsers) {
if (error) return callback(error);
debug(`Found ${ldapUsers.length} users`);
let percent = 10;
let step = 30/(ldapUsers.length+1); // ensure no divide by 0
// we ignore all errors here and just log them for now
async.eachSeries(ldapUsers, function (user, iteratorCallback) {
user = translateUser(externalLdapConfig, user);
if (!validUserRequirements(user)) return iteratorCallback();
percent += step;
progressCallback({ percent, message: `Syncing... ${user.username}` });
users.getByUsername(user.username, function (error, result) {
if (error && error.reason !== BoxError.NOT_FOUND) return iteratorCallback(error);
if (!result) {
debug(`[adding user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
users.create(user.username, null /* password */, user.email, user.displayName, { source: 'ldap' }, auditSource.EXTERNAL_LDAP_TASK, function (error) {
if (error) console.error('Failed to create user', user, error.message);
iteratorCallback();
});
} else if (result.source !== 'ldap') {
debug(`[conflicting user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
iteratorCallback();
} else if (result.email !== user.email || result.displayName !== user.displayName) {
debug(`[updating user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
users.update(result, { email: user.email, fallbackEmail: user.email, displayName: user.displayName }, auditSource.EXTERNAL_LDAP_TASK, function (error) {
if (error) debug('Failed to update user', user, error);
iteratorCallback();
});
} else {
// user known and up-to-date
debug(`[up-to-date user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
iteratorCallback();
}
});
}, callback);
});
}
function syncGroups(externalLdapConfig, progressCallback, callback) {
assert.strictEqual(typeof externalLdapConfig, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
if (!externalLdapConfig.syncGroups) {
debug('Group sync is disabled');
progressCallback({ percent: 70, message: 'Skipping group sync...' });
return callback(null, []);
}
ldapGroupSearch(externalLdapConfig, {}, function (error, ldapGroups) {
if (error) return callback(error);
debug(`Found ${ldapGroups.length} groups`);
let percent = 40;
let step = 30/(ldapGroups.length+1); // ensure no divide by 0
// we ignore all non internal errors here and just log them for now
async.eachSeries(ldapGroups, function (ldapGroup, iteratorCallback) {
var groupName = ldapGroup[externalLdapConfig.groupnameField];
if (!groupName) return iteratorCallback();
// some servers return empty array for unknown properties :-/
if (typeof groupName !== 'string') return iteratorCallback();
// groups are lowercase
groupName = groupName.toLowerCase();
percent += step;
progressCallback({ percent, message: `Syncing... ${groupName}` });
groups.getByName(groupName, function (error, result) {
if (error && error.reason !== BoxError.NOT_FOUND) return iteratorCallback(error);
if (!result) {
debug(`[adding group] groupname=${groupName}`);
groups.create(groupName, 'ldap', function (error) {
if (error) console.error('Failed to create group', groupName, error);
iteratorCallback();
});
} else {
debug(`[up-to-date group] groupname=${groupName}`);
iteratorCallback();
}
});
}, function (error) {
if (error) return callback(error);
debug('sync: ldap sync is done', error);
callback(error);
});
});
}
function syncGroupUsers(externalLdapConfig, progressCallback, callback) {
assert.strictEqual(typeof externalLdapConfig, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
if (!externalLdapConfig.syncGroups) {
debug('Group users sync is disabled');
progressCallback({ percent: 99, message: 'Skipping group users sync...' });
return callback(null, []);
}
groups.getAll(function (error, result) {
if (error) return callback(error);
var ldapGroups = result.filter(function (g) { return g.source === 'ldap'; });
debug(`Found ${ldapGroups.length} groups to sync users`);
async.eachSeries(ldapGroups, function (group, iteratorCallback) {
debug(`Sync users for group ${group.name}`);
ldapGroupSearch(externalLdapConfig, {}, function (error, result) {
if (error) return callback(error);
if (!result || result.length === 0) {
console.error(`Unable to find group ${group.name} ignoring for now.`);
return callback();
}
// since our group names are lowercase we cannot use potentially case matching ldap filters
let found = result.find(function (r) {
if (!r[externalLdapConfig.groupnameField]) return false;
return r[externalLdapConfig.groupnameField].toLowerCase() === group.name;
});
if (!found) {
console.error(`Unable to find group ${group.name} ignoring for now.`);
return callback();
}
var ldapGroupMembers = found.member || found.uniqueMember || [];
debug(`Group ${group.name} has ${ldapGroupMembers.length} members.`);
async.eachSeries(ldapGroupMembers, function (memberDn, iteratorCallback) {
ldapGetByDN(externalLdapConfig, memberDn, function (error, result) {
if (error) {
console.error(`Failed to get ${memberDn}:`, error);
return iteratorCallback();
}
debug(`Found member object at ${memberDn} adding to group ${group.name}`);
const username = result[externalLdapConfig.usernameField];
if (!username) return iteratorCallback();
users.getByUsername(username, function (error, result) {
if (error) {
console.error(`Failed to get user by username ${username}`, error);
return iteratorCallback();
}
groups.addMember(group.id, result.id, function (error) {
if (error && error.reason !== BoxError.ALREADY_EXISTS) console.error('Failed to add member', error);
iteratorCallback();
});
});
});
}, function (error) {
if (error) console.error(error);
iteratorCallback();
});
});
}, callback);
});
}
function sync(progressCallback, callback) {
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
@@ -265,58 +565,18 @@ function sync(progressCallback, callback) {
if (error) return callback(error);
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
ldapSearch(externalLdapConfig, {}, function (error, ldapUsers) {
async.series([
syncUsers.bind(null, externalLdapConfig, progressCallback),
syncGroups.bind(null, externalLdapConfig, progressCallback),
syncGroupUsers.bind(null, externalLdapConfig, progressCallback)
], function (error) {
if (error) return callback(error);
debug(`Found ${ldapUsers.length} users`);
let percent = 10;
let step = 90/(ldapUsers.length+1); // ensure no divide by 0
progressCallback({ percent: 100, message: 'Done' });
// we ignore all errors here and just log them for now
async.eachSeries(ldapUsers, function (user, iteratorCallback) {
user = translateUser(externalLdapConfig, user);
debug('sync: ldap sync is done', error);
if (!validUserRequirements(user)) return iteratorCallback();
percent += step;
progressCallback({ percent, message: `Syncing... ${user.username}` });
users.getByUsername(user.username, function (error, result) {
if (error && error.reason !== BoxError.NOT_FOUND) {
debug(`Could not find user with username ${user.username}: ${error.message}`);
return iteratorCallback();
}
if (error) {
debug(`[adding user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
users.create(user.username, null /* password */, user.email, user.displayName, { source: 'ldap' }, auditSource.EXTERNAL_LDAP_TASK, function (error) {
if (error) console.error('Failed to create user', user, error);
iteratorCallback();
});
} else if (result.source !== 'ldap') {
debug(`[conflicting user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
iteratorCallback();
} else if (result.email !== user.email || result.displayName !== user.displayName) {
debug(`[updating user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
users.update(result, { email: user.email, fallbackEmail: user.email, displayName: user.displayName }, auditSource.EXTERNAL_LDAP_TASK, function (error) {
if (error) debug('Failed to update user', user, error);
iteratorCallback();
});
} else {
// user known and up-to-date
debug(`[up-to-date user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
iteratorCallback();
}
});
}, function (error) {
debug('sync: ldap sync is done', error);
callback(error);
});
callback(error);
});
});
}
+1 -1
View File
@@ -26,7 +26,7 @@ function startGraphite(existingInfra, callback) {
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=graphite \
-m 75m \
-m 150m \
--memory-swap 150m \
--dns 172.18.0.1 \
--dns-search=. \
+18 -18
View File
@@ -2,6 +2,7 @@
exports = module.exports = {
get: get,
getByName: getByName,
getWithMembers: getWithMembers,
getAll: getAll,
getAllWithMembers: getAllWithMembers,
@@ -19,8 +20,6 @@ exports = module.exports = {
getMembership: getMembership,
setMembership: setMembership,
getGroups: getGroups,
_clear: clear
};
@@ -28,7 +27,7 @@ var assert = require('assert'),
BoxError = require('./boxerror.js'),
database = require('./database.js');
var GROUPS_FIELDS = [ 'id', 'name' ].join(',');
var GROUPS_FIELDS = [ 'id', 'name', 'source' ].join(',');
function get(groupId, callback) {
assert.strictEqual(typeof groupId, 'string');
@@ -42,6 +41,18 @@ function get(groupId, callback) {
});
}
function getByName(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ' FROM userGroups WHERE name = ?', [ name ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Group not found'));
callback(null, result[0]);
});
}
function getWithMembers(groupId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -63,7 +74,7 @@ function getWithMembers(groupId, callback) {
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ' FROM userGroups', function (error, results) {
database.query('SELECT ' + GROUPS_FIELDS + ' FROM userGroups ORDER BY name', function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, results);
@@ -83,12 +94,13 @@ function getAllWithMembers(callback) {
});
}
function add(id, name, callback) {
function add(id, name, source, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof source, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO userGroups (id, name) VALUES (?, ?)', [ id, name ], function (error, result) {
database.query('INSERT INTO userGroups (id, name, source) VALUES (?, ?, ?)', [ id, name, source ], function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, error));
if (error || result.affectedRows !== 1) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
@@ -260,15 +272,3 @@ function isMember(groupId, userId, callback) {
callback(null, result.length !== 0);
});
}
function getGroups(userId, callback) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ' ' +
' FROM userGroups INNER JOIN groupMembers ON userGroups.id = groupMembers.groupId AND groupMembers.userId = ?', [ userId ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, results);
});
}
+26 -15
View File
@@ -4,6 +4,7 @@ exports = module.exports = {
create: create,
remove: remove,
get: get,
getByName: getByName,
update: update,
getWithMembers: getWithMembers,
getAll: getAll,
@@ -15,8 +16,6 @@ exports = module.exports = {
removeMember: removeMember,
isMember: isMember,
getGroups: getGroups,
setMembership: setMembership,
getMembership: getMembership,
@@ -45,8 +44,17 @@ function validateGroupname(name) {
return null;
}
function create(name, callback) {
function validateGroupSource(source) {
assert.strictEqual(typeof source, 'string');
if (source !== '' && source !== 'ldap') return new BoxError(BoxError.BAD_FIELD, 'source must be "" or "ldap"', { field: source });
return null;
}
function create(name, source, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof source, 'string');
assert.strictEqual(typeof callback, 'function');
// we store names in lowercase
@@ -55,8 +63,11 @@ function create(name, callback) {
var error = validateGroupname(name);
if (error) return callback(error);
error = validateGroupSource(source);
if (error) return callback(error);
var id = 'gid-' + uuid.v4();
groupdb.add(id, name, function (error) {
groupdb.add(id, name, source, function (error) {
if (error) return callback(error);
callback(null, { id: id, name: name });
@@ -85,6 +96,17 @@ function get(id, callback) {
});
}
function getByName(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
groupdb.getByName(name, function (error, result) {
if (error) return callback(error);
return callback(null, result);
});
}
function getWithMembers(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -217,17 +239,6 @@ function update(groupId, data, callback) {
});
}
function getGroups(userId, callback) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
groupdb.getGroups(userId, function (error, results) {
if (error) return callback(error);
callback(null, results);
});
}
function count(callback) {
assert.strictEqual(typeof callback, 'function');
+9 -8
View File
@@ -9,18 +9,19 @@ exports = module.exports = {
'version': '48.17.0',
'baseImages': [
{ repo: 'cloudron/base', tag: 'cloudron/base:1.0.0@sha256:147a648a068a2e746644746bbfb42eb7a50d682437cead3c67c933c546357617' }
{ repo: 'cloudron/base', tag: 'cloudron/base:2.0.0@sha256:f9fea80513aa7c92fe2e7bf3978b54c8ac5222f47a9a32a7f8833edf0eb5a4f4' }
],
// a major version bump in the db containers will trigger the restore logic that uses the db dumps
// docker inspect --format='{{index .RepoDigests 0}}' $IMAGE to get the sha256
'images': {
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.1.0@sha256:eee0dfd3829d563f2063084bc0d7c8802c4bdd6e233159c6226a17ff7a9a3503' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.0.2@sha256:6dcee0731dfb9b013ed94d56205eee219040ee806c7e251db3b3886eaa4947ff' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.1.0@sha256:6d1bf221cfe6124957e2c58b57c0a47214353496009296acb16adf56df1da9d5' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.1.0@sha256:f2cda21bd15c21bbf44432df412525369ef831a2d53860b5c5b1675e6f384de2' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.6.1@sha256:6ff6028c050d0ca34f6dd17395f18c820281ef262e77b01b82d5a74d5b8f6842' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.2.0@sha256:fc9ca69d16e6ebdbd98ed53143d4a0d2212eef60cb638dc71219234e6f427a2c' },
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:0.1.0@sha256:e177c5bf5f38c84ce1dea35649c22a1b05f96eec67a54a812c5a35e585670f0f' }
'turn': { repo: 'cloudron/turn', tag: 'cloudron/turn:1.1.0@sha256:e1dd22aa6eef5beb7339834b200a8bb787ffc2264ce11139857a054108fefb4f' },
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.3.1@sha256:c1145d43c8a912fe6f5a5629a4052454a4aa6f23391c1efbffeec9d12d72a256' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.3.0@sha256:4112cc31a09b465bdfbd715fedab4bc86898246b4615d789dfb1d2cb728e3872' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.2.0@sha256:205486ff0f6bf6854610572df401cf3651bc62baf28fd26e9c5632497f10c2cb' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.3.0@sha256:0e31ec817e235b1814c04af97b1e7cf0053384aca2569570ce92bef0d95e94d2' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.9.4@sha256:0e169b97a0584a76197d2bbc039d8698bf93f815588b3b43c251bd83dd545465' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.3.0@sha256:b7bc1ca4f4d0603a01369a689129aa273a938ce195fe43d00d42f4f2d5212f50' },
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:2.0.0@sha256:63cba3c70c91d440e1e436417ab33f655aa75c7bf936c0a5f4447e71e4ffbe4f' }
}
};
+12 -12
View File
@@ -154,7 +154,6 @@ function userSearch(req, res, next) {
givenName: firstName,
username: user.username,
samaccountname: user.username, // to support ActiveDirectory clients
isadmin: users.compareRoles(user.role, users.ROLE_ADMIN) >= 0,
memberof: groups
}
};
@@ -347,7 +346,7 @@ function mailboxSearch(req, res, next) {
if (error) return callback(error);
aliases.forEach(function (a, idx) {
obj.attributes['mail' + idx] = `${a}@${mailbox.domain}`;
obj.attributes['mail' + idx] = `${a.name}@${a.domain}`;
});
// ensure all filter values are also lowercase
@@ -392,7 +391,7 @@ function mailAliasSearch(req, res, next) {
objectclass: ['nisMailAlias'],
objectcategory: 'nisMailAlias',
cn: `${alias.name}@${alias.domain}`,
rfc822MailMember: `${alias.aliasTarget}@${alias.domain}`
rfc822MailMember: `${alias.aliasName}@${alias.aliasDomain}`
}
};
@@ -418,7 +417,7 @@ function mailingListSearch(req, res, next) {
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
const name = parts[0], domain = parts[1];
mail.resolveList(parts[0], parts[1], function (error, resolvedMembers) {
mail.resolveList(parts[0], parts[1], function (error, resolvedMembers, list) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.toString()));
@@ -431,6 +430,7 @@ function mailingListSearch(req, res, next) {
objectcategory: 'mailGroup',
cn: `${name}@${domain}`, // fully qualified
mail: `${name}@${domain}`,
membersOnly: list.membersOnly, // ldapjs only supports strings and string array. so this is not a bool!
mgrpRFC822MailMember: resolvedMembers // fully qualified
}
};
@@ -534,13 +534,16 @@ function authenticateSftp(req, res, next) {
var parts = email.split('@');
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
// actual user bind
users.verifyWithUsername(parts[0], req.credentials, users.AP_SFTP, function (error) {
apps.getByFqdn(parts[1], function (error, app) {
if (error) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
debug('sftp auth: success');
users.verifyWithUsername(parts[0], req.credentials, app.id, function (error) {
if (error) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
res.end();
debug('sftp auth: success');
res.end();
});
});
}
@@ -615,10 +618,7 @@ function authenticateMailAddon(req, res, next) {
// note: with sendmail addon, apps can send mail without a mailbox (unlike users)
appdb.getAppIdByAddonConfigValue(addonId, namePattern, req.credentials || '', function (error, appId) {
if (error && error.reason !== BoxError.NOT_FOUND) return next(new ldap.OperationsError(error.message));
if (appId) { // matched app password
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: email }, { appId: appId, addonId: addonId });
return res.end();
}
if (appId) return res.end();
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
+2
View File
@@ -8,6 +8,7 @@
maxsize 1M
missingok
delaycompress
# this truncates the original log file and not the rotated one
copytruncate
}
@@ -18,6 +19,7 @@
missingok
# we never compress so we can simply tail the files
nocompress
# this truncates the original log file and not the rotated one
copytruncate
}
+91 -95
View File
@@ -1,52 +1,53 @@
'use strict';
exports = module.exports = {
getStatus: getStatus,
checkConfiguration: checkConfiguration,
getStatus,
checkConfiguration,
getDomains: getDomains,
getDomains,
getDomain: getDomain,
addDomain: addDomain,
removeDomain: removeDomain,
clearDomains: clearDomains,
getDomain,
clearDomains,
removePrivateFields: removePrivateFields,
onDomainAdded,
onDomainRemoved,
onMailFqdnChanged,
setDnsRecords: setDnsRecords,
onMailFqdnChanged: onMailFqdnChanged,
removePrivateFields,
validateName: validateName,
setDnsRecords,
setMailFromValidation: setMailFromValidation,
setCatchAllAddress: setCatchAllAddress,
setMailRelay: setMailRelay,
setMailEnabled: setMailEnabled,
validateName,
setMailFromValidation,
setCatchAllAddress,
setMailRelay,
setMailEnabled,
startMail: restartMail,
restartMail: restartMail,
handleCertChanged: handleCertChanged,
getMailAuth: getMailAuth,
restartMail,
handleCertChanged,
getMailAuth,
sendTestMail: sendTestMail,
sendTestMail,
listMailboxes: listMailboxes,
removeMailboxes: removeMailboxes,
getMailbox: getMailbox,
addMailbox: addMailbox,
updateMailboxOwner: updateMailboxOwner,
removeMailbox: removeMailbox,
getMailboxCount,
listMailboxes,
removeMailboxes,
getMailbox,
addMailbox,
updateMailboxOwner,
removeMailbox,
listAliases: listAliases,
getAliases: getAliases,
setAliases: setAliases,
getAliases,
setAliases,
getLists: getLists,
getList: getList,
addList: addList,
updateList: updateList,
removeList: removeList,
resolveList: resolveList,
getLists,
getList,
addList,
updateList,
removeList,
resolveList,
_readDkimPublicKeySync: readDkimPublicKeySync
};
@@ -101,7 +102,6 @@ function checkOutboundPort25(callback) {
'smtp.gmail.com',
'smtp.live.com',
'smtp.mail.yahoo.com',
'smtp.comcast.net',
'smtp.1und1.de',
]);
@@ -208,7 +208,8 @@ function checkDkim(mailDomain, callback) {
if (txtRecords.length !== 0) {
dkim.value = txtRecords[0].join('');
dkim.status = (dkim.value === dkim.expected);
const actual = txtToDict(dkim.value);
dkim.status = actual.p === dkimKey;
}
callback(null, dkim);
@@ -269,7 +270,7 @@ function checkMx(domain, mailFqdn, callback) {
if (error) return callback(error, mx);
if (mxRecords.length === 0) return callback(null, mx);
mx.status = mxRecords.length == 1 && mxRecords[0].exchange === mailFqdn;
mx.status = mxRecords.some(mx => mx.exchange === mailFqdn); // this lets use change priority and/or setup backup MX
mx.value = mxRecords.map(function (r) { return r.priority + ' ' + r.exchange + '.'; }).join(' ');
if (mx.status) return callback(null, mx); // MX record is "my."
@@ -313,9 +314,8 @@ function checkDmarc(domain, callback) {
if (txtRecords.length !== 0) {
dmarc.value = txtRecords[0].join('');
// allow extra fields in dmarc like rua
const actual = txtToDict(dmarc.value), expected = txtToDict(dmarc.expected);
dmarc.status = Object.keys(expected).every(k => expected[k] === actual[k]);
const actual = txtToDict(dmarc.value);
dmarc.status = actual.v === 'DMARC1'; // see box#666
}
callback(null, dmarc);
@@ -799,6 +799,7 @@ function ensureDkimKeySync(mailDomain) {
return new BoxError(BoxError.FS_ERROR, safe.error);
}
// https://www.unlocktheinbox.com/dkim-key-length-statistics/ and https://docs.aws.amazon.com/ses/latest/DeveloperGuide/send-email-authentication-dkim-easy.html for key size
if (!safe.child_process.execSync('openssl genrsa -out ' + dkimPrivateKeyFile + ' 1024')) return new BoxError(BoxError.OPENSSL_ERROR, safe.error);
if (!safe.child_process.execSync('openssl rsa -in ' + dkimPrivateKeyFile + ' -out ' + dkimPublicKeyFile + ' -pubout -outform PEM')) return new BoxError(BoxError.OPENSSL_ERROR, safe.error);
@@ -906,37 +907,21 @@ function onMailFqdnChanged(callback) {
});
}
function addDomain(domain, callback) {
function onDomainAdded(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
const dkimSelector = domain === settings.adminDomain() ? 'cloudron' : ('cloudron-' + settings.adminDomain().replace(/\./g, ''));
maildb.add(domain, { dkimSelector }, function (error) {
if (error) return callback(error);
async.series([
upsertDnsRecords.bind(null, domain, settings.mailFqdn()), // do this first to ensure DKIM keys
restartMailIfActivated
], NOOP_CALLBACK); // do these asynchronously
callback();
});
async.series([
upsertDnsRecords.bind(null, domain, settings.mailFqdn()), // do this first to ensure DKIM keys
restartMailIfActivated
], callback);
}
function removeDomain(domain, callback) {
function onDomainRemoved(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
if (domain === settings.adminDomain()) return callback(new BoxError(BoxError.CONFLICT));
maildb.del(domain, function (error) {
if (error) return callback(error);
restartMail(NOOP_CALLBACK);
callback();
});
restartMail(callback);
}
function clearDomains(callback) {
@@ -1048,13 +1033,25 @@ function sendTestMail(domain, to, callback) {
});
}
function listMailboxes(domain, page, perPage, callback) {
function listMailboxes(domain, search, page, perPage, callback) {
assert.strictEqual(typeof domain, 'string');
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
mailboxdb.listMailboxes(domain, page, perPage, function (error, result) {
mailboxdb.listMailboxes(domain, search, page, perPage, function (error, result) {
if (error) return callback(error);
callback(null, result);
});
}
function getMailboxCount(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.getMailboxCount(domain, function (error, result) {
if (error) return callback(error);
callback(null, result);
@@ -1142,19 +1139,6 @@ function removeMailbox(name, domain, auditSource, callback) {
});
}
function listAliases(domain, page, perPage, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
mailboxdb.listAliases(domain, page, perPage, function (error, result) {
if (error) return callback(error);
callback(null, result);
});
}
function getAliases(name, domain, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
@@ -1178,12 +1162,15 @@ function setAliases(name, domain, aliases, callback) {
assert.strictEqual(typeof callback, 'function');
for (var i = 0; i < aliases.length; i++) {
aliases[i] = aliases[i].toLowerCase();
let name = aliases[i].name.toLowerCase();
let domain = aliases[i].domain.toLowerCase();
var error = validateName(aliases[i]);
let error = validateName(name);
if (error) return callback(error);
}
if (!validator.isEmail(`${name}@${domain}`)) return callback(new BoxError(BoxError.BAD_FIELD, `Invalid email: ${name}@${domain}`));
aliases[i] = { name, domain };
}
mailboxdb.setAliasesForName(name, domain, aliases, function (error) {
if (error) return callback(error);
@@ -1191,11 +1178,14 @@ function setAliases(name, domain, aliases, callback) {
});
}
function getLists(domain, callback) {
function getLists(domain, search, page, perPage, callback) {
assert.strictEqual(typeof domain, 'string');
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
mailboxdb.getLists(domain, function (error, result) {
mailboxdb.getLists(domain, search, page, perPage, function (error, result) {
if (error) return callback(error);
callback(null, result);
@@ -1214,10 +1204,11 @@ function getList(name, domain, callback) {
});
}
function addList(name, domain, members, auditSource, callback) {
function addList(name, domain, members, membersOnly, auditSource, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof name, 'string');
assert(Array.isArray(members));
assert.strictEqual(typeof membersOnly, 'boolean');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -1230,19 +1221,20 @@ function addList(name, domain, members, auditSource, callback) {
if (!validator.isEmail(members[i])) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid mail member: ' + members[i]));
}
mailboxdb.addList(name, domain, members, function (error) {
mailboxdb.addList(name, domain, members, membersOnly, function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_MAIL_LIST_ADD, auditSource, { name, domain, members });
eventlog.add(eventlog.ACTION_MAIL_LIST_ADD, auditSource, { name, domain, members, membersOnly });
callback();
});
}
function updateList(name, domain, members, auditSource, callback) {
function updateList(name, domain, members, membersOnly, auditSource, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(members));
assert.strictEqual(typeof membersOnly, 'boolean');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -1258,10 +1250,10 @@ function updateList(name, domain, members, auditSource, callback) {
getList(name, domain, function (error, result) {
if (error) return callback(error);
mailboxdb.updateList(name, domain, members, function (error) {
mailboxdb.updateList(name, domain, members, membersOnly, function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, oldMembers: result.members, members });
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, oldMembers: result.members, members, membersOnly });
callback(null);
});
@@ -1283,6 +1275,7 @@ function removeList(name, domain, auditSource, callback) {
});
}
// resolves the members of a list. i.e the lists and aliases
function resolveList(listName, listDomain, callback) {
assert.strictEqual(typeof listName, 'string');
assert.strictEqual(typeof listDomain, 'string');
@@ -1313,18 +1306,21 @@ function resolveList(listName, listDomain, callback) {
visited.push(member);
mailboxdb.get(memberName, memberDomain, function (error, entry) {
if (error && error.reason == BoxError.NOT_FOUND) { result.push(member); return iteratorCallback(); }
if (error && error.reason == BoxError.NOT_FOUND) { result.push(member); return iteratorCallback(); } // let it bounce
if (error) return iteratorCallback(error);
if (entry.type === mailboxdb.TYPE_MAILBOX) { result.push(member); return iteratorCallback(); }
// no need to resolve alias because we only allow one level and within same domain
if (entry.type === mailboxdb.TYPE_ALIAS) { result.push(`${entry.aliasTarget}@${entry.domain}`); return iteratorCallback(); }
if (entry.type === mailboxdb.TYPE_MAILBOX) { // concrete mailbox
result.push(member);
} else if (entry.type === mailboxdb.TYPE_ALIAS) { // resolve aliases
toResolve = toResolve.concat(`${entry.aliasName}@${entry.aliasTarget}`);
} else { // resolve list members
toResolve = toResolve.concat(entry.members);
}
toResolve = toResolve.concat(entry.members);
iteratorCallback();
});
}, function (error) {
callback(error, result);
callback(error, result, list);
});
});
});
+1 -1
View File
@@ -6,7 +6,7 @@ Dear <%= cloudronName %> Admin,
If this message appears repeatedly, give the app more memory.
* To increase an app's memory limit - https://cloudron.io/documentation/apps/#increasing-the-memory-limit-of-an-app
* To increase an app's memory limit - https://cloudron.io/documentation/apps/#memory-limit
* To increase a service's memory limit - https://cloudron.io/documentation/troubleshooting/#services
Out of memory event:
+5 -1
View File
@@ -8,7 +8,7 @@ be reset. If you did not request this reset, please ignore this message.
To reset your password, please visit the following page:
<%- resetLink %>
Please note that the password reset link will expire in 24 hours.
Powered by https://cloudron.io
@@ -29,6 +29,10 @@ Powered by https://cloudron.io
<a href="<%= resetLink %>">Click to reset your password</a>
</p>
<br/>
Please note that the password reset link will expire in 24 hours.
<br/>
<br/>
+4
View File
@@ -11,6 +11,7 @@ Follow the link to get started.
You are receiving this email because you were invited by <%= invitor.email %>.
<% } %>
Please note that the invite link will expire in 24 hours.
Powered by https://cloudron.io
@@ -36,6 +37,9 @@ Powered by https://cloudron.io
You are receiving this email because you were invited by <%= invitor.email %>.
<% } %>
<br/>
Please note that the invite link will expire in 24 hours.
<br/>
Powered by <a href="https://cloudron.io">Cloudron</a>
+74 -64
View File
@@ -1,32 +1,32 @@
'use strict';
exports = module.exports = {
addMailbox: addMailbox,
addList: addList,
addMailbox,
addList,
updateMailboxOwner: updateMailboxOwner,
updateList: updateList,
del: del,
updateMailboxOwner,
updateList,
del,
listAliases: listAliases,
listMailboxes: listMailboxes,
getLists: getLists,
getMailboxCount,
listMailboxes,
getLists,
listAllMailboxes: listAllMailboxes,
listAllMailboxes,
get: get,
getMailbox: getMailbox,
getList: getList,
getAlias: getAlias,
get,
getMailbox,
getList,
getAlias,
getAliasesForName: getAliasesForName,
setAliasesForName: setAliasesForName,
getAliasesForName,
setAliasesForName,
getByOwnerId: getByOwnerId,
delByOwnerId: delByOwnerId,
delByDomain: delByDomain,
getByOwnerId,
delByOwnerId,
delByDomain,
updateName: updateName,
updateName,
_clear: clear,
@@ -38,15 +38,18 @@ exports = module.exports = {
var assert = require('assert'),
BoxError = require('./boxerror.js'),
database = require('./database.js'),
mysql = require('mysql'),
safe = require('safetydance'),
util = require('util');
var MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'aliasTarget', 'creationTime', 'membersJson', 'domain' ].join(',');
var MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'aliasName', 'aliasDomain', 'creationTime', 'membersJson', 'membersOnly', 'domain' ].join(',');
function postProcess(data) {
data.members = safe.JSON.parse(data.membersJson) || [ ];
delete data.membersJson;
data.membersOnly = !!data.membersOnly;
return data;
}
@@ -78,14 +81,15 @@ function updateMailboxOwner(name, domain, ownerId, callback) {
});
}
function addList(name, domain, members, callback) {
function addList(name, domain, members, membersOnly, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(members));
assert.strictEqual(typeof membersOnly, 'boolean');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, membersJson) VALUES (?, ?, ?, ?, ?)',
[ name, exports.TYPE_LIST, domain, 'admin', JSON.stringify(members) ], function (error) {
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, membersJson, membersOnly) VALUES (?, ?, ?, ?, ?, ?)',
[ name, exports.TYPE_LIST, domain, 'admin', JSON.stringify(members), membersOnly ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, 'mailbox already exists'));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
@@ -93,14 +97,15 @@ function addList(name, domain, members, callback) {
});
}
function updateList(name, domain, members, callback) {
function updateList(name, domain, members, membersOnly, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(members));
assert.strictEqual(typeof membersOnly, 'boolean');
assert.strictEqual(typeof callback, 'function');
database.query('UPDATE mailboxes SET membersJson = ? WHERE name = ? AND domain = ?',
[ JSON.stringify(members), name, domain ], function (error, result) {
database.query('UPDATE mailboxes SET membersJson = ?, membersOnly = ? WHERE name = ? AND domain = ?',
[ JSON.stringify(members), membersOnly, name, domain ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found'));
@@ -123,7 +128,7 @@ function del(name, domain, callback) {
assert.strictEqual(typeof callback, 'function');
// deletes aliases as well
database.query('DELETE FROM mailboxes WHERE (name=? OR aliasTarget = ?) AND domain = ?', [ name, name, domain ], function (error, result) {
database.query('DELETE FROM mailboxes WHERE ((name=? AND domain=?) OR (aliasName = ? AND aliasDomain=?))', [ name, domain, name, domain ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found'));
@@ -200,20 +205,35 @@ function getMailbox(name, domain, callback) {
});
}
function listMailboxes(domain, page, perPage, callback) {
function getMailboxCount(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT COUNT(*) AS total FROM mailboxes WHERE type = ? AND domain = ?', [ exports.TYPE_MAILBOX, domain ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, results[0].total);
});
}
function listMailboxes(domain, search, page, perPage, callback) {
assert.strictEqual(typeof domain, 'string');
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? AND domain = ? ORDER BY name LIMIT ${(page-1)*perPage},${perPage}`,
[ exports.TYPE_MAILBOX, domain ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
let query = `SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? AND domain = ?`;
if (search) query += ' AND (name LIKE ' + mysql.escape('%' + search + '%') + ')';
query += 'ORDER BY name LIMIT ?,?';
results.forEach(function (result) { postProcess(result); });
database.query(query, [ exports.TYPE_MAILBOX, domain, (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, results);
});
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
}
function listAllMailboxes(page, perPage, callback) {
@@ -221,8 +241,8 @@ function listAllMailboxes(page, perPage, callback) {
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? ORDER BY name LIMIT ${(page-1)*perPage},${perPage}`,
[ exports.TYPE_MAILBOX ], function (error, results) {
database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? ORDER BY name LIMIT ?,?`,
[ exports.TYPE_MAILBOX, (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(function (result) { postProcess(result); });
@@ -231,18 +251,25 @@ function listAllMailboxes(page, perPage, callback) {
});
}
function getLists(domain, callback) {
function getLists(domain, search, page, perPage, callback) {
assert.strictEqual(typeof domain, 'string');
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE type = ? AND domain = ?',
[ exports.TYPE_LIST, domain ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
let query = `SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? AND domain = ?`;
if (search) query += ' AND (name LIKE ' + mysql.escape('%' + search + '%') + ' OR membersJson LIKE ' + mysql.escape('%' + search + '%') + ')';
results.forEach(function (result) { postProcess(result); });
query += 'ORDER BY name LIMIT ?,?';
callback(null, results);
});
database.query(query, [ exports.TYPE_LIST, domain, (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
}
function getList(name, domain, callback) {
@@ -285,10 +312,10 @@ function setAliasesForName(name, domain, aliases, callback) {
var queries = [];
// clear existing aliases
queries.push({ query: 'DELETE FROM mailboxes WHERE aliasTarget = ? AND domain = ? AND type = ?', args: [ name, domain, exports.TYPE_ALIAS ] });
queries.push({ query: 'DELETE FROM mailboxes WHERE aliasName = ? AND aliasDomain = ? AND type = ?', args: [ name, domain, exports.TYPE_ALIAS ] });
aliases.forEach(function (alias) {
queries.push({ query: 'INSERT INTO mailboxes (name, type, domain, aliasTarget, ownerId) VALUES (?, ?, ?, ?, ?)',
args: [ alias, exports.TYPE_ALIAS, domain, name, results[0].ownerId ] });
queries.push({ query: 'INSERT INTO mailboxes (name, domain, type, aliasName, aliasDomain, ownerId) VALUES (?, ?, ?, ?, ?, ?)',
args: [ alias.name, alias.domain, exports.TYPE_ALIAS, name, domain, results[0].ownerId ] });
});
database.transaction(queries, function (error) {
@@ -311,27 +338,10 @@ function getAliasesForName(name, domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT name FROM mailboxes WHERE type = ? AND aliasTarget = ? AND domain = ? ORDER BY name',
database.query('SELECT name, domain FROM mailboxes WHERE type = ? AND aliasName = ? AND aliasDomain = ? ORDER BY name',
[ exports.TYPE_ALIAS, name, domain ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results = results.map(function (r) { return r.name; });
callback(null, results);
});
}
function listAliases(domain, page, perPage, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE domain = ? AND type = ? ORDER BY name LIMIT ${(page-1)*perPage},${perPage}`,
[ domain, exports.TYPE_ALIAS ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
}
-30
View File
@@ -1,8 +1,6 @@
'use strict';
exports = module.exports = {
add: add,
del: del,
get: get,
list: list,
update: update,
@@ -34,20 +32,6 @@ function postProcess(data) {
return data;
}
function add(domain, data, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO mail (domain, dkimSelector) VALUES (?, ?)', [ domain, data.dkimSelector || 'cloudron' ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, 'mail domain already exists'));
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new BoxError(BoxError.NOT_FOUND), 'no such domain');
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -58,20 +42,6 @@ function clear(callback) {
});
}
function del(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
// deletes aliases as well
database.query('DELETE FROM mail WHERE domain=?', [ domain ], function (error, result) {
if (error && error.code === 'ER_ROW_IS_REFERENCED_2') return callback(new BoxError(BoxError.CONFLICT));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mail domain not found'));
callback(null);
});
}
function get(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
+1 -1
View File
@@ -210,7 +210,7 @@ function roleChanged(mailTo, user) {
debug('Sending mail for roleChanged');
mailUserEvent(mailTo, user, `now has the role '${user.role}`);
mailUserEvent(mailTo, user, `now has the role '${user.role}'`);
}
function passwordReset(user) {
+34 -15
View File
@@ -7,6 +7,7 @@ map $http_upgrade $connection_upgrade {
# http server
server {
listen 80;
server_tokens off; # hide version
<% if (hasIPv6) { -%>
listen [::]:80;
<% } -%>
@@ -42,33 +43,33 @@ server {
server {
<% if (vhost) { -%>
server_name <%= vhost %>;
listen 443 http2;
listen 443 ssl http2;
<% if (hasIPv6) { -%>
listen [::]:443 http2;
listen [::]:443 ssl http2;
<% } -%>
<% } else { -%>
listen 443 http2 default_server;
listen 443 ssl http2 default_server;
<% if (hasIPv6) { -%>
listen [::]:443 http2 default_server;
listen [::]:443 ssl http2 default_server;
<% } -%>
<% } -%>
ssl on;
server_tokens off; # hide version
# paths are relative to prefix and not to this file
ssl_certificate <%= certFilePath %>;
ssl_certificate_key <%= keyFilePath %>;
ssl_session_timeout 5m;
ssl_session_cache shared:SSL:50m;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
ssl_session_tickets off;
# https://bettercrypto.org/static/applied-crypto-hardening.pdf
# https://mozilla.github.io/server-side-tls/ssl-config-generator/
# https://cipherli.st/
# https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
ssl_prefer_server_ciphers on;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # don't use SSLv3 ref: POODLE
# https://github.com/ssllabs/research/wiki/SSL-and-TLS-Deployment-Best-Practices#25-use-forward-secrecy
# ciphers according to https://ssl-config.mozilla.org/#server=nginx&version=1.14.0&config=intermediate&openssl=1.1.1&guideline=5.4
ssl_protocols TLSv1.2 TLSv1.3;
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;
# ciphers according to https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=nginx-1.10.3&openssl=1.0.2g&hsts=yes&profile=modern
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
ssl_dhparam /home/yellowtent/boxdata/dhparams.pem;
add_header Strict-Transport-Security "max-age=15768000";
@@ -136,8 +137,21 @@ server {
# internal means this is for internal routing and cannot be accessed as URL from browser
internal;
}
location /appstatus.html {
internal;
location @wellknown-upstream {
<% if ( endpoint === 'admin' ) { %>
proxy_pass http://127.0.0.1:3000;
<% } else if ( endpoint === 'app' ) { %>
proxy_pass http://127.0.0.1:<%= port %>;
<% } else if ( endpoint === 'redirect' ) { %>
return 302 https://<%= redirectTo %>$request_uri;
<% } %>
}
# user defined .well-known resources
location ~ ^/.well-known/(.*)$ {
root /home/yellowtent/boxdata/well-known/$host;
try_files /$1 @wellknown-upstream;
}
location / {
@@ -181,6 +195,11 @@ server {
client_max_body_size 0;
}
location ~ ^/api/v1/apps/.*/files/ {
proxy_pass http://127.0.0.1:3000;
client_max_body_size 0;
}
# graphite paths (uncomment block below and visit /graphite-web/dashboard)
# remember to comment out the CSP policy as well to access the graphite dashboard
# location ~ ^/graphite-web/ {
+3 -3
View File
@@ -141,7 +141,7 @@ function roleChanged(performedBy, eventId, user, callback) {
actionForAllAdmins([ performedBy, user.id ], function (admin, done) {
mailer.roleChanged(admin.email, user);
add(admin.id, eventId, `User '${user.displayName}'s role changed`, `User '${user.username || user.email || user.fallbackEmail}' now has the role '${user.role}'.`, done);
add(admin.id, eventId, `User '${user.displayName}'s role changed`, `User '${user.username || user.email || user.fallbackEmail}' now has the role ${user.role}.`, done);
}, callback);
}
@@ -156,7 +156,7 @@ function oomEvent(eventId, app, addon, containerId, event, callback) {
if (app) {
program = `App ${app.fqdn}`;
title = `The application ${app.fqdn} (${app.manifest.title}) ran out of memory.`;
message = 'The application has been restarted automatically. If you see this notification often, consider increasing the [memory limit](https://cloudron.io/documentation/apps/#increasing-the-memory-limit-of-an-app)';
message = 'The application has been restarted automatically. If you see this notification often, consider increasing the [memory limit](https://cloudron.io/documentation/apps/#memory-limit)';
} else if (addon) {
program = `${addon.name} service`;
title = `The ${addon.name} service ran out of memory`;
@@ -273,7 +273,7 @@ function alert(id, title, message, callback) {
assert.strictEqual(typeof message, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`alert: id=${id} title=${title} message=${message}`);
debug(`alert: id=${id} title=${title}`);
const acknowledged = !message;
+4 -2
View File
@@ -17,12 +17,11 @@ exports = module.exports = {
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
INFRA_VERSION_FILE: path.join(baseDir(), 'platformdata/INFRA_VERSION'),
LICENSE_FILE: '/etc/cloudron/LICENSE',
PROVIDER_FILE: '/etc/cloudron/PROVIDER',
PLATFORM_DATA_DIR: path.join(baseDir(), 'platformdata'),
APPS_DATA_DIR: path.join(baseDir(), 'appsdata'),
BOX_DATA_DIR: path.join(baseDir(), 'boxdata'),
BOX_DATA_DIR: path.join(baseDir(), 'boxdata'), // box data dir is part of box backup
ACME_CHALLENGES_DIR: path.join(baseDir(), 'platformdata/acme'),
ADDON_CONFIG_DIR: path.join(baseDir(), 'platformdata/addons'),
@@ -46,11 +45,14 @@ exports = module.exports = {
APP_CERTS_DIR: path.join(baseDir(), 'boxdata/certs'),
CLOUDRON_AVATAR_FILE: path.join(baseDir(), 'boxdata/avatar.png'),
UPDATE_CHECKER_FILE: path.join(baseDir(), 'boxdata/updatechecker.json'),
ADDON_TURN_SECRET_FILE: path.join(baseDir(), 'boxdata/addon-turn-secret'),
LOG_DIR: path.join(baseDir(), 'platformdata/logs'),
TASKS_LOG_DIR: path.join(baseDir(), 'platformdata/logs/tasks'),
CRASH_LOG_DIR: path.join(baseDir(), 'platformdata/logs/crash'),
GHOST_USER_FILE: path.join(baseDir(), 'platformdata/cloudron_ghost.json'),
// this pattern is for the cloudron logs API route to work
BACKUP_LOG_FILE: path.join(baseDir(), 'platformdata/logs/backup/app.log'),
UPDATER_LOG_FILE: path.join(baseDir(), 'platformdata/logs/updater/app.log')
+18 -7
View File
@@ -133,11 +133,10 @@ function pruneInfraImages(callback) {
function stopContainers(existingInfra, callback) {
// always stop addons to restart them on any infra change, regardless of minor or major update
if (existingInfra.version !== infra.version) {
// TODO: only nuke containers with isCloudronManaged=true
debug('stopping all containers for infra upgrade');
async.series([
shell.exec.bind(null, 'stopContainers', 'docker ps -qa --filter \'network=cloudron\' | xargs --no-run-if-empty docker stop'),
shell.exec.bind(null, 'stopContainers', 'docker ps -qa --filter \'network=cloudron\' | xargs --no-run-if-empty docker rm -f')
shell.exec.bind(null, 'stopContainers', 'docker ps -qa --filter \'label=isCloudronManaged\' | xargs --no-run-if-empty docker stop'),
shell.exec.bind(null, 'stopContainers', 'docker ps -qa --filter \'label=isCloudronManaged\' | xargs --no-run-if-empty docker rm -f')
], callback);
} else {
assert(typeof infra.images, 'object');
@@ -150,8 +149,8 @@ function stopContainers(existingInfra, callback) {
let filterArg = changedAddons.map(function (c) { return `--filter 'name=${c}'`; }).join(' '); // name=c matches *c*. required for redis-{appid}
// ignore error if container not found (and fail later) so that this code works across restarts
async.series([
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} --filter 'network=cloudron' | xargs --no-run-if-empty docker stop || true`),
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} --filter 'network=cloudron' | xargs --no-run-if-empty docker rm -f || true`)
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} --filter 'label=isCloudronManaged' | xargs --no-run-if-empty docker stop || true`),
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} --filter 'label=isCloudronManaged' | xargs --no-run-if-empty docker rm -f || true`)
], callback);
}
}
@@ -165,7 +164,19 @@ function startApps(existingInfra, callback) {
reverseProxy.removeAppConfigs(); // should we change the cert location, nginx will not start
apps.configureInstalledApps(callback);
} else {
debug('startApps: apps are already uptodate');
callback();
let changedAddons = [];
if (infra.images.mysql.tag !== existingInfra.images.mysql.tag) changedAddons.push('mysql');
if (infra.images.postgresql.tag !== existingInfra.images.postgresql.tag) changedAddons.push('postgresql');
if (infra.images.mongodb.tag !== existingInfra.images.mongodb.tag) changedAddons.push('mongodb');
if (infra.images.redis.tag !== existingInfra.images.redis.tag) changedAddons.push('redis');
if (changedAddons.length) {
// restart apps if docker image changes since the IP changes and any "persistent" connections fail
debug(`startApps: changedAddons: ${JSON.stringify(changedAddons)}`);
apps.restartAppsUsingAddons(changedAddons, callback);
} else {
debug('startApps: apps are already uptodate');
callback();
}
}
}
+13 -34
View File
@@ -4,13 +4,10 @@ exports = module.exports = {
setup: setup,
restore: restore,
activate: activate,
getStatus: getStatus,
autoRegister: autoRegister
getStatus: getStatus
};
var appstore = require('./appstore.js'),
assert = require('assert'),
var assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
BoxError = require('./boxerror.js'),
@@ -19,10 +16,7 @@ var appstore = require('./appstore.js'),
debug = require('debug')('box:provision'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
mail = require('./mail.js'),
paths = require('./paths.js'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
sysinfo = require('./sysinfo.js'),
@@ -53,27 +47,6 @@ function setProgress(task, message, callback) {
callback();
}
function autoRegister(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
if (!fs.existsSync(paths.LICENSE_FILE)) return callback();
const license = safe.fs.readFileSync(paths.LICENSE_FILE, 'utf8');
if (!license) return callback(new BoxError(BoxError.LICENSE_ERROR, 'Cannot read license'));
debug('Auto-registering cloudron');
appstore.registerWithLicense(license.trim(), domain, function (error) {
if (error && error.reason !== BoxError.CONFLICT) { // not already registered
debug('Failed to auto-register cloudron', error);
return callback(new BoxError(BoxError.LICENSE_ERROR, 'Failed to auto-register Cloudron with license. Please contact support@cloudron.io'));
}
callback();
});
}
function unprovision(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -121,7 +94,8 @@ function setup(dnsConfig, sysinfoConfig, auditSource, callback) {
provider: dnsConfig.provider,
config: dnsConfig.config,
fallbackCertificate: dnsConfig.fallbackCertificate || null,
tlsConfig: dnsConfig.tlsConfig || { provider: 'letsencrypt-prod' }
tlsConfig: dnsConfig.tlsConfig || { provider: 'letsencrypt-prod' },
dkimSelector: 'cloudron'
};
domains.add(domain, data, auditSource, function (error) {
@@ -133,11 +107,9 @@ function setup(dnsConfig, sysinfoConfig, auditSource, callback) {
callback(); // now that args are validated run the task in the background
async.series([
autoRegister.bind(null, domain),
settings.setSysinfoConfig.bind(null, sysinfoConfig),
domains.prepareDashboardDomain.bind(null, domain, auditSource, (progress) => setProgress('setup', progress.message, NOOP_CALLBACK)),
cloudron.setDashboardDomain.bind(null, domain, auditSource),
mail.addDomain.bind(null, domain), // this relies on settings.mailFqdn() and settings.adminDomain()
setProgress.bind(null, 'setup', 'Done'),
eventlog.add.bind(null, eventlog.ACTION_PROVISION, auditSource, { })
], function (error) {
@@ -206,9 +178,16 @@ function restore(backupConfig, backupId, version, sysinfoConfig, auditSource, ca
if (error) return done(error);
if (activated) return done(new BoxError(BoxError.CONFLICT, 'Already activated. Restore with a fresh Cloudron installation.'));
backups.testConfig(backupConfig, function (error) {
backups.testProviderConfig(backupConfig, function (error) {
if (error) return done(error);
if ('password' in backupConfig) {
backupConfig.encryption = backups.generateEncryptionKeysSync(backupConfig.password);
delete backupConfig.password;
} else {
backupConfig.encryption = null;
}
sysinfo.testConfig(sysinfoConfig, function (error) {
if (error) return done(error);
@@ -247,11 +226,11 @@ function getStatus(callback) {
version: constants.VERSION,
apiServerOrigin: settings.apiServerOrigin(), // used by CaaS tool
webServerOrigin: settings.webServerOrigin(), // used by CaaS tool
provider: settings.provider(),
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
footer: allSettings[settings.FOOTER_KEY] || constants.FOOTER,
adminFqdn: settings.adminDomain() ? settings.adminFqdn() : null,
activated: activated,
provider: settings.provider() // used by setup wizard of marketplace images
}, gProvisionStatus));
});
});
+8 -8
View File
@@ -79,7 +79,7 @@ function getCertApi(domainObject, callback) {
// we simply update the account with the latest email we have each time when getting letsencrypt certs
// https://github.com/ietf-wg-acme/acme/issues/30
users.getOwner(function (error, owner) {
options.email = error ? 'support@cloudron.io' : (owner.fallbackEmail || owner.email); // can error if not activated yet
options.email = error ? 'webmaster@cloudron.io' : owner.email; // can error if not activated yet
callback(null, api, options);
});
@@ -146,19 +146,19 @@ function validateCertificate(location, domainObject, certificate) {
// -checkhost checks for SAN or CN exclusively. SAN takes precedence and if present, ignores the CN.
const fqdn = domains.fqdn(location, domainObject);
var result = safe.child_process.execSync(`openssl x509 -noout -checkhost "${fqdn}"`, { encoding: 'utf8', input: cert });
let result = safe.child_process.execSync(`openssl x509 -noout -checkhost "${fqdn}"`, { encoding: 'utf8', input: cert });
if (result === null) return new BoxError(BoxError.BAD_FIELD, 'Unable to get certificate subject:' + safe.error.message, { field: 'cert' });
if (result.indexOf('does match certificate') === -1) return new BoxError(BoxError.BAD_FIELD, `Certificate is not valid for this domain. Expecting ${fqdn}`, { field: 'cert' });
// http://httpd.apache.org/docs/2.0/ssl/ssl_faq.html#verify
var certModulus = safe.child_process.execSync('openssl x509 -noout -modulus', { encoding: 'utf8', input: cert });
if (certModulus === null) return new BoxError(BoxError.BAD_FIELD, `Unable to get cert modulus: ${safe.error.message}`, { field: 'cert' });
// check if public key in the cert and private key matches. pkey below works for RSA and ECDSA keys
const pubKeyFromCert = safe.child_process.execSync('openssl x509 -noout -pubkey', { encoding: 'utf8', input: cert });
if (pubKeyFromCert === null) return new BoxError(BoxError.BAD_FIELD, `Unable to get public key from cert: ${safe.error.message}`, { field: 'cert' });
var keyModulus = safe.child_process.execSync('openssl rsa -noout -modulus', { encoding: 'utf8', input: key });
if (keyModulus === null) return new BoxError(BoxError.BAD_FIELD, `Unable to get key modulus: ${safe.error.message}`, { field: 'cert' });
const pubKeyFromKey = safe.child_process.execSync('openssl pkey -pubout', { encoding: 'utf8', input: key });
if (pubKeyFromKey === null) return new BoxError(BoxError.BAD_FIELD, `Unable to get public key from private key: ${safe.error.message}`, { field: 'cert' });
if (certModulus !== keyModulus) return new BoxError(BoxError.BAD_FIELD, 'Key does not match the certificate.', { field: 'cert' });
if (pubKeyFromCert !== pubKeyFromKey) return new BoxError(BoxError.BAD_FIELD, 'Public key does not match the certificate.', { field: 'cert' });
// check expiration
result = safe.child_process.execSync('openssl x509 -checkend 0', { encoding: 'utf8', input: cert });
+154 -136
View File
@@ -4,16 +4,16 @@ exports = module.exports = {
getApp: getApp,
getApps: getApps,
getAppIcon: getAppIcon,
installApp: installApp,
uninstallApp: uninstallApp,
restoreApp: restoreApp,
install: install,
uninstall: uninstall,
restore: restore,
importApp: importApp,
backupApp: backupApp,
updateApp: updateApp,
backup: backup,
update: update,
getLogs: getLogs,
getLogStream: getLogStream,
listBackups: listBackups,
repairApp: repairApp,
repair: repair,
setAccessRestriction: setAccessRestriction,
setLabel: setLabel,
@@ -30,17 +30,20 @@ exports = module.exports = {
setMailbox: setMailbox,
setLocation: setLocation,
setDataDir: setDataDir,
setBinds: setBinds,
stopApp: stopApp,
startApp: startApp,
restartApp: restartApp,
stop: stop,
start: start,
restart: restart,
exec: exec,
execWebSocket: execWebSocket,
cloneApp: cloneApp,
clone: clone,
uploadFile: uploadFile,
downloadFile: downloadFile
downloadFile: downloadFile,
load: load
};
var apps = require('../apps.js'),
@@ -51,19 +54,28 @@ var apps = require('../apps.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
safe = require('safetydance'),
users = require('../users.js'),
util = require('util'),
WebSocket = require('ws');
function getApp(req, res, next) {
function load(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
apps.get(req.params.id, function (error, app) {
apps.get(req.params.id, function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, apps.removeInternalFields(app)));
req.resource = result;
next();
});
}
function getApp(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
next(new HttpSuccess(200, apps.removeInternalFields(req.resource)));
}
function getApps(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
@@ -77,19 +89,19 @@ function getApps(req, res, next) {
}
function getAppIcon(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
apps.getIconPath(req.params.id, { original: req.query.original }, function (error, iconPath) {
apps.getIconPath(req.resource, { original: req.query.original }, function (error, iconPath) {
if (error) return next(BoxError.toHttpError(error));
res.sendFile(iconPath);
});
}
function installApp(req, res, next) {
function install(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
var data = req.body;
const data = req.body;
// atleast one
if ('manifest' in data && typeof data.manifest !== 'object') return next(new HttpError(400, 'manifest must be an object'));
@@ -133,22 +145,28 @@ function installApp(req, res, next) {
if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean'));
debug('Installing app :%j', data);
apps.install(data, req.user, auditSource.fromRequest(req), function (error, result) {
apps.downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { id: result.id, taskId: result.taskId }));
if (safe.query(manifest, 'addons.docker') && req.user.role !== users.ROLE_OWNER) return next(new HttpError(403, '"owner" role is required to install app with docker addon'));
data.appStoreId = appStoreId;
data.manifest = manifest;
apps.install(data, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { id: result.id, taskId: result.taskId }));
});
});
}
function setAccessRestriction(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
if (typeof req.body.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction must be an object'));
apps.setAccessRestriction(req.params.id, req.body.accessRestriction, auditSource.fromRequest(req), function (error) {
apps.setAccessRestriction(req.resource, req.body.accessRestriction, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
@@ -157,11 +175,11 @@ function setAccessRestriction(req, res, next) {
function setLabel(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
if (typeof req.body.label !== 'string') return next(new HttpError(400, 'label must be a string'));
apps.setLabel(req.params.id, req.body.label, auditSource.fromRequest(req), function (error) {
apps.setLabel(req.resource, req.body.label, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
@@ -170,12 +188,12 @@ function setLabel(req, res, next) {
function setTags(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
if (!Array.isArray(req.body.tags)) return next(new HttpError(400, 'tags must be an array'));
if (req.body.tags.some((t) => typeof t !== 'string')) return next(new HttpError(400, 'tags array must contain strings'));
apps.setTags(req.params.id, req.body.tags, auditSource.fromRequest(req), function (error) {
apps.setTags(req.resource, req.body.tags, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
@@ -184,11 +202,11 @@ function setTags(req, res, next) {
function setIcon(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
if (req.body.icon !== null && typeof req.body.icon !== 'string') return next(new HttpError(400, 'icon is null or a base-64 image string'));
apps.setIcon(req.params.id, req.body.icon, auditSource.fromRequest(req), function (error) {
apps.setIcon(req.resource, req.body.icon, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
@@ -197,11 +215,11 @@ function setIcon(req, res, next) {
function setMemoryLimit(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
if (typeof req.body.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
apps.setMemoryLimit(req.params.id, req.body.memoryLimit, auditSource.fromRequest(req), function (error, result) {
apps.setMemoryLimit(req.resource, req.body.memoryLimit, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
@@ -210,11 +228,11 @@ function setMemoryLimit(req, res, next) {
function setCpuShares(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
if (typeof req.body.cpuShares !== 'number') return next(new HttpError(400, 'cpuShares is not a number'));
apps.setCpuShares(req.params.id, req.body.cpuShares, auditSource.fromRequest(req), function (error, result) {
apps.setCpuShares(req.resource, req.body.cpuShares, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
@@ -223,11 +241,11 @@ function setCpuShares(req, res, next) {
function setAutomaticBackup(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enable must be a boolean'));
apps.setAutomaticBackup(req.params.id, req.body.enable, auditSource.fromRequest(req), function (error) {
apps.setAutomaticBackup(req.resource, req.body.enable, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
@@ -236,11 +254,11 @@ function setAutomaticBackup(req, res, next) {
function setAutomaticUpdate(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enable must be a boolean'));
apps.setAutomaticUpdate(req.params.id, req.body.enable, auditSource.fromRequest(req), function (error) {
apps.setAutomaticUpdate(req.resource, req.body.enable, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
@@ -249,13 +267,13 @@ function setAutomaticUpdate(req, res, next) {
function setReverseProxyConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
if (req.body.robotsTxt !== null && typeof req.body.robotsTxt !== 'string') return next(new HttpError(400, 'robotsTxt is not a string'));
if (req.body.csp !== null && typeof req.body.csp !== 'string') return next(new HttpError(400, 'csp is not a string'));
apps.setReverseProxyConfig(req.params.id, req.body, auditSource.fromRequest(req), function (error) {
apps.setReverseProxyConfig(req.resource, req.body, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
@@ -264,14 +282,14 @@ function setReverseProxyConfig(req, res, next) {
function setCertificate(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
if (req.body.key !== null && typeof req.body.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
if (req.body.cert !== null && typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if (req.body.cert && !req.body.key) return next(new HttpError(400, 'key must be provided'));
if (!req.body.cert && req.body.key) return next(new HttpError(400, 'cert must be provided'));
apps.setCertificate(req.params.id, req.body, auditSource.fromRequest(req), function (error) {
apps.setCertificate(req.resource, req.body, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
@@ -280,12 +298,12 @@ function setCertificate(req, res, next) {
function setEnvironment(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
if (!req.body.env || typeof req.body.env !== 'object') return next(new HttpError(400, 'env must be an object'));
if (Object.keys(req.body.env).some((key) => typeof req.body.env[key] !== 'string')) return next(new HttpError(400, 'env must contain values as strings'));
apps.setEnvironment(req.params.id, req.body.env, auditSource.fromRequest(req), function (error, result) {
apps.setEnvironment(req.resource, req.body.env, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
@@ -294,11 +312,11 @@ function setEnvironment(req, res, next) {
function setDebugMode(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
if (req.body.debugMode !== null && typeof req.body.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object'));
apps.setDebugMode(req.params.id, req.body.debugMode, auditSource.fromRequest(req), function (error, result) {
apps.setDebugMode(req.resource, req.body.debugMode, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
@@ -307,12 +325,12 @@ function setDebugMode(req, res, next) {
function setMailbox(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
if (req.body.mailboxName !== null && typeof req.body.mailboxName !== 'string') return next(new HttpError(400, 'mailboxName must be a string'));
if (typeof req.body.mailboxDomain !== 'string') return next(new HttpError(400, 'mailboxDomain must be a string'));
apps.setMailbox(req.params.id, req.body.mailboxName, req.body.mailboxDomain, auditSource.fromRequest(req), function (error, result) {
apps.setMailbox(req.resource, req.body.mailboxName, req.body.mailboxDomain, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
@@ -321,7 +339,7 @@ function setMailbox(req, res, next) {
function setLocation(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
if (typeof req.body.location !== 'string') return next(new HttpError(400, 'location must be string')); // location may be an empty string
if (!req.body.domain) return next(new HttpError(400, 'domain is required'));
@@ -336,7 +354,7 @@ function setLocation(req, res, next) {
if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean'));
apps.setLocation(req.params.id, req.body, auditSource.fromRequest(req), function (error, result) {
apps.setLocation(req.resource, req.body, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
@@ -345,51 +363,49 @@ function setLocation(req, res, next) {
function setDataDir(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
if (req.body.dataDir !== null && typeof req.body.dataDir !== 'string') return next(new HttpError(400, 'dataDir must be a string'));
apps.setDataDir(req.params.id, req.body.dataDir, auditSource.fromRequest(req), function (error, result) {
apps.setDataDir(req.resource, req.body.dataDir, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function repairApp(req, res, next) {
function repair(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
debug('Repair app id:%s', req.params.id);
assert.strictEqual(typeof req.resource, 'object');
const data = req.body;
if ('manifest' in data) {
if (!data.manifest || typeof data.manifest !== 'object') return next(new HttpError(400, 'manifest must be an object'));
if (safe.query(data.manifest, 'addons.docker') && req.user.role !== users.ROLE_OWNER) return next(new HttpError(403, '"owner" role is required to repair app with docker addon'));
}
if ('dockerImage' in data) {
if (!data.dockerImage || typeof data.dockerImage !== 'string') return next(new HttpError(400, 'dockerImage must be a string'));
}
apps.repair(req.params.id, data, auditSource.fromRequest(req), function (error, result) {
apps.repair(req.resource, data, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function restoreApp(req, res, next) {
function restore(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
var data = req.body;
debug('Restore app id:%s', req.params.id);
if (!data.backupId || typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be non-empty string'));
apps.restore(req.params.id, data.backupId, auditSource.fromRequest(req), function (error, result) {
apps.restore(req.resource, data.backupId, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
@@ -398,12 +414,10 @@ function restoreApp(req, res, next) {
function importApp(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
var data = req.body;
debug('Importing app id:%s', req.params.id);
if ('backupId' in data) { // if not provided, we import in-place
if (typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be string'));
if (typeof data.backupFormat !== 'string') return next(new HttpError(400, 'backupFormat must be string'));
@@ -414,7 +428,7 @@ function importApp(req, res, next) {
if (req.body.backupConfig) {
if (typeof backupConfig.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if ('key' in backupConfig && typeof backupConfig.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if ('password' in backupConfig && typeof backupConfig.password !== 'string') return next(new HttpError(400, 'password must be a string'));
if ('acceptSelfSignedCerts' in backupConfig && typeof backupConfig.acceptSelfSignedCerts !== 'boolean') return next(new HttpError(400, 'format must be a boolean'));
// testing backup config can take sometime
@@ -422,21 +436,19 @@ function importApp(req, res, next) {
}
}
apps.importApp(req.params.id, data, auditSource.fromRequest(req), function (error, result) {
apps.importApp(req.resource, data, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function cloneApp(req, res, next) {
function clone(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
var data = req.body;
debug('Clone app id:%s', req.params.id);
if (typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be a string'));
if (typeof data.location !== 'string') return next(new HttpError(400, 'location is required'));
if (typeof data.domain !== 'string') return next(new HttpError(400, 'domain is required'));
@@ -444,76 +456,66 @@ function cloneApp(req, res, next) {
if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean'));
apps.clone(req.params.id, data, req.user, auditSource.fromRequest(req), function (error, result) {
apps.clone(req.resource, data, req.user, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201, { id: result.id, taskId: result.taskId }));
});
}
function backupApp(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
function backup(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
debug('Backup app id:%s', req.params.id);
apps.backup(req.params.id, function (error, result) {
apps.backup(req.resource, function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function uninstallApp(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
function uninstall(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
debug('Uninstalling app id:%s', req.params.id);
apps.uninstall(req.params.id, auditSource.fromRequest(req), function (error, result) {
apps.uninstall(req.resource, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function startApp(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
function start(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
debug('Start app id:%s', req.params.id);
apps.start(req.params.id, function (error, result) {
apps.start(req.resource, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function stopApp(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
function stop(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
debug('Stop app id:%s', req.params.id);
apps.stop(req.params.id, function (error, result) {
apps.stop(req.resource, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function restartApp(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
function restart(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
debug('Restart app id:%s', req.params.id);
apps.restart(req.params.id, function (error, result) {
apps.restart(req.resource, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function updateApp(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
function update(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
var data = req.body;
@@ -525,20 +527,24 @@ function updateApp(req, res, next) {
if ('skipBackup' in data && typeof data.skipBackup !== 'boolean') return next(new HttpError(400, 'skipBackup must be a boolean'));
if ('force' in data && typeof data.force !== 'boolean') return next(new HttpError(400, 'force must be a boolean'));
debug('Update app id:%s to manifest:%j', req.params.id, data.manifest);
apps.update(req.params.id, req.body, auditSource.fromRequest(req), function (error, result) {
apps.downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
if (safe.query(manifest, 'addons.docker') && req.user.role !== users.ROLE_OWNER) return next(new HttpError(403, '"owner" role is required to update app with docker addon'));
data.appStoreId = appStoreId;
data.manifest = manifest;
apps.update(req.resource, data, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
});
}
// this route is for streaming logs
function getLogStream(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
debug('Getting logstream of app id:%s', req.params.id);
assert.strictEqual(typeof req.resource, 'object');
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
@@ -553,7 +559,7 @@ function getLogStream(req, res, next) {
format: 'json'
};
apps.getLogs(req.params.id, options, function (error, logStream) {
apps.getLogs(req.resource, options, function (error, logStream) {
if (error) return next(BoxError.toHttpError(error));
res.writeHead(200, {
@@ -575,20 +581,18 @@ function getLogStream(req, res, next) {
}
function getLogs(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10;
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number'));
debug('Getting logs of app id:%s', req.params.id);
var options = {
lines: lines,
follow: false,
format: req.query.format || 'json'
};
apps.getLogs(req.params.id, options, function (error, logStream) {
apps.getLogs(req.resource, options, function (error, logStream) {
if (error) return next(BoxError.toHttpError(error));
res.writeHead(200, {
@@ -624,9 +628,7 @@ function demuxStream(stream, stdin) {
}
function exec(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
debug('Execing into app id:%s and cmd:%s', req.params.id, req.query.cmd);
assert.strictEqual(typeof req.resource, 'object');
var cmd = null;
if (req.query.cmd) {
@@ -640,13 +642,16 @@ function exec(req, res, next) {
var rows = req.query.rows ? parseInt(req.query.rows, 10) : null;
if (isNaN(rows)) return next(new HttpError(400, 'rows must be a number'));
var tty = req.query.tty === 'true' ? true : false;
var tty = req.query.tty === 'true';
apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
if (safe.query(req.resource, 'manifest.addons.docker') && req.user.role !== users.ROLE_OWNER) return next(new HttpError(403, '"owner" role is requied to exec app with docker addon'));
// in a badly configured reverse proxy, we might be here without an upgrade
if (req.headers['upgrade'] !== 'tcp') return next(new HttpError(404, 'exec requires TCP upgrade'));
apps.exec(req.resource, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
if (error) return next(BoxError.toHttpError(error));
if (req.headers['upgrade'] !== 'tcp') return next(new HttpError(404, 'exec requires TCP upgrade'));
req.clearTimeout();
res.sendUpgradeHandshake();
@@ -664,9 +669,7 @@ function exec(req, res, next) {
}
function execWebSocket(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
debug('Execing websocket into app id:%s and cmd:%s', req.params.id, req.query.cmd);
assert.strictEqual(typeof req.resource, 'object');
var cmd = null;
if (req.query.cmd) {
@@ -682,10 +685,11 @@ function execWebSocket(req, res, next) {
var tty = req.query.tty === 'true' ? true : false;
apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
if (error) return next(BoxError.toHttpError(error));
// in a badly configured reverse proxy, we might be here without an upgrade
if (req.headers['upgrade'] !== 'websocket') return next(new HttpError(404, 'exec requires websocket'));
debug('Connected to terminal');
apps.exec(req.resource, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
if (error) return next(BoxError.toHttpError(error));
req.clearTimeout();
@@ -714,7 +718,7 @@ function execWebSocket(req, res, next) {
}
function listBackups(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
@@ -722,7 +726,7 @@ function listBackups(req, res, next) {
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
apps.listBackups(page, perPage, req.params.id, function (error, result) {
apps.listBackups(req.resource, page, perPage, function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { backups: result }));
@@ -730,30 +734,24 @@ function listBackups(req, res, next) {
}
function uploadFile(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
debug('uploadFile: %s %j -> %s', req.params.id, req.files, req.query.file);
assert.strictEqual(typeof req.resource, 'object');
if (typeof req.query.file !== 'string' || !req.query.file) return next(new HttpError(400, 'file query argument must be provided'));
if (!req.files.file) return next(new HttpError(400, 'file must be provided as multipart'));
apps.uploadFile(req.params.id, req.files.file.path, req.query.file, function (error) {
apps.uploadFile(req.resource, req.files.file.path, req.query.file, function (error) {
if (error) return next(BoxError.toHttpError(error));
debug('uploadFile: done');
next(new HttpSuccess(202, {}));
});
}
function downloadFile(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
debug('downloadFile: ', req.params.id, req.query.file);
assert.strictEqual(typeof req.resource, 'object');
if (typeof req.query.file !== 'string' || !req.query.file) return next(new HttpError(400, 'file query argument must be provided'));
apps.downloadFile(req.params.id, req.query.file, function (error, stream, info) {
apps.downloadFile(req.resource, req.query.file, function (error, stream, info) {
if (error) return next(BoxError.toHttpError(error));
var headers = {
@@ -767,3 +765,23 @@ function downloadFile(req, res, next) {
stream.pipe(res);
});
}
function setBinds(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
if (!req.body.binds || typeof req.body.binds !== 'object') return next(new HttpError(400, 'binds should be an object'));
for (let name of Object.keys(req.body.binds)) {
if (!req.body.binds[name] || typeof req.body.binds[name] !== 'object') return next(new HttpError(400, 'each bind should be an object'));
if (typeof req.body.binds[name].hostPath !== 'string') return next(new HttpError(400, 'hostPath must be a string'));
if (typeof req.body.binds[name].readOnly !== 'boolean') return next(new HttpError(400, 'readOnly must be a boolean'));
}
apps.setBinds(req.resource, req.body.binds, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
+1 -2
View File
@@ -7,7 +7,6 @@ exports = module.exports = {
};
let auditSource = require('../auditsource.js'),
backupdb = require('../backupdb.js'),
backups = require('../backups.js'),
BoxError = require('../boxerror.js'),
HttpError = require('connect-lastmile').HttpError,
@@ -20,7 +19,7 @@ function list(req, res, next) {
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
backups.getByStatePaged(backupdb.BACKUP_STATE_NORMAL, page, perPage, function (error, result) {
backups.getByIdentifierAndStatePaged(backups.BACKUP_IDENTIFIER_BOX, backups.BACKUP_STATE_NORMAL, page, perPage, function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { backups: result }));
+140
View File
@@ -0,0 +1,140 @@
'use strict';
exports = module.exports = {
get,
set,
getCloudronAvatar
};
var assert = require('assert'),
BoxError = require('../boxerror.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
safe = require('safetydance'),
settings = require('../settings.js'),
_ = require('underscore');
function getFooter(req, res, next) {
settings.getFooter(function (error, footer) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { footer }));
});
}
function setFooter(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.footer !== 'string') return next(new HttpError(400, 'footer is required'));
settings.setFooter(req.body.footer, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
});
}
function setCloudronName(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name is required'));
settings.setCloudronName(req.body.name, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {}));
});
}
function getCloudronName(req, res, next) {
settings.getCloudronName(function (error, name) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { name: name }));
});
}
function setAppstoreListingConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
const listingConfig = _.pick(req.body, 'whitelist', 'blacklist');
if (Object.keys(listingConfig).length === 0) return next(new HttpError(400, 'blacklist or whitelist is required'));
if ('whitelist' in listingConfig) {
if (listingConfig.whitelist !== null && !Array.isArray(listingConfig.whitelist)) return next(new HttpError(400, 'whitelist is null or an array of strings'));
if (listingConfig.whitelist && !listingConfig.whitelist.every(id => typeof id === 'string')) return next(new HttpError(400, 'whitelist must be array of strings'));
}
if ('blacklist' in listingConfig) {
if (!Array.isArray(listingConfig.blacklist)) return next(new HttpError(400, 'blacklist an array of strings'));
if (!listingConfig.blacklist.every(id => typeof id === 'string')) return next(new HttpError(400, 'blacklist must be array of strings'));
}
settings.setAppstoreListingConfig(listingConfig, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {}));
});
}
function getAppstoreListingConfig(req, res, next) {
settings.getAppstoreListingConfig(function (error, listingConfig) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, listingConfig));
});
}
function setCloudronAvatar(req, res, next) {
assert.strictEqual(typeof req.files, 'object');
if (!req.files.avatar) return next(new HttpError(400, 'avatar must be provided'));
var avatar = safe.fs.readFileSync(req.files.avatar.path);
settings.setCloudronAvatar(avatar, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {}));
});
}
function getCloudronAvatar(req, res, next) {
settings.getCloudronAvatar(function (error, avatar) {
if (error) return next(BoxError.toHttpError(error));
// avoid caching the avatar on the client to see avatar changes immediately
res.set('Cache-Control', 'no-cache');
res.set('Content-Type', 'image/png');
res.status(200).send(avatar);
});
}
function get(req, res, next) {
assert.strictEqual(typeof req.params.setting, 'string');
switch (req.params.setting) {
case settings.APPSTORE_LISTING_CONFIG_KEY: return getAppstoreListingConfig(req, res, next);
case settings.CLOUDRON_AVATAR_KEY: return getCloudronAvatar(req, res, next);
case settings.CLOUDRON_NAME_KEY: return getCloudronName(req, res, next);
case settings.FOOTER_KEY: return getFooter(req, res, next);
default: return next(new HttpError(404, 'No such setting'));
}
}
function set(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
switch (req.params.setting) {
case settings.APPSTORE_LISTING_CONFIG_KEY: return setAppstoreListingConfig(req, res, next);
case settings.CLOUDRON_AVATAR_KEY: return setCloudronAvatar(req, res, next);
case settings.CLOUDRON_NAME_KEY: return setCloudronName(req, res, next);
case settings.FOOTER_KEY: return setFooter(req, res, next);
default: return next(new HttpError(404, 'No such branding'));
}
}
+16 -29
View File
@@ -29,7 +29,6 @@ let assert = require('assert'),
BoxError = require('../boxerror.js'),
cloudron = require('../cloudron.js'),
constants = require('../constants.js'),
debug = require('debug')('box:routes/cloudron'),
eventlog = require('../eventlog.js'),
externalLdap = require('../externalldap.js'),
HttpError = require('connect-lastmile').HttpError,
@@ -87,8 +86,8 @@ function logout(req, res) {
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'));
users.resetPasswordByIdentifier(req.body.identifier, function (error) {
if (error && error.reason !== BoxError.NOT_FOUND) console.error(error);
users.sendPasswordResetByIdentifier(req.body.identifier, function (error) {
if (error && error.reason !== BoxError.NOT_FOUND) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {}));
});
@@ -103,15 +102,17 @@ function passwordReset(req, res, next) {
users.getByResetToken(req.body.resetToken, function (error, userObject) {
if (error) return next(new HttpError(401, 'Invalid resetToken'));
// if you fix the duration here, the emails and UI have to be fixed as well
if (Date.now() - userObject.resetTokenCreationTime > 24 * 60 * 60 * 1000) return next(new HttpError(401, 'Token expired'));
if (!userObject.username) return next(new HttpError(409, 'No username set'));
// setPassword clears the resetToken
users.setPassword(userObject, req.body.password, function (error) {
if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
tokens.add(tokens.ID_WEBADMIN, userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { accessToken: result.accessToken }));
});
@@ -122,37 +123,23 @@ function passwordReset(req, res, next) {
function setupAccount(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.email || typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be a non-empty string'));
if (!req.body.resetToken || typeof req.body.resetToken !== 'string') return next(new HttpError(400, 'resetToken must be a non-empty string'));
if (!req.body.password || typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be a non-empty string'));
if (!req.body.username || typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be a non-empty string'));
if (!req.body.displayName || typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be a non-empty string'));
debug(`setupAccount: for email ${req.body.email} and username ${req.body.username} with token ${req.body.resetToken}`);
// only sent if profile is not locked
if ('username' in req.body && typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be a non-empty string'));
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be a non-empty string'));
users.getByResetToken(req.body.resetToken, function (error, userObject) {
if (error) return next(new HttpError(401, 'Invalid Reset Token'));
users.update(userObject, { username: req.body.username, displayName: req.body.displayName }, auditSource.fromRequest(req), function (error) {
if (error && error.reason === BoxError.ALREADY_EXISTS) return next(new HttpError(409, 'Username already used'));
if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === BoxError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
if (error) return next(new HttpError(500, error));
// if you fix the duration here, the emails and UI have to be fixed as well
if (Date.now() - userObject.resetTokenCreationTime > 24 * 60 * 60 * 1000) return next(new HttpError(401, 'Token expired'));
userObject.username = req.body.username;
userObject.displayName = req.body.displayName;
users.setupAccount(userObject, req.body, auditSource.fromRequest(req), function (error, accessToken) {
if (error) return next(BoxError.toHttpError(error));
// setPassword clears the resetToken
users.setPassword(userObject, req.body.password, function (error) {
if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
tokens.add(tokens.ID_WEBADMIN, userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, { accessToken: result.accessToken }));
});
});
next(new HttpSuccess(201, { accessToken }));
});
});
}
@@ -218,8 +205,8 @@ function checkForUpdates(req, res, next) {
req.clearTimeout();
async.series([
updateChecker.checkAppUpdates,
updateChecker.checkBoxUpdates
(done) => updateChecker.checkAppUpdates({ automatic: false }, done),
(done) => updateChecker.checkBoxUpdates({ automatic: false }, done),
], function () {
next(new HttpSuccess(200, { update: updateChecker.getUpdateInfo() }));
});
+43
View File
@@ -0,0 +1,43 @@
'use strict';
exports = module.exports = {
proxy
};
var assert = require('assert'),
BoxError = require('../boxerror.js'),
docker = require('../docker.js'),
middleware = require('../middleware/index.js'),
HttpError = require('connect-lastmile').HttpError,
safe = require('safetydance'),
url = require('url');
function proxy(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
const appId = req.params.id;
req.clearTimeout();
docker.inspect('sftp', function (error, result) {
if (error)return next(BoxError.toHttpError(error));
const ip = safe.query(result, 'NetworkSettings.Networks.cloudron.IPAddress', null);
if (!ip) return next(new BoxError(BoxError.INACTIVE, 'Error getting IP of sftp service'));
req.url = req.originalUrl.replace(`/api/v1/apps/${appId}/files`, `/files/${appId}`);
const proxyOptions = url.parse(`https://${ip}:3000`);
proxyOptions.rejectUnauthorized = false;
const fileManagerProxy = middleware.proxy(proxyOptions);
fileManagerProxy(req, res, function (error) {
if (!error) return next();
if (error.code === 'ECONNREFUSED') return next(new HttpError(424, 'Unable to connect to filemanager server'));
if (error.code === 'ECONNRESET') return next(new HttpError(424, 'Unable to query filemanager server'));
next(new HttpError(500, error));
});
});
}
+3 -1
View File
@@ -20,7 +20,9 @@ function create(req, res, next) {
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be string'));
groups.create(req.body.name, function (error, group) {
var source = ''; // means local
groups.create(req.body.name, source, function (error, group) {
if (error) return next(BoxError.toHttpError(error));
var groupInfo = {
+2
View File
@@ -6,9 +6,11 @@ exports = module.exports = {
apps: require('./apps.js'),
appstore: require('./appstore.js'),
backups: require('./backups.js'),
branding: require('./branding.js'),
cloudron: require('./cloudron.js'),
domains: require('./domains.js'),
eventlog: require('./eventlog.js'),
filemanager: require('./filemanager.js'),
graphs: require('./graphs.js'),
groups: require('./groups.js'),
mail: require('./mail.js'),
+52 -67
View File
@@ -1,36 +1,35 @@
'use strict';
exports = module.exports = {
getDomain: getDomain,
addDomain: addDomain,
removeDomain: removeDomain,
getDomain,
setDnsRecords: setDnsRecords,
setDnsRecords,
getStatus: getStatus,
getStatus,
setMailFromValidation: setMailFromValidation,
setCatchAllAddress: setCatchAllAddress,
setMailRelay: setMailRelay,
setMailEnabled: setMailEnabled,
setMailFromValidation,
setCatchAllAddress,
setMailRelay,
setMailEnabled,
sendTestMail: sendTestMail,
sendTestMail,
listMailboxes: listMailboxes,
getMailbox: getMailbox,
addMailbox: addMailbox,
updateMailbox: updateMailbox,
removeMailbox: removeMailbox,
listMailboxes,
getMailbox,
addMailbox,
updateMailbox,
removeMailbox,
listAliases: listAliases,
getAliases: getAliases,
setAliases: setAliases,
getAliases,
setAliases,
getLists: getLists,
getList: getList,
addList: addList,
updateList: updateList,
removeList: removeList,
getLists,
getList,
addList,
updateList,
removeList,
getMailboxCount
};
var assert = require('assert'),
@@ -50,18 +49,6 @@ function getDomain(req, res, next) {
});
}
function addDomain(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
mail.addDomain(req.body.domain, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201, { domain: req.body.domain }));
});
}
function setDnsRecords(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.domain, 'string');
@@ -77,16 +64,6 @@ function setDnsRecords(req, res, next) {
});
}
function removeDomain(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.removeDomain(req.params.domain, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204));
});
}
function getStatus(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
@@ -184,13 +161,25 @@ function listMailboxes(req, res, next) {
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a positive number'));
mail.listMailboxes(req.params.domain, page, perPage, function (error, result) {
if (req.query.search && typeof req.query.search !== 'string') return next(new HttpError(400, 'search must be a string'));
mail.listMailboxes(req.params.domain, req.query.search || null, page, perPage, function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { mailboxes: result }));
});
}
function getMailboxCount(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.getMailboxCount(req.params.domain, function (error, count) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { count }));
});
}
function getMailbox(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.name, 'string');
@@ -239,22 +228,6 @@ function removeMailbox(req, res, next) {
});
}
function listAliases(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a positive number'));
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a positive number'));
mail.listAliases(req.params.domain, page, perPage, function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { aliases: result }));
});
}
function getAliases(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.name, 'string');
@@ -273,8 +246,10 @@ function setAliases(req, res, next) {
if (!Array.isArray(req.body.aliases)) return next(new HttpError(400, 'aliases must be an array'));
for (var i = 0; i < req.body.aliases.length; i++) {
if (typeof req.body.aliases[i] !== 'string') return next(new HttpError(400, 'alias must be a string'));
for (let alias of req.body.aliases) {
if (!alias || typeof alias !== 'object') return next(new HttpError(400, 'each alias must have a name and domain'));
if (typeof alias.name !== 'string') return next(new HttpError(400, 'name must be a string'));
if (typeof alias.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
}
mail.setAliases(req.params.name, req.params.domain, req.body.aliases, function (error) {
@@ -287,7 +262,15 @@ function setAliases(req, res, next) {
function getLists(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.getLists(req.params.domain, function (error, result) {
const page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a positive number'));
const perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a positive number'));
if (req.query.search && typeof req.query.search !== 'string') return next(new HttpError(400, 'search must be a string'));
mail.getLists(req.params.domain, req.query.search || null, page, perPage, function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { lists: result }));
@@ -316,8 +299,9 @@ function addList(req, res, next) {
for (var i = 0; i < req.body.members.length; i++) {
if (typeof req.body.members[i] !== 'string') return next(new HttpError(400, 'member must be a string'));
}
if (typeof req.body.membersOnly !== 'boolean') return next(new HttpError(400, 'membersOnly must be a boolean'));
mail.addList(req.body.name, req.params.domain, req.body.members, auditSource.fromRequest(req), function (error) {
mail.addList(req.body.name, req.params.domain, req.body.members, req.body.membersOnly, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201, {}));
@@ -334,8 +318,9 @@ function updateList(req, res, next) {
for (var i = 0; i < req.body.members.length; i++) {
if (typeof req.body.members[i] !== 'string') return next(new HttpError(400, 'member must be a string'));
}
if (typeof req.body.membersOnly !== 'boolean') return next(new HttpError(400, 'membersOnly must be a boolean'));
mail.updateList(req.params.name, req.params.domain, req.body.members, auditSource.fromRequest(req), function (error) {
mail.updateList(req.params.name, req.params.domain, req.body.members, req.body.membersOnly, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204));
+1 -1
View File
@@ -21,7 +21,7 @@ function proxy(req, res, next) {
delete req.headers['authorization'];
delete req.headers['cookies'];
addons.getServiceDetails('mail', 'CLOUDRON_MAIL_TOKEN', function (error, addonDetails) {
addons.getContainerDetails('mail', 'CLOUDRON_MAIL_TOKEN', function (error, addonDetails) {
if (error) return next(BoxError.toHttpError(error));
parsedUrl.query['access_token'] = addonDetails.token;
+34 -21
View File
@@ -1,34 +1,41 @@
'use strict';
exports = module.exports = {
get: get,
update: update,
getAvatar: getAvatar,
setAvatar: setAvatar,
clearAvatar: clearAvatar,
changePassword: changePassword,
setTwoFactorAuthenticationSecret: setTwoFactorAuthenticationSecret,
enableTwoFactorAuthentication: enableTwoFactorAuthentication,
disableTwoFactorAuthentication: disableTwoFactorAuthentication
authorize,
get,
update,
getAvatar,
setAvatar,
clearAvatar,
changePassword,
setTwoFactorAuthenticationSecret,
enableTwoFactorAuthentication,
disableTwoFactorAuthentication,
};
var assert = require('assert'),
auditSource = require('../auditsource.js'),
BoxError = require('../boxerror.js'),
fs = require('fs'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
path = require('path'),
paths = require('../paths.js'),
safe = require('safetydance'),
users = require('../users.js'),
settings = require('../settings.js'),
_ = require('underscore');
function get(req, res, next) {
function authorize(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
const emailHash = require('crypto').createHash('md5').update(req.user.email).digest('hex');
settings.getDirectoryConfig(function (error, directoryConfig) {
if (error) return next(BoxError.toHttpError(error));
if (directoryConfig.lockUserProfiles) return next(new HttpError(403, 'admin has disallowed users from editing profiles'));
next();
});
}
function get(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
next(new HttpSuccess(200, {
id: req.user.id,
@@ -39,7 +46,7 @@ function get(req, res, next) {
twoFactorAuthenticationEnabled: req.user.twoFactorAuthenticationEnabled,
role: req.user.role,
source: req.user.source,
avatarUrl: fs.existsSync(path.join(paths.PROFILE_ICONS_DIR, req.user.id)) ? `${settings.adminOrigin()}/api/v1/profile/avatar/${req.user.id}` : `https://www.gravatar.com/avatar/${emailHash}.jpg`
avatarUrl: users.getAvatarUrlSync(req.user)
}));
}
@@ -65,21 +72,27 @@ function setAvatar(req, res, next) {
if (!req.files.avatar) return next(new HttpError(400, 'avatar is missing'));
if (!safe.fs.renameSync(req.files.avatar.path, path.join(paths.PROFILE_ICONS_DIR, req.user.id))) return next(new HttpError(500, safe.error));
users.setAvatar(req.user.id, req.files.avatar.path, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {}));
next(new HttpSuccess(202, {}));
});
}
function clearAvatar(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
safe.fs.unlinkSync(path.join(paths.PROFILE_ICONS_DIR, req.user.id));
users.clearAvatar(req.user.id, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {}));
next(new HttpSuccess(202, {}));
});
}
function getAvatar(req, res) {
res.sendFile(path.join(paths.PROFILE_ICONS_DIR, req.params.identifier));
assert.strictEqual(typeof req.params.identifier, 'string');
res.sendFile(users.getAvatarFileSync(req.params.identifier));
}
function changePassword(req, res, next) {
+3 -3
View File
@@ -98,11 +98,11 @@ function restore(req, res, next) {
var backupConfig = req.body.backupConfig;
if (typeof backupConfig.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if ('key' in backupConfig && typeof backupConfig.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if ('password' in backupConfig && typeof backupConfig.password !== 'string') return next(new HttpError(400, 'password must be a string'));
if (typeof backupConfig.format !== 'string') return next(new HttpError(400, 'format must be a string'));
if ('acceptSelfSignedCerts' in backupConfig && typeof backupConfig.acceptSelfSignedCerts !== 'boolean') return next(new HttpError(400, 'format must be a boolean'));
if (typeof req.body.backupId !== 'string') return next(new HttpError(400, 'backupId must be a string or null'));
if (typeof req.body.backupId !== 'string') return next(new HttpError(400, 'backupId must be a string'));
if (typeof req.body.version !== 'string') return next(new HttpError(400, 'version must be a string'));
if ('sysinfoConfig' in req.body && typeof req.body.sysinfoConfig !== 'object') return next(new HttpError(400, 'sysinfoConfig must be an object'));
@@ -122,7 +122,7 @@ function getStatus(req, res, next) {
// check if Cloudron is not in setup state nor activated and let appstore know of the attempt
if (!status.activated && !status.setup.active && !status.restore.active) {
appstore.trackBeginSetup(status.provider);
appstore.trackBeginSetup();
}
});
}
-7
View File
@@ -12,7 +12,6 @@ exports = module.exports = {
var addons = require('../addons.js'),
assert = require('assert'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:routes/addons'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
@@ -60,8 +59,6 @@ function getLogs(req, res, next) {
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number'));
debug(`Getting logs of service ${req.params.service}`);
var options = {
lines: lines,
follow: false,
@@ -85,8 +82,6 @@ function getLogs(req, res, next) {
function getLogStream(req, res, next) {
assert.strictEqual(typeof req.params.service, 'string');
debug(`Getting logstream of service ${req.params.service}`);
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
@@ -124,8 +119,6 @@ function getLogStream(req, res, next) {
function restart(req, res, next) {
assert.strictEqual(typeof req.params.service, 'string');
debug(`Restarting service ${req.params.service}`);
addons.restartService(req.params.service, function (error) {
if (error) return next(BoxError.toHttpError(error));
+31 -81
View File
@@ -1,10 +1,11 @@
'use strict';
exports = module.exports = {
set: set,
get: get,
set,
get,
getCloudronAvatar: getCloudronAvatar
// owner only settings
setBackupConfig
};
var assert = require('assert'),
@@ -14,7 +15,6 @@ var assert = require('assert'),
externalLdap = require('../externalldap.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
safe = require('safetydance'),
settings = require('../settings.js');
function getAppAutoupdatePattern(req, res, next) {
@@ -57,26 +57,6 @@ function setBoxAutoupdatePattern(req, res, next) {
});
}
function setCloudronName(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name is required'));
settings.setCloudronName(req.body.name, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {}));
});
}
function getCloudronName(req, res, next) {
settings.getCloudronName(function (error, name) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { name: name }));
});
}
function getTimeZone(req, res, next) {
settings.getTimeZone(function (error, tz) {
if (error) return next(BoxError.toHttpError(error));
@@ -97,26 +77,6 @@ function setTimeZone(req, res, next) {
});
}
function getFooter(req, res, next) {
settings.getFooter(function (error, footer) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { footer }));
});
}
function setFooter(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.footer !== 'string') return next(new HttpError(400, 'footer is required'));
settings.setFooter(req.body.footer, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
});
}
function getSupportConfig(req, res, next) {
settings.getSupportConfig(function (error, supportConfig) {
if (error) return next(BoxError.toHttpError(error));
@@ -125,31 +85,6 @@ function getSupportConfig(req, res, next) {
});
}
function setCloudronAvatar(req, res, next) {
assert.strictEqual(typeof req.files, 'object');
if (!req.files.avatar) return next(new HttpError(400, 'avatar must be provided'));
var avatar = safe.fs.readFileSync(req.files.avatar.path);
settings.setCloudronAvatar(avatar, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {}));
});
}
function getCloudronAvatar(req, res, next) {
settings.getCloudronAvatar(function (error, avatar) {
if (error) return next(BoxError.toHttpError(error));
// avoid caching the avatar on the client to see avatar changes immediately
res.set('Cache-Control', 'no-cache');
res.set('Content-Type', 'image/png');
res.status(200).send(avatar);
});
}
function getBackupConfig(req, res, next) {
settings.getBackupConfig(function (error, backupConfig) {
if (error) return next(BoxError.toHttpError(error));
@@ -162,9 +97,8 @@ function setBackupConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if (typeof req.body.retentionSecs !== 'number') return next(new HttpError(400, 'retentionSecs is required'));
if (typeof req.body.intervalSecs !== 'number') return next(new HttpError(400, 'intervalSecs is required'));
if ('key' in req.body && typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be a string'));
if ('syncConcurrency' in req.body) {
if (typeof req.body.syncConcurrency !== 'number') return next(new HttpError(400, 'syncConcurrency must be a positive integer'));
if (req.body.syncConcurrency < 1) return next(new HttpError(400, 'syncConcurrency must be a positive integer'));
@@ -172,6 +106,8 @@ function setBackupConfig(req, res, next) {
if (typeof req.body.format !== 'string') return next(new HttpError(400, 'format must be a string'));
if ('acceptSelfSignedCerts' in req.body && typeof req.body.acceptSelfSignedCerts !== 'boolean') return next(new HttpError(400, 'format must be a boolean'));
if (!req.body.retentionPolicy || typeof req.body.retentionPolicy !== 'object') return next(new HttpError(400, 'retentionPolicy is required'));
// testing the backup using put/del takes a bit of time at times
req.clearTimeout();
@@ -224,6 +160,7 @@ function setExternalLdapConfig(req, res, next) {
if ('baseDn' in req.body && typeof req.body.baseDn !== 'string') return next(new HttpError(400, 'baseDn must be a string'));
if ('usernameField' in req.body && typeof req.body.usernameField !== 'string') return next(new HttpError(400, 'usernameField must be a string'));
if ('filter' in req.body && typeof req.body.filter !== 'string') return next(new HttpError(400, 'filter must be a string'));
if ('groupBaseDn' in req.body && typeof req.body.groupBaseDn !== 'string') return next(new HttpError(400, 'groupBaseDn must be a string'));
if ('bindDn' in req.body && typeof req.body.bindDn !== 'string') return next(new HttpError(400, 'bindDn must be a non empty string'));
if ('bindPassword' in req.body && typeof req.body.bindPassword !== 'string') return next(new HttpError(400, 'bindPassword must be a string'));
@@ -297,6 +234,27 @@ function setRegistryConfig(req, res, next) {
});
}
function getDirectoryConfig(req, res, next) {
settings.getDirectoryConfig(function (error, directoryConfig) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, directoryConfig));
});
}
function setDirectoryConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.lockUserProfiles !== 'boolean') return next(new HttpError(400, 'lockUserProfiles is required'));
if (typeof req.body.mandatory2FA !== 'boolean') return next(new HttpError(400, 'mandatory2FA is required'));
settings.setDirectoryConfig(req.body, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
});
}
function getSysinfoConfig(req, res, next) {
settings.getSysinfoConfig(function (error, sysinfoConfig) {
if (error) return next(BoxError.toHttpError(error));
@@ -332,12 +290,8 @@ function get(req, res, next) {
case settings.APP_AUTOUPDATE_PATTERN_KEY: return getAppAutoupdatePattern(req, res, next);
case settings.BOX_AUTOUPDATE_PATTERN_KEY: return getBoxAutoupdatePattern(req, res, next);
case settings.TIME_ZONE_KEY: return getTimeZone(req, res, next);
case settings.CLOUDRON_NAME_KEY: return getCloudronName(req, res, next);
case settings.FOOTER_KEY: return getFooter(req, res, next);
case settings.CLOUDRON_AVATAR_KEY: return getCloudronAvatar(req, res, next);
case settings.DIRECTORY_CONFIG_KEY: return getDirectoryConfig(req, res, next);
case settings.SUPPORT_CONFIG_KEY: return getSupportConfig(req, res, next);
default: return next(new HttpError(404, 'No such setting'));
@@ -349,7 +303,6 @@ function set(req, res, next) {
switch (req.params.setting) {
case settings.DYNAMIC_DNS_KEY: return setDynamicDnsConfig(req, res, next);
case settings.BACKUP_CONFIG_KEY: return setBackupConfig(req, res, next);
case settings.PLATFORM_CONFIG_KEY: return setPlatformConfig(req, res, next);
case settings.EXTERNAL_LDAP_KEY: return setExternalLdapConfig(req, res, next);
case settings.UNSTABLE_APPS_KEY: return setUnstableAppsConfig(req, res, next);
@@ -359,11 +312,8 @@ function set(req, res, next) {
case settings.APP_AUTOUPDATE_PATTERN_KEY: return setAppAutoupdatePattern(req, res, next);
case settings.BOX_AUTOUPDATE_PATTERN_KEY: return setBoxAutoupdatePattern(req, res, next);
case settings.TIME_ZONE_KEY: return setTimeZone(req, res, next);
case settings.CLOUDRON_NAME_KEY: return setCloudronName(req, res, next);
case settings.FOOTER_KEY: return setFooter(req, res, next);
case settings.CLOUDRON_AVATAR_KEY: return setCloudronAvatar(req, res, next);
case settings.DIRECTORY_CONFIG_KEY: return setDirectoryConfig(req, res, next);
default: return next(new HttpError(404, 'No such setting'));
}
+1 -1
View File
@@ -62,7 +62,7 @@ function setup(done) {
},
function createSettings(callback) {
settings.setBackupConfig({ provider: 'filesystem', backupFolder: '/tmp', format: 'tgz' }, callback);
settings.setBackupConfig({ provider: 'filesystem', backupFolder: '/tmp', format: 'tgz', retentionPolicy: { keepWithinSecs: 2 * 24 * 60 * 60 } }, callback);
}
], done);
}
+238
View File
@@ -0,0 +1,238 @@
'use strict';
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
var async = require('async'),
constants = require('../../constants.js'),
database = require('../../database.js'),
expect = require('expect.js'),
fs = require('fs'),
paths = require('../../paths.js'),
server = require('../../server.js'),
superagent = require('superagent');
var SERVER_URL = 'http://localhost:' + constants.PORT;
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
function setup(done) {
async.series([
server.start.bind(null),
database._clear.bind(null),
function createAdmin(callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
// stash token for further use
token = result.body.token;
callback();
});
}
], done);
}
function cleanup(done) {
database._clear(function (error) {
expect(!error).to.be.ok();
server.stop(done);
});
}
describe('Branding API', function () {
before(setup);
after(cleanup);
describe('cloudron_name', function () {
var name = 'foobar';
it('get default succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/branding/cloudron_name')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.name).to.be.ok();
done();
});
});
it('cannot set without name', function (done) {
superagent.post(SERVER_URL + '/api/v1/branding/cloudron_name')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('cannot set empty name', function (done) {
superagent.post(SERVER_URL + '/api/v1/branding/cloudron_name')
.query({ access_token: token })
.send({ name: '' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('set succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/branding/cloudron_name')
.query({ access_token: token })
.send({ name: name })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
done();
});
});
it('get succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/branding/cloudron_name')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.name).to.eql(name);
done();
});
});
});
describe('cloudron_avatar', function () {
it('get default succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/branding/cloudron_avatar')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body).to.be.a(Buffer);
done();
});
});
it('cannot set without data', function (done) {
superagent.post(SERVER_URL + '/api/v1/branding/cloudron_avatar')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('set succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/branding/cloudron_avatar')
.query({ access_token: token })
.attach('avatar', paths.CLOUDRON_DEFAULT_AVATAR_FILE)
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
done();
});
});
it('get succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/branding/cloudron_avatar')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.toString()).to.eql(fs.readFileSync(paths.CLOUDRON_DEFAULT_AVATAR_FILE, 'utf-8'));
done(err);
});
});
});
describe('appstore listing config', function () {
it('get default succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/branding/appstore_listing_config')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.whitelist).to.eql(null);
expect(res.body.blacklist).to.eql([]);
done();
});
});
it('cannot set with no bl or wl', function (done) {
superagent.post(SERVER_URL + '/api/v1/branding/appstore_listing_config')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('cannot set bad bl', function (done) {
superagent.post(SERVER_URL + '/api/v1/branding/appstore_listing_config')
.query({ access_token: token })
.send({ blacklist: [ 1 ] })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('cannot set bad wl', function (done) {
superagent.post(SERVER_URL + '/api/v1/branding/appstore_listing_config')
.query({ access_token: token })
.send({ whitelist: 4 })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('set bl succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/branding/appstore_listing_config')
.query({ access_token: token })
.send({ blacklist: [ 'id1', 'id2' ] })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
done();
});
});
it('get bl succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/branding/appstore_listing_config')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.blacklist).to.eql([ 'id1', 'id2' ]);
expect(res.body.whitelist).to.be(undefined);
done();
});
});
it('set wl succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/branding/appstore_listing_config')
.query({ access_token: token })
.send({ whitelist: [ 'id1', 'id2' ] })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
done();
});
});
it('get wl succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/branding/appstore_listing_config')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.whitelist).to.eql([ 'id1', 'id2' ]);
expect(res.body.blacklist).to.be(undefined);
done();
});
});
});
});
+2 -2
View File
@@ -32,7 +32,7 @@ function setup(done) {
server.start.bind(server),
database._clear,
settings._setApiServerOrigin.bind(null, 'http://localhost:6060'),
settings.setBackupConfig.bind(null, { provider: 'filesystem', backupFolder: '/tmp', format: 'tgz' })
settings.setBackupConfig.bind(null, { provider: 'filesystem', backupFolder: '/tmp', format: 'tgz', retentionPolicy: { keepWithinSecs: 10000 } })
], done);
}
@@ -44,7 +44,7 @@ function cleanup(done) {
});
}
describe('Cloudron', function () {
describe('Cloudron API', function () {
describe('activate', function () {
+2 -2
View File
@@ -86,7 +86,7 @@ describe('Groups API', function () {
it('create fails due to mising token', function (done) {
superagent.post(SERVER_URL + '/api/v1/groups')
.send({ name: GROUP_NAME})
.send({ name: GROUP_NAME })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
@@ -96,7 +96,7 @@ describe('Groups API', function () {
it('create succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/groups')
.query({ access_token: token })
.send({ name: GROUP_NAME})
.send({ name: GROUP_NAME })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
groupObject = result.body;
+56 -272
View File
@@ -63,8 +63,6 @@ function setup(done) {
.end(function (error, result) {
if (!result || result.statusCode !== 200) return retryCallback(new Error('Bad result'));
console.dir(result.body);
if (!result.body.setup.active && result.body.setup.errorMessage === '' && result.body.adminFqdn) return retryCallback();
retryCallback(new Error('Not done yet: ' + JSON.stringify(result.body)));
@@ -124,46 +122,6 @@ describe('Mail API', function () {
after(cleanup);
describe('crud', function () {
it('cannot add non-existing domain', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail')
.query({ access_token: token })
.send({ domain: 'doesnotexist.com' })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
it('domain must be a string', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail')
.query({ access_token: token })
.send({ domain: ['doesnotexist.com'] })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('can add domain', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail')
.query({ access_token: token })
.send({ domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
it('cannot add domain twice', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail')
.query({ access_token: token })
.send({ domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
done();
});
});
it('cannot get non-existing domain', function (done) {
superagent.get(SERVER_URL + '/api/v1/mail/doesnotexist.com')
.query({ access_token: token })
@@ -188,33 +146,6 @@ describe('Mail API', function () {
done();
});
});
it('cannot delete non-existing domain', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/doesnotexist.com')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
it('cannot delete admin mail domain', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + ADMIN_DOMAIN.domain)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
done();
});
});
it('can delete admin mail domain', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
});
describe('status', function () {
@@ -243,20 +174,13 @@ describe('Mail API', function () {
mxDomain = DOMAIN_0.domain;
dmarcDomain = '_dmarc.' + DOMAIN_0.domain;
superagent.post(SERVER_URL + '/api/v1/mail')
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/enable')
.query({ access_token: token })
.send({ domain: DOMAIN_0.domain })
.send({ enabled: true })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
expect(res.statusCode).to.equal(202);
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/enable')
.query({ access_token: token })
.send({ enabled: true })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
done();
});
done();
});
});
@@ -265,12 +189,7 @@ describe('Mail API', function () {
dns.resolve = resolve;
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
done();
});
it('does not fail when dns errors', function (done) {
@@ -379,7 +298,7 @@ describe('Mail API', function () {
it('succeeds with all different spf, dkim, dmarc, mx, ptr records', function (done) {
clearDnsAnswerQueue();
dnsAnswerQueue[mxDomain].MX = [ { priority: '20', exchange: settings.mailFqdn() }, { priority: '30', exchange: settings.mailFqdn() } ];
dnsAnswerQueue[mxDomain].MX = [ { priority: '20', exchange: settings.mailFqdn() }, { priority: '10', exchange: 'some.other.server' } ];
dnsAnswerQueue[dmarcDomain].TXT = [['v=DMARC2; p=reject; pct=100']];
dnsAnswerQueue[dkimDomain].TXT = [['v=DKIM2; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain)]];
dnsAnswerQueue[spfDomain].TXT = [['v=spf1 a:random.com ~all']];
@@ -396,7 +315,7 @@ describe('Mail API', function () {
expect(res.body.dns.dkim).to.be.an('object');
expect(res.body.dns.dkim.expected).to.eql('v=DKIM1; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain));
expect(res.body.dns.dkim.status).to.eql(false);
expect(res.body.dns.dkim.status).to.eql(true); // as long as p= matches we are good
expect(res.body.dns.dkim.value).to.eql('v=DKIM2; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain));
expect(res.body.dns.dmarc).to.be.an('object');
@@ -405,9 +324,9 @@ describe('Mail API', function () {
expect(res.body.dns.dmarc.value).to.eql('v=DMARC2; p=reject; pct=100');
expect(res.body.dns.mx).to.be.an('object');
expect(res.body.dns.mx.status).to.eql(false);
expect(res.body.dns.mx.status).to.eql(true);
expect(res.body.dns.mx.expected).to.eql('10 ' + settings.mailFqdn() + '.');
expect(res.body.dns.mx.value).to.eql('20 ' + settings.mailFqdn() + '. 30 ' + settings.mailFqdn() + '.');
expect(res.body.dns.mx.value).to.eql('20 ' + settings.mailFqdn() + '. 10 some.other.server.');
expect(res.body.dns.ptr).to.be.an('object');
expect(res.body.dns.ptr.expected).to.eql(settings.mailFqdn());
@@ -503,25 +422,6 @@ describe('Mail API', function () {
});
describe('mail from validation', function () {
before(function (done) {
superagent.post(SERVER_URL + '/api/v1/mail')
.query({ access_token: token })
.send({ domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
after(function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
it('get mail from validation succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
@@ -554,25 +454,6 @@ describe('Mail API', function () {
});
describe('catch_all', function () {
before(function (done) {
superagent.post(SERVER_URL + '/api/v1/mail')
.query({ access_token: token })
.send({ domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
after(function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
it('get catch_all succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
@@ -624,25 +505,6 @@ describe('Mail API', function () {
});
describe('mail relay', function () {
before(function (done) {
superagent.post(SERVER_URL + '/api/v1/mail')
.query({ access_token: token })
.send({ domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
after(function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
it('get mail relay succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
@@ -701,25 +563,6 @@ describe('Mail API', function () {
});
describe('mailboxes', function () {
before(function (done) {
superagent.post(SERVER_URL + '/api/v1/mail')
.query({ access_token: token })
.send({ domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
after(function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
it('add succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes')
.send({ name: MAILBOX_NAME, userId: userId })
@@ -757,7 +600,8 @@ describe('Mail API', function () {
expect(res.body.mailbox).to.be.an('object');
expect(res.body.mailbox.name).to.equal(MAILBOX_NAME);
expect(res.body.mailbox.ownerId).to.equal(userId);
expect(res.body.mailbox.aliasTarget).to.equal(null);
expect(res.body.mailbox.aliasName).to.equal(null);
expect(res.body.mailbox.aliasDomain).to.equal(null);
expect(res.body.mailbox.domain).to.equal(DOMAIN_0.domain);
done();
});
@@ -772,7 +616,8 @@ describe('Mail API', function () {
expect(res.body.mailboxes[0]).to.be.an('object');
expect(res.body.mailboxes[0].name).to.equal(MAILBOX_NAME);
expect(res.body.mailboxes[0].ownerId).to.equal(userId);
expect(res.body.mailboxes[0].aliasTarget).to.equal(null);
expect(res.body.mailboxes[0].aliasName).to.equal(null);
expect(res.body.mailboxes[0].aliasDomain).to.equal(null);
expect(res.body.mailboxes[0].domain).to.equal(DOMAIN_0.domain);
done();
});
@@ -803,69 +648,14 @@ describe('Mail API', function () {
});
describe('aliases', function () {
before(function (done) {
superagent.post(SERVER_URL + '/api/v1/mail')
.query({ access_token: token })
.send({ domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
after(function (done) {
mail.removeMailboxes(DOMAIN_0.domain, function (error) {
if (error) return done(error);
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
done();
});
});
it('set fails if aliases is missing', function (done) {
superagent.put(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/aliases/' + MAILBOX_NAME)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('set fails if user does not exist', function (done) {
superagent.put(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/aliases/' + 'someuserdoesnotexist')
.send({ aliases: ['hello', 'there'] })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
it('set fails if aliases is the wrong type', function (done) {
superagent.put(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/aliases/' + MAILBOX_NAME)
.send({ aliases: 'hello, there' })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('set fails if user is not enabled', function (done) {
superagent.put(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/aliases/' + MAILBOX_NAME)
.send({ aliases: ['hello', 'there'] })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
it('now add the mailbox', function (done) {
it('add the mailbox', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes')
.send({ name: MAILBOX_NAME, userId: userId })
.query({ access_token: token })
@@ -875,9 +665,38 @@ describe('Mail API', function () {
});
});
it('set fails if aliases is missing', function (done) {
superagent.put(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes/' + MAILBOX_NAME + '/aliases')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('set fails if user does not exist', function (done) {
superagent.put(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes/randomuser/aliases')
.send({ aliases: [{ name: 'hello', domain: DOMAIN_0.domain}, {name: 'there', domain: DOMAIN_0.domain}] })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
it('set fails if aliases is the wrong type', function (done) {
superagent.put(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes/' + MAILBOX_NAME + '/aliases')
.send({ aliases: 'hello, there' })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('set succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/aliases/' + MAILBOX_NAME)
.send({ aliases: ['hello', 'there'] })
superagent.put(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes/' + MAILBOX_NAME + '/aliases')
.send({ aliases: [{ name: 'hello', domain: DOMAIN_0.domain}, {name: 'there', domain: DOMAIN_0.domain}] })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
@@ -886,35 +705,17 @@ describe('Mail API', function () {
});
it('get succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/aliases/' + MAILBOX_NAME)
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes/' + MAILBOX_NAME + '/aliases')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.aliases).to.eql(['hello', 'there']);
done();
});
});
it('listing succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/aliases')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.aliases.length).to.eql(2);
expect(res.body.aliases[0].name).to.equal('hello');
expect(res.body.aliases[0].ownerId).to.equal(userId);
expect(res.body.aliases[0].aliasTarget).to.equal(MAILBOX_NAME);
expect(res.body.aliases[0].domain).to.equal(DOMAIN_0.domain);
expect(res.body.aliases[1].name).to.equal('there');
expect(res.body.aliases[1].ownerId).to.equal(userId);
expect(res.body.aliases[1].aliasTarget).to.equal(MAILBOX_NAME);
expect(res.body.aliases[1].domain).to.equal(DOMAIN_0.domain);
expect(res.body.aliases).to.eql([{ name: 'hello', domain: DOMAIN_0.domain}, {name: 'there', domain: DOMAIN_0.domain}]);
done();
});
});
it('get fails if mailbox does not exist', function (done) {
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/aliases/' + 'someuserdoesnotexist')
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes/somerandomuser/aliases')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
@@ -924,30 +725,11 @@ describe('Mail API', function () {
});
describe('mailinglists', function () {
before(function (done) {
async.series([
function (done) {
superagent.post(SERVER_URL + '/api/v1/mail')
.query({ access_token: token })
.send({ domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
}
], done);
});
after(function (done) {
mail.removeMailboxes(DOMAIN_0.domain, function (error) {
if (error) return done(error);
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
done();
});
});
@@ -982,7 +764,7 @@ describe('Mail API', function () {
it('add succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/lists')
.send({ name: LIST_NAME, members: [ `admin2@${DOMAIN_0.domain}`, `${USERNAME}@${DOMAIN_0.domain}`] })
.send({ name: LIST_NAME, members: [ `admin2@${DOMAIN_0.domain}`, `${USERNAME}@${DOMAIN_0.domain}`], membersOnly: false })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
@@ -992,7 +774,7 @@ describe('Mail API', function () {
it('add twice fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/lists')
.send({ name: LIST_NAME, members: [ `admin2@${DOMAIN_0.domain}`, `${USERNAME}@${DOMAIN_0.domain}`] })
.send({ name: LIST_NAME, members: [ `admin2@${DOMAIN_0.domain}`, `${USERNAME}@${DOMAIN_0.domain}`], membersOnly: false })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
@@ -1017,9 +799,10 @@ describe('Mail API', function () {
expect(res.body.list).to.be.an('object');
expect(res.body.list.name).to.equal(LIST_NAME);
expect(res.body.list.ownerId).to.equal('admin');
expect(res.body.list.aliasTarget).to.equal(null);
expect(res.body.list.aliasName).to.equal(null);
expect(res.body.list.domain).to.equal(DOMAIN_0.domain);
expect(res.body.list.members).to.eql([ `admin2@${DOMAIN_0.domain}`, `superadmin@${DOMAIN_0.domain}` ]);
expect(res.body.list.membersOnly).to.be(false);
done();
});
});
@@ -1033,9 +816,10 @@ describe('Mail API', function () {
expect(res.body.lists.length).to.equal(1);
expect(res.body.lists[0].name).to.equal(LIST_NAME);
expect(res.body.lists[0].ownerId).to.equal('admin');
expect(res.body.lists[0].aliasTarget).to.equal(null);
expect(res.body.lists[0].aliasName).to.equal(null);
expect(res.body.lists[0].domain).to.equal(DOMAIN_0.domain);
expect(res.body.lists[0].members).to.eql([ `admin2@${DOMAIN_0.domain}`, `superadmin@${DOMAIN_0.domain}` ]);
expect(res.body.lists[0].membersOnly).to.be(false);
done();
});
});

Some files were not shown because too many files have changed in this diff Show More