Compare commits

...

373 Commits

Author SHA1 Message Date
Girish Ramakrishnan dd44edde0a only clear backup cache if specific fields changed 2020-08-11 14:01:29 -07:00
Girish Ramakrishnan 885e90e810 add a todo 2020-08-11 12:57:37 -07:00
Girish Ramakrishnan 9cdf5dd0f3 backups: time the rotation and total as well 2020-08-11 10:28:11 -07:00
Girish Ramakrishnan df6e3eb1e6 Add deleteConcurrency setting 2020-08-11 09:14:09 -07:00
Girish Ramakrishnan 05026771e1 add memoryLimit, copyConcurrency, downloadConcurrency to backup config 2020-08-10 22:12:01 -07:00
Girish Ramakrishnan 7039108438 pass memory limit as argument to starttask.sh 2020-08-10 21:53:07 -07:00
Girish Ramakrishnan 02ee13cfb2 return empty array when listing 2020-08-10 21:32:54 -07:00
Girish Ramakrishnan 096e244252 Fix typo that causes aliases in lists to bounce
https://forum.cloudron.io/topic/2890/bug-with-mailing-lists-that-point-to-aliases
2020-08-10 17:49:27 -07:00
Girish Ramakrishnan bf5b7294a0 Add missing debugs 2020-08-10 14:54:37 -07:00
Girish Ramakrishnan a5da266643 groups: when listing, return members as well 2020-08-10 13:50:18 -07:00
Girish Ramakrishnan cf7bb49e15 More missing 5.5 changes 2020-08-10 10:16:09 -07:00
Girish Ramakrishnan 208b732bda yet more 5.5 changes 2020-08-10 10:07:50 -07:00
Girish Ramakrishnan c73d93b8bd more 5.5 changes 2020-08-10 10:05:47 -07:00
Girish Ramakrishnan 98a96eae2b Update mongodb
part of #725
2020-08-10 09:36:56 -07:00
Girish Ramakrishnan 2f9fe30c9d sftp: only mount data dirs that exist
when restoring, the platform starts first and the sftp container
goes and creates app data dirs with root permission. this prevents
the app restore logic from downloading the backup since it expects
yellowtent perm
2020-08-09 12:10:20 -07:00
Girish Ramakrishnan aeee8afc02 export database: fix async logic 2020-08-09 11:14:11 -07:00
Girish Ramakrishnan e85f0a4f52 Rename to box-task
this way we can do systemctl stop box*
2020-08-09 11:14:11 -07:00
Johannes Zellner da98649667 Ensure group listAllWitMembers also returns an ordered list 2020-08-09 11:34:53 +02:00
Girish Ramakrishnan 5ac08cc06b sftp: fix home directory path 2020-08-08 18:16:35 -07:00
Girish Ramakrishnan da72597dd3 Fix start/stop task scripts for ubuntu 16 2020-08-08 11:10:02 -07:00
Girish Ramakrishnan 1f1c94de70 Fix certificate ordering logic
* app certs set by user are always preferred
* If fallback, choose fallback certs. ignore others
* If LE, try to pick LE certs. Otherwise, provider fallback.

Fixes #724
2020-08-07 23:02:24 -07:00
Girish Ramakrishnan 60b3fceea6 reset-failed state of tasks during startup 2020-08-07 22:41:09 -07:00
Girish Ramakrishnan 5073809486 More 5.5.0 changes 2020-08-07 22:20:20 -07:00
Girish Ramakrishnan debd779cfd new public gpg key that doesn't expire
gpg --export admin@cloudron.io > releases.gpg
2020-08-07 22:17:30 -07:00
Girish Ramakrishnan 6b9454100e certs: remove caas backend 2020-08-07 17:58:27 -07:00
Girish Ramakrishnan 779ad24542 domains: remove caas backend, it is unused 2020-08-07 17:57:48 -07:00
Girish Ramakrishnan b94dbf5fa3 remove restricted fallback cert
this feature was never used. iirc, it was for managed hosting
2020-08-07 17:57:25 -07:00
Girish Ramakrishnan 45c49c9757 route53: verifyDnsConfig lists zones using old API
It should be using the listHostedZonesByName API but it was using the old
API (which has a 100 zone limitation) because it was using old credentials.
2020-08-07 09:54:02 -07:00
Girish Ramakrishnan 91288c96b1 s3: set queue size to 3
fixes #691
2020-08-07 00:28:00 -07:00
Girish Ramakrishnan f8e22a0730 Fix tests 2020-08-07 00:21:15 -07:00
Girish Ramakrishnan 114b45882a Set memory limit to 400M for tasks 2020-08-07 00:21:15 -07:00
Girish Ramakrishnan b1b6f70118 Kill all tasks on shutdown and startup
BindsTo will kill all the tasks when systemctl stop box is executed.
But when restarted, it keeps the tasks running. Because of this behavior,
we kill the tasks on startup and stop of the box code.
2020-08-06 23:47:40 -07:00
Girish Ramakrishnan 648d42dfe4 Empty debug prints as undefined for some reason 2020-08-06 23:23:56 -07:00
Girish Ramakrishnan 99f989c384 run apptask and backup task with a nice
A child process inherits whatever nice value is held by the parent at the time that it is forked
2020-08-06 16:46:39 -07:00
Girish Ramakrishnan 2112c7d096 sudo: remove the nice support 2020-08-06 16:44:35 -07:00
Girish Ramakrishnan ac63d00c93 run tasks as separate cgroup via systemd
this allows us to adjust the nice value and memory settings per task

part of #691
2020-08-06 16:43:14 -07:00
Girish Ramakrishnan e04871f79f pass log file as argument to task worker
initially, i thought i can hardcode the log file into taskworker.js
depending on the task type but for apptask, it's not easy to get the
appId from the taskId unless we introspect task arguments as well.
it's easier for now to pass it as an argument.
2020-08-05 00:46:34 -07:00
Girish Ramakrishnan 182c162dc4 hardcode logging of box code to box.log 2020-08-04 13:30:18 -07:00
Johannes Zellner 822b38cc89 Fallback to NOOP callback if not supplied 2020-08-04 14:32:01 +02:00
Girish Ramakrishnan d564003c87 backup cleaner: referenced backups must be counted as part of period
otherwise, we end up in a state where box backups keeps referencing
app backups and app backup cleanup is only performed on the remaining
app backups.
2020-08-03 21:22:27 -07:00
Girish Ramakrishnan 1b307632ab Use debug instead of console.* everywhere
No need to patch up console.* anymore

also removes supererror
2020-08-02 12:04:55 -07:00
Girish Ramakrishnan aa747cea85 update postgresl for pg_stat_statements,plpgsql extensions (loomio) 2020-08-02 11:36:42 -07:00
Girish Ramakrishnan f4a322478d cloudron.target is not needed 2020-08-01 20:00:20 -07:00
Girish Ramakrishnan d2882433a5 run backup uploader with a nice of 15
the gzip takes a lot of cpu processing and hogs the CPU. With a nice
level, we give other things higher priority.

An alternate idea that was explored was to use cpulimit. This is to
send SIGSTOP and SIGCONT periodically but this will not make use of the
CPU if it's idle (unlike nice).

Another idea is to use cgroups, but it's not clear how to use it with
the dynamic setup we have.

part of #691
2020-07-31 18:23:36 -07:00
Girish Ramakrishnan a94b175805 Add timing information for backups 2020-07-31 12:59:15 -07:00
Girish Ramakrishnan 37d81da806 do system checks once a day 2020-07-31 11:20:17 -07:00
Girish Ramakrishnan d089444441 db upgrade: stop containers only after exporting
we cannot export if the containers were nuked in the platform logic.
for this reason, move the removal near the place where they get started.
2020-07-30 15:28:53 -07:00
Girish Ramakrishnan b0d65a1bae rename startApps to markApps 2020-07-30 15:28:50 -07:00
Girish Ramakrishnan 16288cf277 better debug 2020-07-30 11:42:03 -07:00
Girish Ramakrishnan 7ddbabf781 Make the error message clearer 2020-07-30 11:29:43 -07:00
Girish Ramakrishnan fe35f4497b Fix two typos 2020-07-30 10:58:24 -07:00
Girish Ramakrishnan 625463f6ab export the database before upgrade
it's possible that
a) backups are completely disabled
b) skip backup option is selected when upgrading

