Compare commits

..

624 Commits

Author SHA1 Message Date
Girish Ramakrishnan 0cf40bd207 More 4.4.4 changes 2020-01-07 18:31:10 -08:00
Girish Ramakrishnan 4a283e9f35 4.4.4 changes 2020-01-06 08:55:22 -08:00
Johannes Zellner 5ab37bcf7e Disable test if dns setup succeeds twice 2020-01-06 12:21:36 +01:00
Johannes Zellner 9151965cd6 Keep user objects in REST api responses more coherent 2020-01-06 11:54:00 +01:00
Girish Ramakrishnan c5cd71f9e3 Disable motd-news
https://forum.cloudron.io/topic/2050/switch-to-debian-ubuntu-spying
2020-01-05 15:25:15 -08:00
Girish Ramakrishnan 602b335c0e add openldap compat
apps like firefly-iii seem to require these fields when using the
openldap driver
2020-01-05 15:14:46 -08:00
Girish Ramakrishnan 837c8b85c2 2020: happy new year 2020-01-02 16:55:47 -08:00
Girish Ramakrishnan 7d16396e72 clone: custom mailbox name is not cloned 2020-01-01 23:05:34 -08:00
Girish Ramakrishnan 66d3d07148 append error message when verifying dns config 2020-01-01 16:17:16 -08:00
Girish Ramakrishnan b5c1161caa add tokenType to cloudflare config 2020-01-01 16:01:39 -08:00
Girish Ramakrishnan b0420889ad cloudflare: add api token support 2019-12-31 16:47:47 -08:00
Girish Ramakrishnan 527819d886 cloudflare: refactor superagent logic 2019-12-31 16:25:49 -08:00
Girish Ramakrishnan 1ad0cff28e Use app.fqdn in output 2019-12-24 11:07:53 -08:00
Johannes Zellner 783ec03ac9 The setup views require webServerOrigin for documentation purpose 2019-12-23 17:15:45 +01:00
Girish Ramakrishnan 6cd395d494 Allow restore from error state 2019-12-20 17:58:42 -08:00
Girish Ramakrishnan 681079e01c repair: reconfigure for all other states
the idea was that the failed routes can be called again in other cases
2019-12-20 17:00:53 -08:00
Girish Ramakrishnan aabbc43769 4.4.3 changes 2019-12-20 11:29:02 -08:00
Girish Ramakrishnan 2692f6ef4e Add restart route for atomicity 2019-12-20 11:15:36 -08:00
Girish Ramakrishnan 887cbb0b22 make percent non-zero 2019-12-18 09:33:44 -08:00
Johannes Zellner ca4fdc1be8 Add azure-image provider argument 2019-12-17 16:42:25 +01:00
Girish Ramakrishnan 93199c7f5b eventlog: support ticket and ssh 2019-12-16 14:06:55 -08:00
Girish Ramakrishnan 4c6566f42f stopped apps should not be updated or auto-updated 2019-12-16 13:29:15 -08:00
Johannes Zellner c38f7d7f93 Make properties explicitly available 2019-12-16 15:21:26 +01:00
Girish Ramakrishnan da85cea329 avatar: remove query param
let the ui add the size and default
2019-12-13 13:45:02 -08:00
Girish Ramakrishnan d5c70a2b11 Add sshd port warning 2019-12-13 11:32:36 -08:00
Girish Ramakrishnan fe355b4bac 4.4.2 changes 2019-12-12 20:44:54 -08:00
Girish Ramakrishnan a7dee6be51 cloudron.runSystemChecks should take a callback 2019-12-12 20:41:03 -08:00
Girish Ramakrishnan 2817dc0603 Not required to run any cron job immediately 2019-12-12 20:39:40 -08:00
Girish Ramakrishnan 6f36c72e88 Fix crash in mail.checkConfiguration 2019-12-12 20:36:27 -08:00
Girish Ramakrishnan 45e806c455 typo in comment 2019-12-12 19:54:59 -08:00
Johannes Zellner bbdd76dd37 Fix and add memory route tests 2019-12-12 13:21:24 +01:00
Johannes Zellner 09921e86c0 Remove redunandant memory property from config
we have a specific route for this now
2019-12-12 12:14:08 +01:00
Girish Ramakrishnan d6e4b64103 4.4.1 changes 2019-12-11 15:27:47 -08:00
Girish Ramakrishnan 9dd3e4537a return 422 on instance id mismatch
the ui redirects otherwise
2019-12-11 15:13:38 -08:00
Girish Ramakrishnan a5f31e8724 Revert "rename ami to aws-mp"
This reverts commit 72ac00b69a.

Existing code relies on this, so don't change it
2019-12-11 12:56:30 -08:00
Girish Ramakrishnan 72ac00b69a rename ami to aws-mp 2019-12-11 12:27:55 -08:00
Girish Ramakrishnan ae5722a7d4 eventlog: typo when mail list is removed 2019-12-11 10:05:45 -08:00
Johannes Zellner 4e3192d450 Avoid double dns setup tracking 2019-12-11 14:02:40 +01:00
Johannes Zellner ccca3aca04 Send setup state to get the actually correct ip 2019-12-10 18:01:07 +01:00
Girish Ramakrishnan e4dd5d6434 Fix crash when uploading file 2019-12-09 15:02:51 -08:00
Girish Ramakrishnan 9a77fb6306 acme2: implement post-as-get
https://tools.ietf.org/html/rfc8555#section-6.3
https://community.letsencrypt.org/t/post-as-get-and-empty-payload-instead-of/86720/3
https://community.letsencrypt.org/t/problem-with-renew-certificates-the-request-message-was-malformed-method-not-allowed/107889/17
2019-12-08 19:17:52 -08:00
Girish Ramakrishnan 3ec5c713bf debug: certFilePath is undefined 2019-12-08 18:23:12 -08:00
Girish Ramakrishnan 837fc27e94 canAutoupdateApp now returns bool 2019-12-08 16:55:56 -08:00
Girish Ramakrishnan 9ad6025310 search and replace gone wrong 2019-12-06 13:52:43 -08:00
Girish Ramakrishnan d765e4c619 add a note 2019-12-06 12:39:46 -08:00
Girish Ramakrishnan f5217236d6 Change the version number 2019-12-06 12:28:08 -08:00
Girish Ramakrishnan 8f8d099faf Add to changes 2019-12-06 12:23:49 -08:00
Girish Ramakrishnan 16660e083f Also set overwriteDns when manifest is not provided 2019-12-06 12:21:28 -08:00
Girish Ramakrishnan 4e35020a1c Set overwriteDns for install task 2019-12-06 12:11:34 -08:00
Girish Ramakrishnan 111e0bcb5f Fix repair route path 2019-12-06 11:44:41 -08:00
Girish Ramakrishnan d7f9a547fc Disable requiredState check for now
there is a race but this is mitigated by the checkAppState non-db logic
for now
2019-12-06 11:29:35 -08:00
Girish Ramakrishnan 6a64f24e98 Fix repair
If a task fails, we can either:
* allow other task ops to be called - we cannot do this because the ops are fine-grained. for example,
  a restore failure removes many things and calling set-memory or set-location in that state won't
  make sense.

* provide a generic repair route - this allows one to override args and call the failed task
  again. this is what we have now but has the issue that this repair function has to know about all
  the other op functions. for example, for argument validation. we can do some complicated refactoring
  to make it work if we want.

* just a generic total re-configure - this does not work because clone/restore/backup/datadir/uninstall/update
  failure leaves the app in a state which re-configure cannot do anything about.

* allow the failed op to be called again - this seems the easiest. we just allow the route to be called again
  in the error state.

* if we hit a state where even providing extra args, cannot get you out of this "error" state, we have to provide
  some repair route. for example, maybe the container disappeared by some docke error. user clicks 'repair' to
  recreate the container. this route does not have to take any args.

The final solution is:
* a failed task can be called again via the route. so we can resubmit any args and we get validation
* repair route just re-configures and can be called in any state to just rebuild container. re-configure is also
  doing only local changes (docker, nginx)
* install/clone failures are fixed using repair route. updated manifest can be passed in.
* UI shows backup selector for restore failures
* UI shows domain selector for change location failulre
2019-12-06 09:56:09 -08:00
Girish Ramakrishnan 37d7be93b5 Move oldManifest out of restoreConfig 2019-12-06 09:56:03 -08:00
Girish Ramakrishnan 9c809aa6e1 remove dead comment 2019-12-06 09:35:08 -08:00
Girish Ramakrishnan 7ab9f3fa2f re-configure does not require oldConfig
this is only needed when changing location now. the configure()
is now entirely local i.e rebuild local container and the reverse
proxy config
2019-12-06 09:23:58 -08:00
Girish Ramakrishnan ffeb484a10 No need to return args as part of task.get
This reverts commit 831e22b4ff.
This reverts commit 6774514bd2.
2019-12-06 08:42:49 -08:00
Girish Ramakrishnan 2ffb32ae60 Skip moving data if source and target are same 2019-12-06 08:09:43 -08:00
Girish Ramakrishnan 905bb92bad s3: ensure BoxError return 2019-12-05 21:50:44 -08:00
Girish Ramakrishnan 3926efd153 restore: only take non-empty backupId 2019-12-05 21:16:35 -08:00
Girish Ramakrishnan c5e5bb90e3 better error message 2019-12-05 21:16:35 -08:00
Girish Ramakrishnan cea543cba5 On backup error, only set the task error
at some point, the backup ui can show this error
2019-12-05 16:34:40 -08:00
Girish Ramakrishnan a8b489624d fix error messages 2019-12-05 16:27:00 -08:00
Girish Ramakrishnan 49d3bddb62 Show download progress when restoring rsync backups 2019-12-05 15:44:52 -08:00
Girish Ramakrishnan c0ff3cbd22 move progressTag to the end 2019-12-05 15:44:52 -08:00
Girish Ramakrishnan 1de97d6967 do not clear localstorage during in-place import 2019-12-05 12:42:08 -08:00
Girish Ramakrishnan a44a82083e Add backups.testProviderConfig
fields like format/retention won't be validated here since it's only
testing the access credentials
2019-12-05 11:55:53 -08:00
Girish Ramakrishnan d57681ff21 put fqdn in the end 2019-12-05 11:15:21 -08:00
Girish Ramakrishnan e3de2f81d3 setup and clear addons before import 2019-12-05 11:12:40 -08:00
Girish Ramakrishnan e8c5f8164c do not delete data dir for in-place import 2019-12-05 11:01:27 -08:00
Girish Ramakrishnan c07e215148 Use BoxError in on error cases 2019-12-05 09:54:29 -08:00
Girish Ramakrishnan 4bb676fb5c add asserts 2019-12-05 09:32:45 -08:00
Johannes Zellner dbdf86edfc No need to return the same data which the route got passed in 2019-12-05 18:02:57 +01:00
Johannes Zellner 2c8e6330ce Do not allow to change the sysinfo in demo mode 2019-12-05 16:06:21 +01:00
Girish Ramakrishnan 1b563854a7 implement in-place import and custom backup config 2019-12-04 19:27:05 -08:00
Girish Ramakrishnan 80b890101b Add changes 2019-12-04 17:53:02 -08:00
Girish Ramakrishnan c3696469ff Add app fqdn to backup progress message 2019-12-04 17:49:31 -08:00
Girish Ramakrishnan 3e08e7c653 Typo in docker socket path 2019-12-04 14:37:00 -08:00
Girish Ramakrishnan 53e39f571c Make addons code remove a BoxError 2019-12-04 14:28:42 -08:00
Girish Ramakrishnan c992853cca lint 2019-12-04 11:18:39 -08:00
Girish Ramakrishnan 85e17b570b Use whilst instead of forever
this gets rid of the Error object
2019-12-04 11:17:44 -08:00
Girish Ramakrishnan 30eccfb54b Use BoxError instead of Error in all places
This moves everything other than the addon code and some 'done' logic
2019-12-04 11:02:54 -08:00
Girish Ramakrishnan 3623831390 Typo 2019-12-04 10:23:16 -08:00
Girish Ramakrishnan d0a3d00492 Use NOT_IMPLEMENTED error code 2019-12-04 10:22:22 -08:00
Girish Ramakrishnan 0b6fbfd910 Better addon error messages 2019-12-04 10:09:57 -08:00
Girish Ramakrishnan 8cfb27fdcd Add changes 2019-12-03 15:39:29 -08:00
Girish Ramakrishnan 841ab54565 better logs 2019-12-03 15:11:27 -08:00
Girish Ramakrishnan a2e9254343 lint 2019-12-03 15:10:06 -08:00
Johannes Zellner 43cb03a292 Send provider and version during registration 2019-12-02 18:19:51 +01:00
Johannes Zellner f2fca33309 Add support to upload custom profile avatar 2019-12-02 18:03:54 +01:00
Johannes Zellner 14d26fe064 Do not crash on migration
A bit late but still
2019-12-02 18:03:54 +01:00
Girish Ramakrishnan 9cc968e790 Pass the new data dir as a task argument 2019-11-25 14:22:27 -08:00
Girish Ramakrishnan 831e22b4ff Fix failing test 2019-11-23 18:35:15 -08:00
Girish Ramakrishnan 6774514bd2 Return args as part of task.get
the ui needs this to repair any failed app task
2019-11-23 18:06:33 -08:00
Girish Ramakrishnan f543b98764 Remove BoxError.UNKNOWN_ERROR 2019-11-22 14:27:41 -08:00
Johannes Zellner 2e94600afe Don't set 'Starting ...' as initial task progress message
This is confusing for tasks like "stop" as it will say "Starting ..."
2019-11-22 13:54:43 +01:00
Johannes Zellner 9295ce783a Other logs are lowercase 2019-11-22 12:31:41 +01:00
Johannes Zellner 134f8a28bf Hide access tokens from logs 2019-11-22 12:29:13 +01:00
Girish Ramakrishnan ab5e4e998c Fix reduce usage 2019-11-21 13:48:31 -08:00
Girish Ramakrishnan a98551f99c rename disks to system 2019-11-21 13:01:08 -08:00
Girish Ramakrishnan 42fe84152a return swap information 2019-11-21 12:55:17 -08:00
Girish Ramakrishnan 8a3d212bd4 Fix note 2019-11-20 16:17:47 -08:00
Girish Ramakrishnan af51ddc347 Fix crash when user with active session is deleted 2019-11-20 16:12:21 -08:00
Girish Ramakrishnan b582e549c2 do not unconfigure reverse proxy on container destroy 2019-11-20 15:38:55 -08:00
Girish Ramakrishnan 5efbccd974 Revert migration change since some cloudrons already got 4.3.3 2019-11-20 14:43:01 -08:00
Johannes Zellner 82f5cd6075 Remove unused stuff in external ldap tests 2019-11-20 22:30:53 +01:00
Johannes Zellner 0d8820c247 Add external ldap tests 2019-11-20 22:21:40 +01:00
Girish Ramakrishnan 37c6a96a3a s3: if etag is not present, flag as error 2019-11-20 12:53:36 -08:00
Johannes Zellner c53b54bda3 Only create external ldap users for oauth logins 2019-11-20 20:05:22 +01:00
Girish Ramakrishnan 808753ad3a CLI tokens are now valid for a month 2019-11-20 10:07:15 -08:00
Girish Ramakrishnan f919570cea Fix tests
mailboxDomain can be null (even though install/clone currently always
allocate one)
2019-11-20 09:57:51 -08:00
Johannes Zellner 9acf49a99e Fix typo 2019-11-20 18:18:21 +01:00
Johannes Zellner 239883d01f Add autoCreate flag to external ldap config 2019-11-20 18:18:21 +01:00
Johannes Zellner e3cee37527 Move autocreation logic into external ldap 2019-11-20 18:18:21 +01:00
Johannes Zellner 8fd0461c62 Auto create users on login if present in external ldap source 2019-11-20 18:18:21 +01:00
Girish Ramakrishnan 4d2b5c83ca Bump version to re-generate configs 2019-11-19 17:36:05 -08:00
Girish Ramakrishnan bc314c1119 Re-generate collectd and logrotate configs on container recreate
this was the reason graphs were not showing up properly
2019-11-19 17:28:31 -08:00
Girish Ramakrishnan d01749a2c2 Add 4.3.4 changes 2019-11-19 11:42:48 -08:00
Girish Ramakrishnan b46154676a Do not error if fallback certs went missing
This atleast lets the user remove and add the domain to fix things up
2019-11-19 09:36:35 -08:00
Girish Ramakrishnan fd2d60dca3 Match the version entirely during restore
Sometimes, we introduce migrations in patch releases and this causes
problems when restoring the sql dump
2019-11-18 15:05:01 -08:00
Girish Ramakrishnan ed17bdc7c3 typo 2019-11-17 21:16:36 -08:00
Girish Ramakrishnan ac05399cda Add changes 2019-11-17 12:29:53 -08:00
Girish Ramakrishnan 1af5c6a418 Fix registry detection
ECR registry does not have a username component
2019-11-17 11:50:58 -08:00
Girish Ramakrishnan e2bb668fe4 add note on appStoreId 2019-11-16 10:31:38 -08:00
Girish Ramakrishnan d255466417 manifest.id is optional for custom apps 2019-11-15 17:28:54 -08:00
Girish Ramakrishnan 5509406395 add mailboxDomain field to apps table 2019-11-15 09:40:35 -08:00
Girish Ramakrishnan 97333474c4 Remove delay introduced by mistake in 5c920fd20 2019-11-14 17:40:21 -08:00
Johannes Zellner 38928d63d6 node's http server has a default timeout of 2min which is too short for build bot 2019-11-14 13:15:18 +01:00
Girish Ramakrishnan 05c64dcbf2 move unbound config to separate file 2019-11-13 14:48:56 -08:00
Girish Ramakrishnan e39b081567 Change restart policy to unless-stopped 2019-11-13 10:29:54 -08:00
Girish Ramakrishnan 62174658cf 4.3.2 changes 2019-11-12 16:43:27 -08:00
Girish Ramakrishnan 3d26e8a666 Revert "Update cloudron-manifestformat and other modules"
This reverts commit 3d337640ef.

Only update cloudron-manifestformat
2019-11-12 14:54:16 -08:00
Girish Ramakrishnan 3d337640ef Update cloudron-manifestformat and other modules 2019-11-12 14:26:13 -08:00
Girish Ramakrishnan 985eaf8ca9 Better progress message 2019-11-11 17:09:46 -08:00
Girish Ramakrishnan e0bee13812 validate sysinfo in setup as well 2019-11-11 16:32:29 -08:00
Girish Ramakrishnan 7c6922d228 validate sysinfo configuration 2019-11-11 16:05:53 -08:00
Girish Ramakrishnan bf68c2d321 default has changed 2019-11-11 11:19:42 -08:00
Girish Ramakrishnan fd51320fb7 sysinfoConfig is now non-optional 2019-11-11 11:05:34 -08:00
Girish Ramakrishnan 815392ba38 restore: add sysinfoConfig 2019-11-11 09:49:18 -08:00
Girish Ramakrishnan f8c110f75c 4.3.1 changes 2019-11-11 09:43:19 -08:00
Girish Ramakrishnan 70f9ceb1b8 better not found message 2019-11-11 09:13:45 -08:00
Girish Ramakrishnan 2353a8b5fa list unstable apps by default 2019-11-11 08:42:00 -08:00
Girish Ramakrishnan cf1c2dc1ee Fix crash when listing mailboxes 2019-11-10 12:44:39 -08:00
Johannes Zellner 467283d5e0 Destroy all session by a user if wanted 2019-11-08 21:32:55 +01:00
Girish Ramakrishnan a887e19d46 Update mail container 2019-11-07 15:16:51 -08:00
Girish Ramakrishnan 2ab941660e Fix haraka crash
https://github.com/haraka/Haraka/issues/2732
2019-11-07 15:10:34 -08:00
Girish Ramakrishnan a75769071c remove obsolete test 2019-11-07 14:23:57 -08:00
Girish Ramakrishnan 7f2af067cf Add enums for cid 2019-11-07 13:38:33 -08:00
Girish Ramakrishnan 88454e7d6c remove unused function 2019-11-07 13:35:37 -08:00
Girish Ramakrishnan 5c920fd200 never skip password verification 2019-11-07 13:10:12 -08:00
Girish Ramakrishnan ab650c7a95 more changes 2019-11-07 11:13:52 -08:00
Girish Ramakrishnan 1e776bbbe0 Add route to get public IP 2019-11-07 10:41:15 -08:00
Girish Ramakrishnan cd0294129f Add changes 2019-11-07 09:25:04 -08:00
Johannes Zellner d1c6e786c2 Remove unused CLOUDRON_ADMIN_EMAIL 2019-11-07 16:38:30 +01:00
Girish Ramakrishnan 58d66b5293 mail: resolve list members 2019-11-06 21:45:54 -08:00
Girish Ramakrishnan 1942a7ecf4 redis: start app redis addons on image update 2019-11-06 09:38:20 -08:00
Girish Ramakrishnan 22c2add55e Update redis 2019-11-05 21:59:35 -08:00
Girish Ramakrishnan 60c5cccfc2 Add MAIL_ERROR 2019-11-05 20:55:21 -08:00
Girish Ramakrishnan b4874ec1f4 refactor getting mail auth 2019-11-05 19:54:53 -08:00
Girish Ramakrishnan d7b326bf2b clone: appdb.add must also put the reverse proxy config 2019-11-05 13:58:02 -08:00
Girish Ramakrishnan b9d8b5f973 clone: copy reverseProxyConfig 2019-11-05 12:50:30 -08:00
Girish Ramakrishnan 64fd6e0dac Allow redis with no password 2019-11-05 10:48:36 -08:00
Girish Ramakrishnan 868103e7e4 Add changes 2019-11-05 09:21:23 -08:00
Johannes Zellner 3354cb8ebe Add network interface check 2019-11-05 15:03:36 +01:00
Johannes Zellner 4fc012dea0 Fix typo in sysinfo route handler 2019-11-05 13:45:06 +01:00
Girish Ramakrishnan 947cb786d6 ldapsync: add progress callback 2019-11-04 12:05:35 -08:00
Girish Ramakrishnan 689f2791ba validate fields in testConfig 2019-10-31 11:46:00 -07:00
Girish Ramakrishnan a5ec5b0ed9 externalLdap: search and then bind 2019-10-30 15:32:49 -07:00
Girish Ramakrishnan 8e5916b785 oauth2: catch any errors in handlers 2019-10-30 15:15:36 -07:00
Girish Ramakrishnan 563f846eba style fixes 2019-10-30 14:27:58 -07:00
Girish Ramakrishnan 7781ea3205 remove this check, let if get marked as conflicting 2019-10-30 11:05:26 -07:00
Girish Ramakrishnan 2f5ece8f1d make displayName also a const 2019-10-30 11:04:19 -07:00
Girish Ramakrishnan ec46dab754 camel case 2019-10-30 11:02:21 -07:00
Girish Ramakrishnan d5d27d512c make email a constant 2019-10-30 10:59:48 -07:00
Girish Ramakrishnan 0a695190c4 Remove bindDn validation
in some AD setups, this is a email
2019-10-30 09:35:33 -07:00
Girish Ramakrishnan 59deca76a1 add changes 2019-10-30 09:16:55 -07:00
Girish Ramakrishnan a829ab44f1 sysinfo: remove the ec2 and scaleway providers
we can just use the generic one for those as well
2019-10-30 09:13:01 -07:00
Girish Ramakrishnan 82a7befb92 Fix crashes 2019-10-29 20:33:32 -07:00
Girish Ramakrishnan 331d0ee717 declare the variable 2019-10-29 20:20:35 -07:00
Girish Ramakrishnan addafa529f sysinfoConfig can be passed when provisioning 2019-10-29 20:12:37 -07:00
Girish Ramakrishnan 8232d471a3 Add route to set/get sysinfo 2019-10-29 20:08:45 -07:00
Girish Ramakrishnan 813454ca82 sysinfo: Add static and network intf providers 2019-10-29 16:12:58 -07:00
Girish Ramakrishnan 7d987d7c79 make sysinfo provider a setting 2019-10-29 15:56:50 -07:00
Girish Ramakrishnan 7a25187bee Disable invite & password reset route for external users 2019-10-29 11:03:28 -07:00
Girish Ramakrishnan f97cbb5fd5 Use private registry auth 2019-10-27 13:07:07 -07:00
Girish Ramakrishnan 12d233c5f9 provide suggestion as part of the error 2019-10-27 12:01:30 -07:00
Girish Ramakrishnan 09fce1978e Add to changes 2019-10-25 17:06:16 -07:00
Girish Ramakrishnan 8ed2f98d1d print username field as well 2019-10-25 17:00:59 -07:00
Girish Ramakrishnan 13262d014b call unbind 2019-10-25 16:58:15 -07:00
Girish Ramakrishnan ade1187fc8 ldap: more logs 2019-10-25 16:46:55 -07:00
Girish Ramakrishnan 2404e79928 ldap: do the secret key dance 2019-10-25 16:46:49 -07:00
Girish Ramakrishnan d68ed91b17 ldap: add usernameField
we need this for okta where uid is the email
2019-10-25 15:50:26 -07:00
Girish Ramakrishnan 1a21423401 ldap: add provider field 2019-10-25 15:40:22 -07:00
Girish Ramakrishnan a478134759 mail: put the type and hostname in notification 2019-10-25 10:16:17 -07:00
Girish Ramakrishnan c639746211 Update changes 2019-10-24 21:43:09 -07:00
Girish Ramakrishnan 7a96e4858a Not found messages at the db level 2019-10-24 20:48:38 -07:00
Girish Ramakrishnan 02339d503c do not re-generate DATABASE_ERROR 2019-10-24 20:31:45 -07:00
Girish Ramakrishnan c3a5360a88 Add not implemented error code 2019-10-24 18:40:37 -07:00
Girish Ramakrishnan ad9097d212 Remove various uses of INTERNAL_ERROR
INTERNAL_ERROR now means there really was some internal error
2019-10-24 18:32:36 -07:00
Girish Ramakrishnan 6e57f8cc03 Refactor toHttpError code into BoxError 2019-10-24 18:09:55 -07:00
Girish Ramakrishnan d6365ff27f Move AppstoreError to BoxError 2019-10-24 17:47:16 -07:00
Girish Ramakrishnan 4793eb9ef5 Finish UsersError removal 2019-10-24 15:19:07 -07:00
Girish Ramakrishnan 03175aa8de IN_USE -> CONFLICT
also, remove databaseerror
2019-10-24 15:07:37 -07:00
Girish Ramakrishnan bc3169deb3 Move UsersError to BoxError 2019-10-24 15:06:41 -07:00
Girish Ramakrishnan 9b4d43075e Fix some typos 2019-10-24 14:34:10 -07:00
Girish Ramakrishnan d2c12297dc Move ExternalLdapError to BoxError 2019-10-24 14:32:27 -07:00
Girish Ramakrishnan 1a8496d61e Move MailError to BoxError 2019-10-24 14:10:23 -07:00
Girish Ramakrishnan a017af41c5 Start moving db code to use BoxError as well 2019-10-24 14:09:53 -07:00
Girish Ramakrishnan ec216d9828 Add PLAN_LIMIT for now
Should remove this and make it something else
2019-10-24 11:05:36 -07:00
Girish Ramakrishnan bce1efb77c Move AppsError to BoxError 2019-10-24 10:39:47 -07:00
Girish Ramakrishnan b078d37f37 Remove REVERSEPROXY_ERROR 2019-10-24 10:31:56 -07:00
Girish Ramakrishnan 8d944f74c0 Make reverseProxy return BoxError consistently 2019-10-24 10:28:38 -07:00
Girish Ramakrishnan dc10b8a07f Move AddonsError to BoxError 2019-10-23 15:57:01 -07:00
Girish Ramakrishnan 7b9f741522 Move ProvisionError to BoxError 2019-10-23 15:45:09 -07:00
Girish Ramakrishnan 51cb3b0ba8 Move DomainsError to BoxError 2019-10-23 15:15:19 -07:00
Girish Ramakrishnan 4db4834c90 rename variable 2019-10-23 15:03:42 -07:00
Girish Ramakrishnan e1f0d12251 Fix error handling 2019-10-23 09:53:46 -07:00
Girish Ramakrishnan e2388b7d88 Move UpdaterError to BoxError 2019-10-23 09:39:26 -07:00
Girish Ramakrishnan d0e6b6bfe4 Do not re-translate to DockerError 2019-10-23 09:30:05 -07:00
Girish Ramakrishnan b6f2c94464 test registry config 2019-10-23 06:49:29 -07:00
Girish Ramakrishnan 8cdddef077 Add registry config to settings table 2019-10-22 22:56:25 -07:00
Girish Ramakrishnan e82ac5ecc5 Ensure docker code returns BoxError 2019-10-22 21:46:32 -07:00
Girish Ramakrishnan db6c07f86a Move ReverseProxyError with BoxError 2019-10-22 21:24:31 -07:00
Girish Ramakrishnan 2df642000d Move ClientsError to BoxError 2019-10-22 21:16:49 -07:00
Girish Ramakrishnan 11d80cec7d Fix mailbox tests 2019-10-22 21:03:47 -07:00
Girish Ramakrishnan 8c9ce30d29 Move BackupsError to BoxError 2019-10-22 21:03:47 -07:00
Girish Ramakrishnan df142994a8 Move TaskError into BoxError 2019-10-22 21:03:47 -07:00
Girish Ramakrishnan 2d115d3d0f Move GroupsError to BoxError 2019-10-22 16:34:17 -07:00
Girish Ramakrishnan 1b594d3e50 Remove unused GroupsError 2019-10-22 16:26:38 -07:00
Girish Ramakrishnan 332f2e7c10 Move SysInfoError to BoxError 2019-10-22 14:09:44 -07:00
Girish Ramakrishnan a7614cef2e Move CloudronError to BoxError 2019-10-22 14:06:19 -07:00
Girish Ramakrishnan 9842b6d4a1 Move EventLogError to BoxError 2019-10-22 13:59:01 -07:00
Girish Ramakrishnan 88818a1ec2 Move NotificationsError to BoxError 2019-10-22 13:00:10 -07:00
Girish Ramakrishnan 812f5cce99 Move DisksError to BoxError 2019-10-22 11:11:41 -07:00
Girish Ramakrishnan fdf7da9111 Move SupportError to BoxError 2019-10-22 11:08:19 -07:00
Girish Ramakrishnan ed9e1772ea move SettingsError to BoxError 2019-10-22 11:06:14 -07:00
Girish Ramakrishnan 657a2cac2f Add pagination to mailbox listing 2019-10-22 10:12:06 -07:00
Girish Ramakrishnan d15aa2744d Fix return code if start.sh is bad 2019-10-20 13:35:19 -07:00
Girish Ramakrishnan 29ab3e91b3 gcs: remove concurrency logic
this is more complicated than necessary
2019-10-18 18:54:25 -07:00
Girish Ramakrishnan f6377fd1c6 Add email_error type 2019-10-15 11:48:20 -07:00
Girish Ramakrishnan 122a987d61 4.3 changes 2019-10-15 11:40:36 -07:00
Girish Ramakrishnan 4610e78d91 Add altEmail to support ticket (when mail is down) 2019-10-15 11:39:44 -07:00
Girish Ramakrishnan 351bd46cb7 Make external backup restore a separate route (import)
fixes #650
2019-10-15 09:20:29 -07:00
Girish Ramakrishnan 8878bc4bf9 frameAncestors -> csp
It seems we cannot separate frame ancestors from CSP because the hide
header just hides everything and not a specific resource. This means
that the user has to set or unset the full policy whole sale.
2019-10-14 17:12:01 -07:00
Girish Ramakrishnan 61b6bee946 Remove unused variable 2019-10-14 16:07:45 -07:00
Girish Ramakrishnan 9997cbddb8 Do not escape as html 2019-10-14 16:03:57 -07:00
Girish Ramakrishnan 7115498f32 Send reverseProxyConfig in REST response 2019-10-14 15:57:41 -07:00
Girish Ramakrishnan 0f05c243aa Remove redundant type checking validation 2019-10-14 15:18:21 -07:00
Girish Ramakrishnan 9c12f1fe15 Add field to configure the reverse proxy
part of #596
2019-10-14 15:05:25 -07:00
Girish Ramakrishnan 7383cc4e90 email: Auto-subscribe to Spam folder 2019-10-14 14:31:39 -07:00
Girish Ramakrishnan 6466b47ada 4.3 changes 2019-10-14 14:16:43 -07:00
Girish Ramakrishnan 1856fc05d9 Add timeout for apptask as well 2019-10-14 14:16:15 -07:00
Girish Ramakrishnan a19662bdfa Add a timeout for update as well
this will send a notification if an update gets stuck
2019-10-14 13:05:12 -07:00
Girish Ramakrishnan 488763fc42 rename appconfig to nginxconfig 2019-10-13 17:08:33 -07:00
Girish Ramakrishnan 7cbe60a484 Fix crash when only udp ports are defined 2019-10-11 20:39:03 -07:00
Girish Ramakrishnan ded9a6e377 Revert "remove unused function"
This reverts commit a19205e3ad.
2019-10-11 20:30:30 -07:00
Girish Ramakrishnan ea205363a0 More 4.2.7 changes 2019-10-11 20:23:33 -07:00
Girish Ramakrishnan ad13445c93 Revert "apptask: backupId/format is not part of install anymore"
This reverts commit 49e5c60422.
2019-10-11 20:21:48 -07:00
Girish Ramakrishnan eb5c2ed30b notify failed backups
fixes #649
2019-10-11 19:54:15 -07:00
Girish Ramakrishnan bd3080a6b3 lint 2019-10-11 19:54:15 -07:00
Girish Ramakrishnan be5290c5ca Add error code for timeout 2019-10-11 19:54:15 -07:00
Girish Ramakrishnan 43fd207164 Kill backup task after 12 hours
this will automatically notify by email

part of #649
2019-10-11 19:13:39 -07:00
Girish Ramakrishnan 34c53694a0 Add timeout option when starting task
Part of #649
2019-10-11 19:13:39 -07:00
Girish Ramakrishnan 927f8483ce 4.2.7 changes 2019-10-11 18:43:39 -07:00
Girish Ramakrishnan a19205e3ad remove unused function 2019-10-07 22:10:02 -07:00
Girish Ramakrishnan 49e5c60422 apptask: backupId/format is not part of install anymore 2019-10-07 15:29:18 -07:00
Girish Ramakrishnan 57b623ee44 Fix install with backupId 2019-10-07 15:01:00 -07:00
Girish Ramakrishnan 0c904af927 tpyo 2019-10-03 15:25:52 -07:00
Girish Ramakrishnan 9cd025972c Try acme flow 3 times 2019-10-03 14:47:18 -07:00
Girish Ramakrishnan 21111eccc4 retry downloadCertificate 2019-10-03 14:37:12 -07:00
Girish Ramakrishnan 917079f341 Add error message to network error 2019-10-03 14:33:49 -07:00
Girish Ramakrishnan 4d6d768be1 Append apptask logs 2019-10-03 12:20:15 -07:00
Girish Ramakrishnan c54cd992ca Validate the location passed in repair route 2019-10-03 12:08:05 -07:00
Girish Ramakrishnan d5ec599dd1 repair can always be called
this is because sometimes cloudron thinks there is no error, but there is
2019-10-03 11:30:00 -07:00
Girish Ramakrishnan 0542ab16d4 If cert renewal failed, continue using old cert 2019-10-03 11:11:02 -07:00
Girish Ramakrishnan 7e75ef7685 cert: add more debugs 2019-10-03 10:36:57 -07:00
Johannes Zellner f296265461 Add changes 2019-10-03 16:31:01 +02:00
Johannes Zellner fb4eade215 Location in configure route may be an empty string 2019-10-03 16:23:01 +02:00
Johannes Zellner 8b3e85907c Add 4.2.5 changes 2019-10-02 18:41:42 +02:00
Johannes Zellner ca4876649d The demo setting didn't go well 2019-10-02 18:39:06 +02:00
Johannes Zellner 7ebc2abe5d Add 4.2.4 changes 2019-10-02 14:15:46 +02:00
Johannes Zellner 37e132319b Ensure demo setting is '' or 'enabled' 2019-10-02 12:58:32 +02:00
Johannes Zellner b2728118e9 Remove unused require 2019-10-02 12:13:18 +02:00
Girish Ramakrishnan c428f649aa typo 2019-10-01 14:40:24 -07:00
Girish Ramakrishnan 7baf979a59 Fix verbose logs 2019-10-01 14:39:40 -07:00
Girish Ramakrishnan ccecaca047 Fix crash 2019-10-01 14:04:39 -07:00
Girish Ramakrishnan c7ee684f25 Fix bug where nginx was not reloaded on cert renewal
Looks like it worked so far because nginx got reloaded in situations
like apptask or server reboot.
2019-10-01 11:25:57 -07:00
Girish Ramakrishnan 52156c9a35 Remove unused type field 2019-10-01 11:17:12 -07:00
Girish Ramakrishnan 4fba216af9 scaleway: try to keep part numbers low 2019-09-30 20:42:37 -07:00
Girish Ramakrishnan 1d00c788d1 Remove dead code 2019-09-30 15:54:18 -07:00
Girish Ramakrishnan d891d39587 reverseproxy: rename to writeDefaultConfig 2019-09-30 15:28:05 -07:00
Girish Ramakrishnan cfde6e31ad reverseproxy: improve the note 2019-09-30 15:25:53 -07:00
Girish Ramakrishnan 243772d1f5 reverseproxy: do not export reload 2019-09-30 15:23:53 -07:00
Girish Ramakrishnan 1c36b8eaf7 Add debugs 2019-09-30 11:52:23 -07:00
Girish Ramakrishnan 120fa4924a Remove confusing isInstalling usage 2019-09-30 09:58:13 -07:00
Girish Ramakrishnan c3c9c2f39a Always pass restoreConfig for the restore case 2019-09-30 09:47:14 -07:00
Girish Ramakrishnan fc90829ba2 repair: Use backupId only if passed in via REST API 2019-09-30 09:13:13 -07:00
Girish Ramakrishnan ce9224c690 Set the domain and subdomain in details 2019-09-27 14:42:18 -07:00
Girish Ramakrishnan 18a2107247 Attach fqdn information consistently in the eventlog 2019-09-27 11:50:22 -07:00
Girish Ramakrishnan f13d05dad7 Update changes 2019-09-27 11:09:50 -07:00
Girish Ramakrishnan 86586444a9 Validate alternate domain
this also sets up fqdn in the eventlog entries
2019-09-27 10:58:59 -07:00
Girish Ramakrishnan 4e47d0595d Remove ACTION_BACKUP_CLEANUP_START 2019-09-27 09:43:40 -07:00
Girish Ramakrishnan 45e85e4d53 Set overwriteDns to be true when re-configuring 2019-09-26 22:30:58 -07:00
Girish Ramakrishnan a3420f885d Fix use of skipBackup
also, store it in the eventlog
2019-09-26 20:18:49 -07:00
Girish Ramakrishnan a266fe13d0 Remove skipNotification flag
we always want a update finish eventlog. Otherwise, the eventlog seems
strange since it says 'started updated' but didn't finish
2019-09-26 20:06:14 -07:00
Girish Ramakrishnan 44aba5d6e1 Add changes 2019-09-26 15:00:00 -07:00
Girish Ramakrishnan 3fe5307ae3 Migrate PROVIDER from cloudron.conf correctly 2019-09-26 14:19:25 -07:00
Girish Ramakrishnan d03fb0e71f Add separate flags for skipping backup and notification 2019-09-26 13:06:15 -07:00
Girish Ramakrishnan d9723b72e4 Replace Acme2Error with BoxError 2019-09-25 14:13:10 -07:00
Girish Ramakrishnan 6ba61f1bda Update changes 2019-09-25 10:30:54 -07:00
Girish Ramakrishnan d1df647ddd Another migration typo 2019-09-25 10:22:43 -07:00
Girish Ramakrishnan 95c4a1f90c Handle db migration failure 2019-09-25 10:17:02 -07:00
Girish Ramakrishnan e00325e694 typo 2019-09-25 10:06:48 -07:00
Girish Ramakrishnan 85c13cae58 Fix platform update logic 2019-09-24 21:21:49 -07:00
Girish Ramakrishnan 00fd9e5b7f add note that db upgrade code path is br0ken 2019-09-24 20:05:55 -07:00
Girish Ramakrishnan dde81ee847 lint 2019-09-24 19:50:24 -07:00
Girish Ramakrishnan c46fc96500 stash the taskId instead of args 2019-09-24 13:06:13 -07:00
Girish Ramakrishnan 1914a9a703 Capitalize the Denied to be in sync with boxerror 2019-09-24 00:58:49 -07:00
Girish Ramakrishnan 1a061e4446 Only check installationState to resume tasks
also, make resumeTasks go via app logic to capture end of task
2019-09-24 00:37:29 -07:00
Girish Ramakrishnan 29ce80cebe Use DOCKER_ERROR for docker errors 2019-09-23 23:27:32 -07:00
Girish Ramakrishnan 4b6ac538ac clear the timeout just to be safe 2019-09-23 18:17:05 -07:00
Girish Ramakrishnan 70b9000b0e Fix various app state issues
Tasks initiated by repair/uninstall can run from any state
Clear taskId for failed uninstall
Clone/install can only be run on new entries
2019-09-23 18:07:12 -07:00
Girish Ramakrishnan 24dcb1b79c typo 2019-09-23 15:31:09 -07:00
Johannes Zellner 384915883f Add namecheap token error changes 2019-09-23 23:49:43 +02:00
Johannes Zellner 4cfc75f1d1 Handle namecheap ACCESS_DENIED 2019-09-23 23:35:29 +02:00
Girish Ramakrishnan c49cbb524d Design the route for pre-flight 2019-09-23 14:34:29 -07:00
Girish Ramakrishnan b401c3d930 Make schedule task take the command as arg 2019-09-23 14:18:14 -07:00
Girish Ramakrishnan 890a7cfb37 runState is not used for task anymore 2019-09-23 13:49:38 -07:00
Girish Ramakrishnan 70a1ef1af3 Can call uninstall as long as no task is active 2019-09-23 13:32:52 -07:00
Girish Ramakrishnan 38a0cdc0be Make the repair re-issue the install command
Otherwise, repairing a repair has to be handled
2019-09-23 13:21:25 -07:00
Girish Ramakrishnan 93344a5a4a Use 424 for access denied 2019-09-23 13:10:16 -07:00
Girish Ramakrishnan 9f792fc04b Remove DockerError 2019-09-23 12:20:25 -07:00
Girish Ramakrishnan 7cb95faacb typo 2019-09-23 10:49:50 -07:00
Girish Ramakrishnan bf122f0f56 Allow uninstall in error state 2019-09-23 10:40:22 -07:00
Girish Ramakrishnan 78e9446a05 Refactor repair into separate function 2019-09-23 10:15:58 -07:00
Girish Ramakrishnan 138e1595fa make legacy error messages as JSON 2019-09-23 09:13:43 -07:00
Johannes Zellner 37b02ad36a oldConfig is only used for location configure now 2019-09-23 16:24:18 +02:00
Johannes Zellner 02f0055594 On repair oldConfig might not come as an argument 2019-09-23 16:18:44 +02:00
Girish Ramakrishnan ec1f0f9320 Do not do dataDir move in configure 2019-09-23 07:15:36 -07:00
Girish Ramakrishnan bfe6389f62 oldConfig is optional in configure 2019-09-23 07:13:51 -07:00
Johannes Zellner 30db3e8973 Fix typo in sql query 2019-09-23 15:01:20 +02:00
Johannes Zellner 5b67f2cf29 Add apt packages required for samba mounts
The linux-generic package is anyways important for ensuring extra
modules will be rebuilt during updates.
2019-09-23 12:21:37 +02:00
Girish Ramakrishnan a007b74b1c Do no reparse null 2019-09-22 23:08:07 -07:00
Girish Ramakrishnan a89482d4fa Send the error as a task argument
Note that if apptask dies, we will automatically restart the repair
task with the args.
2019-09-22 22:50:35 -07:00
Girish Ramakrishnan 0cd4f133aa Do not configure/restore errored apps automatically 2019-09-22 22:44:28 -07:00
Girish Ramakrishnan e5ba4ff973 Handle legacy app error message
For apps in errored state, from a previous version of Cloudron
2019-09-22 22:25:34 -07:00
Girish Ramakrishnan ce133b997d Make runState non-nullable 2019-09-22 22:07:47 -07:00
Girish Ramakrishnan 217632354f Allow repair in non-errored state 2019-09-22 21:56:07 -07:00
Girish Ramakrishnan 9841351190 Call configure for all the non-reinstall states 2019-09-22 09:38:36 -07:00
Girish Ramakrishnan f3341f4b7f Make start/stop just a installation code
the runState now just tracks if an app is stopped.
2019-09-22 01:01:52 -07:00
Girish Ramakrishnan ff1f448860 Fixup repair route
* Do not allow scheduling tasks in error state
* Only repair is allowed in error state
* Use the error object to track what to 'repair' (like the lastState)
* If uninstall failed, repair will do uninstall
* If move dir failed, repair will do move dir
2019-09-22 00:04:25 -07:00
Girish Ramakrishnan 37f28746fc Do not setup logrotate & collectd in update 2019-09-20 20:10:52 -07:00
Girish Ramakrishnan 9a22ba3af7 Add repair route
this is specifically for the case where some task failed and user
wants to get it back.
2019-09-19 23:13:44 -07:00
Girish Ramakrishnan 2942da78de Add TASK_ERROR reason code 2019-09-19 23:13:13 -07:00
Girish Ramakrishnan 89ff6be971 Make domain a field 2019-09-19 22:45:44 -07:00
Girish Ramakrishnan be0d7bcce1 Typo 2019-09-19 22:30:51 -07:00
Johannes Zellner 851b257678 Fix typo 2019-09-20 02:05:19 +02:00
Girish Ramakrishnan 579eacb644 Better pending state check 2019-09-19 16:42:49 -07:00
Girish Ramakrishnan f52c5b584e Fix crash when resuming stopped apps 2019-09-19 16:40:38 -07:00
Girish Ramakrishnan 8980c18deb Send back error when access denied 2019-09-19 15:24:21 -07:00
Girish Ramakrishnan b05a9ce064 Add API to get dns record 2019-09-19 15:10:27 -07:00
Girish Ramakrishnan 1974314c1f Add changes 2019-09-18 09:12:25 -07:00
Johannes Zellner 2bde023d4d Use a file based session store
This prevents login sessions to be lost after a box restart
2019-09-18 13:00:03 +02:00
Girish Ramakrishnan 3a10003246 libssl1 asking for restart prompt during install
https://unix.stackexchange.com/questions/146283/how-to-prevent-prompt-that-ask-to-restart-services-when-installing-libpq-dev
https://github.com/confluentinc/castle/pull/1
https://bugs.launchpad.net/ubuntu/+source/ansible/+bug/1833013
2019-09-17 14:43:11 -07:00
Girish Ramakrishnan 1b08710b7e Update haraka 2019-09-16 14:44:57 -07:00
Girish Ramakrishnan 101d09eeb3 Typo in event log 2019-09-16 13:01:35 -07:00
Girish Ramakrishnan 00f949f156 Add overwriteDns arg to install & clone
this is useful in e2e
2019-09-16 09:31:34 -07:00
Girish Ramakrishnan adbe46d369 print all the domains being registered 2019-09-16 09:22:43 -07:00
Girish Ramakrishnan 3198926cd6 return null for default dataDir 2019-09-15 22:06:03 -07:00
Girish Ramakrishnan 957a6a20fe mail: fix sieve + mail relay 2019-09-13 17:01:36 -07:00
Girish Ramakrishnan 94f75bb0d7 Update mail container for queue rework 2019-09-13 14:39:48 -07:00
Girish Ramakrishnan 0f442755e5 mail: add SRS support for mail forwarding
Fixes #637
2019-09-13 10:22:49 -07:00
Girish Ramakrishnan cd2e782d48 Make mail tests work 2019-09-12 13:59:31 -07:00
Girish Ramakrishnan e97606ca87 Remove internal sysadmin server
this is now unused
2019-09-12 13:33:01 -07:00
Girish Ramakrishnan 00ada80230 Add mail container changes 2019-09-11 15:03:57 -07:00
Girish Ramakrishnan 34db98c489 validate email in addList 2019-09-11 14:36:10 -07:00
Girish Ramakrishnan 110695355c Make mailing list members fully qualified
Part of #637
2019-09-11 12:51:57 -07:00
Girish Ramakrishnan 021fb4bb94 Add skysilk provider 2019-09-11 09:14:04 -07:00
Girish Ramakrishnan dea033e4b0 Fix comment 2019-09-10 15:43:16 -07:00
Girish Ramakrishnan 7dfe40739e Remove apps.getAppConfig 2019-09-10 15:41:35 -07:00
Girish Ramakrishnan 9f0d1b515c Add param to overwrite DNS 2019-09-10 15:41:32 -07:00
Girish Ramakrishnan 2691d46d50 migrate: only pass the old data dir 2019-09-10 15:15:20 -07:00
Girish Ramakrishnan 78c8f1de71 Add specific installation states to help out UI 2019-09-10 14:25:12 -07:00
Girish Ramakrishnan d27ee4bfbc More changes 2019-09-10 14:20:16 -07:00
Girish Ramakrishnan cc5daa428d Fix location change event log 2019-09-10 13:57:58 -07:00
Girish Ramakrishnan 3e2189aeed Remove obsolete configure route 2019-09-09 22:08:08 -07:00
Girish Ramakrishnan 79f9963792 Add robotsTxt tests 2019-09-09 21:52:01 -07:00
Girish Ramakrishnan 6f53723169 test data dir migration 2019-09-09 21:25:39 -07:00
Girish Ramakrishnan d8cb100fc0 Add mailbox test 2019-09-09 16:34:48 -07:00
Girish Ramakrishnan 5f9b2f1159 Add env and debug mode tests 2019-09-09 15:46:29 -07:00
Girish Ramakrishnan 801ca7eda1 Break down the configure route 2019-09-09 14:35:52 -07:00
Girish Ramakrishnan 45a2d3745c Fix app eventlog 2019-09-06 14:47:44 -07:00
Girish Ramakrishnan 551fe4d846 Use BoxError everywhere in apptask 2019-09-06 11:33:24 -07:00
Girish Ramakrishnan 791981c2f2 trim the provider 2019-09-06 10:24:19 -07:00
Girish Ramakrishnan a18a620847 Add BoxError.toPlainObject 2019-09-05 21:11:38 -07:00
Girish Ramakrishnan 99e63ffc3f Use BoxError for apptask errors 2019-09-05 18:13:53 -07:00
Girish Ramakrishnan e10a6d9de5 Bump graphite for buffer size fix 2019-09-05 14:33:32 -07:00
Girish Ramakrishnan 147f16571a Handle error in graphs code 2019-09-05 14:30:21 -07:00
Girish Ramakrishnan bd1fbc4a05 typo 2019-09-05 11:42:32 -07:00
Girish Ramakrishnan 0843f78ec8 Add tasks.setCompleted
this lets us easily grep the code on where the task is completed
2019-09-05 11:29:48 -07:00
Girish Ramakrishnan 9769fbfcf2 Better message 2019-09-05 11:22:29 -07:00
Girish Ramakrishnan 7e73197eb9 Trigger re-configure of apps for collectd config 2019-09-05 09:24:37 -07:00
Girish Ramakrishnan e3964fd710 Fix crash in setUpdateSuccess 2019-09-04 16:11:59 -07:00
Girish Ramakrishnan e66961b814 merge registerSubdomains and registerAlternateDomains
also, merge unregisterSubdomain and unregisterAlternateDomains
also, fix crash where app.oldConfig is used
2019-09-03 19:55:41 -07:00
Girish Ramakrishnan 4176e5a98e Add note in schema 2019-09-03 19:04:12 -07:00
Girish Ramakrishnan 45cf8a62d1 remove obsolete comment 2019-09-03 18:52:37 -07:00
Girish Ramakrishnan b1380819ba debug taskId 2019-09-03 16:06:28 -07:00
Girish Ramakrishnan 57fa457596 Typo in error handling 2019-09-03 15:55:57 -07:00
Girish Ramakrishnan de1e218ce9 Return BAD_FIELD if dataDir conflicts 2019-09-03 15:17:48 -07:00
Girish Ramakrishnan e117ee2bef Cleanup app error codes
1. The error classes (like AppsError) now take a 3rd argument details.
We can attach anything in this 3rd argument and this gets sent in the
REST response as well.

2. The HttpError class is now HttpError(statusCode, errorOrMessage). If
it's an error object, it will take the message and other things which
were attached above from it and send them across. Previously, we used to
mark this case an internal error all the time.

3. AppsError only has generic codes now. The UI code then simply checks
for additional information that we attached to show errors. For example,
BAD_FIELD will have a field: 'xx' indicating which field is at fault.
ALREADY_EXISTS has information on which domain or port caused a problem.
The advantage here is we can drop all these error codes that are
specific to each model code.

4. Maybe some day, we can remove all these error classes and have only
one generic class. AppsError right now is pretty generic already. We can
use that error code everywhere... No need to translate errors also
everywhere.

5. Finally, in the router code, I have this function toHttpError (in
apps.js) which is also so much cleaner than what we have now. We keep
writing the same stuff over and over.
2019-09-03 10:39:02 -07:00
Girish Ramakrishnan a9e101d9f4 Add note on why it is BAD_STATE 2019-09-02 13:55:43 -07:00
Girish Ramakrishnan a2f8203a42 Add location conflict error code 2019-09-02 12:42:28 -07:00
Girish Ramakrishnan b9ee127775 Send detail in apps error 2019-09-02 12:41:32 -07:00
Girish Ramakrishnan 6668bb3e8a Handle BAD_STATE as well 2019-09-02 12:17:48 -07:00
Girish Ramakrishnan 5fd129e509 send reason code as part of details 2019-09-01 21:22:46 -07:00
Girish Ramakrishnan d59c1f53b9 apps: add detail to http error messages 2019-09-01 18:35:06 -07:00
Girish Ramakrishnan d2f38c1abc Remove unused error code 2019-09-01 17:39:07 -07:00
Girish Ramakrishnan c0a1db6941 Send details as part of AppsError
the last mile module has been updated to pipe through additional properties.
2019-09-01 13:42:25 -07:00
Girish Ramakrishnan fc10b4a79b Update lastmile 2019-09-01 13:34:40 -07:00
Girish Ramakrishnan 9da2117e99 Re-enable configure/restore test 2019-08-30 16:12:35 -07:00
Girish Ramakrishnan 7e030b149b More changes 2019-08-30 15:51:50 -07:00
Girish Ramakrishnan bd23abd265 tasks: make error a json
also, handle case where we never got to handle task exit cleanly
2019-08-30 14:49:45 -07:00
Girish Ramakrishnan dd0fb8292c Move state enums to the model code 2019-08-30 13:21:51 -07:00
Girish Ramakrishnan b4cbf63519 Remove installationState contraint when settings health 2019-08-30 12:57:59 -07:00
Girish Ramakrishnan 4fd04fa349 Add proper error codes 2019-08-30 12:42:38 -07:00
Girish Ramakrishnan c22cdb8d81 Return error object in the API 2019-08-30 11:34:04 -07:00
Johannes Zellner eb963b2eb4 Add externalldap pagination 2019-08-30 20:26:09 +02:00
Girish Ramakrishnan 7d299908c9 Fix tests 2019-08-30 10:49:43 -07:00
Girish Ramakrishnan 2585282f86 errorMessage -> errorJson 2019-08-30 10:02:24 -07:00
Johannes Zellner f25d5b3304 Deliver the user account source in the profile api 2019-08-30 13:36:37 +02:00
Johannes Zellner 6e878faa8b Also sync fallbackEmail from ldap 2019-08-30 13:10:49 +02:00
Johannes Zellner 15a6cbe62b Make sure all password change input fields use the same validation pattern 2019-08-30 12:14:32 +02:00
Johannes Zellner 76b0b214ec Do not sync non-ldap users from ldap if usernames match
We might want to make that option in the future depending on use-cases
2019-08-30 10:20:04 +02:00
Johannes Zellner f5c643c960 Add some debugging logs when users are created or updated 2019-08-30 10:20:04 +02:00
Johannes Zellner ca8e0613fb Skip notifications for ldap syncer events 2019-08-30 10:20:04 +02:00
Johannes Zellner 0c9334d0d2 Ensure we wait for all user sync db actions to finish 2019-08-30 10:20:04 +02:00
Johannes Zellner 712dc97e9b Move the basic ldap argument validation 2019-08-30 10:20:04 +02:00
Johannes Zellner 4df48c97ec Ignore the bindDn user in the syncer 2019-08-30 10:20:04 +02:00
Johannes Zellner fe3ea53cda Ldap usually uses cn as displayName 2019-08-30 10:20:04 +02:00
Johannes Zellner d385c80882 Use external ldap bind for users from ldap source 2019-08-30 10:20:04 +02:00
Johannes Zellner b823213c94 Create and update users from external ldap 2019-08-30 10:20:04 +02:00
Johannes Zellner 4b86311ab9 Add user source property to schema 2019-08-30 10:20:04 +02:00
Johannes Zellner b9efa8f445 Use tasks api for external ldap syncer 2019-08-30 10:20:04 +02:00
Johannes Zellner f8db12346d Perform some basic static input validation for external ldap 2019-08-30 10:20:04 +02:00
Johannes Zellner 4d3948f81f Improve external ldap error reporting 2019-08-30 10:20:04 +02:00
Johannes Zellner 5431d50206 Also check errors when no bindDn is provided 2019-08-30 10:20:04 +02:00
Johannes Zellner 6db078c26a Handle externalldap errors correctly in settings route 2019-08-30 10:20:04 +02:00
Johannes Zellner f61e9c7f27 Catch basic protocol errors 2019-08-30 10:20:04 +02:00
Johannes Zellner 567d92ce00 Add external ldap enabled boolean flag 2019-08-30 10:20:04 +02:00
Johannes Zellner 7a6d26c5da Add settings route handler for external ldap configs 2019-08-30 10:20:04 +02:00
Johannes Zellner 046ac85177 Add initial externalldap code to validate ldap configs 2019-08-30 10:20:04 +02:00
Girish Ramakrishnan f0fd088247 Pick values from updateConfig 2019-08-29 20:50:45 -07:00
Girish Ramakrishnan 5ec0d1e691 Add to changes 2019-08-29 15:10:55 -07:00
Girish Ramakrishnan 9391a934c3 Do not update on uninstall 2019-08-29 14:38:42 -07:00
Girish Ramakrishnan bb62e6a318 clear taskId in the parent process 2019-08-29 13:43:45 -07:00
Girish Ramakrishnan 0da6539c48 Add progressCallback to run commands 2019-08-29 13:41:11 -07:00
Girish Ramakrishnan 9cf833dab2 Use taskId instead of states to check bad state
a) this is because, we have install state and run state.
b) we have to put taskId as part of the transaction to prevent race
2019-08-29 13:15:40 -07:00
Girish Ramakrishnan ed57260fcf add note on why it is a state 2019-08-29 11:07:19 -07:00
Girish Ramakrishnan c98f625c4c Make force update as task arg 2019-08-29 10:59:05 -07:00
Girish Ramakrishnan f3008064e4 Fix installation states
App operations can only be done in 'installed' or 'error' state.
If some other operation is in progress, you have to cancel it first.

This guarantees that the old app command got killed.
2019-08-29 10:14:23 -07:00
Girish Ramakrishnan 1faee00764 Better progress text when waiting for other tasks
Fixes #630
2019-08-28 22:13:50 -07:00
Girish Ramakrishnan a40505e2ee Remove pause flag, we already have platform lock 2019-08-28 22:13:50 -07:00
Girish Ramakrishnan 484202b4c6 better variable name 2019-08-28 21:31:42 -07:00
Girish Ramakrishnan 6a7fc17c60 Make restore/configure use scheduleTask 2019-08-28 15:36:50 -07:00
Girish Ramakrishnan 05d3897ae2 Make apps test work again 2019-08-28 15:30:23 -07:00
Girish Ramakrishnan 9f1210202a port taskmanager to use tasks 2019-08-28 15:17:53 -07:00
Girish Ramakrishnan be6b172d6f Remove app task eventlog 2019-08-28 13:24:05 -07:00
Girish Ramakrishnan fef9e0a5c1 Handle app task crashes 2019-08-28 13:19:47 -07:00
Girish Ramakrishnan b84b033bf3 typo 2019-08-28 12:51:00 -07:00
Girish Ramakrishnan b30ff1f55a rework task API to be two-phase
this lets us avoid this EE based API. we now add and then start
explicitly.
2019-08-28 10:39:40 -07:00
Girish Ramakrishnan c6be0b290b updateConfig is no more 2019-08-27 22:03:43 -07:00
Girish Ramakrishnan 33cfd7a629 Add 'success' virtual field to the tasks 2019-08-27 21:36:52 -07:00
Girish Ramakrishnan 5952a5c69d Send taskId in the response 2019-08-27 21:35:40 -07:00
Girish Ramakrishnan 20de563925 rename installationProgress to errorMessage 2019-08-27 20:08:35 -07:00
Girish Ramakrishnan 7da80b4c62 Ensure log directory 2019-08-27 16:36:19 -07:00
Girish Ramakrishnan 15d765be6d Comment out couple of tests 2019-08-27 16:36:19 -07:00
Girish Ramakrishnan bfe2f116a7 Make restoreConfigJson, oldConfigJson, updateConfigJson as task args 2019-08-27 16:36:15 -07:00
Girish Ramakrishnan f535b3de2f Add logFile option to startTask 2019-08-27 15:26:26 -07:00
Girish Ramakrishnan e560c18b57 apptask is not a separate process anymore 2019-08-27 12:31:59 -07:00
Girish Ramakrishnan aecb99b6a3 Use task API in run commands 2019-08-27 12:31:59 -07:00
Girish Ramakrishnan 7da17f8190 Use task API in app backup 2019-08-27 12:31:59 -07:00
Girish Ramakrishnan 1964270a4f Use task API in app update 2019-08-27 12:31:59 -07:00
Girish Ramakrishnan f45b61d95c Use task API for app restore 2019-08-27 12:31:59 -07:00
Girish Ramakrishnan ff11c38169 Use task API for app clone 2019-08-27 12:31:59 -07:00
Girish Ramakrishnan 3e67067431 Use task API for app uninstall 2019-08-27 12:31:59 -07:00
Girish Ramakrishnan 824f00d1e8 Use task API for app configure 2019-08-27 12:31:59 -07:00
Girish Ramakrishnan 96d19f59a4 Use task API for app install 2019-08-27 12:31:59 -07:00
Girish Ramakrishnan 42c6fe50d2 Make progressCallback take an optional callback 2019-08-27 12:31:59 -07:00
Girish Ramakrishnan 9242f7095a Migrate apptask to use tasks framework 2019-08-27 12:31:59 -07:00
Girish Ramakrishnan 99c9fbc38f add taskId to appdb 2019-08-27 12:31:55 -07:00
Girish Ramakrishnan 0d31207ad7 add taskId to tasks table 2019-08-26 14:27:29 -07:00
Girish Ramakrishnan 8af7dbc35a group -> list 2019-08-23 16:36:19 -07:00
Johannes Zellner d0a373cb15 Refresh dns records for dynamic dns setting every hour
Often home IPs change at the full hour, so we give it 5min to settle
2019-08-23 16:20:26 +02:00
Johannes Zellner 3dc87bbca8 Allow embedding pages from same origin 2019-08-22 11:37:54 +02:00
Girish Ramakrishnan a55c399585 cloudron-support: Use the PROVIDER file 2019-08-21 21:23:22 -07:00
Girish Ramakrishnan f74aa24dd2 cloudron-setup: make it work for old installs
need this for the auto-update test
2019-08-21 21:18:08 -07:00
Girish Ramakrishnan 1aa7eb4478 Collect and aggregate du information twice a day 2019-08-21 13:45:52 -07:00
Girish Ramakrishnan 0c7002ba59 settings.adminOrigin has to be loaded after setAdmin
the dangers of caching
2019-08-21 13:26:15 -07:00
Girish Ramakrishnan fd6dd1ea18 Add timestamp to the logs 2019-08-21 10:16:57 -07:00
Girish Ramakrishnan aa74d5cd82 Add a note 2019-08-20 19:47:24 -07:00
Girish Ramakrishnan 8fc10a0bdd Add note 2019-08-20 15:26:00 -07:00
Girish Ramakrishnan 809ed0f0dc clear db to stop the scheduler 2019-08-20 13:42:03 -07:00
Girish Ramakrishnan b8a4e1c4a3 Use docker for apps-test 2019-08-20 13:34:18 -07:00
Girish Ramakrishnan d9e45f732b Fix error message 2019-08-20 13:22:43 -07:00
Girish Ramakrishnan ca025b36f7 Define DockerError.EXTERNAL_ERROR 2019-08-20 13:11:06 -07:00
Girish Ramakrishnan bfb719d35e Remove use of dockerProxy 2019-08-20 11:50:54 -07:00
Girish Ramakrishnan 2a1b61107f Make the ldap test work 2019-08-20 11:45:00 -07:00
Johannes Zellner 969cee7c90 Rebuilding docker container takes a long time, so callback early
Otherwise the service restart request will just time out in the
dashboard
2019-08-20 12:12:24 +02:00
Johannes Zellner 7a3f579d3e Do not crash if a service without active docker container needs restart 2019-08-20 12:06:49 +02:00
Johannes Zellner 288d5efa88 Return DockerError instead of generic Error 2019-08-20 12:06:22 +02:00
Johannes Zellner 7be821963c Ensure we report stopped status for addons without a running docker container 2019-08-20 11:27:35 +02:00
Girish Ramakrishnan a236f8992a graphite: Fixup healthcheck url 2019-08-19 22:27:53 -07:00
Girish Ramakrishnan a5c2257f39 Update changelog 2019-08-19 19:10:30 -07:00
Girish Ramakrishnan 9d3b4ba816 store docker df output as well 2019-08-19 16:15:31 -07:00
Girish Ramakrishnan 43bf0767f1 remove docker proxy from tests
this is too complicated and also makes it hard to keep up with upstream API
2019-08-19 14:35:23 -07:00
Girish Ramakrishnan b301e5b151 Add dockerDataDisk to disks response 2019-08-19 14:14:13 -07:00
Girish Ramakrishnan 2b484c0382 collect maildata size separately 2019-08-19 13:23:31 -07:00
Johannes Zellner f40ab4e2d5 Use git+https for cloudron-io/df 2019-08-19 09:19:06 +02:00
Girish Ramakrishnan c0a27380e9 Add to changes 2019-08-18 21:59:20 -07:00
Girish Ramakrishnan 0d7a3f43c4 Collect du information 2019-08-18 21:52:41 -07:00
Girish Ramakrishnan 8195e439f3 Return all disks
We now return the disk information per app as well
2019-08-16 10:29:56 -07:00
Johannes Zellner b5edbf716c Add interox provider option 2019-08-14 14:47:08 +02:00
Johannes Zellner 466265fde1 Allow iframe embedding of cloudron.io pages 2019-08-14 14:44:12 +02:00
Girish Ramakrishnan 40033e09cd Check disk space before create app/box backups
Fixes #642
2019-08-13 10:55:02 -07:00
Johannes Zellner 573663412c Add more fuzzy user search
Fixes #646
2019-08-13 15:23:26 +02:00
Johannes Zellner 17599417f7 WIP 2019-08-13 15:16:17 +02:00
Girish Ramakrishnan 0ece6d8b0e Add dataDir to schema 2019-08-12 22:16:45 -07:00
Girish Ramakrishnan e0ac0393fe typo 2019-08-12 21:47:22 -07:00
Girish Ramakrishnan 6d38b3255c Check available disk space before update
Part of #642
2019-08-12 21:09:22 -07:00
Girish Ramakrishnan 477ff424d6 Check if we have enough disk space for docker
Part of #642
2019-08-12 20:47:53 -07:00
Girish Ramakrishnan a843104348 sftp: typo 2019-08-12 11:31:59 -07:00
Girish Ramakrishnan 0f4bc0981a graphs: fix render forwarding 2019-08-12 11:01:12 -07:00
Girish Ramakrishnan 07f6351465 Make graphite dashboard appear again 2019-08-11 22:45:11 -07:00
Girish Ramakrishnan 1b26e86365 Fix test 2019-08-10 09:37:42 -07:00
Girish Ramakrishnan 94b4bf94c0 Merge active flag into update route 2019-08-08 08:17:08 -07:00
Girish Ramakrishnan d5de05b633 Send user active flag 2019-08-08 07:19:50 -07:00
Girish Ramakrishnan 0ab6cad048 Add user enable/disable flag 2019-08-08 06:31:46 -07:00
Girish Ramakrishnan 9833ad548b Better progress message 2019-08-07 06:23:28 -07:00
Girish Ramakrishnan aa1ba3b226 Make apps-test pass 2019-08-06 10:27:19 -07:00
Girish Ramakrishnan 3774d4de28 Use API for pullImage
This allows to get proper error code

This mostly reverts commit 734286ba2e.
2019-08-06 09:46:09 -07:00
Girish Ramakrishnan e4961726bc Try to make apps-test work 2019-08-05 16:16:56 -07:00
Girish Ramakrishnan 77cf7d0da6 Bump test version 2019-08-05 06:39:16 -07:00
Girish Ramakrishnan a993e0b228 Add fullstop 2019-08-04 15:35:42 -07:00
Girish Ramakrishnan 43671a9fd6 Clear update task progress after update
Fixes #635
2019-08-04 10:21:42 -07:00
Girish Ramakrishnan 49cfd1e9b7 Add notification for box update
Fixes #634
2019-08-04 05:44:04 -07:00
Girish Ramakrishnan 58d4a4f54f quoting and fullstop 2019-08-03 10:36:38 -07:00
Girish Ramakrishnan e4e328ba6a Make user event titles better 2019-08-03 10:17:07 -07:00
Girish Ramakrishnan fd6bc955ff Remove extra line 2019-08-03 09:41:16 -07:00
Girish Ramakrishnan 511a18e0ed Display app changelog and version
part of #634
2019-08-03 09:22:13 -07:00
Girish Ramakrishnan e29d224a92 Be a bit more specific 2019-07-31 15:45:25 -07:00
Girish Ramakrishnan bb48ffb01f Fixup UA for easier detection (other than IP) 2019-07-31 15:43:15 -07:00
Girish Ramakrishnan 31fd3411f7 Add to changes 2019-07-30 15:41:03 -07:00
Girish Ramakrishnan a737d2675e Fix logrotation rules
* explicitly specify the dirs that are getting rotated
* app log rules are now moved to logrotate.ejs
* we keep task logs for a week

Some testing notes:
* touch -d "10 days ago" foo
* logrotate /etc/logrotate.conf -v to test rotation. there is a state
file created in /var/lib/logrotate/status. If we have a 'daily' rule,
it will get processed only after a log line in status exists and it's atleast
1 day old timestamp.

https://github.com/logrotate/logrotate/blob/master/logrotate.c is quite
readable
2019-07-30 15:37:15 -07:00
Girish Ramakrishnan fd462659cd tmp cleaner: only remove files and not directories
some apps like rocket.chat create directories in tmp and removing those
directories causes problems (for example, uploading)
2019-07-30 14:06:04 -07:00
Johannes Zellner cb10d0d465 Add time4vps provider 2019-07-29 20:54:41 +02:00
Girish Ramakrishnan 61f1c4884c Refactor logic so that settings.adminDomain is read in the closure 2019-07-27 19:21:49 -07:00
Girish Ramakrishnan 2cd00de6e3 initCache after every restore 2019-07-27 19:09:09 -07:00
Girish Ramakrishnan d3c5d53eae silence mysql warning 2019-07-26 22:35:44 -07:00
Girish Ramakrishnan 6dfafae342 move the comment 2019-07-26 22:19:14 -07:00
Girish Ramakrishnan 2f861c3309 specify the database 2019-07-26 22:12:40 -07:00
Girish Ramakrishnan af388f0f16 IP based restore 2019-07-26 21:37:33 -07:00
Girish Ramakrishnan c36cc86c5f init cache in various out of process workers 2019-07-26 19:38:42 -07:00
Girish Ramakrishnan 02f195b25c typo 2019-07-26 15:02:03 -07:00
Girish Ramakrishnan 18623fd9b7 cloudron.conf can be removed post migration 2019-07-26 14:55:36 -07:00
Girish Ramakrishnan 9b74bb73aa config.js is dead, long live config.js
we use settings now
2019-07-26 14:51:51 -07:00
Girish Ramakrishnan ee9636b496 move use of TEST and CLOUDRON to constants 2019-07-26 10:13:20 -07:00
Girish Ramakrishnan 5c2cbd7840 Move config.baseDir to paths 2019-07-26 10:07:08 -07:00
Girish Ramakrishnan 7fbac6cc17 typo 2019-07-26 08:44:37 -07:00
Girish Ramakrishnan 9e7e9d66bf move provider into sysinfo
this is ideally "auto-detectable" runtime information
2019-07-26 07:33:22 -07:00
Girish Ramakrishnan 7fe66aa7fa Remove unused settings.get 2019-07-25 16:31:02 -07:00
Girish Ramakrishnan 2dda0efe83 Move config.database to db code itself 2019-07-25 16:12:42 -07:00
Girish Ramakrishnan 59620ca473 config.get is dead 2019-07-25 16:08:54 -07:00
Girish Ramakrishnan 12eae1eff2 Make port a constant 2019-07-25 16:08:54 -07:00
Girish Ramakrishnan b03bf87b7d remove unused function 2019-07-25 16:08:54 -07:00
Girish Ramakrishnan c32718b164 Make ldap and docker proxy port as constants 2019-07-25 16:08:54 -07:00
Girish Ramakrishnan a6ea12fedc Make internal smtp port a constant 2019-07-25 16:08:54 -07:00
Girish Ramakrishnan 2d260eb0d5 Make sysadminPort a constant 2019-07-25 16:08:51 -07:00
Girish Ramakrishnan d7dd069ae0 Use constants.version instead of config.version 2019-07-25 15:02:14 -07:00
Girish Ramakrishnan 6a77a58489 Move hasIPv6 into sysinfo 2019-07-25 14:35:08 -07:00
Girish Ramakrishnan c30ac5f927 Add setting key names 2019-07-25 14:35:04 -07:00
Girish Ramakrishnan 437f7ef890 Migrate cloudron.conf into db 2019-07-25 14:34:16 -07:00
Girish Ramakrishnan 1f7347e8de Make custom.yml as part of the backup 2019-07-25 10:28:42 -07:00
Girish Ramakrishnan 96f59d7cfe config: edition is long gone 2019-07-24 22:32:16 -07:00
Girish Ramakrishnan d55f65c7c9 Better error message 2019-07-24 22:11:22 -07:00
Girish Ramakrishnan 9a0d5b918f totp: set window to 2
see https://github.com/speakeasyjs/speakeasy#specifying-a-window-for-verifying-hotp-and-totp

A TOTP is incremented every step time-step seconds. By default, the time-step is
30 seconds. Window of 2 means, +- 2 steps.

Fixes #633
2019-07-23 14:45:54 -07:00
Girish Ramakrishnan 3553fbc7b6 Add wasabi storage backend 2019-07-22 16:44:56 -07:00
Girish Ramakrishnan 55d53f13d9 Improve error message 2019-07-18 10:28:37 -07:00
Johannes Zellner 27369a650c Fix disk full docs link 2019-07-16 15:10:56 +02:00
Girish Ramakrishnan 913f0d5d97 Update changes file 2019-07-15 10:50:14 -07:00
Girish Ramakrishnan ada63ec697 Add app.adminEmail 2019-07-12 14:29:35 -07:00
Girish Ramakrishnan 117f06e971 Fix issue where tar backups with files > 8GB was corrupt
Fixes #640
2019-07-10 14:58:54 -07:00
Johannes Zellner 9f03a9a6e2 Do first a patch release 2019-07-03 19:14:00 +02:00
Johannes Zellner ce406c7088 Add groupCount to backendSettings 2019-07-03 13:47:23 +02:00
Girish Ramakrishnan e7127df30d remove app ownerId
this is unused
2019-07-02 21:23:51 -07:00
Girish Ramakrishnan 10e2817257 Add 4.2 changes 2019-07-02 19:24:49 -07:00
Girish Ramakrishnan 337a47c62b typo 2019-07-02 16:46:14 -07:00
Girish Ramakrishnan 14bdac20ef cloudron-support: add --admin-login 2019-07-02 16:37:46 -07:00
Girish Ramakrishnan 88e2b3f9aa Add userCount to backend settings 2019-07-01 23:26:43 -07:00
Girish Ramakrishnan 22d731f06d Fix LDAP not accessible via scheduler containers
Check the IP address against scheduler containers as well
2019-06-27 16:12:09 -07:00
Girish Ramakrishnan e3d288ef7d Add MONGODB_OPLOG_URL for apps that require oplog access
remove the replicaSet arg (it causes problems in tests but not in apps).
it causes some issues because of hostname not being set properly/docker network.
this only prevents the client from using replicaSet features which doesn't apply
to us since it is single instance.
2019-06-27 13:19:59 -07:00
Girish Ramakrishnan 455f597543 Add changes 2019-06-26 21:40:03 -07:00
Girish Ramakrishnan 8c9e626920 Remove twitter and chat link from the login footer 2019-06-26 21:39:07 -07:00
Girish Ramakrishnan 5a000c1ff4 Add MONGODB_REPLICA_SET for mongodb addon
This can be useful for constructing the ?replSet= part of the URI.
replicaSet is used by the client to discover the secondaries and fallback
automatically. if not provided, they just talk to primary.
2019-06-26 21:29:43 -07:00
Girish Ramakrishnan ddf634bfb2 o2 has stopped working 2019-06-26 18:40:07 -07:00
Girish Ramakrishnan 89d3b8cc6a Make hostname more explicit 2019-06-26 14:21:47 -07:00
Girish Ramakrishnan 49af6d09a2 CLOUDRON_APP_HOSTNAME should be the app id always
name is the container name which is "unique"
2019-06-26 14:21:43 -07:00
Girish Ramakrishnan e5b0cac284 Clarify comment 2019-06-26 14:13:26 -07:00
Girish Ramakrishnan 6f33900f85 Fix failing test 2019-06-21 15:05:28 -07:00
Girish Ramakrishnan 514823af7d More changes 2019-06-21 13:34:24 -07:00
Girish Ramakrishnan 65b058f563 More changes 2019-06-21 11:12:25 -07:00
Girish Ramakrishnan 7c8560deff Ensure redis addon vars are replaced with manifest v2 2019-06-20 23:43:18 -07:00
Girish Ramakrishnan 6bbe2613b4 Return 412 for bad password 2019-06-20 16:44:53 -07:00
Girish Ramakrishnan 5771478e4b Use 412 for invalid token, otherwise user gets logged out 2019-06-20 16:37:16 -07:00
Girish Ramakrishnan e13030bc89 fontawesome location has changed 2019-06-20 16:27:27 -07:00
Girish Ramakrishnan 0a0ac93a55 Use pattern match instead for handling v1 to v2 upgrades 2019-06-20 11:59:02 -07:00
Girish Ramakrishnan 214fb50e74 Add 4.1.5 changes 2019-06-20 11:49:39 -07:00
Girish Ramakrishnan 959f8ee31e Ensure passwords are preserved with v2 manifest 2019-06-20 11:46:50 -07:00
Girish Ramakrishnan cb0d75be37 Add changes 2019-06-19 09:19:05 -07:00
Girish Ramakrishnan 11353e9e3a DO DNS API break means this value must atleast be 30
(cherry picked from commit c0c5561aac)
2019-06-17 20:13:32 -07:00
Girish Ramakrishnan 8cd5c15c2b Fix mail auth with manifest v2 2019-06-17 11:13:59 -07:00
Girish Ramakrishnan b86b8b8ee1 4.1.4 changes
(cherry picked from commit 73a44d1fb2)
2019-06-16 17:59:18 -07:00
Girish Ramakrishnan c5f6e6b028 Prefix mysql url/database variables 2019-06-15 10:06:51 -07:00
Girish Ramakrishnan 592d8abc58 Roll back async package
something is broken, not sure what
2019-06-14 16:24:41 -07:00
Girish Ramakrishnan d93068fc62 Update package lock 2019-06-14 15:32:11 -07:00
Girish Ramakrishnan a864af52df Update packages 2019-06-14 15:31:13 -07:00
206 changed files with 10466 additions and 7657 deletions
+135
View File
@@ -1631,3 +1631,138 @@
[4.1.4]
* Add CLOUDRON_ prefix to MySQL addon variables
[4.1.5]
* Make the terminal addon button inject variables based on manifest version
* Preserve addon passwords correctly when using v2 manifest
* Show error message instead of logging out user when invalid 2FA token is provided
* Ensure redis vars are renamed with manifest v2
* Add missing Scaleway Object Storage to restore UI
* Fix Exoscale endpoints in restore UI
* Reset the app icon when showing the configure UI
[4.1.6]
* Fix issue where CLOUDRON_APP_HOSTNAME was incorrectly set
* Remove chat link from the footer of login screen
* Add support for oplog tailing in mongodb
* Fix LDAP not accessible via scheduler containers
[4.1.7]
* Fix issue where login looped when admin bit was removed
[4.2.0]
* Fix issue where tar backups with files > 8GB was corrupt
* Add SparkPost as mail relay backend
* Add Wasabi storage backend
* TOTP tokens are now checked for with +- 60 seconds
* IP based restore
* Fix issue where task logs were not getting rotated correctly
* Add notification for box update
* User enable/disable flag
* Check disk space before various operations like install, update, backup etc
* Collect per app du information
* Set Cloudron specific UA for healthchecks
* Show message why an app task is 'pending'
* Rework app task system so that we can now pass dynamic arguments
* Add external LDAP server integration
[4.2.1]
* Rework the app configuration routes & UI
* Fine grained eventlog for app configuration
* Update Haraka to 2.8.24
* Set sieve_max_redirects to 64
* SRS support for mail forwarding
* Fix issue where sieve responses were not sent via the relay
* File based session store
* Fix API token error reporting for namecheap backend
[4.2.2]
* Fix typos in migration
[4.2.3]
* Remove flicker of custom icon
* Preserve PROVIDER setting from cloudron.conf
* Add Skip backup option when updating an app
* Fix bug where nginx was not reloaded on cert renewal
[4.2.4]
* Fix demo settings state regression
[4.2.5]
* Fix the demo settins fix
[4.2.6]
* Fix configuration of empty app location (subdomain)
[4.2.7]
* Fix issue where the icon for normal users was displayed incorrectly
* Kill stuck backup processes after 12 hours and notify admins
* Reconfigure email apps when mail domain is added/removed
* Fix crash when only udp ports are defined
[4.3.0]
* Add timeout to kill long running tasks in case they get stuck
* email: Auto-subscribe to Spam folder
* Allow setting a custom CSP policy
* ticket: when email is down, add a field to provide alternate contact email
* Re-work app import flow
* Add pagination and search to mailbox and mail alias listing
* Add UI and workflow to add a private registry
* Show external LDAP connector
* Network view: Allow IP address detection to be configurable
* Add support for custom docker registry
* Resolve any lists and aliases in a mailing list
* Rename Accounts view to Profile
* Add search for groups and user association UI
[4.3.1]
* Make logout from all button logout from all sessions
* List unstable apps by default
* Fix crash when listing mailboxes
[4.3.2]
* Update manifestformat module
[4.3.3]
* Fix bug where stopped containers got started on server restart
* Fix external LDAP UI and syncing
* Fix timeout being too low in docker proxy
* Make manifest.id optional for custom apps
* Fix registry detection in private images
* Make mailbox domain configurable for apps
[4.3.4]
* Do not error if fallback certs went missing
* Add 'New Apps' section to Appstore view
* Fix issue where graphs of some apps were not appearing
[4.4.0]
* Show swap in graphs
* Make avatars customizable
* Hide access tokens from logs
* Add missing '@' sign for email address in app mailbox
* Add app fqdn to backup progress message
* import: add option to import app in-place
* import: add option to import app from arbitrary backup config
* Show download progress for rsync backups
* Fix various repair workflows
* acme2: Implement post-as-get
[4.4.1]
* ami: fix AWS provider validation
[4.4.2]
* Fix crash when reporting that DKIM is not setup correctly
* Stopped apps cannot be updated or auto-updated
* eventlog: track support ticket creation and remote support status
[4.4.3]
* Add restart button in recovery section
* Fix issue where memory usage was not computed correctly
* cloudflare: support API tokens
[4.4.4]
* Fix bug where restart button in terminal was not working
* Add search field in apps view
* Make app view tags and domain filter persistent
* Add timezone UI
+1 -1
View File
@@ -1,5 +1,5 @@
The Cloudron Subscription license
Copyright (c) 2019 Cloudron UG
Copyright (c) 2020 Cloudron UG
With regard to the Cloudron Software:
+9
View File
@@ -33,6 +33,7 @@ gpg_package=$([[ "${ubuntu_version}" == "16.04" ]] && echo "gnupg" || echo "gpg"
apt-get -y install \
acl \
build-essential \
cifs-utils \
cron \
curl \
debconf-utils \
@@ -40,6 +41,7 @@ apt-get -y install \
$gpg_package \
iptables \
libpython2.7 \
linux-generic \
logrotate \
mysql-server-5.7 \
nginx-full \
@@ -121,6 +123,13 @@ timedatectl set-ntp 1
# mysql follows the system timezone
timedatectl set-timezone UTC
echo "==> Adding sshd configuration warning"
sed -e '/Port 22/ i # NOTE: Cloudron only supports moving SSH to port 202. See https://cloudron.io/documentation/security/#securing-ssh-access' -i /etc/ssh/sshd_config
# https://bugs.launchpad.net/ubuntu/+source/base-files/+bug/1701068
echo "==> Disabling motd news"
sed -i 's/^ENABLED=.*/ENABLED=0/' /etc/default/motd-news
# Disable bind for good measure (on online.net, kimsufi servers these are pre-installed and conflicts with unbound)
systemctl stop bind9 || true
systemctl disable bind9 || true
+3 -14
View File
@@ -14,25 +14,14 @@
require('supererror')({ splatchError: true });
let async = require('async'),
config = require('./src/config.js'),
ldap = require('./src/ldap.js'),
constants = require('./src/constants.js'),
dockerProxy = require('./src/dockerproxy.js'),
ldap = require('./src/ldap.js'),
server = require('./src/server.js');
console.log();
console.log('==========================================');
console.log(' Cloudron will use the following settings ');
console.log('==========================================');
console.log();
console.log(' Environment: ', config.CLOUDRON ? 'CLOUDRON' : 'TEST');
console.log(' Version: ', config.version());
console.log(' Admin Origin: ', config.adminOrigin());
console.log(' Appstore API server origin: ', config.apiServerOrigin());
console.log(' Appstore Web server origin: ', config.webServerOrigin());
console.log(' SysAdmin Port: ', config.get('sysadminPort'));
console.log(' LDAP Server Port: ', config.get('ldapPort'));
console.log(' Docker Proxy Port: ', config.get('dockerProxyPort'));
console.log();
console.log(` Cloudron ${constants.VERSION} `);
console.log('==========================================');
console.log();
@@ -1,12 +1,6 @@
'use strict';
var async = require('async'),
crypto = require('crypto'),
fs = require('fs'),
os = require('os'),
path = require('path'),
safe = require('safetydance'),
tldjs = require('tldjs');
var async = require('async');
exports.up = function(db, callback) {
db.all('SELECT * FROM apps, subdomains WHERE apps.id=subdomains.appId AND type="primary"', function (error, apps) {
@@ -0,0 +1,14 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE apps DROP FOREIGN KEY apps_owner_constraint'),
db.runSql.bind(db, 'ALTER TABLE apps DROP COLUMN ownerId')
], callback);
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,29 @@
'use strict';
var async = require('async'),
fs = require('fs');
exports.up = function(db, callback) {
if (!fs.existsSync('/etc/cloudron/cloudron.conf')) {
console.log('Unable to locate cloudron.conf');
return callback();
}
const config = JSON.parse(fs.readFileSync('/etc/cloudron/cloudron.conf', 'utf8'));
async.series([
fs.writeFile.bind(null, '/etc/cloudron/PROVIDER', config.provider, 'utf8'),
db.runSql.bind(db, 'START TRANSACTION;'),
// we use replace instead of insert because the cloudron-setup adds api/web_server_origin even for legacy setups
db.runSql.bind(db, 'REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'api_server_origin', config.apiServerOrigin ]),
db.runSql.bind(db, 'REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'web_server_origin', config.webServerOrigin ]),
db.runSql.bind(db, 'REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'admin_domain', config.adminDomain ]),
db.runSql.bind(db, 'REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'admin_fqdn', config.adminFqdn ]),
db.runSql.bind(db, 'REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'demo', config.isDemo ]),
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,17 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN active BOOLEAN DEFAULT 1', function (error) {
if (error) return callback(error);
callback();
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN active', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,17 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE apps ADD COLUMN taskId INTEGER'),
db.runSql.bind(db, 'ALTER TABLE apps ADD CONSTRAINT apps_task_constraint FOREIGN KEY(taskId) REFERENCES tasks(id)')
], callback);
};
exports.down = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE app DROP FOREIGN KEY apps_task_constraint'),
db.runSql.bind(db, 'ALTER TABLE apps DROP COLUMN taskId'),
], callback);
};
@@ -0,0 +1,12 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps DROP updateConfigJson, DROP restoreConfigJson, DROP oldConfigJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps CHANGE installationProgress errorJson TEXT', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps CHANGE errorJson installationProgress TEXT', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,17 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN source VARCHAR(128) DEFAULT ""', function (error) {
if (error) return callback(error);
callback();
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN source', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,26 @@
'use strict';
let async = require('async');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE tasks CHANGE errorMessage errorJson TEXT', [], function (error) {
if (error) console.error(error);
// convert error messages into json
db.all('SELECT id, errorJson FROM apps', function (error, apps) {
async.eachSeries(apps, function (app, iteratorDone) {
if (app.errorJson === 'null') return iteratorDone();
if (app.errorJson === null) return iteratorDone();
db.runSql('UPDATE apps SET errorJson = ? WHERE id = ?', [ JSON.stringify({ message: app.errorJson }), app.id ], iteratorDone);
}, callback);
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE tasks CHANGE errorJson errorMessage TEXT', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,21 @@
'use strict';
var async = require('async');
// imports mailbox entries for existing users
exports.up = function(db, callback) {
db.all('SELECT * FROM mailboxes', function (error, mailboxes) {
async.eachSeries(mailboxes, function (mailbox, iteratorDone) {
if (!mailbox.membersJson) return iteratorDone();
let members = JSON.parse(mailbox.membersJson);
members = members.map((m) => m && m.indexOf('@') === -1 ? `${m}@${mailbox.domain}` : m); // only because we don't do things in a xction
db.runSql('UPDATE mailboxes SET membersJson=? WHERE name=? AND domain=?', [ JSON.stringify(members), mailbox.name, mailbox.domain ], iteratorDone);
}, callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,19 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('UPDATE apps SET runState=? WHERE runState IS NULL', [ 'running' ], function (error) {
if (error) return callback(error);
db.runSql('ALTER TABLE apps MODIFY runState VARCHAR(512) NOT NULL', [], function (error) {
if (error) console.error(error);
callback(error);
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE app MODIFY runState VARCHAR(512)', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,10 @@
'use strict';
exports.up = function(db, callback) {
// We clear all demo state in the Cloudron...the demo cloudron needs manual intervention afterwards
db.runSql('REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'demo', '' ], callback);
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,30 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN reverseProxyConfigJson TEXT', function (error) {
if (error) return callback(error);
db.all('SELECT id, robotsTxt FROM apps', function (error, apps) {
if (error) return callback(error);
async.eachSeries(apps, function (app, iteratorDone) {
if (!app.robotsTxt) return iteratorDone();
db.runSql('UPDATE apps SET reverseProxyConfigJson=? WHERE id=?', [ JSON.stringify({ robotsTxt: JSON.stringify(app.robotsTxt) }), app.id ], iteratorDone);
}, function (error) {
if (error) return callback(error);
db.runSql('ALTER TABLE apps DROP COLUMN robotsTxt', callback);
});
});
});
};
exports.down = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE apps DROP COLUMN reverseProxyConfigJson'),
], callback);
};
@@ -0,0 +1,13 @@
'use strict';
var fs = require('fs');
exports.up = function(db, callback) {
let sysinfoConfig = { provider: 'generic' };
db.runSql('REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'sysinfo_config', JSON.stringify(sysinfoConfig) ], callback);
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,27 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE apps ADD COLUMN mailboxDomain VARCHAR(128)'),
function setDefaultMailboxDomain(done) {
db.all('SELECT * FROM apps, subdomains WHERE apps.id=subdomains.appId AND type="primary"', function (error, apps) {
if (error) return done(error);
async.eachSeries(apps, function (app, iteratorDone) {
db.runSql('UPDATE apps SET mailboxDomain=? WHERE id=?', [ app.domain, app.id ], iteratorDone);
}, done);
});
},
db.runSql.bind(db, 'ALTER TABLE apps MODIFY COLUMN mailboxDomain VARCHAR(128) NOT NULL'),
db.runSql.bind(db, 'ALTER TABLE apps ADD CONSTRAINT apps_mailDomain_constraint FOREIGN KEY(mailboxDomain) REFERENCES domains(domain)'),
], callback);
};
exports.down = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE app DROP FOREIGN KEY apps_mailDomain_constraint'),
db.runSql.bind(db, 'ALTER TABLE apps DROP COLUMN mailboxDomain'),
], callback);
};
@@ -0,0 +1,22 @@
'use strict';
let async = require('async');
exports.up = function(db, callback) {
db.runSql('SELECT * FROM domains', function (error, domains) {
if (error) return callback(error);
async.eachSeries(domains, function (domain, iteratorCallback) {
if (domain.provider !== 'cloudflare') return iteratorCallback();
let config = JSON.parse(domain.configJson);
config.tokenType = 'GlobalApiKey';
db.runSql('UPDATE domains SET configJson = ? WHERE domain = ?', [ JSON.stringify(config), domain.domain ], iteratorCallback);
}, callback);
});
};
exports.down = function(db, callback) {
callback();
};
+15 -17
View File
@@ -27,6 +27,7 @@ CREATE TABLE IF NOT EXISTS users(
twoFactorAuthenticationSecret VARCHAR(128) DEFAULT "",
twoFactorAuthenticationEnabled BOOLEAN DEFAULT false,
admin BOOLEAN DEFAULT false,
source VARCHAR(128) DEFAULT "",
PRIMARY KEY(id));
@@ -62,10 +63,9 @@ CREATE TABLE IF NOT EXISTS clients(
CREATE TABLE IF NOT EXISTS apps(
id VARCHAR(128) NOT NULL UNIQUE,
appStoreId VARCHAR(128) NOT NULL,
installationState VARCHAR(512) NOT NULL,
installationProgress TEXT,
runState VARCHAR(512),
appStoreId VARCHAR(128) NOT NULL, // empty for custom apps
installationState VARCHAR(512) NOT NULL, // the active task on the app
runState VARCHAR(512) NOT NULL, // if the app is stopped
health VARCHAR(128),
healthTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app last responded
containerId VARCHAR(128),
@@ -81,21 +81,19 @@ CREATE TABLE IF NOT EXISTS apps(
xFrameOptions VARCHAR(512),
sso BOOLEAN DEFAULT 1, // whether user chose to enable SSO
debugModeJson TEXT, // options for development mode
robotsTxt TEXT,
reverseProxyConfigJson TEXT, // { robotsTxt, csp }
enableBackup BOOLEAN DEFAULT 1, // misnomer: controls automatic daily backups
enableAutomaticUpdate BOOLEAN DEFAULT 1,
mailboxName VARCHAR(128), // mailbox of this app. default allocated as '.app'
mailboxDomain VARCHAR(128) NOT NULL, // mailbox domain of this apps
label VARCHAR(128), // display name
tagsJson VARCHAR(2048), // array of tags
dataDir VARCHAR(256) UNIQUE,
taskId INTEGER, // current task
errorJson TEXT,
// the following fields do not belong here, they can be removed when we use a queue for apptask
restoreConfigJson VARCHAR(256), // used to pass backupId to restore from to apptask
oldConfigJson TEXT, // used to pass old config to apptask (configure, restore)
updateConfigJson TEXT, // used to pass new config to apptask (update)
ownerId VARCHAR(128),
FOREIGN KEY(ownerId) REFERENCES users(id),
FOREIGN KEY(mailboxDomain) REFERENCES domains(domain),
FOREIGN KEY(taskId) REFERENCES tasks(id),
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS appPortBindings(
@@ -193,7 +191,7 @@ CREATE TABLE IF NOT EXISTS mailboxes(
type VARCHAR(16) NOT NULL, /* 'mailbox', 'alias', 'list' */
ownerId VARCHAR(128) NOT NULL, /* user id */
aliasTarget VARCHAR(128), /* the target name type is an alias */
membersJson TEXT, /* members of a group */
membersJson TEXT, /* members of a group. fully qualified */
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
domain VARCHAR(128),
@@ -204,7 +202,7 @@ CREATE TABLE IF NOT EXISTS subdomains(
appId VARCHAR(128) NOT NULL,
domain VARCHAR(128) NOT NULL,
subdomain VARCHAR(128) NOT NULL,
type VARCHAR(128) NOT NULL,
type VARCHAR(128) NOT NULL, /* primary or redirect */
FOREIGN KEY(domain) REFERENCES domains(domain),
FOREIGN KEY(appId) REFERENCES apps(id),
@@ -215,8 +213,8 @@ CREATE TABLE IF NOT EXISTS tasks(
type VARCHAR(32) NOT NULL,
percent INTEGER DEFAULT 0,
message TEXT,
errorMessage TEXT,
result TEXT,
errorJson TEXT,
resultJson TEXT,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id));
+622 -336
View File
File diff suppressed because it is too large Load Diff
+23 -21
View File
@@ -14,40 +14,40 @@
"node": ">=4.0.0 <=4.1.1"
},
"dependencies": {
"@google-cloud/dns": "^0.9.2",
"@google-cloud/dns": "^1.1.0",
"@google-cloud/storage": "^2.5.0",
"@sindresorhus/df": "^3.1.0",
"@sindresorhus/df": "git+https://github.com/cloudron-io/df.git#type",
"async": "^2.6.2",
"aws-sdk": "^2.441.0",
"body-parser": "^1.18.3",
"cloudron-manifestformat": "^2.15.0",
"connect": "^3.6.6",
"aws-sdk": "^2.476.0",
"body-parser": "^1.19.0",
"cloudron-manifestformat": "^4.0.0",
"connect": "^3.7.0",
"connect-ensure-login": "^0.1.1",
"connect-lastmile": "^1.0.2",
"connect-lastmile": "^1.2.1",
"connect-timeout": "^1.9.0",
"cookie-parser": "^1.4.4",
"cookie-session": "^1.3.3",
"cron": "^1.7.0",
"csurf": "^1.9.0",
"db-migrate": "^0.11.5",
"cron": "^1.7.1",
"csurf": "^1.10.0",
"db-migrate": "^0.11.6",
"db-migrate-mysql": "^1.1.10",
"debug": "^4.1.1",
"dockerode": "^2.5.8",
"ejs": "^2.6.1",
"ejs-cli": "^2.0.1",
"express": "^4.16.4",
"express-session": "^1.16.1",
"express": "^4.17.1",
"express-session": "^1.16.2",
"js-yaml": "^3.13.1",
"json": "^9.0.6",
"ldapjs": "^1.0.2",
"lodash": "^4.17.11",
"lodash.chunk": "^4.2.0",
"mime": "^2.4.2",
"mime": "^2.4.4",
"moment-timezone": "^0.5.25",
"morgan": "^1.9.1",
"multiparty": "^4.2.1",
"mysql": "^2.17.1",
"nodemailer": "^6.1.1",
"nodemailer": "^6.2.1",
"nodemailer-smtp-transport": "^2.7.4",
"oauth2orize": "^1.11.0",
"once": "^1.4.0",
@@ -57,28 +57,30 @@
"passport-http-bearer": "^1.0.1",
"passport-local": "^1.0.0",
"passport-oauth2-client-password": "^0.1.2",
"pretty-bytes": "^5.3.0",
"progress-stream": "^2.0.0",
"proxy-middleware": "^0.15.0",
"qrcode": "^1.3.3",
"readdirp": "^3.0.0",
"readdirp": "^3.0.2",
"request": "^2.88.0",
"rimraf": "^2.6.3",
"s3-block-read-stream": "^0.5.0",
"safetydance": "^0.7.1",
"semver": "^6.0.0",
"semver": "^6.1.1",
"session-file-store": "^1.3.1",
"showdown": "^1.9.0",
"speakeasy": "^2.0.0",
"split": "^1.0.1",
"superagent": "^5.0.2",
"superagent": "^5.0.9",
"supererror": "^0.7.2",
"tar-fs": "github:cloudron-io/tar-fs#ignore_stat_error",
"tar-stream": "^2.0.1",
"tar-stream": "^2.1.0",
"tldjs": "^2.3.1",
"underscore": "^1.9.1",
"uuid": "^3.3.2",
"valid-url": "^1.0.9",
"validator": "^10.11.0",
"ws": "^6.2.1",
"validator": "^11.0.0",
"ws": "^7.0.0",
"xml2js": "^0.4.19"
},
"devDependencies": {
@@ -88,7 +90,7 @@
"mocha": "^6.1.4",
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
"nock": "^10.0.6",
"node-sass": "^4.11.0",
"node-sass": "^4.12.0",
"recursive-readdir": "^2.2.2"
},
"scripts": {
+1 -1
View File
@@ -22,7 +22,7 @@ fi
mkdir -p ${DATA_DIR}
cd ${DATA_DIR}
mkdir -p appsdata
mkdir -p boxdata/appicons boxdata/mail boxdata/certs boxdata/mail/dkim/localhost boxdata/mail/dkim/foobar.com
mkdir -p boxdata/profileicons boxdata/appicons boxdata/mail boxdata/certs boxdata/mail/dkim/localhost boxdata/mail/dkim/foobar.com
mkdir -p platformdata/addons/mail platformdata/nginx/cert platformdata/nginx/applications platformdata/collectd/collectd.conf.d platformdata/addons platformdata/logrotate.d platformdata/backup platformdata/logs/tasks
# put cert
+19 -4
View File
@@ -92,12 +92,14 @@ fi
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, galaxygate, gce, hetzner, interox, lightsail, linode, netcup, ovh, rosehosting, scaleway, skysilk, time4vps, upcloud, vultr or generic"
if [[ -z "${provider}" ]]; then
echo "--provider is required (azure, contabo, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, lightsail, linode, netcup, ovh, rosehosting, scaleway, upcloud, vultr or generic)"
echo "--provider is required ($AVAILABLE_PROVIDERS)"
exit 1
elif [[ \
"${provider}" != "ami" && \
"${provider}" != "azure" && \
"${provider}" != "azure-image" && \
"${provider}" != "caas" && \
"${provider}" != "cloudscale" && \
"${provider}" != "contabo" && \
@@ -106,9 +108,10 @@ elif [[ \
"${provider}" != "ec2" && \
"${provider}" != "exoscale" && \
"${provider}" != "galaxygate" && \
"${provider}" != "digitalocean" && \
"${provider}" != "gce" && \
"${provider}" != "hetzner" && \
"${provider}" != "interox" && \
"${provider}" != "interox-image" && \
"${provider}" != "lightsail" && \
"${provider}" != "linode" && \
"${provider}" != "linode-stackscript" && \
@@ -117,12 +120,16 @@ elif [[ \
"${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: azure, cloudscale.ch, contabo, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, lightsail, linode, netcup, ovh, rosehosting, scaleway, upcloud, vultr or generic"
echo "--provider must be one of: $AVAILABLE_PROVIDERS"
exit 1
fi
@@ -156,7 +163,7 @@ if [[ "${initBaseImage}" == "true" ]]; then
exit 1
fi
if ! apt-get install curl python3 ubuntu-standard -y &>> "${LOG_FILE}"; then
if ! DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" -y install curl python3 ubuntu-standard -y &>> "${LOG_FILE}"; then
echo "Could not install setup dependencies (curl). See ${LOG_FILE}"
exit 1
fi
@@ -199,6 +206,10 @@ fi
# NOTE: this install script only supports 3.x and above
echo "=> Installing version ${version} (this takes some time) ..."
mkdir -p /etc/cloudron
# this file is used >= 4.2
echo "${provider}" > /etc/cloudron/PROVIDER
# this file is unused <= 4.2 and exists to make legacy installations work. the start script will remove this file anyway
cat > "/etc/cloudron/cloudron.conf" <<CONF_END
{
"apiServerOrigin": "${apiServerOrigin}",
@@ -214,6 +225,10 @@ if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" &>> "${LOG_FILE}"; then
exit 1
fi
# only needed for >= 4.2
mysql -uroot -ppassword -e "REPLACE INTO box.settings (name, value) VALUES ('api_server_origin', '${apiServerOrigin}');" 2>/dev/null
mysql -uroot -ppassword -e "REPLACE INTO box.settings (name, value) VALUES ('web_server_origin', '${webServerOrigin}');" 2>/dev/null
echo -n "=> Waiting for cloudron to be ready (this takes some time) ..."
while true; do
echo -n "."
+13 -5
View File
@@ -13,6 +13,7 @@ HELP_MESSAGE="
This script collects diagnostic information to help debug server related issues
Options:
--admin-login Login as administrator
--enable-ssh Enable SSH access for the Cloudron support team
--help Show this message
"
@@ -25,13 +26,20 @@ fi
enableSSH="false"
args=$(getopt -o "" -l "help,enable-ssh" -n "$0" -- "$@")
args=$(getopt -o "" -l "help,enable-ssh,admin-login" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--help) echo -e "${HELP_MESSAGE}"; exit 0;;
--enable-ssh) enableSSH="true"; shift;;
--admin-login)
admin_username=$(mysql -NB -uroot -ppassword -e "SELECT username FROM box.users WHERE admin=1 LIMIT 1" 2>/dev/null)
admin_password=$(pwgen -1s 12)
printf '{"%s":"%s"}\n' "${admin_username}" "${admin_password}" > /tmp/cloudron_ghost.json
echo "Login as ${admin_username} / ${admin_password} . Remove /tmp/cloudron_ghost.json when done."
exit 0
;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
@@ -44,7 +52,7 @@ if [[ "`df --output="avail" / | sed -n 2p`" -lt "10240" ]]; then
echo ""
df -h
echo ""
echo "To recover from a full disk, follow the guide at https://cloudron.io/documentation/server/#recovery-after-disk-full"
echo "To recover from a full disk, follow the guide at https://cloudron.io/documentation/troubleshooting/#recovery-after-disk-full"
exit 1
fi
@@ -60,8 +68,8 @@ echo -n "Generating Cloudron Support stats..."
# clear file
rm -rf $OUT
echo -e $LINE"cloudron.conf"$LINE >> $OUT
cat /etc/cloudron/cloudron.conf &>> $OUT
echo -e $LINE"PROVIDER"$LINE >> $OUT
cat /etc/cloudron/PROVIDER &>> $OUT || true
echo -e $LINE"Docker container"$LINE >> $OUT
if ! timeout --kill-after 10s 15s docker ps -a &>> $OUT 2>&1; then
@@ -99,7 +107,7 @@ if [[ "${enableSSH}" == "true" ]]; then
permit_root_login=$(grep -q ^PermitRootLogin.*yes /etc/ssh/sshd_config && echo "yes" || echo "no")
# support.js uses similar logic
if $(grep -q "ec2\|lightsail\|ami" /etc/cloudron/cloudron.conf); then
if $(grep -q "ec2\|lightsail\|ami" /etc/cloudron/PROVIDER); then
ssh_user="ubuntu"
keys_file="/home/ubuntu/.ssh/authorized_keys"
else
+10 -5
View File
@@ -51,6 +51,7 @@ mkdir -p "${PLATFORM_DATA_DIR}/logs/backup" \
mkdir -p "${PLATFORM_DATA_DIR}/update"
mkdir -p "${BOX_DATA_DIR}/appicons"
mkdir -p "${BOX_DATA_DIR}/profileicons"
mkdir -p "${BOX_DATA_DIR}/certs"
mkdir -p "${BOX_DATA_DIR}/acme" # acme keys
mkdir -p "${BOX_DATA_DIR}/mail/dkim"
@@ -83,7 +84,7 @@ echo "==> Setting up unbound"
# We listen on 0.0.0.0 because there is no way control ordering of docker (which creates the 172.18.0.0/16) and unbound
# If IP6 is not enabled, dns queries seem to fail on some hosts. -s returns false if file missing or 0 size
ip6=$([[ -s /proc/net/if_inet6 ]] && echo "yes" || echo "no")
echo -e "server:\n\tinterface: 0.0.0.0\n\tdo-ip6: ${ip6}\n\taccess-control: 127.0.0.1 allow\n\taccess-control: 172.18.0.1/16 allow\n\tcache-max-negative-ttl: 30\n\tcache-max-ttl: 300\n\t#logfile: /var/log/unbound.log\n\t#verbosity: 10" > /etc/unbound/unbound.conf.d/cloudron-network.conf
cp -f "${script_dir}/start/unbound.conf" /etc/unbound/unbound.conf.d/cloudron-network.conf
# update the root anchor after a out-of-disk-space situation (see #269)
unbound-anchor -a /var/lib/unbound/root.key
@@ -108,8 +109,6 @@ systemctl restart unbound
# ensure cloudron-syslog runs
systemctl restart cloudron-syslog
$json -f /etc/cloudron/cloudron.conf -I -e "delete this.edition" # can be removed after 4.0
echo "==> Configuring sudoers"
rm -f /etc/sudoers.d/${USER}
cp "${script_dir}/start/sudoers" /etc/sudoers.d/${USER}
@@ -124,8 +123,8 @@ echo "==> Configuring logrotate"
if ! grep -q "^include ${PLATFORM_DATA_DIR}/logrotate.d" /etc/logrotate.conf; then
echo -e "\ninclude ${PLATFORM_DATA_DIR}/logrotate.d\n" >> /etc/logrotate.conf
fi
rm -f "${PLATFORM_DATA_DIR}/logrotate.d/"*
cp "${script_dir}/start/logrotate/"* "${PLATFORM_DATA_DIR}/logrotate.d/"
rm -f "${PLATFORM_DATA_DIR}/logrotate.d/box-logrotate" "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate" # remove pre 3.6 config files
# logrotate files have to be owned by root, this is here to fixup existing installations where we were resetting the owner to yellowtent
chown root:root "${PLATFORM_DATA_DIR}/logrotate.d/"
@@ -171,7 +170,13 @@ mysqladmin -u root -ppassword password password # reset default root password
mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
echo "==> Migrating data"
(cd "${BOX_SRC_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)
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
echo "DB migration failed"
exit 1
fi
rm -f /etc/cloudron/cloudron.conf
if [[ ! -f "${BOX_DATA_DIR}/dhparams.pem" ]]; then
echo "==> Generating dhparams (takes forever)"
+17 -2
View File
@@ -240,8 +240,23 @@ LoadPlugin write_graphite
Interactive false
Import "df"
# <Module df>
# </Module>
Import "du"
<Module du>
<Path>
Instance maildata
Dir "/home/yellowtent/boxdata/mail"
</Path>
<Path>
Instance boxdata
Dir "/home/yellowtent/boxdata"
Exclude "mail"
</Path>
<Path>
Instance platformdata
Dir "/home/yellowtent/platformdata"
</Path>
</Module>
</Plugin>
<Plugin write_graphite>
+1
View File
@@ -21,6 +21,7 @@ def read():
except:
continue
# type comes from https://github.com/collectd/collectd/blob/master/src/types.db
val = collectd.Values(type='df_complex', plugin='df', plugin_instance=instance)
free = st.f_bavail * st.f_frsize # bavail is for non-root user. bfree is total
+79
View File
@@ -0,0 +1,79 @@
import collectd,os,subprocess,sys,re,time
# https://www.programcreek.com/python/example/106897/collectd.register_read
PATHS = [] # { name, dir, exclude }
INTERVAL = 60 * 60 * 12 # twice a day. change values in docker-graphite if you change this
def du(pathinfo):
cmd = 'timeout 1800 du -Dsb "{}"'.format(pathinfo['dir'])
if pathinfo['exclude'] != '':
cmd += ' --exclude "{}"'.format(pathinfo['exclude'])
collectd.info('computing size with command: %s' % cmd);
try:
size = subprocess.check_output(cmd, shell=True).split()[0].decode('utf-8')
collectd.info('\tsize of %s is %s (time: %i)' % (pathinfo['dir'], size, int(time.time())))
return size
except Exception as e:
collectd.info('\terror getting the size of %s: %s' % (pathinfo['dir'], str(e)))
return 0
def parseSize(size):
units = {"B": 1, "KB": 10**3, "MB": 10**6, "GB": 10**9, "TB": 10**12}
number, unit, _ = re.split('([a-zA-Z]+)', size.upper())
return int(float(number)*units[unit])
def dockerSize():
# use --format '{{json .}}' to dump the string. '{{if eq .Type "Images"}}{{.Size}}{{end}}' still creates newlines
cmd = 'timeout 1800 docker system df --format "{{.Size}}" | head -n1'
try:
size = subprocess.check_output(cmd, shell=True).strip().decode('utf-8')
collectd.info('size of docker images is %s (%s) (time: %i)' % (size, parseSize(size), int(time.time())))
return parseSize(size)
except Exception as e:
collectd.info('error getting docker images size : %s' % str(e))
return 0
# configure is called for each module block. this is called before init
def configure(config):
global PATHS
for child in config.children:
if child.key != 'Path':
collectd.info('du plugin: Unknown config key "%s"' % key)
continue
pathinfo = { 'name': '', 'dir': '', 'exclude': '' }
for node in child.children:
if node.key == 'Instance':
pathinfo['name'] = node.values[0]
elif node.key == 'Dir':
pathinfo['dir'] = node.values[0]
elif node.key == 'Exclude':
pathinfo['exclude'] = node.values[0]
PATHS.append(pathinfo);
collectd.info('du plugin: monitoring %s' % pathinfo['dir']);
def init():
global PATHS
collectd.info('custom du plugin initialized with %s %s' % (PATHS, sys.version))
def read():
for pathinfo in PATHS:
size = du(pathinfo)
# type comes from https://github.com/collectd/collectd/blob/master/src/types.db
val = collectd.Values(type='capacity', plugin='du', plugin_instance=pathinfo['name'])
val.dispatch(values=[size], type_instance='usage')
size = dockerSize()
val = collectd.Values(type='capacity', plugin='du', plugin_instance='docker')
val.dispatch(values=[size], type_instance='usage')
collectd.register_init(init)
collectd.register_config(configure)
collectd.register_read(read, INTERVAL)
+1 -1
View File
@@ -37,4 +37,4 @@
# notifyCloudronAdmins: false
#
# footer:
# body: '&copy; 2019 [Cloudron](https://cloudron.io) [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)'
# body: '&copy; 2020 [Cloudron](https://cloudron.io) [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)'
-18
View File
@@ -1,18 +0,0 @@
# logrotate config for app, crash, addon and task logs
# man 7 glob
/home/yellowtent/platformdata/logs/[!t][!a][!s][!k][!s]/*.log {
# only keep one rotated file, we currently do not send that over the api
rotate 1
size 10M
# we never compress so we can simply tail the files
nocompress
copytruncate
}
/home/yellowtent/platformdata/logs/tasks/*.log {
monthly
rotate 0
missingok
}
+2 -1
View File
@@ -1,7 +1,8 @@
# logrotate config for box logs
# keep upto 5 logs of size 10M each
/home/yellowtent/platformdata/logs/box.log {
rotate 10
rotate 5
size 10M
# we never compress so we can simply tail the files
nocompress
+31
View File
@@ -0,0 +1,31 @@
# logrotate config for app, crash, addon and task logs
# man 7 glob
/home/yellowtent/platformdata/logs/graphite/*.log
/home/yellowtent/platformdata/logs/mail/*.log
/home/yellowtent/platformdata/logs/mysql/*.log
/home/yellowtent/platformdata/logs/mongodb/*.log
/home/yellowtent/platformdata/logs/postgresql/*.log
/home/yellowtent/platformdata/logs/sftp/*.log
/home/yellowtent/platformdata/logs/redis-*/*.log
/home/yellowtent/platformdata/logs/crash/*.log
/home/yellowtent/platformdata/logs/updater/*.log {
# only keep one rotated file, we currently do not send that over the api
rotate 1
size 10M
missingok
# we never compress so we can simply tail the files
nocompress
copytruncate
}
# keep task logs for a week. the 'nocreate' option ensures empty log files are not
# created post rotation
/home/yellowtent/platformdata/logs/tasks/*.log {
minage 7
daily
rotate 0
missingok
nocreate
}
+11
View File
@@ -0,0 +1,11 @@
server:
interface: 0.0.0.0
do-ip6: no
access-control: 127.0.0.1 allow
access-control: 172.18.0.1/16 allow
cache-max-negative-ttl: 30
cache-max-ttl: 300
# enable below for logging to journalctl -u unbound
# verbosity: 5
# log-queries: yes
+10 -14
View File
@@ -27,12 +27,10 @@ exports = module.exports = {
};
var assert = require('assert'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
BoxError = require('./boxerror.js'),
debug = require('debug')('box:accesscontrol'),
tokendb = require('./tokendb.js'),
users = require('./users.js'),
UsersError = users.UsersError,
_ = require('underscore');
// returns scopes that does not have wildcards and is sorted
@@ -79,13 +77,12 @@ function intersectScopes(allowedScopes, wantedScopes) {
function validateScopeString(scope) {
assert.strictEqual(typeof scope, 'string');
if (scope === '') return new Error('Empty scope not allowed');
if (scope === '') return new BoxError(BoxError.BAD_FIELD, 'Empty scope not allowed', { field: 'scope' });
// NOTE: this function intentionally does not allow '*'. This is only allowed in the db to allow
// us not write a migration script every time we add a new scope
var allValid = scope.split(',').every(function (s) { return exports.VALID_SCOPES.indexOf(s.split(':')[0]) !== -1; });
if (!allValid) return new Error('Invalid scope. Available scopes are ' + exports.VALID_SCOPES.join(', '));
if (!allValid) return new BoxError(BoxError.BAD_FIELD, 'Invalid scope. Available scopes are ' + exports.VALID_SCOPES.join(', '), { field: 'scope' });
return null;
}
@@ -102,7 +99,7 @@ function hasScopes(authorizedScopes, requiredScopes) {
// this allows apps:write if the token has a higher apps scope
if (authorizedScopes.indexOf(requiredScopes[i]) === -1 && authorizedScopes.indexOf(scopeParts[0]) === -1) {
debug('scope: missing scope "%s".', requiredScopes[i]);
return new Error('Missing required scope "' + requiredScopes[i] + '"');
return new BoxError(BoxError.NOT_FOUND, 'Missing required scope "' + requiredScopes[i] + '"');
}
}
@@ -123,21 +120,20 @@ function validateToken(accessToken, callback) {
assert.strictEqual(typeof callback, 'function');
tokendb.getByAccessToken(accessToken, function (error, token) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
if (error) return callback(error); // this triggers 'internal error' in passport
users.get(token.identifier, function (error, user) {
if (error && error.reason === UsersError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
if (error) return callback(error);
if (!user.active) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
scopesForUser(user, function (error, userScopes) {
if (error) return callback(error);
var authorizedScopes = intersectScopes(userScopes, token.scope.split(','));
const skipPasswordVerification = token.clientId === 'cid-sdk' || token.clientId === 'cid-cli'; // these clients do not require password checks unlike UI
var info = { authorizedScopes: authorizedScopes, skipPasswordVerification: skipPasswordVerification }; // ends up in req.authInfo
callback(null, user, info);
const authorizedScopes = intersectScopes(userScopes, token.scope.split(','));
callback(null, user, { authorizedScopes }); // ends up in req.authInfo
});
});
});
+216 -208
View File
@@ -1,8 +1,6 @@
'use strict';
exports = module.exports = {
AddonsError: AddonsError,
getServices: getServices,
getService: getService,
configureService: configureService,
@@ -36,16 +34,14 @@ var accesscontrol = require('./accesscontrol.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
clients = require('./clients.js'),
config = require('./config.js'),
ClientsError = clients.ClientsError,
constants = require('./constants.js'),
crypto = require('crypto'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:addons'),
docker = require('./docker.js'),
dockerConnection = docker.connection,
DockerError = docker.DockerError,
fs = require('fs'),
graphs = require('./graphs.js'),
hat = require('./hat.js'),
infra = require('./infra_version.js'),
mail = require('./mail.js'),
@@ -57,37 +53,13 @@ var accesscontrol = require('./accesscontrol.js'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
sftp = require('./sftp.js'),
shell = require('./shell.js'),
spawn = require('child_process').spawn,
split = require('split'),
request = require('request'),
util = require('util');
// http://dustinsenos.com/articles/customErrorsInNode
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
function AddonsError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(AddonsError, Error);
AddonsError.INTERNAL_ERROR = 'Internal Error';
AddonsError.NOT_FOUND = 'Not Found';
AddonsError.NOT_ACTIVE = 'Not Active';
const NOOP = function (app, options, callback) { return callback(); };
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
const RMADDONDIR_CMD = path.join(__dirname, 'scripts/rmaddondir.sh');
@@ -259,19 +231,36 @@ function dumpPath(addon, appId) {
}
}
function rebuildService(serviceName, callback) {
assert.strictEqual(typeof serviceName, 'string');
assert.strictEqual(typeof callback, 'function');
// this attempts to recreate the service docker container if they don't exist but platform infra version is unchanged
// passing an infra version of 'none' will not attempt to purge existing data, not sure if this is good or bad
if (serviceName === 'mongodb') return startMongodb({ version: 'none' }, callback);
if (serviceName === 'postgresql') return startPostgresql({ version: 'none' }, callback);
if (serviceName === 'mysql') return startMysql({ version: 'none' }, callback);
if (serviceName === 'sftp') return sftp.startSftp({ version: 'none' }, callback);
if (serviceName === 'graphite') return graphs.startGraphite({ version: 'none' }, callback);
// nothing to rebuild for now
callback();
}
function restartContainer(serviceName, callback) {
assert.strictEqual(typeof serviceName, 'string');
assert.strictEqual(typeof callback, 'function');
assert(KNOWN_SERVICES[serviceName], `Unknown service ${serviceName}`);
docker.stopContainer(serviceName, function (error) {
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
if (error) return callback(error);
docker.startContainer(serviceName, function (error) {
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, 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(null);
callback(error);
});
});
}
@@ -282,19 +271,18 @@ function getServiceDetails(containerName, tokenEnvName, callback) {
assert.strictEqual(typeof callback, 'function');
docker.inspect(containerName, function (error, result) {
if (error && error.reason === DockerError.NOT_FOUND) return callback(new AddonsError(AddonsError.NOT_ACTIVE, error));
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
if (error) return callback(error);
const ip = safe.query(result, 'NetworkSettings.Networks.cloudron.IPAddress', null);
if (!ip) return callback(new AddonsError(AddonsError.NOT_ACTIVE, `Error getting ${containerName} container ip`));
if (!ip) return callback(new BoxError(BoxError.INACTIVE, `Error getting IP of ${containerName} service`));
// extract the cloudron token for auth
const env = safe.query(result, 'Config.Env', null);
if (!env) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, `Error getting ${containerName} env`));
if (!env) return callback(new BoxError(BoxError.DOCKER_ERROR, `Error inspecting environment of ${containerName} service`));
const tmp = env.find(function (e) { return e.indexOf(tokenEnvName) === 0; });
if (!tmp) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, `Error getting ${containerName} cloudron token env var`));
if (!tmp) return callback(new BoxError(BoxError.DOCKER_ERROR, `Error getting token of ${containerName} service`));
const token = tmp.slice(tokenEnvName.length + 1); // +1 for the = sign
if (!token) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, `Error getting ${containerName} cloudron token`));
if (!token) return callback(new BoxError(BoxError.DOCKER_ERROR, `Error getting token of ${containerName} service`));
callback(null, { ip: ip, token: token, state: result.State });
});
@@ -306,7 +294,7 @@ function containerStatus(addonName, addonTokenName, callback) {
assert.strictEqual(typeof callback, 'function');
getServiceDetails(addonName, addonTokenName, function (error, addonDetails) {
if (error && error.reason === AddonsError.NOT_ACTIVE) return callback(null, { status: exports.SERVICE_STATUS_STOPPED });
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, { status: exports.SERVICE_STATUS_STOPPED });
if (error) return callback(error);
request.get(`https://${addonDetails.ip}:3000/healthcheck?access_token=${addonDetails.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
@@ -314,7 +302,7 @@ function containerStatus(addonName, addonTokenName, callback) {
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}` });
docker.memoryUsage(addonName, function (error, result) {
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
if (error) return callback(error);
var tmp = {
status: addonDetails.state.Running ? exports.SERVICE_STATUS_ACTIVE : exports.SERVICE_STATUS_STOPPED,
@@ -340,11 +328,14 @@ function getService(serviceName, callback) {
assert.strictEqual(typeof serviceName, 'string');
assert.strictEqual(typeof callback, 'function');
if (!KNOWN_SERVICES[serviceName]) return callback(new AddonsError(AddonsError.NOT_FOUND));
if (!KNOWN_SERVICES[serviceName]) return callback(new BoxError(BoxError.NOT_FOUND));
var tmp = {
name: serviceName,
status: null,
memoryUsed: 0,
memoryPercent: 0,
error: null,
config: {
// If a property is not set then we cannot change it through the api, see below
// memory: 0,
@@ -353,7 +344,7 @@ function getService(serviceName, callback) {
};
settings.getPlatformConfig(function (error, platformConfig) {
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
if (error) return callback(error);
if (platformConfig[serviceName] && platformConfig[serviceName].memory && platformConfig[serviceName].memorySwap) {
tmp.config.memory = platformConfig[serviceName].memory;
@@ -364,7 +355,7 @@ function getService(serviceName, callback) {
}
KNOWN_SERVICES[serviceName].status(function (error, result) {
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
if (error) return callback(error);
tmp.status = result.status;
tmp.memoryUsed = result.memoryUsed;
@@ -381,10 +372,10 @@ function configureService(serviceName, data, callback) {
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
if (!KNOWN_SERVICES[serviceName]) return callback(new AddonsError(AddonsError.NOT_FOUND));
if (!KNOWN_SERVICES[serviceName]) return callback(new BoxError(BoxError.NOT_FOUND));
settings.getPlatformConfig(function (error, platformConfig) {
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
if (error) return callback(error);
if (!platformConfig[serviceName]) platformConfig[serviceName] = {};
@@ -397,7 +388,7 @@ function configureService(serviceName, data, callback) {
}
settings.setPlatformConfig(platformConfig, function (error) {
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback(null);
});
@@ -413,7 +404,7 @@ function getServiceLogs(serviceName, options, callback) {
assert.strictEqual(typeof options.format, 'string');
assert.strictEqual(typeof options.follow, 'boolean');
if (!KNOWN_SERVICES[serviceName]) return callback(new AddonsError(AddonsError.NOT_FOUND));
if (!KNOWN_SERVICES[serviceName]) return callback(new BoxError(BoxError.NOT_FOUND));
debug(`Getting logs for ${serviceName}`);
@@ -472,7 +463,7 @@ function restartService(serviceName, callback) {
assert.strictEqual(typeof serviceName, 'string');
assert.strictEqual(typeof callback, 'function');
if (!KNOWN_SERVICES[serviceName]) return callback(new AddonsError(AddonsError.NOT_FOUND));
if (!KNOWN_SERVICES[serviceName]) return callback(new BoxError(BoxError.NOT_FOUND));
KNOWN_SERVICES[serviceName].restart(callback);
}
@@ -489,8 +480,8 @@ function waitForService(containerName, tokenEnvName, callback) {
async.retry({ times: 10, interval: 15000 }, function (retryCallback) {
request.get(`https://${result.ip}:3000/healthcheck?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
if (error) return retryCallback(new Error(`Error waiting for ${containerName}: ${error.message}`));
if (response.statusCode !== 200 || !response.body.status) return retryCallback(new Error(`Error waiting for ${containerName}. Status code: ${response.statusCode} message: ${response.body.message}`));
if (error) return retryCallback(new BoxError(BoxError.ADDONS_ERROR, `Network error waiting for ${containerName}: ${error.message}`));
if (response.statusCode !== 200 || !response.body.status) return retryCallback(new BoxError(BoxError.ADDONS_ERROR, `Error waiting for ${containerName}. Status code: ${response.statusCode} message: ${response.body.message}`));
retryCallback(null);
});
@@ -508,7 +499,7 @@ function setupAddons(app, addons, callback) {
debugApp(app, 'setupAddons: Setting up %j', Object.keys(addons));
async.eachSeries(Object.keys(addons), function iterator(addon, iteratorCallback) {
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new Error('No such addon:' + addon));
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
debugApp(app, 'Setting up addon %s with options %j', addon, addons[addon]);
@@ -526,7 +517,7 @@ function teardownAddons(app, addons, callback) {
debugApp(app, 'teardownAddons: Tearing down %j', Object.keys(addons));
async.eachSeries(Object.keys(addons), function iterator(addon, iteratorCallback) {
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new Error('No such addon:' + addon));
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
debugApp(app, 'Tearing down addon %s with options %j', addon, addons[addon]);
@@ -546,7 +537,7 @@ function backupAddons(app, addons, callback) {
debugApp(app, 'backupAddons: Backing up %j', Object.keys(addons));
async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) {
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new Error('No such addon:' + addon));
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
KNOWN_ADDONS[addon].backup(app, addons[addon], iteratorCallback);
}, callback);
@@ -564,7 +555,7 @@ function clearAddons(app, addons, callback) {
debugApp(app, 'clearAddons: clearing %j', Object.keys(addons));
async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) {
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new Error('No such addon:' + addon));
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
KNOWN_ADDONS[addon].clear(app, addons[addon], iteratorCallback);
}, callback);
@@ -582,7 +573,7 @@ function restoreAddons(app, addons, callback) {
debugApp(app, 'restoreAddons: restoring %j', Object.keys(addons));
async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) {
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new Error('No such addon:' + addon));
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
KNOWN_ADDONS[addon].restore(app, addons[addon], iteratorCallback);
}, callback);
@@ -593,7 +584,7 @@ function importAppDatabase(app, addon, callback) {
assert.strictEqual(typeof addon, 'string');
assert.strictEqual(typeof callback, 'function');
if (!(addon in KNOWN_ADDONS)) return callback(new Error(`No such addon: ${addon}`));
if (!(addon in KNOWN_ADDONS)) return callback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
async.series([
KNOWN_ADDONS[addon].setup.bind(null, app, app.manifest.addons[addon]),
@@ -620,7 +611,9 @@ function importDatabase(addon, callback) {
if (!error) return iteratorCallback();
debug(`importDatabase: Error importing ${addon} of app ${app.id}. Marking as errored`, error);
appdb.update(app.id, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message }, iteratorCallback);
// FIXME: there is no way to 'repair' if we are here. we need to make a separate apptask that re-imports db
// 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);
});
}, callback);
});
@@ -685,7 +678,7 @@ function getEnvironment(app, callback) {
appdb.getAddonConfigByAppId(app.id, function (error, result) {
if (error) return callback(error);
if (app.manifest.addons['docker']) result.push({ name: 'DOCKER_HOST', value: `tcp://172.18.0.1:${config.get('dockerProxyPort')}` });
if (app.manifest.addons['docker']) result.push({ name: 'DOCKER_HOST', value: `tcp://172.18.0.1:${constants.DOCKER_PROXY_PORT}` });
return callback(null, result.map(function (e) { return e.name + '=' + e.value; }));
});
@@ -790,7 +783,7 @@ function setupOauth(app, options, callback) {
var scope = accesscontrol.SCOPE_PROFILE;
clients.delByAppIdAndType(appId, clients.TYPE_OAUTH, function (error) { // remove existing creds
if (error && error.reason !== ClientsError.NOT_FOUND) return callback(error);
if (error && error.reason !== BoxError.NOT_FOUND) return callback(error);
clients.add(appId, clients.TYPE_OAUTH, redirectURI, scope, function (error, result) {
if (error) return callback(error);
@@ -800,7 +793,7 @@ function setupOauth(app, options, callback) {
var env = [
{ name: `${envPrefix}OAUTH_CLIENT_ID`, value: result.id },
{ name: `${envPrefix}OAUTH_CLIENT_SECRET`, value: result.clientSecret },
{ name: `${envPrefix}OAUTH_ORIGIN`, value: config.adminOrigin() }
{ name: `${envPrefix}OAUTH_ORIGIN`, value: settings.adminOrigin() }
];
debugApp(app, 'Setting oauth addon config to %j', env);
@@ -818,7 +811,7 @@ function teardownOauth(app, options, callback) {
debugApp(app, 'teardownOauth');
clients.delByAppIdAndType(app.id, clients.TYPE_OAUTH, function (error) {
if (error && error.reason !== ClientsError.NOT_FOUND) debug(error);
if (error && error.reason !== BoxError.NOT_FOUND) debug(error);
appdb.unsetAddonConfig(app.id, 'oauth', callback);
});
@@ -876,8 +869,8 @@ function setupLdap(app, options, callback) {
var env = [
{ name: `${envPrefix}LDAP_SERVER`, value: '172.18.0.1' },
{ name: `${envPrefix}LDAP_PORT`, value: '' + config.get('ldapPort') },
{ name: `${envPrefix}LDAP_URL`, value: 'ldap://172.18.0.1:' + config.get('ldapPort') },
{ name: `${envPrefix}LDAP_PORT`, value: '' + constants.LDAP_PORT },
{ name: `${envPrefix}LDAP_URL`, value: 'ldap://172.18.0.1:' + constants.LDAP_PORT },
{ name: `${envPrefix}LDAP_USERS_BASE_DN`, value: 'ou=users,dc=cloudron' },
{ name: `${envPrefix}LDAP_GROUPS_BASE_DN`, value: 'ou=groups,dc=cloudron' },
{ name: `${envPrefix}LDAP_BIND_DN`, value: 'cn='+ app.id + ',ou=apps,dc=cloudron' },
@@ -906,8 +899,8 @@ function setupSendMail(app, options, callback) {
debugApp(app, 'Setting up SendMail');
appdb.getAddonConfigByName(app.id, 'sendmail', 'MAIL_SMTP_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
appdb.getAddonConfigByName(app.id, 'sendmail', '%MAIL_SMTP_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== BoxError.NOT_FOUND) return callback(error);
var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length
@@ -917,10 +910,10 @@ function setupSendMail(app, options, callback) {
{ name: `${envPrefix}MAIL_SMTP_SERVER`, value: 'mail' },
{ name: `${envPrefix}MAIL_SMTP_PORT`, value: '2525' },
{ name: `${envPrefix}MAIL_SMTPS_PORT`, value: '2465' },
{ name: `${envPrefix}MAIL_SMTP_USERNAME`, value: app.mailboxName + '@' + app.domain },
{ name: `${envPrefix}MAIL_SMTP_USERNAME`, value: app.mailboxName + '@' + app.mailboxDomain },
{ name: `${envPrefix}MAIL_SMTP_PASSWORD`, value: password },
{ name: `${envPrefix}MAIL_FROM`, value: app.mailboxName + '@' + app.domain },
{ name: `${envPrefix}MAIL_DOMAIN`, value: app.domain }
{ name: `${envPrefix}MAIL_FROM`, value: app.mailboxName + '@' + app.mailboxDomain },
{ name: `${envPrefix}MAIL_DOMAIN`, value: app.mailboxDomain }
];
debugApp(app, 'Setting sendmail addon config to %j', env);
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
@@ -944,8 +937,8 @@ function setupRecvMail(app, options, callback) {
debugApp(app, 'Setting up recvmail');
appdb.getAddonConfigByName(app.id, 'recvmail', 'MAIL_IMAP_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
appdb.getAddonConfigByName(app.id, 'recvmail', '%MAIL_IMAP_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== BoxError.NOT_FOUND) return callback(error);
var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length
@@ -954,10 +947,10 @@ function setupRecvMail(app, options, callback) {
var env = [
{ name: `${envPrefix}MAIL_IMAP_SERVER`, value: 'mail' },
{ name: `${envPrefix}MAIL_IMAP_PORT`, value: '9993' },
{ name: `${envPrefix}MAIL_IMAP_USERNAME`, value: app.mailboxName + '@' + app.domain },
{ name: `${envPrefix}MAIL_IMAP_USERNAME`, value: app.mailboxName + '@' + app.mailboxDomain },
{ name: `${envPrefix}MAIL_IMAP_PASSWORD`, value: password },
{ name: `${envPrefix}MAIL_TO`, value: app.mailboxName + '@' + app.domain },
{ name: `${envPrefix}MAIL_DOMAIN`, value: app.domain }
{ name: `${envPrefix}MAIL_TO`, value: app.mailboxName + '@' + app.mailboxDomain },
{ name: `${envPrefix}MAIL_DOMAIN`, value: app.mailboxDomain }
];
debugApp(app, 'Setting sendmail addon config to %j', env);
@@ -1040,8 +1033,8 @@ function setupMySql(app, options, callback) {
debugApp(app, 'Setting up mysql');
appdb.getAddonConfigByName(app.id, 'mysql', 'MYSQL_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
appdb.getAddonConfigByName(app.id, 'mysql', '%MYSQL_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== BoxError.NOT_FOUND) return callback(error);
const tmp = mysqlDatabaseName(app.id);
@@ -1056,8 +1049,8 @@ function setupMySql(app, options, callback) {
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) {
if (error) return callback(new Error('Error setting up mysql: ' + error));
if (response.statusCode !== 201) return callback(new Error(`Error setting up mysql. Status code: ${response.statusCode} message: ${response.body.message}`));
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error setting up mysql: ${error.message}`));
if (response.statusCode !== 201) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error setting up mysql. Status code: ${response.statusCode} message: ${response.body.message}`));
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
@@ -1094,9 +1087,10 @@ function clearMySql(app, options, callback) {
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}/clear?access_token=${result.token}`, { rejectUnauthorized: false }, function (error, response) {
if (error) return callback(new Error('Error clearing mysql: ' + error));
if (response.statusCode !== 200) return callback(new Error(`Error clearing mysql. Status code: ${response.statusCode} message: ${response.body.message}`));
request.post(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}/clear?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error clearing mysql: ${error.message}`));
if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error clearing mysql. Status code: ${response.statusCode} message: ${response.body.message}`));
callback();
});
});
@@ -1113,9 +1107,9 @@ function teardownMySql(app, options, callback) {
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
if (error) return callback(error);
request.delete(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}?access_token=${result.token}&username=${username}`, { rejectUnauthorized: false }, function (error, response) {
if (error) return callback(new Error('Error clearing mysql: ' + error));
if (response.statusCode !== 200) return callback(new Error(`Error clearing mysql. Status code: ${response.statusCode} message: ${response.body.message}`));
request.delete(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}?access_token=${result.token}&username=${username}`, { json: true, rejectUnauthorized: false }, function (error, response) {
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error tearing down mysql: ${error.message}`));
if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error tearing down mysql. Status code: ${response.statusCode} message: ${response.body.message}`));
appdb.unsetAddonConfig(app.id, 'mysql', callback);
});
@@ -1134,14 +1128,15 @@ function pipeRequestToFile(url, filename, callback) {
callback(error);
});
writeStream.on('error', done);
writeStream.on('error', (error) => done(new BoxError(BoxError.FS_ERROR, `Error writing to ${filename}: ${error.message}`)));
writeStream.on('open', function () {
// note: do not attach to post callback handler because this will buffer the entire reponse!
// see https://github.com/request/request/issues/2270
const req = request.post(url, { rejectUnauthorized: false });
req.on('error', done); // network error, dns error, request errored in middle etc
req.on('error', (error) => done(new BoxError(BoxError.NETWORK_ERROR, `Request error writing to ${filename}: ${error.message}`))); // network error, dns error, request errored in middle etc
req.on('response', function (response) {
if (response.statusCode !== 200) return done(new Error(`Unexpected response code: ${response.statusCode} message: ${response.statusMessage} filename: ${filename}`));
if (response.statusCode !== 200) return done(new BoxError(BoxError.ADDONS_ERROR, `Unexpected response code when piping ${url}: ${response.statusCode} message: ${response.statusMessage} filename: ${filename}`));
response.pipe(writeStream).on('finish', done); // this is hit after data written to disk
});
@@ -1180,11 +1175,11 @@ function restoreMySql(app, options, callback) {
if (error) return callback(error);
var input = fs.createReadStream(dumpPath('mysql', app.id));
input.on('error', callback);
input.on('error', (error) => callback(new BoxError(BoxError.FS_ERROR, `Error reading input stream when restoring mysql: ${error.message}`)));
const restoreReq = request.post(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}/restore?access_token=${result.token}`, { rejectUnauthorized: false }, function (error, response) {
if (error) return callback(error);
if (response.statusCode !== 200) return callback(new Error(`Unexpected response from mysql addon ${response.statusCode} message: ${response.body.message}`));
const restoreReq = request.post(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}/restore?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring mysql: ${error.message}`));
if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring mysql. Status code: ${response.statusCode} message: ${response.body.message}`));
callback(null);
});
@@ -1256,8 +1251,8 @@ function setupPostgreSql(app, options, callback) {
const { database, username } = postgreSqlNames(app.id);
appdb.getAddonConfigByName(app.id, 'postgresql', 'POSTGRESQL_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
appdb.getAddonConfigByName(app.id, 'postgresql', '%POSTGRESQL_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== BoxError.NOT_FOUND) return callback(error);
const data = {
database: database,
@@ -1269,8 +1264,8 @@ function setupPostgreSql(app, options, callback) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/databases?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) {
if (error) return callback(new Error('Error setting up postgresql: ' + error));
if (response.statusCode !== 201) return callback(new Error(`Error setting up postgresql. Status code: ${response.statusCode} message: ${response.body.message}`));
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error setting up postgresql: ${error.message}`));
if (response.statusCode !== 201) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error setting up postgresql. Status code: ${response.statusCode} message: ${response.body.message}`));
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
@@ -1302,9 +1297,9 @@ function clearPostgreSql(app, options, callback) {
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/databases/${database}/clear?access_token=${result.token}&username=${username}`, { rejectUnauthorized: false }, function (error, response) {
if (error) return callback(new Error('Error clearing postgresql: ' + error));
if (response.statusCode !== 200) return callback(new Error(`Error clearing postgresql. Status code: ${response.statusCode} message: ${response.body.message}`));
request.post(`https://${result.ip}:3000/databases/${database}/clear?access_token=${result.token}&username=${username}`, { json: true, rejectUnauthorized: false }, function (error, response) {
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error clearing postgresql: ${error.message}`));
if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error clearing postgresql. Status code: ${response.statusCode} message: ${response.body.message}`));
callback(null);
});
@@ -1321,9 +1316,9 @@ function teardownPostgreSql(app, options, callback) {
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
if (error) return callback(error);
request.delete(`https://${result.ip}:3000/databases/${database}?access_token=${result.token}&username=${username}`, { rejectUnauthorized: false }, function (error, response) {
if (error) return callback(new Error('Error tearing down postgresql: ' + error));
if (response.statusCode !== 200) return callback(new Error(`Error tearing down postgresql. Status code: ${response.statusCode} message: ${response.body.message}`));
request.delete(`https://${result.ip}:3000/databases/${database}?access_token=${result.token}&username=${username}`, { json: true, rejectUnauthorized: false }, function (error, response) {
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error tearing down postgresql: ${error.message}`));
if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error tearing down postgresql. Status code: ${response.statusCode} message: ${response.body.message}`));
appdb.unsetAddonConfig(app.id, 'postgresql', callback);
});
@@ -1362,11 +1357,11 @@ function restorePostgreSql(app, options, callback) {
if (error) return callback(error);
var input = fs.createReadStream(dumpPath('postgresql', app.id));
input.on('error', callback);
input.on('error', (error) => callback(new BoxError(BoxError.FS_ERROR, `Error reading input stream when restoring postgresql: ${error.message}`)));
const restoreReq = request.post(`https://${result.ip}:3000/databases/${database}/restore?access_token=${result.token}&username=${username}`, { rejectUnauthorized: false }, function (error, response) {
if (error) return callback(error);
if (response.statusCode !== 200) return callback(new Error(`Unexpected response from postgresql addon ${response.statusCode} message: ${response.body.message}`));
const restoreReq = request.post(`https://${result.ip}:3000/databases/${database}/restore?access_token=${result.token}&username=${username}`, { json: true, rejectUnauthorized: false }, function (error, response) {
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring postgresql: ${error.message}`));
if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring postgresql. Status code: ${response.statusCode} message: ${response.body.message}`));
callback(null);
});
@@ -1431,26 +1426,27 @@ function setupMongoDb(app, options, callback) {
debugApp(app, 'Setting up mongodb');
appdb.getAddonConfigByName(app.id, 'mongodb', 'MONGODB_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
appdb.getAddonConfigByName(app.id, 'mongodb', '%MONGODB_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== BoxError.NOT_FOUND) return callback(error);
const data = {
database: app.id,
username: app.id,
password: error ? hat(4 * 128) : existingPassword
password: error ? hat(4 * 128) : existingPassword,
oplog: !!options.oplog
};
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/databases?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) {
if (error) return callback(new Error('Error setting up mongodb: ' + error));
if (response.statusCode !== 201) return callback(new Error(`Error setting up mongodb. Status code: ${response.statusCode}`));
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error setting up mongodb: ${error.message}`));
if (response.statusCode !== 201) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error setting up mongodb. Status code: ${response.statusCode} message: ${response.body.message}`));
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
var env = [
{ name: `${envPrefix}MONGODB_URL`, value : `mongodb://${data.username}:${data.password}@mongodb/${data.database}` },
{ name: `${envPrefix}MONGODB_URL`, value : `mongodb://${data.username}:${data.password}@mongodb:27017/${data.database}` },
{ name: `${envPrefix}MONGODB_USERNAME`, value : data.username },
{ name: `${envPrefix}MONGODB_PASSWORD`, value: data.password },
{ name: `${envPrefix}MONGODB_HOST`, value : 'mongodb' },
@@ -1458,6 +1454,10 @@ function setupMongoDb(app, options, callback) {
{ name: `${envPrefix}MONGODB_DATABASE`, value : data.database }
];
if (options.oplog) {
env.push({ name: `${envPrefix}MONGODB_OPLOG_URL`, value : `mongodb://${data.username}:${data.password}@mongodb:27017/local?authSource=${data.database}` });
}
debugApp(app, 'Setting mongodb addon config to %j', env);
appdb.setAddonConfig(app.id, 'mongodb', env, callback);
});
@@ -1475,9 +1475,9 @@ function clearMongodb(app, options, callback) {
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/databases/${app.id}/clear?access_token=${result.token}`, { rejectUnauthorized: false }, function (error, response) {
if (error) return callback(new Error('Error clearing mongodb: ' + error));
if (response.statusCode !== 200) return callback(new Error(`Error clearing mongodb. Status code: ${response.statusCode} message: ${response.body.message}`));
request.post(`https://${result.ip}:3000/databases/${app.id}/clear?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error clearing mongodb: ${error.message}`));
if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error clearing mongodb. Status code: ${response.statusCode} message: ${response.body.message}`));
callback();
});
@@ -1494,9 +1494,9 @@ function teardownMongoDb(app, options, callback) {
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
if (error) return callback(error);
request.delete(`https://${result.ip}:3000/databases/${app.id}?access_token=${result.token}`, { rejectUnauthorized: false }, function (error, response) {
if (error) return callback(new Error('Error tearing down mongodb: ' + error));
if (response.statusCode !== 200) return callback(new Error(`Error tearing down mongodb. Status code: ${response.statusCode} message: ${response.body.message}`));
request.delete(`https://${result.ip}:3000/databases/${app.id}?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error tearing down mongodb: ${error.message}`));
if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error tearing down mongodb. Status code: ${response.statusCode} message: ${response.body.message}`));
appdb.unsetAddonConfig(app.id, 'mongodb', callback);
});
@@ -1531,11 +1531,11 @@ function restoreMongoDb(app, options, callback) {
if (error) return callback(error);
const readStream = fs.createReadStream(dumpPath('mongodb', app.id));
readStream.on('error', callback);
readStream.on('error', (error) => callback(new BoxError(BoxError.FS_ERROR, `Error reading input stream when restoring mongodb: ${error.message}`)));
const restoreReq = request.post(`https://${result.ip}:3000/databases/${app.id}/restore?access_token=${result.token}`, { rejectUnauthorized: false }, function (error, response) {
if (error) return callback(error);
if (response.statusCode !== 200) return callback(new Error(`Unexpected response from mongodb addon ${response.statusCode} message: ${response.body.message}`));
const restoreReq = request.post(`https://${result.ip}:3000/databases/${app.id}/restore?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring mongodb: ${error.message}`));
if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring mongodb. Status code: ${response.statusCode} message: ${response.body.message}`));
callback(null);
});
@@ -1551,9 +1551,21 @@ function startRedis(existingInfra, callback) {
const tag = infra.images.redis.tag;
const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.redis.tag, tag);
if (!upgrading) return callback();
appdb.getAll(function (error, apps) {
if (error) return callback(error);
importDatabase('redis', callback); // setupRedis currently starts the app container
async.eachSeries(apps, function iterator (app, iteratorCallback) {
if (!('redis' in app.manifest.addons)) return iteratorCallback(); // app doesn't use the addon
setupRedis(app, app.manifest.addons.redis, iteratorCallback);
}, function (error) {
if (error) return callback(error);
if (!upgrading) return callback();
importDatabase('redis', callback); // setupRedis currently starts the app container
});
});
}
// Ensures that app's addon redis container is running. Can be called when named container already exists/running
@@ -1564,68 +1576,69 @@ function setupRedis(app, options, callback) {
const redisName = 'redis-' + app.id;
docker.inspect(redisName, function (error, result) {
if (!error) {
debug(`Re-using existing redis container with state: ${result.State}`);
return callback();
appdb.getAddonConfigByName(app.id, 'redis', '%REDIS_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== BoxError.NOT_FOUND) return callback(error);
const redisPassword = options.noPassword ? '' : (error ? hat(4 * 48) : existingPassword); // see box#362 for password length
const redisServiceToken = hat(4 * 48);
// Compute redis memory limit based on app's memory limit (this is arbitrary)
var memoryLimit = app.memoryLimit || app.manifest.memoryLimit || 0;
if (memoryLimit === -1) { // unrestricted (debug mode)
memoryLimit = 0;
} else if (memoryLimit === 0 || memoryLimit <= (2 * 1024 * 1024 * 1024)) { // less than 2G (ram+swap)
memoryLimit = 150 * 1024 * 1024; // 150m
} else {
memoryLimit = 600 * 1024 * 1024; // 600m
}
appdb.getAddonConfigByName(app.id, 'redis', 'REDIS_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
const tag = infra.images.redis.tag;
const label = app.fqdn;
// note that we do not add appId label because this interferes with the stop/start app logic
const cmd = `docker run --restart=always -d --name=${redisName} \
--hostname ${redisName} \
--label=location=${label} \
--net cloudron \
--net-alias ${redisName} \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag="${redisName}" \
-m ${memoryLimit/2} \
--memory-swap ${memoryLimit} \
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_REDIS_PASSWORD="${redisPassword}" \
-e CLOUDRON_REDIS_TOKEN="${redisServiceToken}" \
-v "${paths.PLATFORM_DATA_DIR}/redis/${app.id}:/var/lib/redis" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run ${tag}`;
const redisPassword = error ? hat(4 * 48) : existingPassword; // see box#362 for password length
const redisServiceToken = hat(4 * 48);
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
// Compute redis memory limit based on app's memory limit (this is arbitrary)
var memoryLimit = app.memoryLimit || app.manifest.memoryLimit || 0;
var env = [
{ name: `${envPrefix}REDIS_URL`, value: 'redis://redisuser:' + redisPassword + '@redis-' + app.id },
{ name: `${envPrefix}REDIS_PASSWORD`, value: redisPassword },
{ name: `${envPrefix}REDIS_HOST`, value: redisName },
{ name: `${envPrefix}REDIS_PORT`, value: '6379' }
];
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 label = app.fqdn;
// note that we do not add appId label because this interferes with the stop/start app logic
const cmd = `docker run --restart=always -d --name=${redisName} \
--hostname ${redisName} \
--label=location=${label} \
--net cloudron \
--net-alias ${redisName} \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag="${redisName}" \
-m ${memoryLimit/2} \
--memory-swap ${memoryLimit} \
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_REDIS_PASSWORD="${redisPassword}" \
-e CLOUDRON_REDIS_TOKEN="${redisServiceToken}" \
-v "${paths.PLATFORM_DATA_DIR}/redis/${app.id}:/var/lib/redis" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run ${tag}`;
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
var env = [
{ name: `${envPrefix}REDIS_URL`, value: 'redis://redisuser:' + redisPassword + '@redis-' + app.id },
{ name: `${envPrefix}REDIS_PASSWORD`, value: redisPassword },
{ name: `${envPrefix}REDIS_HOST`, value: redisName },
{ name: `${envPrefix}REDIS_PORT`, value: '6379' }
];
async.series([
shell.exec.bind(null, 'startRedis', cmd),
appdb.setAddonConfig.bind(null, app.id, 'redis', env),
waitForService.bind(null, 'redis-' + app.id, 'CLOUDRON_REDIS_TOKEN')
], function (error) {
if (error) debug('Error setting up redis: ', error);
callback(error);
});
async.series([
(next) => {
docker.inspect(redisName, function (inspectError, result) { // fast-path
if (!inspectError) {
debug(`Re-using existing redis container with state: ${JSON.stringify(result.State)}`);
return next();
}
shell.exec('startRedis', cmd, next);
});
},
appdb.setAddonConfig.bind(null, app.id, 'redis', env),
waitForService.bind(null, 'redis-' + app.id, 'CLOUDRON_REDIS_TOKEN')
], function (error) {
if (error) debug('Error setting up redis: ', error);
callback(error);
});
});
}
@@ -1640,9 +1653,9 @@ function clearRedis(app, options, callback) {
getServiceDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/clear?access_token=${result.token}`, { rejectUnauthorized: false }, function (error, response) {
if (error) return callback(new Error('Error clearing redis: ' + error));
if (response.statusCode !== 200) return callback(new Error(`Error clearing redis. Status code: ${response.statusCode} message: ${response.body.message}`));
request.post(`https://${result.ip}:3000/clear?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error clearing redis: ${error.message}`));
if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error clearing redis. Status code: ${response.statusCode} message: ${response.body.message}`));
callback(null);
});
@@ -1654,18 +1667,11 @@ function teardownRedis(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var container = dockerConnection.getContainer('redis-' + app.id);
var removeOptions = {
force: true, // kill container if it's running
v: true // removes volumes associated with the container
};
container.remove(removeOptions, function (error) {
if (error && error.statusCode !== 404) return callback(new Error('Error removing container:' + error));
docker.deleteContainer(`redis-${app.id}`, function (error) {
if (error) return callback(error);
shell.sudo('removeVolume', [ RMADDONDIR_CMD, 'redis', app.id ], {}, function (error) {
if (error) return callback(new Error('Error removing redis data:' + error));
if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error removing redis data: ${error.message}`));
rimraf(path.join(paths.LOG_DIR, `redis-${app.id}`), function (error) {
if (error) debugApp(app, 'cannot cleanup logs: %s', error);
@@ -1698,6 +1704,8 @@ function restoreRedis(app, options, callback) {
debugApp(app, 'Restoring redis');
callback = once(callback); // protect from multiple returns with streams
getServiceDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
if (error) return callback(error);
@@ -1708,11 +1716,11 @@ function restoreRedis(app, options, callback) {
} else { // old location of dumps
input = fs.createReadStream(path.join(paths.APPS_DATA_DIR, app.id, 'redis/dump.rdb'));
}
input.on('error', callback);
input.on('error', (error) => callback(new BoxError(BoxError.FS_ERROR, `Error reading input stream when restoring redis: ${error.message}`)));
const restoreReq = request.post(`https://${result.ip}:3000/restore?access_token=${result.token}`, { rejectUnauthorized: false }, function (error, response) {
if (error) return callback(error);
if (response.statusCode !== 200) return callback(new Error(`Unexpected response from redis addon: ${response.statusCode} message: ${response.body.message}`));
const restoreReq = request.post(`https://${result.ip}:3000/restore?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring redis: ${error.message}`));
if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring redis. Status code: ${response.statusCode} message: ${response.body.message}`));
callback(null);
});
@@ -1773,11 +1781,11 @@ function statusSftp(callback) {
assert.strictEqual(typeof callback, 'function');
docker.inspect('sftp', function (error, container) {
if (error && error.reason === DockerError.NOT_FOUND) return callback(new AddonsError(AddonsError.NOT_ACTIVE, error));
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, { status: exports.SERVICE_STATUS_STOPPED });
if (error) return callback(error);
docker.memoryUsage('sftp', function (error, result) {
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
if (error) return callback(error);
var tmp = {
status: container.State.Running ? exports.SERVICE_STATUS_ACTIVE : exports.SERVICE_STATUS_STOPPED,
@@ -1794,15 +1802,15 @@ function statusGraphite(callback) {
assert.strictEqual(typeof callback, 'function');
docker.inspect('graphite', function (error, container) {
if (error && error.reason === DockerError.NOT_FOUND) return callback(new AddonsError(AddonsError.NOT_ACTIVE, error));
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, { status: exports.SERVICE_STATUS_STOPPED });
if (error) return callback(error);
request.get('http://127.0.0.1:8417', { timeout: 3000 }, function (error, response) {
request.get('http://127.0.0.1:8417/graphite-web/dashboard', { json: true, timeout: 3000 }, function (error, response) {
if (error) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for graphite: ${error.message}` });
if (response.statusCode !== 200) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for graphite. Status code: ${response.statusCode} message: ${response.body.message}` });
docker.memoryUsage('graphite', function (error, result) {
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
if (error) return callback(error);
var tmp = {
status: container.State.Running ? exports.SERVICE_STATUS_ACTIVE : exports.SERVICE_STATUS_STOPPED,
+73 -166
View File
@@ -21,36 +21,9 @@ exports = module.exports = {
getAppIdByAddonConfigValue: getAppIdByAddonConfigValue,
setHealth: setHealth,
setInstallationCommand: setInstallationCommand,
setRunCommand: setRunCommand,
setTask: setTask,
getAppStoreIds: getAppStoreIds,
setOwner: setOwner,
transferOwnership: transferOwnership,
// installation codes (keep in sync in UI)
ISTATE_PENDING_INSTALL: 'pending_install', // installs and fresh reinstalls
ISTATE_PENDING_CLONE: 'pending_clone', // clone
ISTATE_PENDING_CONFIGURE: 'pending_configure', // config (location, port) changes and on infra update
ISTATE_PENDING_UNINSTALL: 'pending_uninstall', // uninstallation
ISTATE_PENDING_RESTORE: 'pending_restore', // restore to previous backup or on upgrade
ISTATE_PENDING_UPDATE: 'pending_update', // update from installed state preserving data
ISTATE_PENDING_FORCE_UPDATE: 'pending_force_update', // update from any state preserving data
ISTATE_PENDING_BACKUP: 'pending_backup', // backup the app
ISTATE_ERROR: 'error', // error executing last pending_* command
ISTATE_INSTALLED: 'installed', // app is installed
RSTATE_RUNNING: 'running',
RSTATE_PENDING_START: 'pending_start',
RSTATE_PENDING_STOP: 'pending_stop',
RSTATE_STOPPED: 'stopped', // app stopped by us
// run codes (keep in sync in UI)
HEALTH_HEALTHY: 'healthy',
HEALTH_UNHEALTHY: 'unhealthy',
HEALTH_ERROR: 'error',
HEALTH_DEAD: 'dead',
// subdomain table types
SUBDOMAIN_TYPE_PRIMARY: 'primary',
SUBDOMAIN_TYPE_REDIRECT: 'redirect',
@@ -60,17 +33,17 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
database = require('./database.js'),
DatabaseError = require('./databaseerror'),
safe = require('safetydance'),
util = require('util');
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.installationProgress', '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.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit',
'apps.label', 'apps.tagsJson',
'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.mailboxName', 'apps.enableAutomaticUpdate',
'apps.accessRestrictionJson', 'apps.memoryLimit',
'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson',
'apps.sso', 'apps.debugModeJson', 'apps.enableBackup',
'apps.creationTime', 'apps.updateTime', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableAutomaticUpdate',
'apps.dataDir', 'apps.ts', 'apps.healthTime' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(',');
@@ -84,22 +57,14 @@ function postProcess(result) {
result.manifest = safe.JSON.parse(result.manifestJson);
delete result.manifestJson;
assert(result.oldConfigJson === null || typeof result.oldConfigJson === 'string');
result.oldConfig = safe.JSON.parse(result.oldConfigJson);
delete result.oldConfigJson;
assert(result.updateConfigJson === null || typeof result.updateConfigJson === 'string');
result.updateConfig = safe.JSON.parse(result.updateConfigJson);
delete result.updateConfigJson;
assert(result.restoreConfigJson === null || typeof result.restoreConfigJson === 'string');
result.restoreConfig = safe.JSON.parse(result.restoreConfigJson);
delete result.restoreConfigJson;
assert(result.tagsJson === null || typeof result.tagsJson === 'string');
result.tags = safe.JSON.parse(result.tagsJson) || [];
delete result.tagsJson;
assert(result.reverseProxyConfigJson === null || typeof result.reverseProxyConfigJson === 'string');
result.reverseProxyConfig = safe.JSON.parse(result.reverseProxyConfigJson) || {};
delete result.reverseProxyConfigJson;
assert(result.hostPorts === null || typeof result.hostPorts === 'string');
assert(result.environmentVariables === null || typeof result.environmentVariables === 'string');
@@ -143,8 +108,10 @@ function postProcess(result) {
if (envNames[i]) result.env[envNames[i]] = envValues[i];
}
// in the db, we store dataDir as unique/nullable
result.dataDir = result.dataDir || '';
result.error = safe.JSON.parse(result.errorJson);
delete result.errorJson;
result.taskId = result.taskId ? String(result.taskId) : null;
}
function get(id, callback) {
@@ -159,11 +126,11 @@ function get(id, callback) {
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' WHERE apps.id = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, id ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
result[0].alternateDomains = alternateDomains;
@@ -186,11 +153,11 @@ function getByHttpPort(httpPort, callback) {
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' WHERE httpPort = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, httpPort ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
result[0].alternateDomains = alternateDomains;
postProcess(result[0]);
@@ -212,11 +179,11 @@ function getByContainerId(containerId, callback) {
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' WHERE containerId = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, containerId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
result[0].alternateDomains = alternateDomains;
postProcess(result[0]);
@@ -237,10 +204,10 @@ function getAll(callback) {
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' GROUP BY apps.id ORDER BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE type = ?', [ exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
alternateDomains.forEach(function (d) {
var domain = results.find(function (a) { return d.appId === a.id; });
@@ -257,14 +224,13 @@ function getAll(callback) {
});
}
function add(id, appStoreId, manifest, location, domain, ownerId, portBindings, data, callback) {
function add(id, appStoreId, manifest, location, domain, portBindings, data, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof appStoreId, 'string');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof manifest.version, 'string');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof portBindings, 'object');
assert(data && typeof data === 'object');
assert.strictEqual(typeof callback, 'function');
@@ -276,24 +242,25 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
const accessRestriction = data.accessRestriction || null;
const accessRestrictionJson = JSON.stringify(accessRestriction);
const memoryLimit = data.memoryLimit || 0;
const installationState = data.installationState || exports.ISTATE_PENDING_INSTALL;
const restoreConfigJson = data.restoreConfig ? JSON.stringify(data.restoreConfig) : null; // used when cloning
const installationState = data.installationState;
const runState = data.runState;
const sso = 'sso' in data ? data.sso : null;
const robotsTxt = 'robotsTxt' in data ? data.robotsTxt : null;
const debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null;
const env = data.env || {};
const label = data.label || null;
const tagsJson = data.tags ? JSON.stringify(data.tags) : null;
const mailboxName = data.mailboxName || null;
const mailboxDomain = data.mailboxDomain || null;
const reverseProxyConfigJson = data.reverseProxyConfig ? JSON.stringify(data.reverseProxyConfig) : null;
var queries = [];
queries.push({
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, '
+ 'restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId, mailboxName, label, tagsJson) '
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, '
+ 'sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson) '
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, restoreConfigJson,
sso, debugModeJson, robotsTxt, ownerId, mailboxName, label, tagsJson ]
args: [ id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit,
sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson ]
});
queries.push({
@@ -325,9 +292,9 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
}
database.transaction(queries, function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error.message));
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'no such domain'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, error.message));
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new BoxError(BoxError.NOT_FOUND, 'no such domain'));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
@@ -338,7 +305,7 @@ function exists(id, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT 1 FROM apps WHERE id=?', [ id ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
return callback(null, result.length !== 0);
});
@@ -349,7 +316,7 @@ function getPortBindings(id, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + PORT_BINDINGS_FIELDS + ' FROM appPortBindings WHERE appId = ?', [ id ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
var portBindings = { };
for (var i = 0; i < results.length; i++) {
@@ -366,8 +333,8 @@ function delPortBinding(hostPort, type, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM appPortBindings WHERE hostPort=? AND type=?', [ hostPort, type ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
callback(null);
});
@@ -385,8 +352,8 @@ function del(id, callback) {
];
database.transaction(queries, function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results[3].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results[3].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
callback(null);
});
@@ -402,7 +369,7 @@ function clear(callback) {
database.query.bind(null, 'DELETE FROM appEnvVars'),
database.query.bind(null, 'DELETE FROM apps')
], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
return callback(null);
});
}
@@ -458,7 +425,7 @@ function updateWithConstraints(id, app, constraints, callback) {
var fields = [ ], values = [ ];
for (var p in app) {
if (p === 'manifest' || p === 'oldConfig' || p === 'updateConfig' || p === 'restoreConfig' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode') {
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig') {
fields.push(`${p}Json = ?`);
values.push(JSON.stringify(app[p]));
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'env') {
@@ -473,15 +440,14 @@ function updateWithConstraints(id, app, constraints, callback) {
}
database.transaction(queries, function (error, results) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error.message));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results[results.length - 1].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, error.message));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results[results.length - 1].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
return callback(null);
});
}
// not sure if health should influence runState
function setHealth(appId, health, healthTime, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof health, 'string');
@@ -490,60 +456,29 @@ function setHealth(appId, health, healthTime, callback) {
var values = { health, healthTime };
var constraints = 'AND runState NOT LIKE "pending_%" AND installationState = "installed"';
updateWithConstraints(appId, values, constraints, callback);
updateWithConstraints(appId, values, '', callback);
}
function setInstallationCommand(appId, installationState, values, callback) {
function setTask(appId, values, options, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof installationState, 'string');
if (typeof values === 'function') {
callback = values;
values = { };
} else {
assert.strictEqual(typeof values, 'object');
assert.strictEqual(typeof callback, 'function');
}
values.installationState = installationState;
values.installationProgress = '';
// Rules are:
// uninstall is allowed in any state
// force update is allowed in any state including pending_uninstall! (for better or worse)
// restore is allowed from installed or error state or currently restoring
// configure is allowed in installed state or currently configuring or in error state
// update and backup are allowed only in installed state
if (installationState === exports.ISTATE_PENDING_UNINSTALL || installationState === exports.ISTATE_PENDING_FORCE_UPDATE) {
updateWithConstraints(appId, values, '', callback);
} else if (installationState === exports.ISTATE_PENDING_RESTORE) {
updateWithConstraints(appId, values, 'AND (installationState = "installed" OR installationState = "error" OR installationState = "pending_restore")', callback);
} else if (installationState === exports.ISTATE_PENDING_UPDATE || installationState === exports.ISTATE_PENDING_BACKUP) {
updateWithConstraints(appId, values, 'AND installationState = "installed"', callback);
} else if (installationState === exports.ISTATE_PENDING_CONFIGURE) {
updateWithConstraints(appId, values, 'AND (installationState = "installed" OR installationState = "pending_configure" OR installationState = "error")', callback);
} else {
callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, 'invalid installationState'));
}
}
function setRunCommand(appId, runState, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof runState, 'string');
assert.strictEqual(typeof values, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var values = { runState: runState };
updateWithConstraints(appId, values, 'AND runState NOT LIKE "pending_%" AND installationState = "installed"', callback);
if (!options.requireNullTaskId) return updateWithConstraints(appId, values, '', callback);
if (options.requiredState === null) {
updateWithConstraints(appId, values, 'AND taskId IS NULL', callback);
} else {
updateWithConstraints(appId, values, `AND taskId IS NULL AND installationState = "${options.requiredState}"`, callback);
}
}
function getAppStoreIds(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT id, appStoreId FROM apps', function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, results);
});
@@ -568,7 +503,7 @@ function setAddonConfig(appId, addonId, env, callback) {
}
database.query(query + queryArgs.join(','), args, function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
return callback(null);
});
@@ -581,7 +516,7 @@ function unsetAddonConfig(appId, addonId, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM appAddonConfigs WHERE appId = ? AND addonId = ?', [ appId, addonId ], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
@@ -592,7 +527,7 @@ function unsetAddonConfigByAppId(appId, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM appAddonConfigs WHERE appId = ?', [ appId ], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
@@ -604,7 +539,7 @@ function getAddonConfig(appId, addonId, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT name, value FROM appAddonConfigs WHERE appId = ? AND addonId = ?', [ appId, addonId ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, results);
});
@@ -615,7 +550,7 @@ function getAddonConfigByAppId(appId, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT name, value FROM appAddonConfigs WHERE appId = ?', [ appId ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, results);
});
@@ -628,51 +563,23 @@ function getAppIdByAddonConfigValue(addonId, namePattern, value, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT appId FROM appAddonConfigs WHERE addonId = ? AND name LIKE ? AND value = ?', [ addonId, namePattern, value ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
callback(null, results[0].appId);
});
}
function getAddonConfigByName(appId, addonId, name, callback) {
function getAddonConfigByName(appId, addonId, namePattern, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof addonId, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof namePattern, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT value FROM appAddonConfigs WHERE appId = ? AND addonId = ? AND name = ?', [ appId, addonId, name ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
database.query('SELECT value FROM appAddonConfigs WHERE appId = ? AND addonId = ? AND name LIKE ?', [ appId, addonId, namePattern ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
callback(null, results[0].value);
});
}
function setOwner(appId, ownerId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('UPDATE apps SET ownerId=? WHERE appId=?', [ ownerId, appId ], function (error, results) {
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such user'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such app'));
callback(null);
});
}
function transferOwnership(oldOwnerId, newOwnerId, callback) {
assert.strictEqual(typeof oldOwnerId, 'string');
assert.strictEqual(typeof newOwnerId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('UPDATE apps SET ownerId=? WHERE ownerId=?', [ newOwnerId, oldOwnerId ], function (error) {
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such user'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
+18 -17
View File
@@ -5,7 +5,7 @@ var appdb = require('./appdb.js'),
assert = require('assert'),
async = require('async'),
auditSource = require('./auditsource.js'),
DatabaseError = require('./databaseerror.js'),
BoxError = require('./boxerror.js'),
debug = require('debug')('box:apphealthmonitor'),
docker = require('./docker.js'),
eventlog = require('./eventlog.js'),
@@ -26,7 +26,7 @@ let gLastOomMailTime = Date.now() - (5 * 60 * 1000); // pretend we sent email 5
function debugApp(app) {
assert(typeof app === 'object');
debug(app.fqdn + ' ' + app.manifest.id + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)) + ' - ' + app.id);
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)) + ' - ' + app.id);
}
function setHealth(app, health, callback) {
@@ -36,16 +36,16 @@ function setHealth(app, health, callback) {
let now = new Date(), healthTime = app.healthTime, curHealth = app.health;
if (health === appdb.HEALTH_HEALTHY) {
if (health === apps.HEALTH_HEALTHY) {
healthTime = now;
if (curHealth && curHealth !== appdb.HEALTH_HEALTHY) { // app starts out with null health
if (curHealth && curHealth !== apps.HEALTH_HEALTHY) { // app starts out with null health
debugApp(app, 'app switched from %s to healthy', curHealth);
// do not send mails for dev apps
if (!app.debugMode) eventlog.add(eventlog.ACTION_APP_UP, auditSource.HEALTH_MONITOR, { app: app });
}
} else if (Math.abs(now - healthTime) > UNHEALTHY_THRESHOLD) {
if (curHealth === appdb.HEALTH_HEALTHY) {
if (curHealth === apps.HEALTH_HEALTHY) {
debugApp(app, 'marking as unhealthy since not seen for more than %s minutes', UNHEALTHY_THRESHOLD/(60 * 1000));
// do not send mails for dev apps
@@ -57,7 +57,7 @@ function setHealth(app, health, callback) {
}
appdb.setHealth(app.id, health, healthTime, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null); // app uninstalled?
if (error && error.reason === BoxError.NOT_FOUND) return callback(null); // app uninstalled?
if (error) return callback(error);
app.health = health;
@@ -72,7 +72,7 @@ function checkAppHealth(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.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);
}
@@ -82,34 +82,34 @@ function checkAppHealth(app, callback) {
docker.inspect(app.containerId, function (error, data) {
if (error || !data || !data.State) {
debugApp(app, 'Error inspecting container');
return setHealth(app, appdb.HEALTH_ERROR, callback);
return setHealth(app, apps.HEALTH_ERROR, callback);
}
if (data.State.Running !== true) {
debugApp(app, 'exited');
return setHealth(app, appdb.HEALTH_DEAD, callback);
return setHealth(app, apps.HEALTH_DEAD, callback);
}
// non-appstore apps may not have healthCheckPath
if (!manifest.healthCheckPath) return setHealth(app, appdb.HEALTH_HEALTHY, callback);
if (!manifest.healthCheckPath) return setHealth(app, apps.HEALTH_HEALTHY, callback);
// poll through docker network instead of nginx to bypass any potential oauth proxy
var healthCheckUrl = 'http://127.0.0.1:' + app.httpPort + manifest.healthCheckPath;
superagent
.get(healthCheckUrl)
.set('Host', app.fqdn) // required for some apache configs with rewrite rules
.set('User-Agent', 'Mozilla') // required for some apps (e.g. minio)
.set('User-Agent', 'Mozilla (CloudronHealth)') // required for some apps (e.g. minio)
.redirects(0)
.timeout(HEALTHCHECK_INTERVAL)
.end(function (error, res) {
if (error && !error.response) {
debugApp(app, 'not alive (network error): %s', error.message);
setHealth(app, appdb.HEALTH_UNHEALTHY, callback);
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
} else if (res.statusCode >= 400) { // 2xx and 3xx are ok
debugApp(app, 'not alive : %s', error || res.status);
setHealth(app, appdb.HEALTH_UNHEALTHY, callback);
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
} else {
setHealth(app, appdb.HEALTH_HEALTHY, callback);
setHealth(app, apps.HEALTH_HEALTHY, callback);
}
});
});
@@ -186,9 +186,10 @@ function processApp(callback) {
async.each(result, checkAppHealth, function (error) {
if (error) console.error(error);
var alive = result
.filter(function (a) { return a.installationState === appdb.ISTATE_INSTALLED && a.runState === appdb.RSTATE_RUNNING && a.health === appdb.HEALTH_HEALTHY; })
.map(function (a) { return (a.location || 'naked_domain') + '|' + a.manifest.id; }).join(', ');
const alive = result
.filter(function (a) { return a.installationState === apps.ISTATE_INSTALLED && a.runState === apps.RSTATE_RUNNING && a.health === apps.HEALTH_HEALTHY; })
.map(a => a.fqdn)
.join(', ');
debug('apps alive: [%s]', alive);
+1011 -502
View File
File diff suppressed because it is too large Load Diff
+159 -132
View File
@@ -5,6 +5,9 @@ exports = module.exports = {
getApp: getApp,
getAppVersion: getAppVersion,
trackBeginSetup: trackBeginSetup,
trackFinishedSetup: trackFinishedSetup,
registerWithLoginCredentials: registerWithLoginCredentials,
registerWithLicense: registerWithLicense,
@@ -19,65 +22,36 @@ exports = module.exports = {
getAppUpdate: getAppUpdate,
getBoxUpdate: getBoxUpdate,
createTicket: createTicket,
AppstoreError: AppstoreError
createTicket: createTicket
};
var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
config = require('./config.js'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
custom = require('./custom.js'),
debug = require('debug')('box:appstore'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
groups = require('./groups.js'),
mail = require('./mail.js'),
os = require('os'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
superagent = require('superagent'),
users = require('./users.js'),
util = require('util');
function AppstoreError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(AppstoreError, Error);
AppstoreError.INTERNAL_ERROR = 'Internal Error';
AppstoreError.EXTERNAL_ERROR = 'External Error';
AppstoreError.ALREADY_EXISTS = 'Already Exists';
AppstoreError.ACCESS_DENIED = 'Access Denied';
AppstoreError.NOT_FOUND = 'Not Found';
AppstoreError.PLAN_LIMIT = 'Plan limit reached'; // upstream 402 (subsciption_expired and subscription_required)
AppstoreError.LICENSE_ERROR = 'License Error'; // upstream 422 (no license, invalid license)
AppstoreError.INVALID_TOKEN = 'Invalid token'; // upstream 401 (invalid token)
AppstoreError.NOT_REGISTERED = 'Not registered'; // upstream 412 (no token, not set yet)
AppstoreError.ALREADY_REGISTERED = 'Already registered';
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
function getCloudronToken(callback) {
assert.strictEqual(typeof callback, 'function');
settings.getCloudronToken(function (error, token) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
if (!token) return callback(new AppstoreError(AppstoreError.NOT_REGISTERED));
if (error) return callback(error);
if (!token) return callback(new BoxError(BoxError.LICENSE_ERROR, 'Missing token'));
callback(null, token);
});
@@ -95,11 +69,11 @@ function login(email, password, totpToken, callback) {
totpToken: totpToken
};
const url = config.apiServerOrigin() + '/api/v1/login';
const url = settings.apiServerOrigin() + '/api/v1/login';
superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.ACCESS_DENIED));
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, `login status code: ${result.statusCode}`));
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `login status code: ${result.statusCode}`));
callback(null, result.body); // { userId, accessToken }
});
@@ -115,11 +89,11 @@ function registerUser(email, password, callback) {
password: password,
};
const url = config.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) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode === 409) return callback(new AppstoreError(AppstoreError.ALREADY_EXISTS));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, `register status code: ${result.statusCode}`));
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 !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `register status code: ${result.statusCode}`));
callback(null);
});
@@ -131,13 +105,13 @@ function getSubscription(callback) {
getCloudronToken(function (error, token) {
if (error) return callback(error);
const url = config.apiServerOrigin() + '/api/v1/subscription';
const url = settings.apiServerOrigin() + '/api/v1/subscription';
superagent.get(url).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR));
if (result.statusCode === 502) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, `Stripe error: ${error.message}`));
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, `Unknown 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 === 422) return callback(new BoxError(BoxError.LICENSE_ERROR));
if (result.statusCode === 502) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Stripe error: ${error.message}`));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Unknown error: ${error.message}`));
callback(null, result.body); // { email, subscription }
});
@@ -158,16 +132,16 @@ function purchaseApp(data, callback) {
getCloudronToken(function (error, token) {
if (error) return callback(error);
const url = `${config.apiServerOrigin()}/api/v1/cloudronapps`;
const url = `${settings.apiServerOrigin()}/api/v1/cloudronapps`;
superagent.post(url).send(data).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND)); // appstoreId does not exist
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
if (result.statusCode === 402) return callback(new AppstoreError(AppstoreError.PLAN_LIMIT, result.body.message));
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR, result.body.message));
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND)); // appstoreId does not exist
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 402) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
// 200 if already purchased, 201 is newly purchased
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
callback(null);
});
@@ -183,19 +157,19 @@ function unpurchaseApp(appId, data, callback) {
getCloudronToken(function (error, token) {
if (error) return callback(error);
const url = `${config.apiServerOrigin()}/api/v1/cloudronapps/${appId}`;
const url = `${settings.apiServerOrigin()}/api/v1/cloudronapps/${appId}`;
superagent.get(url).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 404) return callback(null); // was never purchased
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR, result.body.message));
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
superagent.del(url).send(data).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
if (result.statusCode !== 204) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error));
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode !== 204) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
callback(null);
});
@@ -206,36 +180,50 @@ function unpurchaseApp(appId, data, callback) {
function sendAliveStatus(callback) {
callback = callback || NOOP_CALLBACK;
var allSettings, allDomains, mailDomains, loginEvents;
let allSettings, allDomains, mailDomains, loginEvents, userCount, groupCount;
async.series([
function (callback) {
settings.getAll(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
if (error) return callback(error);
allSettings = result;
callback();
});
},
function (callback) {
domains.getAll(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
if (error) return callback(error);
allDomains = result;
callback();
});
},
function (callback) {
mail.getDomains(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
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(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
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);
@@ -255,15 +243,18 @@ function sendAliveStatus(callback) {
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: config.version(),
adminFqdn: config.adminFqdn(),
provider: config.provider(),
version: constants.VERSION,
adminFqdn: settings.adminFqdn(),
provider: settings.provider(),
backendSettings: backendSettings,
machine: {
cpus: os.cpus(),
@@ -277,13 +268,13 @@ function sendAliveStatus(callback) {
getCloudronToken(function (error, token) {
if (error) return callback(error);
const url = `${config.apiServerOrigin()}/api/v1/alive`;
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 AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR, result.body.message));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Sending alive status failed. %s %j', result.status, result.body)));
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);
});
@@ -297,28 +288,28 @@ function getBoxUpdate(callback) {
getCloudronToken(function (error, token) {
if (error) return callback(error);
const url = `${config.apiServerOrigin()}/api/v1/boxupdate`;
const url = `${settings.apiServerOrigin()}/api/v1/boxupdate`;
superagent.get(url).query({ accessToken: token, boxVersion: config.version() }).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR, result.body.message));
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION }).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
if (result.statusCode === 204) return callback(null); // no update
if (result.statusCode !== 200 || !result.body) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
if (result.statusCode !== 200 || !result.body) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
var updateInfo = result.body;
if (!semver.valid(updateInfo.version) || semver.gt(config.version(), updateInfo.version)) {
return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Invalid update version: %s %s', result.statusCode, result.text)));
if (!semver.valid(updateInfo.version) || semver.gt(constants.VERSION, updateInfo.version)) {
return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Invalid update version: %s %s', result.statusCode, result.text)));
}
// updateInfo: { version, changelog, sourceTarballUrl, sourceTarballSigUrl, boxVersionsUrl, boxVersionsSigUrl }
if (!updateInfo.version || typeof updateInfo.version !== 'string') return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response (bad version): %s %s', result.statusCode, result.text)));
if (!updateInfo.changelog || !Array.isArray(updateInfo.changelog)) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response (bad version): %s %s', result.statusCode, result.text)));
if (!updateInfo.sourceTarballUrl || typeof updateInfo.sourceTarballUrl !== 'string') return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response (bad sourceTarballUrl): %s %s', result.statusCode, result.text)));
if (!updateInfo.sourceTarballSigUrl || typeof updateInfo.sourceTarballSigUrl !== 'string') return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response (bad sourceTarballSigUrl): %s %s', result.statusCode, result.text)));
if (!updateInfo.boxVersionsUrl || typeof updateInfo.boxVersionsUrl !== 'string') return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response (bad boxVersionsUrl): %s %s', result.statusCode, result.text)));
if (!updateInfo.boxVersionsSigUrl || typeof updateInfo.boxVersionsSigUrl !== 'string') return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response (bad boxVersionsSigUrl): %s %s', result.statusCode, result.text)));
if (!updateInfo.version || typeof updateInfo.version !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad version): %s %s', result.statusCode, result.text)));
if (!updateInfo.changelog || !Array.isArray(updateInfo.changelog)) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad version): %s %s', result.statusCode, result.text)));
if (!updateInfo.sourceTarballUrl || typeof updateInfo.sourceTarballUrl !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad sourceTarballUrl): %s %s', result.statusCode, result.text)));
if (!updateInfo.sourceTarballSigUrl || typeof updateInfo.sourceTarballSigUrl !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad sourceTarballSigUrl): %s %s', result.statusCode, result.text)));
if (!updateInfo.boxVersionsUrl || typeof updateInfo.boxVersionsUrl !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad boxVersionsUrl): %s %s', result.statusCode, result.text)));
if (!updateInfo.boxVersionsSigUrl || typeof updateInfo.boxVersionsSigUrl !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad boxVersionsSigUrl): %s %s', result.statusCode, result.text)));
callback(null, updateInfo);
});
@@ -332,14 +323,14 @@ function getAppUpdate(app, callback) {
getCloudronToken(function (error, token) {
if (error) return callback(error);
const url = `${config.apiServerOrigin()}/api/v1/appupdate`;
const url = `${settings.apiServerOrigin()}/api/v1/appupdate`;
superagent.get(url).query({ accessToken: token, boxVersion: config.version(), appId: app.appStoreId, appVersion: app.manifest.version }).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR, result.body.message));
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION, appId: app.appStoreId, appVersion: app.manifest.version }).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error));
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
if (result.statusCode === 204) return callback(null); // no update
if (result.statusCode !== 200 || !result.body) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
if (result.statusCode !== 200 || !result.body) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
const updateInfo = result.body;
@@ -349,7 +340,7 @@ function getAppUpdate(app, callback) {
// do some sanity checks
if (!safe.query(updateInfo, 'manifest.version') || semver.gt(curAppVersion, safe.query(updateInfo, 'manifest.version'))) {
debug('Skipping malformed update of app %s version: %s. got %j', app.id, curAppVersion, updateInfo);
return callback(new AppstoreError(AppstoreError.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 }
@@ -362,23 +353,23 @@ function registerCloudron(data, callback) {
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
const url = `${config.apiServerOrigin()}/api/v1/register_cloudron`;
const url = `${settings.apiServerOrigin()}/api/v1/register_cloudron`;
superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, `Unable to register cloudron: ${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}`));
// cloudronId, token, licenseKey
if (!result.body.cloudronId) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, 'Invalid response - no cloudron id'));
if (!result.body.cloudronToken) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, 'Invalid response - no token'));
if (!result.body.licenseKey) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, 'Invalid response - no license'));
if (!result.body.cloudronId) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no cloudron id'));
if (!result.body.cloudronToken) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no token'));
if (!result.body.licenseKey) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no license'));
async.series([
settings.setCloudronId.bind(null, result.body.cloudronId),
settings.setCloudronToken.bind(null, result.body.cloudronToken),
settings.setLicenseKey.bind(null, result.body.licenseKey),
], function (error) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
if (error) return callback(error);
debug(`registerCloudron: Cloudron registered with id ${result.body.cloudronId}`);
@@ -387,15 +378,47 @@ function registerCloudron(data, callback) {
});
}
// This works without a Cloudron token as this Cloudron was not yet registered
let gBeginSetupAlreadyTracked = false;
function trackBeginSetup(provider) {
assert.strictEqual(typeof provider, 'string');
// avoid browser reload double tracking, not perfect since box might restart, but covers most cases and is simple
if (gBeginSetupAlreadyTracked) return;
gBeginSetupAlreadyTracked = true;
const url = `${settings.apiServerOrigin()}/api/v1/helper/setup_begin`;
superagent.post(url).send({ provider }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return console.error(error.message);
if (result.statusCode !== 200) return console.error(error.message);
});
}
// This works without a Cloudron token as this Cloudron was not yet registered
function trackFinishedSetup(domain) {
assert.strictEqual(typeof domain, 'string');
const url = `${settings.apiServerOrigin()}/api/v1/helper/setup_finished`;
superagent.post(url).send({ domain }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return console.error(error.message);
if (result.statusCode !== 200) return console.error(error.message);
});
}
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 AppstoreError(AppstoreError.ALREADY_REGISTERED));
if (token) return callback(new BoxError(BoxError.CONFLICT));
registerCloudron({ license, domain }, callback);
const provider = settings.provider();
const version = constants.VERSION;
registerCloudron({ license, domain, provider, version }, callback);
});
}
@@ -410,7 +433,7 @@ function registerWithLoginCredentials(options, callback) {
}
getCloudronToken(function (error, token) {
if (token) return callback(new AppstoreError(AppstoreError.ALREADY_REGISTERED));
if (token) return callback(new BoxError(BoxError.CONFLICT));
maybeSignup(function (error) {
if (error) return callback(error);
@@ -418,19 +441,20 @@ function registerWithLoginCredentials(options, callback) {
login(options.email, options.password, options.totpToken || '', function (error, result) {
if (error) return callback(error);
registerCloudron({ domain: config.adminDomain(), accessToken: result.accessToken }, callback);
registerCloudron({ domain: settings.adminDomain(), accessToken: result.accessToken, provider: settings.provider(), version: constants.VERSION }, callback);
});
});
});
}
function createTicket(info, callback) {
function createTicket(info, auditSource, callback) {
assert.strictEqual(typeof info, 'object');
assert.strictEqual(typeof info.email, 'string');
assert.strictEqual(typeof info.displayName, 'string');
assert.strictEqual(typeof info.type, 'string');
assert.strictEqual(typeof info.subject, 'string');
assert.strictEqual(typeof info.description, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
function collectAppInfoIfNeeded(callback) {
@@ -445,15 +469,17 @@ function createTicket(info, callback) {
if (error) console.error('Unable to get app info', error);
if (result) info.app = result;
let url = config.apiServerOrigin() + '/api/v1/ticket';
let url = settings.apiServerOrigin() + '/api/v1/ticket';
info.supportEmail = custom.spec().support.email; // destination address for tickets
superagent.post(url).query({ accessToken: token }).send(info).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR, result.body.message));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
eventlog.add(eventlog.ACTION_SUPPORT_TICKET, auditSource, info);
callback(null);
});
@@ -468,14 +494,15 @@ function getApps(callback) {
if (error) return callback(error);
settings.getUnstableAppsConfig(function (error, unstable) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
const url = `${config.apiServerOrigin()}/api/v1/apps`;
superagent.get(url).query({ accessToken: token, boxVersion: config.version(), unstable: unstable }).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR, result.body.message));
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App listing failed. %s %j', result.status, result.body)));
if (!result.body.apps) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
if (error) return callback(error);
const url = `${settings.apiServerOrigin()}/api/v1/apps`;
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION, unstable: unstable }).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App listing failed. %s %j', result.status, result.body)));
if (!result.body.apps) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
callback(null, result.body.apps);
});
@@ -491,15 +518,15 @@ function getAppVersion(appId, version, callback) {
getCloudronToken(function (error, token) {
if (error) return callback(error);
let url = `${config.apiServerOrigin()}/api/v1/apps/${appId}`;
let url = `${settings.apiServerOrigin()}/api/v1/apps/${appId}`;
if (version !== 'latest') url += `/versions/${version}`;
superagent.get(url).query({ accessToken: token }).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR, result.body.message));
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App fetch failed. %s %j', result.status, result.body)));
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App fetch failed. %s %j', result.status, result.body)));
callback(null, result.body);
});
+559 -458
View File
File diff suppressed because it is too large Load Diff
+87
View File
@@ -0,0 +1,87 @@
'use strict';
exports = module.exports = {
scheduleTask: scheduleTask
};
let assert = require('assert'),
BoxError = require('./boxerror.js'),
debug = require('debug')('box:apptaskmanager'),
fs = require('fs'),
locker = require('./locker.js'),
safe = require('safetydance'),
path = require('path'),
paths = require('./paths.js'),
tasks = require('./tasks.js');
let gActiveTasks = { }; // indexed by app id
let gPendingTasks = [ ];
let gInitialized = false;
const TASK_CONCURRENCY = 3;
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
function waitText(lockOperation) {
if (lockOperation === locker.OP_BOX_UPDATE) return 'Waiting for Cloudron to finish updating. See the Settings view';
if (lockOperation === locker.OP_PLATFORM_START) return 'Waiting for Cloudron to initialize';
if (lockOperation === locker.OP_FULL_BACKUP) return 'Wait for Cloudron to finish backup. See the Backups view';
return ''; // cannot happen
}
function initializeSync() {
gInitialized = true;
locker.on('unlocked', startNextTask);
}
// callback is called when task is finished
function scheduleTask(appId, taskId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof taskId, 'string');
assert.strictEqual(typeof callback, 'function');
if (!gInitialized) initializeSync();
if (appId in gActiveTasks) {
return callback(new BoxError(BoxError.CONFLICT, `Task for %s is already active: ${appId}`));
}
if (Object.keys(gActiveTasks).length >= TASK_CONCURRENCY) {
debug(`Reached concurrency limit, queueing task id ${taskId}`);
tasks.update(taskId, { percent: 1, message: 'Waiting for other app tasks to complete' }, NOOP_CALLBACK);
gPendingTasks.push({ appId, taskId, callback });
return;
}
var lockError = locker.recursiveLock(locker.OP_APPTASK);
if (lockError) {
debug(`Could not get lock. ${lockError.message}, queueing task id ${taskId}`);
tasks.update(taskId, { percent: 1, message: waitText(lockError.operation) }, NOOP_CALLBACK);
gPendingTasks.push({ appId, taskId, callback });
return;
}
gActiveTasks[appId] = {};
const logFile = path.join(paths.LOG_DIR, appId, 'apptask.log');
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) {
callback(error, result);
delete gActiveTasks[appId];
locker.unlock(locker.OP_APPTASK); // unlock event will trigger next task
});
}
function startNextTask() {
if (gPendingTasks.length === 0) return;
assert(Object.keys(gActiveTasks).length < TASK_CONCURRENCY);
const t = gPendingTasks.shift();
scheduleTask(t.appId, t.taskId, t.callback);
}
+2 -2
View File
@@ -3,9 +3,9 @@
exports = module.exports = {
CRON: { userId: null, username: 'cron' },
HEALTH_MONITOR: { userId: null, username: 'healthmonitor' },
SYSADMIN: { userId: null, username: 'sysadmin' },
TASK_MANAGER: { userId: null, username: 'taskmanager' },
APP_TASK: { userId: null, username: 'apptask' },
EXTERNAL_LDAP_TASK: { userId: null, username: 'externalldap' },
EXTERNAL_LDAP_AUTO_CREATE: { userId: null, username: 'externalldap' },
fromRequest: fromRequest
};
+13 -13
View File
@@ -12,8 +12,8 @@ exports = module.exports = {
};
var assert = require('assert'),
database = require('./database.js'),
DatabaseError = require('./databaseerror');
BoxError = require('./boxerror.js'),
database = require('./database.js');
var AUTHCODES_FIELDS = [ 'authCode', 'userId', 'clientId', 'expiresAt' ].join(',');
@@ -22,8 +22,8 @@ function get(authCode, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + AUTHCODES_FIELDS + ' FROM authcodes WHERE authCode = ? AND expiresAt > ?', [ authCode, Date.now() ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Authcode not found'));
callback(null, result[0]);
});
@@ -37,12 +37,12 @@ function add(authCode, clientId, userId, expiresAt, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO authcodes (authCode, clientId, userId, expiresAt) VALUES (?, ?, ?, ?)',
[ authCode, clientId, userId, expiresAt ], function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS));
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
[ authCode, clientId, userId, expiresAt ], function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS));
if (error || result.affectedRows !== 1) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
callback(null);
});
}
function del(authCode, callback) {
@@ -50,8 +50,8 @@ function del(authCode, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM authcodes WHERE authCode = ?', [ authCode ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Authcode not found'));
callback(null);
});
@@ -61,7 +61,7 @@ function delExpired(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM authcodes WHERE expiresAt <= ?', [ Date.now() ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
return callback(null, result.affectedRows);
});
}
@@ -70,7 +70,7 @@ function clear(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM authcodes', function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
+12 -12
View File
@@ -1,8 +1,8 @@
'use strict';
var assert = require('assert'),
BoxError = require('./boxerror.js'),
database = require('./database.js'),
DatabaseError = require('./databaseerror.js'),
safe = require('safetydance'),
util = require('util');
@@ -47,7 +47,7 @@ function getByTypeAndStatePaged(type, state, page, perPage, callback) {
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? AND state = ? ORDER BY creationTime DESC LIMIT ?,?',
[ type, state, (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(function (result) { postProcess(result); });
@@ -63,7 +63,7 @@ function getByTypePaged(type, page, perPage, callback) {
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? ORDER BY creationTime DESC LIMIT ?,?',
[ type, (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(function (result) { postProcess(result); });
@@ -80,7 +80,7 @@ function getByAppIdPaged(page, perPage, appId, callback) {
// box versions (0.93.x and below) used to use appbackup_ prefix
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? AND state = ? AND id LIKE ? ORDER BY creationTime DESC LIMIT ?,?',
[ exports.BACKUP_TYPE_APP, exports.BACKUP_STATE_NORMAL, '%app%\\_' + appId + '\\_%', (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(function (result) { postProcess(result); });
@@ -94,8 +94,8 @@ function get(id, callback) {
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE id = ? ORDER BY creationTime DESC',
[ id ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Backup not found'));
postProcess(result[0]);
@@ -119,8 +119,8 @@ function add(id, data, callback) {
database.query('INSERT INTO backups (id, version, type, creationTime, state, dependsOn, manifestJson, format) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[ id, data.version, data.type, creationTime, exports.BACKUP_STATE_NORMAL, data.dependsOn.join(','), manifestJson, data.format ],
function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
@@ -139,8 +139,8 @@ function update(id, backup, callback) {
values.push(id);
database.query('UPDATE backups SET ' + fields.join(', ') + ' WHERE id = ?', values, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.NOT_FOUND, 'Backup not found'));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
@@ -150,7 +150,7 @@ function clear(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('TRUNCATE TABLE backups', [], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}
@@ -160,7 +160,7 @@ function del(id, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM backups WHERE id=?', [ id ], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}
+215 -162
View File
@@ -1,9 +1,8 @@
'use strict';
exports = module.exports = {
BackupsError: BackupsError,
testConfig: testConfig,
testProviderConfig: testProviderConfig,
getByStatePaged: getByStatePaged,
getByAppIdPaged: getByAppIdPaged,
@@ -16,7 +15,7 @@ exports = module.exports = {
restore: restore,
backupApp: backupApp,
restoreApp: restoreApp,
downloadApp: downloadApp,
backupBoxAndApps: backupBoxAndApps,
@@ -40,18 +39,17 @@ exports = module.exports = {
};
var addons = require('./addons.js'),
appdb = require('./appdb.js'),
apps = require('./apps.js'),
AppsError = require('./apps.js').AppsError,
async = require('async'),
assert = require('assert'),
backupdb = require('./backupdb.js'),
config = require('./config.js'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
crypto = require('crypto'),
database = require('./database.js'),
DatabaseError = require('./databaseerror.js'),
DataLayout = require('./datalayout.js'),
debug = require('debug')('box:backups'),
df = require('@sindresorhus/df'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
locker = require('./locker.js'),
@@ -60,6 +58,7 @@ var addons = require('./addons.js'),
path = require('path'),
paths = require('./paths.js'),
progressStream = require('progress-stream'),
prettyBytes = require('pretty-bytes'),
safe = require('safetydance'),
shell = require('./shell.js'),
settings = require('./settings.js'),
@@ -77,31 +76,6 @@ function debugApp(app) {
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function BackupsError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(BackupsError, Error);
BackupsError.EXTERNAL_ERROR = 'external error';
BackupsError.INTERNAL_ERROR = 'internal error';
BackupsError.BAD_STATE = 'bad state';
BackupsError.BAD_FIELD = 'bad field';
BackupsError.NOT_FOUND = 'not found';
// choose which storage backend we use for test purpose we use s3
function api(provider) {
switch (provider) {
@@ -112,6 +86,7 @@ function api(provider) {
case 's3-v4-compat': return require('./storage/s3.js');
case 'digitalocean-spaces': return require('./storage/s3.js');
case 'exoscale-sos': return require('./storage/s3.js');
case 'wasabi': return require('./storage/s3.js');
case 'scaleway-objectstorage': return require('./storage/s3.js');
case 'noop': return require('./storage/noop.js');
default: return null;
@@ -134,12 +109,23 @@ function testConfig(backupConfig, callback) {
assert.strictEqual(typeof callback, 'function');
var func = api(backupConfig.provider);
if (!func) return callback(new BackupsError(BackupsError.BAD_FIELD, 'unknown storage provider'));
if (!func) return callback(new BoxError(BoxError.BAD_FIELD, 'unknown storage provider', { field: 'provider' }));
if (backupConfig.format !== 'tgz' && backupConfig.format !== 'rsync') return callback(new BackupsError(BackupsError.BAD_FIELD, 'unknown format'));
if (backupConfig.format !== 'tgz' && backupConfig.format !== 'rsync') return callback(new BoxError(BoxError.BAD_FIELD, 'unknown format', { field: 'format' }));
// remember to adjust the cron ensureBackup task interval accordingly
if (backupConfig.intervalSecs < 6 * 60 * 60) return callback(new BackupsError(BackupsError.BAD_FIELD, 'Interval must be atleast 6 hours'));
if (backupConfig.intervalSecs < 6 * 60 * 60) return callback(new BoxError(BoxError.BAD_FIELD, 'Interval must be atleast 6 hours', { field: 'interval' }));
api(backupConfig.provider).testConfig(backupConfig, callback);
}
function testProviderConfig(backupConfig, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof callback, 'function');
var func = api(backupConfig.provider);
if (!func) return callback(new BoxError(BoxError.BAD_FIELD, 'unknown storage provider', { field: 'provider' }));
api(backupConfig.provider).testConfig(backupConfig, callback);
}
@@ -151,7 +137,7 @@ function getByStatePaged(state, page, perPage, callback) {
assert.strictEqual(typeof callback, 'function');
backupdb.getByTypeAndStatePaged(backupdb.BACKUP_TYPE_BOX, state, page, perPage, function (error, results) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback(null, results);
});
@@ -164,7 +150,7 @@ function getByAppIdPaged(page, perPage, appId, callback) {
assert.strictEqual(typeof callback, 'function');
backupdb.getByAppIdPaged(page, perPage, appId, function (error, results) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback(null, results);
});
@@ -175,8 +161,7 @@ function get(backupId, callback) {
assert.strictEqual(typeof callback, 'function');
backupdb.get(backupId, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new BackupsError(BackupsError.NOT_FOUND));
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback(null, result);
});
@@ -244,14 +229,14 @@ function createReadStream(sourceFile, key) {
stream.on('error', function (error) {
debug('createReadStream: read stream error.', error);
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
ps.emit('error', new BoxError(BoxError.FS_ERROR, error.message));
});
if (key !== null) {
var encrypt = crypto.createCipher('aes-256-cbc', key);
encrypt.on('error', function (error) {
debug('createReadStream: encrypt stream error.', error);
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
ps.emit('error', new BoxError(BoxError.CRYPTO_ERROR, error.message));
});
return stream.pipe(encrypt).pipe(ps);
} else {
@@ -264,17 +249,25 @@ function createWriteStream(destFile, key) {
assert(key === null || typeof key === 'string');
var stream = fs.createWriteStream(destFile);
var ps = progressStream({ time: 10000 }); // display a progress every 10 seconds
stream.on('error', function (error) {
debug('createWriteStream: write stream error.', error);
ps.emit('error', new BoxError(BoxError.FS_ERROR, error.message));
});
if (key !== null) {
var decrypt = crypto.createDecipher('aes-256-cbc', key);
decrypt.on('error', function (error) {
debug('createWriteStream: decrypt stream error.', error);
ps.emit('error', new BoxError(BoxError.CRYPTO_ERROR, error.message));
});
decrypt.pipe(stream);
return decrypt;
ps.pipe(decrypt).pipe(stream);
} else {
return stream;
ps.pipe(stream);
}
return ps;
}
function tarPack(dataLayout, key, callback) {
@@ -291,6 +284,9 @@ function tarPack(dataLayout, key, callback) {
},
map: function(header) {
header.name = dataLayout.toRemotePath(header.name);
// the tar pax format allows us to encode filenames > 100 and size > 8GB (see #640)
// https://www.systutorials.com/docs/linux/man/5-star/
if (header.size > 8589934590 || header.name > 99) header.pax = { size: header.size };
return header;
},
strict: false // do not error for unknown types (skip fifo, char/block devices)
@@ -301,19 +297,19 @@ function tarPack(dataLayout, key, callback) {
pack.on('error', function (error) {
debug('tarPack: tar stream error.', error);
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
ps.emit('error', new BoxError(BoxError.EXTERNAL_ERROR, error.message));
});
gzip.on('error', function (error) {
debug('tarPack: gzip stream error.', error);
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
ps.emit('error', new BoxError(BoxError.EXTERNAL_ERROR, error.message));
});
if (key !== null) {
var encrypt = crypto.createCipher('aes-256-cbc', key);
encrypt.on('error', function (error) {
debug('tarPack: encrypt stream error.', error);
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
ps.emit('error', new BoxError(BoxError.EXTERNAL_ERROR, error.message));
});
pack.pipe(gzip).pipe(encrypt).pipe(ps);
} else {
@@ -362,7 +358,7 @@ function sync(backupConfig, backupId, dataLayout, progressCallback, callback) {
debug(`read stream error for ${task.path}: ${error.message}`);
retryCallback();
}); // ignore error if file disappears
stream.on('progress', function(progress) {
stream.on('progress', function (progress) {
const transferred = Math.round(progress.transferred/1024/1024), speed = Math.round(progress.speed/1024/1024);
if (!transferred && !speed) return progressCallback({ message: `Uploading ${task.path}` }); // 0M@0Mbps looks wrong
progressCallback({ message: `Uploading ${task.path}: ${transferred}M@${speed}Mbps` }); // 0M@0Mbps looks wrong
@@ -374,7 +370,7 @@ function sync(backupConfig, backupId, dataLayout, progressCallback, callback) {
}
}, iteratorCallback);
}, concurrency, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
callback();
});
@@ -408,6 +404,34 @@ function saveFsMetadata(dataLayout, metadataFile, callback) {
callback();
}
// the du call in the function below requires root
function checkFreeDiskSpace(backupConfig, dataLayout, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert.strictEqual(typeof callback, 'function');
if (backupConfig.provider !== 'filesystem') return callback();
let used = 0;
for (let localPath of dataLayout.localPaths()) {
debug(`checkFreeDiskSpace: getting disk usage of ${localPath}`);
let result = safe.child_process.execSync(`du -Dsb ${localPath}`, { encoding: 'utf8' });
if (!result) return callback(new BoxError(BoxError.FS_ERROR, safe.error));
used += parseInt(result, 10);
}
debug(`checkFreeDiskSpace: ${used} bytes`);
df.file(backupConfig.backupFolder).then(function (diskUsage) {
const needed = used + (1024 * 1024 * 1024); // check if there is atleast 1GB left afterwards
if (diskUsage.available <= needed) return callback(new BoxError(BoxError.FS_ERROR, `Not enough disk space for backup. Needed: ${prettyBytes(needed)} Available: ${prettyBytes(diskUsage.available)}`));
callback(null);
}).catch(function (error) {
callback(new BoxError(BoxError.FS_ERROR, error));
});
}
// this function is called via backupupload (since it needs root to traverse app's directory)
function upload(backupId, format, dataLayoutString, progressCallback, callback) {
assert.strictEqual(typeof backupId, 'string');
@@ -421,31 +445,35 @@ function upload(backupId, format, dataLayoutString, progressCallback, callback)
const dataLayout = DataLayout.fromString(dataLayoutString);
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
if (error) return callback(error);
if (format === 'tgz') {
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
retryCallback = once(retryCallback); // protect again upload() erroring much later after tar stream error
checkFreeDiskSpace(backupConfig, dataLayout, function (error) {
if (error) return callback(error);
tarPack(dataLayout, backupConfig.key || null, function (error, tarStream) {
if (error) return retryCallback(error);
if (format === 'tgz') {
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
retryCallback = once(retryCallback); // protect again upload() erroring much later after tar stream error
tarStream.on('progress', function(progress) {
const transferred = Math.round(progress.transferred/1024/1024), speed = Math.round(progress.speed/1024/1024);
if (!transferred && !speed) return progressCallback({ message: 'Uploading backup' }); // 0M@0Mbps looks wrong
progressCallback({ message: `Uploading backup ${transferred}M@${speed}Mbps` });
tarPack(dataLayout, backupConfig.key || null, function (error, tarStream) {
if (error) return retryCallback(error);
tarStream.on('progress', function (progress) {
const transferred = Math.round(progress.transferred/1024/1024), speed = Math.round(progress.speed/1024/1024);
if (!transferred && !speed) return progressCallback({ message: 'Uploading backup' }); // 0M@0Mbps looks wrong
progressCallback({ message: `Uploading backup ${transferred}M@${speed}Mbps` });
});
tarStream.on('error', retryCallback); // already returns BoxError
api(backupConfig.provider).upload(backupConfig, getBackupFilePath(backupConfig, backupId, format), tarStream, retryCallback);
});
tarStream.on('error', retryCallback); // already returns BackupsError
api(backupConfig.provider).upload(backupConfig, getBackupFilePath(backupConfig, backupId, format), tarStream, retryCallback);
});
}, callback);
} else {
async.series([
saveFsMetadata.bind(null, dataLayout, `${dataLayout.localRoot()}/fsmetadata.json`),
sync.bind(null, backupConfig, backupId, dataLayout, progressCallback)
], callback);
}
}, callback);
} else {
async.series([
saveFsMetadata.bind(null, dataLayout, `${dataLayout.localRoot()}/fsmetadata.json`),
sync.bind(null, backupConfig, backupId, dataLayout, progressCallback)
], callback);
}
});
});
}
@@ -468,17 +496,17 @@ function tarExtract(inStream, dataLayout, key, callback) {
inStream.on('error', function (error) {
debug('tarExtract: input stream error.', error);
emitError(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
emitError(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
});
gunzip.on('error', function (error) {
debug('tarExtract: gunzip stream error.', error);
emitError(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
emitError(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
});
extract.on('error', function (error) {
debug('tarExtract: extract stream error.', error);
emitError(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
emitError(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
});
extract.on('finish', function () {
@@ -491,7 +519,7 @@ function tarExtract(inStream, dataLayout, key, callback) {
var decrypt = crypto.createDecipher('aes-256-cbc', key);
decrypt.on('error', function (error) {
debug('tarExtract: decrypt stream error.', error);
emitError(new BackupsError(BackupsError.EXTERNAL_ERROR, `Failed to decrypt: ${error.message}`));
emitError(new BoxError(BoxError.EXTERNAL_ERROR, `Failed to decrypt: ${error.message}`));
});
inStream.pipe(ps).pipe(decrypt).pipe(gunzip).pipe(extract);
} else {
@@ -509,19 +537,19 @@ function restoreFsMetadata(dataLayout, metadataFile, callback) {
debug(`Recreating empty directories in ${dataLayout.toString()}`);
var metadataJson = safe.fs.readFileSync(metadataFile, 'utf8');
if (metadataJson === null) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error loading fsmetadata.json:' + safe.error.message));
if (metadataJson === null) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Error loading fsmetadata.json:' + safe.error.message));
var metadata = safe.JSON.parse(metadataJson);
if (metadata === null) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error parsing fsmetadata.json:' + safe.error.message));
if (metadata === null) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Error parsing fsmetadata.json:' + safe.error.message));
async.eachSeries(metadata.emptyDirs, function createPath(emptyDir, iteratorDone) {
mkdirp(dataLayout.toLocalPath(emptyDir), iteratorDone);
}, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, `unable to create path: ${error.message}`));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `unable to create path: ${error.message}`));
async.eachSeries(metadata.execFiles, function createPath(execFile, iteratorDone) {
fs.chmod(dataLayout.toLocalPath(execFile), parseInt('0755', 8), iteratorDone);
}, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, `unable to chmod: ${error.message}`));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `unable to chmod: ${error.message}`));
callback();
});
@@ -537,24 +565,30 @@ function downloadDir(backupConfig, backupFilePath, dataLayout, progressCallback,
debug(`downloadDir: ${backupFilePath} to ${dataLayout.toString()}`);
function downloadFile(entry, callback) {
function downloadFile(entry, done) {
let relativePath = path.relative(backupFilePath, entry.fullPath);
if (backupConfig.key) {
relativePath = decryptFilePath(relativePath, backupConfig.key);
if (!relativePath) return callback(new BackupsError(BackupsError.BAD_STATE, 'Unable to decrypt file'));
if (!relativePath) return done(new BoxError(BoxError.BAD_STATE, 'Unable to decrypt file'));
}
const destFilePath = dataLayout.toLocalPath('./' + relativePath);
mkdirp(path.dirname(destFilePath), function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
if (error) return done(new BoxError(BoxError.FS_ERROR, error.message));
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
let destStream = createWriteStream(destFilePath, backupConfig.key || null);
destStream.on('progress', function (progress) {
const transferred = Math.round(progress.transferred/1024/1024), speed = Math.round(progress.speed/1024/1024);
if (!transferred && !speed) return progressCallback({ message: `Downloading ${entry.fullPath}` }); // 0M@0Mbps looks wrong
progressCallback({ message: `Downloading ${entry.fullPath}: ${transferred}M@${speed}Mbps` });
});
// protect against multiple errors. must destroy the write stream so that a previous retry does not write
let closeAndRetry = once((error) => {
if (error) progressCallback({ message: `Download ${entry.fullPath} errored: ${error.message}` });
else progressCallback({ message: `Download ${entry.fullPath} finished` });
if (error) progressCallback({ message: `Download ${entry.fullPath} to ${destFilePath} errored: ${error.message}` });
else progressCallback({ message: `Download ${entry.fullPath} to ${destFilePath} finished` });
destStream.destroy();
retryCallback(error);
});
@@ -563,21 +597,21 @@ function downloadDir(backupConfig, backupFilePath, dataLayout, progressCallback,
if (error) return closeAndRetry(error);
sourceStream.on('error', closeAndRetry);
destStream.on('error', closeAndRetry);
destStream.on('error', closeAndRetry); // already emits BoxError
progressCallback({ message: `Downloading ${entry.fullPath} to ${destFilePath}` });
sourceStream.pipe(destStream, { end: true }).on('finish', closeAndRetry);
});
}, callback);
}, done);
});
}
api(backupConfig.provider).listDir(backupConfig, backupFilePath, 1000, function (entries, done) {
api(backupConfig.provider).listDir(backupConfig, backupFilePath, 1000, function (entries, iteratorDone) {
// https://www.digitalocean.com/community/questions/rate-limiting-on-spaces?answer=40441
const concurrency = backupConfig.downloadConcurrency || (backupConfig.provider === 's3' ? 30 : 10);
async.eachLimit(entries, concurrency, downloadFile, done);
async.eachLimit(entries, concurrency, downloadFile, iteratorDone);
}, callback);
}
@@ -589,7 +623,7 @@ function download(backupConfig, backupId, format, dataLayout, progressCallback,
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
debug(`download - Downloading ${backupId} of format ${format} to ${dataLayout.toString()}`);
debug(`download: Downloading ${backupId} of format ${format} to ${dataLayout.toString()}`);
const backupFilePath = getBackupFilePath(backupConfig, backupId, format);
@@ -603,7 +637,7 @@ function download(backupConfig, backupId, format, dataLayout, progressCallback,
ps.on('progress', function (progress) {
const transferred = Math.round(progress.transferred/1024/1024), speed = Math.round(progress.speed/1024/1024);
if (!transferred && !speed) return progressCallback({ message: 'Downloading' }); // 0M@0Mbps looks wrong
if (!transferred && !speed) return progressCallback({ message: 'Downloading backup' }); // 0M@0Mbps looks wrong
progressCallback({ message: `Downloading ${transferred}M@${speed}Mbps` });
});
ps.on('error', retryCallback);
@@ -634,18 +668,17 @@ function restore(backupConfig, backupId, progressCallback, callback) {
debug('restore: download completed, importing database');
database.importFromFile(`${dataLayout.localRoot()}/box.mysqldump`, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
debug('restore: database imported');
callback();
settings.initCache(callback);
});
});
}
function restoreApp(app, addonsToRestore, restoreConfig, progressCallback, callback) {
function downloadApp(app, restoreConfig, progressCallback, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof addonsToRestore, 'object');
assert.strictEqual(typeof restoreConfig, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
@@ -654,43 +687,45 @@ function restoreApp(app, addonsToRestore, restoreConfig, progressCallback, callb
if (!appDataDir) return callback(safe.error);
const dataLayout = new DataLayout(appDataDir, app.dataDir ? [{ localDir: app.dataDir, remoteDir: 'data' }] : []);
var startTime = new Date();
const startTime = new Date();
const getBackupConfigFunc = restoreConfig.backupConfig ? (next) => next(null, restoreConfig.backupConfig) : settings.getBackupConfig;
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
getBackupConfigFunc(function (error, backupConfig) {
if (error) return callback(error);
async.series([
download.bind(null, backupConfig, restoreConfig.backupId, restoreConfig.backupFormat, dataLayout, progressCallback),
addons.restoreAddons.bind(null, app, addonsToRestore)
], function (error) {
debug('restoreApp: time: %s', (new Date() - startTime)/1000);
download(backupConfig, restoreConfig.backupId, restoreConfig.backupFormat, dataLayout, progressCallback, function (error) {
debug('downloadApp: time: %s', (new Date() - startTime)/1000);
callback(error);
});
});
}
function runBackupUpload(backupId, format, dataLayout, progressCallback, callback) {
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof format, 'string');
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
function runBackupUpload(uploadConfig, progressCallback, callback) {
assert.strictEqual(typeof uploadConfig, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
let result = '';
const { backupId, format, dataLayout, progressTag } = uploadConfig;
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof format, 'string');
assert.strictEqual(typeof progressTag, 'string');
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
let result = ''; // the script communicates error result as a string
shell.sudo(`backup-${backupId}`, [ BACKUP_UPLOAD_CMD, backupId, format, dataLayout.toString() ], { preserveEnv: true, ipc: true }, function (error) {
if (error && (error.code === null /* signal */ || (error.code !== 0 && error.code !== 50))) { // backuptask crashed
return callback(new BackupsError(BackupsError.INTERNAL_ERROR, 'Backuptask crashed'));
return callback(new BoxError(BoxError.INTERNAL_ERROR, 'Backuptask crashed'));
} else if (error && error.code === 50) { // exited with error
return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, result));
return callback(new BoxError(BoxError.EXTERNAL_ERROR, result));
}
callback();
}).on('message', function (message) {
if (!message.result) return progressCallback(message);
debug(`runBackupUpload: result - ${message}`);
result = message.result;
}).on('message', function (progress) { // script sends either 'message' or 'result' property
if (!progress.result) return progressCallback({ message: `${progress.message} (${progressTag})` });
debug(`runBackupUpload: result - ${JSON.stringify(progress)}`);
result = progress.result;
});
}
@@ -711,7 +746,9 @@ function setSnapshotInfo(id, info, callback) {
var contents = safe.fs.readFileSync(paths.SNAPSHOT_INFO_FILE, 'utf8');
var data = safe.JSON.parse(contents) || { };
if (info) data[id] = info; else delete data[id];
if (!safe.fs.writeFileSync(paths.SNAPSHOT_INFO_FILE, JSON.stringify(data, null, 4), 'utf8')) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(paths.SNAPSHOT_INFO_FILE, JSON.stringify(data, null, 4), 'utf8')) {
return callback(new BoxError(BoxError.FS_ERROR, safe.error.message));
}
callback();
}
@@ -723,7 +760,7 @@ function snapshotBox(progressCallback, callback) {
progressCallback({ message: 'Snapshotting box' });
database.exportToFile(`${paths.BOX_DATA_DIR}/box.mysqldump`, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
return callback();
});
@@ -742,8 +779,14 @@ function uploadBoxSnapshot(backupConfig, progressCallback, callback) {
const boxDataDir = safe.fs.realpathSync(paths.BOX_DATA_DIR);
if (!boxDataDir) return callback(safe.error);
const dataLayout = new DataLayout(boxDataDir, []);
runBackupUpload('snapshot/box', backupConfig.format, dataLayout, progressCallback, function (error) {
const uploadConfig = {
backupId: 'snapshot/box',
format: backupConfig.format,
dataLayout: new DataLayout(boxDataDir, []),
progressTag: 'box'
};
runBackupUpload(uploadConfig, progressCallback, function (error) {
if (error) return callback(error);
debug('uploadBoxSnapshot: time: %s secs', (new Date() - startTime)/1000);
@@ -761,25 +804,24 @@ function rotateBoxBackup(backupConfig, tag, appBackupIds, progressCallback, call
assert.strictEqual(typeof callback, 'function');
var snapshotInfo = getSnapshotInfo('box');
if (!snapshotInfo) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, 'Snapshot info missing or corrupt'));
const snapshotTime = snapshotInfo.timestamp.replace(/[T.]/g, '-').replace(/[:Z]/g,''); // add this to filename to make it unique, so it's easy to download them
const backupId = util.format('%s/box_%s_v%s', tag, snapshotTime, config.version());
const backupId = util.format('%s/box_%s_v%s', tag, snapshotTime, constants.VERSION);
const format = backupConfig.format;
debug(`Rotating box backup to id ${backupId}`);
backupdb.add(backupId, { version: config.version(), type: backupdb.BACKUP_TYPE_BOX, dependsOn: appBackupIds, manifest: null, format: format }, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
backupdb.add(backupId, { version: constants.VERSION, type: backupdb.BACKUP_TYPE_BOX, dependsOn: appBackupIds, manifest: null, format: format }, function (error) {
if (error) return callback(error);
var copy = api(backupConfig.provider).copy(backupConfig, getBackupFilePath(backupConfig, 'snapshot/box', format), getBackupFilePath(backupConfig, backupId, format));
copy.on('progress', (message) => progressCallback({ message }));
copy.on('progress', (message) => progressCallback({ message: `box: ${message}` }));
copy.on('done', function (copyBackupError) {
const state = copyBackupError ? backupdb.BACKUP_STATE_ERROR : backupdb.BACKUP_STATE_NORMAL;
backupdb.update(backupId, { state: state }, function (error) {
if (copyBackupError) return callback(copyBackupError);
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
if (error) return callback(error);
debug(`Rotated box backup successfully as id ${backupId}`);
@@ -796,7 +838,7 @@ function backupBoxWithAppBackupIds(appBackupIds, tag, progressCallback, callback
assert.strictEqual(typeof callback, 'function');
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
if (error) return callback(error);
uploadBoxSnapshot(backupConfig, progressCallback, function (error) {
if (error) return callback(error);
@@ -809,10 +851,10 @@ function backupBoxWithAppBackupIds(appBackupIds, tag, progressCallback, callback
function canBackupApp(app) {
// only backup apps that are installed or pending configure or called from apptask. Rest of them are in some
// state not good for consistent backup (i.e addons may not have been setup completely)
return (app.installationState === appdb.ISTATE_INSTALLED && app.health === appdb.HEALTH_HEALTHY) ||
app.installationState === appdb.ISTATE_PENDING_CONFIGURE ||
app.installationState === appdb.ISTATE_PENDING_BACKUP || // called from apptask
app.installationState === appdb.ISTATE_PENDING_UPDATE; // called from apptask
return (app.installationState === apps.ISTATE_INSTALLED && app.health === apps.HEALTH_HEALTHY) ||
app.installationState === apps.ISTATE_PENDING_CONFIGURE ||
app.installationState === apps.ISTATE_PENDING_BACKUP || // called from apptask
app.installationState === apps.ISTATE_PENDING_UPDATE; // called from apptask
}
function snapshotApp(app, progressCallback, callback) {
@@ -822,12 +864,12 @@ function snapshotApp(app, progressCallback, callback) {
progressCallback({ message: `Snapshotting app ${app.fqdn}` });
if (!safe.fs.writeFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/config.json'), JSON.stringify(apps.getAppConfig(app)))) {
return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error creating config.json: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/config.json'), JSON.stringify(app))) {
return callback(new BoxError(BoxError.FS_ERROR, 'Error creating config.json: ' + safe.error.message));
}
addons.backupAddons(app, app.manifest.addons, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
return callback(null);
});
@@ -842,7 +884,6 @@ function rotateAppBackup(backupConfig, app, tag, options, progressCallback, call
assert.strictEqual(typeof callback, 'function');
var snapshotInfo = getSnapshotInfo(app.id);
if (!snapshotInfo) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, 'Snapshot info missing or corrupt'));
var manifest = snapshotInfo.restoreConfig ? snapshotInfo.restoreConfig.manifest : snapshotInfo.manifest; // compat
const snapshotTime = snapshotInfo.timestamp.replace(/[T.]/g, '-').replace(/[:Z]/g,''); // add this for unique filename which helps when downloading them
@@ -852,16 +893,16 @@ function rotateAppBackup(backupConfig, app, tag, options, progressCallback, call
debug(`Rotating app backup of ${app.id} to id ${backupId}`);
backupdb.add(backupId, { version: manifest.version, type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ], manifest: manifest, format: format }, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
if (error) return callback(error);
var copy = api(backupConfig.provider).copy(backupConfig, getBackupFilePath(backupConfig, `snapshot/app_${app.id}`, format), getBackupFilePath(backupConfig, backupId, format));
copy.on('progress', (message) => progressCallback({ message }));
copy.on('progress', (message) => progressCallback({ message: `${message} (${app.fqdn})` }));
copy.on('done', function (copyBackupError) {
const state = copyBackupError ? backupdb.BACKUP_STATE_ERROR : backupdb.BACKUP_STATE_NORMAL;
backupdb.update(backupId, { preserveSecs: options.preserveSecs || 0, state: state }, function (error) {
if (copyBackupError) return callback(copyBackupError);
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
if (error) return callback(error);
debug(`Rotated app backup of ${app.id} successfully to id ${backupId}`);
@@ -890,7 +931,14 @@ function uploadAppSnapshot(backupConfig, app, progressCallback, callback) {
const dataLayout = new DataLayout(appDataDir, app.dataDir ? [{ localDir: app.dataDir, remoteDir: 'data' }] : []);
runBackupUpload(backupId, backupConfig.format, dataLayout, progressCallback, function (error) {
const uploadConfig = {
backupId,
format: backupConfig.format,
dataLayout,
progressTag: app.fqdn
};
runBackupUpload(uploadConfig, progressCallback, function (error) {
if (error) return callback(error);
debugApp(app, 'uploadAppSnapshot: %s done time: %s secs', backupId, (new Date() - startTime)/1000);
@@ -910,7 +958,7 @@ function backupAppWithTag(app, tag, options, progressCallback, callback) {
if (!canBackupApp(app)) return callback(); // nothing to do
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
if (error) return callback(error);
uploadAppSnapshot(backupConfig, app, progressCallback, function (error) {
if (error) return callback(error);
@@ -941,7 +989,7 @@ function backupBoxAndApps(progressCallback, callback) {
const tag = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
apps.getAll(function (error, allApps) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
if (error) return callback(error);
let percent = 1;
let step = 100/(allApps.length+2);
@@ -956,7 +1004,7 @@ function backupBoxAndApps(progressCallback, callback) {
}
backupAppWithTag(app, tag, { /* options */ }, (progress) => progressCallback({ percent: percent, message: progress.message }), function (error, backupId) {
if (error && error.reason !== BackupsError.BAD_STATE) {
if (error && error.reason !== BoxError.BAD_STATE) {
debugApp(app, 'Unable to backup', error);
return iteratorCallback(error);
}
@@ -980,21 +1028,24 @@ function backupBoxAndApps(progressCallback, callback) {
function startBackupTask(auditSource, callback) {
let error = locker.lock(locker.OP_FULL_BACKUP);
if (error) return callback(new BackupsError(BackupsError.BAD_STATE, `Cannot backup now: ${error.message}`));
if (error) return callback(new BoxError(BoxError.BAD_STATE, `Cannot backup now: ${error.message}`));
tasks.add(tasks.TASK_BACKUP, [ ], function (error, taskId) {
if (error) return callback(error);
let task = tasks.startTask(tasks.TASK_BACKUP, []);
task.on('error', (error) => callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)));
task.on('start', (taskId) => {
eventlog.add(eventlog.ACTION_BACKUP_START, auditSource, { taskId });
tasks.startTask(taskId, { timeout: 12 * 60 * 60 * 1000 /* 12 hours */ }, function (error, backupId) {
locker.unlock(locker.OP_FULL_BACKUP);
const errorMessage = error ? error.message : '';
const timedOut = error ? error.code === tasks.ETIMEOUT : false;
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { taskId, errorMessage, timedOut, backupId });
});
callback(null, taskId);
});
task.on('finish', (error, result) => {
locker.unlock(locker.OP_FULL_BACKUP);
const errorMessage = error ? error.message : '';
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { taskId: task.id, errorMessage: errorMessage, backupId: result });
});
}
function ensureBackup(auditSource, callback) {
@@ -1066,7 +1117,7 @@ function cleanupAppBackups(backupConfig, referencedAppBackups, callback) {
// we clean app backups of any state because the ones to keep are determined by the box cleanup code
backupdb.getByTypePaged(backupdb.BACKUP_TYPE_APP, 1, 1000, function (error, appBackups) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
if (error) return callback(error);
async.eachSeries(appBackups, function iterator(appBackup, iteratorDone) {
if (referencedAppBackups.indexOf(appBackup.id) !== -1) return iteratorDone();
@@ -1154,7 +1205,7 @@ function cleanupSnapshots(backupConfig, callback) {
delete info.box;
async.eachSeries(Object.keys(info), function (appId, iteratorDone) {
apps.get(appId, function (error /*, app */) {
if (!error || error.reason !== AppsError.NOT_FOUND) return iteratorDone();
if (!error || error.reason !== BoxError.NOT_FOUND) return iteratorDone();
function done(/* ignoredError */) {
safe.fs.unlinkSync(path.join(paths.BACKUP_INFO_DIR, `${appId}.sync.cache`));
@@ -1218,18 +1269,20 @@ function cleanup(auditSource, progressCallback, callback) {
}
function startCleanupTask(auditSource, callback) {
let task = tasks.startTask(tasks.TASK_CLEAN_BACKUPS, [ auditSource ]);
task.on('error', (error) => callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)));
task.on('start', (taskId) => {
eventlog.add(eventlog.ACTION_BACKUP_CLEANUP_START, auditSource, { taskId });
callback(null, taskId);
});
task.on('finish', (error, result) => { // result is { removedBoxBackups, removedAppBackups }
eventlog.add(eventlog.ACTION_BACKUP_CLEANUP_FINISH, auditSource, {
errorMessage: error ? error.message : null,
removedBoxBackups: result ? result.removedBoxBackups : [],
removedAppBackups: result ? result.removedAppBackups : []
tasks.add(tasks.TASK_CLEAN_BACKUPS, [ auditSource ], function (error, taskId) {
if (error) return callback(error);
tasks.startTask(taskId, {}, (error, result) => { // result is { removedBoxBackups, removedAppBackups }
eventlog.add(eventlog.ACTION_BACKUP_CLEANUP_FINISH, auditSource, {
taskId,
errorMessage: error ? error.message : null,
removedBoxBackups: result ? result.removedBoxBackups : [],
removedAppBackups: result ? result.removedAppBackups : []
});
});
callback(null, taskId);
});
}
+98
View File
@@ -0,0 +1,98 @@
/* jslint node:true */
'use strict';
const assert = require('assert'),
HttpError = require('connect-lastmile').HttpError,
util = require('util'),
_ = require('underscore');
exports = module.exports = BoxError;
function BoxError(reason, errorOrMessage, details) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
assert(typeof details === 'object' || typeof details === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
this.details = details || {};
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else { // error object
this.message = errorOrMessage.message;
this.nestedError = errorOrMessage;
_.extend(this.details, errorOrMessage); // copy enumerable properies
}
}
util.inherits(BoxError, Error);
BoxError.ACCESS_DENIED = 'Access Denied';
BoxError.ADDONS_ERROR = 'Addons Error';
BoxError.ALREADY_EXISTS = 'Already Exists';
BoxError.BAD_FIELD = 'Bad Field';
BoxError.BAD_STATE = 'Bad State';
BoxError.BUSY = 'Busy';
BoxError.COLLECTD_ERROR = 'Collectd Error';
BoxError.CONFLICT = 'Conflict';
BoxError.CRYPTO_ERROR = 'Crypto Error';
BoxError.DATABASE_ERROR = 'Database Error';
BoxError.DNS_ERROR = 'DNS Error';
BoxError.DOCKER_ERROR = 'Docker Error';
BoxError.EXTERNAL_ERROR = 'External Error'; // use this for external API errors
BoxError.FS_ERROR = 'FileSystem Error';
BoxError.INACTIVE = 'Inactive';
BoxError.INTERNAL_ERROR = 'Internal Error';
BoxError.INVALID_CREDENTIALS = 'Invalid Credentials';
BoxError.LICENSE_ERROR = 'License Error';
BoxError.LOGROTATE_ERROR = 'Logrotate Error';
BoxError.MAIL_ERROR = 'Mail Error';
BoxError.NETWORK_ERROR = 'Network Error';
BoxError.NGINX_ERROR = 'Nginx Error';
BoxError.NOT_FOUND = 'Not found';
BoxError.NOT_IMPLEMENTED = 'Not implemented';
BoxError.NOT_SIGNED = 'Not Signed';
BoxError.OPENSSL_ERROR = 'OpenSSL Error';
BoxError.PLAN_LIMIT = 'Plan Limit';
BoxError.SPAWN_ERROR = 'Spawn Error';
BoxError.TASK_ERROR = 'Task Error';
BoxError.TIMEOUT = 'Timeout';
BoxError.TRY_AGAIN = 'Try Again';
BoxError.prototype.toPlainObject = function () {
return _.extend({}, { message: this.message, reason: this.reason }, this.details);
};
// this is a class method for now in case error is not a BoxError
BoxError.toHttpError = function (error) {
switch (error.reason) {
case BoxError.BAD_FIELD:
return new HttpError(400, error);
case BoxError.LICENSE_ERROR:
return new HttpError(402, error);
case BoxError.NOT_FOUND:
return new HttpError(404, error);
case BoxError.ALREADY_EXISTS:
case BoxError.BAD_STATE:
case BoxError.CONFLICT:
return new HttpError(409, error);
case BoxError.INVALID_CREDENTIALS:
return new HttpError(412, error);
case BoxError.EXTERNAL_ERROR:
case BoxError.NETWORK_ERROR:
case BoxError.FS_ERROR:
case BoxError.MAIL_ERROR:
case BoxError.DOCKER_ERROR:
case BoxError.ADDONS_ERROR:
return new HttpError(424, error);
case BoxError.DATABASE_ERROR:
case BoxError.INTERNAL_ERROR:
default:
return new HttpError(500, error);
}
};
+93 -101
View File
@@ -2,14 +2,15 @@
var assert = require('assert'),
async = require('async'),
BoxError = require('../boxerror.js'),
crypto = require('crypto'),
debug = require('debug')('box:cert/acme2'),
domains = require('../domains.js'),
fs = require('fs'),
path = require('path'),
paths = require('../paths.js'),
request = require('request'),
safe = require('safetydance'),
superagent = require('superagent'),
util = require('util'),
_ = require('underscore');
@@ -24,31 +25,6 @@ exports = module.exports = {
_getChallengeSubdomain: getChallengeSubdomain
};
function Acme2Error(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(Acme2Error, Error);
Acme2Error.INTERNAL_ERROR = 'Internal Error';
Acme2Error.EXTERNAL_ERROR = 'External Error';
Acme2Error.ALREADY_EXISTS = 'Already Exists';
Acme2Error.NOT_COMPLETED = 'Not Completed';
Acme2Error.FORBIDDEN = 'Forbidden';
// http://jose.readthedocs.org/en/latest/
// https://www.ietf.org/proceedings/92/slides/slides-92-acme-1.pdf
// https://community.letsencrypt.org/t/list-of-client-implementations/2103
@@ -65,15 +41,6 @@ function Acme2(options) {
this.wildcard = !!options.wildcard;
}
Acme2.prototype.getNonce = function (callback) {
superagent.get(this.directory.newNonce).timeout(30 * 1000).end(function (error, response) {
if (error && !error.response) return callback(error);
if (response.statusCode !== 204) return callback(new Error('Invalid response code when fetching nonce : ' + response.statusCode));
return callback(null, response.headers['Replay-Nonce'.toLowerCase()]);
});
};
// urlsafe base64 encoding (jose)
function urlBase64Encode(string) {
return string.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
@@ -120,8 +87,12 @@ Acme2.prototype.sendSignedRequest = function (url, payload, callback) {
var payload64 = b64(payload);
this.getNonce(function (error, nonce) {
if (error) return callback(error);
request.get(this.directory.newNonce, { json: true, timeout: 30000 }, function (error, response) {
if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error sending signed request: ${error.message}`));
if (response.statusCode !== 204) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response code when fetching nonce : ' + response.statusCode));
const nonce = response.headers['Replay-Nonce'.toLowerCase()];
if (!nonce) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'No nonce in response'));
debug('sendSignedRequest: using nonce %s for url %s', nonce, url);
@@ -137,14 +108,23 @@ Acme2.prototype.sendSignedRequest = function (url, payload, callback) {
signature: signature64
};
superagent.post(url).set('Content-Type', 'application/jose+json').set('User-Agent', 'acme-cloudron').send(JSON.stringify(data)).timeout(30 * 1000).end(function (error, res) {
if (error && !error.response) return callback(error); // network errors
request.post(url, { headers: { 'Content-Type': 'application/jose+json', 'User-Agent': 'acme-cloudron' }, body: JSON.stringify(data), timeout: 30000 }, function (error, response) {
if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error sending signed request: ${error.message}`)); // network error
callback(null, res);
// we don't set json: true in request because it ends up mangling the content-type
// we don't set json: true in request because it ends up mangling the content-type
if (response.headers['content-type'] === 'application/json') response.body = safe.JSON.parse(response.body);
callback(null, response);
});
});
};
// https://tools.ietf.org/html/rfc8555#section-6.3
Acme2.prototype.postAsGet = function (url, callback) {
this.sendSignedRequest(url, '', callback);
};
Acme2.prototype.updateContact = function (registrationUri, callback) {
assert.strictEqual(typeof registrationUri, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -158,8 +138,8 @@ Acme2.prototype.updateContact = function (registrationUri, callback) {
const that = this;
this.sendSignedRequest(registrationUri, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to update contact. Expecting 200, got %s %s', result.statusCode, result.text)));
if (error) return callback(error);
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to update contact. Expecting 200, got %s %s', result.statusCode, result.text)));
debug(`updateContact: contact of user updated to ${that.email}`);
@@ -178,9 +158,9 @@ Acme2.prototype.registerUser = function (callback) {
var that = this;
this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when registering new account: ' + error.message));
if (error) return callback(error);
// 200 if already exists. 201 for new accounts
if (result.statusCode !== 200 && result.statusCode !== 201) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to register new account. Expecting 200 or 201, got %s %s', result.statusCode, result.text)));
if (result.statusCode !== 200 && result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to register new account. Expecting 200 or 201, got %s %s', result.statusCode, result.text)));
debug(`registerUser: user registered keyid: ${result.headers.location}`);
@@ -204,17 +184,17 @@ Acme2.prototype.newOrder = function (domain, callback) {
debug('newOrder: %s', domain);
this.sendSignedRequest(this.directory.newOrder, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when registering domain: ' + error.message));
if (result.statusCode === 403) return callback(new Acme2Error(Acme2Error.FORBIDDEN, result.body.detail));
if (result.statusCode !== 201) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
if (error) return callback(error);
if (result.statusCode === 403) return callback(new BoxError(BoxError.ACCESS_DENIED, `Forbidden sending signed request: ${result.body.detail}`));
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
debug('newOrder: created order %s %j', domain, result.body);
const order = result.body, orderUrl = result.headers.location;
if (!Array.isArray(order.authorizations)) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'invalid authorizations in order'));
if (typeof order.finalize !== 'string') return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'invalid finalize in order'));
if (typeof orderUrl !== 'string') return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'invalid order location in order header'));
if (!Array.isArray(order.authorizations)) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'invalid authorizations in order'));
if (typeof order.finalize !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'invalid finalize in order'));
if (typeof orderUrl !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'invalid order location in order header'));
callback(null, order, orderUrl);
});
@@ -225,25 +205,26 @@ Acme2.prototype.waitForOrder = function (orderUrl, callback) {
assert.strictEqual(typeof callback, 'function');
debug(`waitForOrder: ${orderUrl}`);
const that = this;
async.retry({ times: 15, interval: 20000 }, function (retryCallback) {
debug('waitForOrder: getting status');
superagent.get(orderUrl).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) {
that.postAsGet(orderUrl, function (error, result) {
if (error) {
debug('waitForOrder: network error getting uri %s', orderUrl);
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message)); // network error
return retryCallback(error);
}
if (result.statusCode !== 200) {
debug('waitForOrder: invalid response code getting uri %s', result.statusCode);
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
}
debug('waitForOrder: status is "%s %j', result.body.status, result.body);
if (result.body.status === 'pending' || result.body.status === 'processing') return retryCallback(new Acme2Error(Acme2Error.NOT_COMPLETED));
if (result.body.status === 'pending' || result.body.status === 'processing') return retryCallback(new BoxError(BoxError.TRY_AGAIN, `Request is in ${result.body.status} state`));
else if (result.body.status === 'valid' && result.body.certificate) return retryCallback(null, result.body.certificate);
else return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Unexpected status or invalid response: ' + result.body));
else return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, 'Unexpected status or invalid response: ' + result.body));
});
}, callback);
};
@@ -277,8 +258,8 @@ Acme2.prototype.notifyChallengeReady = function (challenge, callback) {
};
this.sendSignedRequest(challenge.url, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when notifying challenge: ' + error.message));
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 200, got %s %s', result.statusCode, result.text)));
if (error) return callback(error);
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 200, got %s %s', result.statusCode, result.text)));
callback();
});
@@ -289,25 +270,26 @@ Acme2.prototype.waitForChallenge = function (challenge, callback) {
assert.strictEqual(typeof callback, 'function');
debug('waitingForChallenge: %j', challenge);
const that = this;
async.retry({ times: 15, interval: 20000 }, function (retryCallback) {
debug('waitingForChallenge: getting status');
superagent.get(challenge.url).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) {
that.postAsGet(challenge.url, function (error, result) {
if (error) {
debug('waitForChallenge: network error getting uri %s', challenge.url);
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message)); // network error
return retryCallback(error);
}
if (result.statusCode !== 200) {
debug('waitForChallenge: invalid response code getting uri %s', result.statusCode);
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
}
debug('waitForChallenge: status is "%s %j', result.body.status, result.body);
if (result.body.status === 'pending') return retryCallback(new Acme2Error(Acme2Error.NOT_COMPLETED));
if (result.body.status === 'pending') return retryCallback(new BoxError(BoxError.TRY_AGAIN));
else if (result.body.status === 'valid') return retryCallback();
else return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Unexpected status: ' + result.body.status));
else return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, 'Unexpected status: ' + result.body.status));
});
}, function retryFinished(error) {
// async.retry will pass 'undefined' as second arg making it unusable with async.waterfall()
@@ -329,9 +311,9 @@ Acme2.prototype.signCertificate = function (domain, finalizationUrl, csrDer, cal
debug('signCertificate: sending sign request');
this.sendSignedRequest(finalizationUrl, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when signing certificate: ' + error.message));
if (error) return callback(error);
// 429 means we reached the cert limit for this domain
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 200, got %s %s', result.statusCode, result.text)));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 200, got %s %s', result.statusCode, result.text)));
return callback(null);
});
@@ -351,15 +333,15 @@ Acme2.prototype.createKeyAndCsr = function (hostname, callback) {
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
} else {
var key = safe.child_process.execSync('openssl genrsa 4096');
if (!key) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new Acme2Error(Acme2Error.INTERNAL_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));
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
}
var csrDer = safe.child_process.execSync(`openssl req -new -key ${privateKeyFile} -outform DER -subj /CN=${hostname}`);
if (!csrDer) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error)); // bookkeeping
if (!csrDer) return callback(new BoxError(BoxError.OPENSSL_ERROR, safe.error));
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new BoxError(BoxError.FS_ERROR, safe.error)); // bookkeeping
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFile);
@@ -372,26 +354,27 @@ Acme2.prototype.downloadCertificate = function (hostname, certUrl, callback) {
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
const that = this;
superagent.get(certUrl).buffer().parse(function (res, done) {
var data = [ ];
res.on('data', function(chunk) { data.push(chunk); });
res.on('end', function () { res.text = Buffer.concat(data); done(); });
}).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when downloading certificate'));
if (result.statusCode === 202) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, 'Retry not implemented yet'));
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
debug('downloadCertificate: downloading certificate');
const fullChainPem = result.text;
that.postAsGet(certUrl, function (error, result) {
if (error) return retryCallback(new BoxError(BoxError.NETWORK_ERROR, `Network error when downloading certificate: ${error.message}`));
if (result.statusCode === 202) return retryCallback(new BoxError(BoxError.TRY_AGAIN, 'Retry downloading certificate'));
if (result.statusCode !== 200) return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
const certName = hostname.replace('*.', '_.');
var certificateFile = path.join(outdir, `${certName}.cert`);
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
const fullChainPem = result.body; // buffer
debug('downloadCertificate: cert file for %s saved at %s', hostname, certificateFile);
const certName = hostname.replace('*.', '_.');
var certificateFile = path.join(outdir, `${certName}.cert`);
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return retryCallback(new BoxError(BoxError.FS_ERROR, safe.error));
callback();
});
debug('downloadCertificate: cert file for %s saved at %s', hostname, certificateFile);
retryCallback(null);
});
}, callback);
};
Acme2.prototype.prepareHttpChallenge = function (hostname, domain, authorization, callback) {
@@ -402,7 +385,7 @@ Acme2.prototype.prepareHttpChallenge = function (hostname, domain, authorization
debug('acmeFlow: challenges: %j', authorization);
let httpChallenges = authorization.challenges.filter(function(x) { return x.type === 'http-01'; });
if (httpChallenges.length === 0) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'no http challenges'));
if (httpChallenges.length === 0) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'no http challenges'));
let challenge = httpChallenges[0];
debug('prepareHttpChallenge: preparing for challenge %j', challenge);
@@ -412,7 +395,7 @@ Acme2.prototype.prepareHttpChallenge = function (hostname, domain, authorization
debug('prepareHttpChallenge: writing %s to %s', keyAuthorization, path.join(paths.ACME_CHALLENGES_DIR, challenge.token));
fs.writeFile(path.join(paths.ACME_CHALLENGES_DIR, challenge.token), keyAuthorization, function (error) {
if (error) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.FS_ERROR, error));
callback(null, challenge);
});
@@ -454,7 +437,7 @@ Acme2.prototype.prepareDnsChallenge = function (hostname, domain, authorization,
debug('acmeFlow: challenges: %j', authorization);
let dnsChallenges = authorization.challenges.filter(function(x) { return x.type === 'dns-01'; });
if (dnsChallenges.length === 0) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'no dns challenges'));
if (dnsChallenges.length === 0) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'no dns challenges'));
let challenge = dnsChallenges[0];
const keyAuthorization = this.getKeyAuthorization(challenge.token);
@@ -467,10 +450,10 @@ Acme2.prototype.prepareDnsChallenge = function (hostname, domain, authorization,
debug(`prepareDnsChallenge: update ${challengeSubdomain} with ${txtValue}`);
domains.upsertDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message));
if (error) return callback(error);
domains.waitForDnsRecord(challengeSubdomain, domain, 'TXT', txtValue, { interval: 5000, times: 200 }, function (error) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message));
if (error) return callback(error);
callback(null, challenge);
});
@@ -493,7 +476,7 @@ Acme2.prototype.cleanupDnsChallenge = function (hostname, domain, challenge, cal
debug(`cleanupDnsChallenge: remove ${challengeSubdomain} with ${txtValue}`);
domains.removeDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error));
if (error) return callback(error);
callback(null);
});
@@ -505,10 +488,12 @@ Acme2.prototype.prepareChallenge = function (hostname, domain, authorizationUrl,
assert.strictEqual(typeof authorizationUrl, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`prepareChallenge: http: ${this.performHttpAuthorization}`);
const that = this;
superagent.get(authorizationUrl).timeout(30 * 1000).end(function (error, response) {
if (error && !error.response) return callback(error);
if (response.statusCode !== 200) return callback(new Error('Invalid response code getting authorization : ' + response.statusCode));
this.postAsGet(authorizationUrl, function (error, response) {
if (error) return callback(error);
if (response.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response code getting authorization : ' + response.statusCode));
const authorization = response.body;
@@ -526,6 +511,8 @@ Acme2.prototype.cleanupChallenge = function (hostname, domain, challenge, callba
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
debug(`cleanupChallenge: http: ${this.performHttpAuthorization}`);
if (this.performHttpAuthorization) {
this.cleanupHttpChallenge(hostname, domain, challenge, callback);
} else {
@@ -541,7 +528,7 @@ Acme2.prototype.acmeFlow = function (hostname, domain, callback) {
if (!fs.existsSync(paths.ACME_ACCOUNT_KEY_FILE)) {
debug('getCertificate: generating acme account key on first run');
this.accountKeyPem = safe.child_process.execSync('openssl genrsa 4096');
if (!this.accountKeyPem) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
if (!this.accountKeyPem) return callback(new BoxError(BoxError.OPENSSL_ERROR, safe.error));
safe.fs.writeFileSync(paths.ACME_ACCOUNT_KEY_FILE, this.accountKeyPem);
} else {
@@ -585,13 +572,13 @@ Acme2.prototype.acmeFlow = function (hostname, domain, callback) {
Acme2.prototype.getDirectory = function (callback) {
const that = this;
superagent.get(this.caDirectory).timeout(30 * 1000).end(function (error, response) {
if (error && !error.response) return callback(error);
if (response.statusCode !== 200) return callback(new Error('Invalid response code when fetching directory : ' + response.statusCode));
request.get(this.caDirectory, { json: true, timeout: 30000 }, function (error, response) {
if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error getting directory: ${error.message}`));
if (response.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response code when fetching directory : ' + response.statusCode));
if (typeof response.body.newNonce !== 'string' ||
typeof response.body.newOrder !== 'string' ||
typeof response.body.newAccount !== 'string') return callback(new Error(`Invalid response body : ${response.body}`));
typeof response.body.newAccount !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Invalid response body : ${response.body}`));
that.directory = response.body;
@@ -631,6 +618,11 @@ function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var acme = new Acme2(options || { });
acme.getCertificate(hostname, domain, callback);
let attempt = 1;
async.retry({ times: 3, interval: 0 }, function (retryCallback) {
debug(`getCertificate: attempt ${attempt++}`);
let acme = new Acme2(options || { });
acme.getCertificate(hostname, domain, retryCallback);
}, callback);
}
+3 -2
View File
@@ -10,7 +10,8 @@ exports = module.exports = {
getCertificate: getCertificate
};
var assert = require('assert');
var assert = require('assert'),
BoxError = require('../boxerror.js');
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
@@ -18,6 +19,6 @@ function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
return callback(new Error('Not implemented'));
return callback(new BoxError(BoxError.NOT_IMPLEMENTED, 'getCertificate is not implemented'));
}
+38
View File
@@ -0,0 +1,38 @@
'use strict';
let assert = require('assert'),
fs = require('fs'),
path = require('path');
exports = module.exports = {
getChanges: getChanges
};
function getChanges(version) {
assert.strictEqual(typeof version, 'string');
let changelog = [ ];
const lines = fs.readFileSync(path.join(__dirname, '../CHANGES'), 'utf8').split('\n');
version = version.replace(/[+-].*/, ''); // strip prerelease
let i;
for (i = 0; i < lines.length; i++) {
if (lines[i] === '[' + version + ']') break;
}
for (i = i + 1; i < lines.length; i++) {
if (lines[i] === '') continue;
if (lines[i][0] === '[') break;
lines[i] = lines[i].trim();
// detect and remove list style - and * in changelog lines
if (lines[i].indexOf('-') === 0) lines[i] = lines[i].slice(1).trim();
if (lines[i].indexOf('*') === 0) lines[i] = lines[i].slice(1).trim();
changelog.push(lines[i]);
}
return changelog;
}
+24 -34
View File
@@ -15,14 +15,12 @@ exports = module.exports = {
delByAppId: delByAppId,
delByAppIdAndType: delByAppIdAndType,
_clear: clear,
_addDefaultClients: addDefaultClients
_clear: clear
};
var assert = require('assert'),
async = require('async'),
database = require('./database.js'),
DatabaseError = require('./databaseerror.js');
BoxError = require('./boxerror.js'),
database = require('./database.js');
var CLIENTS_FIELDS = [ 'id', 'appId', 'type', 'clientSecret', 'redirectURI', 'scope' ].join(',');
var CLIENTS_FIELDS_PREFIXED = [ 'clients.id', 'clients.appId', 'clients.type', 'clients.clientSecret', 'clients.redirectURI', 'clients.scope' ].join(',');
@@ -32,8 +30,8 @@ function get(id, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + CLIENTS_FIELDS + ' FROM clients WHERE id = ?', [ id ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, `Client not found: ${id}`));
callback(null, result[0]);
});
@@ -43,7 +41,7 @@ function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + CLIENTS_FIELDS + ' FROM clients ORDER BY appId', function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, results);
});
@@ -53,7 +51,7 @@ function getAllWithTokenCount(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + CLIENTS_FIELDS_PREFIXED + ',COUNT(tokens.clientId) AS tokenCount FROM clients LEFT OUTER JOIN tokens ON clients.id=tokens.clientId GROUP BY clients.id', [], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, results);
});
@@ -64,7 +62,7 @@ function getAllWithTokenCountByIdentifier(identifier, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + CLIENTS_FIELDS_PREFIXED + ',COUNT(tokens.clientId) AS tokenCount FROM clients LEFT OUTER JOIN tokens ON clients.id=tokens.clientId WHERE tokens.identifier=? GROUP BY clients.id', [ identifier ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, results);
});
@@ -75,8 +73,8 @@ function getByAppId(appId, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + CLIENTS_FIELDS + ' FROM clients WHERE appId = ? LIMIT 1', [ appId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Client not found'));
callback(null, result[0]);
});
@@ -88,8 +86,8 @@ function getByAppIdAndType(appId, type, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + CLIENTS_FIELDS + ' FROM clients WHERE appId = ? AND type = ? LIMIT 1', [ appId, type ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Client not found'));
callback(null, result[0]);
});
@@ -107,8 +105,8 @@ function add(id, appId, type, clientSecret, redirectURI, scope, callback) {
var data = [ id, appId, type, clientSecret, redirectURI, scope ];
database.query('INSERT INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (?, ?, ?, ?, ?, ?)', data, function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS));
if (error || result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS));
if (error || result.affectedRows === 0) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
@@ -126,8 +124,8 @@ function upsert(id, appId, type, clientSecret, redirectURI, scope, callback) {
var data = [ id, appId, type, clientSecret, redirectURI, scope ];
database.query('REPLACE INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (?, ?, ?, ?, ?, ?)', data, function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS));
if (error || result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS));
if (error || result.affectedRows === 0) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
@@ -138,8 +136,8 @@ function del(id, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM clients WHERE id = ?', [ id ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, `Client not found: ${id}`));
callback(null);
});
@@ -150,8 +148,8 @@ function delByAppId(appId, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM clients WHERE appId=?', [ appId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Client not found'));
callback(null);
});
@@ -163,8 +161,8 @@ function delByAppIdAndType(appId, type, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM clients WHERE appId=? AND type=?', [ appId, type ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Client not found'));
callback(null);
});
@@ -173,17 +171,9 @@ function delByAppIdAndType(appId, type, callback) {
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM clients WHERE id!="cid-webadmin" AND id!="cid-sdk" AND id!="cid-cli"', function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
database.query('DELETE FROM clients', function (error) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}
function addDefaultClients(callback) {
async.series([
add.bind(null, 'cid-webadmin', 'Settings', 'built-in', 'secret-webadmin', 'https://admin-localhost', '*'),
add.bind(null, 'cid-sdk', 'SDK', 'built-in', 'secret-sdk', 'https://admin-localhost', '*'),
add.bind(null, 'cid-cli', 'Cloudron Tool', 'built-in', 'secret-cli', 'https://admin-localhost', '*')
], callback);
}
+27 -57
View File
@@ -1,8 +1,6 @@
'use strict';
exports = module.exports = {
ClientsError: ClientsError,
add: add,
get: get,
del: del,
@@ -20,6 +18,11 @@ exports = module.exports = {
removeTokenPrivateFields: removeTokenPrivateFields,
// client ids. we categorize them so we can have different restrictions based on the client
ID_WEBADMIN: 'cid-webadmin', // dashboard oauth
ID_SDK: 'cid-sdk', // created by user via dashboard
ID_CLI: 'cid-cli', // created via cli tool
// client type enums
TYPE_EXTERNAL: 'external',
TYPE_BUILT_IN: 'built-in',
@@ -30,54 +33,25 @@ exports = module.exports = {
var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
clientdb = require('./clientdb.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:clients'),
eventlog = require('./eventlog.js'),
hat = require('./hat.js'),
accesscontrol = require('./accesscontrol.js'),
tokendb = require('./tokendb.js'),
users = require('./users.js'),
UsersError = users.UsersError,
util = require('util'),
uuid = require('uuid'),
_ = require('underscore');
function ClientsError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(ClientsError, Error);
ClientsError.INVALID_SCOPE = 'Invalid scope';
ClientsError.INVALID_CLIENT = 'Invalid client';
ClientsError.INVALID_TOKEN = 'Invalid token';
ClientsError.BAD_FIELD = 'Bad field';
ClientsError.NOT_FOUND = 'Not found';
ClientsError.INTERNAL_ERROR = 'Internal Error';
ClientsError.NOT_ALLOWED = 'Not allowed to remove this client';
function validateClientName(name) {
assert.strictEqual(typeof name, 'string');
if (name.length < 1) return new ClientsError(ClientsError.BAD_FIELD, 'Name must be atleast 1 character');
if (name.length > 128) return new ClientsError(ClientsError.BAD_FIELD, 'Name too long');
if (name.length < 1) return new BoxError(BoxError.BAD_FIELD, 'name must be atleast 1 character', { field: 'name' });
if (name.length > 128) return new BoxError(BoxError.BAD_FIELD, 'name too long', { field: 'name' });
if (/[^a-zA-Z0-9-]/.test(name)) return new ClientsError(ClientsError.BAD_FIELD, 'Username can only contain alphanumerals and dash');
if (/[^a-zA-Z0-9-]/.test(name)) return new BoxError(BoxError.BAD_FIELD, 'name can only contain alphanumerals and dash', { field: 'name' });
return null;
}
@@ -85,7 +59,7 @@ function validateClientName(name) {
function validateTokenName(name) {
assert.strictEqual(typeof name, 'string');
if (name.length > 64) return new ClientsError(ClientsError.BAD_FIELD, 'Name too long');
if (name.length > 64) return new BoxError(BoxError.BAD_FIELD, 'name too long', { field: 'name' });
return null;
}
@@ -98,7 +72,7 @@ function add(appId, type, redirectURI, scope, callback) {
assert.strictEqual(typeof callback, 'function');
var error = accesscontrol.validateScopeString(scope);
if (error) return callback(new ClientsError(ClientsError.INVALID_SCOPE, error.message));
if (error) return callback(error);
error = validateClientName(appId);
if (error) return callback(error);
@@ -127,8 +101,8 @@ function get(id, callback) {
assert.strictEqual(typeof callback, 'function');
clientdb.get(id, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such client'));
if (error) return callback(error);
callback(null, result);
});
}
@@ -138,8 +112,8 @@ function del(id, callback) {
assert.strictEqual(typeof callback, 'function');
clientdb.del(id, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such client'));
if (error) return callback(error);
callback(null, result);
});
}
@@ -148,7 +122,7 @@ function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
clientdb.getAll(function (error, results) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, []);
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, []);
if (error) return callback(error);
var tmp = [];
@@ -190,8 +164,8 @@ function getByAppIdAndType(appId, type, callback) {
assert.strictEqual(typeof callback, 'function');
clientdb.getByAppIdAndType(appId, type, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such client'));
if (error) return callback(error);
callback(null, result);
});
}
@@ -202,7 +176,7 @@ function getTokensByUserId(clientId, userId, callback) {
assert.strictEqual(typeof callback, 'function');
tokendb.getByIdentifierAndClientId(userId, clientId, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) {
if (error && error.reason === BoxError.NOT_FOUND) {
// this can mean either that there are no tokens or the clientId is actually unknown
get(clientId, function (error/*, result*/) {
if (error) return callback(error);
@@ -221,7 +195,7 @@ function delTokensByUserId(clientId, userId, callback) {
assert.strictEqual(typeof callback, 'function');
tokendb.delByIdentifierAndClientId(userId, clientId, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) {
if (error && error.reason === BoxError.NOT_FOUND) {
// this can mean either that there are no tokens or the clientId is actually unknown
get(clientId, function (error/*, result*/) {
if (error) return callback(error);
@@ -243,10 +217,9 @@ function delByAppIdAndType(appId, type, callback) {
if (error) return callback(error);
tokendb.delByClientId(result.id, function (error) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
if (error && error.reason !== BoxError.NOT_FOUND) return callback(error);
clientdb.delByAppIdAndType(appId, type, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such client'));
if (error) return callback(error);
callback(null);
@@ -270,11 +243,10 @@ function addTokenByUserId(clientId, userId, expiresAt, options, callback) {
if (error) return callback(error);
users.get(userId, function (error, user) {
if (error && error.reason === UsersError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such user'));
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
if (error) return callback(error);
accesscontrol.scopesForUser(user, function (error, userScopes) {
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
if (error) return callback(error);
const scope = accesscontrol.canonicalScopeString(result.scope);
const authorizedScopes = accesscontrol.intersectScopes(userScopes, scope.split(','));
@@ -290,7 +262,7 @@ function addTokenByUserId(clientId, userId, expiresAt, options, callback) {
};
tokendb.add(token, function (error) {
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback(null, {
accessToken: token.accessToken,
@@ -305,15 +277,14 @@ function addTokenByUserId(clientId, userId, expiresAt, options, callback) {
});
}
// this issues a cid-cli token that does not require a password in various routes
function issueDeveloperToken(userObject, auditSource, callback) {
assert.strictEqual(typeof userObject, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
const expiresAt = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
const expiresAt = Date.now() + (30 * 24 * 60 * 60 * 1000); // cli tokens are valid for a month
addTokenByUserId('cid-cli', userObject.id, expiresAt, {}, function (error, result) {
addTokenByUserId(exports.ID_CLI, userObject.id, expiresAt, {}, function (error, result) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { userId: userObject.id, user: users.removePrivateFields(userObject) });
@@ -331,8 +302,7 @@ function delToken(clientId, tokenId, callback) {
if (error) return callback(error);
tokendb.del(tokenId, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.INVALID_TOKEN, 'Invalid token'));
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback(null);
});
@@ -348,9 +318,9 @@ function addDefaultClients(origin, callback) {
// The domain might have changed, therefor we have to update the record
// id, appId, type, clientSecret, redirectURI, scope
async.series([
clientdb.upsert.bind(null, 'cid-webadmin', 'Settings', 'built-in', 'secret-webadmin', origin, '*'),
clientdb.upsert.bind(null, 'cid-sdk', 'SDK', 'built-in', 'secret-sdk', origin, '*'),
clientdb.upsert.bind(null, 'cid-cli', 'Cloudron Tool', 'built-in', 'secret-cli', origin, '*')
clientdb.upsert.bind(null, exports.ID_WEBADMIN, 'Settings', 'built-in', 'secret-webadmin', origin, '*'),
clientdb.upsert.bind(null, exports.ID_SDK, 'SDK', 'built-in', 'secret-sdk', origin, '*'),
clientdb.upsert.bind(null, exports.ID_CLI, 'Cloudron Tool', 'built-in', 'secret-cli', origin, '*')
], callback);
}
+79 -140
View File
@@ -1,12 +1,9 @@
'use strict';
exports = module.exports = {
CloudronError: CloudronError,
initialize: initialize,
uninitialize: uninitialize,
getConfig: getConfig,
getDisks: getDisks,
getLogs: getLogs,
reboot: reboot,
@@ -19,24 +16,22 @@ exports = module.exports = {
setDashboardAndMailDomain: setDashboardAndMailDomain,
renewCerts: renewCerts,
runSystemChecks: runSystemChecks,
setupDashboard: setupDashboard,
// exposed for testing
_checkDiskSpace: checkDiskSpace
runSystemChecks: runSystemChecks,
};
var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
auditSource = require('./auditsource.js'),
backups = require('./backups.js'),
BoxError = require('./boxerror.js'),
clients = require('./clients.js'),
config = require('./config.js'),
constants = require('./constants.js'),
cron = require('./cron.js'),
debug = require('debug')('box:cloudron'),
domains = require('./domains.js'),
DomainsError = require('./domains.js').DomainsError,
df = require('@sindresorhus/df'),
eventlog = require('./eventlog.js'),
custom = require('./custom.js'),
fs = require('fs'),
@@ -47,49 +42,24 @@ var apps = require('./apps.js'),
paths = require('./paths.js'),
platform = require('./platform.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
shell = require('./shell.js'),
spawn = require('child_process').spawn,
split = require('split'),
tasks = require('./tasks.js'),
users = require('./users.js'),
util = require('util');
users = require('./users.js');
var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function CloudronError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(CloudronError, Error);
CloudronError.BAD_FIELD = 'Field error';
CloudronError.INTERNAL_ERROR = 'Internal Error';
CloudronError.EXTERNAL_ERROR = 'External Error';
CloudronError.BAD_STATE = 'Bad state';
CloudronError.ALREADY_UPTODATE = 'No Update Available';
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
runStartupTasks();
callback();
notifyUpdate(callback);
}
function uninitialize(callback) {
@@ -113,13 +83,32 @@ function onActivated(callback) {
], callback);
}
function notifyUpdate(callback) {
assert.strictEqual(typeof callback, 'function');
const version = safe.fs.readFileSync(paths.VERSION_FILE, 'utf8');
if (version === constants.VERSION) return callback();
eventlog.add(eventlog.ACTION_UPDATE_FINISH, auditSource.CRON, { errorMessage: '', oldVersion: version || 'dev', newVersion: constants.VERSION }, function (error) {
if (error) return callback(error);
tasks.setCompletedByType(tasks.TASK_UPDATE, { error: null }, function (error) {
if (error && error.reason !== BoxError.NOT_FOUND) return callback(error); // when hotfixing, task may not exist
safe.fs.writeFileSync(paths.VERSION_FILE, constants.VERSION, 'utf8');
callback();
});
});
}
// each of these tasks can fail. we will add some routes to fix/re-run them
function runStartupTasks() {
// configure nginx to be reachable by IP
reverseProxy.configureDefaultServer(NOOP_CALLBACK);
reverseProxy.writeDefaultConfig(NOOP_CALLBACK);
// always generate webadmin config since we have no versioning mechanism for the ejs
if (config.adminDomain()) reverseProxy.writeAdminConfig(config.adminDomain(), NOOP_CALLBACK);
if (settings.adminDomain()) reverseProxy.writeAdminConfig(settings.adminDomain(), NOOP_CALLBACK);
// check activation state and start the platform
users.isActivated(function (error, activated) {
@@ -130,49 +119,22 @@ function runStartupTasks() {
});
}
function getDisks(callback) {
assert.strictEqual(typeof callback, 'function');
var disks = {
boxDataDisk: null,
platformDataDisk: null,
appsDataDisk: null
};
df.file(paths.BOX_DATA_DIR).then(function (result) {
disks.boxDataDisk = result.filesystem;
return df.file(paths.PLATFORM_DATA_DIR);
}).then(function (result) {
disks.platformDataDisk = result.filesystem;
return df.file(paths.APPS_DATA_DIR);
}).then(function (result) {
disks.appsDataDisk = result.filesystem;
callback(null, disks);
}).catch(function (error) {
callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
});
}
function getConfig(callback) {
assert.strictEqual(typeof callback, 'function');
settings.getAll(function (error, allSettings) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
if (error) return callback(error);
// be picky about what we send out here since this is sent for 'normal' users as well
callback(null, {
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
adminDomain: config.adminDomain(),
adminFqdn: config.adminFqdn(),
mailFqdn: config.mailFqdn(),
version: config.version(),
isDemo: config.isDemo(),
memory: os.totalmem(),
provider: config.provider(),
apiServerOrigin: settings.apiServerOrigin(),
webServerOrigin: settings.webServerOrigin(),
adminDomain: settings.adminDomain(),
adminFqdn: settings.adminFqdn(),
mailFqdn: settings.mailFqdn(),
version: constants.VERSION,
isDemo: settings.isDemo(),
provider: settings.provider(),
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
uiSpec: custom.uiSpec()
});
@@ -191,21 +153,20 @@ function isRebootRequired(callback) {
}
// called from cron.js
function runSystemChecks() {
function runSystemChecks(callback) {
assert.strictEqual(typeof callback, 'function');
async.parallel([
checkBackupConfiguration,
checkDiskSpace,
checkMailStatus,
checkRebootRequired
], function (error) {
debug('runSystemChecks: done', error);
});
], callback);
}
function checkBackupConfiguration(callback) {
assert.strictEqual(typeof callback, 'function');
debug('Checking backup configuration');
debug('checking backup configuration');
backups.checkConfiguration(function (error, message) {
if (error) return callback(error);
@@ -214,45 +175,6 @@ function checkBackupConfiguration(callback) {
});
}
function checkDiskSpace(callback) {
assert.strictEqual(typeof callback, 'function');
debug('Checking disk space');
getDisks(function (error, disks) {
if (error) {
debug('df error %s', error.message);
return callback();
}
df().then(function (entries) {
/*
[{
filesystem: '/dev/disk1',
size: 499046809600,
used: 443222245376,
available: 55562420224,
capacity: 0.89,
mountpoint: '/'
}, ...]
*/
var oos = entries.some(function (entry) {
// ignore other filesystems but where box, app and platform data is
if (entry.filesystem !== disks.boxDataDisk && entry.filesystem !== disks.platformDataDisk && entry.filesystem !== disks.appsDataDisk) return false;
return (entry.available <= (1.25 * 1024 * 1024 * 1024)); // 1.5G
});
debug('Disk space checked. ok: %s', !oos);
notifications.alert(notifications.ALERT_DISK_SPACE, 'Server is running out of disk space', oos ? JSON.stringify(entries, null, 4) : '', callback);
}).catch(function (error) {
if (error) console.error(error);
callback();
});
});
}
function checkMailStatus(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -298,7 +220,7 @@ function getLogs(unit, options, callback) {
// need to handle box.log without subdir
if (unit === 'box') args.push(path.join(paths.LOG_DIR, 'box.log'));
else if (unit.startsWith('crash-')) args.push(path.join(paths.CRASH_LOG_DIR, unit.slice(6) + '.log'));
else return callback(new CloudronError(CloudronError.BAD_FIELD, 'No such unit'));
else return callback(new BoxError(BoxError.BAD_FIELD, 'No such unit', { field: 'unit' }));
var cp = spawn('/usr/bin/tail', args);
@@ -331,20 +253,23 @@ function prepareDashboardDomain(domain, auditSource, callback) {
debug(`prepareDashboardDomain: ${domain}`);
domains.get(domain, function (error, domainObject) {
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new CloudronError(CloudronError.BAD_FIELD, 'No such domain'));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
if (error) return callback(error);
const fqdn = domains.fqdn(constants.ADMIN_LOCATION, domainObject);
apps.getAll(function (error, result) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
if (error) return callback(error);
const conflict = result.filter(app => app.fqdn === fqdn);
if (conflict.length) return callback(new CloudronError(CloudronError.BAD_STATE, 'Dashboard location conflicts with an existing app'));
if (conflict.length) return callback(new BoxError(BoxError.BAD_STATE, 'Dashboard location conflicts with an existing app'));
let task = tasks.startTask(tasks.TASK_PREPARE_DASHBOARD_DOMAIN, [ domain, auditSource ]);
task.on('error', (error) => callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)));
task.on('start', (taskId) => callback(null, taskId));
tasks.add(tasks.TASK_PREPARE_DASHBOARD_DOMAIN, [ domain, auditSource ], function (error, taskId) {
if (error) return callback(error);
tasks.startTask(taskId, {}, NOOP_CALLBACK);
callback(null, taskId);
});
});
});
}
@@ -358,19 +283,18 @@ function setDashboardDomain(domain, auditSource, callback) {
debug(`setDashboardDomain: ${domain}`);
domains.get(domain, function (error, domainObject) {
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new CloudronError(CloudronError.BAD_FIELD, 'No such domain'));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
if (error) return callback(error);
reverseProxy.writeAdminConfig(domain, function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
if (error) return callback(error);
const fqdn = domains.fqdn(constants.ADMIN_LOCATION, domainObject);
config.setAdminDomain(domain);
config.setAdminFqdn(fqdn);
clients.addDefaultClients(config.adminOrigin(), function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
async.series([
(done) => settings.setAdmin(domain, fqdn, done),
(done) => clients.addDefaultClients(settings.adminOrigin(), done)
], function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_DASHBOARD_DOMAIN_UPDATE, auditSource, { domain: domain, fqdn: fqdn });
@@ -397,12 +321,27 @@ function setDashboardAndMailDomain(domain, auditSource, callback) {
});
}
function setupDashboard(auditSource, progressCallback, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
async.series([
domains.prepareDashboardDomain.bind(null, settings.adminDomain(), auditSource, progressCallback),
setDashboardDomain.bind(null, settings.adminDomain(), auditSource)
], callback);
}
function renewCerts(options, auditSource, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
let task = tasks.startTask(tasks.TASK_RENEW_CERTS, [ options, auditSource ]);
task.on('error', (error) => callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)));
task.on('start', (taskId) => callback(null, taskId));
tasks.add(tasks.TASK_RENEW_CERTS, [ options, auditSource ], function (error, taskId) {
if (error) return callback(error);
tasks.startTask(taskId, {}, NOOP_CALLBACK);
callback(null, taskId);
});
}
+10
View File
@@ -30,3 +30,13 @@ LoadPlugin "table"
</Result>
</Table>
</Plugin>
<Plugin python>
<Module du>
<Path>
Instance "<%= appId %>"
Dir "<%= appDataDir %>"
</Path>
</Module>
</Plugin>
-203
View File
@@ -1,203 +0,0 @@
'use strict';
exports = module.exports = {
baseDir: baseDir,
// values set here will be lost after a upgrade/update. use the sqlite database
// for persistent values that need to be backed up
get: get,
set: set,
// ifdefs to check environment
CLOUDRON: process.env.BOX_ENV === 'cloudron',
TEST: process.env.BOX_ENV === 'test',
// convenience getters
provider: provider,
apiServerOrigin: apiServerOrigin,
webServerOrigin: webServerOrigin,
adminDomain: adminDomain,
setFqdn: setAdminDomain,
setAdminDomain: setAdminDomain,
setAdminFqdn: setAdminFqdn,
version: version,
database: database,
// these values are derived
adminOrigin: adminOrigin,
internalAdminOrigin: internalAdminOrigin,
sysadminOrigin: sysadminOrigin, // localhost routes
adminFqdn: adminFqdn,
mailFqdn: mailFqdn,
hasIPv6: hasIPv6,
isDemo: isDemo,
// for testing resets to defaults
_reset: _reset
};
var assert = require('assert'),
fs = require('fs'),
path = require('path'),
safe = require('safetydance'),
_ = require('underscore');
// assert on unknown environment can't proceed
assert(exports.CLOUDRON || exports.TEST, 'Unknown environment. This should not happen!');
var data = { };
function baseDir() {
const homeDir = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
if (exports.CLOUDRON) return homeDir;
if (exports.TEST) return path.join(homeDir, '.cloudron_test');
// cannot reach
}
const cloudronConfigFileName = exports.CLOUDRON ? '/etc/cloudron/cloudron.conf' : path.join(baseDir(), 'cloudron.conf');
function saveSync() {
// only save values we want to have in the cloudron.conf, see start.sh
var conf = {
apiServerOrigin: data.apiServerOrigin,
webServerOrigin: data.webServerOrigin,
adminDomain: data.adminDomain,
adminFqdn: data.adminFqdn,
provider: data.provider,
isDemo: data.isDemo
};
fs.writeFileSync(cloudronConfigFileName, JSON.stringify(conf, null, 4)); // functions are ignored by JSON.stringify
}
function _reset(callback) {
safe.fs.unlinkSync(cloudronConfigFileName);
initConfig();
if (callback) callback();
}
function initConfig() {
// setup defaults
data.adminFqdn = '';
data.adminDomain = '';
data.port = 3000;
data.apiServerOrigin = null;
data.webServerOrigin = null;
data.provider = 'generic';
data.smtpPort = 2525; // this value comes from mail container
data.sysadminPort = 3001;
data.ldapPort = 3002;
data.dockerProxyPort = 3003;
// keep in sync with start.sh
data.database = {
hostname: '127.0.0.1',
username: 'root',
password: 'password',
port: 3306,
name: 'box'
};
// overrides for local testings
if (exports.TEST) {
data.port = 5454;
data.apiServerOrigin = 'http://localhost:6060'; // hock doesn't support https
// see setupTest script how the mysql-server is run
data.database.hostname = require('child_process').execSync('docker inspect -f "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" mysql-server').toString().trim();
}
// overwrite defaults with saved config
var existingData = safe.JSON.parse(safe.fs.readFileSync(cloudronConfigFileName, 'utf8'));
_.extend(data, existingData);
}
initConfig();
// set(obj) or set(key, value)
function set(key, value) {
if (typeof key === 'object') {
var obj = key;
for (var k in obj) {
assert(k in data, 'config.js is missing key "' + k + '"');
data[k] = obj[k];
}
} else {
data = safe.set(data, key, value);
}
saveSync();
}
function get(key) {
assert.strictEqual(typeof key, 'string');
return safe.query(data, key);
}
function apiServerOrigin() {
return get('apiServerOrigin');
}
function webServerOrigin() {
return get('webServerOrigin');
}
function setAdminDomain(domain) {
set('adminDomain', domain);
}
function adminDomain() {
return get('adminDomain');
}
function setAdminFqdn(adminFqdn) {
set('adminFqdn', adminFqdn);
}
function adminFqdn() {
return get('adminFqdn');
}
function mailFqdn() {
return adminFqdn();
}
function adminOrigin() {
return 'https://' + adminFqdn();
}
function internalAdminOrigin() {
return 'http://127.0.0.1:' + get('port');
}
function sysadminOrigin() {
return 'http://127.0.0.1:' + get('sysadminPort');
}
function version() {
if (exports.TEST) return '3.0.0-test';
return fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim();
}
function database() {
return get('database');
}
function isDemo() {
return get('isDemo') === true;
}
function provider() {
return get('provider');
}
function hasIPv6() {
const IPV6_PROC_FILE = '/proc/net/if_inet6';
// on contabo, /proc/net/if_inet6 is an empty file. so just exists is not enough
return fs.existsSync(IPV6_PROC_FILE) && fs.readFileSync(IPV6_PROC_FILE, 'utf8').trim().length !== 0;
}
+18 -1
View File
@@ -1,5 +1,11 @@
'use strict';
let fs = require('fs'),
path = require('path');
const CLOUDRON = process.env.BOX_ENV === 'cloudron',
TEST = process.env.BOX_ENV === 'test';
exports = module.exports = {
SMTP_LOCATION: 'smtp',
IMAP_LOCATION: 'imap',
@@ -18,6 +24,12 @@ exports = module.exports = {
ADMIN_LOCATION: 'my',
PORT: CLOUDRON ? 3000 : 5454,
INTERNAL_SMTP_PORT: 2525, // this value comes from the mail container
SYSADMIN_PORT: 3001, // unused
LDAP_PORT: 3002,
DOCKER_PROXY_PORT: 3003,
NGINX_DEFAULT_CONFIG_FILE_NAME: 'default.conf',
GHOST_USER_FILE: '/tmp/cloudron_ghost.json',
@@ -30,6 +42,11 @@ exports = module.exports = {
AUTOUPDATE_PATTERN_NEVER: 'never',
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8)
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8),
CLOUDRON: CLOUDRON,
TEST: TEST,
VERSION: process.env.BOX_ENV === 'cloudron' ? fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim() : '4.2.0-test'
};
+13 -5
View File
@@ -15,7 +15,6 @@ var appHealthMonitor = require('./apphealthmonitor.js'),
auditSource = require('./auditsource.js'),
backups = require('./backups.js'),
cloudron = require('./cloudron.js'),
config = require('./config.js'),
constants = require('./constants.js'),
CronJob = require('cron').CronJob,
debug = require('debug')('box:cron'),
@@ -24,6 +23,7 @@ var appHealthMonitor = require('./apphealthmonitor.js'),
janitor = require('./janitor.js'),
scheduler = require('./scheduler.js'),
settings = require('./settings.js'),
system = require('./system.js'),
updater = require('./updater.js'),
updateChecker = require('./updatechecker.js');
@@ -35,6 +35,7 @@ var gJobs = {
backup: null,
boxUpdateChecker: null,
systemChecks: null,
diskSpaceChecker: null,
certificateRenew: null,
cleanupBackups: null,
cleanupEventlog: null,
@@ -106,9 +107,16 @@ function recreateJobs(tz) {
if (gJobs.systemChecks) gJobs.systemChecks.stop();
gJobs.systemChecks = new CronJob({
cronTime: '00 30 * * * *', // every 30 minutes. if you change this interval, change the notification messages with correct duration
onTick: cloudron.runSystemChecks,
onTick: () => cloudron.runSystemChecks(NOOP_CALLBACK),
start: true,
timeZone: tz
});
if (gJobs.diskSpaceChecker) gJobs.diskSpaceChecker.stop();
gJobs.diskSpaceChecker = new CronJob({
cronTime: '00 30 * * * *', // every 30 minutes. if you change this interval, change the notification messages with correct duration
onTick: () => system.checkDiskSpace(NOOP_CALLBACK),
start: true,
runOnInit: true, // run system check immediately
timeZone: tz
});
@@ -165,7 +173,7 @@ function recreateJobs(tz) {
if (gJobs.schedulerSync) gJobs.schedulerSync.stop();
gJobs.schedulerSync = new CronJob({
cronTime: config.TEST ? '*/10 * * * * *' : '00 */1 * * * *', // every minute
cronTime: constants.TEST ? '*/10 * * * * *' : '00 */1 * * * *', // every minute
onTick: scheduler.sync,
start: true,
timeZone: tz
@@ -248,7 +256,7 @@ function dynamicDnsChanged(enabled) {
if (enabled) {
gJobs.dynamicDns = new CronJob({
cronTime: '00 */10 * * * *',
cronTime: '5 * * * * *', // we only update the records if the ip has changed.
onTick: dyndns.sync.bind(null, auditSource.CRON, NOOP_CALLBACK),
start: true,
timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack
+6 -7
View File
@@ -1,7 +1,6 @@
'use strict';
let config = require('./config.js'),
debug = require('debug')('box:features'),
let debug = require('debug')('box:custom'),
lodash = require('lodash'),
paths = require('./paths.js'),
safe = require('safetydance'),
@@ -32,17 +31,17 @@ const DEFAULT_SPEC = {
remoteSupport: true,
ticketFormBody:
'Use this form to open support tickets. You can also write directly to [support@cloudron.io](mailto:support@cloudron.io).\n\n'
+ `* [Knowledge Base & App Docs](${config.webServerOrigin()}/documentation/apps/?support_view)\n`
+ `* [Custom App Packaging & API](${config.webServerOrigin()}/developer/packaging/?support_view)\n`
+ '* [Knowledge Base & App Docs](https://cloudron.io/documentation/apps/?support_view)\n'
+ '* [Custom App Packaging & API](https://cloudron.io/developer/packaging/?support_view)\n'
+ '* [Forum](https://forum.cloudron.io/)\n\n',
submitTickets: true
},
alerts: {
email: '',
notifyCloudronAdmins: false
notifyCloudronAdmins: true
},
footer: {
body: '&copy; 2019 [Cloudron](https://cloudron.io) [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)'
body: '&copy; 2020 [Cloudron](https://cloudron.io) [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)'
}
};
@@ -64,4 +63,4 @@ function uiSpec() {
function spec() {
return gSpec;
}
}
+28 -14
View File
@@ -14,8 +14,9 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
child_process = require('child_process'),
config = require('./config.js'),
constants = require('./constants.js'),
mysql = require('mysql'),
once = require('once'),
util = require('util');
@@ -23,25 +24,38 @@ var assert = require('assert'),
var gConnectionPool = null,
gDefaultConnection = null;
const gDatabase = {
hostname: '127.0.0.1',
username: 'root',
password: 'password',
port: 3306,
name: 'box'
};
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
if (gConnectionPool !== null) return callback(null);
if (constants.TEST) {
// see setupTest script how the mysql-server is run
gDatabase.hostname = require('child_process').execSync('docker inspect -f "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" mysql-server').toString().trim();
}
gConnectionPool = mysql.createPool({
connectionLimit: 5, // this has to be > 1 since we store one connection as 'default'. the rest for transactions
host: config.database().hostname,
user: config.database().username,
password: config.database().password,
port: config.database().port,
database: config.database().name,
host: gDatabase.hostname,
user: gDatabase.username,
password: gDatabase.password,
port: gDatabase.port,
database: gDatabase.name,
multipleStatements: false,
ssl: false,
timezone: 'Z' // mysql follows the SYSTEM timezone. on Cloudron, this is UTC
});
gConnectionPool.on('connection', function (connection) {
connection.query('USE ' + config.database().name);
connection.query('USE ' + gDatabase.name);
connection.query('SET SESSION sql_mode = \'strict_all_tables\'');
});
@@ -87,19 +101,19 @@ function clear(callback) {
assert.strictEqual(typeof callback, 'function');
var cmd = util.format('mysql --host="%s" --user="%s" --password="%s" -Nse "SHOW TABLES" %s | grep -v "^migrations$" | while read table; do mysql --host="%s" --user="%s" --password="%s" -e "SET FOREIGN_KEY_CHECKS = 0; TRUNCATE TABLE $table" %s; done',
config.database().hostname, config.database().username, config.database().password, config.database().name,
config.database().hostname, config.database().username, config.database().password, config.database().name);
gDatabase.hostname, gDatabase.username, gDatabase.password, gDatabase.name,
gDatabase.hostname, gDatabase.username, gDatabase.password, gDatabase.name);
async.series([
child_process.exec.bind(null, cmd),
require('./clientdb.js')._addDefaultClients
require('./clients.js').addDefaultClients.bind(null, 'https://admin-localhost')
], callback);
}
function beginTransaction(callback) {
assert.strictEqual(typeof callback, 'function');
if (gConnectionPool === null) return callback(new Error('No database connection pool.'));
if (gConnectionPool === null) return callback(new BoxError(BoxError.DATABASE_ERROR, 'No database connection pool.'));
gConnectionPool.getConnection(function (error, connection) {
if (error) {
@@ -143,7 +157,7 @@ function query() {
var callback = args[args.length - 1];
assert.strictEqual(typeof callback, 'function');
if (gDefaultConnection === null) return callback(new Error('No connection to database'));
if (gDefaultConnection === null) return callback(new BoxError(BoxError.DATABASE_ERROR, 'No connection to database'));
args[args.length -1 ] = function (error, result) {
if (error && error.fatal) {
@@ -178,7 +192,7 @@ function importFromFile(file, callback) {
assert.strictEqual(typeof file, 'string');
assert.strictEqual(typeof callback, 'function');
var cmd = `/usr/bin/mysql -h "${config.database().hostname}" -u ${config.database().username} -p${config.database().password} ${config.database().name} < ${file}`;
var cmd = `/usr/bin/mysql -h "${gDatabase.hostname}" -u ${gDatabase.username} -p${gDatabase.password} ${gDatabase.name} < ${file}`;
async.series([
query.bind(null, 'CREATE DATABASE IF NOT EXISTS box'),
@@ -190,7 +204,7 @@ function exportToFile(file, callback) {
assert.strictEqual(typeof file, 'string');
assert.strictEqual(typeof callback, 'function');
var cmd = `/usr/bin/mysqldump -h "${config.database().hostname}" -u root -p${config.database().password} --single-transaction --routines --triggers ${config.database().name} > "${file}"`;
var cmd = `/usr/bin/mysqldump -h "${gDatabase.hostname}" -u root -p${gDatabase.password} --single-transaction --routines --triggers ${gDatabase.name} > "${file}"`;
child_process.exec(cmd, callback);
}
-34
View File
@@ -1,34 +0,0 @@
/* jslint node:true */
'use strict';
exports = module.exports = DatabaseError;
var assert = require('assert'),
util = require('util');
function DatabaseError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(DatabaseError, Error);
DatabaseError.INTERNAL_ERROR = 'Internal error';
DatabaseError.ALREADY_EXISTS = 'Entry already exist';
DatabaseError.NOT_FOUND = 'Record not found';
DatabaseError.BAD_FIELD = 'Invalid field';
DatabaseError.IN_USE = 'In Use';
+21 -17
View File
@@ -11,14 +11,18 @@ exports = module.exports = {
};
var assert = require('assert'),
config = require('../config.js'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:dns/caas'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
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');
@@ -58,15 +62,15 @@ function upsert(domainObject, location, type, values, callback) {
};
superagent
.post(config.apiServerOrigin() + '/api/v1/caas/domains/' + fqdn)
.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 DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 400) return callback(new DomainsError(DomainsError.BAD_FIELD, result.body.message));
if (result.statusCode === 420) return callback(new DomainsError(DomainsError.STILL_BUSY));
if (result.statusCode !== 201) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
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);
});
@@ -84,12 +88,12 @@ function get(domainObject, location, type, callback) {
debug('get: zoneName: %s subdomain: %s type: %s fqdn: %s', domainObject.domain, location, type, fqdn);
superagent
.get(config.apiServerOrigin() + '/api/v1/caas/domains/' + fqdn)
.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 DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
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);
});
@@ -111,16 +115,16 @@ function del(domainObject, location, type, values, callback) {
};
superagent
.del(config.apiServerOrigin() + '/api/v1/caas/domains/' + getFqdn(location, domainObject.domain))
.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 DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 400) return callback(new DomainsError(DomainsError.BAD_FIELD, result.body.message));
if (result.statusCode === 420) return callback(new DomainsError(DomainsError.STILL_BUSY));
if (result.statusCode === 404) return callback(new DomainsError(DomainsError.NOT_FOUND));
if (result.statusCode !== 204) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
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);
});
@@ -145,7 +149,7 @@ function verifyDnsConfig(domainObject, callback) {
const dnsConfig = domainObject.config;
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
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';
+41 -33
View File
@@ -12,10 +12,10 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:dns/cloudflare'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
superagent = require('superagent'),
util = require('util'),
waitForDns = require('./waitfordns.js'),
@@ -37,15 +37,32 @@ function translateRequestError(result, callback) {
assert.strictEqual(typeof result, 'object');
assert.strictEqual(typeof callback, 'function');
if (result.statusCode === 404) return callback(new DomainsError(DomainsError.NOT_FOUND, util.format('%s %j', result.statusCode, 'API does not exist')));
if (result.statusCode === 422) return callback(new DomainsError(DomainsError.BAD_FIELD, result.body.message));
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND, util.format('%s %j', result.statusCode, 'API does not exist')));
if (result.statusCode === 422) return callback(new BoxError(BoxError.BAD_FIELD, result.body.message));
if ((result.statusCode === 400 || result.statusCode === 401 || result.statusCode === 403) && result.body.errors.length > 0) {
let error = result.body.errors[0];
let message = `message: ${error.message} statusCode: ${result.statusCode} code:${error.code}`;
return callback(new DomainsError(DomainsError.ACCESS_DENIED, message));
return callback(new BoxError(BoxError.ACCESS_DENIED, message));
}
callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
}
function createRequest(method, url, dnsConfig) {
assert.strictEqual(typeof method, 'string');
assert.strictEqual(typeof url, 'string');
assert.strictEqual(typeof dnsConfig, 'object');
let request = superagent(method, url)
.timeout(30 * 1000);
if (dnsConfig.tokenType === 'GlobalApiKey') {
request.set('X-Auth-Key', dnsConfig.token).set('X-Auth-Email', dnsConfig.email);
} else {
request.set('Authorization', 'Bearer ' + dnsConfig.token);
}
return request;
}
function getZoneByName(dnsConfig, zoneName, callback) {
@@ -53,14 +70,11 @@ function getZoneByName(dnsConfig, zoneName, callback) {
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof callback, 'function');
superagent.get(CLOUDFLARE_ENDPOINT + '/zones?name=' + zoneName + '&status=active')
.set('X-Auth-Key', dnsConfig.token)
.set('X-Auth-Email', dnsConfig.email)
.timeout(30 * 1000)
createRequest('GET', CLOUDFLARE_ENDPOINT + '/zones?name=' + zoneName + '&status=active', dnsConfig)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
if (!result.body.result.length) return callback(new DomainsError(DomainsError.NOT_FOUND, util.format('%s %j', result.statusCode, result.body)));
if (!result.body.result.length) return callback(new BoxError(BoxError.NOT_FOUND, util.format('%s %j', result.statusCode, result.body)));
callback(null, result.body.result[0]);
});
@@ -74,11 +88,8 @@ function getDnsRecords(dnsConfig, zoneId, fqdn, type, callback) {
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
superagent.get(CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records')
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
createRequest('GET', CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records', dnsConfig)
.query({ type: type, name: fqdn })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
@@ -132,11 +143,8 @@ function upsert(domainObject, location, type, values, callback) {
if (i >= dnsRecords.length) { // create a new record
debug(`upsert: Adding new record fqdn: ${fqdn}, zoneName: ${zoneName} proxied: false`);
superagent.post(CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records')
.set('X-Auth-Key', dnsConfig.token)
.set('X-Auth-Email', dnsConfig.email)
createRequest('POST', CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records', dnsConfig)
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return iteratorCallback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, iteratorCallback);
@@ -148,11 +156,8 @@ function upsert(domainObject, location, type, values, callback) {
debug(`upsert: Updating existing record fqdn: ${fqdn}, zoneName: ${zoneName} proxied: ${data.proxied}`);
superagent.put(CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records/' + dnsRecords[i].id)
.set('X-Auth-Key', dnsConfig.token)
.set('X-Auth-Email', dnsConfig.email)
createRequest('PUT', CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records/' + dnsRecords[i].id, dnsConfig)
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
++i; // increment, as we have consumed the record
@@ -217,10 +222,7 @@ function del(domainObject, location, type, values, callback) {
if (tmp.length === 0) return callback(null);
async.eachSeries(tmp, function (record, callback) {
superagent.del(CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records/' + record.id)
.set('X-Auth-Key', dnsConfig.token)
.set('X-Auth-Email', dnsConfig.email)
.timeout(30 * 1000)
createRequest('DELETE', CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records/' + record.id, dnsConfig)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
@@ -259,7 +261,7 @@ function wait(domainObject, location, type, value, options, callback) {
getDnsRecords(dnsConfig, zoneId, fqdn, type, function (error, dnsRecords) {
if (error) return callback(error);
if (dnsRecords.length === 0) return callback(new DomainsError(DomainsError.NOT_FOUND, 'Domain not found'));
if (dnsRecords.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Domain not found'));
if (!dnsRecords[0].proxied) return waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
@@ -277,28 +279,34 @@ function verifyDnsConfig(domainObject, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
if (!dnsConfig.email || typeof dnsConfig.email !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'email must be a non-empty string'));
// token can be api token or global api key
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'token must be a non-empty string', { field: 'token' }));
if (dnsConfig.tokenType !== 'GlobalApiKey' && dnsConfig.tokenType !== 'ApiToken') return callback(new BoxError(BoxError.BAD_FIELD, 'tokenType is required', { field: 'tokenType' }));
if (dnsConfig.tokenType === 'GlobalApiKey') {
if ('email' in dnsConfig && typeof dnsConfig.email !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'email must be a non-empty string', { field: 'email' }));
}
const ip = '127.0.0.1';
var credentials = {
token: dnsConfig.token,
email: dnsConfig.email
tokenType: dnsConfig.tokenType,
email: dnsConfig.email || null
};
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
if (error && error.code === 'ENOTFOUND') return callback(new BoxError(BoxError.BAD_FIELD, 'Unable to resolve nameservers for this domain', { field: 'nameservers' }));
if (error || !nameservers) return callback(new BoxError(BoxError.BAD_FIELD, error ? error.message : 'Unable to get nameservers', { field: 'nameservers' }));
getZoneByName(dnsConfig, zoneName, function(error, zone) {
if (error) return callback(error);
if (!_.isEqual(zone.name_servers.sort(), nameservers.sort())) {
debug('verifyDnsConfig: %j and %j do not match', nameservers, zone.name_servers);
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Cloudflare'));
return callback(new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to Cloudflare', { field: 'nameservers' }));
}
const location = 'cloudrontestdns';
+21 -23
View File
@@ -12,10 +12,10 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:dns/digitalocean'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
safe = require('safetydance'),
superagent = require('superagent'),
util = require('util'),
@@ -55,10 +55,10 @@ function getInternal(dnsConfig, zoneName, name, type, callback) {
.timeout(30 * 1000)
.retry(5)
.end(function (error, result) {
if (error && !error.response) return iteratorDone(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 404) return iteratorDone(new DomainsError(DomainsError.NOT_FOUND, formatError(result)));
if (result.statusCode === 403 || result.statusCode === 401) return iteratorDone(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return iteratorDone(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
if (error && !error.response) return iteratorDone(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 404) return iteratorDone(new BoxError(BoxError.NOT_FOUND, formatError(result)));
if (result.statusCode === 403 || result.statusCode === 401) return iteratorDone(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return iteratorDone(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
matchingRecords = matchingRecords.concat(result.body.domain_records.filter(function (record) {
return (record.type === type && record.name === name);
@@ -66,12 +66,10 @@ function getInternal(dnsConfig, zoneName, name, type, callback) {
nextPage = (result.body.links && result.body.links.pages) ? result.body.links.pages.next : null;
debug(`getInternal: next page - ${nextPage}`);
iteratorDone();
});
}, function () { return !!nextPage; }, function (error) {
debug('getInternal:', error, matchingRecords);
debug('getInternal:', error, JSON.stringify(matchingRecords));
if (error) return callback(error);
@@ -121,10 +119,10 @@ function upsert(domainObject, location, type, values, callback) {
.timeout(30 * 1000)
.retry(5)
.end(function (error, result) {
if (error && !error.response) return iteratorCallback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
if (result.statusCode === 422) return iteratorCallback(new DomainsError(DomainsError.BAD_FIELD, result.body.message));
if (result.statusCode !== 201) return iteratorCallback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
if (error && !error.response) return iteratorCallback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode === 422) return iteratorCallback(new BoxError(BoxError.BAD_FIELD, result.body.message));
if (result.statusCode !== 201) return iteratorCallback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
recordIds.push(safe.query(result.body, 'domain_record.id'));
@@ -140,10 +138,10 @@ function upsert(domainObject, location, type, values, callback) {
// increment, as we have consumed the record
++i;
if (error && !error.response) return iteratorCallback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
if (result.statusCode === 422) return iteratorCallback(new DomainsError(DomainsError.BAD_FIELD, result.body.message));
if (result.statusCode !== 200) return iteratorCallback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
if (error && !error.response) return iteratorCallback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode === 422) return iteratorCallback(new BoxError(BoxError.BAD_FIELD, result.body.message));
if (result.statusCode !== 200) return iteratorCallback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
recordIds.push(safe.query(result.body, 'domain_record.id'));
@@ -211,10 +209,10 @@ function del(domainObject, location, type, values, callback) {
.timeout(30 * 1000)
.retry(5)
.end(function (error, result) {
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 404) return callback(null);
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 204) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 204) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
debug('del: done');
@@ -243,7 +241,7 @@ function verifyDnsConfig(domainObject, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
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';
@@ -254,12 +252,12 @@ function verifyDnsConfig(domainObject, callback) {
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
if (error && error.code === 'ENOTFOUND') return callback(new BoxError(BoxError.BAD_FIELD, 'Unable to resolve nameservers for this domain', { field: 'nameservers' }));
if (error || !nameservers) return callback(new BoxError(BoxError.BAD_FIELD, error ? error.message : 'Unable to get nameservers', { field: 'nameservers' }));
if (nameservers.map(function (n) { return n.toLowerCase(); }).indexOf('ns1.digitalocean.com') === -1) {
debug('verifyDnsConfig: %j does not contains DO NS', nameservers);
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to DigitalOcean'));
return callback(new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to DigitalOcean', { field: 'nameservers' }));
}
const location = 'cloudrontestdns';
+15 -15
View File
@@ -11,10 +11,10 @@ exports = module.exports = {
};
var assert = require('assert'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:dns/gandi'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
superagent = require('superagent'),
util = require('util'),
waitForDns = require('./waitfordns.js');
@@ -57,10 +57,10 @@ function upsert(domainObject, location, type, values, callback) {
.timeout(30 * 1000)
.send(data)
.end(function (error, result) {
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
if (result.statusCode === 400) return callback(new DomainsError(DomainsError.BAD_FIELD, formatError(result)));
if (result.statusCode !== 201) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode === 400) return callback(new BoxError(BoxError.BAD_FIELD, formatError(result)));
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
return callback(null);
});
@@ -82,10 +82,10 @@ function get(domainObject, location, type, callback) {
.set('X-Api-Key', dnsConfig.token)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode === 404) return callback(null, [ ]);
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
debug('get: %j', result.body);
@@ -110,10 +110,10 @@ function del(domainObject, location, type, values, callback) {
.set('X-Api-Key', dnsConfig.token)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 404) return callback(null);
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 204) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 204) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
debug('del: done');
@@ -141,7 +141,7 @@ function verifyDnsConfig(domainObject, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'token must be a non-empty string', { field: 'token' }));
var credentials = {
token: dnsConfig.token
@@ -152,12 +152,12 @@ function verifyDnsConfig(domainObject, callback) {
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
if (error && error.code === 'ENOTFOUND') return callback(new BoxError(BoxError.BAD_FIELD, 'Unable to resolve nameservers for this domain', { field: 'nameservers' }));
if (error || !nameservers) return callback(new BoxError(BoxError.BAD_FIELD, error ? error.message : 'Unable to get nameservers', { field: 'nameservers' }));
if (!nameservers.every(function (n) { return n.toLowerCase().indexOf('.gandi.net') !== -1; })) {
debug('verifyDnsConfig: %j does not contain Gandi NS', nameservers);
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Gandi'));
return callback(new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to Gandi', { field: 'nameservers' }));
}
const location = 'cloudrontestdns';
+26 -26
View File
@@ -11,10 +11,10 @@ exports = module.exports = {
};
var assert = require('assert'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:dns/gcdns'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
GCDNS = require('@google-cloud/dns').DNS,
util = require('util'),
waitForDns = require('./waitfordns.js'),
@@ -49,20 +49,20 @@ function getZoneByName(dnsConfig, zoneName, callback) {
var gcdns = new GCDNS(getDnsCredentials(dnsConfig));
gcdns.getZones(function (error, zones) {
if (error && error.message === 'invalid_grant') return callback(new DomainsError(DomainsError.ACCESS_DENIED, 'The key was probably revoked'));
if (error && error.reason === 'No such domain') return callback(new DomainsError(DomainsError.NOT_FOUND, error.message));
if (error && error.code === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error && error.code === 404) return callback(new DomainsError(DomainsError.NOT_FOUND, error.message));
if (error && error.message === 'invalid_grant') return callback(new BoxError(BoxError.ACCESS_DENIED, 'The key was probably revoked'));
if (error && error.reason === 'No such domain') return callback(new BoxError(BoxError.NOT_FOUND, error.message));
if (error && error.code === 403) return callback(new BoxError(BoxError.ACCESS_DENIED, error.message));
if (error && error.code === 404) return callback(new BoxError(BoxError.NOT_FOUND, error.message));
if (error) {
debug('gcdns.getZones', error);
return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error));
return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
}
var zone = zones.filter(function (zone) {
return zone.metadata.dnsName.slice(0, -1) === zoneName; // the zone name contains a '.' at the end
})[0];
if (!zone) return callback(new DomainsError(DomainsError.NOT_FOUND, 'no such zone'));
if (!zone) return callback(new BoxError(BoxError.NOT_FOUND, 'no such zone'));
callback(null, zone); //zone.metadata ~= {name="", dnsName="", nameServers:[]}
});
@@ -85,10 +85,10 @@ function upsert(domainObject, location, type, values, callback) {
if (error) return callback(error);
zone.getRecords({ type: type, name: fqdn + '.' }, function (error, oldRecords) {
if (error && error.code === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error && error.code === 403) return callback(new BoxError(BoxError.ACCESS_DENIED, error.message));
if (error) {
debug('upsert->zone.getRecords', error);
return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
}
var newRecord = zone.record(type, {
@@ -98,11 +98,11 @@ function upsert(domainObject, location, type, values, callback) {
});
zone.createChange({ delete: oldRecords, add: newRecord }, function(error /*, change */) {
if (error && error.code === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error && error.code === 412) return callback(new DomainsError(DomainsError.STILL_BUSY, error.message));
if (error && error.code === 403) return callback(new BoxError(BoxError.ACCESS_DENIED, error.message));
if (error && error.code === 412) return callback(new BoxError(BoxError.BUSY, error.message));
if (error) {
debug('upsert->zone.createChange', error);
return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
}
callback(null);
@@ -130,8 +130,8 @@ function get(domainObject, location, type, callback) {
};
zone.getRecords(params, function (error, records) {
if (error && error.code === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error));
if (error && error.code === 403) return callback(new BoxError(BoxError.ACCESS_DENIED, error.message));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
if (records.length === 0) return callback(null, [ ]);
return callback(null, records[0].data);
@@ -154,18 +154,18 @@ function del(domainObject, location, type, values, callback) {
if (error) return callback(error);
zone.getRecords({ type: type, name: fqdn + '.' }, function(error, oldRecords) {
if (error && error.code === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error && error.code === 403) return callback(new BoxError(BoxError.ACCESS_DENIED, error.message));
if (error) {
debug('del->zone.getRecords', error);
return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
}
zone.deleteRecords(oldRecords, function (error, change) {
if (error && error.code === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error && error.code === 412) return callback(new DomainsError(DomainsError.STILL_BUSY, error.message));
if (error && error.code === 403) return callback(new BoxError(BoxError.ACCESS_DENIED, error.message));
if (error && error.code === 412) return callback(new BoxError(BoxError.BUSY, error.message));
if (error) {
debug('del->zone.createChange', error);
return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
}
callback(null, change.id);
@@ -194,10 +194,10 @@ function verifyDnsConfig(domainObject, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
if (typeof dnsConfig.projectId !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'projectId must be a string'));
if (!dnsConfig.credentials || typeof dnsConfig.credentials !== 'object') return callback(new DomainsError(DomainsError.BAD_FIELD, 'credentials must be an object'));
if (typeof dnsConfig.credentials.client_email !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'credentials.client_email must be a string'));
if (typeof dnsConfig.credentials.private_key !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'credentials.private_key must be a string'));
if (typeof dnsConfig.projectId !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'projectId must be a string', { field: 'projectId' }));
if (!dnsConfig.credentials || typeof dnsConfig.credentials !== 'object') return callback(new BoxError(BoxError.BAD_FIELD, 'credentials must be an object', { field: 'credentials' }));
if (typeof dnsConfig.credentials.client_email !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'credentials.client_email must be a string', { field: 'client_email' }));
if (typeof dnsConfig.credentials.private_key !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'credentials.private_key must be a string', { field: 'private_key' }));
var credentials = getDnsCredentials(dnsConfig);
@@ -206,8 +206,8 @@ function verifyDnsConfig(domainObject, callback) {
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
if (error && error.code === 'ENOTFOUND') return callback(new BoxError(BoxError.BAD_FIELD, 'Unable to resolve nameservers for this domain', { field: 'nameservers' }));
if (error || !nameservers) return callback(new BoxError(BoxError.BAD_FIELD, error ? error.message : 'Unable to get nameservers', { field: 'nameservers' }));
getZoneByName(credentials, zoneName, function (error, zone) {
if (error) return callback(error);
@@ -215,7 +215,7 @@ function verifyDnsConfig(domainObject, callback) {
var definedNS = zone.metadata.nameServers.sort().map(function(r) { return r.replace(/\.$/, ''); });
if (!_.isEqual(definedNS, nameservers.sort())) {
debug('verifyDnsConfig: %j and %j do not match', nameservers, definedNS);
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Google Cloud DNS'));
return callback(new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to Google Cloud DNS', { field: 'nameservers' }));
}
const location = 'cloudrontestdns';
+18 -18
View File
@@ -11,10 +11,10 @@ exports = module.exports = {
};
var assert = require('assert'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:dns/godaddy'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
superagent = require('superagent'),
util = require('util'),
waitForDns = require('./waitfordns.js');
@@ -72,11 +72,11 @@ function upsert(domainObject, location, type, values, callback) {
.timeout(30 * 1000)
.send(records)
.end(function (error, result) {
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
if (result.statusCode === 400) return callback(new DomainsError(DomainsError.BAD_FIELD, formatError(result))); // no such zone
if (result.statusCode === 422) return callback(new DomainsError(DomainsError.BAD_FIELD, formatError(result))); // conflict
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode === 400) return callback(new BoxError(BoxError.BAD_FIELD, formatError(result))); // no such zone
if (result.statusCode === 422) return callback(new BoxError(BoxError.BAD_FIELD, formatError(result))); // conflict
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
return callback(null);
});
@@ -98,10 +98,10 @@ function get(domainObject, location, type, callback) {
.set('Authorization', `sso-key ${dnsConfig.apiKey}:${dnsConfig.apiSecret}`)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode === 404) return callback(null, [ ]);
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
debug('get: %j', result.body);
@@ -126,7 +126,7 @@ function del(domainObject, location, type, values, callback) {
debug(`get: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
if (type !== 'A' && type !== 'TXT') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, new Error('Record deletion is not supported by GoDaddy API')));
if (type !== 'A' && type !== 'TXT') return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Record deletion is not supported by GoDaddy API'));
// check if the record exists at all so that we don't insert the "Dead" record for no reason
get(domainObject, location, type, function (error, values) {
@@ -144,10 +144,10 @@ function del(domainObject, location, type, values, callback) {
.send(records)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 404) return callback(null);
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
debug('del: done');
@@ -176,8 +176,8 @@ function verifyDnsConfig(domainObject, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
if (!dnsConfig.apiKey || typeof dnsConfig.apiKey !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'apiKey must be a non-empty string'));
if (!dnsConfig.apiSecret || typeof dnsConfig.apiSecret !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'apiSecret must be a non-empty string'));
if (!dnsConfig.apiKey || typeof dnsConfig.apiKey !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'apiKey must be a non-empty string', { field: 'apiKey' }));
if (!dnsConfig.apiSecret || typeof dnsConfig.apiSecret !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'apiSecret must be a non-empty string', { field: 'apiSecret' }));
const ip = '127.0.0.1';
@@ -189,12 +189,12 @@ function verifyDnsConfig(domainObject, callback) {
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
if (error && error.code === 'ENOTFOUND') return callback(new BoxError(BoxError.BAD_FIELD, 'Unable to resolve nameservers for this domain', { field: 'nameservers' }));
if (error || !nameservers) return callback(new BoxError(BoxError.BAD_FIELD, error ? error.message : 'Unable to get nameservers', { field: 'nameservers' }));
if (!nameservers.every(function (n) { return n.toLowerCase().indexOf('.domaincontrol.com') !== -1; })) {
debug('verifyDnsConfig: %j does not contain GoDaddy NS', nameservers);
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to GoDaddy'));
return callback(new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to GoDaddy', { field: 'nameservers' }));
}
const location = 'cloudrontestdns';
+6 -4
View File
@@ -17,6 +17,7 @@ exports = module.exports = {
};
var assert = require('assert'),
BoxError = require('../boxerror.js'),
util = require('util');
function removePrivateFields(domainObject) {
@@ -24,6 +25,7 @@ function removePrivateFields(domainObject) {
return domainObject;
}
// eslint-disable-next-line no-unused-vars
function injectPrivateFields(newConfig, currentConfig) {
// in-place injection of tokens and api keys which came in with domains.SECRET_PLACEHOLDER
}
@@ -37,7 +39,7 @@ function upsert(domainObject, location, type, values, callback) {
// Result: none
callback(new Error('not implemented'));
callback(new BoxError(BoxError.NOT_IMPLEMENTED, 'upsert is not implemented'));
}
function get(domainObject, location, type, callback) {
@@ -48,7 +50,7 @@ function get(domainObject, location, type, callback) {
// Result: Array of matching DNS records in string format
callback(new Error('not implemented'));
callback(new BoxError(BoxError.NOT_IMPLEMENTED, 'get is not implemented'));
}
function del(domainObject, location, type, values, callback) {
@@ -60,7 +62,7 @@ function del(domainObject, location, type, values, callback) {
// Result: none
callback(new Error('not implemented'));
callback(new BoxError(BoxError.NOT_IMPLEMENTED, 'del is not implemented'));
}
function wait(domainObject, location, type, value, options, callback) {
@@ -80,5 +82,5 @@ function verifyDnsConfig(domainObject, callback) {
// Result: dnsConfig object
callback(new Error('not implemented'));
callback(new BoxError(BoxError.NOT_IMPLEMENTED, 'verifyDnsConfig is not implemented'));
}
+4 -3
View File
@@ -11,10 +11,10 @@ exports = module.exports = {
};
var assert = require('assert'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:dns/manual'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
util = require('util'),
waitForDns = require('./waitfordns.js');
@@ -22,6 +22,7 @@ function removePrivateFields(domainObject) {
return domainObject;
}
// eslint-disable-next-line no-unused-vars
function injectPrivateFields(newConfig, currentConfig) {
}
@@ -78,8 +79,8 @@ function verifyDnsConfig(domainObject, callback) {
// Very basic check if the nameservers can be fetched
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
if (error && error.code === 'ENOTFOUND') return callback(new BoxError(BoxError.BAD_FIELD, 'Unable to resolve nameservers for this domain', { field: 'nameservers' }));
if (error || !nameservers) return callback(new BoxError(BoxError.BAD_FIELD, error ? error.message : 'Unable to get nameservers', { field: 'nameservers' }));
callback(null, {});
});
+29 -19
View File
@@ -11,10 +11,10 @@ exports = module.exports = {
};
var assert = require('assert'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:dns/namecheap'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
safe = require('safetydance'),
superagent = require('superagent'),
sysinfo = require('../sysinfo.js'),
@@ -37,8 +37,8 @@ function getQuery(dnsConfig, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(error);
callback(null, {
ApiUser: dnsConfig.username,
@@ -64,16 +64,21 @@ function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
query.TLD = zoneName.split('.')[1];
superagent.get(ENDPOINT).query(query).end(function (error, result) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
var parser = new xml2js.Parser();
parser.parseString(result.text, function (error, result) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
var tmp = result.ApiResponse;
if (tmp['$'].Status !== 'OK') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, safe.query(tmp, 'Errors[0].Error[0]._', 'Invalid response')));
if (!tmp.CommandResponse[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
if (!tmp.CommandResponse[0].DomainDNSGetHostsResult[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
if (tmp['$'].Status !== 'OK') {
var errorMessage = safe.query(tmp, 'Errors[0].Error[0]._', 'Invalid response');
if (errorMessage === 'API Key is invalid or API access has not been enabled') return callback(new BoxError(BoxError.ACCESS_DENIED, errorMessage));
return callback(new BoxError(BoxError.EXTERNAL_ERROR, errorMessage));
}
if (!tmp.CommandResponse[0]) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response'));
if (!tmp.CommandResponse[0].DomainDNSGetHostsResult[0]) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response'));
var hosts = result.ApiResponse.CommandResponse[0].DomainDNSGetHostsResult[0].host.map(function (h) {
return h['$'];
@@ -113,17 +118,22 @@ function setInternal(dnsConfig, zoneName, hosts, callback) {
});
superagent.post(ENDPOINT).query(query).end(function (error, result) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
var parser = new xml2js.Parser();
parser.parseString(result.text, function (error, result) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
var tmp = result.ApiResponse;
if (tmp['$'].Status !== 'OK') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, safe.query(tmp, 'Errors[0].Error[0]._', 'Invalid response')));
if (!tmp.CommandResponse[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
if (!tmp.CommandResponse[0].DomainDNSSetHostsResult[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
if (tmp.CommandResponse[0].DomainDNSSetHostsResult[0]['$'].IsSuccess !== 'true') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
if (tmp['$'].Status !== 'OK') {
var errorMessage = safe.query(tmp, 'Errors[0].Error[0]._', 'Invalid response');
if (errorMessage === 'API Key is invalid or API access has not been enabled') return callback(new BoxError(BoxError.ACCESS_DENIED, errorMessage));
return callback(new BoxError(BoxError.EXTERNAL_ERROR, errorMessage));
}
if (!tmp.CommandResponse[0]) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response'));
if (!tmp.CommandResponse[0].DomainDNSSetHostsResult[0]) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response'));
if (tmp.CommandResponse[0].DomainDNSSetHostsResult[0]['$'].IsSuccess !== 'true') return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response'));
callback(null);
});
@@ -271,8 +281,8 @@ function verifyDnsConfig(domainObject, callback) {
const zoneName = domainObject.zoneName;
const ip = '127.0.0.1';
if (!dnsConfig.username || typeof dnsConfig.username !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'username must be a non-empty string'));
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
if (!dnsConfig.username || typeof dnsConfig.username !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'username must be a non-empty string', { field: 'username' }));
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'token must be a non-empty string', { field: 'token' }));
let credentials = {
username: dnsConfig.username,
@@ -282,12 +292,12 @@ function verifyDnsConfig(domainObject, callback) {
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
if (error && error.code === 'ENOTFOUND') return callback(new BoxError(BoxError.BAD_FIELD, 'Unable to resolve nameservers for this domain', { field: 'nameservers' }));
if (error || !nameservers) return callback(new BoxError(BoxError.BAD_FIELD, error ? error.message : 'Unable to get nameservers', { field: 'nameservers' }));
if (nameservers.some(function (n) { return n.toLowerCase().indexOf('.registrar-servers.com') === -1; })) {
debug('verifyDnsConfig: %j does not contains NC NS', nameservers);
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to NameCheap'));
return callback(new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to NameCheap', { field: 'nameservers' }));
}
const testSubdomain = 'cloudrontestdns';
+18 -18
View File
@@ -11,10 +11,10 @@ exports = module.exports = {
};
var assert = require('assert'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:dns/namecom'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
safe = require('safetydance'),
superagent = require('superagent'),
util = require('util'),
@@ -63,9 +63,9 @@ function addRecord(dnsConfig, zoneName, name, type, values, callback) {
.timeout(30 * 1000)
.send(data)
.end(function (error, result) {
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, `Network error ${error.message}`));
if (result.statusCode === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 403) return callback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
return callback(null, 'unused-id');
});
@@ -100,9 +100,9 @@ function updateRecord(dnsConfig, zoneName, recordId, name, type, values, callbac
.timeout(30 * 1000)
.send(data)
.end(function (error, result) {
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, `Network error ${error.message}`));
if (result.statusCode === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 403) return callback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
return callback(null);
});
@@ -121,9 +121,9 @@ function getInternal(dnsConfig, zoneName, name, type, callback) {
.auth(dnsConfig.username, dnsConfig.token)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, `Network error ${error.message}`));
if (result.statusCode === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 403) return callback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
// name.com does not return the correct content-type
result.body = safe.JSON.parse(result.text);
@@ -209,9 +209,9 @@ function del(domainObject, location, type, values, callback) {
.auth(dnsConfig.username, dnsConfig.token)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, `Network error ${error.message}`));
if (result.statusCode === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 403) return callback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
return callback(null);
});
@@ -238,8 +238,8 @@ function verifyDnsConfig(domainObject, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
if (typeof dnsConfig.username !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'username must be a string'));
if (typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a string'));
if (typeof dnsConfig.username !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'username must be a string', { field: 'username' }));
if (typeof dnsConfig.token !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'token must be a string', { field: 'token' }));
var credentials = {
username: dnsConfig.username,
@@ -251,12 +251,12 @@ function verifyDnsConfig(domainObject, callback) {
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
if (error && error.code === 'ENOTFOUND') return callback(new BoxError(BoxError.BAD_FIELD, 'Unable to resolve nameservers for this domain', { field: 'nameservers' }));
if (error || !nameservers) return callback(new BoxError(BoxError.BAD_FIELD, error ? error.message : 'Unable to get nameservers', { field: 'nameservers' }));
if (!nameservers.every(function (n) { return n.toLowerCase().indexOf('.name.com') !== -1; })) {
debug('verifyDnsConfig: %j does not contain Name.com NS', nameservers);
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Name.com'));
return callback(new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to name.com', { field: 'nameservers' }));
}
const location = 'cloudrontestdns';
+1
View File
@@ -18,6 +18,7 @@ function removePrivateFields(domainObject) {
return domainObject;
}
// eslint-disable-next-line no-unused-vars
function injectPrivateFields(newConfig, currentConfig) {
}
+28 -28
View File
@@ -12,10 +12,10 @@ exports = module.exports = {
var assert = require('assert'),
AWS = require('aws-sdk'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:dns/route53'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
util = require('util'),
waitForDns = require('./waitfordns.js'),
_ = require('underscore');
@@ -59,15 +59,15 @@ function getZoneByName(dnsConfig, zoneName, callback) {
}
listHostedZones(function (error, result) {
if (error && error.code === 'AccessDenied') return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error && error.code === 'InvalidClientTokenId') return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
if (error && error.code === 'AccessDenied') return callback(new BoxError(BoxError.ACCESS_DENIED, error.message));
if (error && error.code === 'InvalidClientTokenId') return callback(new BoxError(BoxError.ACCESS_DENIED, error.message));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
var zone = result.HostedZones.filter(function (zone) {
return zone.Name.slice(0, -1) === zoneName; // aws zone name contains a '.' at the end
})[0];
if (!zone) return callback(new DomainsError(DomainsError.NOT_FOUND, 'no such zone'));
if (!zone) return callback(new BoxError(BoxError.NOT_FOUND, 'no such zone'));
callback(null, zone);
});
@@ -83,9 +83,9 @@ function getHostedZone(dnsConfig, zoneName, callback) {
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
route53.getHostedZone({ Id: zone.Id }, function (error, result) {
if (error && error.code === 'AccessDenied') return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error && error.code === 'InvalidClientTokenId') return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
if (error && error.code === 'AccessDenied') return callback(new BoxError(BoxError.ACCESS_DENIED, error.message));
if (error && error.code === 'InvalidClientTokenId') return callback(new BoxError(BoxError.ACCESS_DENIED, error.message));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
callback(null, result);
});
@@ -127,11 +127,11 @@ function upsert(domainObject, location, type, values, callback) {
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
route53.changeResourceRecordSets(params, function(error) {
if (error && error.code === 'AccessDenied') return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error && error.code === 'InvalidClientTokenId') return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error && error.code === 'PriorRequestNotComplete') return callback(new DomainsError(DomainsError.STILL_BUSY, error.message));
if (error && error.code === 'InvalidChangeBatch') return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
if (error && error.code === 'AccessDenied') return callback(new BoxError(BoxError.ACCESS_DENIED, error.message));
if (error && error.code === 'InvalidClientTokenId') return callback(new BoxError(BoxError.ACCESS_DENIED, error.message));
if (error && error.code === 'PriorRequestNotComplete') return callback(new BoxError(BoxError.BUSY, error.message));
if (error && error.code === 'InvalidChangeBatch') return callback(new BoxError(BoxError.BAD_FIELD, error.message));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
callback(null);
});
@@ -160,9 +160,9 @@ function get(domainObject, location, type, callback) {
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
route53.listResourceRecordSets(params, function (error, result) {
if (error && error.code === 'AccessDenied') return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error && error.code === 'InvalidClientTokenId') return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
if (error && error.code === 'AccessDenied') return callback(new BoxError(BoxError.ACCESS_DENIED, error.message));
if (error && error.code === 'InvalidClientTokenId') return callback(new BoxError(BoxError.ACCESS_DENIED, error.message));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
if (result.ResourceRecordSets.length === 0) return callback(null, [ ]);
if (result.ResourceRecordSets[0].Name !== params.StartRecordName || result.ResourceRecordSets[0].Type !== params.StartRecordType) return callback(null, [ ]);
@@ -208,23 +208,23 @@ function del(domainObject, location, type, values, callback) {
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
route53.changeResourceRecordSets(params, function(error) {
if (error && error.code === 'AccessDenied') return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error && error.code === 'InvalidClientTokenId') return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error && error.code === 'AccessDenied') return callback(new BoxError(BoxError.ACCESS_DENIED, error.message));
if (error && error.code === 'InvalidClientTokenId') return callback(new BoxError(BoxError.ACCESS_DENIED, error.message));
if (error && error.message && error.message.indexOf('it was not found') !== -1) {
debug('del: resource record set not found.', error);
return callback(new DomainsError(DomainsError.NOT_FOUND, error.message));
return callback(new BoxError(BoxError.NOT_FOUND, error.message));
} else if (error && error.code === 'NoSuchHostedZone') {
debug('del: hosted zone not found.', error);
return callback(new DomainsError(DomainsError.NOT_FOUND, error.message));
return callback(new BoxError(BoxError.NOT_FOUND, error.message));
} else if (error && error.code === 'PriorRequestNotComplete') {
debug('del: resource is still busy', error);
return callback(new DomainsError(DomainsError.STILL_BUSY, error.message));
return callback(new BoxError(BoxError.BUSY, error.message));
} else if (error && error.code === 'InvalidChangeBatch') {
debug('del: invalid change batch. No such record to be deleted.');
return callback(new DomainsError(DomainsError.NOT_FOUND, error.message));
return callback(new BoxError(BoxError.NOT_FOUND, error.message));
} else if (error) {
debug('del: error', error);
return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
}
callback(null);
@@ -252,8 +252,8 @@ function verifyDnsConfig(domainObject, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
if (!dnsConfig.accessKeyId || typeof dnsConfig.accessKeyId !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'accessKeyId must be a non-empty string'));
if (!dnsConfig.secretAccessKey || typeof dnsConfig.secretAccessKey !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'secretAccessKey must be a non-empty string'));
if (!dnsConfig.accessKeyId || typeof dnsConfig.accessKeyId !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'accessKeyId must be a non-empty string', { field: 'accessKeyId' }));
if (!dnsConfig.secretAccessKey || typeof dnsConfig.secretAccessKey !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'secretAccessKey must be a non-empty string', { field: 'secretAccessKey' }));
var credentials = {
accessKeyId: dnsConfig.accessKeyId,
@@ -268,15 +268,15 @@ function verifyDnsConfig(domainObject, callback) {
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
if (error && error.code === 'ENOTFOUND') return callback(new BoxError(BoxError.BAD_FIELD, 'Unable to resolve nameservers for this domain', { field: 'nameservers' }));
if (error || !nameservers) return callback(new BoxError(BoxError.BAD_FIELD, error ? error.message : 'Unable to get nameservers', { field: 'nameservers' }));
getHostedZone(credentials, zoneName, function (error, zone) {
if (error) return callback(error);
if (!_.isEqual(zone.DelegationSet.NameServers.sort(), nameservers.sort())) {
debug('verifyDnsConfig: %j and %j do not match', nameservers, zone.DelegationSet.NameServers);
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Route53'));
return callback(new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to Route53', { field: 'nameservers' }));
}
const location = 'cloudrontestdns';
+4 -4
View File
@@ -4,9 +4,9 @@ exports = module.exports = waitForDns;
var assert = require('assert'),
async = require('async'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:dns/waitfordns'),
dns = require('../native-dns.js'),
DomainsError = require('../domains.js').DomainsError;
dns = require('../native-dns.js');
function resolveIp(hostname, options, callback) {
assert.strictEqual(typeof hostname, 'string');
@@ -92,12 +92,12 @@ function waitForDns(hostname, zoneName, type, value, options, callback) {
debug(`waitForDns (try ${attempt}): ${hostname} to be ${value} in zone ${zoneName}`);
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error || !nameservers) return retryCallback(error || new DomainsError(DomainsError.EXTERNAL_ERROR, 'Unable to get nameservers'));
if (error || !nameservers) return retryCallback(error || new BoxError(BoxError.EXTERNAL_ERROR, 'Unable to get nameservers'));
async.every(nameservers, isChangeSynced.bind(null, hostname, type, value), function (error, synced) {
debug('waitForDns: %s %s ns: %j', hostname, synced ? 'done' : 'not done', nameservers);
retryCallback(synced ? null : new DomainsError(DomainsError.EXTERNAL_ERROR, 'ETRYAGAIN'));
retryCallback(synced ? null : new BoxError(BoxError.EXTERNAL_ERROR, 'ETRYAGAIN'));
});
});
}, function retryDone(error) {
+9 -8
View File
@@ -11,10 +11,10 @@ exports = module.exports = {
};
var assert = require('assert'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:dns/manual'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
sysinfo = require('../sysinfo.js'),
util = require('util'),
waitForDns = require('./waitfordns.js');
@@ -23,6 +23,7 @@ function removePrivateFields(domainObject) {
return domainObject;
}
// eslint-disable-next-line no-unused-vars
function injectPrivateFields(newConfig, currentConfig) {
}
@@ -78,20 +79,20 @@ function verifyDnsConfig(domainObject, callback) {
// Very basic check if the nameservers can be fetched
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
if (error && error.code === 'ENOTFOUND') return callback(new BoxError(BoxError.BAD_FIELD, 'Unable to resolve nameservers for this domain', { field: 'nameservers' }));
if (error || !nameservers) return callback(new BoxError(BoxError.BAD_FIELD, error ? error.message : 'Unable to get nameservers', { field: 'nameservers' }));
const location = 'cloudrontestdns';
const fqdn = domains.fqdn(location, domainObject);
dns.resolve(fqdn, 'A', { server: '127.0.0.1', timeout: 5000 }, function (error, result) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, `Unable to resolve ${fqdn}`));
if (error || !result) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : `Unable to resolve ${fqdn}`));
if (error && error.code === 'ENOTFOUND') return callback(new BoxError(BoxError.BAD_FIELD, `Unable to resolve ${fqdn}`, { field: 'nameservers' }));
if (error || !result) return callback(new BoxError(BoxError.BAD_FIELD, error ? error.message : `Unable to resolve ${fqdn}`, { field: 'nameservers' }));
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, `Failed to detect IP of this server: ${error.message}`));
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Failed to detect IP of this server: ${error.message}`));
if (result.length !== 1 || ip !== result[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, `Domain resolves to ${JSON.stringify(result)} instead of ${ip}`));
if (result.length !== 1 || ip !== result[0]) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Domain resolves to ${JSON.stringify(result)} instead of ${ip}`));
callback(null, {});
});
+209 -168
View File
@@ -1,16 +1,20 @@
'use strict';
exports = module.exports = {
DockerError: DockerError,
connection: connectionInstance(),
testRegistryConfig: testRegistryConfig,
setRegistryConfig: setRegistryConfig,
injectPrivateFields: injectPrivateFields,
removePrivateFields: removePrivateFields,
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8),
ping: ping,
info: info,
downloadImage: downloadImage,
createContainer: createContainer,
startContainer: startContainer,
restartContainer: restartContainer,
stopContainer: stopContainer,
stopContainerByName: stopContainer,
stopContainers: stopContainers,
@@ -22,78 +26,64 @@ exports = module.exports = {
getContainerIdByIp: getContainerIdByIp,
inspect: inspect,
inspectByName: inspect,
execContainer: execContainer,
getEvents: getEvents,
memoryUsage: memoryUsage,
execContainer: execContainer,
createVolume: createVolume,
removeVolume: removeVolume,
clearVolume: clearVolume
};
// timeout is optional
function connectionInstance(timeout) {
var Docker = require('dockerode');
var docker;
if (process.env.BOX_ENV === 'test') {
// test code runs a docker proxy on this port
docker = new Docker({ host: 'http://localhost', port: 5687, timeout: timeout });
// proxy code uses this to route to the real docker
docker.options = { socketPath: '/var/run/docker.sock' };
} else {
docker = new Docker({ socketPath: '/var/run/docker.sock', timeout: timeout });
}
return docker;
}
var addons = require('./addons.js'),
async = require('async'),
assert = require('assert'),
BoxError = require('./boxerror.js'),
child_process = require('child_process'),
config = require('./config.js'),
constants = require('./constants.js'),
debug = require('debug')('box:docker.js'),
once = require('once'),
debug = require('debug')('box:docker'),
Docker = require('dockerode'),
path = require('path'),
settings = require('./settings.js'),
shell = require('./shell.js'),
spawn = child_process.spawn,
safe = require('safetydance'),
util = require('util'),
_ = require('underscore');
const CLEARVOLUME_CMD = path.join(__dirname, 'scripts/clearvolume.sh'),
MKDIRVOLUME_CMD = path.join(__dirname, 'scripts/mkdirvolume.sh');
function DockerError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
const DOCKER_SOCKET_PATH = '/var/run/docker.sock';
const gConnection = new Docker({ socketPath: DOCKER_SOCKET_PATH });
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(DockerError, Error);
DockerError.INTERNAL_ERROR = 'Internal Error';
DockerError.NOT_FOUND = 'Not found';
DockerError.BAD_FIELD = 'Bad field';
function debugApp(app, args) {
function debugApp(app) {
assert(typeof app === 'object');
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function testRegistryConfig(auth, callback) {
assert.strictEqual(typeof auth, 'object');
assert.strictEqual(typeof callback, 'function');
gConnection.checkAuth(auth, function (error /*, data */) { // this returns a 500 even for auth errors
if (error) return callback(new BoxError(BoxError.BAD_FIELD, error, { field: 'serverAddress' }));
callback();
});
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.password === exports.SECRET_PLACEHOLDER) newConfig.password = currentConfig.password;
}
function removePrivateFields(registryConfig) {
assert.strictEqual(typeof registryConfig, 'object');
if (registryConfig.password) registryConfig.password = exports.SECRET_PLACEHOLDER;
return registryConfig;
}
function setRegistryConfig(auth, callback) {
assert.strictEqual(typeof auth, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -101,10 +91,10 @@ function setRegistryConfig(auth, callback) {
const isLogin = !!auth.password;
// currently, auth info is not stashed in the db but maybe it should for restore to work?
const cmd = isLogin ? `docker login ${auth.serveraddress} --username ${auth.username} --password ${auth.password}` : `docker logout ${auth.serveraddress}`;
const cmd = isLogin ? `docker login ${auth.serverAddress} --username ${auth.username} --password ${auth.password}` : `docker logout ${auth.serverAddress}`;
child_process.exec(cmd, { }, function (error, stdout, stderr) {
if (error) return callback(new DockerError(DockerError.BAD_FIELD, stderr));
child_process.exec(cmd, { }, function (error /*, stdout, stderr */) {
if (error) return callback(new BoxError(BoxError.ACCESS_DENIED, error.message));
callback();
});
@@ -114,37 +104,69 @@ function ping(callback) {
assert.strictEqual(typeof callback, 'function');
// do not let the request linger
var docker = connectionInstance(1000);
const connection = new Docker({ socketPath: DOCKER_SOCKET_PATH, timeout: 1000 });
docker.ping(function (error, result) {
if (error) return callback(new DockerError(DockerError.INTERNAL_ERROR, error));
if (result !== 'OK') return callback(new DockerError(DockerError.INTERNAL_ERROR, 'Unable to ping the docker daemon'));
connection.ping(function (error, result) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
if (result !== 'OK') return callback(new BoxError(BoxError.DOCKER_ERROR, 'Unable to ping the docker daemon'));
callback(null);
});
}
function getRegistryConfig(image, callback) {
// https://github.com/docker/distribution/blob/release/2.7/reference/normalize.go#L62
const parts = image.split('/');
if (parts.length === 1 || (parts[0].match(/[.:]/) === null)) return callback(null, null); // public docker registry
settings.getRegistryConfig(function (error, registryConfig) {
if (error) return callback(error);
// https://github.com/apocas/dockerode#pull-from-private-repos
const auth = {
username: registryConfig.username,
password: registryConfig.password,
auth: registryConfig.auth || '', // the auth token at login time
email: registryConfig.email || '',
serveraddress: registryConfig.serverAddress
};
callback(null, auth);
});
}
function pullImage(manifest, callback) {
var docker = exports.connection;
getRegistryConfig(manifest.dockerImage, function (error, authConfig) {
if (error) return callback(error);
// Use docker CLI here to support downloading of private repos. for dockerode, we have to use
// https://github.com/apocas/dockerode#pull-from-private-repos
shell.spawn('pullImage', '/usr/bin/docker', [ 'pull', manifest.dockerImage ], {}, function (error) {
if (error) {
debug(`pullImage: Error pulling image ${manifest.dockerImage} of ${manifest.id}: ${error.message}`);
return callback(new Error('Failed to pull image'));
}
debug(`pullImage: will pull ${manifest.dockerImage}. auth: ${authConfig ? 'yes' : 'no'}`);
var image = docker.getImage(manifest.dockerImage);
gConnection.pull(manifest.dockerImage, { authconfig: authConfig }, function (error, stream) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Unable to pull image. Please check the network or if the image needs authentication. statusCode: ' + error.statusCode));
image.inspect(function (err, data) {
if (err) return callback(new Error('Error inspecting image:' + err.message));
if (!data || !data.Config) return callback(new Error('Missing Config in image:' + JSON.stringify(data, null, 4)));
if (!data.Config.Entrypoint && !data.Config.Cmd) return callback(new Error('Only images with entry point are allowed'));
// https://github.com/dotcloud/docker/issues/1074 says each status message
// is emitted as a chunk
stream.on('data', function (chunk) {
var data = safe.JSON.parse(chunk) || { };
debug('pullImage: %j', data);
if (data.Config.ExposedPorts) debug('This image of %s exposes ports: %j', manifest.id, data.Config.ExposedPorts);
// The data.status here is useless because this is per layer as opposed to per image
if (!data.status && data.error) {
debug('pullImage error %s: %s', manifest.dockerImage, data.errorDetail.message);
}
});
callback(null);
stream.on('end', function () {
debug('downloaded image %s', manifest.dockerImage);
callback(null);
});
stream.on('error', function (error) {
debug('error pulling image %s: %j', manifest.dockerImage, error);
callback(new BoxError(BoxError.DOCKER_ERROR, error.message));
});
});
});
}
@@ -153,12 +175,12 @@ function downloadImage(manifest, callback) {
assert.strictEqual(typeof manifest, 'object');
assert.strictEqual(typeof callback, 'function');
debug('downloadImage %s %s', manifest.id, manifest.dockerImage);
debug('downloadImage %s', manifest.dockerImage);
var attempt = 1;
async.retry({ times: 10, interval: 15000 }, function (retryCallback) {
debug('Downloading image %s %s. attempt: %s', manifest.id, manifest.dockerImage, attempt++);
debug('Downloading image %s. attempt: %s', manifest.dockerImage, attempt++);
pullImage(manifest, function (error) {
if (error) console.error(error);
@@ -175,21 +197,21 @@ function createSubcontainer(app, name, cmd, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var docker = exports.connection,
isAppContainer = !cmd; // non app-containers are like scheduler containers
let isAppContainer = !cmd; // non app-containers are like scheduler and exec (terminal) containers
var manifest = app.manifest;
var exposedPorts = {}, dockerPortBindings = { };
var domain = app.fqdn;
const hostname = isAppContainer ? app.id : name;
const envPrefix = manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
let stdEnv = [
'CLOUDRON=1',
'CLOUDRON_PROXY_IP=172.18.0.1',
`CLOUDRON_APP_HOSTNAME=${name}`,
`${envPrefix}WEBADMIN_ORIGIN=${config.adminOrigin()}`,
`${envPrefix}API_ORIGIN=${config.adminOrigin()}`,
`CLOUDRON_APP_HOSTNAME=${app.id}`,
`${envPrefix}WEBADMIN_ORIGIN=${settings.adminOrigin()}`,
`${envPrefix}API_ORIGIN=${settings.adminOrigin()}`,
`${envPrefix}APP_ORIGIN=https://${domain}`,
`${envPrefix}APP_DOMAIN=${domain}`
];
@@ -202,7 +224,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
var portEnv = [];
for (let portName in app.portBindings) {
const hostPort = app.portBindings[portName];
const portType = portName in manifest.tcpPorts ? 'tcp' : 'udp';
const portType = (manifest.tcpPorts && portName in manifest.tcpPorts) ? 'tcp' : 'udp';
const ports = portType == 'tcp' ? manifest.tcpPorts : manifest.udpPorts;
var containerPort = ports[portName].containerPort || hostPort;
@@ -230,7 +252,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
if (!isAppContainer) memoryLimit *= 2;
addons.getEnvironment(app, function (error, addonEnv) {
if (error) return callback(new Error('Error getting addon environment : ' + error));
if (error) return callback(error);
// do no set hostname of containers to location as it might conflict with addons names. for example, an app installed in mail
// location may not reach mail container anymore by DNS. We cannot set hostname to fqdn either as that sets up the dns
@@ -240,7 +262,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
var containerOptions = {
name: name, // for referencing containers
Tty: isAppContainer,
Hostname: name,
Hostname: hostname,
Image: app.manifest.dockerImage,
Cmd: (isAppContainer && app.debugMode && app.debugMode.cmd) ? app.debugMode.cmd : cmd,
Env: stdEnv.concat(addonEnv).concat(portEnv).concat(appEnv),
@@ -271,7 +293,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
PublishAllPorts: false,
ReadonlyRootfs: app.debugMode ? !!app.debugMode.readonlyRootfs : true,
RestartPolicy: {
'Name': isAppContainer ? 'always' : 'no',
'Name': isAppContainer ? 'unless-stopped' : 'no',
'MaximumRetryCount': 0
},
CpuShares: 512, // relative to 1024 for system processes
@@ -301,7 +323,11 @@ function createSubcontainer(app, name, cmd, options, callback) {
debugApp(app, 'Creating container for %s', app.manifest.dockerImage);
docker.createContainer(containerOptions, callback);
gConnection.createContainer(containerOptions, function (error, container) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
callback(null, container);
});
});
}
@@ -313,13 +339,29 @@ function startContainer(containerId, callback) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof callback, 'function');
var docker = exports.connection;
var container = docker.getContainer(containerId);
var container = gConnection.getContainer(containerId);
debug('Starting container %s', containerId);
container.start(function (error) {
if (error && error.statusCode !== 304) return callback(new Error('Error starting container :' + error));
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
if (error && error.statusCode === 400) return callback(new BoxError(BoxError.BAD_FIELD, error)); // e.g start.sh is not executable
if (error && error.statusCode !== 304) return callback(new BoxError(BoxError.DOCKER_ERROR, error)); // 304 means already started
return callback(null);
});
}
function restartContainer(containerId, callback) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof callback, 'function');
var container = gConnection.getContainer(containerId);
debug('Restarting container %s', containerId);
container.restart(function (error) {
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
if (error && error.statusCode === 400) return callback(new BoxError(BoxError.BAD_FIELD, error)); // e.g start.sh is not executable
if (error && error.statusCode !== 204) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
return callback(null);
});
@@ -334,8 +376,7 @@ function stopContainer(containerId, callback) {
return callback();
}
var docker = exports.connection;
var container = docker.getContainer(containerId);
var container = gConnection.getContainer(containerId);
debug('Stopping container %s', containerId);
var options = {
@@ -343,12 +384,12 @@ function stopContainer(containerId, callback) {
};
container.stop(options, function (error) {
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new Error('Error stopping container:' + error));
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Error stopping container:' + error.message));
debug('Waiting for container ' + containerId);
container.wait(function (error, data) {
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new Error('Error waiting on container:' + error));
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) : '');
@@ -365,8 +406,7 @@ function deleteContainer(containerId, callback) {
if (containerId === null) return callback(null);
var docker = exports.connection;
var container = docker.getContainer(containerId);
var container = gConnection.getContainer(containerId);
var removeOptions = {
force: true, // kill container if it's running
@@ -376,9 +416,12 @@ function deleteContainer(containerId, callback) {
container.remove(removeOptions, function (error) {
if (error && error.statusCode === 404) return callback(null);
if (error) debug('Error removing container %s : %j', containerId, error);
if (error) {
debug('Error removing container %s : %j', containerId, error);
return callback(new BoxError(BoxError.DOCKER_ERROR, error));
}
callback(error);
callback(null);
});
}
@@ -387,15 +430,13 @@ function deleteContainers(appId, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var docker = exports.connection;
debug('deleting containers of %s', appId);
let labels = [ 'appId=' + appId ];
if (options.managedOnly) labels.push('isCloudronManaged=true');
docker.listContainers({ all: 1, filters: JSON.stringify({ label: labels }) }, function (error, containers) {
if (error) return callback(error);
gConnection.listContainers({ all: 1, filters: JSON.stringify({ label: labels }) }, function (error, containers) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
async.eachSeries(containers, function (container, iteratorDone) {
deleteContainer(container.Id, iteratorDone);
@@ -407,12 +448,10 @@ function stopContainers(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
var docker = exports.connection;
debug('stopping containers of %s', appId);
docker.listContainers({ all: 1, filters: JSON.stringify({ label: [ 'appId=' + appId ] }) }, function (error, containers) {
if (error) return callback(error);
gConnection.listContainers({ all: 1, filters: JSON.stringify({ label: [ 'appId=' + appId ] }) }, function (error, containers) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
async.eachSeries(containers, function (container, iteratorDone) {
stopContainer(container.Id, iteratorDone);
@@ -427,8 +466,6 @@ function deleteImage(manifest, callback) {
var dockerImage = manifest ? manifest.dockerImage : null;
if (!dockerImage) return callback(null);
var docker = exports.connection;
var removeOptions = {
force: false, // might be shared with another instance of this app
noprune: false // delete untagged parents
@@ -437,14 +474,17 @@ function deleteImage(manifest, callback) {
// registry v1 used to pull down all *tags*. this meant that deleting image by tag was not enough (since that
// just removes the tag). we used to remove the image by id. this is not required anymore because aliases are
// not created anymore after https://github.com/docker/docker/pull/10571
docker.getImage(dockerImage).remove(removeOptions, function (error) {
gConnection.getImage(dockerImage).remove(removeOptions, function (error) {
if (error && error.statusCode === 400) return callback(null); // invalid image format. this can happen if user installed with a bad --docker-image
if (error && error.statusCode === 404) return callback(null); // not found
if (error && error.statusCode === 409) return callback(null); // another container using the image
if (error) debug('Error removing image %s : %j', dockerImage, error);
if (error) {
debug('Error removing image %s : %j', dockerImage, error);
return callback(new BoxError(BoxError.DOCKER_ERROR, error));
}
callback(error);
callback(null);
});
}
@@ -452,11 +492,9 @@ function getContainerIdByIp(ip, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
var docker = exports.connection;
docker.getNetwork('cloudron').inspect(function (error, bridge) {
if (error && error.statusCode === 404) return callback(new Error('Unable to find the cloudron network'));
if (error) return callback(error);
gConnection.getNetwork('cloudron').inspect(function (error, bridge) {
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Unable to find the cloudron network'));
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
var containerId;
for (var id in bridge.Containers) {
@@ -465,7 +503,7 @@ function getContainerIdByIp(ip, callback) {
break;
}
}
if (!containerId) return callback(new Error('No container with that ip'));
if (!containerId) return callback(new BoxError(BoxError.DOCKER_ERROR, 'No container with that ip'));
callback(null, containerId);
});
@@ -475,24 +513,49 @@ function inspect(containerId, callback) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof callback, 'function');
var container = exports.connection.getContainer(containerId);
var container = gConnection.getContainer(containerId);
container.inspect(function (error, result) {
if (error && error.statusCode === 404) return callback(new DockerError(DockerError.NOT_FOUND));
if (error) return callback(new DockerError(DockerError.INTERNAL_ERROR, error));
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
callback(null, result);
});
}
function execContainer(containerId, options, callback) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var container = gConnection.getContainer(containerId);
container.exec(options.execOptions, function (error, exec) {
if (error && error.statusCode === 409) return callback(new BoxError(BoxError.BAD_STATE, error.message)); // container restarting/not running
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
exec.start(options.startOptions, function(error, stream /* in hijacked mode, this is a net.socket */) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
if (options.rows && options.columns) {
// there is a race where resizing too early results in a 404 "no such exec"
// https://git.cloudron.io/cloudron/box/issues/549
setTimeout(function () {
exec.resize({ h: options.rows, w: options.columns }, function (error) { if (error) debug('Error resizing console', error); });
}, 2000);
}
callback(null, stream);
});
});
}
function getEvents(options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
let docker = exports.connection;
docker.getEvents(options, function (error, stream) {
if (error) return callback(new DockerError(DockerError.INTERNAL_ERROR, error));
gConnection.getEvents(options, function (error, stream) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
callback(null, stream);
});
@@ -502,55 +565,22 @@ function memoryUsage(containerId, callback) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof callback, 'function');
var container = exports.connection.getContainer(containerId);
var container = gConnection.getContainer(containerId);
container.stats({ stream: false }, function (error, result) {
if (error && error.statusCode === 404) return callback(new DockerError(DockerError.NOT_FOUND));
if (error) return callback(new DockerError(DockerError.INTERNAL_ERROR, error));
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
callback(null, result);
});
}
function execContainer(containerId, cmd, options, callback) {
assert.strictEqual(typeof containerId, 'string');
assert(util.isArray(cmd));
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
callback = once(callback); // ChildProcess exit may or may not be called after error
var cp = spawn('/usr/bin/docker', [ 'exec', '-i', containerId ].concat(cmd));
var chunks = [ ];
if (options.stdout) {
cp.stdout.pipe(options.stdout);
} else if (options.bufferStdout) {
cp.stdout.on('data', function (chunk) { chunks.push(chunk); });
} else {
cp.stdout.pipe(process.stdout);
}
cp.on('error', callback);
cp.on('exit', function (code, signal) {
debug('execContainer code: %s signal: %s', code, signal);
if (!callback.called) callback(code ? 'Failed with status ' + code : null, Buffer.concat(chunks));
});
cp.stderr.pipe(options.stderr || process.stderr);
if (options.stdin) options.stdin.pipe(cp.stdin).on('error', callback);
}
function createVolume(app, name, volumeDataDir, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof volumeDataDir, 'string');
assert.strictEqual(typeof callback, 'function');
let docker = exports.connection;
const volumeOptions = {
Name: name,
Driver: 'local',
@@ -567,10 +597,10 @@ function createVolume(app, name, volumeDataDir, callback) {
// requires sudo because the path can be outside appsdata
shell.sudo('createVolume', [ MKDIRVOLUME_CMD, volumeDataDir ], {}, function (error) {
if (error) return callback(new Error(`Error creating app data dir: ${error.message}`));
if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error creating app data dir: ${error.message}`));
docker.createVolume(volumeOptions, function (error) {
if (error) return callback(error);
gConnection.createVolume(volumeOptions, function (error) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
callback();
});
@@ -583,14 +613,17 @@ function clearVolume(app, name, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
let docker = exports.connection;
let volume = docker.getVolume(name);
let volume = gConnection.getVolume(name);
volume.inspect(function (error, v) {
if (error && error.statusCode === 404) return callback();
if (error) return callback(error);
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
const volumeDataDir = v.Options.device;
shell.sudo('clearVolume', [ CLEARVOLUME_CMD, options.removeDirectory ? 'rmdir' : 'clear', volumeDataDir ], {}, callback);
shell.sudo('clearVolume', [ CLEARVOLUME_CMD, options.removeDirectory ? 'rmdir' : 'clear', volumeDataDir ], {}, function (error) {
if (error) return callback(new BoxError(BoxError.FS_ERROR, error));
callback();
});
});
}
@@ -600,12 +633,20 @@ function removeVolume(app, name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
let docker = exports.connection;
let volume = docker.getVolume(name);
let volume = gConnection.getVolume(name);
volume.remove(function (error) {
if (error && error.statusCode !== 404) return callback(new 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 of ${app.id} ${error.message}`));
callback();
});
}
function info(callback) {
assert.strictEqual(typeof callback, 'function');
gConnection.info(function (error, result) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Error connecting to docker'));
callback(null, result);
});
}
+14 -8
View File
@@ -6,9 +6,9 @@ exports = module.exports = {
};
var apps = require('./apps.js'),
AppsError = apps.AppsError,
assert = require('assert'),
config = require('./config.js'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
express = require('express'),
debug = require('debug')('box:dockerproxy'),
http = require('http'),
@@ -29,13 +29,13 @@ function authorizeApp(req, res, next) {
// - only allow managing and inspection of containers belonging to the app
// make the tests pass for now
if (config.TEST) {
if (constants.TEST) {
req.app = { id: 'testappid' };
return next();
}
apps.getByIpAddress(req.connection.remoteAddress, function (error, app) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(401, 'Unauthorized'));
if (error && error.reason === BoxError.NOT_FOUND) return next(new HttpError(401, 'Unauthorized'));
if (error) return next(new HttpError(500, error));
if (!('docker' in app.manifest.addons)) return next(new HttpError(401, 'Unauthorized'));
@@ -67,6 +67,7 @@ function attachDockerRequest(req, res, next) {
next();
}
// eslint-disable-next-line no-unused-vars
function containersCreate(req, res, next) {
safe.set(req.body, 'HostConfig.NetworkMode', 'cloudron'); // overwrite the network the container lives in
safe.set(req.body, 'NetworkingConfig', {}); // drop any custom network configs
@@ -97,6 +98,7 @@ function containersCreate(req, res, next) {
req.dockerRequest.end(plainBody);
}
// eslint-disable-next-line no-unused-vars
function process(req, res, next) {
// we have to rebuild the body since we consumed in in the parser
if (Object.keys(req.body).length !== 0) {
@@ -120,7 +122,7 @@ function start(callback) {
let proxyServer = express();
if (config.TEST) {
if (constants.TEST) {
proxyServer.use(function (req, res, next) {
debug('proxying: ' + req.method, req.url);
next();
@@ -135,10 +137,14 @@ function start(callback) {
.use(middleware.lastMile());
gHttpServer = http.createServer(proxyServer);
gHttpServer.listen(config.get('dockerProxyPort'), '0.0.0.0', callback);
gHttpServer.listen(constants.DOCKER_PROXY_PORT, '0.0.0.0', callback);
debug(`startDockerProxy: started proxy on port ${config.get('dockerProxyPort')}`);
// Overwrite the default 2min request timeout. This is required for large builds for example
gHttpServer.setTimeout(60 * 60 * 1000);
debug(`startDockerProxy: started proxy on port ${constants.DOCKER_PROXY_PORT}`);
// eslint-disable-next-line no-unused-vars
gHttpServer.on('upgrade', function (req, client, head) {
// Create a new tcp connection to the TCP server
var remote = net.connect('/var/run/docker.sock', function () {
@@ -150,7 +156,7 @@ function start(callback) {
if (req.headers['content-type'] === 'application/json') {
// TODO we have to parse the immediate upgrade request body, but I don't know how
let plainBody = '{"Detach":false,"Tty":false}\r\n';
upgradeMessage += `Content-Type: application/json\r\n`;
upgradeMessage += 'Content-Type: application/json\r\n';
upgradeMessage += `Content-Length: ${Buffer.byteLength(plainBody)}\r\n`;
upgradeMessage += '\r\n';
upgradeMessage += plainBody;
+12 -12
View File
@@ -12,8 +12,8 @@ exports = module.exports = {
};
var assert = require('assert'),
BoxError = require('./boxerror.js'),
database = require('./database.js'),
DatabaseError = require('./databaseerror'),
safe = require('safetydance');
var DOMAINS_FIELDS = [ 'domain', 'zoneName', 'provider', 'configJson', 'tlsConfigJson', 'locked' ].join(',');
@@ -34,8 +34,8 @@ function get(domain, callback) {
assert.strictEqual(typeof callback, 'function');
database.query(`SELECT ${DOMAINS_FIELDS} FROM domains WHERE domain=?`, [ domain ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Domain not found'));
postProcess(result[0]);
@@ -45,7 +45,7 @@ function get(domain, callback) {
function getAll(callback) {
database.query(`SELECT ${DOMAINS_FIELDS} FROM domains ORDER BY domain`, function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(postProcess);
@@ -63,8 +63,8 @@ function add(name, domain, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson) VALUES (?, ?, ?, ?, ?)', [ name, domain.zoneName, domain.provider, JSON.stringify(domain.config), JSON.stringify(domain.tlsConfig) ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
@@ -91,8 +91,8 @@ function update(name, domain, callback) {
args.push(name);
database.query('UPDATE domains SET ' + fields.join(', ') + ' WHERE domain=?', args, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.NOT_FOUND, 'Domain not found'));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
@@ -103,9 +103,9 @@ function del(domain, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM domains WHERE domain=?', [ domain ], function (error, result) {
if (error && error.code === 'ER_ROW_IS_REFERENCED_2') return callback(new DatabaseError(DatabaseError.IN_USE));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error && error.code === 'ER_ROW_IS_REFERENCED_2') return callback(new BoxError(BoxError.CONFLICT));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Domain not found'));
callback(null);
});
@@ -113,7 +113,7 @@ function del(domain, callback) {
function clear(callback) {
database.query('DELETE FROM domains', function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(error);
});
+70 -90
View File
@@ -26,59 +26,28 @@ module.exports = exports = {
parentDomain: parentDomain,
prepareDashboardDomain: prepareDashboardDomain,
checkDnsRecords: checkDnsRecords,
DomainsError: DomainsError,
prepareDashboardDomain: prepareDashboardDomain,
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8)
};
var assert = require('assert'),
async = require('async'),
config = require('./config.js'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:domains'),
domaindb = require('./domaindb.js'),
eventlog = require('./eventlog.js'),
reverseProxy = require('./reverseproxy.js'),
ReverseProxyError = reverseProxy.ReverseProxyError,
safe = require('safetydance'),
settings = require('./settings.js'),
sysinfo = require('./sysinfo.js'),
tld = require('tldjs'),
util = require('util'),
_ = require('underscore');
function DomainsError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(DomainsError, Error);
DomainsError.NOT_FOUND = 'No such domain';
DomainsError.ALREADY_EXISTS = 'Domain already exists';
DomainsError.EXTERNAL_ERROR = 'External error';
DomainsError.BAD_FIELD = 'Bad Field';
DomainsError.STILL_BUSY = 'Still busy';
DomainsError.IN_USE = 'In Use';
DomainsError.INTERNAL_ERROR = 'Internal error';
DomainsError.ACCESS_DENIED = 'Access denied';
DomainsError.INVALID_PROVIDER = 'provider must be route53, gcdns, digitalocean, gandi, cloudflare, namecom, noop, wildcard, manual or caas';
// choose which subdomain backend we use for test purpose we use route53
function api(provider) {
assert.strictEqual(typeof provider, 'string');
@@ -113,16 +82,14 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, provider, callback) {
assert.strictEqual(typeof callback, 'function');
var backend = api(provider);
if (!backend) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid provider'));
if (!backend) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid provider', { field: 'provider' }));
const domainObject = { config: dnsConfig, domain: domain, zoneName: zoneName };
api(provider).verifyDnsConfig(domainObject, function (error, result) {
if (error && error.reason === DomainsError.ACCESS_DENIED) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Incorrect configuration. Access denied'));
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Zone not found'));
if (error && error.reason === DomainsError.EXTERNAL_ERROR) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Configuration error: ' + error.message));
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
if (error && error.reason === DomainsError.INVALID_PROVIDER) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
if (error && error.reason === BoxError.ACCESS_DENIED) return callback(new BoxError(BoxError.BAD_FIELD, `Access denied: ${error.message}`));
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.BAD_FIELD, `Zone not found: ${error.message}`));
if (error && error.reason === BoxError.EXTERNAL_ERROR) return callback(new BoxError(BoxError.BAD_FIELD, `Configuration error: ${error.message}`));
if (error) return callback(error);
result.hyphenatedSubdomains = !!dnsConfig.hyphenatedSubdomains;
@@ -148,25 +115,25 @@ function validateHostname(location, domainObject) {
constants.SMTP_LOCATION,
constants.IMAP_LOCATION
];
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new DomainsError(DomainsError.BAD_FIELD, location + ' is reserved');
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new BoxError(BoxError.BAD_FIELD, location + ' is reserved', { field: 'location' });
if (hostname === config.adminFqdn()) return new DomainsError(DomainsError.BAD_FIELD, location + ' is reserved');
if (hostname === settings.adminFqdn()) return new BoxError(BoxError.BAD_FIELD, location + ' is reserved', { field: 'location' });
// workaround https://github.com/oncletom/tld.js/issues/73
var tmp = hostname.replace('_', '-');
if (!tld.isValid(tmp)) return new DomainsError(DomainsError.BAD_FIELD, 'Hostname is not a valid domain name');
if (!tld.isValid(tmp)) return new BoxError(BoxError.BAD_FIELD, 'Hostname is not a valid domain name', { field: 'location' });
if (hostname.length > 253) return new DomainsError(DomainsError.BAD_FIELD, 'Hostname length exceeds 253 characters');
if (hostname.length > 253) return new BoxError(BoxError.BAD_FIELD, 'Hostname length exceeds 253 characters', { field: 'location' });
if (location) {
// label validation
if (location.split('.').some(function (p) { return p.length > 63 || p.length < 1; })) return new DomainsError(DomainsError.BAD_FIELD, 'Invalid subdomain length');
if (location.match(/^[A-Za-z0-9-.]+$/) === null) return new DomainsError(DomainsError.BAD_FIELD, 'Subdomain can only contain alphanumeric, hyphen and dot');
if (/^[-.]/.test(location)) return new DomainsError(DomainsError.BAD_FIELD, 'Subdomain cannot start or end with hyphen or dot');
if (location.split('.').some(function (p) { return p.length > 63 || p.length < 1; })) return new BoxError(BoxError.BAD_FIELD, 'Invalid subdomain length', { field: 'location' });
if (location.match(/^[A-Za-z0-9-.]+$/) === null) return new BoxError(BoxError.BAD_FIELD, 'Subdomain can only contain alphanumeric, hyphen and dot', { field: 'location' });
if (/^[-.]/.test(location)) return new BoxError(BoxError.BAD_FIELD, 'Subdomain cannot start or end with hyphen or dot', { field: 'location' });
}
if (domainObject.config.hyphenatedSubdomains) {
if (location.indexOf('.') !== -1) return new DomainsError(DomainsError.BAD_FIELD, 'Subdomain cannot contain a dot');
if (location.indexOf('.') !== -1) return new BoxError(BoxError.BAD_FIELD, 'Subdomain cannot contain a dot', { field: 'location' });
}
return null;
@@ -183,12 +150,12 @@ function validateTlsConfig(tlsConfig, dnsProvider) {
case 'caas':
break;
default:
return new DomainsError(DomainsError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback, letsencrypt-prod/staging');
return new BoxError(BoxError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback, letsencrypt-prod/staging', { field: 'tlsProvider' });
}
if (tlsConfig.wildcard) {
if (!tlsConfig.provider.startsWith('letsencrypt')) return new DomainsError(DomainsError.BAD_FIELD, 'wildcard can only be set with letsencrypt');
if (dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard') return new DomainsError(DomainsError.BAD_FIELD, 'wildcard cert requires a programmable DNS backend');
if (!tlsConfig.provider.startsWith('letsencrypt')) return new BoxError(BoxError.BAD_FIELD, 'wildcard can only be set with letsencrypt', { field: 'wildcard' });
if (dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard') return new BoxError(BoxError.BAD_FIELD, 'wildcard cert requires a programmable DNS backend', { field: 'tlsProvider' });
}
return null;
@@ -205,22 +172,22 @@ function add(domain, data, auditSource, callback) {
let { zoneName, provider, config, fallbackCertificate, tlsConfig } = data;
if (!tld.isValid(domain)) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid domain'));
if (domain.endsWith('.')) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid domain'));
if (!tld.isValid(domain)) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid domain', { field: 'domain' }));
if (domain.endsWith('.')) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid domain', { field: 'domain' }));
if (zoneName) {
if (!tld.isValid(zoneName)) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid zoneName'));
if (zoneName.endsWith('.')) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid zoneName'));
if (!tld.isValid(zoneName)) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid zoneName', { field: 'zoneName' }));
if (zoneName.endsWith('.')) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid zoneName', { field: 'zoneName' }));
} else {
zoneName = tld.getDomain(domain) || domain;
}
if (fallbackCertificate) {
let error = reverseProxy.validateCertificate('test', { domain, config }, fallbackCertificate);
if (error) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
if (error) return callback(error);
} else {
fallbackCertificate = reverseProxy.generateFallbackCertificateSync({ domain, config });
if (fallbackCertificate.error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, fallbackCertificate.error));
if (fallbackCertificate.error) return callback(error);
}
let error = validateTlsConfig(tlsConfig, provider);
@@ -230,11 +197,10 @@ function add(domain, data, auditSource, callback) {
if (error) return callback(error);
domaindb.add(domain, { zoneName: zoneName, provider: provider, config: sanitizedConfig, tlsConfig: tlsConfig }, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new DomainsError(DomainsError.ALREADY_EXISTS));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
if (error) return callback(error);
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
if (error) return callback(error);
eventlog.add(eventlog.ACTION_DOMAIN_ADD, auditSource, { domain, zoneName, provider });
@@ -249,17 +215,14 @@ function get(domain, callback) {
assert.strictEqual(typeof callback, 'function');
domaindb.get(domain, function (error, result) {
// TODO try to find subdomain entries maybe based on zoneNames or so
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
reverseProxy.getFallbackCertificate(domain, function (error, bundle) {
if (error && error.reason !== ReverseProxyError.NOT_FOUND) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
if (error) return callback(error);
reverseProxy.getFallbackCertificate(domain, function (_, bundle) { // never returns an error
var cert = safe.fs.readFileSync(bundle.certFilePath, 'utf-8');
var key = safe.fs.readFileSync(bundle.keyFilePath, 'utf-8');
if (!cert || !key) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, 'unable to read certificates from disk'));
// do not error here. otherwise, there is no way to fix things up from the UI
if (!cert || !key) debug(`Unable to read fallback certificates of ${domain} from disk`);
result.fallbackCertificate = { cert: cert, key: key };
@@ -272,7 +235,7 @@ function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
domaindb.getAll(function (error, result) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
if (error) return callback(error);
return callback(null, result);
});
@@ -291,18 +254,17 @@ function update(domain, data, auditSource, callback) {
let { zoneName, provider, config, fallbackCertificate, tlsConfig } = data;
domaindb.get(domain, function (error, domainObject) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
if (error) return callback(error);
if (zoneName) {
if (!tld.isValid(zoneName)) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid zoneName'));
if (!tld.isValid(zoneName)) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid zoneName', { field: 'zoneName' }));
} else {
zoneName = domainObject.zoneName;
}
if (fallbackCertificate) {
let error = reverseProxy.validateCertificate('test', domainObject, fallbackCertificate);
if (error) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
if (error) return callback(error);
}
error = validateTlsConfig(tlsConfig, provider);
@@ -321,13 +283,12 @@ function update(domain, data, auditSource, callback) {
};
domaindb.update(domain, newData, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
if (error) return callback(error);
if (!fallbackCertificate) return callback();
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
if (error) return callback(error);
eventlog.add(eventlog.ACTION_DOMAIN_UPDATE, auditSource, { domain, zoneName, provider });
@@ -343,12 +304,10 @@ function del(domain, auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
if (domain === config.adminDomain()) return callback(new DomainsError(DomainsError.IN_USE));
if (domain === settings.adminDomain()) return callback(new BoxError(BoxError.CONFLICT, 'Cannot remove admin domain'));
domaindb.del(domain, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
if (error && error.reason === DatabaseError.IN_USE) return callback(new DomainsError(DomainsError.IN_USE));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
if (error) return callback(error);
eventlog.add(eventlog.ACTION_DOMAIN_REMOVE, auditSource, { domain });
@@ -360,7 +319,7 @@ function clear(callback) {
assert.strictEqual(typeof callback, 'function');
domaindb.clear(function (error) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
if (error) return callback(error);
return callback(null);
});
@@ -394,7 +353,7 @@ function getDnsRecords(location, domain, type, callback) {
assert.strictEqual(typeof callback, 'function');
get(domain, function (error, domainObject) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
if (error) return callback(error);
api(domainObject.provider).get(domainObject, location, type, function (error, values) {
if (error) return callback(error);
@@ -404,6 +363,25 @@ function getDnsRecords(location, domain, type, callback) {
});
}
function checkDnsRecords(location, domain, callback) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
getDnsRecords(location, domain, 'A', function (error, values) {
if (error) return callback(error);
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(error);
if (values.length === 0) return callback(null, { needsOverwrite: false }); // does not exist
if (values[0] === ip) return callback(null, { needsOverwrite: false }); // exists but in sync
callback(null, { needsOverwrite: true });
});
});
}
// note: for TXT records the values must be quoted
function upsertDnsRecords(location, domain, type, values, callback) {
assert.strictEqual(typeof location, 'string');
@@ -415,7 +393,7 @@ function upsertDnsRecords(location, domain, type, values, callback) {
debug('upsertDNSRecord: %s on %s type %s values', location, domain, type, values);
get(domain, function (error, domainObject) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
if (error) return callback(error);
api(domainObject.provider).upsert(domainObject, location, type, values, function (error) {
if (error) return callback(error);
@@ -438,7 +416,7 @@ function removeDnsRecords(location, domain, type, values, callback) {
if (error) return callback(error);
api(domainObject.provider).del(domainObject, location, type, values, function (error) {
if (error && error.reason !== DomainsError.NOT_FOUND) return callback(error);
if (error && error.reason !== BoxError.NOT_FOUND) return callback(error);
callback(null);
});
@@ -493,15 +471,17 @@ function prepareDashboardDomain(domain, auditSource, progressCallback, callback)
get(domain, function (error, domainObject) {
if (error) return callback(error);
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
const adminFqdn = fqdn(constants.ADMIN_LOCATION, domainObject);
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(error);
async.series([
(done) => { progressCallback({ percent: 10, message: 'Updating DNS' }); done(); },
(done) => { progressCallback({ percent: 10, message: `Updating DNS of ${adminFqdn}` }); done(); },
upsertDnsRecords.bind(null, constants.ADMIN_LOCATION, domain, 'A', [ ip ]),
(done) => { progressCallback({ percent: 40, message: 'Waiting for DNS' }); done(); },
(done) => { progressCallback({ percent: 40, message: `Waiting for DNS of ${adminFqdn}` }); done(); },
waitForDnsRecord.bind(null, constants.ADMIN_LOCATION, domain, 'A', ip, { interval: 30000, times: 50000 }),
(done) => { progressCallback({ percent: 70, message: 'Getting certificate' }); done(); },
(done) => { progressCallback({ percent: 70, message: `Getting certificate of ${adminFqdn}` }); done(); },
reverseProxy.ensureCertificate.bind(null, fqdn(constants.ADMIN_LOCATION, domainObject), domain, auditSource)
], function (error) {
if (error) return callback(error);
+5 -6
View File
@@ -4,17 +4,16 @@ exports = module.exports = {
sync: sync
};
var appdb = require('./appdb.js'),
apps = require('./apps.js'),
let apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
config = require('./config.js'),
constants = require('./constants.js'),
debug = require('debug')('box:dyndns'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
paths = require('./paths.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
sysinfo = require('./sysinfo.js');
// called for dynamic dns setups where we have to update the IP
@@ -22,7 +21,7 @@ function sync(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
sysinfo.getPublicIp(function (error, ip) {
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(error);
let info = safe.JSON.parse(safe.fs.readFileSync(paths.DYNDNS_INFO_FILE, 'utf8')) || { ip: null };
@@ -33,7 +32,7 @@ function sync(auditSource, callback) {
debug(`refreshDNS: updating ip from ${info.ip} to ${ip}`);
domains.upsertDnsRecords(constants.ADMIN_LOCATION, config.adminDomain(), 'A', [ ip ], function (error) {
domains.upsertDnsRecords(constants.ADMIN_LOCATION, settings.adminDomain(), 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('refreshDNS: updated admin location');
@@ -43,7 +42,7 @@ function sync(auditSource, callback) {
async.each(result, function (app, callback) {
// do not change state of installing apps since apptask will error if dns record already exists
if (app.installationState !== appdb.ISTATE_INSTALLED) return callback();
if (app.installationState !== apps.ISTATE_INSTALLED) return callback();
domains.upsertDnsRecords(app.location, app.domain, 'A', [ ip ], callback);
}, function (error) {
+12 -36
View File
@@ -1,8 +1,6 @@
'use strict';
exports = module.exports = {
EventLogError: EventLogError,
add: add,
get: get,
getAllPaged: getAllPaged,
@@ -13,6 +11,7 @@ exports = module.exports = {
ACTION_ACTIVATE: 'cloudron.activate',
ACTION_APP_CLONE: 'app.clone',
ACTION_APP_CONFIGURE: 'app.configure',
ACTION_APP_REPAIR: 'app.repair',
ACTION_APP_INSTALL: 'app.install',
ACTION_APP_RESTORE: 'app.restore',
ACTION_APP_UNINSTALL: 'app.uninstall',
@@ -22,13 +21,10 @@ exports = module.exports = {
ACTION_APP_OOM: 'app.oom',
ACTION_APP_UP: 'app.up',
ACTION_APP_DOWN: 'app.down',
ACTION_APP_TASK_START: 'app.task.start',
ACTION_APP_TASK_CRASH: 'app.task.crash',
ACTION_APP_TASK_SUCCESS: 'app.task.success',
ACTION_BACKUP_FINISH: 'backup.finish',
ACTION_BACKUP_START: 'backup.start',
ACTION_BACKUP_CLEANUP_START: 'backup.cleanup.start',
ACTION_BACKUP_CLEANUP_START: 'backup.cleanup.start', // obsolete
ACTION_BACKUP_CLEANUP_FINISH: 'backup.cleanup.finish',
ACTION_CERTIFICATE_NEW: 'certificate.new',
@@ -51,6 +47,7 @@ exports = module.exports = {
ACTION_RESTORE: 'cloudron.restore', // unused
ACTION_START: 'cloudron.start',
ACTION_UPDATE: 'cloudron.update',
ACTION_UPDATE_FINISH: 'cloudron.update.finish',
ACTION_USER_ADD: 'user.add',
ACTION_USER_LOGIN: 'user.login',
@@ -60,11 +57,13 @@ exports = module.exports = {
ACTION_DYNDNS_UPDATE: 'dyndns.update',
ACTION_SUPPORT_TICKET: 'support.ticket',
ACTION_SUPPORT_SSH: 'support.ssh',
ACTION_PROCESS_CRASH: 'system.crash'
};
var assert = require('assert'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:eventlog'),
eventlogdb = require('./eventlogdb.js'),
notifications = require('./notifications.js'),
@@ -73,28 +72,6 @@ var assert = require('assert'),
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function EventLogError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(EventLogError, Error);
EventLogError.INTERNAL_ERROR = 'Internal error';
EventLogError.NOT_FOUND = 'Not Found';
function add(action, source, data, callback) {
assert.strictEqual(typeof action, 'string');
assert.strictEqual(typeof source, 'object');
@@ -106,10 +83,10 @@ function add(action, source, data, callback) {
// we do only daily upserts for login actions, so they don't spam the db
var api = action === exports.ACTION_USER_LOGIN ? eventlogdb.upsert : eventlogdb.add;
api(uuid.v4(), action, source, data, function (error, id) {
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
if (error) return callback(error);
notifications.onEvent(id, action, source, data, function (error) {
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback(null, { id: id });
});
@@ -121,8 +98,7 @@ function get(id, callback) {
assert.strictEqual(typeof callback, 'function');
eventlogdb.get(id, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new EventLogError(EventLogError.NOT_FOUND, 'No such event'));
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback(null, result);
});
@@ -136,7 +112,7 @@ function getAllPaged(actions, search, page, perPage, callback) {
assert.strictEqual(typeof callback, 'function');
eventlogdb.getAllPaged(actions, search, page, perPage, function (error, events) {
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback(null, events);
});
@@ -147,7 +123,7 @@ function getByCreationTime(creationTime, callback) {
assert.strictEqual(typeof callback, 'function');
eventlogdb.getByCreationTime(creationTime, function (error, events) {
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback(null, events);
});
@@ -160,7 +136,7 @@ function cleanup(callback) {
d.setDate(d.getDate() - 10); // 10 days ago
eventlogdb.delByCreationTime(d, function (error) {
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback(null);
});
+12 -12
View File
@@ -14,8 +14,8 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
database = require('./database.js'),
DatabaseError = require('./databaseerror'),
mysql = require('mysql'),
safe = require('safetydance'),
util = require('util');
@@ -35,8 +35,8 @@ function get(eventId, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + EVENTLOG_FIELDS + ' FROM eventlog WHERE id = ?', [ eventId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Eventlog not found'));
callback(null, postProcess(result[0]));
});
@@ -68,7 +68,7 @@ function getAllPaged(actions, search, page, perPage, callback) {
data.push(perPage);
database.query(query, data, function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(postProcess);
@@ -82,7 +82,7 @@ function getByCreationTime(creationTime, callback) {
var query = 'SELECT ' + EVENTLOG_FIELDS + ' FROM eventlog WHERE creationTime >= ? ORDER BY creationTime DESC';
database.query(query, [ creationTime ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(postProcess);
@@ -98,8 +98,8 @@ function add(id, action, source, data, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO eventlog (id, action, source, data) VALUES (?, ?, ?, ?)', [ id, action, JSON.stringify(source), JSON.stringify(data) ], function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, 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));
callback(null, id);
});
@@ -123,7 +123,7 @@ function upsert(id, action, source, data, callback) {
}];
database.transaction(queries, function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result[0].affectedRows >= 1) return callback(null, result[1][0].id);
// no existing eventlog found, create one
@@ -135,7 +135,7 @@ function count(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT COUNT(*) AS total FROM eventlog', function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
return callback(null, result[0].total);
});
@@ -143,7 +143,7 @@ function count(callback) {
function clear(callback) {
database.query('DELETE FROM eventlog', function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
@@ -155,7 +155,7 @@ function delByCreationTime(creationTime, callback) {
// remove notifications that reference the events as well
database.query('SELECT * FROM eventlog WHERE creationTime <= ?', [ creationTime ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
async.eachSeries(result, function (item, iteratorCallback) {
async.series([
@@ -163,7 +163,7 @@ function delByCreationTime(creationTime, callback) {
database.query.bind(null, 'DELETE FROM eventlog WHERE id=?', [ item.id ])
], iteratorCallback);
}, function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
+322
View File
@@ -0,0 +1,322 @@
'use strict';
exports = module.exports = {
search: search,
verifyPassword: verifyPassword,
createAndVerifyUserIfNotExist: createAndVerifyUserIfNotExist,
testConfig: testConfig,
startSyncer: startSyncer,
injectPrivateFields: injectPrivateFields,
removePrivateFields: removePrivateFields,
sync: sync
};
var assert = require('assert'),
async = require('async'),
auditSource = require('./auditsource.js'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
debug = require('debug')('box:externalldap'),
ldap = require('ldapjs'),
settings = require('./settings.js'),
tasks = require('./tasks.js'),
users = require('./users.js');
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.bindPassword === constants.SECRET_PLACEHOLDER) newConfig.bindPassword = currentConfig.bindPassword;
}
function removePrivateFields(ldapConfig) {
assert.strictEqual(typeof ldapConfig, 'object');
if (ldapConfig.bindPassword) ldapConfig.bindPassword = constants.SECRET_PLACEHOLDER;
return ldapConfig;
}
function translateUser(ldapConfig, ldapUser) {
assert.strictEqual(typeof ldapConfig, 'object');
return {
username: ldapUser[ldapConfig.usernameField],
email: ldapUser.mail,
displayName: ldapUser.cn // user.giveName + ' ' + user.sn
};
}
function validUserRequirements(user) {
if (!user.username || !user.email || !user.displayName) {
debug(`[LDAP user empty username/email/displayName] username=${user.username} email=${user.email} displayName=${user.displayName}`);
return false;
} else {
return true;
}
}
// performs service bind if required
function getClient(externalLdapConfig, callback) {
assert.strictEqual(typeof externalLdapConfig, 'object');
assert.strictEqual(typeof callback, 'function');
// basic validation to not crash
try { ldap.parseDN(externalLdapConfig.baseDn); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid baseDn')); }
try { ldap.parseFilter(externalLdapConfig.filter); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid filter')); }
var client;
try {
client = ldap.createClient({ url: externalLdapConfig.url });
} catch (e) {
if (e instanceof ldap.ProtocolError) return callback(new BoxError(BoxError.BAD_FIELD, 'url protocol is invalid'));
return callback(new BoxError(BoxError.INTERNAL_ERROR, e));
}
if (!externalLdapConfig.bindDn) return callback(null, client);
client.bind(externalLdapConfig.bindDn, externalLdapConfig.bindPassword, function (error) {
if (error instanceof ldap.InvalidCredentialsError) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
callback(null, client, externalLdapConfig);
});
}
// TODO support search by email
function ldapSearch(externalLdapConfig, options, callback) {
assert.strictEqual(typeof externalLdapConfig, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
getClient(externalLdapConfig, function (error, client) {
if (error) return callback(error);
let searchOptions = {
paged: true,
filter: ldap.parseFilter(externalLdapConfig.filter),
scope: 'sub' // We may have to make this configurable
};
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 users at ${externalLdapConfig.baseDn} with filter ${searchOptions.filter.toString()}`);
client.search(externalLdapConfig.baseDn, 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 ldapUsers = [];
result.on('searchEntry', entry => ldapUsers.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, ldapUsers);
});
});
});
}
function testConfig(config, callback) {
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof callback, 'function');
if (config.provider === 'noop') return callback();
if (!config.url) return callback(new BoxError(BoxError.BAD_FIELD, 'url must not be empty'));
if (!config.url.startsWith('ldap://') && !config.url.startsWith('ldaps://')) return callback(new BoxError(BoxError.BAD_FIELD, 'url is missing ldap:// or ldaps:// prefix'));
if (!config.usernameField) config.usernameField = 'uid';
// bindDn may not be a dn!
if (!config.baseDn) return callback(new BoxError(BoxError.BAD_FIELD, 'basedn must not be empty'));
try { ldap.parseDN(config.baseDn); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid baseDn')); }
if (!config.filter) return callback(new BoxError(BoxError.BAD_FIELD, 'filter must not be empty'));
try { ldap.parseFilter(config.filter); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid filter')); }
getClient(config, function (error, client) {
if (error) return callback(error);
var opts = {
filter: config.filter,
scope: 'sub'
};
client.search(config.baseDn, opts, function (error, result) {
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
result.on('searchEntry', function (/* entry */) {});
result.on('error', function (error) { client.unbind(); callback(new BoxError(BoxError.BAD_FIELD, `Unable to search directory: ${error.message}`)); });
result.on('end', function (/* result */) { client.unbind(); callback(); });
});
});
}
function search(identifier, callback) {
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof callback, 'function');
settings.getExternalLdapConfig(function (error, externalLdapConfig) {
if (error) return callback(error);
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
ldapSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${identifier}` }, function (error, ldapUsers) {
if (error) return callback(error);
// translate ldap properties to ours
let users = ldapUsers.map(function (u) { return translateUser(externalLdapConfig, u); });
callback(null, users);
});
});
}
function createAndVerifyUserIfNotExist(identifier, password, callback) {
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof callback, 'function');
settings.getExternalLdapConfig(function (error, externalLdapConfig) {
if (error) return callback(error);
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
if (!externalLdapConfig.autoCreate) return callback(new BoxError(BoxError.BAD_STATE, 'auto create not enabled'));
ldapSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${identifier}` }, function (error, ldapUsers) {
if (error) return callback(error);
if (ldapUsers.length === 0) return callback(new BoxError(BoxError.NOT_FOUND));
if (ldapUsers.length > 1) return callback(new BoxError(BoxError.CONFLICT));
let user = translateUser(externalLdapConfig, ldapUsers[0]);
if (!validUserRequirements(user)) return callback(new BoxError(BoxError.BAD_FIELD));
users.create(user.username, null /* password */, user.email, user.displayName, { source: 'ldap' }, auditSource.EXTERNAL_LDAP_AUTO_CREATE, function (error, user) {
if (error) {
console.error('Failed to auto create user', user.username, error);
return callback(new BoxError(BoxError.INTERNAL_ERROR));
}
verifyPassword(user, password, function (error) {
if (error) return callback(error);
callback(null, user);
});
});
});
});
}
function verifyPassword(user, password, callback) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof callback, 'function');
settings.getExternalLdapConfig(function (error, externalLdapConfig) {
if (error) return callback(error);
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
ldapSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${user.username}` }, function (error, ldapUsers) {
if (error) return callback(error);
if (ldapUsers.length === 0) return callback(new BoxError(BoxError.NOT_FOUND));
if (ldapUsers.length > 1) return callback(new BoxError(BoxError.CONFLICT));
let client = ldap.createClient({ url: externalLdapConfig.url });
client.bind(ldapUsers[0].dn, password, function (error) {
if (error instanceof ldap.InvalidCredentialsError) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
callback(null, translateUser(externalLdapConfig, ldapUsers[0]));
});
});
});
}
function startSyncer(callback) {
assert.strictEqual(typeof callback, 'function');
settings.getExternalLdapConfig(function (error, externalLdapConfig) {
if (error) return callback(error);
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
tasks.add(tasks.TASK_SYNC_EXTERNAL_LDAP, [], function (error, taskId) {
if (error) return callback(error);
tasks.startTask(taskId, {}, function (error, result) {
debug('sync: done', error, result);
});
callback(null, taskId);
});
});
}
function sync(progressCallback, callback) {
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
progressCallback({ percent: 10, message: 'Starting ldap user sync' });
settings.getExternalLdapConfig(function (error, externalLdapConfig) {
if (error) return callback(error);
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
ldapSearch(externalLdapConfig, {}, function (error, ldapUsers) {
if (error) return callback(error);
debug(`Found ${ldapUsers.length} users`);
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
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) {
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.id, { 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);
});
});
});
}
+34 -34
View File
@@ -25,8 +25,8 @@ exports = module.exports = {
};
var assert = require('assert'),
database = require('./database.js'),
DatabaseError = require('./databaseerror');
BoxError = require('./boxerror.js'),
database = require('./database.js');
var GROUPS_FIELDS = [ 'id', 'name' ].join(',');
@@ -35,8 +35,8 @@ function get(groupId, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ' FROM userGroups WHERE id = ? ORDER BY name', [ groupId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
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]);
});
@@ -50,8 +50,8 @@ function getWithMembers(groupId, callback) {
' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' +
' WHERE userGroups.id = ? ' +
' GROUP BY userGroups.id', [ groupId ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Group not found'));
var result = results[0];
result.userIds = result.userIds ? result.userIds.split(',') : [ ];
@@ -64,7 +64,7 @@ function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ' FROM userGroups', function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, results);
});
@@ -74,8 +74,8 @@ function getAllWithMembers(callback) {
database.query('SELECT ' + GROUPS_FIELDS + ',GROUP_CONCAT(groupMembers.userId) AS userIds ' +
' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' +
' GROUP BY userGroups.id', function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
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(',') : [ ]; });
@@ -89,8 +89,8 @@ function add(id, name, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO userGroups (id, name) VALUES (?, ?)', [ id, name ], function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, 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));
callback(null);
});
@@ -113,9 +113,9 @@ function update(id, data, callback) {
args.push(id);
database.query('UPDATE userGroups SET ' + fields.join(', ') + ' WHERE id = ?', args, function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('userGroups_name') !== -1) return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'name already exists'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('userGroups_name') !== -1) return callback(new BoxError(BoxError.ALREADY_EXISTS, 'name already exists'));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Group not found'));
return callback(null);
});
@@ -131,8 +131,8 @@ function del(id, callback) {
queries.push({ query: 'DELETE FROM userGroups WHERE id = ?', args: [ id ] });
database.transaction(queries, function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result[1].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result[1].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Group not found'));
callback(error);
});
@@ -142,7 +142,7 @@ function count(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT COUNT(*) AS total FROM userGroups', function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
return callback(null, result[0].total);
});
@@ -150,10 +150,10 @@ function count(callback) {
function clear(callback) {
database.query('DELETE FROM groupMembers', function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
database.query('DELETE FROM userGroups', function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(error);
});
@@ -165,8 +165,8 @@ function getMembers(groupId, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT userId FROM groupMembers WHERE groupId=?', [ groupId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
// if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND)); // need to differentiate group with no members and invalid groupId
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
// if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Group not found')); // need to differentiate group with no members and invalid groupId
callback(error, result.map(function (r) { return r.userId; }));
});
@@ -184,8 +184,8 @@ function setMembers(groupId, userIds, callback) {
}
database.transaction(queries, function (error) {
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new BoxError(BoxError.NOT_FOUND, 'Group not found'));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(error);
});
@@ -196,8 +196,8 @@ function getMembership(userId, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT groupId FROM groupMembers WHERE userId=? ORDER BY groupId', [ userId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
// if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND)); // need to differentiate group with no members and invalid groupId
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
// if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Group not found')); // need to differentiate group with no members and invalid groupId
callback(error, result.map(function (r) { return r.groupId; }));
});
@@ -215,8 +215,8 @@ function setMembership(userId, groupIds, callback) {
});
database.transaction(queries, function (error) {
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, error.message));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new BoxError(BoxError.NOT_FOUND, error.message));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
@@ -228,9 +228,9 @@ function addMember(groupId, userId, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO groupMembers (groupId, userId) VALUES (?, ?)', [ groupId, userId ], function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, error));
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new BoxError(BoxError.NOT_FOUND, 'Group not found'));
if (error || result.affectedRows !== 1) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
@@ -242,8 +242,8 @@ function removeMember(groupId, userId, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM groupMembers WHERE groupId = ? AND userId = ?', [ groupId, userId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Group not found'));
callback(null);
});
@@ -255,7 +255,7 @@ function isMember(groupId, userId, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT 1 FROM groupMembers WHERE groupId=? AND userId=?', [ groupId, userId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, result.length !== 0);
});
@@ -267,7 +267,7 @@ function getGroups(userId, callback) {
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 DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, results);
});
+33 -64
View File
@@ -1,8 +1,6 @@
'use strict';
exports = module.exports = {
GroupsError: GroupsError,
create: create,
remove: remove,
get: get,
@@ -20,56 +18,29 @@ exports = module.exports = {
getGroups: getGroups,
setMembership: setMembership,
getMembership: getMembership
getMembership: getMembership,
count: count
};
var assert = require('assert'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
groupdb = require('./groupdb.js'),
util = require('util'),
uuid = require('uuid'),
_ = require('underscore');
// http://dustinsenos.com/articles/customErrorsInNode
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
function GroupsError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(GroupsError, Error);
GroupsError.INTERNAL_ERROR = 'Internal Error';
GroupsError.ALREADY_EXISTS = 'Already Exists';
GroupsError.NOT_FOUND = 'Not Found';
GroupsError.BAD_FIELD = 'Field error';
GroupsError.NOT_EMPTY = 'Not Empty';
GroupsError.NOT_ALLOWED = 'Not Allowed';
// keep this in sync with validateUsername
function validateGroupname(name) {
assert.strictEqual(typeof name, 'string');
if (name.length < 1) return new GroupsError(GroupsError.BAD_FIELD, 'name must be atleast 1 char');
if (name.length >= 200) return new GroupsError(GroupsError.BAD_FIELD, 'name too long');
if (name.length < 1) return new BoxError(BoxError.BAD_FIELD, 'name must be atleast 1 char', { field: 'name' });
if (name.length >= 200) return new BoxError(BoxError.BAD_FIELD, 'name too long', { field: 'name' });
if (constants.RESERVED_NAMES.indexOf(name) !== -1) return new GroupsError(GroupsError.BAD_FIELD, 'name is reserved');
if (constants.RESERVED_NAMES.indexOf(name) !== -1) return new BoxError(BoxError.BAD_FIELD, 'name is reserved', { field: name });
// need to consider valid LDAP characters here (e.g '+' is reserved)
if (/[^a-zA-Z0-9.-]/.test(name)) return new GroupsError(GroupsError.BAD_FIELD, 'name can only contain alphanumerals, hyphen and dot');
if (/[^a-zA-Z0-9.-]/.test(name)) return new BoxError(BoxError.BAD_FIELD, 'name can only contain alphanumerals, hyphen and dot', { field: 'name' });
return null;
}
@@ -86,8 +57,7 @@ function create(name, callback) {
var id = 'gid-' + uuid.v4();
groupdb.add(id, name, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new GroupsError(GroupsError.ALREADY_EXISTS));
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback(null, { id: id, name: name });
});
@@ -98,8 +68,7 @@ function remove(id, callback) {
assert.strictEqual(typeof callback, 'function');
groupdb.del(id, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupsError(GroupsError.NOT_FOUND));
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback(null);
});
@@ -110,8 +79,7 @@ function get(id, callback) {
assert.strictEqual(typeof callback, 'function');
groupdb.get(id, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupsError(GroupsError.NOT_FOUND));
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
if (error) return callback(error);
return callback(null, result);
});
@@ -122,8 +90,7 @@ function getWithMembers(id, callback) {
assert.strictEqual(typeof callback, 'function');
groupdb.getWithMembers(id, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupsError(GroupsError.NOT_FOUND));
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
if (error) return callback(error);
return callback(null, result);
});
@@ -133,7 +100,7 @@ function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
groupdb.getAll(function (error, result) {
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
if (error) return callback(error);
return callback(null, result);
});
@@ -143,7 +110,7 @@ function getAllWithMembers(callback) {
assert.strictEqual(typeof callback, 'function');
groupdb.getAllWithMembers(function (error, result) {
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
if (error) return callback(error);
return callback(null, result);
});
@@ -154,8 +121,7 @@ function getMembers(groupId, callback) {
assert.strictEqual(typeof callback, 'function');
groupdb.getMembers(groupId, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupsError(GroupsError.NOT_FOUND));
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
if (error) return callback(error);
return callback(null, result);
});
@@ -166,8 +132,7 @@ function getMembership(userId, callback) {
assert.strictEqual(typeof callback, 'function');
groupdb.getMembership(userId, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupsError(GroupsError.NOT_FOUND));
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
if (error) return callback(error);
return callback(null, result);
});
@@ -179,8 +144,7 @@ function setMembership(userId, groupIds, callback) {
assert.strictEqual(typeof callback, 'function');
groupdb.setMembership(userId, groupIds, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupsError(GroupsError.NOT_FOUND));
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
if (error) return callback(error);
return callback(null);
});
@@ -192,8 +156,7 @@ function addMember(groupId, userId, callback) {
assert.strictEqual(typeof callback, 'function');
groupdb.addMember(groupId, userId, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupsError(GroupsError.NOT_FOUND));
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
if (error) return callback(error);
return callback(null);
});
@@ -205,8 +168,7 @@ function setMembers(groupId, userIds, callback) {
assert.strictEqual(typeof callback, 'function');
groupdb.setMembers(groupId, userIds, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupsError(GroupsError.NOT_FOUND, 'Invalid group or user id'));
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
if (error) return callback(error);
return callback(null);
});
@@ -218,8 +180,7 @@ function removeMember(groupId, userId, callback) {
assert.strictEqual(typeof callback, 'function');
groupdb.removeMember(groupId, userId, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupsError(GroupsError.NOT_FOUND));
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
if (error) return callback(error);
return callback(null);
});
@@ -231,8 +192,7 @@ function isMember(groupId, userId, callback) {
assert.strictEqual(typeof callback, 'function');
groupdb.isMember(groupId, userId, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupsError(GroupsError.NOT_FOUND));
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
if (error) return callback(error);
return callback(null, result);
});
@@ -251,8 +211,7 @@ function update(groupId, data, callback) {
}
groupdb.update(groupId, _.pick(data, 'name'), function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupsError(GroupsError.NOT_FOUND));
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback(null);
});
@@ -263,8 +222,18 @@ function getGroups(userId, callback) {
assert.strictEqual(typeof callback, 'function');
groupdb.getGroups(userId, function (error, results) {
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback(null, results);
});
}
function count(callback) {
assert.strictEqual(typeof callback, 'function');
groupdb.count(function (error, count) {
if (error) return callback(error);
callback(null, count);
});
}
+5 -5
View File
@@ -6,7 +6,7 @@
exports = module.exports = {
// a version change recreates all containers with latest docker config
'version': '48.15.0',
'version': '48.17.0',
'baseImages': [
{ repo: 'cloudron/base', tag: 'cloudron/base:1.0.0@sha256:147a648a068a2e746644746bbfb42eb7a50d682437cead3c67c933c546357617' }
@@ -17,10 +17,10 @@ exports = module.exports = {
'images': {
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.0.2@sha256:a28320f313785816be60e3f865e09065504170a3d20ed37de675c719b32b01eb' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.0.2@sha256:6dcee0731dfb9b013ed94d56205eee219040ee806c7e251db3b3886eaa4947ff' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.0.2@sha256:95e006390ddce7db637e1672eb6f3c257d3c2652747424f529b1dee3cbe6728c' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.0.0@sha256:8a88dd334b62b578530a014ca1a2425a54cb9df1e475f5d3a36806e5cfa22121' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.3.1@sha256:9693e3ae42a12a7ac8cf5df94d828d46f5b22b4e2e1c7d1bc614d6ee2a22c365' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.2@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.1.0@sha256:6d1bf221cfe6124957e2c58b57c0a47214353496009296acb16adf56df1da9d5' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.1.0@sha256:f2cda21bd15c21bbf44432df412525369ef831a2d53860b5c5b1675e6f384de2' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.5.0@sha256:086ae1c9433d90a820326aa43914a2afe94ad707074ef2bc05a7ef4798e83655' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.2.0@sha256:fc9ca69d16e6ebdbd98ed53143d4a0d2212eef60cb638dc71219234e6f427a2c' },
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:0.1.0@sha256:e177c5bf5f38c84ce1dea35649c22a1b05f96eec67a54a812c5a35e585670f0f' }
}
};
+11 -8
View File
@@ -3,8 +3,9 @@
var assert = require('assert'),
async = require('async'),
authcodedb = require('./authcodedb.js'),
BoxError = require('./boxerror.js'),
debug = require('debug')('box:janitor'),
docker = require('./docker.js').connection,
Docker = require('dockerode'),
tokendb = require('./tokendb.js');
exports = module.exports = {
@@ -12,7 +13,9 @@ exports = module.exports = {
cleanupDockerVolumes: cleanupDockerVolumes
};
var NOOP_CALLBACK = function () { };
const NOOP_CALLBACK = function () { };
const gConnection = new Docker({ socketPath: '/var/run/docker.sock' });
function ignoreError(func) {
return function (callback) {
@@ -63,20 +66,20 @@ function cleanupTmpVolume(containerInfo, callback) {
assert.strictEqual(typeof containerInfo, 'object');
assert.strictEqual(typeof callback, 'function');
var cmd = 'find /tmp -mtime +10 -exec rm -rf {} +'.split(' '); // 10 days old
var cmd = 'find /tmp -type f -mtime +10 -exec rm -rf {} +'.split(' '); // 10 day old files
debug('cleanupTmpVolume %j', containerInfo.Names);
docker.getContainer(containerInfo.Id).exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true, Tty: false }, function (error, execContainer) {
if (error) return callback(new Error('Failed to exec container : ' + error.message));
gConnection.getContainer(containerInfo.Id).exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true, Tty: false }, function (error, execContainer) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, `Failed to exec container: ${error.message}`));
execContainer.start({ hijack: true }, function (error, stream) {
if (error) return callback(new Error('Failed to start exec container : ' + error.message));
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, `Failed to start exec container: ${error.message}`));
stream.on('error', callback);
stream.on('end', callback);
docker.modem.demuxStream(stream, process.stdout, process.stderr);
gConnection.modem.demuxStream(stream, process.stdout, process.stderr);
});
});
}
@@ -88,7 +91,7 @@ function cleanupDockerVolumes(callback) {
debug('Cleaning up docker volumes');
docker.listContainers({ all: 0 }, function (error, containers) {
gConnection.listContainers({ all: 0 }, function (error, containers) {
if (error) return callback(error);
async.eachSeries(containers, function (container, iteratorDone) {
+32 -32
View File
@@ -9,18 +9,16 @@ var assert = require('assert'),
appdb = require('./appdb.js'),
apps = require('./apps.js'),
async = require('async'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
debug = require('debug')('box:ldap'),
eventlog = require('./eventlog.js'),
ldap = require('ldapjs'),
mail = require('./mail.js'),
MailError = mail.MailError,
mailboxdb = require('./mailboxdb.js'),
path = require('path'),
safe = require('safetydance'),
users = require('./users.js'),
UsersError = users.UsersError;
users = require('./users.js');
var gServer = null;
@@ -135,7 +133,7 @@ function userSearch(req, res, next) {
var dn = ldap.parseDN('cn=' + entry.id + ',ou=users,dc=cloudron');
var groups = [ GROUP_USERS_DN ];
if (entry.admin || req.app.ownerId === entry.id) groups.push(GROUP_ADMINS_DN);
if (entry.admin) groups.push(GROUP_ADMINS_DN);
var displayName = entry.displayName || entry.username || ''; // displayName can be empty and username can be null
var nameParts = displayName.split(' ');
@@ -145,17 +143,18 @@ function userSearch(req, res, next) {
var obj = {
dn: dn.toString(),
attributes: {
objectclass: ['user'],
objectclass: ['user', 'inetorgperson', 'person' ],
objectcategory: 'person',
cn: entry.id,
uid: entry.id,
entryuuid: entry.id, // to support OpenLDAP clients
mail: entry.email,
mailAlternateAddress: entry.fallbackEmail,
displayname: displayName,
givenName: firstName,
username: entry.username,
samaccountname: entry.username, // to support ActiveDirectory clients
isadmin: (entry.admin || req.app.ownerId === entry.id) ? 1 : 0,
isadmin: entry.admin,
memberof: groups
}
};
@@ -195,7 +194,7 @@ function groupSearch(req, res, next) {
groups.forEach(function (group) {
var dn = ldap.parseDN('cn=' + group.name + ',ou=groups,dc=cloudron');
var members = group.admin ? result.filter(function (entry) { return entry.admin || req.app.ownerId === entry.id; }) : result;
var members = group.admin ? result.filter(function (entry) { return entry.admin; }) : result;
var obj = {
dn: dn.toString(),
@@ -244,7 +243,7 @@ function groupAdminsCompare(req, res, next) {
// we only support memberuid here, if we add new group attributes later add them here
if (req.attribute === 'memberuid') {
var found = result.find(function (u) { return u.id === req.value; });
if (found && (found.admin || req.app.ownerId == found.id)) return res.end(true);
if (found && found.admin) return res.end(true);
}
res.end(false);
@@ -261,7 +260,7 @@ function mailboxSearch(req, res, next) {
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
if (error && error.reason === DatabaseError.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()));
var obj = {
@@ -288,7 +287,7 @@ function mailboxSearch(req, res, next) {
} else if (req.dn.rdns[0].attrs.domain) {
var domain = req.dn.rdns[0].attrs.domain.value.toLowerCase();
mailboxdb.listMailboxes(domain, function (error, result) {
mailboxdb.listMailboxes(domain, 1, 1000, function (error, result) {
if (error) return next(new ldap.OperationsError(error.toString()));
var results = [];
@@ -334,7 +333,7 @@ function mailAliasSearch(req, res, next) {
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
mailboxdb.getAlias(parts[0], parts[1], function (error, alias) {
if (error && error.reason === DatabaseError.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()));
// https://wiki.debian.org/LDAP/MigrationTools/Examples
@@ -367,12 +366,13 @@ function mailingListSearch(req, res, next) {
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
var email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
var parts = email.split('@');
let email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
let parts = email.split('@');
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
const name = parts[0], domain = parts[1];
mailboxdb.getGroup(parts[0], parts[1], function (error, group) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
mail.resolveList(parts[0], parts[1], function (error, resolvedMembers) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.toString()));
// http://ldapwiki.willeke.com/wiki/Original%20Mailgroup%20Schema%20From%20Netscape
@@ -382,9 +382,9 @@ function mailingListSearch(req, res, next) {
attributes: {
objectclass: ['mailGroup'],
objectcategory: 'mailGroup',
cn: `${group.name}@${group.domain}`, // fully qualified
mail: `${group.name}@${group.domain}`,
mgrpRFC822MailMember: group.members.map(function (m) { return `${m}@${group.domain}`; })
cn: `${name}@${domain}`, // fully qualified
mail: `${name}@${domain}`,
mgrpRFC822MailMember: resolvedMembers // fully qualified
}
};
@@ -421,8 +421,8 @@ function authenticateUser(req, res, next) {
}
api(commonName, req.credentials || '', function (error, user) {
if (error && error.reason === UsersError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error && error.reason === UsersError.WRONG_PASSWORD) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
req.user = user;
@@ -457,18 +457,18 @@ function authenticateUserMailbox(req, res, next) {
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
mail.getDomain(parts[1], function (error, domain) {
if (error && error.reason === MailError.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.message));
if (!domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
if (error && error.reason === DatabaseError.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.message));
users.verify(mailbox.ownerId, req.credentials || '', function (error, result) {
if (error && error.reason === UsersError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error && error.reason === UsersError.WRONG_PASSWORD) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) });
@@ -555,7 +555,7 @@ function authenticateMailAddon(req, res, next) {
const addonId = req.dn.rdns[1].attrs.ou.value.toLowerCase(); // 'sendmail' or 'recvmail'
mail.getDomain(parts[1], function (error, domain) {
if (error && error.reason === MailError.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.message));
if (addonId === 'recvmail' && !domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
@@ -567,19 +567,19 @@ function authenticateMailAddon(req, res, next) {
// note: with sendmail addon, apps can send mail without a mailbox (unlike users)
appdb.getAppIdByAddonConfigValue(addonId, namePattern, req.credentials || '', function (error, appId) {
if (error && error.reason !== DatabaseError.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
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) {
if (error && error.reason === DatabaseError.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.message));
users.verify(mailbox.ownerId, req.credentials || '', function (error, result) {
if (error && error.reason === UsersError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error && error.reason === UsersError.WRONG_PASSWORD) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) });
@@ -640,7 +640,7 @@ function start(callback) {
res.end();
});
gServer.listen(config.get('ldapPort'), '0.0.0.0', callback);
gServer.listen(constants.LDAP_PORT, '0.0.0.0', callback);
}
function stop(callback) {
+7 -2
View File
@@ -1,6 +1,7 @@
'use strict';
var assert = require('assert'),
BoxError = require('./boxerror.js'),
debug = require('debug')('box:locker'),
EventEmitter = require('events').EventEmitter,
util = require('util');
@@ -22,7 +23,11 @@ Locker.prototype.OP_APPTASK = 'apptask';
Locker.prototype.lock = function (operation) {
assert.strictEqual(typeof operation, 'string');
if (this._operation !== null) return new Error('Already locked for ' + this._operation);
if (this._operation !== null) {
let error = new BoxError(BoxError.CONFLICT, `Locked for ${this._operation}`);
error.operation = this._operation;
return error;
}
this._operation = operation;
++this._lockDepth;
@@ -50,7 +55,7 @@ Locker.prototype.recursiveLock = function (operation) {
Locker.prototype.unlock = function (operation) {
assert.strictEqual(typeof operation, 'string');
if (this._operation !== operation) throw new Error('Mismatched unlock. Current lock is for ' + this._operation); // throw because this is a programming error
if (this._operation !== operation) throw BoxError(BoxError.BAD_STATE, 'Mismatched unlock. Current lock is for ' + this._operation); // throw because this is a programming error
if (--this._lockDepth === 0) {
debug('Released : %s', this._operation);
+20 -8
View File
@@ -1,11 +1,23 @@
# Generated by apptask for the /run mount
# Generated by apptask
# keep upto 7 rotated logs. rotation triggered daily or ahead of time if size is > 1M
<%= volumePath %>/*.log <%= volumePath %>/*/*.log <%= volumePath %>/*/*/*.log {
rotate 7
daily
compress
maxsize=1M
missingok
delaycompress
copytruncate
rotate 7
daily
compress
maxsize 1M
missingok
delaycompress
copytruncate
}
/home/yellowtent/platformdata/logs/<%= appId %>/*.log {
# only keep one rotated file, we currently do not send that over the api
rotate 1
size 10M
missingok
# we never compress so we can simply tail the files
nocompress
copytruncate
}
+168 -146
View File
@@ -26,6 +26,7 @@ exports = module.exports = {
startMail: restartMail,
restartMail: restartMail,
handleCertChanged: handleCertChanged,
getMailAuth: getMailAuth,
sendTestMail: sendTestMail,
@@ -45,19 +46,18 @@ exports = module.exports = {
addList: addList,
updateList: updateList,
removeList: removeList,
resolveList: resolveList,
_readDkimPublicKeySync: readDkimPublicKeySync,
MailError: MailError
_readDkimPublicKeySync: readDkimPublicKeySync
};
var assert = require('assert'),
async = require('async'),
config = require('./config.js'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:mail'),
dns = require('./native-dns.js'),
docker = require('./docker.js'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
hat = require('./hat.js'),
@@ -71,50 +71,25 @@ var assert = require('assert'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
shell = require('./shell.js'),
smtpTransport = require('nodemailer-smtp-transport'),
sysinfo = require('./sysinfo.js'),
users = require('./users.js'),
util = require('util'),
validator = require('validator'),
_ = require('underscore');
const DNS_OPTIONS = { timeout: 5000 };
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function MailError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(MailError, Error);
MailError.INTERNAL_ERROR = 'Internal Error';
MailError.EXTERNAL_ERROR = 'External Error';
MailError.BAD_FIELD = 'Bad Field';
MailError.ALREADY_EXISTS = 'Already Exists';
MailError.NOT_FOUND = 'Not Found';
MailError.IN_USE = 'In Use';
function validateName(name) {
assert.strictEqual(typeof name, 'string');
if (name.length < 1) return new MailError(MailError.BAD_FIELD, 'mailbox name must be atleast 1 char');
if (name.length >= 200) return new MailError(MailError.BAD_FIELD, 'mailbox name too long');
if (name.length < 1) return new BoxError(BoxError.BAD_FIELD, 'mailbox name must be atleast 1 char');
if (name.length >= 200) return new BoxError(BoxError.BAD_FIELD, 'mailbox name too long');
// also need to consider valid LDAP characters here (e.g '+' is reserved)
if (/[^a-zA-Z0-9.-]/.test(name)) return new MailError(MailError.BAD_FIELD, 'mailbox name can only contain alphanumerals and dot');
if (/[^a-zA-Z0-9.-]/.test(name)) return new BoxError(BoxError.BAD_FIELD, 'mailbox name can only contain alphanumerals and dot');
return null;
}
@@ -126,7 +101,6 @@ function checkOutboundPort25(callback) {
'smtp.gmail.com',
'smtp.live.com',
'smtp.mail.yahoo.com',
'smtp.o2.ie',
'smtp.comcast.net',
'smtp.1und1.de',
]);
@@ -149,13 +123,13 @@ function checkOutboundPort25(callback) {
relay.status = false;
relay.value = `Connect to ${smtpServer} timed out. Check if port 25 (outbound) is blocked`;
client.destroy();
callback(new Error('Timeout'), relay);
callback(new BoxError(BoxError.TIMEOUT, `Connect to ${smtpServer} timed out.`), relay);
});
client.on('error', function (error) {
relay.status = false;
relay.value = `Connect to ${smtpServer} failed: ${error.message}. Check if port 25 (outbound) is blocked`;
client.destroy();
callback(error, relay);
callback(new BoxError(BoxError.NETWORK_ERROR, `Connect to ${smtpServer} failed.`), relay);
});
}
@@ -204,7 +178,7 @@ function verifyRelay(relay, callback) {
if (relay.provider === 'cloudron-smtp' || relay.provider === 'noop') return callback();
checkSmtpRelay(relay, function (error) {
if (error) return callback(new MailError(MailError.BAD_FIELD, error.message));
if (error) return callback(new BoxError(BoxError.BAD_FIELD, error.message));
callback();
});
@@ -225,7 +199,7 @@ function checkDkim(mailDomain, callback) {
};
var dkimKey = readDkimPublicKeySync(domain);
if (!dkimKey) return callback(new Error('Failed to read dkim public key'), dkim);
if (!dkimKey) return callback(new BoxError(BoxError.FS_ERROR, `Failed to read dkim public key of ${domain}`), dkim);
dkim.expected = 'v=DKIM1; t=s; p=' + dkimKey;
@@ -263,14 +237,14 @@ function checkSpf(domain, mailFqdn, callback) {
let txtRecord = txtRecords[i].join(''); // https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
if (txtRecord.indexOf('v=spf1 ') !== 0) continue; // not SPF
spf.value = txtRecord;
spf.status = spf.value.indexOf(' a:' + config.adminFqdn()) !== -1;
spf.status = spf.value.indexOf(' a:' + settings.adminFqdn()) !== -1;
break;
}
if (spf.status) {
spf.expected = spf.value;
} else if (i !== txtRecords.length) {
spf.expected = 'v=spf1 a:' + config.adminFqdn() + ' ' + spf.value.slice('v=spf1 '.length);
spf.expected = 'v=spf1 a:' + settings.adminFqdn() + ' ' + spf.value.slice('v=spf1 '.length);
}
callback(null, spf);
@@ -304,7 +278,7 @@ function checkMx(domain, mailFqdn, callback) {
dns.resolve(mxRecords[0].exchange, 'A', DNS_OPTIONS, function (error, mxIps) {
if (error || mxIps.length !== 1) return callback(null, mx);
sysinfo.getPublicIp(function (error, ip) {
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(null, mx);
mx.status = mxIps[0] === ip;
@@ -354,16 +328,18 @@ function checkPtr(mailFqdn, callback) {
var ptr = {
domain: null,
name: null,
type: 'PTR',
value: null,
expected: mailFqdn, // any trailing '.' is added by client software (https://lists.gt.net/spf/devel/7918)
status: false
};
sysinfo.getPublicIp(function (error, ip) {
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(error, ptr);
ptr.domain = ip.split('.').reverse().join('.') + '.in-addr.arpa';
ptr.name = ip;
dns.resolve(ptr.domain, 'PTR', DNS_OPTIONS, function (error, ptrRecords) {
if (error) return callback(error, ptr);
@@ -442,7 +418,7 @@ const RBL_LIST = [
function checkRblStatus(domain, callback) {
assert.strictEqual(typeof callback, 'function');
sysinfo.getPublicIp(function (error, ip) {
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(error, ip);
var flippedIp = ip.split('.').reverse().join('.');
@@ -480,7 +456,7 @@ function getStatus(domain, callback) {
// ensure we always have a valid toplevel properties for the api
var results = {
dns: {}, // { mx: { expected, value }, dmarc: { expected, value }, dkim: { expected, value }, spf: { expected, value }, ptr: { expected, value } }
dns: {}, // { mx/dmarc/dkim/spf/ptr: { expected, value, name, domain, type } }
rbl: {}, // { status, ip, servers: [{name,site,dns}]} optional. only for cloudron-smtp
relay: {} // { status, value } always checked
};
@@ -490,14 +466,14 @@ function getStatus(domain, callback) {
func(function (error, result) {
if (error) debug('Ignored error - ' + what + ':', error);
safe.set(results, what, result);
safe.set(results, what, result || {});
callback();
});
};
}
const mailFqdn = config.mailFqdn();
const mailFqdn = settings.mailFqdn();
getDomain(domain, function (error, mailDomain) {
if (error) return callback(error);
@@ -545,7 +521,7 @@ function checkConfiguration(callback) {
Object.keys(result.dns).forEach((type) => {
const record = result.dns[type];
if (!record.status) message.push(`${type.toUpperCase()} DNS record did not match. Expected: \`${record.expected}\`. Actual: \`${record.value}\``);
if (!record.status) message.push(`${type.toUpperCase()} DNS record (${record.type}) did not match.\n * Hostname: \`${record.name}\`\n * Expected: \`${record.expected}\`\n * Actual: \`${record.value}\``);
});
if (result.relay && result.relay.status === false) message.push(`Relay error: ${result.relay.value}`);
if (result.rbl && result.rbl.status === false) { // rbl field contents is optional
@@ -568,15 +544,16 @@ function checkConfiguration(callback) {
markdownMessage += '\n\n';
});
if (markdownMessage) markdownMessage += 'Email Status is checked every 30 minutes\n See the [troubleshooting docs](https://cloudron.io/documentation/troubleshooting/#mail-dns) for more information.\n';
if (markdownMessage) markdownMessage += 'Email Status is checked every 30 minutes.\n See the [troubleshooting docs](https://cloudron.io/documentation/troubleshooting/#mail-dns) for more information.\n';
callback(null, markdownMessage); // empty message means all status checks succeeded
});
});
}
function createMailConfig(mailFqdn, callback) {
function createMailConfig(mailFqdn, mailDomain, callback) {
assert.strictEqual(typeof mailFqdn, 'string');
assert.strictEqual(typeof mailDomain, 'string');
assert.strictEqual(typeof callback, 'function');
debug('createMailConfig: generating mail config');
@@ -587,14 +564,15 @@ function createMailConfig(mailFqdn, callback) {
const mailOutDomains = mailDomains.filter(d => d.relay.provider !== 'noop').map(d => d.domain).join(',');
const mailInDomains = mailDomains.filter(function (d) { return d.enabled; }).map(function (d) { return d.domain; }).join(',');
// mail_domain is used for SRS
if (!safe.fs.writeFileSync(path.join(paths.ADDON_CONFIG_DIR, 'mail/mail.ini'),
`mail_in_domains=${mailInDomains}\nmail_out_domains=${mailOutDomains}\nmail_server_name=${mailFqdn}\n\n`, 'utf8')) {
return callback(new Error('Could not create mail var file:' + safe.error.message));
`mail_in_domains=${mailInDomains}\nmail_out_domains=${mailOutDomains}\nmail_server_name=${mailFqdn}\nmail_domain=${mailDomain}\n\n`, 'utf8')) {
return callback(new BoxError(BoxError.FS_ERROR, 'Could not create mail var file:' + safe.error.message));
}
// enable_outbound makes plugin forward email for relayed mail. non-relayed mail always hits LMTP plugin first
if (!safe.fs.writeFileSync(path.join(paths.ADDON_CONFIG_DIR, 'mail/smtp_forward.ini'), 'enable_outbound=false\ndomain_selector=mail_from\n', 'utf8')) {
return callback(new Error('Could not create smtp forward file:' + safe.error.message));
return callback(new BoxError(BoxError.FS_ERROR, 'Could not create smtp forward file:' + safe.error.message));
}
// create sections for per-domain configuration
@@ -604,7 +582,7 @@ function createMailConfig(mailFqdn, callback) {
if (!safe.fs.appendFileSync(path.join(paths.ADDON_CONFIG_DIR, 'mail/mail.ini'),
`[${domain.domain}]\ncatch_all=${catchAll}\nmail_from_validation=${mailFromValidation}\n\n`, 'utf8')) {
return callback(new Error('Could not create mail var file:' + safe.error.message));
return callback(new BoxError(BoxError.FS_ERROR, 'Could not create mail var file:' + safe.error.message));
}
const relay = domain.relay;
@@ -620,7 +598,7 @@ function createMailConfig(mailFqdn, callback) {
if (!safe.fs.appendFileSync(paths.ADDON_CONFIG_DIR + '/mail/smtp_forward.ini',
`[${domain.domain}]\nenable_outbound=true\nhost=${host}\nport=${port}\nenable_tls=true\nauth_type=${authType}\nauth_user=${username}\nauth_pass=${password}\n\n`, 'utf8')) {
return callback(new Error('Could not create mail var file:' + safe.error.message));
return callback(new BoxError(BoxError.FS_ERROR, 'Could not create mail var file:' + safe.error.message));
}
});
@@ -649,13 +627,13 @@ function configureMail(mailFqdn, mailDomain, callback) {
const mailCertFilePath = path.join(paths.ADDON_CONFIG_DIR, 'mail/tls_cert.pem');
const mailKeyFilePath = path.join(paths.ADDON_CONFIG_DIR, 'mail/tls_key.pem');
if (!safe.child_process.execSync(`cp ${bundle.certFilePath} ${mailCertFilePath}`)) return callback(new Error('Could not create cert file:' + safe.error.message));
if (!safe.child_process.execSync(`cp ${bundle.keyFilePath} ${mailKeyFilePath}`)) return callback(new Error('Could not create key 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));
shell.exec('startMail', 'docker rm -f mail || true', function (error) {
if (error) return callback(error);
createMailConfig(mailFqdn, function (error, allowInbound) {
createMailConfig(mailFqdn, mailDomain, function (error, allowInbound) {
if (error) return callback(error);
var ports = allowInbound ? '-p 587:2525 -p 993:9993 -p 4190:4190 -p 25:2525' : '';
@@ -686,20 +664,46 @@ function configureMail(mailFqdn, mailDomain, callback) {
});
}
function getMailAuth(callback) {
assert.strictEqual(typeof callback, 'function');
docker.inspect('mail', function (error, data) {
if (error) return callback(error);
const ip = safe.query(data, 'NetworkSettings.Networks.cloudron.IPAddress');
if (!ip) return callback(new BoxError(BoxError.MAIL_ERROR, 'Error querying mail server IP'));
// extract the relay token for auth
const env = safe.query(data, 'Config.Env', null);
if (!env) return callback(new BoxError(BoxError.MAIL_ERROR, 'Error getting mail env'));
const tmp = env.find(function (e) { return e.indexOf('CLOUDRON_RELAY_TOKEN') === 0; });
if (!tmp) return callback(new BoxError(BoxError.MAIL_ERROR, 'Error getting CLOUDRON_RELAY_TOKEN env var'));
const relayToken = tmp.slice('CLOUDRON_RELAY_TOKEN'.length + 1); // +1 for the = sign
if (!relayToken) return callback(new BoxError(BoxError.MAIL_ERROR, 'Error parsing CLOUDRON_RELAY_TOKEN'));
callback(null, {
ip,
port: constants.INTERNAL_SMTP_PORT,
relayToken
});
});
}
function restartMail(callback) {
assert.strictEqual(typeof callback, 'function');
if (process.env.BOX_ENV === 'test' && !process.env.TEST_CREATE_INFRA) return callback();
debug(`restartMail: restarting mail container with ${config.mailFqdn()} ${config.adminDomain()}`);
configureMail(config.mailFqdn(), config.adminDomain(), callback);
debug(`restartMail: restarting mail container with ${settings.mailFqdn()} ${settings.adminDomain()}`);
configureMail(settings.mailFqdn(), settings.adminDomain(), callback);
}
function restartMailIfActivated(callback) {
assert.strictEqual(typeof callback, 'function');
users.isActivated(function (error, activated) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
if (error) return callback(error);
if (!activated) {
debug('restartMailIfActivated: skipping restart of mail container since Cloudron is not activated yet');
return callback(); // not provisioned yet, do not restart container after dns setup
@@ -721,8 +725,7 @@ function getDomain(domain, callback) {
assert.strictEqual(typeof callback, 'function');
maildb.get(domain, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
if (error) return callback(error);
return callback(null, result);
});
@@ -732,7 +735,7 @@ function getDomains(callback) {
assert.strictEqual(typeof callback, 'function');
maildb.list(function (error, results) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
if (error) return callback(error);
return callback(null, results);
});
@@ -745,7 +748,7 @@ function txtRecordsWithSpf(domain, mailFqdn, callback) {
assert.strictEqual(typeof callback, 'function');
domains.getDnsRecords('', domain, 'TXT', function (error, txtRecords) {
if (error) return new MailError(MailError.EXTERNAL_ERROR, error.message);
if (error) return error;
debug('txtRecordsWithSpf: current txt records - %j', txtRecords);
@@ -794,16 +797,16 @@ function ensureDkimKeySync(mailDomain) {
if (!safe.fs.mkdirSync(dkimPath) && safe.error.code !== 'EEXIST') {
debug('Error creating dkim.', safe.error);
return new MailError(MailError.INTERNAL_ERROR, safe.error);
return new BoxError(BoxError.FS_ERROR, safe.error);
}
if (!safe.child_process.execSync('openssl genrsa -out ' + dkimPrivateKeyFile + ' 1024')) return new MailError(MailError.INTERNAL_ERROR, safe.error);
if (!safe.child_process.execSync('openssl rsa -in ' + dkimPrivateKeyFile + ' -out ' + dkimPublicKeyFile + ' -pubout -outform PEM')) return new MailError(MailError.INTERNAL_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.fs.writeFileSync(dkimSelectorFile, mailDomain.dkimSelector, 'utf8')) return new MailError(MailError.INTERNAL_ERROR, safe.error);
if (!safe.fs.writeFileSync(dkimSelectorFile, mailDomain.dkimSelector, 'utf8')) return new BoxError(BoxError.FS_ERROR, safe.error);
// if the 'yellowtent' user of OS and the 'cloudron' user of mail container don't match, the keys become inaccessible by mail code
if (!safe.fs.chmodSync(dkimPrivateKeyFile, 0o644)) return new MailError(MailError.INTERNAL_ERROR, safe.error);
if (!safe.fs.chmodSync(dkimPrivateKeyFile, 0o644)) return new BoxError(BoxError.FS_ERROR, safe.error);
return null;
}
@@ -835,8 +838,7 @@ function upsertDnsRecords(domain, mailFqdn, callback) {
debug(`upsertDnsRecords: updating mail dns records of domain ${domain} and mail fqdn ${mailFqdn}`);
maildb.get(domain, function (error, mailDomain) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
if (error) return callback(error);
error = ensureDkimKeySync(mailDomain);
if (error) return callback(error);
@@ -844,7 +846,7 @@ function upsertDnsRecords(domain, mailFqdn, callback) {
if (process.env.BOX_ENV === 'test') return callback();
var dkimKey = readDkimPublicKeySync(domain);
if (!dkimKey) return callback(new MailError(MailError.INTERNAL_ERROR, new Error('Failed to read dkim public key')));
if (!dkimKey) return callback(new BoxError(BoxError.FS_ERROR, 'Failed to read dkim public key'));
// t=s limits the domainkey to this domain and not it's subdomains
var dkimRecord = { subdomain: `${mailDomain.dkimSelector}._domainkey`, domain: domain, type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
@@ -868,7 +870,7 @@ function upsertDnsRecords(domain, mailFqdn, callback) {
}, function (error, changeIds) {
if (error) {
debug(`upsertDnsRecords: failed to update: ${error}`);
return callback(new MailError(MailError.EXTERNAL_ERROR, error.message));
return callback(error);
}
debug('upsertDnsRecords: records %j added with changeIds %j', records, changeIds);
@@ -883,22 +885,22 @@ function setDnsRecords(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
upsertDnsRecords(domain, config.mailFqdn(), callback);
upsertDnsRecords(domain, settings.mailFqdn(), callback);
}
function onMailFqdnChanged(callback) {
assert.strictEqual(typeof callback, 'function');
const mailFqdn = config.mailFqdn(),
mailDomain = config.adminDomain();
const mailFqdn = settings.mailFqdn(),
mailDomain = settings.adminDomain();
domains.getAll(function (error, allDomains) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
if (error) return callback(error);
async.eachOfSeries(allDomains, function (domainObject, idx, iteratorDone) {
upsertDnsRecords(domainObject.domain, mailFqdn, iteratorDone);
}, function (error) {
if (error) return callback(new MailError(MailError.EXTERNAL_ERROR, error.message));
if (error) return callback(error);
configureMail(mailFqdn, mailDomain, callback);
});
@@ -909,15 +911,13 @@ function addDomain(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
const dkimSelector = domain === config.adminDomain() ? 'cloudron' : ('cloudron-' + config.adminDomain().replace(/\./g, ''));
const dkimSelector = domain === settings.adminDomain() ? 'cloudron' : ('cloudron-' + settings.adminDomain().replace(/\./g, ''));
maildb.add(domain, { dkimSelector }, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, 'Domain already exists'));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'No such domain'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
if (error) return callback(error);
async.series([
upsertDnsRecords.bind(null, domain, config.mailFqdn()), // do this first to ensure DKIM keys
upsertDnsRecords.bind(null, domain, settings.mailFqdn()), // do this first to ensure DKIM keys
restartMailIfActivated
], NOOP_CALLBACK); // do these asynchronously
@@ -929,12 +929,10 @@ function removeDomain(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
if (domain === config.adminDomain()) return callback(new MailError(MailError.IN_USE));
if (domain === settings.adminDomain()) return callback(new BoxError(BoxError.CONFLICT));
maildb.del(domain, function (error) {
if (error && error.reason === DatabaseError.IN_USE) return callback(new MailError(MailError.IN_USE));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, error.message));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
if (error) return callback(error);
restartMail(NOOP_CALLBACK);
@@ -946,7 +944,7 @@ function clearDomains(callback) {
assert.strictEqual(typeof callback, 'function');
maildb.clear(function (error) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback();
});
@@ -968,8 +966,7 @@ function setMailFromValidation(domain, enabled, callback) {
assert.strictEqual(typeof callback, 'function');
maildb.update(domain, { mailFromValidation: enabled }, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
if (error) return callback(error);
restartMail(NOOP_CALLBACK); // have to restart mail container since haraka cannot watch symlinked config files (mail.ini)
@@ -983,8 +980,7 @@ function setCatchAllAddress(domain, addresses, callback) {
assert.strictEqual(typeof callback, 'function');
maildb.update(domain, { catchAll: addresses }, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
if (error) return callback(error);
restartMail(NOOP_CALLBACK); // have to restart mail container since haraka cannot watch symlinked config files (mail.ini)
@@ -1010,8 +1006,7 @@ function setMailRelay(domain, relay, callback) {
if (error) return callback(error);
maildb.update(domain, { relay: relay }, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
if (error) return callback(error);
restartMail(NOOP_CALLBACK);
@@ -1028,8 +1023,7 @@ function setMailEnabled(domain, enabled, auditSource, callback) {
assert.strictEqual(typeof callback, 'function');
maildb.update(domain, { enabled: enabled }, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
if (error) return callback(error);
restartMail(NOOP_CALLBACK);
@@ -1048,19 +1042,21 @@ function sendTestMail(domain, to, callback) {
if (error) return callback(error);
mailer.sendTestMail(result.domain, to, function (error) {
if (error) return callback(new MailError(MailError.EXTERNAL_ERROR, error.message));
if (error) return callback(error);
callback();
});
});
}
function listMailboxes(domain, callback) {
function listMailboxes(domain, page, perPage, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
mailboxdb.listMailboxes(domain, function (error, result) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
mailboxdb.listMailboxes(domain, page, perPage, function (error, result) {
if (error) return callback(error);
callback(null, result);
});
@@ -1071,7 +1067,7 @@ function removeMailboxes(domain, callback) {
assert.strictEqual(typeof callback, 'function');
mailboxdb.delByDomain(domain, function (error) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback();
});
@@ -1083,8 +1079,7 @@ function getMailbox(name, domain, callback) {
assert.strictEqual(typeof callback, 'function');
mailboxdb.getMailbox(name, domain, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such mailbox'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback(null, result);
});
@@ -1103,8 +1098,7 @@ function addMailbox(name, domain, userId, auditSource, callback) {
if (error) return callback(error);
mailboxdb.addMailbox(name, domain, userId, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, `mailbox ${name} already exists`));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
if (error) return callback(error);
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_ADD, auditSource, { name, domain, userId });
@@ -1121,8 +1115,7 @@ function updateMailboxOwner(name, domain, userId, callback) {
name = name.toLowerCase();
mailboxdb.updateMailboxOwner(name, domain, userId, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such mailbox'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback(null);
});
@@ -1135,8 +1128,7 @@ function removeMailbox(name, domain, auditSource, callback) {
assert.strictEqual(typeof callback, 'function');
mailboxdb.del(name, domain, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such mailbox'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
if (error) return callback(error);
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_REMOVE, auditSource, { name, domain });
@@ -1144,13 +1136,14 @@ function removeMailbox(name, domain, auditSource, callback) {
});
}
function listAliases(domain, callback) {
function listAliases(domain, page, perPage, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
mailboxdb.listAliases(domain, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, error.message));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
mailboxdb.listAliases(domain, page, perPage, function (error, result) {
if (error) return callback(error);
callback(null, result);
});
@@ -1165,8 +1158,7 @@ function getAliases(name, domain, callback) {
if (error) return callback(error);
mailboxdb.getAliasesForName(name, domain, function (error, aliases) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such mailbox'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback(null, aliases);
});
@@ -1187,14 +1179,7 @@ function setAliases(name, domain, aliases, callback) {
}
mailboxdb.setAliasesForName(name, domain, aliases, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS && error.message.indexOf('mailboxes_name_domain_unique_index') !== -1) {
var aliasMatch = error.message.match(new RegExp(`^ER_DUP_ENTRY: Duplicate entry '(.*)-${domain}' for key 'mailboxes_name_domain_unique_index'$`));
if (!aliasMatch) return callback(new MailError(MailError.ALREADY_EXISTS, error.message));
return callback(new MailError(MailError.ALREADY_EXISTS, `Mailbox, mailinglist or alias for ${aliasMatch[1]} already exists`));
}
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, error.message));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such mailbox'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback(null);
});
@@ -1204,8 +1189,8 @@ function getLists(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.listGroups(domain, function (error, result) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
mailboxdb.getLists(domain, function (error, result) {
if (error) return callback(error);
callback(null, result);
});
@@ -1216,9 +1201,8 @@ function getList(domain, listName, callback) {
assert.strictEqual(typeof listName, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.getGroup(listName, domain, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such list'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
mailboxdb.getList(listName, domain, function (error, result) {
if (error) return callback(error);
callback(null, result);
});
@@ -1237,15 +1221,11 @@ function addList(name, domain, members, auditSource, callback) {
if (error) return callback(error);
for (var i = 0; i < members.length; i++) {
members[i] = members[i].toLowerCase();
error = validateName(members[i]);
if (error) return callback(error);
if (!validator.isEmail(members[i])) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid mail member: ' + members[i]));
}
mailboxdb.addGroup(name, domain, members, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, 'list already exits'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
mailboxdb.addList(name, domain, members, function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_MAIL_LIST_ADD, auditSource, { name, domain });
@@ -1265,15 +1245,11 @@ function updateList(name, domain, members, callback) {
if (error) return callback(error);
for (var i = 0; i < members.length; i++) {
members[i] = members[i].toLowerCase();
error = validateName(members[i]);
if (error) return callback(error);
if (!validator.isEmail(members[i])) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid email: ' + members[i]));
}
mailboxdb.updateList(name, domain, members, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such mailbox'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback(null);
});
@@ -1286,11 +1262,57 @@ function removeList(name, domain, auditSource, callback) {
assert.strictEqual(typeof callback, 'function');
mailboxdb.del(name, domain, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such list'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
if (error) return callback(error);
eventlog.add(eventlog.ACTION_MAIL_LIST_ADD, auditSource, { name, domain });
eventlog.add(eventlog.ACTION_MAIL_LIST_REMOVE, auditSource, { name, domain });
callback();
});
}
function resolveList(listName, listDomain, callback) {
assert.strictEqual(typeof listName, 'string');
assert.strictEqual(typeof listDomain, 'string');
assert.strictEqual(typeof callback, 'function');
getDomains(function (error, mailDomains) {
if (error) return callback(error);
const mailInDomains = mailDomains.filter(function (d) { return d.enabled; }).map(function (d) { return d.domain; }).join(',');
mailboxdb.getList(listName, listDomain, function (error, list) {
if (error) return callback(error);
let result = [], toResolve = list.members.slice(), visited = []; // slice creates a copy of array
async.whilst(() => toResolve.length != 0, function (iteratorCallback) {
const toProcess = toResolve.shift();
const parts = toProcess.split('@');
const memberName = parts[0].split('+')[0], memberDomain = parts[1];
if (!mailInDomains.includes(memberDomain)) { result.push(toProcess); return iteratorCallback(); } // external domain
const member =`${memberName}@${memberDomain}`; // cleaned up without any '+' subaddress
if (visited.includes(member)) {
debug(`resolveList: list ${listName}@${listDomain} has a recursion at member ${member}`);
return iteratorCallback();
}
visited.push(member);
mailboxdb.get(memberName, memberDomain, function (error, entry) {
if (error && error.reason == BoxError.NOT_FOUND) { result.push(member); return iteratorCallback(); }
if (error) return iteratorCallback(error);
if (entry.type === mailboxdb.TYPE_MAILBOX) { result.push(member); return iteratorCallback(); }
// no need to resolve alias because we only allow one level and within same domain
if (entry.type === mailboxdb.TYPE_ALIAS) { result.push(`${entry.aliasTarget}@${entry.domain}`); return iteratorCallback(); }
toResolve = toResolve.concat(entry.members);
iteratorCallback();
});
}, function (error) {
callback(error, result);
});
});
});
}
+20
View File
@@ -0,0 +1,20 @@
<%if (format === 'text') { %>
Dear Cloudron Admin,
Cloudron update failed because of the following reason:
-------------------------------------
<%- message %>
-------------------------------------
Powered by https://cloudron.io
Sent at: <%= new Date().toUTCString() %>
<% } else { %>
<% } %>
+68 -43
View File
@@ -2,7 +2,7 @@
exports = module.exports = {
addMailbox: addMailbox,
addGroup: addGroup,
addList: addList,
updateMailboxOwner: updateMailboxOwner,
updateList: updateList,
@@ -10,10 +10,11 @@ exports = module.exports = {
listAliases: listAliases,
listMailboxes: listMailboxes,
listGroups: listGroups,
getLists: getLists,
get: get,
getMailbox: getMailbox,
getGroup: getGroup,
getList: getList,
getAlias: getAlias,
getAliasesForName: getAliasesForName,
@@ -33,8 +34,8 @@ exports = module.exports = {
};
var assert = require('assert'),
BoxError = require('./boxerror.js'),
database = require('./database.js'),
DatabaseError = require('./databaseerror.js'),
safe = require('safetydance'),
util = require('util');
@@ -54,8 +55,8 @@ function addMailbox(name, domain, ownerId, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO mailboxes (name, type, domain, ownerId) VALUES (?, ?, ?, ?)', [ name, exports.TYPE_MAILBOX, domain, ownerId ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'mailbox already exists'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, 'mailbox already exists'));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
@@ -68,14 +69,14 @@ function updateMailboxOwner(name, domain, ownerId, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('UPDATE mailboxes SET ownerId = ? WHERE name = ? AND domain = ?', [ ownerId, name, domain ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found'));
callback(null);
});
}
function addGroup(name, domain, members, callback) {
function addList(name, domain, members, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(members));
@@ -83,8 +84,8 @@ function addGroup(name, domain, members, callback) {
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, membersJson) VALUES (?, ?, ?, ?, ?)',
[ name, exports.TYPE_LIST, domain, 'admin', JSON.stringify(members) ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'mailbox already exists'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, 'mailbox already exists'));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
@@ -98,8 +99,8 @@ function updateList(name, domain, members, callback) {
database.query('UPDATE mailboxes SET membersJson = ? WHERE name = ? AND domain = ?',
[ JSON.stringify(members), name, domain ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found'));
callback(null);
});
@@ -109,7 +110,7 @@ function clear(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('TRUNCATE TABLE mailboxes', [], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}
@@ -121,8 +122,8 @@ function del(name, domain, callback) {
// deletes aliases as well
database.query('DELETE FROM mailboxes WHERE (name=? OR aliasTarget = ?) AND domain = ?', [ name, name, domain ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found'));
callback(null);
});
@@ -133,7 +134,7 @@ function delByDomain(domain, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM mailboxes WHERE domain = ?', [ domain ], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
@@ -144,7 +145,7 @@ function delByOwnerId(id, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM mailboxes WHERE ownerId=?', [ id ], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
@@ -161,14 +162,28 @@ function updateName(oldName, oldDomain, newName, newDomain, callback) {
if (oldName === newName && oldDomain === newDomain) return callback(null);
database.query('UPDATE mailboxes SET name=?, domain=? WHERE name=? AND domain = ?', [ newName, newDomain, oldName, oldDomain ], function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'mailbox already exists'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
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 (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found'));
callback(null);
});
}
function get(name, domain, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND domain = ?',
[ name, domain ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found'));
callback(null, postProcess(results[0]));
});
}
function getMailbox(name, domain, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
@@ -176,20 +191,22 @@ function getMailbox(name, domain, callback) {
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND type = ? AND domain = ?',
[ name, exports.TYPE_MAILBOX, domain ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found'));
callback(null, postProcess(results[0]));
});
}
function listMailboxes(domain, callback) {
function listMailboxes(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 type = ? AND domain = ? ORDER BY name',
database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? AND domain = ? ORDER BY name LIMIT ${(page-1)*perPage},${perPage}`,
[ exports.TYPE_MAILBOX, domain ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(function (result) { postProcess(result); });
@@ -197,13 +214,13 @@ function listMailboxes(domain, callback) {
});
}
function listGroups(domain, callback) {
function getLists(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE type = ? AND domain = ?',
[ exports.TYPE_LIST, domain ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(function (result) { postProcess(result); });
@@ -211,15 +228,15 @@ function listGroups(domain, callback) {
});
}
function getGroup(name, domain, callback) {
function getList(name, domain, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE type = ? AND name = ? AND domain = ?',
[ exports.TYPE_LIST, name, domain ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found'));
callback(null, postProcess(results[0]));
});
@@ -230,8 +247,8 @@ function getByOwnerId(ownerId, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE ownerId = ? ORDER BY name', [ ownerId ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found'));
results.forEach(function (result) { postProcess(result); });
@@ -246,8 +263,8 @@ function setAliasesForName(name, domain, aliases, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND domain = ?', [ name, domain ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found'));
var queries = [];
// clear existing aliases
@@ -258,8 +275,14 @@ function setAliasesForName(name, domain, aliases, callback) {
});
database.transaction(queries, function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error.message));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error && error.code === 'ER_DUP_ENTRY' && error.message.indexOf('mailboxes_name_domain_unique_index') !== -1) {
var aliasMatch = error.message.match(new RegExp(`^ER_DUP_ENTRY: Duplicate entry '(.*)-${domain}' for key 'mailboxes_name_domain_unique_index'$`));
if (!aliasMatch) return callback(new BoxError(BoxError.ALREADY_EXISTS, error));
return callback(new BoxError(BoxError.ALREADY_EXISTS, `Mailbox, mailinglist or alias for ${aliasMatch[1]} already exists`));
}
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
@@ -273,20 +296,22 @@ function getAliasesForName(name, domain, callback) {
database.query('SELECT name FROM mailboxes WHERE type = ? AND aliasTarget = ? AND domain = ? ORDER BY name',
[ exports.TYPE_ALIAS, name, domain ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_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, callback) {
function listAliases(domain, page, perPage, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE domain = ? AND type = ? ORDER BY name',
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 DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(function (result) { postProcess(result); });
@@ -301,8 +326,8 @@ function getAlias(name, domain, callback) {
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND type = ? AND domain = ?',
[ name, exports.TYPE_ALIAS, domain ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found'));
results.forEach(function (result) { postProcess(result); });
+13 -13
View File
@@ -15,8 +15,8 @@ exports = module.exports = {
};
var assert = require('assert'),
BoxError = require('./boxerror.js'),
database = require('./database.js'),
DatabaseError = require('./databaseerror.js'),
safe = require('safetydance');
var MAILDB_FIELDS = [ 'domain', 'enabled', 'mailFromValidation', 'catchAllJson', 'relayJson', 'dkimSelector' ].join(',');
@@ -40,9 +40,9 @@ function add(domain, data, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO mail (domain, dkimSelector) VALUES (?, ?)', [ domain, data.dkimSelector || 'cloudron' ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'mail domain already exists'));
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND), 'no such domain');
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, 'mail domain already exists'));
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new BoxError(BoxError.NOT_FOUND), 'no such domain');
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
@@ -53,7 +53,7 @@ function clear(callback) {
// using TRUNCATE makes it fail foreign key check
database.query('DELETE FROM mail', [], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}
@@ -64,9 +64,9 @@ function del(domain, callback) {
// deletes aliases as well
database.query('DELETE FROM mail WHERE domain=?', [ domain ], function (error, result) {
if (error && error.code === 'ER_ROW_IS_REFERENCED_2') return callback(new DatabaseError(DatabaseError.IN_USE));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error && error.code === 'ER_ROW_IS_REFERENCED_2') return callback(new BoxError(BoxError.CONFLICT));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mail domain not found'));
callback(null);
});
@@ -77,8 +77,8 @@ function get(domain, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILDB_FIELDS + ' FROM mail WHERE domain = ?', [ domain ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mail domain not found'));
callback(null, postProcess(results[0]));
});
@@ -88,7 +88,7 @@ function list(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILDB_FIELDS + ' FROM mail ORDER BY domain', function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(function (result) { postProcess(result); });
@@ -118,8 +118,8 @@ function update(domain, data, callback) {
args.push(domain);
database.query('UPDATE mail SET ' + fields.join(', ') + ' WHERE domain=?', args, function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Mail domain not found'));
callback(null);
});
+37 -29
View File
@@ -17,6 +17,7 @@ exports = module.exports = {
backupFailed: backupFailed,
certificateRenewalError: certificateRenewalError,
boxUpdateError: boxUpdateError,
sendTestMail: sendTestMail,
@@ -24,11 +25,11 @@ exports = module.exports = {
};
var assert = require('assert'),
config = require('./config.js'),
BoxError = require('./boxerror.js'),
custom = require('./custom.js'),
debug = require('debug')('box:mailer'),
docker = require('./docker.js').connection,
ejs = require('ejs'),
mail = require('./mail.js'),
nodemailer = require('nodemailer'),
path = require('path'),
safe = require('safetydance'),
@@ -54,7 +55,7 @@ function getMailConfig(callback) {
callback(null, {
cloudronName: cloudronName,
notificationFrom: `"${cloudronName}" <no-reply@${config.adminDomain()}>`
notificationFrom: `"${cloudronName}" <no-reply@${settings.adminDomain()}>`
});
});
}
@@ -68,31 +69,20 @@ function sendMail(mailOptions, callback) {
return callback();
}
docker.getContainer('mail').inspect(function (error, data) {
mail.getMailAuth(function (error, data) {
if (error) return callback(error);
var mailServerIp = safe.query(data, 'NetworkSettings.Networks.cloudron.IPAddress');
if (!mailServerIp) return callback('Error querying mail server IP');
// extract the relay token for auth
const env = safe.query(data, 'Config.Env', null);
if (!env) return callback(new Error('Error getting mail env'));
const tmp = env.find(function (e) { return e.indexOf('CLOUDRON_RELAY_TOKEN') === 0; });
if (!tmp) return callback(new Error('Error getting CLOUDRON_RELAY_TOKEN env var'));
const relayToken = tmp.slice('CLOUDRON_RELAY_TOKEN'.length + 1); // +1 for the = sign
if (!relayToken) return callback(new Error('Error parsing CLOUDRON_RELAY_TOKEN'));
var transport = nodemailer.createTransport(smtpTransport({
host: mailServerIp,
port: config.get('smtpPort'),
host: data.ip,
port: data.port,
auth: {
user: mailOptions.authUser || `no-reply@${config.adminDomain()}`,
pass: relayToken
user: mailOptions.authUser || `no-reply@${settings.adminDomain()}`,
pass: data.relayToken
}
}));
transport.sendMail(mailOptions, function (error) {
if (error) return callback(error);
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
debug(`Email "${mailOptions.subject}" sent to ${mailOptions.to}`);
@@ -146,11 +136,11 @@ function sendInvite(user, invitor) {
var templateData = {
user: user,
webadminUrl: config.adminOrigin(),
setupLink: `${config.adminOrigin()}/api/v1/session/account/setup.html?reset_token=${user.resetToken}&email=${encodeURIComponent(user.email)}`,
webadminUrl: settings.adminOrigin(),
setupLink: `${settings.adminOrigin()}/api/v1/session/account/setup.html?reset_token=${user.resetToken}&email=${encodeURIComponent(user.email)}`,
invitor: invitor,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataText = JSON.parse(JSON.stringify(templateData));
@@ -183,7 +173,7 @@ function userAdded(mailTo, user) {
var templateData = {
user: user,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataText = JSON.parse(JSON.stringify(templateData));
@@ -233,9 +223,9 @@ function passwordReset(user) {
var templateData = {
user: user,
resetLink: `${config.adminOrigin()}/api/v1/session/password/reset.html?reset_token=${user.resetToken}&email=${encodeURIComponent(user.email)}`,
resetLink: `${settings.adminOrigin()}/api/v1/session/password/reset.html?reset_token=${user.resetToken}&email=${encodeURIComponent(user.email)}`,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataText = JSON.parse(JSON.stringify(templateData));
@@ -314,7 +304,7 @@ function appUpdated(mailTo, app, callback) {
changelog: app.manifest.changelog,
changelogHTML: converter.makeHtml(app.manifest.changelog),
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataText = JSON.parse(JSON.stringify(templateData));
@@ -350,11 +340,11 @@ function appUpdatesAvailable(mailTo, apps, hasSubscription, callback) {
});
var templateData = {
webadminUrl: config.adminOrigin(),
webadminUrl: settings.adminOrigin(),
hasSubscription: hasSubscription,
apps: apps,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataText = JSON.parse(JSON.stringify(templateData));
@@ -411,6 +401,24 @@ function certificateRenewalError(mailTo, domain, message) {
});
}
function boxUpdateError(mailTo, message) {
assert.strictEqual(typeof mailTo, 'string');
assert.strictEqual(typeof message, 'string');
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
var mailOptions = {
from: mailConfig.notificationFrom,
to: mailTo,
subject: util.format('[%s] Cloudron update error', mailConfig.cloudronName),
text: render('box_update_error.ejs', { message: message, format: 'text' })
};
sendMail(mailOptions);
});
}
function oomEvent(mailTo, program, event) {
assert.strictEqual(typeof mailTo, 'string');
assert.strictEqual(typeof program, 'string');
+2 -2
View File
@@ -5,7 +5,7 @@ exports = module.exports = {
};
var assert = require('assert'),
config = require('./config.js'),
constants = require('./constants.js'),
dns = require('dns'),
_ = require('underscore');
@@ -24,7 +24,7 @@ function resolve(hostname, rrtype, options, callback) {
options = _.extend({ }, DEFAULT_OPTIONS, options);
// Only use unbound on a Cloudron
if (config.CLOUDRON) resolver.setServers([ options.server ]);
if (constants.CLOUDRON) resolver.setServers([ options.server ]);
// should callback with ECANCELLED but looks like we might hit https://github.com/nodejs/node/issues/14814
const timerId = setTimeout(resolver.cancel.bind(resolver), options.timeout || 5000);
+11 -3
View File
@@ -96,7 +96,15 @@ server {
<% if ( endpoint === 'admin' ) { -%>
# CSP headers for the admin/dashboard resources
add_header Content-Security-Policy "default-src 'none'; connect-src wss: https: 'self' *.cloudron.io; script-src https: 'self' 'unsafe-inline' 'unsafe-eval'; img-src * data:; style-src https: 'unsafe-inline'; object-src 'none'; font-src https: 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self';";
add_header Content-Security-Policy "default-src 'none'; frame-src 'self' cloudron.io *.cloudron.io; connect-src wss: https: 'self' *.cloudron.io; script-src https: 'self' 'unsafe-inline' 'unsafe-eval'; img-src * data:; style-src https: 'unsafe-inline'; object-src 'none'; font-src https: 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self';";
<% } else { %>
<% if (cspQuoted) { %>
add_header Content-Security-Policy <%- cspQuoted %>;
<% } %>
<% for (var i = 0; i < hideHeaders.length; i++) { -%>
proxy_hide_header <%- hideHeaders[i] %>;
<% } %>
<% } -%>
proxy_http_version 1.1;
@@ -163,9 +171,9 @@ server {
client_max_body_size 0;
}
# graphite paths (uncomment block below and visit /graphite/index.html)
# graphite paths (uncomment block below and visit /graphite-web/dashboard)
# remember to comment out the CSP policy as well to access the graphite dashboard
# location ~ ^/(graphite|content|metrics|dashboard|render|browser|composer)/ {
# location ~ ^/graphite-web/ {
# proxy_pass http://127.0.0.1:8417;
# client_max_body_size 1m;
# }
+14 -14
View File
@@ -13,8 +13,8 @@ exports = module.exports = {
};
let assert = require('assert'),
database = require('./database.js'),
DatabaseError = require('./databaseerror');
BoxError = require('./boxerror.js'),
database = require('./database.js');
const NOTIFICATION_FIELDS = [ 'id', 'userId', 'eventId', 'title', 'message', 'creationTime', 'acknowledged' ];
@@ -34,8 +34,8 @@ function add(notification, callback) {
const args = [ notification.userId, notification.eventId, notification.title, notification.message, notification.acknowledged ];
database.query(query, args, function (error, result) {
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'no such eventlog entry'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new BoxError(BoxError.NOT_FOUND, 'no such eventlog entry'));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, String(result.insertId));
});
@@ -47,8 +47,8 @@ function getByUserIdAndTitle(userId, title, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + NOTIFICATION_FIELDS + ' from notifications WHERE userId = ? AND title = ? ORDER BY creationTime LIMIT 1', [ userId, title ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Notification not found'));
postProcess(results[0]);
@@ -70,8 +70,8 @@ function update(id, data, callback) {
args.push(id);
database.query('UPDATE notifications SET ' + fields.join(', ') + ' WHERE id = ?', args, function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Notification not found'));
callback(null);
});
@@ -82,8 +82,8 @@ function get(id, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + NOTIFICATION_FIELDS + ' FROM notifications WHERE id = ?', [ id ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Notification not found'));
postProcess(result[0]);
@@ -96,8 +96,8 @@ function del(id, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM notifications WHERE id = ?', [ id ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Notification not found'));
callback(null);
});
@@ -118,7 +118,7 @@ function listByUserIdPaged(userId, page, perPage, callback) {
data.push(perPage);
database.query(query, data, function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(postProcess);
@@ -130,7 +130,7 @@ function clear(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM notifications', function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
+65 -48
View File
@@ -1,8 +1,6 @@
'use strict';
exports = module.exports = {
NotificationsError: NotificationsError,
get: get,
ack: ack,
getAllPaged: getAllPaged,
@@ -24,37 +22,16 @@ exports = module.exports = {
let assert = require('assert'),
async = require('async'),
config = require('./config.js'),
auditSource = require('./auditsource.js'),
BoxError = require('./boxerror.js'),
changelog = require('./changelog.js'),
custom = require('./custom.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:notifications'),
eventlog = require('./eventlog.js'),
mailer = require('./mailer.js'),
notificationdb = require('./notificationdb.js'),
users = require('./users.js'),
util = require('util');
function NotificationsError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(NotificationsError, Error);
NotificationsError.INTERNAL_ERROR = 'Internal Error';
NotificationsError.NOT_FOUND = 'Not Found';
settings = require('./settings.js'),
users = require('./users.js');
function add(userId, eventId, title, message, callback) {
assert.strictEqual(typeof userId, 'string');
@@ -72,8 +49,7 @@ function add(userId, eventId, title, message, callback) {
message: message,
acknowledged: false
}, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new NotificationsError(NotificationsError.NOT_FOUND, error.message));
if (error) return callback(new NotificationsError(NotificationsError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback(null, { id: result });
});
@@ -84,8 +60,7 @@ function get(id, callback) {
assert.strictEqual(typeof callback, 'function');
notificationdb.get(id, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new NotificationsError(NotificationsError.NOT_FOUND));
if (error) return callback(new NotificationsError(NotificationsError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback(null, result);
});
@@ -96,8 +71,7 @@ function ack(id, callback) {
assert.strictEqual(typeof callback, 'function');
notificationdb.update(id, { acknowledged: true }, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new NotificationsError(NotificationsError.NOT_FOUND));
if (error) return callback(new NotificationsError(NotificationsError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback(null);
});
@@ -112,7 +86,7 @@ function getAllPaged(userId, acknowledged, page, perPage, callback) {
assert.strictEqual(typeof callback, 'function');
notificationdb.listByUserIdPaged(userId, page, perPage, function (error, result) {
if (error) return callback(new NotificationsError(NotificationsError.INTERNAL_ERROR, error));
if (error) return callback(error);
if (acknowledged === null) return callback(null, result);
@@ -127,7 +101,7 @@ function actionForAllAdmins(skippingUserIds, iterator, callback) {
assert.strictEqual(typeof callback, 'function');
users.getAllAdmins(function (error, result) {
if (error) return callback(new NotificationsError(NotificationsError.INTERNAL_ERROR, error));
if (error) return callback(error);
// filter out users we want to skip (like the user who did the action or the user the action was performed on)
result = result.filter(function (r) { return skippingUserIds.indexOf(r.id) === -1; });
@@ -144,7 +118,7 @@ function userAdded(performedBy, eventId, user, callback) {
actionForAllAdmins([ performedBy, user.id ], function (admin, done) {
mailer.userAdded(admin.email, user);
add(admin.id, eventId, 'User added', `User ${user.fallbackEmail} was added`, done);
add(admin.id, eventId, `User '${user.displayName}' added`, `User '${user.username || user.email || user.fallbackEmail}' was added.`, done);
}, callback);
}
@@ -156,7 +130,7 @@ function userRemoved(performedBy, eventId, user, callback) {
actionForAllAdmins([ performedBy, user.id ], function (admin, done) {
mailer.userRemoved(admin.email, user);
add(admin.id, eventId, 'User removed', `User ${user.username || user.email || user.fallbackEmail} was removed`, done);
add(admin.id, eventId, `User '${user.displayName}' removed`, `User '${user.username || user.email || user.fallbackEmail}' was removed.`, done);
}, callback);
}
@@ -167,7 +141,7 @@ function adminChanged(performedBy, eventId, user, callback) {
actionForAllAdmins([ performedBy, user.id ], function (admin, done) {
mailer.adminChanged(admin.email, user, user.admin);
add(admin.id, eventId, 'Admin status change', `User ${user.username || user.email || user.fallbackEmail} ${user.admin ? 'is now an admin' : 'is no more an admin'}`, done);
add(admin.id, eventId, `User '${user.displayName} ' ${user.admin ? 'is now an admin' : 'is no more an admin'}`, `User '${user.username || user.email || user.fallbackEmail}' ${user.admin ? 'is now an admin' : 'is no more an admin'}.`, done);
}, callback);
}
@@ -236,8 +210,13 @@ function appUpdated(eventId, app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
const tmp = app.manifest.description.match(/<upstream>(.*)<\/upstream>/i);
const upstreamVersion = (tmp && tmp[1]) ? tmp[1] : '';
const title = upstreamVersion ? `${app.manifest.title} at ${app.fqdn} updated to ${upstreamVersion} (package version ${app.manifest.version})`
: `${app.manifest.title} at ${app.fqdn} updated to package version ${app.manifest.version}`;
actionForAllAdmins([], function (admin, done) {
add(admin.id, eventId, `App ${app.fqdn} updated`, `The application ${app.manifest.title} installed at https://${app.fqdn} was updated to package version ${app.manifest.version}.`, function (error) {
add(admin.id, eventId, title, `The application ${app.manifest.title} installed at https://${app.fqdn} was updated.\n\nChangelog:\n${app.manifest.changelog}\n`, function (error) {
if (error) return callback(error);
mailer.appUpdated(admin.email, app, function (error) {
@@ -248,6 +227,34 @@ function appUpdated(eventId, app, callback) {
}, callback);
}
function boxUpdated(eventId, oldVersion, newVersion, callback) {
assert.strictEqual(typeof eventId, 'string');
assert.strictEqual(typeof oldVersion, 'string');
assert.strictEqual(typeof newVersion, 'string');
assert.strictEqual(typeof callback, 'function');
const changes = changelog.getChanges(newVersion);
const changelogMarkdown = changes.map((m) => `* ${m}\n`).join('');
actionForAllAdmins([], function (admin, done) {
add(admin.id, eventId, `Cloudron updated to v${newVersion}`, `Cloudron was updated from v${oldVersion} to v${newVersion}.\n\nChangelog:\n${changelogMarkdown}\n`, done);
}, callback);
}
function boxUpdateError(eventId, errorMessage, callback) {
assert.strictEqual(typeof eventId, 'string');
assert.strictEqual(typeof errorMessage, 'string');
assert.strictEqual(typeof callback, 'function');
if (custom.spec().alerts.email) mailer.boxUpdateError(custom.spec().alerts.email, errorMessage);
if (!custom.spec().alerts.notifyCloudronAdmins) return callback();
actionForAllAdmins([], function (admin, done) {
mailer.boxUpdateError(admin.email, errorMessage);
add(admin.id, eventId, 'Cloudron update failed', `Failed to update Cloudron: ${errorMessage}. Update will be retried in 4 hours`, done);
}, callback);
}
function certificateRenewalError(eventId, vhost, errorMessage, callback) {
assert.strictEqual(typeof eventId, 'string');
assert.strictEqual(typeof vhost, 'string');
@@ -269,12 +276,12 @@ function backupFailed(eventId, taskId, errorMessage, callback) {
assert.strictEqual(typeof errorMessage, 'string');
assert.strictEqual(typeof callback, 'function');
if (custom.spec().alerts.email) mailer.backupFailed(custom.spec().alerts.email, errorMessage, `${config.adminOrigin()}/logs.html?taskId=${taskId}`);
if (custom.spec().alerts.email) mailer.backupFailed(custom.spec().alerts.email, errorMessage, `${settings.adminOrigin()}/logs.html?taskId=${taskId}`);
if (!custom.spec().alerts.notifyCloudronAdmins) return callback();
actionForAllAdmins([], function (admin, callback) {
mailer.backupFailed(admin.email, errorMessage, `${config.adminOrigin()}/logs.html?taskId=${taskId}`);
add(admin.id, eventId, 'Failed to backup', `Backup failed: ${errorMessage}. Logs are available [here](/logs.html?taskId=${taskId}). Will be retried in 4 hours`, callback);
mailer.backupFailed(admin.email, errorMessage, `${settings.adminOrigin()}/logs.html?taskId=${taskId}`);
add(admin.id, eventId, 'Backup failed', `Backup failed: ${errorMessage}. Logs are available [here](/logs.html?taskId=${taskId}). Will be retried in 4 hours`, callback);
}, callback);
}
@@ -299,15 +306,14 @@ function alert(id, title, message, callback) {
};
notificationdb.getByUserIdAndTitle(admin.id, title, function (error, result) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new NotificationsError(NotificationsError.INTERNAL_ERROR, error));
if (error && error.reason !== BoxError.NOT_FOUND) return callback(error);
if (!result && acknowledged) return callback(); // do not add acked alerts
let updateFunc = !result ? notificationdb.add.bind(null, data) : notificationdb.update.bind(null, result.id, data);
updateFunc(function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new NotificationsError(NotificationsError.NOT_FOUND, error.message));
if (error) return callback(new NotificationsError(NotificationsError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback(null);
});
@@ -326,6 +332,9 @@ function onEvent(id, action, source, data, callback) {
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
// external ldap syncer does not generate notifications - FIXME username might be an issue here
if (source.username === auditSource.EXTERNAL_LDAP_TASK.username) return callback();
switch (action) {
case eventlog.ACTION_USER_ADD:
return userAdded(source.userId, id, data.user, callback);
@@ -347,6 +356,7 @@ function onEvent(id, action, source, data, callback) {
return appUp(id, data.app, callback);
case eventlog.ACTION_APP_UPDATE_FINISH:
if (!data.app.appStoreId) return callback(); // skip notification of dev apps
return appUpdated(id, data.app, callback);
case eventlog.ACTION_CERTIFICATE_RENEWAL:
@@ -355,8 +365,15 @@ function onEvent(id, action, source, data, callback) {
return certificateRenewalError(id, data.domain, data.errorMessage, callback);
case eventlog.ACTION_BACKUP_FINISH:
if (!data.errorMessage || source.username !== 'cron') return callback();
return backupFailed(id, data.taskId, data.errorMessage, callback); // only notify for automated backups
if (!data.errorMessage) return callback();
if (source.username !== auditSource.CRON.username && !data.timedOut) return callback(); // manual stop by user
return backupFailed(id, data.taskId, data.errorMessage, callback); // only notify for automated backups or timedout
case eventlog.ACTION_UPDATE_FINISH:
if (!data.errorMessage) return boxUpdated(id, data.oldVersion, data.newVersion, callback);
if (data.timedOut) return boxUpdateError(id, data.errorMessage, callback);
return callback();
default:
return callback();
+1 -1
View File
@@ -61,7 +61,7 @@ app.controller('Controller', ['$scope', function ($scope) {
<div class="control-label" ng-show="setupForm.password.$dirty && setupForm.password.$invalid">
<small ng-show="setupForm.password.$dirty && setupForm.password.$invalid">Password must be atleast 8 characters</small>
</div>
<input type="password" class="form-control" ng-model="password" name="password" ng-pattern="/^.{8,30}$/" required>
<input type="password" class="form-control" ng-model="password" name="password" ng-pattern="/^.{8,}$/" required>
</div>
<div class="form-group" ng-class="{ 'has-error': (setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)) }">

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