in the above cases, the dump file is not generated and thus any addon
upgrade will fail. to fix, we dump the db fresh for database upgrades.
2020-07-30 10:23:08 -07:00
Johannes Zellner ff632b6816 Add more external ldap tests 2020-07-30 15:22:03 +02:00
Johannes Zellner fbc666f178 Make externalldap sync more robust 2020-07-30 15:08:01 +02:00
Girish Ramakrishnan d89bbdd50c Update to PostgreSQL 11 2020-07-29 21:54:05 -07:00
Girish Ramakrishnan 96f9aa39b2 add note on why we check for app updates separately 2020-07-29 20:27:06 -07:00
Girish Ramakrishnan 7330814d0f More 5.5 changes 2020-07-29 16:11:09 -07:00
Johannes Zellner 312efdcd94 Fix debug message 2020-07-29 20:38:46 +02:00
Girish Ramakrishnan 5db78ae359 Fix more usages of backup.intervalSecs 2020-07-29 11:25:59 -07:00
Girish Ramakrishnan 97967e60e8 remove yahoo from smtp test list 2020-07-29 11:25:59 -07:00
Johannes Zellner 9106b5d182 Avoid using extra /data dir for filemanager 2020-07-29 20:14:14 +02:00
Johannes Zellner 74bdb6cb9d Only mount app data volumes if localstorage is used 2020-07-29 19:58:41 +02:00
Johannes Zellner 0a44d426fa Explicitly mount all apps into the sftp container 2020-07-29 19:47:37 +02:00
Johannes Zellner e1718c4e8d If app.dataDir is set, first unmount from sftp before deleting on uninstall 2020-07-29 17:54:32 +02:00
Girish Ramakrishnan f511a610b5 backups: take a pattern instead of interval secs
part of #699
2020-07-28 21:54:56 -07:00
Girish Ramakrishnan 4d5715188d Increase invite link expiry to a week 2020-07-28 14:19:19 -07:00
Johannes Zellner 2ea21be5bd Add basic backup check route tests 2020-07-28 17:23:21 +02:00
Johannes Zellner 5bb0419699 Add backup check route
Part of #719
2020-07-28 17:18:50 +02:00
Johannes Zellner a8131eed71 Run initial backup configuration check only after activation
Part of #719
2020-07-28 17:12:38 +02:00
Girish Ramakrishnan ed09c06ba4 Add option to remove mailbox data
Fixes #720
2020-07-27 22:55:09 -07:00
Girish Ramakrishnan 3c59a0ff31 make it clear it is exported for testing 2020-07-27 22:07:25 -07:00
Girish Ramakrishnan a6d24b3e48 postgresql: add btree_gist,postgres_fdw extensions for gitlab 2020-07-24 22:30:45 -07:00
Girish Ramakrishnan 060135eecb Next release is 5.5 2020-07-24 09:33:53 -07:00
Johannes Zellner ef296c24fe Mount data custom app data location specifically into sftp addon
Fixes #722
2020-07-24 15:43:26 +02:00
Girish Ramakrishnan 707aaf25ec Add note on underscore in usernames 2020-07-23 16:29:54 -07:00
Girish Ramakrishnan 7edeb0c358 nginx displays version in stderr 2020-07-22 17:57:55 -07:00
Girish Ramakrishnan e516af14b2 typo 2020-07-22 17:53:04 -07:00
Girish Ramakrishnan 4086f2671d Disable ldap/directory config/2fa in demo mode 2020-07-22 16:18:22 -07:00
Girish Ramakrishnan 23c4550430 Update postgresql addon to have citext extension for loomio 2020-07-22 08:29:44 -07:00
Johannes Zellner 31d25cd6be Add 5.4.1 changes 2020-07-19 21:11:05 +02:00
Johannes Zellner 07b3c7a245 Use sftp addon with fixed symlinks 2020-07-18 19:27:02 +02:00
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
146 changed files with 4987 additions and 2813 deletions
+156
View File
@@ -1909,3 +1909,159 @@
* Add ECDHE-RSA-AES128-SHA256 to cipher list * Add ECDHE-RSA-AES128-SHA256 to cipher list
* Fix GPG signature verification * 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
[5.4.1]
* 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
[5.5.0]
* postgresql: update to PostgreSQL 11
* postgresql: add citext extension to whitelist for loomio
* postgresql: add btree_gist,postgres_fdw,pg_stat_statements,plpgsql extensions for gitlab
* SFTP/Filebrowser: fix access of external data directories
* Fix contrast issues in dark mode
* Add option to delete mailbox data when mailbox is delete
* Allow days/hours of backups and updates to be configurable
* backup cleaner: fix issue where referenced backups where not counted against time periods
* route53: fix issue where verification failed if user had more than 100 zones
* rework task workers to run them in a separate cgroup
* backups: now much faster thanks to reworking of task worker
* When custom fallback cert is set, make sure it's used over LE certs
* mongodb: update to MongoDB 4.0.19
* List groups ordered by name
* Invite links are now valid for a week
* Update release GPG key
* Add pre-defined variables ($CLOUDRON_APPID) for better post install messages
* filemanager: show folder first
+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 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. clone this repo and npm install and expect something to work.
## Documentation ## Support
* [Documentation](https://cloudron.io/documentation/) * [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/) * [Forum](https://forum.cloudron.io/)
* [Support](mailto:support@cloudron.io)
+10 -15
View File
@@ -4,8 +4,7 @@ set -euv -o pipefail
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly arg_provider="${1:-generic}" readonly arg_infraversionpath="${SOURCE_DIR}/../src"
readonly arg_infraversionpath="${SOURCE_DIR}/${2:-}"
function die { function die {
echo $1 echo $1
@@ -14,6 +13,9 @@ function die {
export DEBIAN_FRONTEND=noninteractive 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 # hold grub since updating it breaks on some VPS providers. also, dist-upgrade will trigger it
apt-mark hold grub* >/dev/null apt-mark hold grub* >/dev/null
apt-get -o Dpkg::Options::="--force-confdef" update -y 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) # 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 # 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") gpg_package=$([[ "${ubuntu_version}" == "16.04" ]] && echo "gnupg" || echo "gpg")
apt-get -y install \ apt-get -y install \
acl \ acl \
@@ -53,16 +53,11 @@ apt-get -y install \
unbound \ unbound \
xfsprogs xfsprogs
if [[ "${ubuntu_version}" == "16.04" ]]; then echo "==> installing nginx for xenial for TLSv3 support"
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)
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.14.0-1~xenial_amd64.deb -o /tmp/nginx.deb apt install -y /tmp/nginx.deb
# apt install with install deps (as opposed to dpkg -i) rm /tmp/nginx.deb
apt install -y /tmp/nginx.deb
rm /tmp/nginx.deb
else
apt install -y nginx-full
fi
# on some providers like scaleway the sudo file is changed and we want to keep the old one # 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 apt-get -o Dpkg::Options::="--force-confold" install -y sudo
@@ -73,7 +68,7 @@ cp /usr/share/unattended-upgrades/20auto-upgrades /etc/apt/apt.conf.d/20auto-upg
echo "==> Installing node.js" echo "==> Installing node.js"
mkdir -p /usr/local/node-10.18.1 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/node /usr/bin/node
ln -sf /usr/local/node-10.18.1/bin/npm /usr/bin/npm 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 apt-get install -y python # Install python which is required for npm rebuild
+43 -40
View File
@@ -2,57 +2,60 @@
'use strict'; 'use strict';
// prefix all output with a timestamp
// debug() already prefixes and uses process.stderr NOT console.*
['log', 'info', 'warn', 'debug', 'error'].forEach(function (log) {
var orig = console[log];
console[log] = function () {
orig.apply(console, [new Date().toISOString()].concat(Array.prototype.slice.call(arguments)));
};
});
require('supererror')({ splatchError: true });
let async = require('async'), let async = require('async'),
constants = require('./src/constants.js'),
dockerProxy = require('./src/dockerproxy.js'), dockerProxy = require('./src/dockerproxy.js'),
fs = require('fs'),
ldap = require('./src/ldap.js'), ldap = require('./src/ldap.js'),
server = require('./src/server.js'); paths = require('./src/paths.js'),
server = require('./src/server.js'),
util = require('util');
console.log(); const NOOP_CALLBACK = function () { };
console.log('==========================================');
console.log(` Cloudron ${constants.VERSION} `); function setupLogging(callback) {
console.log('=========================================='); if (process.env.BOX_ENV === 'test') return callback();
console.log();
fs.open(paths.BOX_LOG_FILE, 'a', function (error, fd) {
if (error) return callback(error);
require('debug').log = function (...args) {
fs.appendFileSync(fd, util.format(...args) + '\n');
};
callback();
});
}
async.series([ async.series([
setupLogging,
server.start, server.start,
ldap.start, ldap.start,
dockerProxy.start dockerProxy.start
], function (error) { ], function (error) {
if (error) { if (error) {
console.error('Error starting server', error); console.log('Error starting server', error);
process.exit(1); process.exit(1);
} }
console.log('Cloudron is up and running');
}); const debug = require('debug')('box:box'); // require this here so that logging handler is already setup
var NOOP_CALLBACK = function () { }; process.on('SIGINT', function () {
debug('Received SIGINT. Shutting down.');
process.on('SIGINT', function () {
console.log('Received SIGINT. Shutting down.'); server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
server.stop(NOOP_CALLBACK); dockerProxy.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK); setTimeout(process.exit.bind(process), 3000);
dockerProxy.stop(NOOP_CALLBACK); });
setTimeout(process.exit.bind(process), 3000);
}); process.on('SIGTERM', function () {
debug('Received SIGTERM. Shutting down.');
process.on('SIGTERM', function () {
console.log('Received SIGTERM. Shutting down.'); server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
server.stop(NOOP_CALLBACK); dockerProxy.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK); setTimeout(process.exit.bind(process), 3000);
dockerProxy.stop(NOOP_CALLBACK); });
setTimeout(process.exit.bind(process), 3000);
console.log(`Cloudron is up and running. Logs are at ${paths.BOX_LOG_FILE}`); // this goes to journalctl
}); });
@@ -12,8 +12,6 @@ exports.up = function(db, callback) {
db.all('SELECT * FROM users WHERE admin=1', function (error, results) { db.all('SELECT * FROM users WHERE admin=1', function (error, results) {
if (error) return done(error); if (error) return done(error);
console.dir(results);
async.eachSeries(results, function (r, next) { async.eachSeries(results, function (r, next) {
db.runSql('INSERT INTO groupMembers (groupId, userId) VALUES (?, ?)', [ ADMIN_GROUP_ID, r.id ], next); db.runSql('INSERT INTO groupMembers (groupId, userId) VALUES (?, ?)', [ ADMIN_GROUP_ID, r.id ], next);
}, done); }, done);
@@ -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);
});
};
@@ -0,0 +1,29 @@
'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.intervalSecs === 6 * 60 * 60) { // every 6 hours
backupConfig.schedulePattern = '00 00 5,11,17,23 * * *';
} else if (backupConfig.intervalSecs === 12 * 60 * 60) { // every 12 hours
backupConfig.schedulePattern = '00 00 5,17 * * *';
} else if (backupConfig.intervalSecs === 24 * 60 * 60) { // every day
backupConfig.schedulePattern = '00 00 23 * * *';
} else if (backupConfig.intervalSecs === 3 * 24 * 60 * 60) { // every 3 days (based on day)
backupConfig.schedulePattern = '00 00 23 * * 1,3,5';
} else if (backupConfig.intervalSecs === 7 * 24 * 60 * 60) { // every week (saturday)
backupConfig.schedulePattern = '00 00 23 * * 6';
} else { // default to everyday
backupConfig.schedulePattern = '00 00 23 * * *';
}
delete backupConfig.intervalSecs;
db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [ JSON.stringify(backupConfig) ], callback);
});
};
exports.down = function(db, callback) {
callback();
};
+13 -5
View File
@@ -21,7 +21,7 @@ CREATE TABLE IF NOT EXISTS users(
password VARCHAR(1024) NOT NULL, password VARCHAR(1024) NOT NULL,
salt VARCHAR(512) NOT NULL, salt VARCHAR(512) NOT NULL,
createdAt 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 "", displayName VARCHAR(512) DEFAULT "",
fallbackEmail VARCHAR(512) DEFAULT "", fallbackEmail VARCHAR(512) DEFAULT "",
twoFactorAuthenticationSecret VARCHAR(128) DEFAULT "", twoFactorAuthenticationSecret VARCHAR(128) DEFAULT "",
@@ -37,6 +37,7 @@ CREATE TABLE IF NOT EXISTS users(
CREATE TABLE IF NOT EXISTS userGroups( CREATE TABLE IF NOT EXISTS userGroups(
id VARCHAR(128) NOT NULL UNIQUE, id VARCHAR(128) NOT NULL UNIQUE,
name VARCHAR(254) NOT NULL UNIQUE, name VARCHAR(254) NOT NULL UNIQUE,
source VARCHAR(128) DEFAULT "",
PRIMARY KEY(id)); PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS groupMembers( CREATE TABLE IF NOT EXISTS groupMembers(
@@ -86,6 +87,8 @@ CREATE TABLE IF NOT EXISTS apps(
dataDir VARCHAR(256) UNIQUE, dataDir VARCHAR(256) UNIQUE,
taskId INTEGER, // current task taskId INTEGER, // current task
errorJson TEXT, errorJson TEXT,
bindsJson TEXT, // bind mounts
servicesConfigJson TEXT, // app services configuration
FOREIGN KEY(mailboxDomain) REFERENCES domains(domain), FOREIGN KEY(mailboxDomain) REFERENCES domains(domain),
FOREIGN KEY(taskId) REFERENCES tasks(id), FOREIGN KEY(taskId) REFERENCES tasks(id),
@@ -120,8 +123,10 @@ CREATE TABLE IF NOT EXISTS appEnvVars(
CREATE TABLE IF NOT EXISTS backups( CREATE TABLE IF NOT EXISTS backups(
id VARCHAR(128) NOT NULL, id VARCHAR(128) NOT NULL,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 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' */ 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 */ dependsOn TEXT, /* comma separate list of objects this backup depends on */
state VARCHAR(16) NOT NULL, state VARCHAR(16) NOT NULL,
manifestJson TEXT, /* to validate if the app can be installed in this version of box */ manifestJson TEXT, /* to validate if the app can be installed in this version of box */
@@ -177,12 +182,15 @@ CREATE TABLE IF NOT EXISTS mailboxes(
name VARCHAR(128) NOT NULL, name VARCHAR(128) NOT NULL,
type VARCHAR(16) NOT NULL, /* 'mailbox', 'alias', 'list' */ type VARCHAR(16) NOT NULL, /* 'mailbox', 'alias', 'list' */
ownerId VARCHAR(128) NOT NULL, /* user id */ 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 */ membersJson TEXT, /* members of a group. fully qualified */
membersOnly BOOLEAN DEFAULT false,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
domain VARCHAR(128), domain VARCHAR(128),
FOREIGN KEY(domain) REFERENCES mail(domain), FOREIGN KEY(domain) REFERENCES mail(domain),
FOREIGN KEY(aliasDomain) REFERENCES mail(domain),
UNIQUE (name, domain)); UNIQUE (name, domain));
CREATE TABLE IF NOT EXISTS subdomains( CREATE TABLE IF NOT EXISTS subdomains(
@@ -214,7 +222,7 @@ CREATE TABLE IF NOT EXISTS notifications(
message TEXT, message TEXT,
acknowledged BOOLEAN DEFAULT false, acknowledged BOOLEAN DEFAULT false,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY appPasswords_name_appId_identifier (name, userId, identifier),
PRIMARY KEY (id) PRIMARY KEY (id)
); );
@@ -226,7 +234,7 @@ CREATE TABLE IF NOT EXISTS appPasswords(
hashedPassword VARCHAR(1024) NOT NULL, hashedPassword VARCHAR(1024) NOT NULL,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(userId) REFERENCES users(id), FOREIGN KEY(userId) REFERENCES users(id),
UNIQUE (name, userId),
PRIMARY KEY (id) PRIMARY KEY (id)
); );
+360 -326
View File
File diff suppressed because it is too large Load Diff
+21 -21
View File
@@ -18,32 +18,33 @@
"@google-cloud/storage": "^2.5.0", "@google-cloud/storage": "^2.5.0",
"@sindresorhus/df": "git+https://github.com/cloudron-io/df.git#type", "@sindresorhus/df": "git+https://github.com/cloudron-io/df.git#type",
"async": "^2.6.3", "async": "^2.6.3",
"aws-sdk": "^2.610.0", "aws-sdk": "^2.685.0",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"cloudron-manifestformat": "^5.1.1", "cloudron-manifestformat": "^5.5.0",
"connect": "^3.7.0", "connect": "^3.7.0",
"connect-lastmile": "^1.2.2", "connect-lastmile": "^2.0.0",
"connect-timeout": "^1.9.0", "connect-timeout": "^1.9.0",
"cookie-session": "^1.4.0", "cookie-session": "^1.4.0",
"cron": "^1.8.2", "cron": "^1.8.2",
"db-migrate": "^0.11.6", "db-migrate": "^0.11.11",
"db-migrate-mysql": "^1.1.10", "db-migrate-mysql": "^1.1.10",
"debug": "^4.1.1", "debug": "^4.1.1",
"dockerode": "^2.5.8", "dockerode": "^2.5.8",
"ejs": "^2.6.1", "ejs": "^2.6.1",
"ejs-cli": "^2.1.1", "ejs-cli": "^2.2.0",
"express": "^4.17.1", "express": "^4.17.1",
"js-yaml": "^3.13.1", "js-yaml": "^3.14.0",
"json": "^9.0.6", "json": "^9.0.6",
"ldapjs": "^1.0.2", "ldapjs": "^1.0.2",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"lodash.chunk": "^4.2.0", "lodash.chunk": "^4.2.0",
"mime": "^2.4.4", "mime": "^2.4.6",
"moment-timezone": "^0.5.27", "moment": "^2.26.0",
"morgan": "^1.9.1", "moment-timezone": "^0.5.31",
"morgan": "^1.10.0",
"multiparty": "^4.2.1", "multiparty": "^4.2.1",
"mysql": "^2.18.1", "mysql": "^2.18.1",
"nodemailer": "^6.4.2", "nodemailer": "^6.4.6",
"nodemailer-smtp-transport": "^2.7.4", "nodemailer-smtp-transport": "^2.7.4",
"once": "^1.4.0", "once": "^1.4.0",
"parse-links": "^0.1.0", "parse-links": "^0.1.0",
@@ -51,34 +52,33 @@
"progress-stream": "^2.0.0", "progress-stream": "^2.0.0",
"proxy-middleware": "^0.15.0", "proxy-middleware": "^0.15.0",
"qrcode": "^1.4.4", "qrcode": "^1.4.4",
"readdirp": "^3.3.0", "readdirp": "^3.4.0",
"request": "^2.88.0", "request": "^2.88.2",
"rimraf": "^2.6.3", "rimraf": "^2.6.3",
"s3-block-read-stream": "^0.5.0", "s3-block-read-stream": "^0.5.0",
"safetydance": "^1.0.0", "safetydance": "^1.1.1",
"semver": "^6.1.1", "semver": "^6.1.1",
"showdown": "^1.9.1", "showdown": "^1.9.1",
"speakeasy": "^2.0.0", "speakeasy": "^2.0.0",
"split": "^1.0.1", "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-fs": "github:cloudron-io/tar-fs#ignore_stat_error",
"tar-stream": "^2.1.0", "tar-stream": "^2.1.2",
"tldjs": "^2.3.1", "tldjs": "^2.3.1",
"underscore": "^1.9.2", "underscore": "^1.10.2",
"uuid": "^3.4.0", "uuid": "^3.4.0",
"validator": "^11.0.0", "validator": "^11.0.0",
"ws": "^7.2.1", "ws": "^7.3.0",
"xml2js": "^0.4.23" "xml2js": "^0.4.23"
}, },
"devDependencies": { "devDependencies": {
"expect.js": "*", "expect.js": "*",
"hock": "^1.3.3", "hock": "^1.4.1",
"js2xmlparser": "^4.0.0", "js2xmlparser": "^4.0.1",
"mocha": "^6.1.4", "mocha": "^6.1.4",
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git", "mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
"nock": "^10.0.6", "nock": "^10.0.6",
"node-sass": "^4.12.0", "node-sass": "^4.14.1",
"recursive-readdir": "^2.2.2" "recursive-readdir": "^2.2.2"
}, },
"scripts": { "scripts": {
+7 -59
View File
@@ -41,16 +41,14 @@ if systemctl -q is-active box; then
fi fi
initBaseImage="true" initBaseImage="true"
# provisioning data provider="generic"
provider=""
requestedVersion="" requestedVersion=""
apiServerOrigin="https://api.cloudron.io" apiServerOrigin="https://api.cloudron.io"
webServerOrigin="https://cloudron.io" webServerOrigin="https://cloudron.io"
sourceTarballUrl="" sourceTarballUrl=""
rebootServer="true" 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}" eval set -- "${args}"
while true; do while true; do
@@ -67,7 +65,6 @@ while true; do
webServerOrigin="https://staging.cloudron.io" webServerOrigin="https://staging.cloudron.io"
fi fi
shift 2;; shift 2;;
--license) license="$2"; shift 2;;
--skip-baseimage-init) initBaseImage="false"; shift;; --skip-baseimage-init) initBaseImage="false"; shift;;
--skip-reboot) rebootServer="false"; shift;; --skip-reboot) rebootServer="false"; shift;;
--) break;; --) break;;
@@ -91,48 +88,6 @@ fi
# Can only write after we have confirmed script has root access # Can only write after we have confirmed script has root access
echo "Running cloudron-setup with args : $@" > "${LOG_FILE}" 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 "##############################################" echo "##############################################"
echo " Cloudron Setup (${requestedVersion:-latest})" echo " Cloudron Setup (${requestedVersion:-latest})"
@@ -151,12 +106,6 @@ if [[ "${initBaseImage}" == "true" ]]; then
exit 1 exit 1
fi 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" echo "=> Updating apt and installing script dependencies"
if ! apt-get update &>> "${LOG_FILE}"; then if ! apt-get update &>> "${LOG_FILE}"; then
echo "Could not update package repositories. See ${LOG_FILE}" echo "Could not update package repositories. See ${LOG_FILE}"
@@ -196,20 +145,19 @@ fi
if [[ "${initBaseImage}" == "true" ]]; then if [[ "${initBaseImage}" == "true" ]]; then
echo -n "=> Installing base dependencies and downloading docker images (this takes some time) ..." 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" echo "Init script failed. See ${LOG_FILE} for details"
exit 1 exit 1
fi fi
echo "" echo ""
fi 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) ..." echo "=> Installing version ${version} (this takes some time) ..."
mkdir -p /etc/cloudron mkdir -p /etc/cloudron
echo "${provider}" > /etc/cloudron/PROVIDER 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 if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" &>> "${LOG_FILE}"; then
echo "Failed to install cloudron. See ${LOG_FILE} for details" echo "Failed to install cloudron. See ${LOG_FILE} for details"
exit 1 exit 1
@@ -221,13 +169,13 @@ mysql -uroot -ppassword -e "REPLACE INTO box.settings (name, value) VALUES ('web
echo -n "=> Waiting for cloudron to be ready (this takes some time) ..." echo -n "=> Waiting for cloudron to be ready (this takes some time) ..."
while true; do while true; do
echo -n "." 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 break # we are up and running
fi fi
sleep 10 sleep 10
done done
if ! 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 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>' ip='<IP>'
fi fi
echo -e "\n\n${GREEN}Visit https://${ip} and accept the self-signed certificate to finish setup.${DONE}\n" echo -e "\n\n${GREEN}Visit https://${ip} and accept the self-signed certificate to finish setup.${DONE}\n"
+2 -2
View File
@@ -94,7 +94,7 @@ echo -e $LINE"Backup stats (possibly misleading)"$LINE >> $OUT
du -hcsL /var/backups/* &>> $OUT || true du -hcsL /var/backups/* &>> $OUT || true
echo -e $LINE"System daemon status"$LINE >> $OUT echo -e $LINE"System daemon status"$LINE >> $OUT
systemctl status --lines=100 cloudron.target box mysql unbound cloudron-syslog nginx collectd docker &>> $OUT systemctl status --lines=100 box mysql unbound cloudron-syslog nginx collectd docker &>> $OUT
echo -e $LINE"Box logs"$LINE >> $OUT echo -e $LINE"Box logs"$LINE >> $OUT
tail -n 100 /home/yellowtent/platformdata/logs/box.log &>> $OUT tail -n 100 /home/yellowtent/platformdata/logs/box.log &>> $OUT
@@ -112,7 +112,7 @@ if [[ "${enableSSH}" == "true" ]]; then
permit_root_login=$(grep -q ^PermitRootLogin.*yes /etc/ssh/sshd_config && echo "yes" || echo "no") permit_root_login=$(grep -q ^PermitRootLogin.*yes /etc/ssh/sshd_config && echo "yes" || echo "no")
# support.js uses similar logic # support.js uses similar logic
if $(grep -q "ec2\|lightsail\|ami" /etc/cloudron/PROVIDER); then if [[ -d /home/ubuntu ]]; then
ssh_user="ubuntu" ssh_user="ubuntu"
keys_file="/home/ubuntu/.ssh/authorized_keys" keys_file="/home/ubuntu/.ssh/authorized_keys"
else else
+16 -15
View File
@@ -11,9 +11,8 @@ if [[ ${EUID} -ne 0 ]]; then
exit 1 exit 1
fi fi
readonly USER=yellowtent readonly user=yellowtent
readonly BOX_SRC_DIR=/home/${USER}/box readonly box_src_dir=/home/${user}/box
readonly BASE_DATA_DIR=/home/${USER}
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400" readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 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") 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" echo "==> installer: updating docker"
if [[ $(docker version --format {{.Client.Version}}) != "18.09.2" ]]; then if [[ $(docker version --format {{.Client.Version}}) != "18.09.2" ]]; then
@@ -56,10 +57,10 @@ if [[ $(docker version --format {{.Client.Version}}) != "18.09.2" ]]; then
rm /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb rm /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb
fi fi
readonly nginx_version=$(nginx -v) readonly nginx_version=$(nginx -v 2>&1)
if [[ "${nginx_version}" != *"1.14."* && "${ubuntu_version}" == "16.04" ]]; then if [[ "${nginx_version}" != *"1.18."* ]]; then
echo "==> installer: installing nginx for xenial for TLSv3 support" echo "==> installer: installing nginx 1.18"
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.14.0-1~xenial_amd64.deb -o /tmp/nginx.deb 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 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 apt install -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" --force-yes /tmp/nginx.deb
rm /tmp/nginx.deb rm /tmp/nginx.deb
@@ -118,22 +119,22 @@ while [[ ! -f "${CLOUDRON_SYSLOG}" || "$(${CLOUDRON_SYSLOG} --version)" != ${CLO
sleep 5 sleep 5
done done
if ! id "${USER}" 2>/dev/null; then if ! id "${user}" 2>/dev/null; then
useradd "${USER}" -m useradd "${user}" -m
fi fi
if [[ "${is_update}" == "yes" ]]; then if [[ "${is_update}" == "yes" ]]; then
echo "==> installer: stop cloudron.target service for update" echo "==> installer: stop box service for update"
${BOX_SRC_DIR}/setup/stop.sh ${box_src_dir}/setup/stop.sh
fi fi
# ensure we are not inside the source directory, which we will remove now # ensure we are not inside the source directory, which we will remove now
cd /root cd /root
echo "==> installer: switching the box code" echo "==> installer: switching the box code"
rm -rf "${BOX_SRC_DIR}" rm -rf "${box_src_dir}"
mv "${box_src_tmp_dir}" "${BOX_SRC_DIR}" mv "${box_src_tmp_dir}" "${box_src_dir}"
chown -R "${USER}:${USER}" "${BOX_SRC_DIR}" chown -R "${user}:${user}" "${box_src_dir}"
echo "==> installer: calling box setup script" echo "==> installer: calling box setup script"
"${BOX_SRC_DIR}/setup/start.sh" "${box_src_dir}/setup/start.sh"
+26 -4
View File
@@ -20,6 +20,11 @@ readonly ubuntu_version=$(lsb_release -rs)
cp -f "${script_dir}/../scripts/cloudron-support" /usr/bin/cloudron-support 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" echo "==> Configuring docker"
cp "${script_dir}/start/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app cp "${script_dir}/start/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
systemctl enable apparmor systemctl enable apparmor
@@ -80,6 +85,9 @@ systemctl daemon-reload
systemctl restart systemd-journald systemctl restart systemd-journald
setfacl -n -m u:${USER}:r /var/log/journal/*/system.journal 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" echo "==> Setting up unbound"
# DO uses Google nameservers by default. This causes RBL queries to fail (host 2.0.0.127.zen.spamhaus.org) # 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!) # 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!)
@@ -92,11 +100,13 @@ unbound-anchor -a /var/lib/unbound/root.key
echo "==> Adding systemd services" echo "==> Adding systemd services"
cp -r "${script_dir}/start/systemd/." /etc/systemd/system/ cp -r "${script_dir}/start/systemd/." /etc/systemd/system/
systemctl disable cloudron.target || true
rm -f /etc/systemd/system/cloudron.target
[[ "${ubuntu_version}" == "16.04" ]] && sed -e 's/MemoryMax/MemoryLimit/g' -i /etc/systemd/system/box.service [[ "${ubuntu_version}" == "16.04" ]] && sed -e 's/MemoryMax/MemoryLimit/g' -i /etc/systemd/system/box.service
systemctl daemon-reload systemctl daemon-reload
systemctl enable unbound systemctl enable unbound
systemctl enable cloudron-syslog systemctl enable cloudron-syslog
systemctl enable cloudron.target systemctl enable box
systemctl enable cloudron-firewall systemctl enable cloudron-firewall
# update firewall rules # update firewall rules
@@ -145,8 +155,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 if ! grep -q "^Restart=" /etc/systemd/system/multi-user.target.wants/nginx.service; then
# default nginx service file does not restart on crash # 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 echo -e "\n[Service]\nRestart=always\n" >> /etc/systemd/system/multi-user.target.wants/nginx.service
systemctl daemon-reload
fi 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 systemctl start nginx
# restart mysql to make sure it has latest config # restart mysql to make sure it has latest config
@@ -171,9 +188,11 @@ readonly mysql_root_password="password"
mysqladmin -u root -ppassword password password # reset default root 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' 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" echo "==> Migrating data"
cd "${BOX_SRC_DIR}" 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" echo "DB migration failed"
exit 1 exit 1
fi fi
@@ -192,6 +211,9 @@ fi
echo "==> Cleaning up stale redis directories" echo "==> Cleaning up stale redis directories"
find "${APPS_DATA_DIR}" -maxdepth 2 -type d -name redis -exec rm -rf {} + 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" 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 # 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 chown -R "${USER}" /etc/cloudron
@@ -207,7 +229,7 @@ chown "${USER}:${USER}" "${BOX_DATA_DIR}/mail"
chown "${USER}:${USER}" -R "${BOX_DATA_DIR}/mail/dkim" # this is owned by box currently since it generates the keys chown "${USER}:${USER}" -R "${BOX_DATA_DIR}/mail/dkim" # this is owned by box currently since it generates the keys
echo "==> Starting Cloudron" echo "==> Starting Cloudron"
systemctl start cloudron.target systemctl start box
sleep 2 # give systemd sometime to start the processes sleep 2 # give systemd sometime to start the processes
+1
View File
@@ -3,6 +3,7 @@ import collectd,os,subprocess,sys,re,time
# https://www.programcreek.com/python/example/106897/collectd.register_read # https://www.programcreek.com/python/example/106897/collectd.register_read
PATHS = [] # { name, dir, exclude } 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 INTERVAL = 60 * 60 * 12 # twice a day. change values in docker-graphite if you change this
def du(pathinfo): def du(pathinfo):
+2
View File
@@ -10,6 +10,7 @@
/home/yellowtent/platformdata/logs/redis-*/*.log /home/yellowtent/platformdata/logs/redis-*/*.log
/home/yellowtent/platformdata/logs/crash/*.log /home/yellowtent/platformdata/logs/crash/*.log
/home/yellowtent/platformdata/logs/collectd/*.log /home/yellowtent/platformdata/logs/collectd/*.log
/home/yellowtent/platformdata/logs/turn/*.log
/home/yellowtent/platformdata/logs/updater/*.log { /home/yellowtent/platformdata/logs/updater/*.log {
# only keep one rotated file, we currently do not send that over the api # only keep one rotated file, we currently do not send that over the api
rotate 1 rotate 1
@@ -17,6 +18,7 @@
missingok missingok
# we never compress so we can simply tail the files # we never compress so we can simply tail the files
nocompress nocompress
# this truncates the original log file and not the rotated one
copytruncate copytruncate
} }
+9 -2
View File
@@ -1,11 +1,18 @@
user www-data; 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; pid /run/nginx.pid;
events { events {
worker_connections 1024; # a single worker has these many simultaneous connections max
worker_connections 4096;
} }
http { http {
+9
View File
@@ -50,3 +50,12 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartdocker.s
Defaults!/home/yellowtent/box/src/scripts/restartunbound.sh env_keep="HOME BOX_ENV" Defaults!/home/yellowtent/box/src/scripts/restartunbound.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartunbound.sh yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartunbound.sh
Defaults!/home/yellowtent/box/src/scripts/rmmailbox.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmmailbox.sh
Defaults!/home/yellowtent/box/src/scripts/starttask.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD:SETENV: /home/yellowtent/box/src/scripts/starttask.sh
Defaults!/home/yellowtent/box/src/scripts/stoptask.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/stoptask.sh
+5 -4
View File
@@ -1,20 +1,21 @@
[Unit] [Unit]
Description=Cloudron Admin Description=Cloudron Admin
OnFailure=crashnotifier@%n.service OnFailure=crashnotifier@%n.service
StopWhenUnneeded=true
; journald crashes result in a EPIPE in node. Cannot ignore it as it results in loss of logs. ; journald crashes result in a EPIPE in node. Cannot ignore it as it results in loss of logs.
BindsTo=systemd-journald.service BindsTo=systemd-journald.service
After=mysql.service nginx.service After=mysql.service nginx.service
; As cloudron-resize-fs is a one-shot, the Wants= automatically ensures that the service *finishes* ; As cloudron-resize-fs is a one-shot, the Wants= automatically ensures that the service *finishes*
Wants=cloudron-resize-fs.service Wants=cloudron-resize-fs.service
[Install]
WantedBy=multi-user.target
[Service] [Service]
Type=idle Type=idle
WorkingDirectory=/home/yellowtent/box WorkingDirectory=/home/yellowtent/box
Restart=always 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=/home/yellowtent/box/box.js
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:ldap" "BOX_ENV=cloudron" "NODE_ENV=production"
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
; kill apptask processes as well ; kill apptask processes as well
KillMode=control-group 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 ; Do not kill this process on OOM. Children inherit this score. Do not set it to -1000 so that MemoryMax can keep working
-10
View File
@@ -1,10 +0,0 @@
[Unit]
Description=Cloudron Smartserver
Documentation=https://cloudron.io/documentation.html
StopWhenUnneeded=true
Requires=box.service
After=box.service
# AllowIsolate=yes
[Install]
WantedBy=multi-user.target
+1 -1
View File
@@ -4,4 +4,4 @@ set -eu -o pipefail
echo "Stopping cloudron" echo "Stopping cloudron"
systemctl stop cloudron.target systemctl stop box
+358 -138
View File
@@ -7,6 +7,9 @@ exports = module.exports = {
getServiceLogs: getServiceLogs, getServiceLogs: getServiceLogs,
restartService: restartService, restartService: restartService,
startAppServices,
stopAppServices,
startServices: startServices, startServices: startServices,
updateServiceConfig: updateServiceConfig, updateServiceConfig: updateServiceConfig,
@@ -20,7 +23,7 @@ exports = module.exports = {
getMountsSync: getMountsSync, getMountsSync: getMountsSync,
getContainerNamesSync: getContainerNamesSync, getContainerNamesSync: getContainerNamesSync,
getServiceDetails: getServiceDetails, getContainerDetails: getContainerDetails,
SERVICE_STATUS_STARTING: 'starting', // container up, waiting for healthcheck SERVICE_STATUS_STARTING: 'starting', // container up, waiting for healthcheck
SERVICE_STATUS_ACTIVE: 'active', SERVICE_STATUS_ACTIVE: 'active',
@@ -62,7 +65,7 @@ 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 // 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 // teardown is destructive. app data stored with the addon is lost
var KNOWN_ADDONS = { var ADDONS = {
turn: { turn: {
setup: setupTurn, setup: setupTurn,
teardown: teardownTurn, teardown: teardownTurn,
@@ -146,10 +149,18 @@ var KNOWN_ADDONS = {
backup: NOOP, backup: NOOP,
restore: NOOP, restore: NOOP,
clear: 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: { turn: {
status: statusTurn, status: statusTurn,
restart: restartContainer.bind(null, 'turn'), restart: restartContainer.bind(null, 'turn'),
@@ -202,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 */) { function debugApp(app /*, args */) {
assert(typeof app === 'object'); assert(typeof app === 'object');
@@ -249,25 +270,22 @@ function rebuildService(serviceName, callback) {
callback(); callback();
} }
function restartContainer(serviceName, callback) { function restartContainer(name, callback) {
assert.strictEqual(typeof serviceName, 'string'); assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function'); 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) debug(`restartContainer: Unable to rebuild service ${name}`, error); });
}
if (error) return callback(error); if (error) return callback(error);
docker.startContainer(serviceName, function (error) { callback(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);
});
}); });
} }
function getServiceDetails(containerName, tokenEnvName, callback) { function getContainerDetails(containerName, tokenEnvName, callback) {
assert.strictEqual(typeof containerName, 'string'); assert.strictEqual(typeof containerName, 'string');
assert.strictEqual(typeof tokenEnvName, 'string'); assert.strictEqual(typeof tokenEnvName, 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
@@ -290,20 +308,20 @@ function getServiceDetails(containerName, tokenEnvName, callback) {
}); });
} }
function containerStatus(addonName, addonTokenName, callback) { function containerStatus(containerName, tokenEnvName, callback) {
assert.strictEqual(typeof addonName, 'string'); assert.strictEqual(typeof containerName, 'string');
assert.strictEqual(typeof addonTokenName, 'string'); assert.strictEqual(typeof tokenEnvName, 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
getServiceDetails(addonName, addonTokenName, function (error, addonDetails) { getContainerDetails(containerName, tokenEnvName, function (error, addonDetails) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, { status: exports.SERVICE_STATUS_STOPPED }); if (error && (error.reason === BoxError.NOT_FOUND || error.reason === BoxError.INACTIVE)) return callback(null, { status: exports.SERVICE_STATUS_STOPPED });
if (error) return callback(error); if (error) return callback(error);
request.get(`https://${addonDetails.ip}:3000/healthcheck?access_token=${addonDetails.token}`, { json: true, rejectUnauthorized: false }, function (error, response) { 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 (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 ${addonName}. Status code: ${response.statusCode} message: ${response.body.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); if (error) return callback(error);
var tmp = { var tmp = {
@@ -321,19 +339,59 @@ function containerStatus(addonName, addonTokenName, callback) {
function getServices(callback) { function getServices(callback) {
assert.strictEqual(typeof callback, 'function'); 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) { function getServicesConfig(id, callback) {
assert.strictEqual(typeof serviceName, 'string'); assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function'); 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 = { var tmp = {
name: serviceName, name: name,
status: null, status: null,
memoryUsed: 0, memoryUsed: 0,
memoryPercent: 0, memoryPercent: 0,
@@ -345,60 +403,76 @@ function getService(serviceName, callback) {
} }
}; };
settings.getPlatformConfig(function (error, platformConfig) { containerStatusFunc(function (error, result) {
if (error) return callback(error); if (error) return callback(error);
if (platformConfig[serviceName] && platformConfig[serviceName].memory && platformConfig[serviceName].memorySwap) { tmp.status = result.status;
tmp.config.memory = platformConfig[serviceName].memory; tmp.memoryUsed = result.memoryUsed;
tmp.config.memorySwap = platformConfig[serviceName].memorySwap; tmp.memoryPercent = result.memoryPercent;
} else if (KNOWN_SERVICES[serviceName].defaultMemoryLimit) { tmp.error = result.error || null;
tmp.config.memory = KNOWN_SERVICES[serviceName].defaultMemoryLimit;
tmp.config.memorySwap = tmp.config.memory * 2;
}
KNOWN_SERVICES[serviceName].status(function (error, result) { getServicesConfig(id, function (error, service, servicesConfig) {
if (error) return callback(error); if (error) return callback(error);
tmp.status = result.status; const serviceConfig = servicesConfig[name];
tmp.memoryUsed = result.memoryUsed;
tmp.memoryPercent = result.memoryPercent; if (serviceConfig && serviceConfig.memory && serviceConfig.memorySwap) {
tmp.error = result.error || null; 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); callback(null, tmp);
}); });
}); });
} }
function configureService(serviceName, data, callback) { function configureService(id, data, callback) {
assert.strictEqual(typeof serviceName, 'string'); assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof data, 'object'); assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function'); 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 (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 not specified we clear the entry and use defaults
if (!data.memory || !data.memorySwap) { if (!data.memory || !data.memorySwap) {
delete platformConfig[serviceName]; delete servicesConfig[name];
} else { } else {
platformConfig[serviceName].memory = data.memory; servicesConfig[name].memory = data.memory;
platformConfig[serviceName].memorySwap = data.memorySwap; servicesConfig[name].memorySwap = data.memorySwap;
} }
settings.setPlatformConfig(platformConfig, function (error) { if (instance) {
if (error) return callback(error); 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) { function getServiceLogs(id, options, callback) {
assert.strictEqual(typeof serviceName, 'string'); assert.strictEqual(typeof id, 'string');
assert(options && typeof options === 'object'); assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
@@ -406,9 +480,15 @@ function getServiceLogs(serviceName, options, callback) {
assert.strictEqual(typeof options.format, 'string'); assert.strictEqual(typeof options.format, 'string');
assert.strictEqual(typeof options.follow, 'boolean'); 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, var lines = options.lines,
format = options.format || 'json', format = options.format || 'json',
@@ -417,21 +497,29 @@ function getServiceLogs(serviceName, options, callback) {
let cmd, args = []; let cmd, args = [];
// docker and unbound use journald // docker and unbound use journald
if (serviceName === 'docker' || serviceName === 'unbound') { if (name === 'docker' || name === 'unbound') {
cmd = 'journalctl'; cmd = 'journalctl';
args.push('--lines=' + (lines === -1 ? 'all' : lines)); args.push('--lines=' + (lines === -1 ? 'all' : lines));
args.push(`--unit=${serviceName}`); args.push(`--unit=${name}`);
args.push('--no-pager'); args.push('--no-pager');
args.push('--output=short-iso'); args.push('--output=short-iso');
if (follow) args.push('--follow'); 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 { } else {
cmd = '/usr/bin/tail'; cmd = '/usr/bin/tail';
args.push('--lines=' + (lines === -1 ? '+1' : lines)); 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 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); var cp = spawn(cmd, args);
@@ -450,7 +538,7 @@ function getServiceLogs(serviceName, options, callback) {
return JSON.stringify({ return JSON.stringify({
realtimeTimestamp: timestamp * 1000, realtimeTimestamp: timestamp * 1000,
message: message, message: message,
source: serviceName source: name
}) + '\n'; }) + '\n';
}); });
@@ -461,23 +549,67 @@ function getServiceLogs(serviceName, options, callback) {
callback(null, transformStream); callback(null, transformStream);
} }
function restartService(serviceName, callback) { function restartService(id, callback) {
assert.strictEqual(typeof serviceName, 'string'); assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function'); 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 containerName, 'string');
assert.strictEqual(typeof tokenEnvName, 'string'); assert.strictEqual(typeof tokenEnvName, 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
debug(`Waiting for ${containerName}`); debug(`Waiting for ${containerName}`);
getServiceDetails(containerName, tokenEnvName, function (error, result) { getContainerDetails(containerName, tokenEnvName, function (error, result) {
if (error) return callback(error); if (error) return callback(error);
async.retry({ times: 10, interval: 15000 }, function (retryCallback) { async.retry({ times: 10, interval: 15000 }, function (retryCallback) {
@@ -501,11 +633,11 @@ function setupAddons(app, addons, callback) {
debugApp(app, 'setupAddons: Setting up %j', Object.keys(addons)); debugApp(app, 'setupAddons: Setting up %j', Object.keys(addons));
async.eachSeries(Object.keys(addons), function iterator(addon, iteratorCallback) { 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]); 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); }, callback);
} }
@@ -519,11 +651,11 @@ function teardownAddons(app, addons, callback) {
debugApp(app, 'teardownAddons: Tearing down %j', Object.keys(addons)); debugApp(app, 'teardownAddons: Tearing down %j', Object.keys(addons));
async.eachSeries(Object.keys(addons), function iterator(addon, iteratorCallback) { 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]); 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); }, callback);
} }
@@ -539,9 +671,9 @@ function backupAddons(app, addons, callback) {
debugApp(app, 'backupAddons: Backing up %j', Object.keys(addons)); debugApp(app, 'backupAddons: Backing up %j', Object.keys(addons));
async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) { 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); }, callback);
} }
@@ -557,9 +689,9 @@ function clearAddons(app, addons, callback) {
debugApp(app, 'clearAddons: clearing %j', Object.keys(addons)); debugApp(app, 'clearAddons: clearing %j', Object.keys(addons));
async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) { 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); }, callback);
} }
@@ -575,9 +707,9 @@ function restoreAddons(app, addons, callback) {
debugApp(app, 'restoreAddons: restoring %j', Object.keys(addons)); debugApp(app, 'restoreAddons: restoring %j', Object.keys(addons));
async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) { 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); }, callback);
} }
@@ -586,12 +718,12 @@ function importAppDatabase(app, addon, callback) {
assert.strictEqual(typeof addon, 'string'); assert.strictEqual(typeof addon, 'string');
assert.strictEqual(typeof callback, 'function'); 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([ async.series([
KNOWN_ADDONS[addon].setup.bind(null, app, app.manifest.addons[addon]), 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 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].restore.bind(null, app, app.manifest.addons[addon])
], callback); ], callback);
} }
@@ -601,10 +733,10 @@ function importDatabase(addon, callback) {
debug(`importDatabase: Importing ${addon}`); debug(`importDatabase: Importing ${addon}`);
appdb.getAll(function (error, apps) { appdb.getAll(function (error, allApps) {
if (error) return callback(error); if (error) return callback(error);
async.eachSeries(apps, function iterator (app, iteratorCallback) { async.eachSeries(allApps, function iterator (app, iteratorCallback) {
if (!(addon in app.manifest.addons)) return iteratorCallback(); // app doesn't use the addon if (!(addon in app.manifest.addons)) return iteratorCallback(); // app doesn't use the addon
debug(`importDatabase: Importing addon ${addon} of app ${app.id}`); debug(`importDatabase: Importing addon ${addon} of app ${app.id}`);
@@ -617,7 +749,51 @@ function importDatabase(addon, callback) {
// not clear, if repair workflow should be part of addon or per-app // not clear, if repair workflow should be part of addon or per-app
appdb.update(app.id, { installationState: apps.ISTATE_ERROR, error: { message: error.message } }, iteratorCallback); appdb.update(app.id, { installationState: apps.ISTATE_ERROR, error: { message: error.message } }, iteratorCallback);
}); });
}, callback); }, function (error) {
safe.fs.unlinkSync(path.join(paths.ADDON_CONFIG_DIR, `exported-${addon}`)); // clean up for future migrations
callback(error);
});
});
}
function exportDatabase(addon, callback) {
assert.strictEqual(typeof addon, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`exportDatabase: Exporting ${addon}`);
if (fs.existsSync(path.join(paths.ADDON_CONFIG_DIR, `exported-${addon}`))) {
debug(`exportDatabase: Already exported addon ${addon} in previous run`);
return callback(null);
}
appdb.getAll(function (error, apps) {
if (error) return callback(error);
async.eachSeries(apps, function iterator (app, iteratorCallback) {
if (!app.manifest.addons || !(addon in app.manifest.addons)) return iteratorCallback(); // app doesn't use the addon
debug(`exportDatabase: Exporting addon ${addon} of app ${app.id}`);
ADDONS[addon].backup(app, app.manifest.addons[addon], function (error) {
if (error) {
debug(`exportDatabase: Error exporting ${addon} of app ${app.id}.`, error);
return iteratorCallback(error);
}
iteratorCallback();
});
}, function (error) {
if (error) return callback(error);
async.series([
(done) => fs.writeFile(path.join(paths.ADDON_CONFIG_DIR, `exported-${addon}`), '', 'utf8', done),
// note: after this point, we are restart safe. it's ok if the box code crashes at this point
(done) => shell.exec(`exportDatabase - remove${addon}`, `docker rm -f ${addon}`, done), // what if db writes something when quitting ...
(done) => shell.sudo(`exportDatabase - removeAddonDir${addon}`, [ RMADDONDIR_CMD, addon ], {}, done) // ready to start afresh
], callback);
});
}); });
} }
@@ -633,15 +809,37 @@ function updateServiceConfig(platformConfig, callback) {
memory = containerConfig.memory; memory = containerConfig.memory;
memorySwap = containerConfig.memorySwap; memorySwap = containerConfig.memorySwap;
} else { } else {
memory = KNOWN_SERVICES[serviceName].defaultMemoryLimit; memory = SERVICES[serviceName].defaultMemoryLimit;
memorySwap = memory * 2; memorySwap = memory * 2;
} }
const args = `update --memory ${memory} --memory-swap ${memorySwap} ${serviceName}`.split(' '); const args = `update --memory ${memory} --memory-swap ${memorySwap} ${serviceName}`.split(' ');
shell.spawn(`update${serviceName}`, '/usr/bin/docker', args, { }, iteratorCallback); shell.spawn(`updateServiceConfig(${serviceName})`, '/usr/bin/docker', args, { }, iteratorCallback);
}, 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) { function startServices(existingInfra, callback) {
assert.strictEqual(typeof existingInfra, 'object'); assert.strictEqual(typeof existingInfra, 'object');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
@@ -661,7 +859,7 @@ function startServices(existingInfra, callback) {
} else { } else {
assert.strictEqual(typeof existingInfra.images, 'object'); 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.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.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.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)); if (infra.images.mongodb.tag !== existingInfra.images.mongodb.tag) startFuncs.push(startMongodb.bind(null, existingInfra));
@@ -744,8 +942,8 @@ function setupLocalStorage(app, options, callback) {
// reomve any existing volume in case it's bound with an old dataDir // reomve any existing volume in case it's bound with an old dataDir
async.series([ async.series([
docker.removeVolume.bind(null, app, `${app.id}-localstorage`), docker.removeVolume.bind(null, `${app.id}-localstorage`),
docker.createVolume.bind(null, app, `${app.id}-localstorage`, volumeDataDir) docker.createVolume.bind(null, `${app.id}-localstorage`, volumeDataDir, { fqdn: app.fqdn, appId: app.id })
], callback); ], callback);
} }
@@ -756,7 +954,7 @@ function clearLocalStorage(app, options, callback) {
debugApp(app, 'clearLocalStorage'); 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) { function teardownLocalStorage(app, options, callback) {
@@ -767,8 +965,8 @@ function teardownLocalStorage(app, options, callback) {
debugApp(app, 'teardownLocalStorage'); debugApp(app, 'teardownLocalStorage');
async.series([ async.series([
docker.clearVolume.bind(null, app, `${app.id}-localstorage`, { removeDirectory: true }), docker.clearVolume.bind(null, `${app.id}-localstorage`, { removeDirectory: true }),
docker.removeVolume.bind(null, app, `${app.id}-localstorage`) docker.removeVolume.bind(null, `${app.id}-localstorage`)
], callback); ], callback);
} }
@@ -778,7 +976,7 @@ function setupTurn(app, options, callback) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
var turnSecret = safe.fs.readFileSync(paths.ADDON_TURN_SECRET_FILE, 'utf8'); 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 (!turnSecret) debug('setupTurn: no turn secret set. Will leave emtpy, but this is a problem!');
const env = [ const env = [
{ name: 'CLOUDRON_STUN_SERVER', value: settings.adminFqdn() }, { name: 'CLOUDRON_STUN_SERVER', value: settings.adminFqdn() },
@@ -977,7 +1175,7 @@ function startMysql(existingInfra, callback) {
const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.mysql.tag, tag); const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.mysql.tag, tag);
if (upgrading) debug('startMysql: mysql will be upgraded'); if (upgrading) debug('startMysql: mysql will be upgraded');
const upgradeFunc = upgrading ? shell.sudo.bind(null, 'startMysql', [ RMADDONDIR_CMD, 'mysql' ], {}) : (next) => next(); const upgradeFunc = upgrading ? exportDatabase.bind(null, 'mysql') : (next) => next();
upgradeFunc(function (error) { upgradeFunc(function (error) {
if (error) return callback(error); if (error) return callback(error);
@@ -1001,10 +1199,14 @@ function startMysql(existingInfra, callback) {
--label isCloudronManaged=true \ --label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`; --read-only -v /tmp -v /run "${tag}"`;
shell.exec('startMysql', cmd, function (error) { async.series([
shell.exec.bind(null, 'stopMysql', 'docker stop mysql || true'),
shell.exec.bind(null, 'removeMysql', 'docker rm -f mysql || true'),
shell.exec.bind(null, 'startMysql', cmd)
], function (error) {
if (error) return callback(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 (error) return callback(error);
if (!upgrading) return callback(null); if (!upgrading) return callback(null);
@@ -1033,7 +1235,7 @@ function setupMySql(app, options, callback) {
password: error ? hat(4 * 48) : existingPassword // see box#362 for password length 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); 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) { request.post(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) {
@@ -1072,7 +1274,7 @@ function clearMySql(app, options, callback) {
const database = mysqlDatabaseName(app.id); 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); 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) { request.post(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}/clear?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
@@ -1092,7 +1294,7 @@ function teardownMySql(app, options, callback) {
const database = mysqlDatabaseName(app.id); const database = mysqlDatabaseName(app.id);
const username = database; const username = database;
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) { getContainerDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
if (error) return callback(error); 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) { request.delete(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}?access_token=${result.token}&username=${username}`, { json: true, rejectUnauthorized: false }, function (error, response) {
@@ -1140,7 +1342,7 @@ function backupMySql(app, options, callback) {
debugApp(app, 'Backing up mysql'); 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); if (error) return callback(error);
const url = `https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}/backup?access_token=${result.token}`; const url = `https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}/backup?access_token=${result.token}`;
@@ -1159,7 +1361,7 @@ function restoreMySql(app, options, callback) {
callback = once(callback); // protect from multiple returns with streams 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); if (error) return callback(error);
var input = fs.createReadStream(dumpPath('mysql', app.id)); var input = fs.createReadStream(dumpPath('mysql', app.id));
@@ -1194,7 +1396,7 @@ function startPostgresql(existingInfra, callback) {
const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.postgresql.tag, tag); const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.postgresql.tag, tag);
if (upgrading) debug('startPostgresql: postgresql will be upgraded'); if (upgrading) debug('startPostgresql: postgresql will be upgraded');
const upgradeFunc = upgrading ? shell.sudo.bind(null, 'startPostgresql', [ RMADDONDIR_CMD, 'postgresql' ], {}) : (next) => next(); const upgradeFunc = upgrading ? exportDatabase.bind(null, 'postgresql') : (next) => next();
upgradeFunc(function (error) { upgradeFunc(function (error) {
if (error) return callback(error); if (error) return callback(error);
@@ -1217,10 +1419,14 @@ function startPostgresql(existingInfra, callback) {
--label isCloudronManaged=true \ --label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`; --read-only -v /tmp -v /run "${tag}"`;
shell.exec('startPostgresql', cmd, function (error) { async.series([
shell.exec.bind(null, 'stopPostgresql', 'docker stop postgresql || true'),
shell.exec.bind(null, 'removePostgresql', 'docker rm -f postgresql || true'),
shell.exec.bind(null, 'startPostgresql', cmd)
], function (error) {
if (error) return callback(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 (error) return callback(error);
if (!upgrading) return callback(null); if (!upgrading) return callback(null);
@@ -1248,7 +1454,7 @@ function setupPostgreSql(app, options, callback) {
password: error ? hat(4 * 128) : existingPassword 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); if (error) return callback(error);
request.post(`https://${result.ip}:3000/databases?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) { request.post(`https://${result.ip}:3000/databases?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) {
@@ -1282,7 +1488,7 @@ function clearPostgreSql(app, options, callback) {
debugApp(app, 'Clearing postgresql'); 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); 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) { request.post(`https://${result.ip}:3000/databases/${database}/clear?access_token=${result.token}&username=${username}`, { json: true, rejectUnauthorized: false }, function (error, response) {
@@ -1301,7 +1507,7 @@ function teardownPostgreSql(app, options, callback) {
const { database, username } = postgreSqlNames(app.id); 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); 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) { request.delete(`https://${result.ip}:3000/databases/${database}?access_token=${result.token}&username=${username}`, { json: true, rejectUnauthorized: false }, function (error, response) {
@@ -1322,7 +1528,7 @@ function backupPostgreSql(app, options, callback) {
const { database } = postgreSqlNames(app.id); 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); if (error) return callback(error);
const url = `https://${result.ip}:3000/databases/${database}/backup?access_token=${result.token}`; const url = `https://${result.ip}:3000/databases/${database}/backup?access_token=${result.token}`;
@@ -1341,7 +1547,7 @@ function restorePostgreSql(app, options, callback) {
callback = once(callback); // protect from multiple returns with streams 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); if (error) return callback(error);
var input = fs.createReadStream(dumpPath('postgresql', app.id)); var input = fs.createReadStream(dumpPath('postgresql', app.id));
@@ -1373,8 +1579,6 @@ function startTurn(existingInfra, callback) {
const memoryLimit = 256; const memoryLimit = 256;
const realm = settings.adminFqdn(); 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 // this exports 3478/tcp, 5349/tls and 50000-51000/udp
const cmd = `docker run --restart=always -d --name="turn" \ const cmd = `docker run --restart=always -d --name="turn" \
--hostname turn \ --hostname turn \
@@ -1392,7 +1596,11 @@ function startTurn(existingInfra, callback) {
--label isCloudronManaged=true \ --label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`; --read-only -v /tmp -v /run "${tag}"`;
shell.exec('startTurn', cmd, callback); async.series([
shell.exec.bind(null, 'stopTurn', 'docker stop turn || true'),
shell.exec.bind(null, 'removeTurn', 'docker rm -f turn || true'),
shell.exec.bind(null, 'startTurn', cmd)
], callback);
} }
function startMongodb(existingInfra, callback) { function startMongodb(existingInfra, callback) {
@@ -1408,7 +1616,7 @@ function startMongodb(existingInfra, callback) {
const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.mongodb.tag, tag); const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.mongodb.tag, tag);
if (upgrading) debug('startMongodb: mongodb will be upgraded'); if (upgrading) debug('startMongodb: mongodb will be upgraded');
const upgradeFunc = upgrading ? shell.sudo.bind(null, 'startMongodb', [ RMADDONDIR_CMD, 'mongodb' ], {}) : (next) => next(); const upgradeFunc = upgrading ? exportDatabase.bind(null, 'mongodb') : (next) => next();
upgradeFunc(function (error) { upgradeFunc(function (error) {
if (error) return callback(error); if (error) return callback(error);
@@ -1431,10 +1639,14 @@ function startMongodb(existingInfra, callback) {
--label isCloudronManaged=true \ --label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`; --read-only -v /tmp -v /run "${tag}"`;
shell.exec('startMongodb', cmd, function (error) { async.series([
shell.exec.bind(null, 'stopMongodb', 'docker stop mongodb || true'),
shell.exec.bind(null, 'removeMongodb', 'docker rm -f mongodb || true'),
shell.exec.bind(null, 'startMongodb', cmd)
], function (error) {
if (error) return callback(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 (error) return callback(error);
if (!upgrading) return callback(null); if (!upgrading) return callback(null);
@@ -1461,7 +1673,7 @@ function setupMongoDb(app, options, callback) {
oplog: !!options.oplog oplog: !!options.oplog
}; };
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) { getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
if (error) return callback(error); if (error) return callback(error);
request.post(`https://${result.ip}:3000/databases?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) { request.post(`https://${result.ip}:3000/databases?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) {
@@ -1497,7 +1709,7 @@ function clearMongodb(app, options, callback) {
debugApp(app, 'Clearing mongodb'); 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); 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) { request.post(`https://${result.ip}:3000/databases/${app.id}/clear?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
@@ -1516,7 +1728,7 @@ function teardownMongoDb(app, options, callback) {
debugApp(app, 'Tearing down mongodb'); 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); 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) { request.delete(`https://${result.ip}:3000/databases/${app.id}?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
@@ -1535,7 +1747,7 @@ function backupMongoDb(app, options, callback) {
debugApp(app, 'Backing up mongodb'); 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); if (error) return callback(error);
const url = `https://${result.ip}:3000/databases/${app.id}/backup?access_token=${result.token}`; const url = `https://${result.ip}:3000/databases/${app.id}/backup?access_token=${result.token}`;
@@ -1552,7 +1764,7 @@ function restoreMongoDb(app, options, callback) {
debugApp(app, 'restoreMongoDb'); debugApp(app, 'restoreMongoDb');
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) { getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
if (error) return callback(error); if (error) return callback(error);
const readStream = fs.createReadStream(dumpPath('mongodb', app.id)); const readStream = fs.createReadStream(dumpPath('mongodb', app.id));
@@ -1576,19 +1788,25 @@ function startRedis(existingInfra, callback) {
const tag = infra.images.redis.tag; const tag = infra.images.redis.tag;
const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.redis.tag, 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); 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 if (!('redis' in app.manifest.addons)) return iteratorCallback(); // app doesn't use the addon
setupRedis(app, app.manifest.addons.redis, iteratorCallback); const redisName = 'redis-' + app.id;
async.series([
shell.exec.bind(null, 'stopRedis', `docker stop ${redisName} || true`), // redis will backup as part of signal handling
shell.exec.bind(null, 'removeRedis', `docker rm -f ${redisName} || true`),
setupRedis.bind(null, app, app.manifest.addons.redis) // starts the container
], iteratorCallback);
}, function (error) { }, function (error) {
if (error) return callback(error); if (error) return callback(error);
if (!upgrading) return callback(); if (!upgrading) return callback();
importDatabase('redis', callback); // setupRedis currently starts the app container importDatabase('redis', callback);
}); });
}); });
} }
@@ -1608,15 +1826,7 @@ function setupRedis(app, options, callback) {
const redisServiceToken = hat(4 * 48); const redisServiceToken = hat(4 * 48);
// Compute redis memory limit based on app's memory limit (this is arbitrary) // Compute redis memory limit based on app's memory limit (this is arbitrary)
var memoryLimit = app.memoryLimit || app.manifest.memoryLimit || 0; const memoryLimit = app.servicesConfig['redis'] ? app.servicesConfig['redis'].memory : APP_SERVICES['redis'].defaultMemoryLimit;
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 tag = infra.images.redis.tag; const tag = infra.images.redis.tag;
const label = app.fqdn; const label = app.fqdn;
@@ -1660,7 +1870,7 @@ function setupRedis(app, options, callback) {
}); });
}, },
appdb.setAddonConfig.bind(null, app.id, 'redis', env), 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) { ], function (error) {
if (error) debug('Error setting up redis: ', error); if (error) debug('Error setting up redis: ', error);
callback(error); callback(error);
@@ -1675,7 +1885,7 @@ function clearRedis(app, options, callback) {
debugApp(app, 'Clearing redis'); 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); if (error) return callback(error);
request.post(`https://${result.ip}:3000/clear?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) { request.post(`https://${result.ip}:3000/clear?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
@@ -1714,7 +1924,7 @@ function backupRedis(app, options, callback) {
debugApp(app, 'Backing up redis'); 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); if (error) return callback(error);
const url = `https://${result.ip}:3000/backup?access_token=${result.token}`; const url = `https://${result.ip}:3000/backup?access_token=${result.token}`;
@@ -1731,7 +1941,7 @@ function restoreRedis(app, options, callback) {
callback = once(callback); // protect from multiple returns with streams 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); if (error) return callback(error);
let input; let input;
@@ -1869,3 +2079,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', 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.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'subdomains.subdomain AS location', 'subdomains.domain',
'apps.accessRestrictionJson', 'apps.memoryLimit', 'apps.cpuShares', '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.sso', 'apps.debugModeJson', 'apps.enableBackup',
'apps.creationTime', 'apps.updateTime', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableAutomaticUpdate', 'apps.creationTime', 'apps.updateTime', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableAutomaticUpdate',
'apps.dataDir', 'apps.ts', 'apps.healthTime' ].join(','); 'apps.dataDir', 'apps.ts', 'apps.healthTime' ].join(',');
@@ -94,6 +94,14 @@ function postProcess(result) {
result.debugMode = safe.JSON.parse(result.debugModeJson); result.debugMode = safe.JSON.parse(result.debugModeJson);
delete 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 = result.alternateDomains || [];
result.alternateDomains.forEach(function (d) { result.alternateDomains.forEach(function (d) {
delete d.appId; delete d.appId;
@@ -427,7 +435,7 @@ function updateWithConstraints(id, app, constraints, callback) {
var fields = [ ], values = [ ]; var fields = [ ], values = [ ];
for (var p in app) { 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 = ?`); fields.push(`${p}Json = ?`);
values.push(JSON.stringify(app[p])); values.push(JSON.stringify(app[p]));
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'env') { } 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'); assert.strictEqual(typeof callback, 'function');
if (app.installationState !== apps.ISTATE_INSTALLED || app.runState !== apps.RSTATE_RUNNING) { 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); return callback(null);
} }
@@ -103,10 +102,8 @@ function checkAppHealth(app, callback) {
.timeout(HEALTHCHECK_INTERVAL) .timeout(HEALTHCHECK_INTERVAL)
.end(function (error, res) { .end(function (error, res) {
if (error && !error.response) { if (error && !error.response) {
debugApp(app, 'not alive (network error): %s', error.message);
setHealth(app, apps.HEALTH_UNHEALTHY, callback); setHealth(app, apps.HEALTH_UNHEALTHY, callback);
} else if (res.statusCode >= 400) { // 2xx and 3xx are ok } else if (res.statusCode >= 400) { // 2xx and 3xx are ok
debugApp(app, 'not alive : %s', error || res.status);
setHealth(app, apps.HEALTH_UNHEALTHY, callback); setHealth(app, apps.HEALTH_UNHEALTHY, callback);
} else { } else {
setHealth(app, apps.HEALTH_HEALTHY, callback); setHealth(app, apps.HEALTH_HEALTHY, callback);
@@ -180,18 +177,14 @@ function processDockerEvents(intervalSecs, callback) {
function processApp(callback) { function processApp(callback) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
apps.getAll(function (error, result) { apps.getAll(function (error, allApps) {
if (error) return callback(error); if (error) return callback(error);
async.each(result, checkAppHealth, function (error) { async.each(allApps, checkAppHealth, function (error) {
if (error) console.error(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 debug(`app health: ${alive.length} alive / ${allApps.length - alive.length} dead.` + (error ? ` ${error.reason}` : ''));
.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);
callback(null); callback(null);
}); });
@@ -206,7 +199,7 @@ function run(intervalSecs, callback) {
processApp, // this is first because docker.getEvents seems to get 'stuck' sometimes processApp, // this is first because docker.getEvents seems to get 'stuck' sometimes
processDockerEvents.bind(null, intervalSecs) processDockerEvents.bind(null, intervalSecs)
], function (error) { ], function (error) {
if (error) debug(error); if (error) debug(`run: could not check app health. ${error.message}`);
callback(); callback();
}); });
+115 -13
View File
@@ -20,6 +20,7 @@ exports = module.exports = {
setTags: setTags, setTags: setTags,
setMemoryLimit: setMemoryLimit, setMemoryLimit: setMemoryLimit,
setCpuShares: setCpuShares, setCpuShares: setCpuShares,
setBinds: setBinds,
setAutomaticBackup: setAutomaticBackup, setAutomaticBackup: setAutomaticBackup,
setAutomaticUpdate: setAutomaticUpdate, setAutomaticUpdate: setAutomaticUpdate,
setReverseProxyConfig: setReverseProxyConfig, setReverseProxyConfig: setReverseProxyConfig,
@@ -57,6 +58,7 @@ exports = module.exports = {
restoreInstalledApps: restoreInstalledApps, restoreInstalledApps: restoreInstalledApps,
configureInstalledApps: configureInstalledApps, configureInstalledApps: configureInstalledApps,
schedulePendingTasks: schedulePendingTasks, schedulePendingTasks: schedulePendingTasks,
restartAppsUsingAddons: restartAppsUsingAddons,
getDataDir: getDataDir, getDataDir: getDataDir,
getIconPath: getIconPath, getIconPath: getIconPath,
@@ -332,6 +334,20 @@ function validateEnv(env) {
return null; return null;
} }
function validateBinds(binds) {
for (let name of Object.keys(binds)) {
// just have friendly characters under /media
if (!/^[-0-9a-zA-Z_@$=#.%+]+$/.test(name)) return new BoxError(BoxError.BAD_FIELD, `Invalid bind name: ${name}`);
const bind = binds[name];
if (!bind.hostPath.startsWith('/mnt') && !bind.hostPath.startsWith('/media')) return new BoxError(BoxError.BAD_FIELD, 'hostPath must be in /mnt or /media');
if (path.normalize(bind.hostPath) !== bind.hostPath) return new BoxError(BoxError.BAD_FIELD, 'hostPath is not normalized');
}
return null;
}
function validateDataDir(dataDir) { function validateDataDir(dataDir) {
if (dataDir === null) return null; if (dataDir === null) return null;
@@ -404,14 +420,14 @@ function removeInternalFields(app) {
'location', 'domain', 'fqdn', 'mailboxName', 'mailboxDomain', 'location', 'domain', 'fqdn', 'mailboxName', 'mailboxDomain',
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'cpuShares', 'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'cpuShares',
'sso', 'debugMode', 'reverseProxyConfig', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags', 'sso', 'debugMode', 'reverseProxyConfig', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags',
'label', 'alternateDomains', 'env', 'enableAutomaticUpdate', 'dataDir'); 'label', 'alternateDomains', 'env', 'enableAutomaticUpdate', 'dataDir', 'binds');
} }
// non-admins can only see these // non-admins can only see these
function removeRestrictedFields(app) { function removeRestrictedFields(app) {
return _.pick(app, return _.pick(app,
'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId', 'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId', 'alternateDomains', 'sso',
'location', 'domain', 'fqdn', 'manifest', 'portBindings', 'iconUrl', 'creationTime', 'ts', 'tags', 'label'); 'location', 'domain', 'fqdn', 'manifest', 'portBindings', 'iconUrl', 'creationTime', 'ts', 'tags', 'label', 'enableBackup');
} }
function getIconUrlSync(app) { function getIconUrlSync(app) {
@@ -605,6 +621,8 @@ function downloadManifest(appStoreId, manifest, callback) {
if (result.statusCode !== 200) return callback(new BoxError(BoxError.NOT_FOUND, util.format('Failed to get app info from store.', result.statusCode, result.text))); if (result.statusCode !== 200) return callback(new BoxError(BoxError.NOT_FOUND, util.format('Failed to get app info from store.', result.statusCode, result.text)));
if (!result.body.manifest || typeof result.body.manifest !== 'object') return callback(new BoxError(BoxError.NOT_FOUND, util.format('Missing manifest. Failed to get app info from store.', result.statusCode, result.text)));
callback(null, parts[0], result.body.manifest); callback(null, parts[0], result.body.manifest);
}); });
} }
@@ -622,7 +640,7 @@ function scheduleTask(appId, installationState, taskId, callback) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
appTaskManager.scheduleTask(appId, taskId, function (error) { appTaskManager.scheduleTask(appId, taskId, function (error) {
debug(`scheduleTask: task ${taskId} of $${appId} completed`); debug(`scheduleTask: task ${taskId} of ${appId} completed`);
if (error && (error.code === tasks.ECRASHED || error.code === tasks.ESTOPPED)) { // if task crashed, update the error if (error && (error.code === tasks.ECRASHED || error.code === tasks.ESTOPPED)) { // if task crashed, update the error
debug(`Apptask crashed/stopped: ${error.message}`); debug(`Apptask crashed/stopped: ${error.message}`);
let boxError = new BoxError(BoxError.TASK_ERROR, error.message); let boxError = new BoxError(BoxError.TASK_ERROR, error.message);
@@ -678,6 +696,11 @@ function checkAppState(app, state) {
if (state !== exports.ISTATE_PENDING_UNINSTALL && state !== exports.ISTATE_PENDING_RESTORE) return new BoxError(BoxError.BAD_STATE, 'Not allowed in error state'); if (state !== exports.ISTATE_PENDING_UNINSTALL && state !== exports.ISTATE_PENDING_RESTORE) return new BoxError(BoxError.BAD_STATE, 'Not allowed in error state');
} }
if (app.runState === exports.RSTATE_STOPPED) {
// can't backup or restore since app addons are down. can't update because migration scripts won't run
if (state === exports.ISTATE_PENDING_UPDATE || state === exports.ISTATE_PENDING_BACKUP || state === exports.ISTATE_PENDING_RESTORE) return new BoxError(BoxError.BAD_STATE, 'Not allowed in stopped state');
}
return null; return null;
} }
@@ -761,6 +784,8 @@ function install(data, auditSource, callback) {
error = validateEnv(env); error = validateEnv(env);
if (error) return callback(error); if (error) return callback(error);
if (settings.isDemo() && constants.DEMO_BLACKLISTED_APPS.includes(appStoreId)) return callback(new BoxError(BoxError.BAD_FIELD, 'This app is blacklisted in the demo'));
const mailboxName = hasMailAddon(manifest) ? mailboxNameForLocation(location, manifest) : null; const mailboxName = hasMailAddon(manifest) ? mailboxNameForLocation(location, manifest) : null;
const mailboxDomain = hasMailAddon(manifest) ? domain : null; const mailboxDomain = hasMailAddon(manifest) ? domain : null;
const appId = uuid.v4(); const appId = uuid.v4();
@@ -968,6 +993,32 @@ function setCpuShares(app, cpuShares, auditSource, callback) {
}); });
} }
function setBinds(app, binds, auditSource, callback) {
assert.strictEqual(typeof app, 'object');
assert(binds && typeof binds === 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
const appId = app.id;
let error = checkAppState(app, exports.ISTATE_PENDING_RECREATE_CONTAINER);
if (error) return callback(error);
error = validateBinds(binds);
if (error) return callback(error);
const task = {
args: {},
values: { binds }
};
addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task, function (error, result) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, binds, taskId: result.taskId });
callback(null, { taskId: result.taskId });
});
}
function setEnvironment(app, env, auditSource, callback) { function setEnvironment(app, env, auditSource, callback) {
assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof env, 'object'); assert.strictEqual(typeof env, 'object');
@@ -1435,7 +1486,8 @@ function restore(app, backupId, auditSource, callback) {
func(function (error, backupInfo) { func(function (error, backupInfo) {
if (error) return callback(error); if (error) return callback(error);
if (!backupInfo.manifest) callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Could not get restore manifest')); if (!backupInfo.manifest) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Could not get restore manifest'));
if (backupInfo.encryptionVersion === 1) return callback(new BoxError(BoxError.BAD_FIELD, 'This encrypted backup was created with an older Cloudron version and has to be restored using the CLI tool'));
// re-validate because this new box version may not accept old configs // re-validate because this new box version may not accept old configs
error = checkManifestConstraints(backupInfo.manifest); error = checkManifestConstraints(backupInfo.manifest);
@@ -1495,6 +1547,15 @@ function importApp(app, data, auditSource, callback) {
testBackupConfig(function (error) { testBackupConfig(function (error) {
if (error) return callback(error); if (error) return callback(error);
if (backupConfig) {
if ('password' in backupConfig) {
backupConfig.encryption = backups.generateEncryptionKeysSync(backupConfig.password);
delete backupConfig.password;
} else {
backupConfig.encryption = null;
}
}
const restoreConfig = { backupId, backupFormat, backupConfig }; const restoreConfig = { backupId, backupFormat, backupConfig };
const task = { const task = {
@@ -1553,7 +1614,8 @@ function clone(app, data, user, auditSource, callback) {
backups.get(backupId, function (error, backupInfo) { backups.get(backupId, function (error, backupInfo) {
if (error) return callback(error); if (error) return callback(error);
if (!backupInfo.manifest) callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Could not get restore config')); if (!backupInfo.manifest) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Could not get restore config'));
if (backupInfo.encryptionVersion === 1) return callback(new BoxError(BoxError.BAD_FIELD, 'This encrypted backup was created with an older Cloudron version and cannot be cloned'));
const manifest = backupInfo.manifest, appStoreId = app.appStoreId; const manifest = backupInfo.manifest, appStoreId = app.appStoreId;
@@ -1775,14 +1837,25 @@ function exec(app, options, callback) {
}); });
} }
function canAutoupdateApp(app, newManifest) { function canAutoupdateApp(app, updateInfo) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof updateInfo, 'object');
const manifest = updateInfo.manifest;
if (!app.enableAutomaticUpdate) return false; if (!app.enableAutomaticUpdate) return false;
if ((semver.major(app.manifest.version) !== 0) && (semver.major(app.manifest.version) !== semver.major(newManifest.version))) return false; // major changes are blocking
// for invalid subscriptions the appstore does not return a dockerImage
if (!manifest.dockerImage) return false;
if (updateInfo.unstable) return false; // only manual update allowed for unstable updates
if ((semver.major(app.manifest.version) !== 0) && (semver.major(app.manifest.version) !== semver.major(manifest.version))) return false; // major changes are blocking
if (app.runState === exports.RSTATE_STOPPED) return false; // stopped apps won't run migration scripts and shouldn't be updated if (app.runState === exports.RSTATE_STOPPED) return false; // stopped apps won't run migration scripts and shouldn't be updated
const newTcpPorts = newManifest.tcpPorts || { }; const newTcpPorts = manifest.tcpPorts || { };
const newUdpPorts = newManifest.udpPorts || { }; const newUdpPorts = manifest.udpPorts || { };
const portBindings = app.portBindings; // this is never null const portBindings = app.portBindings; // this is never null
for (let portName in portBindings) { for (let portName in portBindings) {
@@ -1807,7 +1880,7 @@ function autoupdateApps(updateInfo, auditSource, callback) { // updateInfo is {
return iteratorDone(); return iteratorDone();
} }
if (!canAutoupdateApp(app, updateInfo[appId].manifest)) { if (!canAutoupdateApp(app, updateInfo[appId])) {
debug(`app ${app.fqdn} requires manual update`); debug(`app ${app.fqdn} requires manual update`);
return iteratorDone(); return iteratorDone();
} }
@@ -1852,7 +1925,7 @@ function listBackups(app, page, perPage, callback) {
assert(typeof perPage === 'number' && perPage > 0); assert(typeof perPage === 'number' && perPage > 0);
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
backups.getByAppIdPaged(page, perPage, app.id, function (error, results) { backups.getByIdentifierAndStatePaged(app.id, backups.BACKUP_STATE_NORMAL, page, perPage, function (error, results) {
if (error) return callback(error); if (error) return callback(error);
callback(null, results); callback(null, results);
@@ -1869,7 +1942,7 @@ function restoreInstalledApps(callback) {
apps = apps.filter(app => app.installationState !== exports.ISTATE_PENDING_RESTORE); // safeguard against tasks being created non-stop if we crash on startup apps = apps.filter(app => app.installationState !== exports.ISTATE_PENDING_RESTORE); // safeguard against tasks being created non-stop if we crash on startup
async.eachSeries(apps, function (app, iteratorDone) { async.eachSeries(apps, function (app, iteratorDone) {
backups.getByAppIdPaged(1, 1, app.id, function (error, results) { backups.getByIdentifierAndStatePaged(app.id, backups.BACKUP_STATE_NORMAL, 1, 1, function (error, results) {
let installationState, restoreConfig, oldManifest; let installationState, restoreConfig, oldManifest;
if (!error && results.length) { if (!error && results.length) {
installationState = exports.ISTATE_PENDING_RESTORE; installationState = exports.ISTATE_PENDING_RESTORE;
@@ -1930,6 +2003,35 @@ function configureInstalledApps(callback) {
}); });
} }
function restartAppsUsingAddons(changedAddons, callback) {
assert(Array.isArray(changedAddons));
assert.strictEqual(typeof callback, 'function');
getAll(function (error, apps) {
if (error) return callback(error);
apps = apps.filter(app => app.manifest.addons && _.intersection(Object.keys(app.manifest.addons), changedAddons).length !== 0);
apps = apps.filter(app => app.installationState !== exports.ISTATE_ERROR); // remove errored apps. let them be 'repaired' by hand
apps = apps.filter(app => app.installationState !== exports.ISTATE_PENDING_RESTART); // safeguard against tasks being created non-stop restart if we crash on startup
apps = apps.filter(app => app.runState !== exports.RSTATE_STOPPED); // don't start stopped apps
async.eachSeries(apps, function (app, iteratorDone) {
debug(`restartAppsUsingAddons: marking ${app.fqdn} for restart`);
const task = {
args: {},
values: { runState: exports.RSTATE_RUNNING }
};
addTask(app.id, exports.ISTATE_PENDING_RESTART, task, function (error, result) {
if (error) debug(`restartAppsUsingAddons: error marking ${app.fqdn} for restart: ${JSON.stringify(error)}`);
else debug(`restartAppsUsingAddons: marked ${app.id} for restart with taskId ${result.taskId}`);
iteratorDone(); // ignore error
});
}, callback);
});
}
// auto-restart app tasks after a crash // auto-restart app tasks after a crash
function schedulePendingTasks(callback) { function schedulePendingTasks(callback) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
+42 -156
View File
@@ -11,7 +11,6 @@ exports = module.exports = {
trackFinishedSetup: trackFinishedSetup, trackFinishedSetup: trackFinishedSetup,
registerWithLoginCredentials: registerWithLoginCredentials, registerWithLoginCredentials: registerWithLoginCredentials,
registerWithLicense: registerWithLicense,
purchaseApp: purchaseApp, purchaseApp: purchaseApp,
unpurchaseApp: unpurchaseApp, unpurchaseApp: unpurchaseApp,
@@ -20,8 +19,6 @@ exports = module.exports = {
getSubscription: getSubscription, getSubscription: getSubscription,
isFreePlan: isFreePlan, isFreePlan: isFreePlan,
sendAliveStatus: sendAliveStatus,
getAppUpdate: getAppUpdate, getAppUpdate: getAppUpdate,
getBoxUpdate: getBoxUpdate, getBoxUpdate: getBoxUpdate,
@@ -34,32 +31,26 @@ var apps = require('./apps.js'),
BoxError = require('./boxerror.js'), BoxError = require('./boxerror.js'),
constants = require('./constants.js'), constants = require('./constants.js'),
debug = require('debug')('box:appstore'), debug = require('debug')('box:appstore'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'), eventlog = require('./eventlog.js'),
groups = require('./groups.js'),
mail = require('./mail.js'),
os = require('os'),
paths = require('./paths.js'), paths = require('./paths.js'),
safe = require('safetydance'), safe = require('safetydance'),
semver = require('semver'), semver = require('semver'),
settings = require('./settings.js'), settings = require('./settings.js'),
superagent = require('superagent'), superagent = require('superagent'),
users = require('./users.js'),
util = require('util'); 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 // These are the default options and will be adjusted once a subscription state is obtained
// Keep in sync with appstore/routes/cloudrons.js // Keep in sync with appstore/routes/cloudrons.js
let gFeatures = { let gFeatures = {
userMaxCount: null, userMaxCount: 5,
externalLdap: true, domainMaxCount: 1,
eventLog: true, externalLdap: false,
privateDockerRegistry: true, privateDockerRegistry: false,
branding: true, branding: false,
userManager: true, support: false,
multiAdmin: true, directoryConfig: false,
support: true mailboxMaxCount: 5,
emailPremium: false
}; };
// attempt to load feature cache in case appstore would be down // 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'; const url = settings.apiServerOrigin() + '/api/v1/register_user';
superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) { 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 (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}`)); if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `register status code: ${result.statusCode}`));
callback(null); callback(null);
@@ -235,112 +226,8 @@ function unpurchaseApp(appId, data, callback) {
}); });
} }
function sendAliveStatus(callback) { function getBoxUpdate(options, callback) {
callback = callback || NOOP_CALLBACK; assert.strictEqual(typeof options, 'object');
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) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
getCloudronToken(function (error, token) { getCloudronToken(function (error, token) {
@@ -348,7 +235,13 @@ function getBoxUpdate(callback) {
const url = `${settings.apiServerOrigin()}/api/v1/boxupdate`; 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 (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 === 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 === 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 app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
getCloudronToken(function (error, token) { getCloudronToken(function (error, token) {
if (error) return callback(error); if (error) return callback(error);
const url = `${settings.apiServerOrigin()}/api/v1/appupdate`; 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 (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 === 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 === 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))); 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); callback(null, updateInfo);
}); });
}); });
@@ -415,7 +318,7 @@ function registerCloudron(data, callback) {
superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) { 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 (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 // cloudronId, token, licenseKey
if (!result.body.cloudronId) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no cloudron id')); if (!result.body.cloudronId) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no cloudron id'));
@@ -438,18 +341,16 @@ function registerCloudron(data, callback) {
// This works without a Cloudron token as this Cloudron was not yet registered // This works without a Cloudron token as this Cloudron was not yet registered
let gBeginSetupAlreadyTracked = false; let gBeginSetupAlreadyTracked = false;
function trackBeginSetup(provider) { function trackBeginSetup() {
assert.strictEqual(typeof provider, 'string');
// avoid browser reload double tracking, not perfect since box might restart, but covers most cases and is simple // avoid browser reload double tracking, not perfect since box might restart, but covers most cases and is simple
if (gBeginSetupAlreadyTracked) return; if (gBeginSetupAlreadyTracked) return;
gBeginSetupAlreadyTracked = true; gBeginSetupAlreadyTracked = true;
const url = `${settings.apiServerOrigin()}/api/v1/helper/setup_begin`; 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 (error && !error.response) return debug(`trackBeginSetup: ${error.message}`);
if (result.statusCode !== 200) return console.error(error.message); if (result.statusCode !== 200) return debug(`trackBeginSetup: ${result.statusCode} ${error.message}`);
}); });
} }
@@ -460,23 +361,8 @@ function trackFinishedSetup(domain) {
const url = `${settings.apiServerOrigin()}/api/v1/helper/setup_finished`; const url = `${settings.apiServerOrigin()}/api/v1/helper/setup_finished`;
superagent.post(url).send({ domain }).timeout(30 * 1000).end(function (error, result) { superagent.post(url).send({ domain }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return console.error(error.message); if (error && !error.response) return debug(`trackFinishedSetup: ${error.message}`);
if (result.statusCode !== 200) return console.error(error.message); if (result.statusCode !== 200) return debug(`trackFinishedSetup: ${result.statusCode} ${error.message}`);
});
}
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);
}); });
} }
@@ -491,7 +377,7 @@ function registerWithLoginCredentials(options, callback) {
} }
getCloudronToken(function (error, token) { 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) { maybeSignup(function (error) {
if (error) return callback(error); if (error) return callback(error);
@@ -499,7 +385,7 @@ function registerWithLoginCredentials(options, callback) {
login(options.email, options.password, options.totpToken || '', function (error, result) { login(options.email, options.password, options.totpToken || '', function (error, result) {
if (error) return callback(error); 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); if (error) return callback(error);
collectAppInfoIfNeeded(function (error, result) { collectAppInfoIfNeeded(function (error, result) {
if (error) console.error('Unable to get app info', error); if (error) return callback(error);
if (result) info.app = result; if (result) info.app = result;
let url = settings.apiServerOrigin() + '/api/v1/ticket'; let url = settings.apiServerOrigin() + '/api/v1/ticket';
+41 -14
View File
@@ -17,8 +17,6 @@ exports = module.exports = {
_waitForDnsPropagation: waitForDnsPropagation _waitForDnsPropagation: waitForDnsPropagation
}; };
require('supererror')({ splatchError: true });
var addons = require('./addons.js'), var addons = require('./addons.js'),
appdb = require('./appdb.js'), appdb = require('./appdb.js'),
apps = require('./apps.js'), apps = require('./apps.js'),
@@ -37,7 +35,6 @@ var addons = require('./addons.js'),
eventlog = require('./eventlog.js'), eventlog = require('./eventlog.js'),
fs = require('fs'), fs = require('fs'),
manifestFormat = require('cloudron-manifestformat'), manifestFormat = require('cloudron-manifestformat'),
mkdirp = require('mkdirp'),
net = require('net'), net = require('net'),
os = require('os'), os = require('os'),
path = require('path'), path = require('path'),
@@ -46,6 +43,7 @@ var addons = require('./addons.js'),
rimraf = require('rimraf'), rimraf = require('rimraf'),
safe = require('safetydance'), safe = require('safetydance'),
settings = require('./settings.js'), settings = require('./settings.js'),
sftp = require('./sftp.js'),
shell = require('./shell.js'), shell = require('./shell.js'),
superagent = require('superagent'), superagent = require('superagent'),
sysinfo = require('./sysinfo.js'), sysinfo = require('./sysinfo.js'),
@@ -176,7 +174,7 @@ function createAppDir(app, callback) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
const appDir = path.join(paths.APPS_DATA_DIR, app.id); 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 })); if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error creating directory: ${error.message}`, { appDir }));
callback(null); callback(null);
@@ -580,6 +578,9 @@ function install(app, args, progressCallback, callback) {
startApp.bind(null, app), startApp.bind(null, app),
progressCallback.bind(null, { percent: 80, message: 'Configuring file manager' }),
sftp.rebuild.bind(null, {}),
progressCallback.bind(null, { percent: 85, message: 'Waiting for DNS propagation' }), progressCallback.bind(null, { percent: 85, message: 'Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app), exports._waitForDnsPropagation.bind(null, app),
@@ -741,7 +742,12 @@ function migrateDataDir(app, args, progressCallback, callback) {
debugApp(app, 'error migrating data dir : %s', error); debugApp(app, 'error migrating data dir : %s', error);
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error)); return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
} }
callback(null);
// We do this after the app has the new data commited to the database
sftp.rebuild({}, function (error) {
if (error) debug('migrateDataDir: failed to rebuild sftp addon:', error);
callback();
});
}); });
} }
@@ -776,6 +782,9 @@ function configure(app, args, progressCallback, callback) {
startApp.bind(null, app), startApp.bind(null, app),
progressCallback.bind(null, { percent: 80, message: 'Configuring file manager' }),
sftp.rebuild.bind(null, {}),
progressCallback.bind(null, { percent: 90, message: 'Configuring reverse proxy' }), progressCallback.bind(null, { percent: 90, message: 'Configuring reverse proxy' }),
configureReverseProxy.bind(null, app), configureReverseProxy.bind(null, app),
@@ -786,7 +795,8 @@ function configure(app, args, progressCallback, callback) {
debugApp(app, 'error reconfiguring : %s', error); debugApp(app, 'error reconfiguring : %s', error);
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error)); return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
} }
callback(null);
callback();
}); });
} }
@@ -853,7 +863,7 @@ function update(app, args, progressCallback, callback) {
if (newTcpPorts[portName] || newUdpPorts[portName]) return callback(null); // port still in use if (newTcpPorts[portName] || newUdpPorts[portName]) return callback(null); // port still in use
appdb.delPortBinding(currentPorts[portName], apps.PORT_TYPE_TCP, function (error) { appdb.delPortBinding(currentPorts[portName], apps.PORT_TYPE_TCP, function (error) {
if (error && error.reason === BoxError.NOT_FOUND) console.error('Portbinding does not exist in database.'); if (error && error.reason === BoxError.NOT_FOUND) debug('update: portbinding does not exist in database', error);
else if (error) return next(error); else if (error) return next(error);
// also delete from app object for further processing (the db is updated in the next step) // also delete from app object for further processing (the db is updated in the next step)
@@ -869,14 +879,17 @@ function update(app, args, progressCallback, callback) {
progressCallback.bind(null, { percent: 45, message: 'Downloading icon' }), progressCallback.bind(null, { percent: 45, message: 'Downloading icon' }),
downloadIcon.bind(null, app), downloadIcon.bind(null, app),
progressCallback.bind(null, { percent: 70, message: 'Updating addons' }), progressCallback.bind(null, { percent: 60, message: 'Updating addons' }),
addons.setupAddons.bind(null, app, updateConfig.manifest.addons), addons.setupAddons.bind(null, app, updateConfig.manifest.addons),
progressCallback.bind(null, { percent: 80, message: 'Creating container' }), progressCallback.bind(null, { percent: 70, message: 'Creating container' }),
createContainer.bind(null, app), createContainer.bind(null, app),
startApp.bind(null, app), startApp.bind(null, app),
progressCallback.bind(null, { percent: 80, message: 'Configuring file manager' }),
sftp.rebuild.bind(null, {}),
progressCallback.bind(null, { percent: 100, message: 'Done' }), progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null, updateTime: new Date() }) updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null, updateTime: new Date() })
], function seriesDone(error) { ], function seriesDone(error) {
@@ -899,7 +912,10 @@ function start(app, args, progressCallback, callback) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
async.series([ 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), docker.startContainer.bind(null, app.id),
// stopped apps do not renew certs. currently, we don't do DNS to not overwrite existing user settings // stopped apps do not renew certs. currently, we don't do DNS to not overwrite existing user settings
@@ -927,6 +943,9 @@ function stop(app, args, progressCallback, callback) {
progressCallback.bind(null, { percent: 20, message: 'Stopping container' }), progressCallback.bind(null, { percent: 20, message: 'Stopping container' }),
docker.stopContainers.bind(null, app.id), 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' }), progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null }) updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
], function seriesDone(error) { ], function seriesDone(error) {
@@ -973,16 +992,22 @@ function uninstall(app, args, progressCallback, callback) {
progressCallback.bind(null, { percent: 30, message: 'Teardown addons' }), progressCallback.bind(null, { percent: 30, message: 'Teardown addons' }),
addons.teardownAddons.bind(null, app, app.manifest.addons), addons.teardownAddons.bind(null, app, app.manifest.addons),
progressCallback.bind(null, { percent: 40, message: 'Deleting app data directory' }), progressCallback.bind(null, { percent: 40, message: 'Cleanup file manager' }),
function (callback) {
if (!app.dataDir) return callback();
sftp.rebuild({ ignoredApps: [ app.id ] }, callback);
},
progressCallback.bind(null, { percent: 50, message: 'Deleting app data directory' }),
deleteAppDir.bind(null, app, { removeDirectory: true }), deleteAppDir.bind(null, app, { removeDirectory: true }),
progressCallback.bind(null, { percent: 50, message: 'Deleting image' }), progressCallback.bind(null, { percent: 60, message: 'Deleting image' }),
docker.deleteImage.bind(null, app.manifest), docker.deleteImage.bind(null, app.manifest),
progressCallback.bind(null, { percent: 60, message: 'Unregistering domains' }), progressCallback.bind(null, { percent: 70, message: 'Unregistering domains' }),
unregisterSubdomains.bind(null, app, [ { subdomain: app.location, domain: app.domain } ].concat(app.alternateDomains)), unregisterSubdomains.bind(null, app, [ { subdomain: app.location, domain: app.domain } ].concat(app.alternateDomains)),
progressCallback.bind(null, { percent: 70, message: 'Cleanup icon' }), progressCallback.bind(null, { percent: 80, message: 'Cleanup icon' }),
removeIcon.bind(null, app), removeIcon.bind(null, app),
progressCallback.bind(null, { percent: 90, message: 'Cleanup logs' }), progressCallback.bind(null, { percent: 90, message: 'Cleanup logs' }),
@@ -1038,6 +1063,8 @@ function run(appId, args, progressCallback, callback) {
return stop(app, args, progressCallback, callback); return stop(app, args, progressCallback, callback);
case apps.ISTATE_PENDING_RESTART: case apps.ISTATE_PENDING_RESTART:
return restart(app, args, progressCallback, callback); return restart(app, args, progressCallback, callback);
case apps.ISTATE_INSTALLED: // can only happen when we have a bug in our code while testing/development
return updateApp(app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null }, callback);
default: default:
debugApp(app, 'apptask launched with invalid command'); debugApp(app, 'apptask launched with invalid command');
return callback(new BoxError(BoxError.INTERNAL_ERROR, 'Unknown install command in apptask:' + app.installationState)); return callback(new BoxError(BoxError.INTERNAL_ERROR, 'Unknown install command in apptask:' + app.installationState));
+2 -1
View File
@@ -68,7 +68,8 @@ function scheduleTask(appId, taskId, callback) {
if (!fs.existsSync(path.dirname(logFile))) safe.fs.mkdirSync(path.dirname(logFile)); // ensure directory if (!fs.existsSync(path.dirname(logFile))) safe.fs.mkdirSync(path.dirname(logFile)); // ensure directory
tasks.startTask(taskId, { logFile, timeout: 20 * 60 * 60 * 1000 /* 20 hours */ }, function (error, result) { // TODO: set memory limit for app backup task
tasks.startTask(taskId, { logFile, timeout: 20 * 60 * 60 * 1000 /* 20 hours */, nice: 15 }, function (error, result) {
callback(error, result); callback(error, result);
delete gActiveTasks[appId]; delete gActiveTasks[appId];
+25 -30
View File
@@ -6,27 +6,20 @@ var assert = require('assert'),
safe = require('safetydance'), safe = require('safetydance'),
util = require('util'); 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 = { exports = module.exports = {
add: add, add,
getByTypeAndStatePaged: getByTypeAndStatePaged, getByTypePaged,
getByTypePaged: getByTypePaged, getByIdentifierPaged,
getByIdentifierAndStatePaged,
get: get, get,
del: del, del,
update: update, update,
getByAppIdPaged: getByAppIdPaged,
_clear: clear, _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'
}; };
function postProcess(result) { function postProcess(result) {
@@ -38,15 +31,15 @@ function postProcess(result) {
delete result.manifestJson; delete result.manifestJson;
} }
function getByTypeAndStatePaged(type, state, page, perPage, callback) { function getByIdentifierAndStatePaged(identifier, state, page, perPage, callback) {
assert(type === exports.BACKUP_TYPE_APP || type === exports.BACKUP_TYPE_BOX); assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof state, 'string'); assert.strictEqual(typeof state, 'string');
assert(typeof page === 'number' && page > 0); assert(typeof page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0); assert(typeof perPage === 'number' && perPage > 0);
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? AND state = ? ORDER BY creationTime DESC LIMIT ?,?', database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE identifier = ? AND state = ? ORDER BY creationTime DESC LIMIT ?,?',
[ type, state, (page-1)*perPage, perPage ], function (error, results) { [ identifier, state, (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(function (result) { postProcess(result); }); results.forEach(function (result) { postProcess(result); });
@@ -56,7 +49,7 @@ function getByTypeAndStatePaged(type, state, page, perPage, callback) {
} }
function getByTypePaged(type, 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 page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0); assert(typeof perPage === 'number' && perPage > 0);
assert.strictEqual(typeof callback, 'function'); 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 page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0); assert(typeof perPage === 'number' && perPage > 0);
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function'); 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 identifier = ? ORDER BY creationTime DESC LIMIT ?,?',
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? AND state = ? AND id LIKE ? ORDER BY creationTime DESC LIMIT ?,?', [ identifier, (page-1)*perPage, perPage ], function (error, results) {
[ exports.BACKUP_TYPE_APP, exports.BACKUP_STATE_NORMAL, '%app%\\_' + appId + '\\_%', (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(function (result) { postProcess(result); }); results.forEach(function (result) { postProcess(result); });
@@ -106,8 +98,11 @@ function get(id, callback) {
function add(id, data, callback) { function add(id, data, callback) {
assert(data && typeof data === 'object'); assert(data && typeof data === 'object');
assert.strictEqual(typeof id, 'string'); assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof data.version, 'string'); assert(data.encryptionVersion === null || typeof data.encryptionVersion === 'number');
assert(data.type === exports.BACKUP_TYPE_APP || data.type === exports.BACKUP_TYPE_BOX); 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(util.isArray(data.dependsOn));
assert.strictEqual(typeof data.manifest, 'object'); assert.strictEqual(typeof data.manifest, 'object');
assert.strictEqual(typeof data.format, 'string'); 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 creationTime = data.creationTime || new Date(); // allow tests to set the time
var manifestJson = JSON.stringify(data.manifest); var manifestJson = JSON.stringify(data.manifest);
database.query('INSERT INTO backups (id, version, type, creationTime, state, dependsOn, manifestJson, format) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', database.query('INSERT INTO backups (id, identifier, encryptionVersion, packageVersion, type, creationTime, state, dependsOn, manifestJson, format) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[ id, data.version, data.type, creationTime, exports.BACKUP_STATE_NORMAL, data.dependsOn.join(','), manifestJson, data.format ], [ id, data.identifier, data.encryptionVersion, data.packageVersion, data.type, creationTime, data.state, data.dependsOn.join(','), manifestJson, data.format ],
function (error) { function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS)); if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
+467 -247
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -332,7 +332,7 @@ Acme2.prototype.createKeyAndCsr = function (hostname, callback) {
// in some old releases, csr file was corrupt. so always regenerate it // in some old releases, csr file was corrupt. so always regenerate it
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile); debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
} else { } 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 (!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)); if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new BoxError(BoxError.FS_ERROR, safe.error));
-22
View File
@@ -1,22 +0,0 @@
'use strict';
exports = module.exports = {
getCertificate: getCertificate,
// testing
_name: 'caas'
};
var assert = require('assert'),
debug = require('debug')('box:cert/caas.js');
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debug('getCertificate: using fallback certificate', hostname);
return callback(null, '', '');
}
-22
View File
@@ -1,22 +0,0 @@
'use strict';
exports = module.exports = {
getCertificate: getCertificate,
// testing
_name: 'fallback'
};
var assert = require('assert'),
debug = require('debug')('box:cert/fallback.js');
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debug('getCertificate: using fallback certificate', hostname);
return callback(null, '', '');
}
-24
View File
@@ -1,24 +0,0 @@
'use strict';
// -------------------------------------------
// This file just describes the interface
//
// New backends can start from here
// -------------------------------------------
exports = module.exports = {
getCertificate: getCertificate
};
var assert = require('assert'),
BoxError = require('../boxerror.js');
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
return callback(new BoxError(BoxError.NOT_IMPLEMENTED, 'getCertificate is not implemented'));
}
+17 -20
View File
@@ -18,7 +18,7 @@ exports = module.exports = {
setupDashboard: setupDashboard, setupDashboard: setupDashboard,
runSystemChecks: runSystemChecks, runSystemChecks: runSystemChecks
}; };
var addons = require('./addons.js'), var addons = require('./addons.js'),
@@ -66,7 +66,7 @@ function uninitialize(callback) {
async.series([ async.series([
cron.stopJobs, cron.stopJobs,
platform.stop platform.stopAllTasks
], callback); ], callback);
} }
@@ -78,7 +78,13 @@ function onActivated(callback) {
// 2. the restore code path can run without sudo (since mail/ is non-root) // 2. the restore code path can run without sudo (since mail/ is non-root)
async.series([ async.series([
platform.start, platform.start,
cron.startJobs cron.startJobs,
function checkBackupConfiguration(callback) {
backups.checkConfiguration(function (error, message) {
if (error) return callback(error);
notifications.alert(notifications.ALERT_BACKUP_CONFIG, 'Backup configuration is unsafe', message, callback);
});
}
], callback); ], callback);
} }
@@ -103,12 +109,15 @@ function notifyUpdate(callback) {
// each of these tasks can fail. we will add some routes to fix/re-run them // each of these tasks can fail. we will add some routes to fix/re-run them
function runStartupTasks() { function runStartupTasks() {
// stop all the systemd tasks
platform.stopAllTasks(NOOP_CALLBACK);
// configure nginx to be reachable by IP // configure nginx to be reachable by IP
reverseProxy.writeDefaultConfig(NOOP_CALLBACK); reverseProxy.writeDefaultConfig(NOOP_CALLBACK);
// this configures collectd to collect backup storage metrics if filesystem is used. This is also triggerd when the settings change with the rest api // this configures collectd to collect backup storage metrics if filesystem is used. This is also triggerd when the settings change with the rest api
settings.getBackupConfig(function (error, backupConfig) { settings.getBackupConfig(function (error, backupConfig) {
if (error) return console.error('Failed to read backup config.', error); if (error) return debug('runStartupTasks: failed to get backup config.', error);
backups.configureCollectd(backupConfig, NOOP_CALLBACK); backups.configureCollectd(backupConfig, NOOP_CALLBACK);
}); });
@@ -139,17 +148,18 @@ function getConfig(callback) {
mailFqdn: settings.mailFqdn(), mailFqdn: settings.mailFqdn(),
version: constants.VERSION, version: constants.VERSION,
isDemo: settings.isDemo(), isDemo: settings.isDemo(),
provider: settings.provider(),
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY], cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
footer: allSettings[settings.FOOTER_KEY] || constants.FOOTER, 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
}); });
}); });
} }
function reboot(callback) { function reboot(callback) {
notifications.alert(notifications.ALERT_REBOOT, 'Reboot Required', '', function (error) { notifications.alert(notifications.ALERT_REBOOT, 'Reboot Required', '', function (error) {
if (error) console.error('Failed to clear reboot notification.', error); if (error) debug('reboot: failed to clear reboot notification.', error);
shell.sudo('reboot', [ REBOOT_CMD ], {}, callback); shell.sudo('reboot', [ REBOOT_CMD ], {}, callback);
}); });
@@ -167,24 +177,11 @@ function runSystemChecks(callback) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
async.parallel([ async.parallel([
checkBackupConfiguration,
checkMailStatus, checkMailStatus,
checkRebootRequired checkRebootRequired
], callback); ], callback);
} }
function checkBackupConfiguration(callback) {
assert.strictEqual(typeof callback, 'function');
debug('checking backup configuration');
backups.checkConfiguration(function (error, message) {
if (error) return callback(error);
notifications.alert(notifications.ALERT_BACKUP_CONFIG, 'Backup configuration is unsafe', message, callback);
});
}
function checkMailStatus(callback) { function checkMailStatus(callback) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
+2 -1
View File
@@ -37,10 +37,11 @@ exports = module.exports = {
DEFAULT_MEMORY_LIMIT: (256 * 1024 * 1024), // see also client.js DEFAULT_MEMORY_LIMIT: (256 * 1024 * 1024), // see also client.js
DEMO_USERNAME: 'cloudron', DEMO_USERNAME: 'cloudron',
DEMO_BLACKLISTED_APPS: [ 'com.github.cloudtorrent' ],
AUTOUPDATE_PATTERN_NEVER: 'never', 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, CLOUDRON: CLOUDRON,
TEST: TEST, TEST: TEST,
+23 -26
View File
@@ -1,16 +1,25 @@
'use strict'; '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_AUTOUPDATE_PATTERN = '00 00 1,3,5,23 * * *',
DEFAULT_APP_AUTOUPDATE_PATTERN = '00 15 1,3,5,23 * * *';
exports = module.exports = { 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'), var appHealthMonitor = require('./apphealthmonitor.js'),
apps = require('./apps.js'), apps = require('./apps.js'),
appstore = require('./appstore.js'),
assert = require('assert'), assert = require('assert'),
async = require('async'), async = require('async'),
auditSource = require('./auditsource.js'), auditSource = require('./auditsource.js'),
@@ -29,7 +38,6 @@ var appHealthMonitor = require('./apphealthmonitor.js'),
updateChecker = require('./updatechecker.js'); updateChecker = require('./updatechecker.js');
var gJobs = { var gJobs = {
alive: null, // send periodic stats
appAutoUpdater: null, appAutoUpdater: null,
boxAutoUpdater: null, boxAutoUpdater: null,
appUpdateChecker: null, appUpdateChecker: null,
@@ -61,14 +69,8 @@ function startJobs(callback) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
const randomMinute = Math.floor(60*Math.random()); 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({ gJobs.systemChecks = new CronJob({
cronTime: '00 30 * * * *', // every 30 minutes. if you change this interval, change the notification messages with correct duration cronTime: '00 30 2 * * *', // once a day. if you change this interval, change the notification messages with correct duration
onTick: () => cloudron.runSystemChecks(NOOP_CALLBACK), onTick: () => cloudron.runSystemChecks(NOOP_CALLBACK),
start: true start: true
}); });
@@ -80,14 +82,15 @@ function startJobs(callback) {
}); });
gJobs.boxUpdateCheckerJob = new CronJob({ gJobs.boxUpdateCheckerJob = new CronJob({
cronTime: '00 ' + randomMinute + ' * * * *', // once an hour cronTime: '00 ' + randomMinute + ' 1,3,5,21,23 * * *', // 5 times
onTick: () => updateChecker.checkBoxUpdates(NOOP_CALLBACK), onTick: () => updateChecker.checkBoxUpdates({ automatic: true }, NOOP_CALLBACK),
start: true start: true
}); });
// this is run separately from the update itself so that the user can disable automatic updates but can still get a notification
gJobs.appUpdateChecker = new CronJob({ gJobs.appUpdateChecker = new CronJob({
cronTime: '00 ' + randomMinute + ' * * * *', // once an hour cronTime: '00 ' + randomMinute + ' 2,4,6,20,22 * * *', // 5 times
onTick: () => updateChecker.checkAppUpdates(NOOP_CALLBACK), onTick: () => updateChecker.checkAppUpdates({ automatic: true }, NOOP_CALLBACK),
start: true start: true
}); });
@@ -98,7 +101,7 @@ function startJobs(callback) {
}); });
gJobs.cleanupBackups = new CronJob({ 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), onTick: backups.startCleanupTask.bind(null, auditSource.CRON, NOOP_CALLBACK),
start: true start: true
}); });
@@ -172,19 +175,13 @@ function backupConfigChanged(value, tz) {
assert.strictEqual(typeof value, 'object'); assert.strictEqual(typeof value, 'object');
assert.strictEqual(typeof tz, 'string'); assert.strictEqual(typeof tz, 'string');
debug(`backupConfigChanged: interval ${value.intervalSecs} (${tz})`); debug(`backupConfigChanged: schedule ${value.schedulePattern} (${tz})`);
if (gJobs.backup) gJobs.backup.stop(); 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
} else {
pattern = '00 00 1,3,5,23 * * *'; // avoid middle of the day backups
}
gJobs.backup = new CronJob({ gJobs.backup = new CronJob({
cronTime: pattern, cronTime: value.schedulePattern,
onTick: backups.ensureBackup.bind(null, auditSource.CRON, NOOP_CALLBACK), onTick: backups.startBackupTask.bind(null, auditSource.CRON, NOOP_CALLBACK),
start: true, start: true,
timeZone: tz timeZone: tz
}); });
+38 -97
View File
@@ -17,12 +17,12 @@ var assert = require('assert'),
BoxError = require('./boxerror.js'), BoxError = require('./boxerror.js'),
child_process = require('child_process'), child_process = require('child_process'),
constants = require('./constants.js'), constants = require('./constants.js'),
debug = require('debug')('box:database'),
mysql = require('mysql'), mysql = require('mysql'),
once = require('once'), once = require('once'),
util = require('util'); util = require('util');
var gConnectionPool = null, var gConnectionPool = null;
gDefaultConnection = null;
const gDatabase = { const gDatabase = {
hostname: '127.0.0.1', 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(); 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({ 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, host: gDatabase.hostname,
user: gDatabase.username, user: gDatabase.username,
password: gDatabase.password, password: gDatabase.password,
port: gDatabase.port, port: gDatabase.port,
database: gDatabase.name, database: gDatabase.name,
multipleStatements: false, multipleStatements: false,
waitForConnections: true, // getConnection() will wait until a connection is avaiable
ssl: false, ssl: false,
timezone: 'Z' // mysql follows the SYSTEM timezone. on Cloudron, this is UTC timezone: 'Z' // mysql follows the SYSTEM timezone. on Cloudron, this is UTC
}); });
gConnectionPool.on('connection', function (connection) { 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('USE ' + gDatabase.name);
connection.query('SET SESSION sql_mode = \'strict_all_tables\''); connection.query('SET SESSION sql_mode = \'strict_all_tables\'');
}); });
reconnect(callback); callback(null);
} }
function uninitialize(callback) { function uninitialize(callback) {
if (gConnectionPool) { if (!gConnectionPool) return callback(null);
gConnectionPool.end(callback);
gConnectionPool = null;
} else {
callback(null);
}
}
function reconnect(callback) { gConnectionPool.end(callback);
callback = callback ? once(callback) : function () {}; gConnectionPool = null;
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);
});
} }
function clear(callback) { function clear(callback) {
@@ -107,80 +85,43 @@ function clear(callback) {
child_process.exec(cmd, 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() { function query() {
var args = Array.prototype.slice.call(arguments); const args = Array.prototype.slice.call(arguments);
var callback = args[args.length - 1]; const callback = args[args.length - 1];
assert.strictEqual(typeof callback, 'function'); 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) { gConnectionPool.query.apply(gConnectionPool, args); // this is same as getConnection/query/release
if (error && error.fatal) {
gDefaultConnection = null;
setTimeout(reconnect, 1000);
}
callback(error, result);
};
gDefaultConnection.query.apply(gDefaultConnection, args);
} }
function transaction(queries, callback) { function transaction(queries, callback) {
assert(util.isArray(queries)); assert(util.isArray(queries));
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
beginTransaction(function (error, conn) { callback = once(callback);
gConnectionPool.getConnection(function (error, connection) {
if (error) return callback(error); if (error) return callback(error);
async.mapSeries(queries, function iterator(query, done) { const releaseConnection = (error) => { connection.release(); callback(error); };
conn.query(query.query, query.args, done);
}, function seriesDone(error, results) {
if (error) return rollback(conn, callback.bind(null, 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 // 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 // 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}"`; var cmd = `/usr/bin/mysqldump -h "${gDatabase.hostname}" -u root -p${gDatabase.password} ${disableColStats} --single-transaction --routines --triggers ${gDatabase.name} > "${file}"`;
-176
View File
@@ -1,176 +0,0 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:dns/caas'),
domains = require('../domains.js'),
settings = require('../settings.js'),
superagent = require('superagent'),
util = require('util'),
waitForDns = require('./waitfordns.js');
function formatError(response) {
return util.format('Caas DNS error [%s] %j', response.statusCode, response.body);
}
function getFqdn(location, domain) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
return (location === '') ? domain : location + '-' + domain;
}
function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER;
// do not return the 'key'. in caas, this is private
delete domainObject.fallbackCertificate.key;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
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;
let fqdn = location !== '' && type === 'TXT' ? location + '.' + domainObject.domain : getFqdn(location, domainObject.domain);
debug('add: %s for zone %s of type %s with values %j', location, domainObject.domain, type, values);
var data = {
type: type,
values: values
};
superagent
.post(settings.apiServerOrigin() + '/api/v1/caas/domains/' + fqdn)
.query({ token: dnsConfig.token })
.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 === 400) return callback(new BoxError(BoxError.BAD_FIELD, result.body.message));
if (result.statusCode === 420) return callback(new BoxError(BoxError.BUSY));
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
return callback(null);
});
}
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;
const fqdn = location !== '' && type === 'TXT' ? location + '.' + domainObject.domain : getFqdn(location, domainObject.domain);
debug('get: zoneName: %s subdomain: %s type: %s fqdn: %s', domainObject.domain, location, type, fqdn);
superagent
.get(settings.apiServerOrigin() + '/api/v1/caas/domains/' + fqdn)
.query({ token: dnsConfig.token, type: type })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
return callback(null, result.body.values);
});
}
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;
debug('del: %s for zone %s of type %s with values %j', location, domainObject.domain, type, values);
var data = {
type: type,
values: values
};
superagent
.del(settings.apiServerOrigin() + '/api/v1/caas/domains/' + getFqdn(location, domainObject.domain))
.query({ token: dnsConfig.token })
.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 === 400) return callback(new BoxError(BoxError.BAD_FIELD, result.body.message));
if (result.statusCode === 420) return callback(new BoxError(BoxError.BUSY));
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
if (result.statusCode !== 204) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
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;
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,
hyphenatedSubdomains: true // this will ensure we always use them, regardless of passed-in configs
};
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);
});
});
}
+12 -6
View File
@@ -13,6 +13,7 @@ exports = module.exports = {
var assert = require('assert'), var assert = require('assert'),
async = require('async'), async = require('async'),
BoxError = require('../boxerror.js'), BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/cloudflare'), debug = require('debug')('box:dns/cloudflare'),
dns = require('../native-dns.js'), dns = require('../native-dns.js'),
domains = require('../domains.js'), domains = require('../domains.js'),
@@ -25,12 +26,12 @@ var assert = require('assert'),
var CLOUDFLARE_ENDPOINT = 'https://api.cloudflare.com/client/v4'; var CLOUDFLARE_ENDPOINT = 'https://api.cloudflare.com/client/v4';
function removePrivateFields(domainObject) { function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER; domainObject.config.token = constants.SECRET_PLACEHOLDER;
return domainObject; return domainObject;
} }
function injectPrivateFields(newConfig, currentConfig) { 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) { 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 === 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 === 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) { if (result.statusCode === 400 || result.statusCode === 401 || result.statusCode === 403) {
let error = result.body.errors[0]; let message = 'Unknown error';
let message = `message: ${error.message} statusCode: ${result.statusCode} code:${error.code}`; 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)); 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' && dnsConfig.tokenType !== 'ApiToken') return callback(new BoxError(BoxError.BAD_FIELD, 'tokenType is required', { field: 'tokenType' }));
if (dnsConfig.tokenType === 'GlobalApiKey') { 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'; const ip = '127.0.0.1';
+3 -2
View File
@@ -13,6 +13,7 @@ exports = module.exports = {
var assert = require('assert'), var assert = require('assert'),
async = require('async'), async = require('async'),
BoxError = require('../boxerror.js'), BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/digitalocean'), debug = require('debug')('box:dns/digitalocean'),
dns = require('../native-dns.js'), dns = require('../native-dns.js'),
domains = require('../domains.js'), domains = require('../domains.js'),
@@ -28,12 +29,12 @@ function formatError(response) {
} }
function removePrivateFields(domainObject) { function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER; domainObject.config.token = constants.SECRET_PLACEHOLDER;
return domainObject; return domainObject;
} }
function injectPrivateFields(newConfig, currentConfig) { 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) { function getInternal(dnsConfig, zoneName, name, type, callback) {
+3 -2
View File
@@ -12,6 +12,7 @@ exports = module.exports = {
var assert = require('assert'), var assert = require('assert'),
BoxError = require('../boxerror.js'), BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/gandi'), debug = require('debug')('box:dns/gandi'),
dns = require('../native-dns.js'), dns = require('../native-dns.js'),
domains = require('../domains.js'), domains = require('../domains.js'),
@@ -26,12 +27,12 @@ function formatError(response) {
} }
function removePrivateFields(domainObject) { function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER; domainObject.config.token = constants.SECRET_PLACEHOLDER;
return domainObject; return domainObject;
} }
function injectPrivateFields(newConfig, currentConfig) { 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) { function upsert(domainObject, location, type, values, callback) {
+3 -2
View File
@@ -12,6 +12,7 @@ exports = module.exports = {
var assert = require('assert'), var assert = require('assert'),
BoxError = require('../boxerror.js'), BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/gcdns'), debug = require('debug')('box:dns/gcdns'),
dns = require('../native-dns.js'), dns = require('../native-dns.js'),
domains = require('../domains.js'), domains = require('../domains.js'),
@@ -21,12 +22,12 @@ var assert = require('assert'),
_ = require('underscore'); _ = require('underscore');
function removePrivateFields(domainObject) { function removePrivateFields(domainObject) {
domainObject.config.credentials.private_key = domains.SECRET_PLACEHOLDER; domainObject.config.credentials.private_key = constants.SECRET_PLACEHOLDER;
return domainObject; return domainObject;
} }
function injectPrivateFields(newConfig, currentConfig) { 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) { function getDnsCredentials(dnsConfig) {
+3 -2
View File
@@ -12,6 +12,7 @@ exports = module.exports = {
var assert = require('assert'), var assert = require('assert'),
BoxError = require('../boxerror.js'), BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/godaddy'), debug = require('debug')('box:dns/godaddy'),
dns = require('../native-dns.js'), dns = require('../native-dns.js'),
domains = require('../domains.js'), domains = require('../domains.js'),
@@ -32,12 +33,12 @@ function formatError(response) {
} }
function removePrivateFields(domainObject) { function removePrivateFields(domainObject) {
domainObject.config.apiSecret = domains.SECRET_PLACEHOLDER; domainObject.config.apiSecret = constants.SECRET_PLACEHOLDER;
return domainObject; return domainObject;
} }
function injectPrivateFields(newConfig, currentConfig) { 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) { function upsert(domainObject, location, type, values, callback) {
+2 -2
View File
@@ -21,13 +21,13 @@ var assert = require('assert'),
util = require('util'); util = require('util');
function removePrivateFields(domainObject) { 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; return domainObject;
} }
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
function injectPrivateFields(newConfig, currentConfig) { 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) { function upsert(domainObject, location, type, values, callback) {
+3 -2
View File
@@ -12,6 +12,7 @@ exports = module.exports = {
let async = require('async'), let async = require('async'),
assert = require('assert'), assert = require('assert'),
constants = require('../constants.js'),
BoxError = require('../boxerror.js'), BoxError = require('../boxerror.js'),
debug = require('debug')('box:dns/linode'), debug = require('debug')('box:dns/linode'),
dns = require('../native-dns.js'), dns = require('../native-dns.js'),
@@ -27,12 +28,12 @@ function formatError(response) {
} }
function removePrivateFields(domainObject) { function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER; domainObject.config.token = constants.SECRET_PLACEHOLDER;
return domainObject; return domainObject;
} }
function injectPrivateFields(newConfig, currentConfig) { 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 getZoneId(dnsConfig, zoneName, callback) { function getZoneId(dnsConfig, zoneName, callback) {
+3 -2
View File
@@ -12,6 +12,7 @@ exports = module.exports = {
var assert = require('assert'), var assert = require('assert'),
BoxError = require('../boxerror.js'), BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/namecheap'), debug = require('debug')('box:dns/namecheap'),
dns = require('../native-dns.js'), dns = require('../native-dns.js'),
domains = require('../domains.js'), domains = require('../domains.js'),
@@ -25,12 +26,12 @@ var assert = require('assert'),
const ENDPOINT = 'https://api.namecheap.com/xml.response'; const ENDPOINT = 'https://api.namecheap.com/xml.response';
function removePrivateFields(domainObject) { function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER; domainObject.config.token = constants.SECRET_PLACEHOLDER;
return domainObject; return domainObject;
} }
function injectPrivateFields(newConfig, currentConfig) { 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) { function getQuery(dnsConfig, callback) {
+11 -2
View File
@@ -12,6 +12,7 @@ exports = module.exports = {
var assert = require('assert'), var assert = require('assert'),
BoxError = require('../boxerror.js'), BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/namecom'), debug = require('debug')('box:dns/namecom'),
dns = require('../native-dns.js'), dns = require('../native-dns.js'),
domains = require('../domains.js'), domains = require('../domains.js'),
@@ -27,12 +28,12 @@ function formatError(response) {
} }
function removePrivateFields(domainObject) { function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER; domainObject.config.token = constants.SECRET_PLACEHOLDER;
return domainObject; return domainObject;
} }
function injectPrivateFields(newConfig, currentConfig) { 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) { function addRecord(dnsConfig, zoneName, name, type, values, callback) {
@@ -54,6 +55,10 @@ function addRecord(dnsConfig, zoneName, name, type, values, callback) {
if (type === 'MX') { if (type === 'MX') {
data.priority = parseInt(values[0].split(' ')[0], 10); data.priority = parseInt(values[0].split(' ')[0], 10);
data.answer = values[0].split(' ')[1]; 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 { } else {
data.answer = values[0]; data.answer = values[0];
} }
@@ -91,6 +96,10 @@ function updateRecord(dnsConfig, zoneName, recordId, name, type, values, callbac
if (type === 'MX') { if (type === 'MX') {
data.priority = parseInt(values[0].split(' ')[0], 10); data.priority = parseInt(values[0].split(' ')[0], 10);
data.answer = values[0].split(' ')[1]; 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 { } else {
data.answer = values[0]; data.answer = values[0];
} }
+6 -4
View File
@@ -13,6 +13,7 @@ exports = module.exports = {
var assert = require('assert'), var assert = require('assert'),
AWS = require('aws-sdk'), AWS = require('aws-sdk'),
BoxError = require('../boxerror.js'), BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/route53'), debug = require('debug')('box:dns/route53'),
dns = require('../native-dns.js'), dns = require('../native-dns.js'),
domains = require('../domains.js'), domains = require('../domains.js'),
@@ -21,12 +22,12 @@ var assert = require('assert'),
_ = require('underscore'); _ = require('underscore');
function removePrivateFields(domainObject) { function removePrivateFields(domainObject) {
domainObject.config.secretAccessKey = domains.SECRET_PLACEHOLDER; domainObject.config.secretAccessKey = constants.SECRET_PLACEHOLDER;
return domainObject; return domainObject;
} }
function injectPrivateFields(newConfig, currentConfig) { 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) { function getDnsCredentials(dnsConfig) {
@@ -280,13 +281,14 @@ function verifyDnsConfig(domainObject, callback) {
} }
const location = 'cloudrontestdns'; const location = 'cloudrontestdns';
const newDomainObject = Object.assign({ }, domainObject, { config: credentials });
upsert(domainObject, location, 'A', [ ip ], function (error) { upsert(newDomainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error); if (error) return callback(error);
debug('verifyDnsConfig: Test A record added'); debug('verifyDnsConfig: Test A record added');
del(domainObject, location, 'A', [ ip ], function (error) { del(newDomainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error); if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again'); debug('verifyDnsConfig: Test A record removed again');
+32 -44
View File
@@ -6,8 +6,6 @@ exports = module.exports = {
injectPrivateFields: injectPrivateFields, injectPrivateFields: injectPrivateFields,
removePrivateFields: removePrivateFields, removePrivateFields: removePrivateFields,
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8),
ping: ping, ping: ping,
info: info, info: info,
@@ -55,12 +53,6 @@ const CLEARVOLUME_CMD = path.join(__dirname, 'scripts/clearvolume.sh'),
const DOCKER_SOCKET_PATH = '/var/run/docker.sock'; const DOCKER_SOCKET_PATH = '/var/run/docker.sock';
const gConnection = new Docker({ socketPath: DOCKER_SOCKET_PATH }); 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) { function testRegistryConfig(auth, callback) {
assert.strictEqual(typeof auth, 'object'); assert.strictEqual(typeof auth, 'object');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
@@ -73,13 +65,13 @@ function testRegistryConfig(auth, callback) {
} }
function injectPrivateFields(newConfig, currentConfig) { 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) { function removePrivateFields(registryConfig) {
assert.strictEqual(typeof registryConfig, 'object'); assert.strictEqual(typeof registryConfig, 'object');
if (registryConfig.password) registryConfig.password = exports.SECRET_PLACEHOLDER; if (registryConfig.password) registryConfig.password = constants.SECRET_PLACEHOLDER;
return registryConfig; return registryConfig;
} }
@@ -188,6 +180,19 @@ function downloadImage(manifest, callback) {
}, 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) { function createSubcontainer(app, name, cmd, options, callback) {
assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof name, 'string'); assert.strictEqual(typeof name, 'string');
@@ -277,6 +282,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
}, },
HostConfig: { HostConfig: {
Mounts: addons.getMountsSync(app, app.manifest.addons), Mounts: addons.getMountsSync(app, app.manifest.addons),
Binds: getBindsSync(app), // ideally, we have to use 'Mounts' but we have to create volumes then
LogConfig: { LogConfig: {
Type: 'syslog', Type: 'syslog',
Config: { Config: {
@@ -299,7 +305,9 @@ function createSubcontainer(app, name, cmd, options, callback) {
NetworkMode: 'cloudron', // user defined bridge network NetworkMode: 'cloudron', // user defined bridge network
Dns: ['172.18.0.1'], // use internal dns Dns: ['172.18.0.1'], // use internal dns
DnsSearch: ['.'], // use internal dns DnsSearch: ['.'], // use internal dns
SecurityOpt: [ 'apparmor=docker-cloudron-app' ] SecurityOpt: [ 'apparmor=docker-cloudron-app' ],
CapAdd: [],
CapDrop: []
}, },
NetworkingConfig: { NetworkingConfig: {
EndpointsConfig: { EndpointsConfig: {
@@ -311,16 +319,14 @@ function createSubcontainer(app, name, cmd, options, callback) {
}; };
var capabilities = manifest.capabilities || []; var capabilities = manifest.capabilities || [];
if (capabilities.includes('net_admin')) {
containerOptions.HostConfig.CapAdd = [ // https://docs-stage.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
'NET_ADMIN' 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); containerOptions = _.extend(containerOptions, options);
debugApp(app, 'Creating container for %s', app.manifest.dockerImage);
gConnection.createContainer(containerOptions, function (error, container) { gConnection.createContainer(containerOptions, function (error, container) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error)); if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
@@ -338,7 +344,6 @@ function startContainer(containerId, callback) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
var container = gConnection.getContainer(containerId); var container = gConnection.getContainer(containerId);
debug('Starting container %s', containerId);
container.start(function (error) { container.start(function (error) {
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND)); 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'); assert.strictEqual(typeof callback, 'function');
var container = gConnection.getContainer(containerId); var container = gConnection.getContainer(containerId);
debug('Restarting container %s', containerId);
container.restart(function (error) { container.restart(function (error) {
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND)); 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); var container = gConnection.getContainer(containerId);
debug('Stopping container %s', containerId);
var options = { var options = {
t: 10 // wait for 10 seconds before killing it t: 10 // wait for 10 seconds before killing it
@@ -384,13 +387,9 @@ function stopContainer(containerId, callback) {
container.stop(options, function (error) { 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)); 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)); 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); return callback(null);
}); });
}); });
@@ -400,8 +399,6 @@ function deleteContainer(containerId, callback) {
assert(!containerId || typeof containerId === 'string'); assert(!containerId || typeof containerId === 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
debug('deleting container %s', containerId);
if (containerId === null) return callback(null); if (containerId === null) return callback(null);
var container = gConnection.getContainer(containerId); var container = gConnection.getContainer(containerId);
@@ -428,8 +425,6 @@ function deleteContainers(appId, options, callback) {
assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
debug('deleting containers of %s', appId);
let labels = [ 'appId=' + appId ]; let labels = [ 'appId=' + appId ];
if (options.managedOnly) labels.push('isCloudronManaged=true'); if (options.managedOnly) labels.push('isCloudronManaged=true');
@@ -446,8 +441,6 @@ function stopContainers(appId, callback) {
assert.strictEqual(typeof appId, 'string'); assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
debug('Stopping containers of %s', appId);
gConnection.listContainers({ all: 1, filters: JSON.stringify({ label: [ 'appId=' + appId ] }) }, function (error, containers) { gConnection.listContainers({ all: 1, filters: JSON.stringify({ label: [ 'appId=' + appId ] }) }, function (error, containers) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error)); if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
@@ -514,7 +507,7 @@ function inspect(containerId, callback) {
var container = gConnection.getContainer(containerId); var container = gConnection.getContainer(containerId);
container.inspect(function (error, result) { container.inspect(function (error, result) {
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND)); if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND, `Unable to find container ${containerId}`));
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error)); if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
callback(null, result); callback(null, result);
@@ -573,10 +566,10 @@ function memoryUsage(containerId, callback) {
}); });
} }
function createVolume(app, name, volumeDataDir, callback) { function createVolume(name, volumeDataDir, labels, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof name, 'string'); assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof volumeDataDir, 'string'); assert.strictEqual(typeof volumeDataDir, 'string');
assert.strictEqual(typeof labels, 'object');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
const volumeOptions = { const volumeOptions = {
@@ -587,10 +580,7 @@ function createVolume(app, name, volumeDataDir, callback) {
device: volumeDataDir, device: volumeDataDir,
o: 'bind' o: 'bind'
}, },
Labels: { Labels: labels
'fqdn': app.fqdn,
'appId': app.id
},
}; };
// requires sudo because the path can be outside appsdata // 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) { function clearVolume(name, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof name, 'string'); assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
@@ -626,14 +615,13 @@ function clearVolume(app, name, options, callback) {
} }
// this only removes the volume and not the data // this only removes the volume and not the data
function removeVolume(app, name, callback) { function removeVolume(name, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof name, 'string'); assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
let volume = gConnection.getVolume(name); let volume = gConnection.getVolume(name);
volume.remove(function (error) { 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(); callback();
}); });
+1 -1
View File
@@ -55,7 +55,7 @@ function attachDockerRequest(req, res, next) {
// Force node to send out the headers, this is required for the /container/wait api to make the docker cli proceed // Force node to send out the headers, this is required for the /container/wait api to make the docker cli proceed
res.write(' '); res.write(' ');
dockerResponse.on('error', function (error) { console.error('dockerResponse error:', error); }); dockerResponse.on('error', function (error) { debug('dockerResponse error:', error); });
dockerResponse.pipe(res, { end: true }); dockerResponse.pipe(res, { end: true });
}); });
+1 -1
View File
@@ -66,7 +66,7 @@ function add(name, data, callback) {
]; ];
database.transaction(queries, function (error) { database.transaction(queries, function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, 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)); if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null); callback(null);
+2 -6
View File
@@ -28,9 +28,7 @@ module.exports = exports = {
checkDnsRecords: checkDnsRecords, checkDnsRecords: checkDnsRecords,
prepareDashboardDomain: prepareDashboardDomain, prepareDashboardDomain: prepareDashboardDomain
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8)
}; };
var assert = require('assert'), var assert = require('assert'),
@@ -56,7 +54,6 @@ function api(provider) {
assert.strictEqual(typeof provider, 'string'); assert.strictEqual(typeof provider, 'string');
switch (provider) { switch (provider) {
case 'caas': return require('./dns/caas.js');
case 'cloudflare': return require('./dns/cloudflare.js'); case 'cloudflare': return require('./dns/cloudflare.js');
case 'route53': return require('./dns/route53.js'); case 'route53': return require('./dns/route53.js');
case 'gcdns': return require('./dns/gcdns.js'); case 'gcdns': return require('./dns/gcdns.js');
@@ -151,10 +148,9 @@ function validateTlsConfig(tlsConfig, dnsProvider) {
case 'letsencrypt-prod': case 'letsencrypt-prod':
case 'letsencrypt-staging': case 'letsencrypt-staging':
case 'fallback': case 'fallback':
case 'caas':
break; break;
default: default:
return new BoxError(BoxError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback, letsencrypt-prod/staging', { field: 'tlsProvider' }); return new BoxError(BoxError.BAD_FIELD, 'tlsConfig.provider must be fallback, letsencrypt-prod/staging', { field: 'tlsProvider' });
} }
if (tlsConfig.wildcard) { if (tlsConfig.wildcard) {
+332 -66
View File
@@ -20,7 +20,9 @@ var assert = require('assert'),
BoxError = require('./boxerror.js'), BoxError = require('./boxerror.js'),
constants = require('./constants.js'), constants = require('./constants.js'),
debug = require('debug')('box:externalldap'), debug = require('debug')('box:externalldap'),
groups = require('./groups.js'),
ldap = require('ldapjs'), ldap = require('ldapjs'),
once = require('once'),
settings = require('./settings.js'), settings = require('./settings.js'),
tasks = require('./tasks.js'), tasks = require('./tasks.js'),
users = require('./users.js'); users = require('./users.js');
@@ -40,14 +42,14 @@ function translateUser(ldapConfig, ldapUser) {
return { return {
username: ldapUser[ldapConfig.usernameField], username: ldapUser[ldapConfig.usernameField],
email: ldapUser.mail, email: ldapUser.mail || ldapUser.mailPrimaryAddress,
displayName: ldapUser.cn // user.giveName + ' ' + user.sn displayName: ldapUser.cn // user.giveName + ' ' + user.sn
}; };
} }
function validUserRequirements(user) { function validUserRequirements(user) {
if (!user.username || !user.email || !user.displayName) { 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; return false;
} else { } else {
return true; return true;
@@ -55,40 +57,95 @@ function validUserRequirements(user) {
} }
// performs service bind if required // performs service bind if required
function getClient(externalLdapConfig, callback) { function getClient(externalLdapConfig, doBindAuth, callback) {
assert.strictEqual(typeof externalLdapConfig, 'object'); assert.strictEqual(typeof externalLdapConfig, 'object');
assert.strictEqual(typeof doBindAuth, 'boolean');
assert.strictEqual(typeof callback, 'function'); 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 // basic validation to not crash
try { ldap.parseDN(externalLdapConfig.baseDn); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid baseDn')); } 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')); } 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; var client;
try { try {
client = ldap.createClient({ url: externalLdapConfig.url }); client = ldap.createClient(config);
} catch (e) { } catch (e) {
if (e instanceof ldap.ProtocolError) return callback(new BoxError(BoxError.BAD_FIELD, 'url protocol is invalid')); if (e instanceof ldap.ProtocolError) return callback(new BoxError(BoxError.BAD_FIELD, 'url protocol is invalid'));
return callback(new BoxError(BoxError.INTERNAL_ERROR, e)); 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) { client.bind(externalLdapConfig.bindDn, externalLdapConfig.bindPassword, function (error) {
if (error instanceof ldap.InvalidCredentialsError) return callback(new BoxError(BoxError.INVALID_CREDENTIALS)); if (error instanceof ldap.InvalidCredentialsError) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error)); 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}`);
// basic validation to not crash
try { ldap.parseDN(dn); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid 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 // TODO support search by email
function ldapSearch(externalLdapConfig, options, callback) { function ldapUserSearch(externalLdapConfig, options, callback) {
assert.strictEqual(typeof externalLdapConfig, 'object'); assert.strictEqual(typeof externalLdapConfig, 'object');
assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
getClient(externalLdapConfig, function (error, client) { getClient(externalLdapConfig, true, function (error, client) {
if (error) return callback(error); if (error) return callback(error);
let searchOptions = { let searchOptions = {
@@ -124,6 +181,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) { function testConfig(config, callback) {
assert.strictEqual(typeof config, 'object'); assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
@@ -141,7 +240,20 @@ function testConfig(config, callback) {
if (!config.filter) return callback(new BoxError(BoxError.BAD_FIELD, 'filter must not be empty')); 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')); } 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); if (error) return callback(error);
var opts = { var opts = {
@@ -167,7 +279,7 @@ function search(identifier, callback) {
if (error) return callback(error); if (error) return callback(error);
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled')); 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); if (error) return callback(error);
// translate ldap properties to ours // translate ldap properties to ours
@@ -188,7 +300,7 @@ function createAndVerifyUserIfNotExist(identifier, password, callback) {
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled')); 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')); 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 (error) return callback(error);
if (ldapUsers.length === 0) return callback(new BoxError(BoxError.NOT_FOUND)); if (ldapUsers.length === 0) return callback(new BoxError(BoxError.NOT_FOUND));
if (ldapUsers.length > 1) return callback(new BoxError(BoxError.CONFLICT)); if (ldapUsers.length > 1) return callback(new BoxError(BoxError.CONFLICT));
@@ -198,7 +310,7 @@ function createAndVerifyUserIfNotExist(identifier, password, callback) {
users.create(user.username, null /* password */, user.email, user.displayName, { source: 'ldap' }, auditSource.EXTERNAL_LDAP_AUTO_CREATE, function (error, user) { users.create(user.username, null /* password */, user.email, user.displayName, { source: 'ldap' }, auditSource.EXTERNAL_LDAP_AUTO_CREATE, function (error, user) {
if (error) { if (error) {
console.error('Failed to auto create user', user.username, error); debug(`createAndVerifyUserIfNotExist: Failed to auto create user ${user.username}`, error);
return callback(new BoxError(BoxError.INTERNAL_ERROR)); return callback(new BoxError(BoxError.INTERNAL_ERROR));
} }
@@ -220,17 +332,20 @@ function verifyPassword(user, password, callback) {
if (error) return callback(error); if (error) return callback(error);
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled')); 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 (error) return callback(error);
if (ldapUsers.length === 0) return callback(new BoxError(BoxError.NOT_FOUND)); if (ldapUsers.length === 0) return callback(new BoxError(BoxError.NOT_FOUND));
if (ldapUsers.length > 1) return callback(new BoxError(BoxError.CONFLICT)); if (ldapUsers.length > 1) return callback(new BoxError(BoxError.CONFLICT));
let client = ldap.createClient({ url: externalLdapConfig.url }); getClient(externalLdapConfig, false, function (error, client) {
client.bind(ldapUsers[0].dn, password, function (error) { if (error) return callback(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])); 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 +370,197 @@ 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) debug('syncUsers: 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) debug('syncGroups: 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) {
debug(`syncGroupUsers: 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) {
debug(`syncGroupUsers: Unable to find group ${group.name} ignoring for now.`);
return callback();
}
var ldapGroupMembers = found.member || found.uniqueMember || [];
// if only one entry is in the group ldap returns a string, not an array!
if (typeof ldapGroupMembers === 'string') ldapGroupMembers = [ ldapGroupMembers ];
debug(`Group ${group.name} has ${ldapGroupMembers.length} members.`);
async.eachSeries(ldapGroupMembers, function (memberDn, iteratorCallback) {
ldapGetByDN(externalLdapConfig, memberDn, function (error, result) {
if (error) {
debug(`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) {
debug(`syncGroupUsers: 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) debug('syncGroupUsers: Failed to add member', error);
iteratorCallback();
});
});
});
}, function (error) {
if (error) debug('syncGroupUsers: ', error);
iteratorCallback();
});
});
}, callback);
});
}
function sync(progressCallback, callback) { function sync(progressCallback, callback) {
assert.strictEqual(typeof progressCallback, 'function'); assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
@@ -265,58 +571,18 @@ function sync(progressCallback, callback) {
if (error) return callback(error); if (error) return callback(error);
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled')); 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); if (error) return callback(error);
debug(`Found ${ldapUsers.length} users`); progressCallback({ percent: 100, message: 'Done' });
let percent = 10;
let step = 90/(ldapUsers.length+1); // ensure no divide by 0
// we ignore all errors here and just log them for now debug('sync: ldap sync is done', error);
async.eachSeries(ldapUsers, function (user, iteratorCallback) {
user = translateUser(externalLdapConfig, user);
if (!validUserRequirements(user)) return iteratorCallback(); callback(error);
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);
});
}); });
}); });
} }
+7 -2
View File
@@ -5,6 +5,7 @@ exports = module.exports = {
}; };
var assert = require('assert'), var assert = require('assert'),
async = require('async'),
infra = require('./infra_version.js'), infra = require('./infra_version.js'),
paths = require('./paths.js'), paths = require('./paths.js'),
shell = require('./shell.js'); shell = require('./shell.js');
@@ -26,7 +27,7 @@ function startGraphite(existingInfra, callback) {
--log-opt syslog-address=udp://127.0.0.1:2514 \ --log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \ --log-opt syslog-format=rfc5424 \
--log-opt tag=graphite \ --log-opt tag=graphite \
-m 75m \ -m 150m \
--memory-swap 150m \ --memory-swap 150m \
--dns 172.18.0.1 \ --dns 172.18.0.1 \
--dns-search=. \ --dns-search=. \
@@ -37,5 +38,9 @@ function startGraphite(existingInfra, callback) {
--label isCloudronManaged=true \ --label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`; --read-only -v /tmp -v /run "${tag}"`;
shell.exec('startGraphite', cmd, callback); async.series([
shell.exec.bind(null, 'stopGraphite', 'docker stop graphite || true'),
shell.exec.bind(null, 'removeGraphite', 'docker rm -f graphite || true'),
shell.exec.bind(null, 'startGraphite', cmd)
], callback);
} }
+19 -20
View File
@@ -2,6 +2,7 @@
exports = module.exports = { exports = module.exports = {
get: get, get: get,
getByName: getByName,
getWithMembers: getWithMembers, getWithMembers: getWithMembers,
getAll: getAll, getAll: getAll,
getAllWithMembers: getAllWithMembers, getAllWithMembers: getAllWithMembers,
@@ -19,8 +20,6 @@ exports = module.exports = {
getMembership: getMembership, getMembership: getMembership,
setMembership: setMembership, setMembership: setMembership,
getGroups: getGroups,
_clear: clear _clear: clear
}; };
@@ -28,7 +27,7 @@ var assert = require('assert'),
BoxError = require('./boxerror.js'), BoxError = require('./boxerror.js'),
database = require('./database.js'); database = require('./database.js');
var GROUPS_FIELDS = [ 'id', 'name' ].join(','); var GROUPS_FIELDS = [ 'id', 'name', 'source' ].join(',');
function get(groupId, callback) { function get(groupId, callback) {
assert.strictEqual(typeof groupId, 'string'); 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) { function getWithMembers(groupId, callback) {
assert.strictEqual(typeof groupId, 'string'); assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
@@ -63,7 +74,7 @@ function getWithMembers(groupId, callback) {
function getAll(callback) { function getAll(callback) {
assert.strictEqual(typeof callback, 'function'); 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)); if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, results); callback(null, results);
@@ -73,9 +84,8 @@ function getAll(callback) {
function getAllWithMembers(callback) { function getAllWithMembers(callback) {
database.query('SELECT ' + GROUPS_FIELDS + ',GROUP_CONCAT(groupMembers.userId) AS userIds ' + database.query('SELECT ' + GROUPS_FIELDS + ',GROUP_CONCAT(groupMembers.userId) AS userIds ' +
' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' + ' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' +
' GROUP BY userGroups.id', function (error, results) { ' GROUP BY userGroups.id ORDER BY name', function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Group not found'));
results.forEach(function (result) { result.userIds = result.userIds ? result.userIds.split(',') : [ ]; }); results.forEach(function (result) { result.userIds = result.userIds ? result.userIds.split(',') : [ ]; });
@@ -83,12 +93,13 @@ function getAllWithMembers(callback) {
}); });
} }
function add(id, name, callback) { function add(id, name, source, callback) {
assert.strictEqual(typeof id, 'string'); assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof name, 'string'); assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof source, 'string');
assert.strictEqual(typeof callback, 'function'); 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 && 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)); if (error || result.affectedRows !== 1) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
@@ -260,15 +271,3 @@ function isMember(groupId, userId, callback) {
callback(null, result.length !== 0); 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, create: create,
remove: remove, remove: remove,
get: get, get: get,
getByName: getByName,
update: update, update: update,
getWithMembers: getWithMembers, getWithMembers: getWithMembers,
getAll: getAll, getAll: getAll,
@@ -15,8 +16,6 @@ exports = module.exports = {
removeMember: removeMember, removeMember: removeMember,
isMember: isMember, isMember: isMember,
getGroups: getGroups,
setMembership: setMembership, setMembership: setMembership,
getMembership: getMembership, getMembership: getMembership,
@@ -45,8 +44,17 @@ function validateGroupname(name) {
return null; 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 name, 'string');
assert.strictEqual(typeof source, 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
// we store names in lowercase // we store names in lowercase
@@ -55,8 +63,11 @@ function create(name, callback) {
var error = validateGroupname(name); var error = validateGroupname(name);
if (error) return callback(error); if (error) return callback(error);
error = validateGroupSource(source);
if (error) return callback(error);
var id = 'gid-' + uuid.v4(); var id = 'gid-' + uuid.v4();
groupdb.add(id, name, function (error) { groupdb.add(id, name, source, function (error) {
if (error) return callback(error); if (error) return callback(error);
callback(null, { id: id, name: name }); 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) { function getWithMembers(id, callback) {
assert.strictEqual(typeof id, 'string'); assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function'); 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) { function count(callback) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
+9 -9
View File
@@ -9,19 +9,19 @@ exports = module.exports = {
'version': '48.17.0', 'version': '48.17.0',
'baseImages': [ '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 // 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 // docker inspect --format='{{index .RepoDigests 0}}' $IMAGE to get the sha256
'images': { 'images': {
'turn': { repo: 'cloudron/turn', tag: 'cloudron/turn:1.0.2@sha256:2643b73fe371154e37647957cc7103cacb34c50737f2954abd7d70f167a1f33a' }, 'turn': { repo: 'cloudron/turn', tag: 'cloudron/turn:1.1.0@sha256:e1dd22aa6eef5beb7339834b200a8bb787ffc2264ce11139857a054108fefb4f' },
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.2.0@sha256:440c8a9ca4d2958d51a375359f8158ef702b83395aa9ac4f450c51825ec09239' }, 'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.3.1@sha256:c1145d43c8a912fe6f5a5629a4052454a4aa6f23391c1efbffeec9d12d72a256' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.0.2@sha256:6dcee0731dfb9b013ed94d56205eee219040ee806c7e251db3b3886eaa4947ff' }, 'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:3.0.0@sha256:b00e5118a8f829c422234117bf113803be79a1d5102c51497c6d3005b041ce37' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.1.0@sha256:6d1bf221cfe6124957e2c58b57c0a47214353496009296acb16adf56df1da9d5' }, 'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:3.0.0@sha256:59e50b1f55e433ffdf6d678f8c658812b4119f631db8325572a52ee40d3bc562' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.1.0@sha256:f2cda21bd15c21bbf44432df412525369ef831a2d53860b5c5b1675e6f384de2' }, 'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.3.0@sha256:0e31ec817e235b1814c04af97b1e7cf0053384aca2569570ce92bef0d95e94d2' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.7.2@sha256:f20d112ff9a97e052a9187063eabbd8d484ce369114d44186e344169a1b3ef6b' }, 'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.9.4@sha256:0e169b97a0584a76197d2bbc039d8698bf93f815588b3b43c251bd83dd545465' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.2.0@sha256:fc9ca69d16e6ebdbd98ed53143d4a0d2212eef60cb638dc71219234e6f427a2c' }, 'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.3.0@sha256:b7bc1ca4f4d0603a01369a689129aa273a938ce195fe43d00d42f4f2d5212f50' },
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:1.0.0@sha256:3b70aac36700225945a4a39b5a400c28e010e980879d0dcca76e4a37b04a16ed' } 'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:2.0.2@sha256:cbd604eaa970c99ba5c4c2e7984929668e05de824172f880e8c576b2fb7c976d' }
} }
}; };
+5 -21
View File
@@ -16,21 +16,15 @@ const NOOP_CALLBACK = function () { };
const gConnection = new Docker({ socketPath: '/var/run/docker.sock' }); const gConnection = new Docker({ socketPath: '/var/run/docker.sock' });
function ignoreError(func) { function cleanupTokens(callback) {
return function (callback) { assert(!callback || typeof callback === 'function'); // callback is null when called from cronjob
func(function (error) {
if (error) console.error('Ignored error:', error);
callback(); callback = callback || NOOP_CALLBACK;
});
};
}
function cleanupExpiredTokens(callback) { debug('Cleaning up expired tokens');
assert.strictEqual(typeof callback, 'function');
tokendb.delExpired(function (error, result) { tokendb.delExpired(function (error, result) {
if (error) return callback(error); if (error) return debug('cleanupTokens: error removing expired tokens', error);
debug('Cleaned up %s expired tokens.', result); debug('Cleaned up %s expired tokens.', result);
@@ -38,16 +32,6 @@ function cleanupExpiredTokens(callback) {
}); });
} }
function cleanupTokens(callback) {
assert(!callback || typeof callback === 'function'); // callback is null when called from cronjob
debug('Cleaning up expired tokens');
async.series([
ignoreError(cleanupExpiredTokens)
], callback);
}
function cleanupTmpVolume(containerInfo, callback) { function cleanupTmpVolume(containerInfo, callback) {
assert.strictEqual(typeof containerInfo, 'object'); assert.strictEqual(typeof containerInfo, 'object');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
+9 -12
View File
@@ -154,7 +154,6 @@ function userSearch(req, res, next) {
givenName: firstName, givenName: firstName,
username: user.username, username: user.username,
samaccountname: user.username, // to support ActiveDirectory clients samaccountname: user.username, // to support ActiveDirectory clients
isadmin: users.compareRoles(user.role, users.ROLE_ADMIN) >= 0,
memberof: groups memberof: groups
} }
}; };
@@ -347,7 +346,7 @@ function mailboxSearch(req, res, next) {
if (error) return callback(error); if (error) return callback(error);
aliases.forEach(function (a, idx) { 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 // ensure all filter values are also lowercase
@@ -392,7 +391,7 @@ function mailAliasSearch(req, res, next) {
objectclass: ['nisMailAlias'], objectclass: ['nisMailAlias'],
objectcategory: 'nisMailAlias', objectcategory: 'nisMailAlias',
cn: `${alias.name}@${alias.domain}`, 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())); if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
const name = parts[0], domain = parts[1]; 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 && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.toString())); if (error) return next(new ldap.OperationsError(error.toString()));
@@ -431,6 +430,7 @@ function mailingListSearch(req, res, next) {
objectcategory: 'mailGroup', objectcategory: 'mailGroup',
cn: `${name}@${domain}`, // fully qualified cn: `${name}@${domain}`, // fully qualified
mail: `${name}@${domain}`, mail: `${name}@${domain}`,
membersOnly: list.membersOnly, // ldapjs only supports strings and string array. so this is not a bool!
mgrpRFC822MailMember: resolvedMembers // fully qualified mgrpRFC822MailMember: resolvedMembers // fully qualified
} }
}; };
@@ -577,7 +577,7 @@ function userSearchSftp(req, res, next) {
var obj = { var obj = {
dn: ldap.parseDN(`cn=${username}@${appFqdn},ou=sftp,dc=cloudron`).toString(), dn: ldap.parseDN(`cn=${username}@${appFqdn},ou=sftp,dc=cloudron`).toString(),
attributes: { attributes: {
homeDirectory: path.join('/app/data', app.id, 'data'), homeDirectory: path.join('/app/data', app.id),
objectclass: ['user'], objectclass: ['user'],
objectcategory: 'person', objectcategory: 'person',
cn: user.id, cn: user.id,
@@ -618,10 +618,7 @@ function authenticateMailAddon(req, res, next) {
// note: with sendmail addon, apps can send mail without a mailbox (unlike users) // note: with sendmail addon, apps can send mail without a mailbox (unlike users)
appdb.getAppIdByAddonConfigValue(addonId, namePattern, req.credentials || '', function (error, appId) { appdb.getAppIdByAddonConfigValue(addonId, namePattern, req.credentials || '', function (error, appId) {
if (error && error.reason !== BoxError.NOT_FOUND) return next(new ldap.OperationsError(error.message)); if (error && error.reason !== BoxError.NOT_FOUND) return next(new ldap.OperationsError(error.message));
if (appId) { // matched app password if (appId) return res.end();
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: email }, { appId: appId, addonId: addonId });
return res.end();
}
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) { mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
@@ -648,14 +645,14 @@ function start(callback) {
debug: NOOP, debug: NOOP,
info: debug, info: debug,
warn: debug, warn: debug,
error: console.error, error: debug,
fatal: console.error fatal: debug
}; };
gServer = ldap.createServer({ log: logger }); gServer = ldap.createServer({ log: logger });
gServer.on('error', function (error) { gServer.on('error', function (error) {
console.error('LDAP:', error); debug('start: server error ', error);
}); });
gServer.search('ou=users,dc=cloudron', authenticateApp, userSearch); gServer.search('ou=users,dc=cloudron', authenticateApp, userSearch);
+2
View File
@@ -8,6 +8,7 @@
maxsize 1M maxsize 1M
missingok missingok
delaycompress delaycompress
# this truncates the original log file and not the rotated one
copytruncate copytruncate
} }
@@ -18,6 +19,7 @@
missingok missingok
# we never compress so we can simply tail the files # we never compress so we can simply tail the files
nocompress nocompress
# this truncates the original log file and not the rotated one
copytruncate copytruncate
} }
+96 -73
View File
@@ -1,54 +1,54 @@
'use strict'; 'use strict';
exports = module.exports = { exports = module.exports = {
getStatus: getStatus, getStatus,
checkConfiguration: checkConfiguration, checkConfiguration,
getDomains: getDomains, getDomains,
getDomain: getDomain, getDomain,
clearDomains: clearDomains, clearDomains,
onDomainAdded: onDomainAdded, onDomainAdded,
onDomainRemoved: onDomainRemoved, onDomainRemoved,
onMailFqdnChanged,
removePrivateFields: removePrivateFields, removePrivateFields,
setDnsRecords: setDnsRecords, setDnsRecords,
onMailFqdnChanged: onMailFqdnChanged,
validateName: validateName, validateName,
setMailFromValidation: setMailFromValidation, setMailFromValidation,
setCatchAllAddress: setCatchAllAddress, setCatchAllAddress,
setMailRelay: setMailRelay, setMailRelay,
setMailEnabled: setMailEnabled, setMailEnabled,
startMail: restartMail, startMail: restartMail,
restartMail: restartMail, restartMail,
handleCertChanged: handleCertChanged, handleCertChanged,
getMailAuth: getMailAuth, getMailAuth,
sendTestMail: sendTestMail, sendTestMail,
listMailboxes: listMailboxes, getMailboxCount,
removeMailboxes: removeMailboxes, listMailboxes,
getMailbox: getMailbox, getMailbox,
addMailbox: addMailbox, addMailbox,
updateMailboxOwner: updateMailboxOwner, updateMailboxOwner,
removeMailbox: removeMailbox, removeMailbox,
listAliases: listAliases, getAliases,
getAliases: getAliases, setAliases,
setAliases: setAliases,
getLists: getLists, getLists,
getList: getList, getList,
addList: addList, addList,
updateList: updateList, updateList,
removeList: removeList, removeList,
resolveList: resolveList, resolveList,
_removeMailboxes: removeMailboxes,
_readDkimPublicKeySync: readDkimPublicKeySync _readDkimPublicKeySync: readDkimPublicKeySync
}; };
@@ -82,6 +82,7 @@ var assert = require('assert'),
const DNS_OPTIONS = { timeout: 5000 }; const DNS_OPTIONS = { timeout: 5000 };
var NOOP_CALLBACK = function (error) { if (error) debug(error); }; var NOOP_CALLBACK = function (error) { if (error) debug(error); };
const REMOVE_MAILBOX = path.join(__dirname, 'scripts/rmmailbox.sh');
function validateName(name) { function validateName(name) {
assert.strictEqual(typeof name, 'string'); assert.strictEqual(typeof name, 'string');
@@ -101,7 +102,6 @@ function checkOutboundPort25(callback) {
var smtpServer = _.sample([ var smtpServer = _.sample([
'smtp.gmail.com', 'smtp.gmail.com',
'smtp.live.com', 'smtp.live.com',
'smtp.mail.yahoo.com',
'smtp.1und1.de', 'smtp.1und1.de',
]); ]);
@@ -208,7 +208,8 @@ function checkDkim(mailDomain, callback) {
if (txtRecords.length !== 0) { if (txtRecords.length !== 0) {
dkim.value = txtRecords[0].join(''); dkim.value = txtRecords[0].join('');
dkim.status = (dkim.value === dkim.expected); const actual = txtToDict(dkim.value);
dkim.status = actual.p === dkimKey;
} }
callback(null, dkim); callback(null, dkim);
@@ -269,7 +270,7 @@ function checkMx(domain, mailFqdn, callback) {
if (error) return callback(error, mx); if (error) return callback(error, mx);
if (mxRecords.length === 0) return callback(null, 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(' '); mx.value = mxRecords.map(function (r) { return r.priority + ' ' + r.exchange + '.'; }).join(' ');
if (mx.status) return callback(null, mx); // MX record is "my." if (mx.status) return callback(null, mx); // MX record is "my."
@@ -629,7 +630,10 @@ function configureMail(mailFqdn, mailDomain, callback) {
if (!safe.child_process.execSync(`cp ${bundle.certFilePath} ${mailCertFilePath}`)) return callback(new BoxError(BoxError.FS_ERROR, 'Could not create cert file:' + safe.error.message)); if (!safe.child_process.execSync(`cp ${bundle.certFilePath} ${mailCertFilePath}`)) return callback(new BoxError(BoxError.FS_ERROR, 'Could not create cert file:' + safe.error.message));
if (!safe.child_process.execSync(`cp ${bundle.keyFilePath} ${mailKeyFilePath}`)) return callback(new BoxError(BoxError.FS_ERROR, 'Could not create key file:' + safe.error.message)); if (!safe.child_process.execSync(`cp ${bundle.keyFilePath} ${mailKeyFilePath}`)) return callback(new BoxError(BoxError.FS_ERROR, 'Could not create key file:' + safe.error.message));
shell.exec('startMail', 'docker rm -f mail || true', function (error) { async.series([
shell.exec.bind(null, 'stopMail', 'docker stop mail || true'),
shell.exec.bind(null, 'removeMail', 'docker rm -f mail || true'),
], function (error) {
if (error) return callback(error); if (error) return callback(error);
createMailConfig(mailFqdn, mailDomain, function (error, allowInbound) { createMailConfig(mailFqdn, mailDomain, function (error, allowInbound) {
@@ -798,6 +802,7 @@ function ensureDkimKeySync(mailDomain) {
return new BoxError(BoxError.FS_ERROR, safe.error); 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 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); if (!safe.child_process.execSync('openssl rsa -in ' + dkimPrivateKeyFile + ' -out ' + dkimPublicKeyFile + ' -pubout -outform PEM')) return new BoxError(BoxError.OPENSSL_ERROR, safe.error);
@@ -1031,13 +1036,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.strictEqual(typeof domain, 'string');
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number'); assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number'); assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function'); 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); if (error) return callback(error);
callback(null, result); callback(null, result);
@@ -1110,31 +1127,25 @@ function updateMailboxOwner(name, domain, userId, auditSource, callback) {
}); });
} }
function removeMailbox(name, domain, auditSource, callback) { function removeMailbox(name, domain, options, auditSource, callback) {
assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof name, 'string'); assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
mailboxdb.del(name, domain, function (error) { const deleteMailFunc = options.deleteMails ? shell.sudo.bind(null, 'removeMailbox', [ REMOVE_MAILBOX, `${name}@${domain}` ], {}) : (next) => next();
if (error) return callback(error);
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_REMOVE, auditSource, { name, domain }); deleteMailFunc(function (error) {
if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error removing mailbox: ${error.message}`));
callback(null); mailboxdb.del(name, domain, function (error) {
}); if (error) return callback(error);
}
function listAliases(domain, page, perPage, callback) { eventlog.add(eventlog.ACTION_MAIL_MAILBOX_REMOVE, auditSource, { name, domain });
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) { callback();
if (error) return callback(error); });
callback(null, result);
}); });
} }
@@ -1161,12 +1172,15 @@ function setAliases(name, domain, aliases, callback) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
for (var i = 0; i < aliases.length; i++) { 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 (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) { mailboxdb.setAliasesForName(name, domain, aliases, function (error) {
if (error) return callback(error); if (error) return callback(error);
@@ -1174,11 +1188,14 @@ function setAliases(name, domain, aliases, callback) {
}); });
} }
function getLists(domain, callback) { function getLists(domain, search, page, perPage, callback) {
assert.strictEqual(typeof domain, 'string'); 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'); 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); if (error) return callback(error);
callback(null, result); callback(null, result);
@@ -1197,10 +1214,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 domain, 'string');
assert.strictEqual(typeof name, 'string'); assert.strictEqual(typeof name, 'string');
assert(Array.isArray(members)); assert(Array.isArray(members));
assert.strictEqual(typeof membersOnly, 'boolean');
assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
@@ -1213,19 +1231,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])); 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); 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(); callback();
}); });
} }
function updateList(name, domain, members, auditSource, callback) { function updateList(name, domain, members, membersOnly, auditSource, callback) {
assert.strictEqual(typeof name, 'string'); assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(members)); assert(Array.isArray(members));
assert.strictEqual(typeof membersOnly, 'boolean');
assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
@@ -1241,10 +1260,10 @@ function updateList(name, domain, members, auditSource, callback) {
getList(name, domain, function (error, result) { getList(name, domain, function (error, result) {
if (error) return callback(error); 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); 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); callback(null);
}); });
@@ -1266,6 +1285,7 @@ function removeList(name, domain, auditSource, callback) {
}); });
} }
// resolves the members of a list. i.e the lists and aliases
function resolveList(listName, listDomain, callback) { function resolveList(listName, listDomain, callback) {
assert.strictEqual(typeof listName, 'string'); assert.strictEqual(typeof listName, 'string');
assert.strictEqual(typeof listDomain, 'string'); assert.strictEqual(typeof listDomain, 'string');
@@ -1296,18 +1316,21 @@ function resolveList(listName, listDomain, callback) {
visited.push(member); visited.push(member);
mailboxdb.get(memberName, memberDomain, function (error, entry) { 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 (error) return iteratorCallback(error);
if (entry.type === mailboxdb.TYPE_MAILBOX) { result.push(member); return iteratorCallback(); } if (entry.type === mailboxdb.TYPE_MAILBOX) { // concrete mailbox
// no need to resolve alias because we only allow one level and within same domain result.push(member);
if (entry.type === mailboxdb.TYPE_ALIAS) { result.push(`${entry.aliasTarget}@${entry.domain}`); return iteratorCallback(); } } else if (entry.type === mailboxdb.TYPE_ALIAS) { // resolve aliases
toResolve = toResolve.concat(`${entry.aliasName}@${entry.aliasDomain}`);
} else { // resolve list members
toResolve = toResolve.concat(entry.members);
}
toResolve = toResolve.concat(entry.members);
iteratorCallback(); iteratorCallback();
}); });
}, function (error) { }, 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. 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 * To increase a service's memory limit - https://cloudron.io/documentation/troubleshooting/#services
Out of memory event: 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: To reset your password, please visit the following page:
<%- resetLink %> <%- resetLink %>
Please note that the password reset link will expire in 24 hours.
Powered by https://cloudron.io Powered by https://cloudron.io
@@ -29,6 +29,10 @@ Powered by https://cloudron.io
<a href="<%= resetLink %>">Click to reset your password</a> <a href="<%= resetLink %>">Click to reset your password</a>
</p> </p>
<br/>
Please note that the password reset link will expire in 24 hours.
<br/> <br/>
<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 %>. You are receiving this email because you were invited by <%= invitor.email %>.
<% } %> <% } %>
Please note that the invite link will expire in 7 days.
Powered by https://cloudron.io 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 %>. You are receiving this email because you were invited by <%= invitor.email %>.
<% } %> <% } %>
<br/>
Please note that the invite link will expire in 7 days.
<br/> <br/>
Powered by <a href="https://cloudron.io">Cloudron</a> Powered by <a href="https://cloudron.io">Cloudron</a>
+74 -64
View File
@@ -1,32 +1,32 @@
'use strict'; 'use strict';
exports = module.exports = { exports = module.exports = {
addMailbox: addMailbox, addMailbox,
addList: addList, addList,
updateMailboxOwner: updateMailboxOwner, updateMailboxOwner,
updateList: updateList, updateList,
del: del, del,
listAliases: listAliases, getMailboxCount,
listMailboxes: listMailboxes, listMailboxes,
getLists: getLists, getLists,
listAllMailboxes: listAllMailboxes, listAllMailboxes,
get: get, get,
getMailbox: getMailbox, getMailbox,
getList: getList, getList,
getAlias: getAlias, getAlias,
getAliasesForName: getAliasesForName, getAliasesForName,
setAliasesForName: setAliasesForName, setAliasesForName,
getByOwnerId: getByOwnerId, getByOwnerId,
delByOwnerId: delByOwnerId, delByOwnerId,
delByDomain: delByDomain, delByDomain,
updateName: updateName, updateName,
_clear: clear, _clear: clear,
@@ -38,15 +38,18 @@ exports = module.exports = {
var assert = require('assert'), var assert = require('assert'),
BoxError = require('./boxerror.js'), BoxError = require('./boxerror.js'),
database = require('./database.js'), database = require('./database.js'),
mysql = require('mysql'),
safe = require('safetydance'), safe = require('safetydance'),
util = require('util'); 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) { function postProcess(data) {
data.members = safe.JSON.parse(data.membersJson) || [ ]; data.members = safe.JSON.parse(data.membersJson) || [ ];
delete data.membersJson; delete data.membersJson;
data.membersOnly = !!data.membersOnly;
return data; 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 name, 'string');
assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(members)); assert(Array.isArray(members));
assert.strictEqual(typeof membersOnly, 'boolean');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, membersJson) VALUES (?, ?, ?, ?, ?)', database.query('INSERT INTO mailboxes (name, type, domain, ownerId, membersJson, membersOnly) VALUES (?, ?, ?, ?, ?, ?)',
[ name, exports.TYPE_LIST, domain, 'admin', JSON.stringify(members) ], function (error) { [ 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 && 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)); 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 name, 'string');
assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(members)); assert(Array.isArray(members));
assert.strictEqual(typeof membersOnly, 'boolean');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
database.query('UPDATE mailboxes SET membersJson = ? WHERE name = ? AND domain = ?', database.query('UPDATE mailboxes SET membersJson = ?, membersOnly = ? WHERE name = ? AND domain = ?',
[ JSON.stringify(members), name, domain ], function (error, result) { [ JSON.stringify(members), membersOnly, name, domain ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found')); 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'); assert.strictEqual(typeof callback, 'function');
// deletes aliases as well // 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 (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found')); 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 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 page, 'number');
assert.strictEqual(typeof perPage, 'number'); assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? AND domain = ? ORDER BY name LIMIT ${(page-1)*perPage},${perPage}`, let query = `SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? AND domain = ?`;
[ exports.TYPE_MAILBOX, domain ], function (error, results) { if (search) query += ' AND (name LIKE ' + mysql.escape('%' + search + '%') + ')';
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); 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) { function listAllMailboxes(page, perPage, callback) {
@@ -221,8 +241,8 @@ function listAllMailboxes(page, perPage, callback) {
assert.strictEqual(typeof perPage, 'number'); assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? ORDER BY name LIMIT ${(page-1)*perPage},${perPage}`, database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? ORDER BY name LIMIT ?,?`,
[ exports.TYPE_MAILBOX ], function (error, results) { [ exports.TYPE_MAILBOX, (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(function (result) { postProcess(result); }); 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.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'); assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE type = ? AND domain = ?', let query = `SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? AND domain = ?`;
[ exports.TYPE_LIST, domain ], function (error, results) { if (search) query += ' AND (name LIKE ' + mysql.escape('%' + search + '%') + ' OR membersJson LIKE ' + mysql.escape('%' + search + '%') + ')';
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
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) { function getList(name, domain, callback) {
@@ -285,10 +312,10 @@ function setAliasesForName(name, domain, aliases, callback) {
var queries = []; var queries = [];
// clear existing aliases // 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) { aliases.forEach(function (alias) {
queries.push({ query: 'INSERT INTO mailboxes (name, type, domain, aliasTarget, ownerId) VALUES (?, ?, ?, ?, ?)', queries.push({ query: 'INSERT INTO mailboxes (name, domain, type, aliasName, aliasDomain, ownerId) VALUES (?, ?, ?, ?, ?, ?)',
args: [ alias, exports.TYPE_ALIAS, domain, name, results[0].ownerId ] }); args: [ alias.name, alias.domain, exports.TYPE_ALIAS, name, domain, results[0].ownerId ] });
}); });
database.transaction(queries, function (error) { database.transaction(queries, function (error) {
@@ -311,27 +338,10 @@ function getAliasesForName(name, domain, callback) {
assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function'); 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) { [ exports.TYPE_ALIAS, name, domain ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); 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); callback(null, results);
}); });
} }
+24 -10
View File
@@ -7,6 +7,7 @@ map $http_upgrade $connection_upgrade {
# http server # http server
server { server {
listen 80; listen 80;
server_tokens off; # hide version
<% if (hasIPv6) { -%> <% if (hasIPv6) { -%>
listen [::]:80; listen [::]:80;
<% } -%> <% } -%>
@@ -42,18 +43,19 @@ server {
server { server {
<% if (vhost) { -%> <% if (vhost) { -%>
server_name <%= vhost %>; server_name <%= vhost %>;
listen 443 http2; listen 443 ssl http2;
<% if (hasIPv6) { -%> <% if (hasIPv6) { -%>
listen [::]:443 http2; listen [::]:443 ssl http2;
<% } -%> <% } -%>
<% } else { -%> <% } else { -%>
listen 443 http2 default_server; listen 443 ssl http2 default_server;
<% if (hasIPv6) { -%> <% 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 # paths are relative to prefix and not to this file
ssl_certificate <%= certFilePath %>; ssl_certificate <%= certFilePath %>;
ssl_certificate_key <%= keyFilePath %>; ssl_certificate_key <%= keyFilePath %>;
@@ -135,14 +137,21 @@ server {
# internal means this is for internal routing and cannot be accessed as URL from browser # internal means this is for internal routing and cannot be accessed as URL from browser
internal; 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 # user defined .well-known resources
# alias means only the part after matched location is appended (unlike root) location ~ ^/.well-known/(.*)$ {
location /.well-known/ { root /home/yellowtent/boxdata/well-known/$host;
alias /home/yellowtent/boxdata/well-known/$host/; try_files /$1 @wellknown-upstream;
} }
location / { location / {
@@ -186,6 +195,11 @@ server {
client_max_body_size 0; 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) # graphite paths (uncomment block below and visit /graphite-web/dashboard)
# remember to comment out the CSP policy as well to access the graphite dashboard # remember to comment out the CSP policy as well to access the graphite dashboard
# location ~ ^/graphite-web/ { # location ~ ^/graphite-web/ {
+4 -4
View File
@@ -156,7 +156,7 @@ function oomEvent(eventId, app, addon, containerId, event, callback) {
if (app) { if (app) {
program = `App ${app.fqdn}`; program = `App ${app.fqdn}`;
title = `The application ${app.fqdn} (${app.manifest.title}) ran out of memory.`; 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) { } else if (addon) {
program = `${addon.name} service`; program = `${addon.name} service`;
title = `The ${addon.name} service ran out of memory`; title = `The ${addon.name} service ran out of memory`;
@@ -211,7 +211,7 @@ function appUpdated(eventId, app, callback) {
if (error) return callback(error); if (error) return callback(error);
mailer.appUpdated(admin.email, app, function (error) { mailer.appUpdated(admin.email, app, function (error) {
if (error) console.error('Failed to send app updated email', error); // non fatal if (error) debug('appUpdated: Failed to send app updated email', error); // non fatal
done(); done();
}); });
}); });
@@ -273,7 +273,7 @@ function alert(id, title, message, callback) {
assert.strictEqual(typeof message, 'string'); assert.strictEqual(typeof message, 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
debug(`alert: id=${id} title=${title} message=${message}`); debug(`alert: id=${id} title=${title}`);
const acknowledged = !message; const acknowledged = !message;
@@ -301,7 +301,7 @@ function alert(id, title, message, callback) {
}); });
}); });
}, function (error) { }, function (error) {
if (error) console.error(error); if (error) debug('alert: error notifying', error);
callback(); callback();
}); });
+1 -1
View File
@@ -17,7 +17,6 @@ exports = module.exports = {
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'), CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
INFRA_VERSION_FILE: path.join(baseDir(), 'platformdata/INFRA_VERSION'), INFRA_VERSION_FILE: path.join(baseDir(), 'platformdata/INFRA_VERSION'),
LICENSE_FILE: '/etc/cloudron/LICENSE',
PROVIDER_FILE: '/etc/cloudron/PROVIDER', PROVIDER_FILE: '/etc/cloudron/PROVIDER',
PLATFORM_DATA_DIR: path.join(baseDir(), 'platformdata'), PLATFORM_DATA_DIR: path.join(baseDir(), 'platformdata'),
@@ -51,6 +50,7 @@ exports = module.exports = {
LOG_DIR: path.join(baseDir(), 'platformdata/logs'), LOG_DIR: path.join(baseDir(), 'platformdata/logs'),
TASKS_LOG_DIR: path.join(baseDir(), 'platformdata/logs/tasks'), TASKS_LOG_DIR: path.join(baseDir(), 'platformdata/logs/tasks'),
CRASH_LOG_DIR: path.join(baseDir(), 'platformdata/logs/crash'), CRASH_LOG_DIR: path.join(baseDir(), 'platformdata/logs/crash'),
BOX_LOG_FILE: path.join(baseDir(), 'platformdata/logs/box.log'),
GHOST_USER_FILE: path.join(baseDir(), 'platformdata/cloudron_ghost.json'), GHOST_USER_FILE: path.join(baseDir(), 'platformdata/cloudron_ghost.json'),
+27 -32
View File
@@ -2,7 +2,7 @@
exports = module.exports = { exports = module.exports = {
start: start, start: start,
stop: stop, stopAllTasks: stopAllTasks,
// exported for testing // exported for testing
_isReady: false _isReady: false
@@ -56,9 +56,8 @@ function start(callback) {
if (error) return callback(error); if (error) return callback(error);
async.series([ async.series([
stopContainers.bind(null, existingInfra), (next) => { if (existingInfra.version !== infra.version) removeAllContainers(existingInfra, next); else next(); },
// mark app state before we start addons. this gives the db import logic a chance to mark an app as errored markApps.bind(null, existingInfra), // mark app state before we start addons. this gives the db import logic a chance to mark an app as errored
startApps.bind(null, existingInfra),
graphs.startGraphite.bind(null, existingInfra), graphs.startGraphite.bind(null, existingInfra),
sftp.startSftp.bind(null, existingInfra), sftp.startSftp.bind(null, existingInfra),
addons.startServices.bind(null, existingInfra), addons.startServices.bind(null, existingInfra),
@@ -74,7 +73,7 @@ function start(callback) {
}); });
} }
function stop(callback) { function stopAllTasks(callback) {
tasks.stopAllTasks(callback); tasks.stopAllTasks(callback);
} }
@@ -130,41 +129,37 @@ function pruneInfraImages(callback) {
}, callback); }, callback);
} }
function stopContainers(existingInfra, callback) { function removeAllContainers(existingInfra, callback) {
// always stop addons to restart them on any infra change, regardless of minor or major update debug('removeAllContainers: removing all containers for infra upgrade');
if (existingInfra.version !== infra.version) {
debug('stopping all containers for infra upgrade');
async.series([
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');
var changedAddons = [ ];
for (var imageName in existingInfra.images) { // do not use infra.images because we can only stop things which are existing
if (infra.images[imageName].tag !== existingInfra.images[imageName].tag) changedAddons.push(imageName);
}
debug('stopContainer: stopping addons for incremental infra update: %j', changedAddons); async.series([
let filterArg = changedAddons.map(function (c) { return `--filter 'name=${c}'`; }).join(' '); // name=c matches *c*. required for redis-{appid} shell.exec.bind(null, 'removeAllContainers', 'docker ps -qa --filter \'label=isCloudronManaged\' | xargs --no-run-if-empty docker stop'),
// ignore error if container not found (and fail later) so that this code works across restarts shell.exec.bind(null, 'removeAllContainers', 'docker ps -qa --filter \'label=isCloudronManaged\' | xargs --no-run-if-empty docker rm -f')
async.series([ ], callback);
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);
}
} }
function startApps(existingInfra, callback) { function markApps(existingInfra, callback) {
if (existingInfra.version === 'none') { // cloudron is being restored from backup if (existingInfra.version === 'none') { // cloudron is being restored from backup
debug('startApps: restoring installed apps'); debug('markApps: restoring installed apps');
apps.restoreInstalledApps(callback); apps.restoreInstalledApps(callback);
} else if (existingInfra.version !== infra.version) { } else if (existingInfra.version !== infra.version) {
debug('startApps: reconfiguring installed apps'); debug('markApps: reconfiguring installed apps');
reverseProxy.removeAppConfigs(); // should we change the cert location, nginx will not start reverseProxy.removeAppConfigs(); // should we change the cert location, nginx will not start
apps.configureInstalledApps(callback); apps.configureInstalledApps(callback);
} else { } else {
debug('startApps: apps are already uptodate'); let changedAddons = [];
callback(); 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(`markApps: changedAddons: ${JSON.stringify(changedAddons)}`);
apps.restartAppsUsingAddons(changedAddons, callback);
} else {
debug('markApps: apps are already uptodate');
callback();
}
} }
} }
+11 -32
View File
@@ -4,13 +4,10 @@ exports = module.exports = {
setup: setup, setup: setup,
restore: restore, restore: restore,
activate: activate, activate: activate,
getStatus: getStatus, getStatus: getStatus
autoRegister: autoRegister
}; };
var appstore = require('./appstore.js'), var assert = require('assert'),
assert = require('assert'),
async = require('async'), async = require('async'),
backups = require('./backups.js'), backups = require('./backups.js'),
BoxError = require('./boxerror.js'), BoxError = require('./boxerror.js'),
@@ -19,10 +16,7 @@ var appstore = require('./appstore.js'),
debug = require('debug')('box:provision'), debug = require('debug')('box:provision'),
domains = require('./domains.js'), domains = require('./domains.js'),
eventlog = require('./eventlog.js'), eventlog = require('./eventlog.js'),
fs = require('fs'),
mail = require('./mail.js'), mail = require('./mail.js'),
paths = require('./paths.js'),
safe = require('safetydance'),
semver = require('semver'), semver = require('semver'),
settings = require('./settings.js'), settings = require('./settings.js'),
sysinfo = require('./sysinfo.js'), sysinfo = require('./sysinfo.js'),
@@ -53,27 +47,6 @@ function setProgress(task, message, callback) {
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) { function unprovision(callback) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
@@ -134,7 +107,6 @@ function setup(dnsConfig, sysinfoConfig, auditSource, callback) {
callback(); // now that args are validated run the task in the background callback(); // now that args are validated run the task in the background
async.series([ async.series([
autoRegister.bind(null, domain),
settings.setSysinfoConfig.bind(null, sysinfoConfig), settings.setSysinfoConfig.bind(null, sysinfoConfig),
domains.prepareDashboardDomain.bind(null, domain, auditSource, (progress) => setProgress('setup', progress.message, NOOP_CALLBACK)), domains.prepareDashboardDomain.bind(null, domain, auditSource, (progress) => setProgress('setup', progress.message, NOOP_CALLBACK)),
cloudron.setDashboardDomain.bind(null, domain, auditSource), cloudron.setDashboardDomain.bind(null, domain, auditSource),
@@ -206,9 +178,16 @@ function restore(backupConfig, backupId, version, sysinfoConfig, auditSource, ca
if (error) return done(error); if (error) return done(error);
if (activated) return done(new BoxError(BoxError.CONFLICT, 'Already activated. Restore with a fresh Cloudron installation.')); 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 (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) { sysinfo.testConfig(sysinfoConfig, function (error) {
if (error) return done(error); if (error) return done(error);
@@ -247,11 +226,11 @@ function getStatus(callback) {
version: constants.VERSION, version: constants.VERSION,
apiServerOrigin: settings.apiServerOrigin(), // used by CaaS tool apiServerOrigin: settings.apiServerOrigin(), // used by CaaS tool
webServerOrigin: settings.webServerOrigin(), // used by CaaS tool webServerOrigin: settings.webServerOrigin(), // used by CaaS tool
provider: settings.provider(),
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY], cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
footer: allSettings[settings.FOOTER_KEY] || constants.FOOTER, footer: allSettings[settings.FOOTER_KEY] || constants.FOOTER,
adminFqdn: settings.adminDomain() ? settings.adminFqdn() : null, adminFqdn: settings.adminDomain() ? settings.adminFqdn() : null,
activated: activated, activated: activated,
provider: settings.provider() // used by setup wizard of marketplace images
}, gProvisionStatus)); }, gProvisionStatus));
}); });
}); });
BIN
View File
Binary file not shown.
+50 -45
View File
@@ -27,7 +27,7 @@ exports = module.exports = {
removeAppConfigs: removeAppConfigs, removeAppConfigs: removeAppConfigs,
// exported for testing // exported for testing
_getCertApi: getCertApi _getAcmeApi: getAcmeApi
}; };
var acme2 = require('./cert/acme2.js'), var acme2 = require('./cert/acme2.js'),
@@ -35,14 +35,12 @@ var acme2 = require('./cert/acme2.js'),
assert = require('assert'), assert = require('assert'),
async = require('async'), async = require('async'),
BoxError = require('./boxerror.js'), BoxError = require('./boxerror.js'),
caas = require('./cert/caas.js'),
constants = require('./constants.js'), constants = require('./constants.js'),
crypto = require('crypto'), crypto = require('crypto'),
debug = require('debug')('box:reverseproxy'), debug = require('debug')('box:reverseproxy'),
domains = require('./domains.js'), domains = require('./domains.js'),
ejs = require('ejs'), ejs = require('ejs'),
eventlog = require('./eventlog.js'), eventlog = require('./eventlog.js'),
fallback = require('./cert/fallback.js'),
fs = require('fs'), fs = require('fs'),
mail = require('./mail.js'), mail = require('./mail.js'),
os = require('os'), os = require('os'),
@@ -59,27 +57,23 @@ var acme2 = require('./cert/acme2.js'),
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/nginxconfig.ejs', { encoding: 'utf8' }), var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/nginxconfig.ejs', { encoding: 'utf8' }),
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh'); RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh');
function getCertApi(domainObject, callback) { function getAcmeApi(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object'); assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
if (domainObject.tlsConfig.provider === 'fallback') return callback(null, fallback, { fallback: true }); const api = acme2;
var api = domainObject.tlsConfig.provider === 'caas' ? caas : acme2; let options = { prod: false, performHttpAuthorization: false, wildcard: false, email: '' };
options.prod = domainObject.tlsConfig.provider.match(/.*-prod/) !== null; // matches 'le-prod' or 'letsencrypt-prod'
var options = { prod: false, performHttpAuthorization: false, wildcard: false, email: '' }; options.performHttpAuthorization = domainObject.provider.match(/noop|manual|wildcard/) !== null;
if (domainObject.tlsConfig.provider !== 'caas') { options.wildcard = !!domainObject.tlsConfig.wildcard;
options.prod = domainObject.tlsConfig.provider.match(/.*-prod/) !== null; // matches 'le-prod' or 'letsencrypt-prod'
options.performHttpAuthorization = domainObject.provider.match(/noop|manual|wildcard/) !== null;
options.wildcard = !!domainObject.tlsConfig.wildcard;
}
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197) // registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
// we cannot use admin@fqdn because the user might not have set it up. // we cannot use admin@fqdn because the user might not have set it up.
// we simply update the account with the latest email we have each time when getting letsencrypt certs // 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 // https://github.com/ietf-wg-acme/acme/issues/30
users.getOwner(function (error, owner) { users.getOwner(function (error, owner) {
options.email = error ? 'support@cloudron.io' : 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); callback(null, api, options);
}); });
@@ -108,8 +102,6 @@ function providerMatchesSync(domainObject, certFilePath, apiOptions) {
if (!fs.existsSync(certFilePath)) return false; // not found if (!fs.existsSync(certFilePath)) return false; // not found
if (apiOptions.fallback) return certFilePath.includes('.host.cert');
const subjectAndIssuer = safe.child_process.execSync(`/usr/bin/openssl x509 -noout -subject -issuer -in "${certFilePath}"`, { encoding: 'utf8' }); const subjectAndIssuer = safe.child_process.execSync(`/usr/bin/openssl x509 -noout -subject -issuer -in "${certFilePath}"`, { encoding: 'utf8' });
if (!subjectAndIssuer) return false; // something bad happenned if (!subjectAndIssuer) return false; // something bad happenned
@@ -215,15 +207,9 @@ function setFallbackCertificate(domain, fallback, callback) {
assert.strictEqual(typeof fallback, 'object'); assert.strictEqual(typeof fallback, 'object');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
if (fallback.restricted) { // restricted certs are not backed up debug(`setFallbackCertificate: setting certs for domain ${domain}`);
debug(`setFallbackCertificate: setting restricted certs for domain ${domain}`); if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`), fallback.cert)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`), fallback.cert)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message)); if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.key`), fallback.key)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`), fallback.key)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message));
} else {
debug(`setFallbackCertificate: setting certs for domain ${domain}`);
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`), fallback.cert)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.key`), fallback.key)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message));
}
// TODO: maybe the cert is being used by the mail container // TODO: maybe the cert is being used by the mail container
reload(function (error) { reload(function (error) {
@@ -237,15 +223,8 @@ function getFallbackCertificate(domain, callback) {
assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
// check for any pre-provisioned (caas) certs. they get first priority const certFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`);
var certFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`); const keyFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.key`);
var keyFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
// check for auto-generated or user set fallback certs
certFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`);
keyFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.key`);
callback(null, { certFilePath, keyFilePath }); callback(null, { certFilePath, keyFilePath });
} }
@@ -267,15 +246,12 @@ function setAppCertificateSync(location, domainObject, certificate) {
return null; return null;
} }
function getCertificateByHostname(hostname, domainObject, callback) { function getAcmeCertificate(hostname, domainObject, callback) {
assert.strictEqual(typeof hostname, 'string'); assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domainObject, 'object'); assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
let certFilePath = path.join(paths.APP_CERTS_DIR, `${hostname}.user.cert`); let certFilePath, keyFilePath;
let keyFilePath = path.join(paths.APP_CERTS_DIR, `${hostname}.user.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
if (hostname !== domainObject.domain && domainObject.tlsConfig.wildcard) { // bare domain is not part of wildcard SAN if (hostname !== domainObject.domain && domainObject.tlsConfig.wildcard) { // bare domain is not part of wildcard SAN
let certName = domains.makeWildcard(hostname).replace('*.', '_.'); let certName = domains.makeWildcard(hostname).replace('*.', '_.');
@@ -298,10 +274,22 @@ function getCertificate(fqdn, domain, callback) {
assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
// 1. user cert always wins
// 2. if using fallback provider, return that cert
// 3. look for LE certs
domains.get(domain, function (error, domainObject) { domains.get(domain, function (error, domainObject) {
if (error) return callback(error); if (error) return callback(error);
getCertificateByHostname(fqdn, domainObject, function (error, result) { // user cert always wins
let certFilePath = path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`);
let keyFilePath = path.join(paths.APP_CERTS_DIR, `${fqdn}.user.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
if (domainObject.tlsConfig.provider === 'fallback') return getFallbackCertificate(domain, callback);
getAcmeCertificate(fqdn, domainObject, function (error, result) {
if (error || result) return callback(error, result); if (error || result) return callback(error, result);
return getFallbackCertificate(domain, callback); return getFallbackCertificate(domain, callback);
@@ -329,14 +317,32 @@ function ensureCertificate(vhost, domain, auditSource, callback) {
domains.get(domain, function (error, domainObject) { domains.get(domain, function (error, domainObject) {
if (error) return callback(error); if (error) return callback(error);
getCertApi(domainObject, function (error, api, apiOptions) { // user cert always wins
let certFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.user.cert`);
let keyFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.user.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) {
debug(`ensureCertificate: ${vhost} will use custom app certs`);
return callback(null, { certFilePath, keyFilePath }, { renewed: false });
}
if (domainObject.tlsConfig.provider === 'fallback') {
debug(`ensureCertificate: ${vhost} will use fallback certs`);
return getFallbackCertificate(domain, function (error, bundle) {
if (error) return callback(error);
callback(null, bundle, { renewed: false });
});
}
getAcmeApi(domainObject, function (error, acmeApi, apiOptions) {
if (error) return callback(error); if (error) return callback(error);
getCertificateByHostname(vhost, domainObject, function (_error, currentBundle) { getAcmeCertificate(vhost, domainObject, function (_error, currentBundle) {
if (currentBundle) { if (currentBundle) {
debug(`ensureCertificate: ${vhost} certificate already exists at ${currentBundle.keyFilePath}`); debug(`ensureCertificate: ${vhost} certificate already exists at ${currentBundle.keyFilePath}`);
if (currentBundle.certFilePath.endsWith('.user.cert')) return callback(null, currentBundle, { renewed: false }); // user certs cannot be renewed
if (!isExpiringSync(currentBundle.certFilePath, 24 * 30) && providerMatchesSync(domainObject, currentBundle.certFilePath, apiOptions)) return callback(null, currentBundle, { renewed: false }); if (!isExpiringSync(currentBundle.certFilePath, 24 * 30) && providerMatchesSync(domainObject, currentBundle.certFilePath, apiOptions)) return callback(null, currentBundle, { renewed: false });
debug(`ensureCertificate: ${vhost} cert require renewal`); debug(`ensureCertificate: ${vhost} cert require renewal`);
} else { } else {
@@ -345,7 +351,7 @@ function ensureCertificate(vhost, domain, auditSource, callback) {
debug('ensureCertificate: getting certificate for %s with options %j', vhost, apiOptions); debug('ensureCertificate: getting certificate for %s with options %j', vhost, apiOptions);
api.getCertificate(vhost, domain, apiOptions, function (error, certFilePath, keyFilePath) { acmeApi.getCertificate(vhost, domain, apiOptions, function (error, certFilePath, keyFilePath) {
debug(`ensureCertificate: error: ${error ? error.message : 'null'} cert: ${certFilePath || 'null'}`); debug(`ensureCertificate: error: ${error ? error.message : 'null'} cert: ${certFilePath || 'null'}`);
eventlog.add(currentBundle ? eventlog.ACTION_CERTIFICATE_RENEWAL : eventlog.ACTION_CERTIFICATE_NEW, auditSource, { domain: vhost, errorMessage: error ? error.message : '' }); eventlog.add(currentBundle ? eventlog.ACTION_CERTIFICATE_RENEWAL : eventlog.ACTION_CERTIFICATE_NEW, auditSource, { domain: vhost, errorMessage: error ? error.message : '' });
@@ -362,7 +368,6 @@ function ensureCertificate(vhost, domain, auditSource, callback) {
debug(`ensureCertificate: renewal of ${vhost} failed. using fallback certificates for ${domain}`); debug(`ensureCertificate: renewal of ${vhost} failed. using fallback certificates for ${domain}`);
// if no cert was returned use fallback. the fallback/caas provider will not provide any for example
getFallbackCertificate(domain, function (error, bundle) { getFallbackCertificate(domain, function (error, bundle) {
if (error) return callback(error); if (error) return callback(error);
+22 -1
View File
@@ -30,6 +30,7 @@ exports = module.exports = {
setMailbox: setMailbox, setMailbox: setMailbox,
setLocation: setLocation, setLocation: setLocation,
setDataDir: setDataDir, setDataDir: setDataDir,
setBinds: setBinds,
stop: stop, stop: stop,
start: start, start: start,
@@ -427,7 +428,7 @@ function importApp(req, res, next) {
if (req.body.backupConfig) { if (req.body.backupConfig) {
if (typeof backupConfig.provider !== 'string') return next(new HttpError(400, 'provider is required')); 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')); if ('acceptSelfSignedCerts' in backupConfig && typeof backupConfig.acceptSelfSignedCerts !== 'boolean') return next(new HttpError(400, 'format must be a boolean'));
// testing backup config can take sometime // testing backup config can take sometime
@@ -764,3 +765,23 @@ function downloadFile(req, res, next) {
stream.pipe(res); 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 }));
});
}
+11 -3
View File
@@ -3,11 +3,11 @@
exports = module.exports = { exports = module.exports = {
list: list, list: list,
startBackup: startBackup, startBackup: startBackup,
cleanup: cleanup cleanup: cleanup,
check: check
}; };
let auditSource = require('../auditsource.js'), let auditSource = require('../auditsource.js'),
backupdb = require('../backupdb.js'),
backups = require('../backups.js'), backups = require('../backups.js'),
BoxError = require('../boxerror.js'), BoxError = require('../boxerror.js'),
HttpError = require('connect-lastmile').HttpError, HttpError = require('connect-lastmile').HttpError,
@@ -20,7 +20,7 @@ function list(req, res, next) {
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25; var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number')); if (!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)); if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { backups: result })); next(new HttpSuccess(200, { backups: result }));
@@ -42,3 +42,11 @@ function cleanup(req, res, next) {
next(new HttpSuccess(202, { taskId })); next(new HttpSuccess(202, { taskId }));
}); });
} }
function check(req, res, next) {
backups.checkConfiguration(function (error, message) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { ok: !message, message: message }));
});
}
+16 -29
View File
@@ -86,8 +86,8 @@ function logout(req, res) {
function passwordResetRequest(req, res, next) { 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')); 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) { users.sendPasswordResetByIdentifier(req.body.identifier, function (error) {
if (error && error.reason !== BoxError.NOT_FOUND) console.error(error); if (error && error.reason !== BoxError.NOT_FOUND) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {})); next(new HttpSuccess(202, {}));
}); });
@@ -102,16 +102,17 @@ function passwordReset(req, res, next) {
users.getByResetToken(req.body.resetToken, function (error, userObject) { users.getByResetToken(req.body.resetToken, function (error, userObject) {
if (error) return next(new HttpError(401, 'Invalid resetToken')); if (error) return next(new HttpError(401, 'Invalid resetToken'));
if (Date.now() - userObject.resetTokenCreationTime > 24 * 60 * 60 * 1000) return next(new HttpError(401, 'Token expired')); // if you fix the duration here, the emails and UI have to be fixed as well
if (Date.now() - userObject.resetTokenCreationTime > 7 * 24 * 60 * 60 * 1000) return next(new HttpError(401, 'Token expired'));
if (!userObject.username) return next(new HttpError(409, 'No username set')); if (!userObject.username) return next(new HttpError(409, 'No username set'));
// setPassword clears the resetToken // setPassword clears the resetToken
users.setPassword(userObject, req.body.password, function (error) { users.setPassword(userObject, req.body.password, function (error) {
if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(400, error.message)); 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) { 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 })); next(new HttpSuccess(202, { accessToken: result.accessToken }));
}); });
@@ -122,37 +123,23 @@ function passwordReset(req, res, next) {
function setupAccount(req, res, next) { function setupAccount(req, res, next) {
assert.strictEqual(typeof req.body, 'object'); 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.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.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')); // 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) { users.getByResetToken(req.body.resetToken, function (error, userObject) {
if (error) return next(new HttpError(401, 'Invalid Reset Token')); if (error) return next(new HttpError(401, 'Invalid Reset Token'));
// 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 (Date.now() - userObject.resetTokenCreationTime > 24 * 60 * 60 * 1000) return next(new HttpError(401, 'Token expired'));
users.update(userObject, { username: req.body.username, displayName: req.body.displayName }, auditSource.fromRequest(req), function (error) { users.setupAccount(userObject, req.body, auditSource.fromRequest(req), function (error, accessToken) {
if (error && error.reason === BoxError.ALREADY_EXISTS) return next(new HttpError(409, 'Username already used')); if (error) return next(BoxError.toHttpError(error));
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));
userObject.username = req.body.username; next(new HttpSuccess(201, { accessToken }));
userObject.displayName = req.body.displayName;
// 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 }));
});
});
}); });
}); });
} }
@@ -218,8 +205,8 @@ function checkForUpdates(req, res, next) {
req.clearTimeout(); req.clearTimeout();
async.series([ async.series([
updateChecker.checkAppUpdates, (done) => updateChecker.checkAppUpdates({ automatic: false }, done),
updateChecker.checkBoxUpdates (done) => updateChecker.checkBoxUpdates({ automatic: false }, done),
], function () { ], function () {
next(new HttpSuccess(200, { update: updateChecker.getUpdateInfo() })); next(new HttpSuccess(200, { update: updateChecker.getUpdateInfo() }));
}); });
-2
View File
@@ -33,7 +33,6 @@ function add(req, res, next) {
let fallbackCertificate = req.body.fallbackCertificate; let fallbackCertificate = req.body.fallbackCertificate;
if (!fallbackCertificate.cert || typeof fallbackCertificate.cert !== 'string') return next(new HttpError(400, 'fallbackCertificate.cert must be a string')); if (!fallbackCertificate.cert || typeof fallbackCertificate.cert !== 'string') return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
if (!fallbackCertificate.key || typeof fallbackCertificate.key !== 'string') return next(new HttpError(400, 'fallbackCertificate.key must be a string')); if (!fallbackCertificate.key || typeof fallbackCertificate.key !== 'string') return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
if ('restricted' in fallbackCertificate && typeof fallbackCertificate.restricted !== 'boolean') return next(new HttpError(400, 'fallbackCertificate.restricted must be a boolean'));
} }
if ('tlsConfig' in req.body) { if ('tlsConfig' in req.body) {
@@ -95,7 +94,6 @@ function update(req, res, next) {
let fallbackCertificate = req.body.fallbackCertificate; let fallbackCertificate = req.body.fallbackCertificate;
if (!fallbackCertificate.cert || typeof fallbackCertificate.cert !== 'string') return next(new HttpError(400, 'fallbackCertificate.cert must be a string')); if (!fallbackCertificate.cert || typeof fallbackCertificate.cert !== 'string') return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
if (!fallbackCertificate.key || typeof fallbackCertificate.key !== 'string') return next(new HttpError(400, 'fallbackCertificate.key must be a string')); if (!fallbackCertificate.key || typeof fallbackCertificate.key !== 'string') return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
if ('restricted' in fallbackCertificate && typeof fallbackCertificate.restricted !== 'boolean') return next(new HttpError(400, 'fallbackCertificate.restricted must be a boolean'));
} }
if ('tlsConfig' in req.body) { if ('tlsConfig' in req.body) {
+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));
});
});
}
+4 -2
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')); 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)); if (error) return next(BoxError.toHttpError(error));
var groupInfo = { var groupInfo = {
@@ -69,7 +71,7 @@ function updateMembers(req, res, next) {
} }
function list(req, res, next) { function list(req, res, next) {
groups.getAll(function (error, result) { groups.getAllWithMembers(function (error, result) {
if (error) return next(BoxError.toHttpError(error)); if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { groups: result })); next(new HttpSuccess(200, { groups: result }));
+1
View File
@@ -10,6 +10,7 @@ exports = module.exports = {
cloudron: require('./cloudron.js'), cloudron: require('./cloudron.js'),
domains: require('./domains.js'), domains: require('./domains.js'),
eventlog: require('./eventlog.js'), eventlog: require('./eventlog.js'),
filemanager: require('./filemanager.js'),
graphs: require('./graphs.js'), graphs: require('./graphs.js'),
groups: require('./groups.js'), groups: require('./groups.js'),
mail: require('./mail.js'), mail: require('./mail.js'),
+55 -44
View File
@@ -1,34 +1,35 @@
'use strict'; 'use strict';
exports = module.exports = { exports = module.exports = {
getDomain: getDomain, getDomain,
setDnsRecords: setDnsRecords, setDnsRecords,
getStatus: getStatus, getStatus,
setMailFromValidation: setMailFromValidation, setMailFromValidation,
setCatchAllAddress: setCatchAllAddress, setCatchAllAddress,
setMailRelay: setMailRelay, setMailRelay,
setMailEnabled: setMailEnabled, setMailEnabled,
sendTestMail: sendTestMail, sendTestMail,
listMailboxes: listMailboxes, listMailboxes,
getMailbox: getMailbox, getMailbox,
addMailbox: addMailbox, addMailbox,
updateMailbox: updateMailbox, updateMailbox,
removeMailbox: removeMailbox, removeMailbox,
listAliases: listAliases, getAliases,
getAliases: getAliases, setAliases,
setAliases: setAliases,
getLists: getLists, getLists,
getList: getList, getList,
addList: addList, addList,
updateList: updateList, updateList,
removeList: removeList, removeList,
getMailboxCount
}; };
var assert = require('assert'), var assert = require('assert'),
@@ -160,13 +161,25 @@ function listMailboxes(req, res, next) {
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25; 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')); 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)); if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { mailboxes: result })); 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) { function getMailbox(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string'); assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.name, 'string'); assert.strictEqual(typeof req.params.name, 'string');
@@ -208,29 +221,15 @@ function removeMailbox(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string'); assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.name, 'string'); assert.strictEqual(typeof req.params.name, 'string');
mail.removeMailbox(req.params.name, req.params.domain, auditSource.fromRequest(req), function (error) { if (typeof req.body.deleteMails !== 'boolean') return next(new HttpError(400, 'deleteMails must be a boolean'));
mail.removeMailbox(req.params.name, req.params.domain, req.body, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error)); if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201, {})); next(new HttpSuccess(201, {}));
}); });
} }
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) { function getAliases(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string'); assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.name, 'string'); assert.strictEqual(typeof req.params.name, 'string');
@@ -249,8 +248,10 @@ function setAliases(req, res, next) {
if (!Array.isArray(req.body.aliases)) return next(new HttpError(400, 'aliases must be an array')); 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++) { for (let alias of req.body.aliases) {
if (typeof req.body.aliases[i] !== 'string') return next(new HttpError(400, 'alias must be a string')); 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) { mail.setAliases(req.params.name, req.params.domain, req.body.aliases, function (error) {
@@ -263,7 +264,15 @@ function setAliases(req, res, next) {
function getLists(req, res, next) { function getLists(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string'); 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)); if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { lists: result })); next(new HttpSuccess(200, { lists: result }));
@@ -292,8 +301,9 @@ function addList(req, res, next) {
for (var i = 0; i < req.body.members.length; i++) { 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.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)); if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201, {})); next(new HttpSuccess(201, {}));
@@ -310,8 +320,9 @@ function updateList(req, res, next) {
for (var i = 0; i < req.body.members.length; i++) { 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.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)); if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204)); next(new HttpSuccess(204));
+1 -1
View File
@@ -21,7 +21,7 @@ function proxy(req, res, next) {
delete req.headers['authorization']; delete req.headers['authorization'];
delete req.headers['cookies']; 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)); if (error) return next(BoxError.toHttpError(error));
parsedUrl.query['access_token'] = addonDetails.token; parsedUrl.query['access_token'] = addonDetails.token;
+34 -21
View File
@@ -1,34 +1,41 @@
'use strict'; 'use strict';
exports = module.exports = { exports = module.exports = {
get: get, authorize,
update: update, get,
getAvatar: getAvatar, update,
setAvatar: setAvatar, getAvatar,
clearAvatar: clearAvatar, setAvatar,
changePassword: changePassword, clearAvatar,
setTwoFactorAuthenticationSecret: setTwoFactorAuthenticationSecret, changePassword,
enableTwoFactorAuthentication: enableTwoFactorAuthentication, setTwoFactorAuthenticationSecret,
disableTwoFactorAuthentication: disableTwoFactorAuthentication enableTwoFactorAuthentication,
disableTwoFactorAuthentication,
}; };
var assert = require('assert'), var assert = require('assert'),
auditSource = require('../auditsource.js'), auditSource = require('../auditsource.js'),
BoxError = require('../boxerror.js'), BoxError = require('../boxerror.js'),
fs = require('fs'),
HttpError = require('connect-lastmile').HttpError, HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess, HttpSuccess = require('connect-lastmile').HttpSuccess,
path = require('path'),
paths = require('../paths.js'),
safe = require('safetydance'),
users = require('../users.js'), users = require('../users.js'),
settings = require('../settings.js'), settings = require('../settings.js'),
_ = require('underscore'); _ = require('underscore');
function get(req, res, next) { function authorize(req, res, next) {
assert.strictEqual(typeof req.user, 'object'); 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, { next(new HttpSuccess(200, {
id: req.user.id, id: req.user.id,
@@ -39,7 +46,7 @@ function get(req, res, next) {
twoFactorAuthenticationEnabled: req.user.twoFactorAuthenticationEnabled, twoFactorAuthenticationEnabled: req.user.twoFactorAuthenticationEnabled,
role: req.user.role, role: req.user.role,
source: req.user.source, 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 (!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) { function clearAvatar(req, res, next) {
assert.strictEqual(typeof req.user, 'object'); 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) { 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) { function changePassword(req, res, next) {
+3 -3
View File
@@ -98,11 +98,11 @@ function restore(req, res, next) {
var backupConfig = req.body.backupConfig; var backupConfig = req.body.backupConfig;
if (typeof backupConfig.provider !== 'string') return next(new HttpError(400, 'provider is required')); 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 (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 ('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 (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')); 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 // 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) { if (!status.activated && !status.setup.active && !status.restore.active) {
appstore.trackBeginSetup(status.provider); appstore.trackBeginSetup();
} }
}); });
} }
+42 -3
View File
@@ -97,16 +97,30 @@ function setBackupConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required')); 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.schedulePattern !== 'string') return next(new HttpError(400, 'schedulePattern is required'));
if (typeof req.body.intervalSecs !== 'number') return next(new HttpError(400, 'intervalSecs is required')); if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be a string'));
if ('key' in req.body && typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if ('syncConcurrency' in req.body) { if ('syncConcurrency' in req.body) {
if (typeof req.body.syncConcurrency !== 'number') return next(new HttpError(400, 'syncConcurrency must be a positive integer')); 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')); if (req.body.syncConcurrency < 1) return next(new HttpError(400, 'syncConcurrency must be a positive integer'));
} }
if ('copyConcurrency' in req.body) {
if (typeof req.body.copyConcurrency !== 'number') return next(new HttpError(400, 'copyConcurrency must be a positive integer'));
if (req.body.copyConcurrency < 1) return next(new HttpError(400, 'copyConcurrency must be a positive integer'));
}
if ('downloadConcurrency' in req.body) {
if (typeof req.body.downloadConcurrency !== 'number') return next(new HttpError(400, 'downloadConcurrency must be a positive integer'));
if (req.body.downloadConcurrency < 1) return next(new HttpError(400, 'downloadConcurrency must be a positive integer'));
}
if ('deleteConcurrency' in req.body) {
if (typeof req.body.deleteConcurrency !== 'number') return next(new HttpError(400, 'deleteConcurrency must be a positive integer'));
if (req.body.deleteConcurrency < 1) return next(new HttpError(400, 'deleteConcurrency must be a positive integer'));
}
if ('memoryLimit' in req.body && typeof req.body.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit must be a positive integer'));
if (typeof req.body.format !== 'string') return next(new HttpError(400, 'format must be a string')); 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 ('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 // testing the backup using put/del takes a bit of time at times
req.clearTimeout(); req.clearTimeout();
@@ -159,6 +173,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 ('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 ('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 ('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 ('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')); if ('bindPassword' in req.body && typeof req.body.bindPassword !== 'string') return next(new HttpError(400, 'bindPassword must be a string'));
@@ -232,6 +247,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) { function getSysinfoConfig(req, res, next) {
settings.getSysinfoConfig(function (error, sysinfoConfig) { settings.getSysinfoConfig(function (error, sysinfoConfig) {
if (error) return next(BoxError.toHttpError(error)); if (error) return next(BoxError.toHttpError(error));
@@ -268,6 +304,7 @@ function get(req, res, next) {
case settings.BOX_AUTOUPDATE_PATTERN_KEY: return getBoxAutoupdatePattern(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.TIME_ZONE_KEY: return getTimeZone(req, res, next);
case settings.DIRECTORY_CONFIG_KEY: return getDirectoryConfig(req, res, next);
case settings.SUPPORT_CONFIG_KEY: return getSupportConfig(req, res, next); case settings.SUPPORT_CONFIG_KEY: return getSupportConfig(req, res, next);
default: return next(new HttpError(404, 'No such setting')); default: return next(new HttpError(404, 'No such setting'));
@@ -289,6 +326,8 @@ function set(req, res, next) {
case settings.BOX_AUTOUPDATE_PATTERN_KEY: return setBoxAutoupdatePattern(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.TIME_ZONE_KEY: return setTimeZone(req, res, next);
case settings.DIRECTORY_CONFIG_KEY: return setDirectoryConfig(req, res, next);
default: return next(new HttpError(404, 'No such setting')); default: return next(new HttpError(404, 'No such setting'));
} }
} }
+32 -1
View File
@@ -62,7 +62,7 @@ function setup(done) {
}, },
function createSettings(callback) { 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 }, schedulePattern: '00 00 23 * * *' }, callback);
} }
], done); ], done);
} }
@@ -108,4 +108,35 @@ describe('Backups API', function () {
}); });
}); });
}); });
describe('check', function () {
it('fails due to mising token', function (done) {
superagent.get(SERVER_URL + '/api/v1/backups/check')
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails due to wrong token', function (done) {
superagent.get(SERVER_URL + '/api/v1/backups/check')
.query({ access_token: token.toUpperCase() })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/backups/check')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.ok).to.equal(false);
expect(result.body.message).to.not.be.empty();
done();
});
});
});
}); });
+2 -2
View File
@@ -32,7 +32,7 @@ function setup(done) {
server.start.bind(server), server.start.bind(server),
database._clear, database._clear,
settings._setApiServerOrigin.bind(null, 'http://localhost:6060'), 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 }, schedulePattern: '00 00 23 * * *' })
], done); ], done);
} }
@@ -44,7 +44,7 @@ function cleanup(done) {
}); });
} }
describe('Cloudron', function () { describe('Cloudron API', function () {
describe('activate', 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) { it('create fails due to mising token', function (done) {
superagent.post(SERVER_URL + '/api/v1/groups') superagent.post(SERVER_URL + '/api/v1/groups')
.send({ name: GROUP_NAME}) .send({ name: GROUP_NAME })
.end(function (error, result) { .end(function (error, result) {
expect(result.statusCode).to.equal(401); expect(result.statusCode).to.equal(401);
done(); done();
@@ -96,7 +96,7 @@ describe('Groups API', function () {
it('create succeeds', function (done) { it('create succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/groups') superagent.post(SERVER_URL + '/api/v1/groups')
.query({ access_token: token }) .query({ access_token: token })
.send({ name: GROUP_NAME}) .send({ name: GROUP_NAME })
.end(function (error, result) { .end(function (error, result) {
expect(result.statusCode).to.equal(201); expect(result.statusCode).to.equal(201);
groupObject = result.body; groupObject = result.body;

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