Compare commits

..

190 Commits

Author SHA1 Message Date
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
122 changed files with 3141 additions and 3267 deletions
+56
View File
@@ -1674,3 +1674,59 @@
* 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
@@ -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) {
@@ -1,8 +1,7 @@
'use strict';
var async = require('async'),
fs = require('fs'),
superagent = require('superagent');
fs = require('fs');
exports.up = function(db, callback) {
if (!fs.existsSync('/etc/cloudron/cloudron.conf')) {
@@ -13,6 +12,7 @@ exports.up = function(db, 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 ]),
@@ -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);
};
+4 -2
View File
@@ -63,7 +63,7 @@ CREATE TABLE IF NOT EXISTS clients(
CREATE TABLE IF NOT EXISTS apps(
id VARCHAR(128) NOT NULL UNIQUE,
appStoreId VARCHAR(128) NOT NULL,
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),
@@ -81,16 +81,18 @@ 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,
FOREIGN KEY(mailboxDomain) REFERENCES domains(domain),
FOREIGN KEY(taskId) REFERENCES tasks(id),
PRIMARY KEY(id));
+21 -18
View File
@@ -814,32 +814,35 @@
}
},
"cloudron-manifestformat": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-2.15.0.tgz",
"integrity": "sha512-hSL+n/ttjrjZby/tSa5YSTRUAcxfzAi9CFUSPyu3dx8OMxzHsDyTvtKHjwBtIZ0Fjz7B3THfR3kfvIgP0lULSg==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-4.0.0.tgz",
"integrity": "sha512-St/Quu8ofQOf0rUAMaIsOL0u0dZ46irweU8rYVMvAXU0CGwSD9KDaeLW5NjGRg3FVjNzladUDVUE/BGD4rwEvA==",
"requires": {
"cron": "^1.0.9",
"cron": "^1.7.2",
"java-packagename-regex": "^1.0.0",
"safetydance": "0.0.15",
"semver": "^4.3.1",
"tv4": "^1.1.9",
"validator": "^3.34.0"
"safetydance": "0.7.1",
"semver": "^6.3.0",
"tv4": "^1.3.0",
"validator": "^12.0.0"
},
"dependencies": {
"safetydance": {
"version": "0.0.15",
"resolved": "https://registry.npmjs.org/safetydance/-/safetydance-0.0.15.tgz",
"integrity": "sha1-VlUnZ18F92XxxoglYjRWpcVSEMQ="
"cron": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/cron/-/cron-1.7.2.tgz",
"integrity": "sha512-+SaJ2OfeRvfQqwXQ2kgr0Y5pzBR/lijf5OpnnaruwWnmI799JfWr2jN2ItOV9s3A/+TFOt6mxvKzQq5F0Jp6VQ==",
"requires": {
"moment-timezone": "^0.5.x"
}
},
"semver": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz",
"integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto="
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
},
"validator": {
"version": "3.43.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-3.43.0.tgz",
"integrity": "sha1-lkZLmS1BloM9l6GUv0Cxn/VLrgU="
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-12.0.0.tgz",
"integrity": "sha512-r5zA1cQBEOgYlesRmSEwc9LkbfNLTtji+vWyaHzRZUxCTHdsX3bd+sdHfs5tGZ2W6ILGGsxWxCNwT/h3IY/3ng=="
}
}
},
+1 -1
View File
@@ -20,7 +20,7 @@
"async": "^2.6.2",
"aws-sdk": "^2.476.0",
"body-parser": "^1.19.0",
"cloudron-manifestformat": "^2.15.0",
"cloudron-manifestformat": "^4.0.0",
"connect": "^3.7.0",
"connect-ensure-login": "^0.1.1",
"connect-lastmile": "^1.2.1",
+1 -1
View File
@@ -83,7 +83,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
+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
+8 -13
View File
@@ -27,11 +27,10 @@ exports = module.exports = {
};
var assert = require('assert'),
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
@@ -78,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;
}
@@ -101,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] + '"');
}
}
@@ -122,11 +120,11 @@ 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
@@ -134,11 +132,8 @@ function validateToken(accessToken, callback) {
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
});
});
});
+50 -68
View File
@@ -1,8 +1,6 @@
'use strict';
exports = module.exports = {
AddonsError: AddonsError,
getServices: getServices,
getService: getService,
configureService: configureService,
@@ -39,9 +37,7 @@ var accesscontrol = require('./accesscontrol.js'),
BoxError = require('./boxerror.js'),
clients = require('./clients.js'),
constants = require('./constants.js'),
ClientsError = clients.ClientsError,
crypto = require('crypto'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:addons'),
docker = require('./docker.js'),
dockerConnection = docker.connection,
@@ -65,31 +61,6 @@ var accesscontrol = require('./accesscontrol.js'),
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');
@@ -268,14 +239,14 @@ function restartContainer(serviceName, callback) {
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 && 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); });
}
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback(null);
});
@@ -306,19 +277,18 @@ function getServiceDetails(containerName, tokenEnvName, callback) {
assert.strictEqual(typeof callback, 'function');
docker.inspect(containerName, function (error, result) {
if (error && error.reason === BoxError.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 ${containerName} container ip`));
// 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 getting ${containerName} env`));
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 ${containerName} cloudron token env var`));
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 ${containerName} cloudron token`));
callback(null, { ip: ip, token: token, state: result.State });
});
@@ -330,7 +300,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) {
@@ -338,7 +308,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,
@@ -364,7 +334,7 @@ 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,
@@ -377,7 +347,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;
@@ -405,10 +375,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] = {};
@@ -421,7 +391,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);
});
@@ -437,7 +407,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}`);
@@ -496,7 +466,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);
}
@@ -816,7 +786,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);
@@ -844,7 +814,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);
});
@@ -933,7 +903,7 @@ 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);
if (error && error.reason !== BoxError.NOT_FOUND) return callback(error);
var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length
@@ -943,10 +913,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);
@@ -971,7 +941,7 @@ 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);
if (error && error.reason !== BoxError.NOT_FOUND) return callback(error);
var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length
@@ -980,10 +950,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);
@@ -1067,7 +1037,7 @@ 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);
if (error && error.reason !== BoxError.NOT_FOUND) return callback(error);
const tmp = mysqlDatabaseName(app.id);
@@ -1283,7 +1253,7 @@ 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);
if (error && error.reason !== BoxError.NOT_FOUND) return callback(error);
const data = {
database: database,
@@ -1458,7 +1428,7 @@ 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);
if (error && error.reason !== BoxError.NOT_FOUND) return callback(error);
const data = {
database: app.id,
@@ -1582,9 +1552,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
@@ -1596,9 +1578,9 @@ function setupRedis(app, options, callback) {
const redisName = 'redis-' + app.id;
appdb.getAddonConfigByName(app.id, 'redis', '%REDIS_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
if (error && error.reason !== BoxError.NOT_FOUND) return callback(error);
const redisPassword = error ? hat(4 * 48) : existingPassword; // see box#362 for password length
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)
@@ -1645,7 +1627,7 @@ function setupRedis(app, options, callback) {
async.series([
(next) => {
docker.inspect(redisName, function (inspectError, result) {
docker.inspect(redisName, function (inspectError, result) { // fast-path
if (!inspectError) {
debug(`Re-using existing redis container with state: ${JSON.stringify(result.State)}`);
return next();
@@ -1806,10 +1788,10 @@ function statusSftp(callback) {
docker.inspect('sftp', function (error, container) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, { status: exports.SERVICE_STATUS_STOPPED });
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
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,
@@ -1827,14 +1809,14 @@ function statusGraphite(callback) {
docker.inspect('graphite', function (error, container) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, { status: exports.SERVICE_STATUS_STOPPED });
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
if (error) return callback(error);
request.get('http://127.0.0.1:8417/graphite-web/dashboard', { 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,
+48 -43
View File
@@ -33,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.errorJson', 'apps.runState',
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'subdomains.subdomain AS location', 'subdomains.domain',
'apps.accessRestrictionJson', 'apps.memoryLimit',
'apps.label', 'apps.tagsJson', 'apps.taskId',
'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
'apps.creationTime', 'apps.updateTime', 'apps.mailboxName', 'apps.enableAutomaticUpdate',
'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(',');
@@ -61,6 +61,10 @@ function postProcess(result) {
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');
@@ -122,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;
@@ -149,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]);
@@ -175,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]);
@@ -200,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; });
@@ -241,21 +245,22 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal
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, runState, accessRestrictionJson, memoryLimit, '
+ 'sso, debugModeJson, robotsTxt, mailboxName, label, tagsJson) '
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
+ 'sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson) '
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit,
sso, debugModeJson, robotsTxt, mailboxName, label, tagsJson ]
sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson ]
});
queries.push({
@@ -287,9 +292,9 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal
}
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);
});
@@ -300,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);
});
@@ -311,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++) {
@@ -328,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);
});
@@ -347,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);
});
@@ -364,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);
});
}
@@ -420,7 +425,7 @@ function updateWithConstraints(id, app, constraints, callback) {
var fields = [ ], values = [ ];
for (var p in app) {
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error') {
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') {
@@ -435,9 +440,9 @@ 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);
});
@@ -473,7 +478,7 @@ 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);
});
@@ -498,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);
});
@@ -511,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);
});
@@ -522,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);
});
@@ -534,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);
});
@@ -545,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);
});
@@ -558,8 +563,8 @@ 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);
});
@@ -572,8 +577,8 @@ function getAddonConfigByName(appId, addonId, namePattern, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT value FROM appAddonConfigs WHERE appId = ? AND addonId = ? AND name LIKE ?', [ appId, addonId, namePattern ], 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].value);
});
+4 -4
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) {
@@ -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;
@@ -188,7 +188,7 @@ function processApp(callback) {
var alive = result
.filter(function (a) { return a.installationState === apps.ISTATE_INSTALLED && a.runState === apps.RSTATE_RUNNING && a.health === apps.HEALTH_HEALTHY; })
.map(function (a) { return (a.location || 'naked_domain') + '|' + a.manifest.id; }).join(', ');
.map(function (a) { return (a.location || 'naked_domain') + '|' + (a.manifest.id || 'customapp'); }).join(', ');
debug('apps alive: [%s]', alive);
+293 -292
View File
File diff suppressed because it is too large Load Diff
+82 -112
View File
@@ -19,14 +19,13 @@ exports = module.exports = {
getAppUpdate: getAppUpdate,
getBoxUpdate: getBoxUpdate,
createTicket: createTicket,
AppstoreError: AppstoreError
createTicket: createTicket
};
var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
custom = require('./custom.js'),
debug = require('debug')('box:appstore'),
@@ -39,48 +38,17 @@ var apps = require('./apps.js'),
semver = require('semver'),
settings = require('./settings.js'),
superagent = require('superagent'),
sysinfo = require('./sysinfo.js'),
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);
});
@@ -100,9 +68,9 @@ function login(email, password, totpToken, callback) {
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 }
});
@@ -120,9 +88,9 @@ function registerUser(email, password, callback) {
const url = settings.apiServerOrigin() + '/api/v1/register_user';
superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new 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);
});
@@ -136,11 +104,11 @@ function getSubscription(callback) {
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 }
});
@@ -164,13 +132,13 @@ function purchaseApp(data, callback) {
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);
});
@@ -189,16 +157,16 @@ function unpurchaseApp(appId, data, callback) {
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);
});
@@ -214,42 +182,42 @@ function sendAliveStatus(callback) {
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(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
if (error) return callback(error);
userCount = result;
callback();
});
},
function (callback) {
groups.count(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
if (error) return callback(error);
groupCount = result;
callback();
});
@@ -277,12 +245,13 @@ function sendAliveStatus(callback) {
appAutoupdatePattern: allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY],
boxAutoupdatePattern: allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY],
timeZone: allSettings[settings.TIME_ZONE_KEY],
sysinfoProvider: allSettings[settings.SYSINFO_CONFIG_KEY].provider
};
var data = {
version: constants.VERSION,
adminFqdn: settings.adminFqdn(),
provider: sysinfo.provider(),
provider: settings.provider(),
backendSettings: backendSettings,
machine: {
cpus: os.cpus(),
@@ -298,11 +267,11 @@ function sendAliveStatus(callback) {
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);
});
@@ -319,25 +288,25 @@ function getBoxUpdate(callback) {
const url = `${settings.apiServerOrigin()}/api/v1/boxupdate`;
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION }).timeout(10 * 1000).end(function (error, result) {
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 (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(constants.VERSION, updateInfo.version)) {
return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Invalid update version: %s %s', result.statusCode, result.text)));
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);
});
@@ -354,11 +323,11 @@ function getAppUpdate(app, callback) {
const url = `${settings.apiServerOrigin()}/api/v1/appupdate`;
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 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));
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;
@@ -368,7 +337,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 }
@@ -384,20 +353,20 @@ function registerCloudron(data, callback) {
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}`);
@@ -412,7 +381,7 @@ function registerWithLicense(license, domain, callback) {
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);
});
@@ -429,7 +398,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);
@@ -469,10 +438,10 @@ function createTicket(info, callback) {
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)));
callback(null);
});
@@ -487,14 +456,15 @@ function getApps(callback) {
if (error) return callback(error);
settings.getUnstableAppsConfig(function (error, unstable) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
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 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 && !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);
});
@@ -514,11 +484,11 @@ function getAppVersion(appId, version, callback) {
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);
});
+38 -35
View File
@@ -24,16 +24,14 @@ var addons = require('./addons.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
auditsource = require('./auditsource.js'),
auditSource = require('./auditsource.js'),
backups = require('./backups.js'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:apptask'),
df = require('@sindresorhus/df'),
docker = require('./docker.js'),
domains = require('./domains.js'),
DomainsError = domains.DomainsError,
ejs = require('ejs'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
@@ -82,7 +80,7 @@ function updateApp(app, values, callback) {
debugApp(app, 'updating app with values: %j', values);
appdb.update(app.id, values, function (error) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (error) return callback(error);
for (var value in values) {
app[value] = values[value];
@@ -115,7 +113,7 @@ function configureReverseProxy(app, callback) {
assert.strictEqual(typeof callback, 'function');
reverseProxy.configureApp(app, { userId: null, username: 'apptask' }, function (error) {
if (error) return callback(new BoxError(BoxError.REVERSEPROXY_ERROR, `Error configuring nginx: ${error.message}`));
if (error) return callback(error);
callback(null);
});
@@ -126,7 +124,7 @@ function unconfigureReverseProxy(app, callback) {
assert.strictEqual(typeof callback, 'function');
reverseProxy.unconfigureApp(app, function (error) {
if (error) return callback(new BoxError(BoxError.REVERSEPROXY_ERROR, `Error unconfiguring nginx: ${error.message}`));
if (error) return callback(error);
callback(null);
});
@@ -140,7 +138,7 @@ function createContainer(app, callback) {
debugApp(app, 'creating container');
docker.createContainer(app, function (error, container) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, `Error creating container: ${error.message}`));
if (error) return callback(error);
updateApp(app, { containerId: container.id }, callback);
});
@@ -154,7 +152,7 @@ function deleteContainers(app, options, callback) {
debugApp(app, 'deleting app containers (app, scheduler)');
docker.deleteContainers(app.id, options, function (error) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, `Error deleting container: ${error.message}`));
if (error) return callback(error);
updateApp(app, { containerId: null }, callback);
});
@@ -248,7 +246,7 @@ function addLogrotateConfig(app, callback) {
assert.strictEqual(typeof callback, 'function');
docker.inspect(app.containerId, function (error, result) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, `Error inspecting app container: ${error.message}`, { containerId: app.containerId }));
if (error) return callback(error);
var runVolume = result.Mounts.find(function (mount) { return mount.Destination === '/run'; });
if (!runVolume) return callback(new BoxError(BoxError.DOCKER_ERROR, 'App does not have /run mounted'));
@@ -352,7 +350,7 @@ function registerSubdomains(app, overwrite, callback) {
assert.strictEqual(typeof overwrite, 'boolean');
assert.strictEqual(typeof callback, 'function');
sysinfo.getPublicIp(function (error, ip) {
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(error);
const allDomains = [ { subdomain: app.location, domain: app.domain }].concat(app.alternateDomains);
@@ -365,9 +363,9 @@ function registerSubdomains(app, overwrite, callback) {
// get the current record before updating it
domains.getDnsRecords(domain.subdomain, domain.domain, 'A', function (error, values) {
if (error && error.reason === DomainsError.EXTERNAL_ERROR) return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain })); // try again
if (error && error.reason === DomainsError.ACCESS_DENIED) return retryCallback(null, new BoxError(BoxError.ACCESS_DENIED, error.message, { domain }));
if (error && error.reason === DomainsError.NOT_FOUND) return retryCallback(null, new BoxError(BoxError.NOT_FOUND, error.message, { domain }));
if (error && error.reason === BoxError.EXTERNAL_ERROR) return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain })); // try again
if (error && error.reason === BoxError.ACCESS_DENIED) return retryCallback(null, new BoxError(BoxError.ACCESS_DENIED, error.message, { domain }));
if (error && error.reason === BoxError.NOT_FOUND) return retryCallback(null, new BoxError(BoxError.NOT_FOUND, error.message, { domain }));
if (error) return retryCallback(null, new BoxError(BoxError.EXTERNAL_ERROR, error.message, domain)); // give up for other errors
if (values.length !== 0 && values[0] === ip) return retryCallback(null); // up-to-date
@@ -376,7 +374,7 @@ function registerSubdomains(app, overwrite, callback) {
if (values.length !== 0 && !overwrite) return retryCallback(null, new BoxError(BoxError.ALREADY_EXISTS, 'DNS Record already exists', { domain }));
domains.upsertDnsRecords(domain.subdomain, domain.domain, 'A', [ ip ], function (error) {
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) {
if (error && (error.reason === BoxError.BUSY || error.reason === BoxError.EXTERNAL_ERROR)) {
debug('registerSubdomains: Upsert error. Will retry.', error.message);
return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain })); // try again
}
@@ -398,7 +396,7 @@ function unregisterSubdomains(app, allDomains, callback) {
assert(Array.isArray(allDomains));
assert.strictEqual(typeof callback, 'function');
sysinfo.getPublicIp(function (error, ip) {
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(error);
async.eachSeries(allDomains, function (domain, iteratorDone) {
@@ -406,8 +404,8 @@ function unregisterSubdomains(app, allDomains, callback) {
debugApp(app, 'Unregistering subdomain: %s%s', domain.subdomain ? (domain.subdomain + '.') : '', domain.domain);
domains.removeDnsRecords(domain.subdomain, domain.domain, 'A', [ ip ], function (error) {
if (error && error.reason === DomainsError.NOT_FOUND) return retryCallback(null, null);
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) {
if (error && error.reason === BoxError.NOT_FOUND) return retryCallback(null, null);
if (error && (error.reason === BoxError.SBUSY || error.reason === BoxError.EXTERNAL_ERROR)) {
debug('registerSubdomains: Remove error. Will retry.', error.message);
return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain })); // try again
}
@@ -432,7 +430,7 @@ function waitForDnsPropagation(app, callback) {
return callback(null);
}
sysinfo.getPublicIp(function (error, ip) {
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Error getting public IP: ${error.message}`));
domains.waitForDnsRecord(app.location, app.domain, 'A', ip, { interval: 5000, times: 240 }, function (error) {
@@ -472,7 +470,7 @@ function downloadImage(manifest, callback) {
assert.strictEqual(typeof callback, 'function');
docker.info(function (error, info) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, `Error getting docker info: ${error.message}`));
if (error) return callback(error);
const dfAsync = util.callbackify(df.file);
dfAsync(info.DockerRootDir, function (error, diskUsage) {
@@ -480,7 +478,7 @@ function downloadImage(manifest, callback) {
if (diskUsage.available < (1024*1024*1024)) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Not enough disk space to pull docker image', { diskUsage: diskUsage, dockerRootDir: info.DockerRootDir }));
docker.downloadImage(manifest, function (error) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, `Error downloading image: ${error.message}`, { image: manifest.dockerImage }));
if (error) return callback(error);
callback(null);
});
@@ -511,8 +509,7 @@ function install(app, args, progressCallback, callback) {
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
const isInstalling = app.installationState !== apps.ISTATE_PENDING_RESTORE; // install or clone or repair
const restoreConfig = args.restoreConfig || {};
const restoreConfig = args.restoreConfig; // has to be set when restoring
const overwriteDns = args.overwriteDns;
async.series([
@@ -529,15 +526,19 @@ function install(app, args, progressCallback, callback) {
deleteContainers.bind(null, app, { managedOnly: true }),
function teardownAddons(next) {
// when restoring, app does not require these addons anymore. remove carefully to preserve the db passwords
var addonsToRemove = isInstalling ? app.manifest.addons : _.omit(restoreConfig.oldManifest.addons, Object.keys(app.manifest.addons));
let addonsToRemove;
if (restoreConfig && restoreConfig.oldManifest) { // oldManifest is null for clone
addonsToRemove = _.omit(restoreConfig.oldManifest.addons, Object.keys(app.manifest.addons));
} else {
addonsToRemove = app.manifest.addons;
}
addons.teardownAddons(app, addonsToRemove, next);
},
deleteAppDir.bind(null, app, { removeDirectory: false }), // do not remove any symlinked appdata dir
// for restore case
function deleteImageIfChanged(done) {
if (isInstalling) return done();
if (!restoreConfig || !restoreConfig.oldManifest) return done();
if (restoreConfig.oldManifest.dockerImage === app.manifest.dockerImage) return done();
@@ -559,7 +560,7 @@ function install(app, args, progressCallback, callback) {
createAppDir.bind(null, app),
function restoreFromBackup(next) {
if (!restoreConfig.backupId) {
if (!restoreConfig) {
async.series([
progressCallback.bind(null, { percent: 60, message: 'Setting up addons' }),
addons.setupAddons.bind(null, app, app.manifest.addons),
@@ -667,6 +668,7 @@ function changeLocation(app, args, progressCallback, callback) {
const oldConfig = args.oldConfig;
const locationChanged = oldConfig.fqdn !== app.fqdn;
const overwriteDns = args.overwriteDns;
async.series([
progressCallback.bind(null, { percent: 10, message: 'Cleaning up old install' }),
@@ -686,7 +688,7 @@ function changeLocation(app, args, progressCallback, callback) {
},
progressCallback.bind(null, { percent: 30, message: 'Registering subdomains' }),
registerSubdomains.bind(null, app, args.overwriteDns),
registerSubdomains.bind(null, app, overwriteDns),
// re-setup addons since they rely on the app's fqdn (e.g oauth)
progressCallback.bind(null, { percent: 50, message: 'Setting up addons' }),
@@ -767,6 +769,9 @@ function configure(app, args, progressCallback, callback) {
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
const oldConfig = args.oldConfig || null;
const overwriteDns = args.overwriteDns;
async.series([
progressCallback.bind(null, { percent: 10, message: 'Cleaning up old install' }),
unconfigureReverseProxy.bind(null, app),
@@ -775,13 +780,13 @@ function configure(app, args, progressCallback, callback) {
docker.stopContainers.bind(null, app.id),
deleteContainers.bind(null, app, { managedOnly: true }),
function (next) {
if (!args.oldConfig) return next();
if (!oldConfig) return next();
let obsoleteDomains = args.oldConfig.alternateDomains.filter(function (o) {
let obsoleteDomains = oldConfig.alternateDomains.filter(function (o) {
return !app.alternateDomains.some(function (n) { return n.subdomain === o.subdomain && n.domain === o.domain; });
});
if (args.oldConfig.fqdn !== app.fqdn) obsoleteDomains.push({ subdomain: args.oldConfig.location, domain: args.oldConfig.domain });
if (oldConfig.fqdn !== app.fqdn) obsoleteDomains.push({ subdomain: oldConfig.location, domain: oldConfig.domain });
if (obsoleteDomains.length === 0) return next();
@@ -794,7 +799,7 @@ function configure(app, args, progressCallback, callback) {
downloadIcon.bind(null, app),
progressCallback.bind(null, { percent: 30, message: 'Registering subdomains' }),
registerSubdomains.bind(null, app, false /* overwrite */), // if location changed, do not overwrite to detect conflicts
registerSubdomains.bind(null, app, overwriteDns),
progressCallback.bind(null, { percent: 40, message: 'Downloading image' }),
downloadImage.bind(null, app.manifest),
@@ -898,7 +903,7 @@ function update(app, args, progressCallback, callback) {
if (newTcpPorts[portName] || newUdpPorts[portName]) return callback(null); // port still in use
appdb.delPortBinding(currentPorts[portName], apps.PORT_TYPE_TCP, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) console.error('Portbinding does not exist in database.');
if (error && error.reason === BoxError.NOT_FOUND) console.error('Portbinding does not exist in database.');
else if (error) return next(error);
// also delete from app object for further processing (the db is updated in the next step)
@@ -932,9 +937,7 @@ function update(app, args, progressCallback, callback) {
debugApp(app, 'Error updating app: %s', error);
updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
} else {
if (updateConfig.skipNotification) return callback(null);
eventlog.add(eventlog.ACTION_APP_UPDATE_FINISH, auditsource.APP_TASK, { app: app, success: true }, callback);
eventlog.add(eventlog.ACTION_APP_UPDATE_FINISH, auditSource.APP_TASK, { app: app, success: true }, () => callback()); // ignore error
}
});
}
+1 -1
View File
@@ -67,7 +67,7 @@ function scheduleTask(appId, taskId, callback) {
if (!fs.existsSync(path.dirname(logFile))) safe.fs.mkdirSync(path.dirname(logFile)); // ensure directory
tasks.startTask(taskId, { logFile }, function (error, result) {
tasks.startTask(taskId, { logFile, timeout: 20 * 60 * 60 * 1000 /* 20 hours */ }, function (error, result) {
callback(error, result);
delete gActiveTasks[appId];
+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);
});
}
+56 -85
View File
@@ -1,8 +1,6 @@
'use strict';
exports = module.exports = {
BackupsError: BackupsError,
testConfig: testConfig,
getByStatePaged: getByStatePaged,
@@ -41,14 +39,13 @@ exports = module.exports = {
var addons = require('./addons.js'),
apps = require('./apps.js'),
AppsError = require('./apps.js').AppsError,
async = require('async'),
assert = require('assert'),
backupdb = require('./backupdb.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'),
@@ -78,31 +75,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) {
@@ -136,12 +108,12 @@ 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);
}
@@ -153,7 +125,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);
});
@@ -166,7 +138,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);
});
@@ -177,8 +149,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);
});
@@ -246,14 +217,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.EXTERNAL_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.EXTERNAL_ERROR, error.message));
});
return stream.pipe(encrypt).pipe(ps);
} else {
@@ -306,19 +277,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 {
@@ -379,7 +350,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();
});
@@ -425,7 +396,7 @@ function checkFreeDiskSpace(backupConfig, dataLayout, callback) {
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 BackupsError(BackupsError.INTERNAL_ERROR, safe.error));
if (!result) return callback(new BoxError(BoxError.FS_ERROR, safe.error));
used += parseInt(result, 10);
}
@@ -433,11 +404,11 @@ function checkFreeDiskSpace(backupConfig, dataLayout, callback) {
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 BackupsError(BackupsError.EXTERNAL_ERROR, `Not enough disk space for backup. Needed: ${prettyBytes(needed)} Available: ${prettyBytes(diskUsage.available)}`));
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 BackupsError(BackupsError.INTERNAL_ERROR, error));
callback(new BoxError(BoxError.FS_ERROR, error));
});
}
@@ -454,7 +425,7 @@ 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);
checkFreeDiskSpace(backupConfig, dataLayout, function (error) {
if (error) return callback(error);
@@ -471,7 +442,7 @@ function upload(backupId, format, dataLayoutString, progressCallback, callback)
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 BackupsError
tarStream.on('error', retryCallback); // already returns BoxError
api(backupConfig.provider).upload(backupConfig, getBackupFilePath(backupConfig, backupId, format), tarStream, retryCallback);
});
@@ -505,17 +476,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 () {
@@ -528,7 +499,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 {
@@ -546,19 +517,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();
});
@@ -578,12 +549,12 @@ function downloadDir(backupConfig, backupFilePath, dataLayout, progressCallback,
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 callback(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 callback(new BoxError(BoxError.FS_ERROR, error.message));
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
let destStream = createWriteStream(destFilePath, backupConfig.key || null);
@@ -640,7 +611,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);
@@ -671,7 +642,7 @@ 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');
@@ -694,7 +665,7 @@ function restoreApp(app, addonsToRestore, restoreConfig, progressCallback, callb
var startTime = new Date();
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
if (error) return callback(error);
async.series([
download.bind(null, backupConfig, restoreConfig.backupId, restoreConfig.backupFormat, dataLayout, progressCallback),
@@ -718,9 +689,9 @@ function runBackupUpload(backupId, format, dataLayout, progressCallback, callbac
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();
@@ -748,7 +719,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();
}
@@ -760,7 +733,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();
});
@@ -798,7 +771,6 @@ 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, constants.VERSION);
@@ -807,7 +779,7 @@ function rotateBoxBackup(backupConfig, tag, appBackupIds, progressCallback, call
debug(`Rotating box backup to id ${backupId}`);
backupdb.add(backupId, { version: constants.VERSION, type: backupdb.BACKUP_TYPE_BOX, dependsOn: appBackupIds, manifest: null, 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/box', format), getBackupFilePath(backupConfig, backupId, format));
copy.on('progress', (message) => progressCallback({ message }));
@@ -816,7 +788,7 @@ function rotateBoxBackup(backupConfig, tag, appBackupIds, progressCallback, call
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}`);
@@ -833,7 +805,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);
@@ -860,11 +832,11 @@ 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(app))) {
return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error creating config.json: ' + safe.error.message));
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);
});
@@ -879,7 +851,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
@@ -889,7 +860,7 @@ 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 }));
@@ -898,7 +869,7 @@ function rotateAppBackup(backupConfig, app, tag, options, progressCallback, call
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}`);
@@ -947,7 +918,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);
@@ -978,7 +949,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);
@@ -993,7 +964,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);
}
@@ -1017,19 +988,20 @@ 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(new BackupsError(BackupsError.INTERNAL_ERROR, error));
if (error) return callback(error);
eventlog.add(eventlog.ACTION_BACKUP_START, auditSource, { taskId });
tasks.startTask(taskId, {}, function (error, result) {
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: taskId, errorMessage: errorMessage, backupId: result });
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { taskId, errorMessage, timedOut, backupId });
});
callback(null, taskId);
@@ -1105,7 +1077,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();
@@ -1193,7 +1165,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`));
@@ -1259,12 +1231,11 @@ function cleanup(auditSource, progressCallback, callback) {
function startCleanupTask(auditSource, callback) {
tasks.add(tasks.TASK_CLEAN_BACKUPS, [ auditSource ], function (error, taskId) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_BACKUP_CLEANUP_START, auditSource, { 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 : []
+43 -2
View File
@@ -3,6 +3,7 @@
'use strict';
const assert = require('assert'),
HttpError = require('connect-lastmile').HttpError,
util = require('util'),
_ = require('underscore');
@@ -34,21 +35,61 @@ util.inherits(BoxError, Error);
BoxError.ACCESS_DENIED = 'Access Denied';
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';
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.REVERSEPROXY_ERROR = 'ReverseProxy Error';
BoxError.NOT_IMPLEMENTED = 'Not implemented';
BoxError.NOT_SIGNED = 'Not Signed';
BoxError.OPENSSL_ERROR = 'OpenSSL Error';
BoxError.PLAN_LIMIT = 'Plan Limit';
BoxError.TASK_ERROR = 'Task Error';
BoxError.TRY_AGAIN = 'Try Again';
BoxError.UNKNOWN_ERROR = 'Unknown Error'; // only used for porting
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:
return new HttpError(424, error);
case BoxError.DATABASE_ERROR:
case BoxError.INTERNAL_ERROR:
default:
return new HttpError(500, error);
}
};
+68 -79
View File
@@ -2,6 +2,7 @@
var assert = require('assert'),
async = require('async'),
BoxError = require('../boxerror.js'),
crypto = require('crypto'),
debug = require('debug')('box:cert/acme2'),
domains = require('../domains.js'),
@@ -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
@@ -158,8 +134,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(new BoxError(BoxError.NETWORK_ERROR, `Network error when updating contact: ${error.message}`));
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 +154,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(new BoxError(BoxError.NETWORK_ERROR, `Network error when registering user: ${error.message}`));
// 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 +180,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(new BoxError(BoxError.NETWORK_ERROR, `Network error when creating new order: ${error.message}`));
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);
});
@@ -232,18 +208,18 @@ Acme2.prototype.waitForOrder = function (orderUrl, callback) {
superagent.get(orderUrl).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) {
debug('waitForOrder: network error getting uri %s', orderUrl);
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message)); // network error
return retryCallback(new BoxError(BoxError.NETWORK_ERROR, `Network error waiting for order: ${error.message}`)); // network 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 +253,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(new BoxError(BoxError.NETWORK_ERROR, `Network error when notifying challenge: ${error.message}`));
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();
});
@@ -296,18 +272,18 @@ Acme2.prototype.waitForChallenge = function (challenge, callback) {
superagent.get(challenge.url).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) {
debug('waitForChallenge: network error getting uri %s', challenge.url);
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message)); // network error
return retryCallback(new BoxError(BoxError.NETWORK_ERROR, `Network error waiting for challenge: ${error.message}`));
}
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 +305,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(new BoxError(BoxError.NETWORK_ERROR, `Network error when signing certificate: ${error.message}`));
// 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 +327,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);
@@ -373,25 +349,29 @@ Acme2.prototype.downloadCertificate = function (hostname, certUrl, callback) {
var outdir = paths.APP_CERTS_DIR;
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;
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 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'));
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.text;
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 +382,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 +392,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 +434,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 +447,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 +473,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 +485,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));
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error when preparing challenge: ${error.message}`));
if (response.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response code getting authorization : ' + response.statusCode));
const authorization = response.body;
@@ -526,6 +508,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 +525,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 {
@@ -586,8 +570,8 @@ 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));
if (error && !error.response) 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' ||
@@ -631,6 +615,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);
}
+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);
}
+26 -56
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,7 +277,6 @@ 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');
@@ -313,7 +284,7 @@ function issueDeveloperToken(userObject, auditSource, callback) {
const expiresAt = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
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);
}
+18 -50
View File
@@ -1,8 +1,6 @@
'use strict';
exports = module.exports = {
CloudronError: CloudronError,
initialize: initialize,
uninitialize: uninitialize,
getConfig: getConfig,
@@ -28,12 +26,12 @@ var apps = require('./apps.js'),
async = require('async'),
auditSource = require('./auditsource.js'),
backups = require('./backups.js'),
BoxError = require('./boxerror.js'),
clients = require('./clients.js'),
constants = require('./constants.js'),
cron = require('./cron.js'),
debug = require('debug')('box:cloudron'),
domains = require('./domains.js'),
DomainsError = require('./domains.js').DomainsError,
eventlog = require('./eventlog.js'),
custom = require('./custom.js'),
fs = require('fs'),
@@ -49,40 +47,12 @@ var apps = require('./apps.js'),
shell = require('./shell.js'),
spawn = require('child_process').spawn,
split = require('split'),
sysinfo = require('./sysinfo.js'),
tasks = require('./tasks.js'),
TaskError = require('./tasks.js').TaskError,
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');
@@ -119,11 +89,11 @@ function notifyUpdate(callback) {
const version = safe.fs.readFileSync(paths.VERSION_FILE, 'utf8');
if (version === constants.VERSION) return callback();
eventlog.add(eventlog.ACTION_UPDATE_FINISH, auditSource.CRON, { oldVersion: version || 'dev', newVersion: constants.VERSION }, function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
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 !== TaskError.NOT_FOUND) return callback(error); // when hotfixing, task may not exist
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');
@@ -135,7 +105,7 @@ function notifyUpdate(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 (settings.adminDomain()) reverseProxy.writeAdminConfig(settings.adminDomain(), NOOP_CALLBACK);
@@ -153,7 +123,7 @@ 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, {
@@ -165,7 +135,7 @@ function getConfig(callback) {
version: constants.VERSION,
isDemo: settings.isDemo(),
memory: os.totalmem(),
provider: sysinfo.provider(),
provider: settings.provider(),
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
uiSpec: custom.uiSpec()
});
@@ -251,7 +221,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);
@@ -284,19 +254,18 @@ 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'));
tasks.add(tasks.TASK_PREPARE_DASHBOARD_DOMAIN, [ domain, auditSource ], function (error, taskId) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
if (error) return callback(error);
tasks.startTask(taskId, {}, NOOP_CALLBACK);
@@ -315,11 +284,10 @@ 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);
@@ -327,7 +295,7 @@ function setDashboardDomain(domain, auditSource, callback) {
(done) => settings.setAdmin(domain, fqdn, done),
(done) => clients.addDefaultClients(settings.adminOrigin(), done)
], function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
if (error) return callback(error);
eventlog.add(eventlog.ACTION_DASHBOARD_DOMAIN_UPDATE, auditSource, { domain: domain, fqdn: fqdn });
@@ -371,7 +339,7 @@ function renewCerts(options, auditSource, callback) {
assert.strictEqual(typeof callback, 'function');
tasks.add(tasks.TASK_RENEW_CERTS, [ options, auditSource ], function (error, taskId) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
if (error) return callback(error);
tasks.startTask(taskId, {}, NOOP_CALLBACK);
+1 -1
View File
@@ -38,7 +38,7 @@ const DEFAULT_SPEC = {
},
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)'
+1 -1
View File
@@ -105,7 +105,7 @@ function clear(callback) {
async.series([
child_process.exec.bind(null, cmd),
require('./clientdb.js')._addDefaultClients
require('./clients.js').addDefaultClients.bind(null, 'https://admin-localhost')
], 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';
+6 -28
View File
@@ -8,34 +8,12 @@ exports = module.exports = {
const apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
debug = require('debug')('box:disks'),
df = require('@sindresorhus/df'),
docker = require('./docker.js'),
notifications = require('./notifications.js'),
paths = require('./paths.js'),
util = require('util');
function DisksError(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(DisksError, Error);
DisksError.INTERNAL_ERROR = 'Internal Error';
DisksError.EXTERNAL_ERROR = 'External Error';
paths = require('./paths.js');
function getDisks(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -43,7 +21,7 @@ function getDisks(callback) {
const dfAsync = async.asyncify(df), dfFileAsync = async.asyncify(df.file);
docker.info(function (error, info) {
if (error) return callback(new DisksError(DisksError.INTERNAL_ERROR, error));
if (error) return callback(error);
async.series([
dfAsync,
@@ -52,7 +30,7 @@ function getDisks(callback) {
dfFileAsync.bind(null, paths.APPS_DATA_DIR),
dfFileAsync.bind(null, info.DockerRootDir)
], function (error, values) {
if (error) return callback(new DisksError(DisksError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.FS_ERROR, error));
// filter by ext4 and then sort to make sure root disk is first
const ext4Disks = values[0].filter((r) => r.type === 'ext4').sort((a, b) => a.mountpoint.localeCompare(b.mountpoint));
@@ -68,7 +46,7 @@ function getDisks(callback) {
};
apps.getAll(function (error, allApps) {
if (error) return callback(new DisksError(DisksError.INTERNAL_ERROR, error));
if (error) return callback(error);
async.eachSeries(allApps, function (app, iteratorDone) {
if (!app.dataDir) {
@@ -81,7 +59,7 @@ function getDisks(callback) {
iteratorDone();
});
}, function (error) {
if (error) return callback(new DisksError(DisksError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback(null, disks);
});
+17 -13
View File
@@ -11,14 +11,18 @@ exports = module.exports = {
};
var assert = require('assert'),
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');
@@ -63,10 +67,10 @@ function upsert(domainObject, location, type, values, callback) {
.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);
});
@@ -88,8 +92,8 @@ function get(domainObject, location, type, callback) {
.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);
});
@@ -116,11 +120,11 @@ function del(domainObject, location, type, values, callback) {
.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';
+12 -12
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,15 @@ 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 getZoneByName(dnsConfig, zoneName, callback) {
@@ -60,7 +60,7 @@ function getZoneByName(dnsConfig, zoneName, callback) {
.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]);
});
@@ -259,7 +259,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,8 +277,8 @@ 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'));
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.email || 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';
@@ -290,15 +290,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' }));
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, new 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';
+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, {});
});
+21 -21
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,21 +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') {
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 DomainsError(DomainsError.ACCESS_DENIED, errorMessage));
if (errorMessage === 'API Key is invalid or API access has not been enabled') return callback(new BoxError(BoxError.ACCESS_DENIED, errorMessage));
return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, errorMessage));
return callback(new BoxError(BoxError.EXTERNAL_ERROR, errorMessage));
}
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.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['$'];
@@ -118,22 +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') {
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 DomainsError(DomainsError.ACCESS_DENIED, errorMessage));
if (errorMessage === 'API Key is invalid or API access has not been enabled') return callback(new BoxError(BoxError.ACCESS_DENIED, errorMessage));
return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, errorMessage));
return callback(new BoxError(BoxError.EXTERNAL_ERROR, errorMessage));
}
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.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);
});
@@ -281,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,
@@ -292,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, {});
});
+106 -71
View File
@@ -2,7 +2,13 @@
exports = module.exports = {
connection: connectionInstance(),
testRegistryConfig: testRegistryConfig,
setRegistryConfig: setRegistryConfig,
injectPrivateFields: injectPrivateFields,
removePrivateFields: removePrivateFields,
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8),
ping: ping,
@@ -23,7 +29,6 @@ exports = module.exports = {
inspectByName: inspect,
getEvents: getEvents,
memoryUsage: memoryUsage,
execContainer: execContainer,
createVolume: createVolume,
removeVolume: removeVolume,
clearVolume: clearVolume
@@ -43,12 +48,10 @@ var addons = require('./addons.js'),
child_process = require('child_process'),
constants = require('./constants.js'),
debug = require('debug')('box:docker.js'),
once = require('once'),
path = require('path'),
settings = require('./settings.js'),
shell = require('./shell.js'),
safe = require('safetydance'),
spawn = child_process.spawn,
util = require('util'),
_ = require('underscore');
@@ -61,6 +64,30 @@ function debugApp(app) {
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');
let docker = exports.connection;
docker.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');
@@ -68,7 +95,7 @@ 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 BoxError(BoxError.ACCESS_DENIED, error.message));
@@ -91,36 +118,61 @@ function ping(callback) {
});
}
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;
// 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
docker.pull(manifest.dockerImage, function (error, stream) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Unable to pull image. statusCode: ' + error.statusCode));
getRegistryConfig(manifest.dockerImage, function (error, authConfig) {
if (error) return callback(error);
// 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 %s: %j', manifest.id, data);
debug(`pullImage: will pull ${manifest.dockerImage}. auth: ${authConfig ? 'yes' : 'no'}`);
// 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.id, data.errorDetail.message);
}
});
docker.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));
stream.on('end', function () {
debug('downloaded image %s of %s successfully', manifest.dockerImage, manifest.id);
// 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);
callback(null);
});
// 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);
}
});
stream.on('error', function (error) {
debug('error pulling image %s of %s: %j', manifest.dockerImage, manifest.id, error);
stream.on('end', function () {
debug('downloaded image %s', manifest.dockerImage);
callback(new BoxError(BoxError.DOCKER_ERROR, error.message));
callback(null);
});
stream.on('error', function (error) {
debug('error pulling image %s: %j', manifest.dockerImage, error);
callback(new BoxError(BoxError.DOCKER_ERROR, error.message));
});
});
});
}
@@ -129,12 +181,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);
@@ -165,7 +217,6 @@ function createSubcontainer(app, name, cmd, options, callback) {
'CLOUDRON=1',
'CLOUDRON_PROXY_IP=172.18.0.1',
`CLOUDRON_APP_HOSTNAME=${app.id}`,
`CLOUDRON_ADMIN_EMAIL=${app.adminEmail}`,
`${envPrefix}WEBADMIN_ORIGIN=${settings.adminOrigin()}`,
`${envPrefix}API_ORIGIN=${settings.adminOrigin()}`,
`${envPrefix}APP_ORIGIN=https://${domain}`,
@@ -180,7 +231,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;
@@ -208,7 +259,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
@@ -249,7 +300,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
@@ -279,7 +330,11 @@ function createSubcontainer(app, name, cmd, options, callback) {
debugApp(app, 'Creating container for %s', app.manifest.dockerImage);
docker.createContainer(containerOptions, callback);
docker.createContainer(containerOptions, function (error, container) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
callback(null, container);
});
});
}
@@ -298,6 +353,7 @@ function startContainer(containerId, callback) {
container.start(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 !== 304) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
return callback(null);
@@ -322,12 +378,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) : '');
@@ -355,9 +411,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);
});
}
@@ -421,9 +480,12 @@ function deleteImage(manifest, callback) {
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);
});
}
@@ -491,37 +553,6 @@ function memoryUsage(containerId, callback) {
});
}
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');
@@ -569,7 +600,11 @@ function clearVolume(app, name, options, callback) {
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();
});
});
}
@@ -583,7 +618,7 @@ function removeVolume(app, name, callback) {
let volume = docker.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();
});
+8 -2
View File
@@ -6,8 +6,8 @@ exports = module.exports = {
};
var apps = require('./apps.js'),
AppsError = apps.AppsError,
assert = require('assert'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
express = require('express'),
debug = require('debug')('box:dockerproxy'),
@@ -35,7 +35,7 @@ function authorizeApp(req, res, 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) {
@@ -137,8 +139,12 @@ function start(callback) {
gHttpServer = http.createServer(proxyServer);
gHttpServer.listen(constants.DOCKER_PROXY_PORT, '0.0.0.0', callback);
// 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 () {
+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);
});
+43 -86
View File
@@ -30,20 +30,17 @@ module.exports = exports = {
prepareDashboardDomain: prepareDashboardDomain,
DomainsError: DomainsError,
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8)
};
var assert = require('assert'),
async = require('async'),
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'),
@@ -51,36 +48,6 @@ var assert = require('assert'),
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');
@@ -115,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, 'Incorrect configuration. Access denied'));
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.BAD_FIELD, 'Zone not found'));
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;
@@ -150,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 === settings.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;
@@ -185,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;
@@ -207,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);
@@ -232,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 });
@@ -252,16 +216,13 @@ function get(domain, callback) {
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'));
if (!cert || !key) return callback(new BoxError(BoxError.FS_ERROR, 'unable to read certificates from disk'));
result.fallbackCertificate = { cert: cert, key: key };
@@ -274,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);
});
@@ -293,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);
@@ -323,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 });
@@ -345,12 +304,10 @@ function del(domain, auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
if (domain === settings.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 });
@@ -362,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);
});
@@ -414,8 +371,8 @@ function checkDnsRecords(location, domain, callback) {
getDnsRecords(location, domain, 'A', function (error, values) {
if (error) return callback(error);
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
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
@@ -436,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);
@@ -459,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);
});
@@ -516,8 +473,8 @@ function prepareDashboardDomain(domain, auditSource, progressCallback, callback)
const adminFqdn = fqdn(constants.ADMIN_LOCATION, domainObject);
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(error);
async.series([
(done) => { progressCallback({ percent: 10, message: `Updating DNS of ${adminFqdn}` }); done(); },
+1 -1
View File
@@ -21,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 };
+7 -33
View File
@@ -1,8 +1,6 @@
'use strict';
exports = module.exports = {
EventLogError: EventLogError,
add: add,
get: get,
getAllPaged: getAllPaged,
@@ -26,7 +24,7 @@ exports = module.exports = {
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',
@@ -63,7 +61,6 @@ exports = module.exports = {
};
var assert = require('assert'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:eventlog'),
eventlogdb = require('./eventlogdb.js'),
notifications = require('./notifications.js'),
@@ -72,28 +69,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');
@@ -105,10 +80,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 });
});
@@ -120,8 +95,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);
});
@@ -135,7 +109,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);
});
@@ -146,7 +120,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);
});
@@ -159,7 +133,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);
});
+140 -117
View File
@@ -1,89 +1,122 @@
'use strict';
exports = module.exports = {
ExternalLdapError: ExternalLdapError,
verifyPassword: verifyPassword,
testConfig: testConfig,
startSyncer: startSyncer,
injectPrivateFields: injectPrivateFields,
removePrivateFields: removePrivateFields,
sync: sync
};
var assert = require('assert'),
async = require('async'),
auditsource = require('./auditsource.js'),
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'),
UserError = users.UsersError,
util = require('util');
users = require('./users.js');
function ExternalLdapError(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;
}
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;
}
util.inherits(ExternalLdapError, Error);
ExternalLdapError.EXTERNAL_ERROR = 'external error';
ExternalLdapError.INTERNAL_ERROR = 'internal error';
ExternalLdapError.INVALID_CREDENTIALS = 'invalid credentials';
ExternalLdapError.BAD_STATE = 'bad state';
ExternalLdapError.BAD_FIELD = 'bad field';
ExternalLdapError.NOT_FOUND = 'not found';
// 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 ExternalLdapError(ExternalLdapError.BAD_FIELD, 'invalid baseDn')); }
try { ldap.parseFilter(externalLdapConfig.filter); } catch (e) { return callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'invalid filter')); }
if (externalLdapConfig.bindDn) try { ldap.parseFilter(externalLdapConfig.bindDn); } catch (e) { return callback(new ExternalLdapError(ExternalLdapError.INVALID_CREDENTIALS)); }
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 ExternalLdapError(ExternalLdapError.BAD_FIELD, 'url protocol is invalid'));
return callback(new ExternalLdapError(ExternalLdapError.INTERNAL_ERROR, 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 ExternalLdapError(ExternalLdapError.INVALID_CREDENTIALS));
if (error) return callback(new ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, 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);
});
}
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.enabled) return callback();
if (config.provider === 'noop') return callback();
if (!config.url) return callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'url must not be empty'));
if (!config.baseDn) return callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'basedn must not be empty'));
if (!config.filter) return callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'filter must not be empty'));
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);
@@ -94,11 +127,11 @@ function testConfig(config, callback) {
};
client.search(config.baseDn, opts, function (error, result) {
if (error) return callback(new ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
result.on('searchEntry', function (entry) {});
result.on('error', function (error) { callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'Unable to search directory')); });
result.on('end', function (result) { callback(); });
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(); });
});
});
}
@@ -109,17 +142,20 @@ function verifyPassword(user, password, callback) {
assert.strictEqual(typeof callback, 'function');
settings.getExternalLdapConfig(function (error, externalLdapConfig) {
if (error) return callback(new ExternalLdapError(ExternalLdapError.INTERNAL_ERROR, error));
if (!externalLdapConfig.enabled) return callback(new ExternalLdapError(ExternalLdapError.BAD_STATE, 'not enabled'));
if (error) return callback(error);
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
getClient(externalLdapConfig, function (error, client) {
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));
const dn = `uid=${user.username},${externalLdapConfig.baseDn}`;
const userDn = ldapUsers[0].dn;
let client = ldap.createClient({ url: externalLdapConfig.url });
client.bind(dn, password, function (error) {
if (error instanceof ldap.InvalidCredentialsError) return callback(new ExternalLdapError(ExternalLdapError.INVALID_CREDENTIALS));
if (error) return callback(new ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, error));
client.bind(userDn, 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();
});
@@ -131,11 +167,11 @@ function startSyncer(callback) {
assert.strictEqual(typeof callback, 'function');
settings.getExternalLdapConfig(function (error, externalLdapConfig) {
if (error) return callback(new ExternalLdapError(ExternalLdapError.INTERNAL_ERROR, error));
if (!externalLdapConfig.enabled) return callback(new ExternalLdapError(ExternalLdapError.BAD_STATE, 'not enabled'));
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(new ExternalLdapError(ExternalLdapError.INTERNAL_ERROR, error));
if (error) return callback(error);
tasks.startTask(taskId, {}, function (error, result) {
debug('sync: done', error, result);
@@ -150,81 +186,68 @@ function sync(progressCallback, callback) {
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
debug('Start user syncing ...');
progressCallback({ percent: 10, message: 'Starting ldap user sync' });
settings.getExternalLdapConfig(function (error, externalLdapConfig) {
if (error) return callback(new ExternalLdapError(ExternalLdapError.INTERNAL_ERROR, error));
if (!externalLdapConfig.enabled) return callback(new ExternalLdapError(ExternalLdapError.BAD_STATE, 'not enabled'));
if (error) return callback(error);
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
getClient(externalLdapConfig, function (error, client) {
ldapSearch(externalLdapConfig, {}, function (error, ldapUsers) {
if (error) return callback(error);
var opts = {
paged: true,
filter: externalLdapConfig.filter,
scope: 'sub' // We may have to make this configurable
};
debug(`Found ${ldapUsers.length} users`);
let percent = 10;
let step = 90/(ldapUsers.length+1); // ensure no divide by 0
debug(`Listing users at ${externalLdapConfig.baseDn} with filter ${externalLdapConfig.filter}`);
// we ignore all errors here and just log them for now
async.eachSeries(ldapUsers, function (user, iteratorCallback) {
const username = user[externalLdapConfig.usernameField];
const email = user.mail;
const displayName = user.cn; // user.giveName + ' ' + user.sn
client.search(externalLdapConfig.baseDn, opts, function (error, result) {
if (error) return callback(new ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, error));
if (!username || !email || !displayName) {
debug(`[empty username/email/displayName] username=${username} email=${email} displayName=${displayName} usernameField=${externalLdapConfig.usernameField}`);
return iteratorCallback();
}
var ldapUsers = [];
percent += step;
progressCallback({ percent, message: `Syncing... ${username}` });
result.on('searchEntry', function (entry) {
ldapUsers.push(entry.object);
});
users.getByUsername(username, function (error, result) {
if (error && error.reason !== BoxError.NOT_FOUND) {
debug(`Could not find user with username ${username}: ${error.message}`);
return iteratorCallback();
}
result.on('error', function (error) {
callback(new ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, error));
});
if (error) {
debug(`[adding user] username=${username} email=${email} displayName=${displayName}`);
result.on('end', function (result) {
if (result.status !== 0) return callback(new ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, 'Server returned status ' + result.status));
debug(`Found ${ldapUsers.length} users`);
// we ignore all errors here and just log them for now
async.eachSeries(ldapUsers, function (user, callback) {
// ignore the bindDn user if any
if (user.dn === externalLdapConfig.bindDn) return callback();
users.getByUsername(user.uid, function (error, result) {
if (error && error.reason !== UserError.NOT_FOUND) {
console.error(error);
return callback();
}
if (error) {
debug('[adding user] ', user.uid, user.mail, user.cn);
users.create(user.uid, null, user.mail, user.cn, { source: 'ldap' }, auditsource.EXTERNAL_LDAP_TASK, function (error) {
if (error) console.error('Failed to create user', user, error);
callback();
});
} else if (result.source !== 'ldap') {
debug('[conflicting user]', user.uid, user.mail, user.cn);
callback();
} else if (result.email !== user.mail || result.displayName !== user.cn) {
debug('[updating user] ', user.uid, user.mail, user.cn);
users.update(result.id, { email: user.mail, fallbackEmail: user.mail, displayName: user.cn }, auditsource.EXTERNAL_LDAP_TASK, function (error) {
if (error) console.error('Failed to update user', user, error);
callback();
});
} else {
// user known and up-to-date
callback();
}
users.create(username, null /* password */, email, displayName, { source: 'ldap' }, auditSource.EXTERNAL_LDAP_TASK, function (error) {
if (error) console.error('Failed to create user', user, error);
iteratorCallback();
});
}, function () {
debug('User sync done.');
callback();
});
} else if (result.source !== 'ldap') {
debug(`[conflicting user] username=${username} email=${email} displayName=${displayName}`);
iteratorCallback();
} else if (result.email !== email || result.displayName !== displayName) {
debug(`[updating user] username=${username} email=${email} displayName=${displayName}`);
users.update(result.id, { email: email, fallbackEmail: email, displayName: 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=${username} email=${email} displayName=${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);
});
+21 -64
View File
@@ -1,8 +1,6 @@
'use strict';
exports = module.exports = {
GroupsError: GroupsError,
create: create,
remove: remove,
get: get,
@@ -26,52 +24,23 @@ exports = module.exports = {
};
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;
}
@@ -88,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 });
});
@@ -100,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);
});
@@ -112,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);
});
@@ -124,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);
});
@@ -135,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);
});
@@ -145,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);
});
@@ -156,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);
});
@@ -168,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);
});
@@ -181,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);
});
@@ -194,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);
});
@@ -207,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);
});
@@ -220,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);
});
@@ -233,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);
});
@@ -253,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);
});
@@ -265,7 +222,7 @@ 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);
});
@@ -275,7 +232,7 @@ function count(callback) {
assert.strictEqual(typeof callback, 'function');
groupdb.count(function (error, count) {
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback(null, count);
});
+2 -2
View File
@@ -18,8 +18,8 @@ exports = module.exports = {
'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.1.0@sha256:6d1bf221cfe6124957e2c58b57c0a47214353496009296acb16adf56df1da9d5' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.0.0@sha256:8a88dd334b62b578530a014ca1a2425a54cb9df1e475f5d3a36806e5cfa22121' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.4.0@sha256:209f76833ff8cce58be8a09897c378e3b706e1e318249870afb7294a4ee83cad' },
'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' }
}
+24 -25
View File
@@ -9,18 +9,16 @@ var assert = require('assert'),
appdb = require('./appdb.js'),
apps = require('./apps.js'),
async = require('async'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.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;
@@ -261,7 +259,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 +286,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 +332,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 +365,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.getList(parts[0], parts[1], function (error, list) {
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 +381,9 @@ function mailingListSearch(req, res, next) {
attributes: {
objectclass: ['mailGroup'],
objectcategory: 'mailGroup',
cn: `${list.name}@${list.domain}`, // fully qualified
mail: `${list.name}@${list.domain}`,
mgrpRFC822MailMember: list.members // fully qualified
cn: `${name}@${domain}`, // fully qualified
mail: `${name}@${domain}`,
mgrpRFC822MailMember: resolvedMembers // fully qualified
}
};
@@ -421,8 +420,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 +456,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 +554,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 +566,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) });
+136 -110
View File
@@ -26,6 +26,7 @@ exports = module.exports = {
startMail: restartMail,
restartMail: restartMail,
handleCertChanged: handleCertChanged,
getMailAuth: getMailAuth,
sendTestMail: sendTestMail,
@@ -45,18 +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'),
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'),
@@ -75,47 +76,20 @@ var assert = require('assert'),
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;
}
@@ -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();
});
@@ -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/dmar/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
};
@@ -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
@@ -688,6 +664,31 @@ 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');
@@ -701,7 +702,8 @@ 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
@@ -723,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);
});
@@ -734,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);
});
@@ -747,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);
@@ -796,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;
}
@@ -837,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);
@@ -846,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, new 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 + '"' ] };
@@ -870,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);
@@ -895,12 +895,12 @@ function onMailFqdnChanged(callback) {
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);
});
@@ -914,9 +914,7 @@ function addDomain(domain, callback) {
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, settings.mailFqdn()), // do this first to ensure DKIM keys
@@ -931,12 +929,10 @@ function removeDomain(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
if (domain === settings.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);
@@ -948,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();
});
@@ -970,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)
@@ -985,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)
@@ -1012,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);
@@ -1030,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);
@@ -1050,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);
});
@@ -1073,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();
});
@@ -1085,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);
});
@@ -1105,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 });
@@ -1123,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);
});
@@ -1137,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 });
@@ -1146,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);
});
@@ -1167,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);
});
@@ -1189,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);
});
@@ -1207,7 +1190,7 @@ function getLists(domain, callback) {
assert.strictEqual(typeof callback, 'function');
mailboxdb.getLists(domain, function (error, result) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback(null, result);
});
@@ -1219,8 +1202,7 @@ function getList(domain, listName, callback) {
assert.strictEqual(typeof callback, 'function');
mailboxdb.getList(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));
if (error) return callback(error);
callback(null, result);
});
@@ -1239,12 +1221,11 @@ function addList(name, domain, members, auditSource, callback) {
if (error) return callback(error);
for (var i = 0; i < members.length; i++) {
if (!validator.isEmail(members[i])) return callback(new MailError(MailError.BAD_FIELD, 'Invalid mail member: ' + members[i]));
if (!validator.isEmail(members[i])) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid mail member: ' + members[i]));
}
mailboxdb.addList(name, domain, members, function (error) {
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));
if (error) return callback(error);
eventlog.add(eventlog.ACTION_MAIL_LIST_ADD, auditSource, { name, domain });
@@ -1264,12 +1245,11 @@ function updateList(name, domain, members, callback) {
if (error) return callback(error);
for (var i = 0; i < members.length; i++) {
if (!validator.isEmail(members[i])) return callback(new MailError(MailError.BAD_FIELD, 'Invalid email: ' + members[i]));
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);
});
@@ -1282,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 });
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 { %>
<% } %>
+62 -37
View File
@@ -12,6 +12,7 @@ exports = module.exports = {
listMailboxes: listMailboxes,
getLists: getLists,
get: get,
getMailbox: getMailbox,
getList: getList,
getAlias: getAlias,
@@ -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,8 +69,8 @@ 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);
});
@@ -83,8 +84,8 @@ function addList(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); });
@@ -203,7 +220,7 @@ function getLists(domain, callback) {
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); });
@@ -218,8 +235,8 @@ function getList(name, domain, callback) {
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);
});
+26 -18
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'),
constants = require('./constants.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'),
@@ -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: constants.INTERNAL_SMTP_PORT,
host: data.ip,
port: data.port,
auth: {
user: mailOptions.authUser || `no-reply@${settings.adminDomain()}`,
pass: relayToken
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}`);
@@ -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');
@@ -97,6 +97,14 @@ server {
<% if ( endpoint === 'admin' ) { -%>
# CSP headers for the admin/dashboard resources
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;
+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);
});
+37 -46
View File
@@ -1,8 +1,6 @@
'use strict';
exports = module.exports = {
NotificationsError: NotificationsError,
get: get,
ack: ack,
getAllPaged: getAllPaged,
@@ -24,39 +22,16 @@ exports = module.exports = {
let assert = require('assert'),
async = require('async'),
auditsource = require('./auditsource.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'),
settings = require('./settings.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';
users = require('./users.js');
function add(userId, eventId, title, message, callback) {
assert.strictEqual(typeof userId, 'string');
@@ -74,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 });
});
@@ -86,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);
});
@@ -98,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);
});
@@ -114,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);
@@ -129,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; });
@@ -255,7 +227,8 @@ function appUpdated(eventId, app, callback) {
}, callback);
}
function boxUpdated(oldVersion, newVersion, 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');
@@ -264,7 +237,21 @@ function boxUpdated(oldVersion, newVersion, callback) {
const changelogMarkdown = changes.map((m) => `* ${m}\n`).join('');
actionForAllAdmins([], function (admin, done) {
add(admin.id, null, `Cloudron updated to v${newVersion}`, `Cloudron was updated from v${oldVersion} to v${newVersion}.\n\nChangelog:\n${changelogMarkdown}\n`, 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);
}
@@ -294,7 +281,7 @@ function backupFailed(eventId, taskId, errorMessage, callback) {
actionForAllAdmins([], function (admin, callback) {
mailer.backupFailed(admin.email, errorMessage, `${settings.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);
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);
}
@@ -319,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);
});
@@ -347,7 +333,7 @@ function onEvent(id, action, source, data, callback) {
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();
if (source.username === auditSource.EXTERNAL_LDAP_TASK.username) return callback();
switch (action) {
case eventlog.ACTION_USER_ADD:
@@ -370,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:
@@ -378,11 +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:
return boxUpdated(data.oldVersion, data.newVersion, callback);
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();
+60 -88
View File
@@ -6,23 +6,19 @@ exports = module.exports = {
activate: activate,
getStatus: getStatus,
autoRegister: autoRegister,
ProvisionError: ProvisionError
autoRegister: autoRegister
};
var appstore = require('./appstore.js'),
AppstoreError = require('./appstore.js').AppstoreError,
assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
BackupsError = require('./backups.js').BackupsError,
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
clients = require('./clients.js'),
cloudron = require('./cloudron.js'),
debug = require('debug')('box:provision'),
domains = require('./domains.js'),
DomainsError = domains.DomainsError,
eventlog = require('./eventlog.js'),
fs = require('fs'),
mail = require('./mail.js'),
@@ -30,12 +26,10 @@ var appstore = require('./appstore.js'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
superagent = require('superagent'),
sysinfo = require('./sysinfo.js'),
superagent = require('superagent'),
users = require('./users.js'),
UsersError = users.UsersError,
tld = require('tldjs'),
util = require('util'),
_ = require('underscore');
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
@@ -54,33 +48,6 @@ let gProvisionStatus = {
}
};
function ProvisionError(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(ProvisionError, Error);
ProvisionError.BAD_FIELD = 'Field error';
ProvisionError.BAD_STATE = 'Bad State';
ProvisionError.ALREADY_SETUP = 'Already Setup';
ProvisionError.INTERNAL_ERROR = 'Internal Error';
ProvisionError.EXTERNAL_ERROR = 'External Error';
ProvisionError.LICENSE_ERROR = 'License Error';
ProvisionError.ALREADY_PROVISIONED = 'Already Provisioned';
function setProgress(task, message, callback) {
debug(`setProgress: ${task} - ${message}`);
gProvisionStatus[task].message = message;
@@ -94,14 +61,14 @@ function autoRegister(domain, callback) {
if (!fs.existsSync(paths.LICENSE_FILE)) return callback();
const license = safe.fs.readFileSync(paths.LICENSE_FILE, 'utf8');
if (!license) return callback(new ProvisionError(ProvisionError.EXTERNAL_ERROR, 'Cannot read license'));
if (!license) return callback(new BoxError(BoxError.LICENSE_ERROR, 'Cannot read license'));
debug('Auto-registering cloudron');
appstore.registerWithLicense(license.trim(), domain, function (error) {
if (error && error.reason !== AppstoreError.ALREADY_REGISTERED) {
if (error && error.reason !== BoxError.CONFLICT) { // not already registered
debug('Failed to auto-register cloudron', error);
return callback(new ProvisionError(ProvisionError.LICENSE_ERROR, 'Failed to auto-register Cloudron with license. Please contact support@cloudron.io'));
return callback(new BoxError(BoxError.LICENSE_ERROR, 'Failed to auto-register Cloudron with license. Please contact support@cloudron.io'));
}
callback();
@@ -122,13 +89,13 @@ function unprovision(callback) {
}
function setup(dnsConfig, backupConfig, auditSource, callback) {
function setup(dnsConfig, sysinfoConfig, auditSource, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof sysinfoConfig, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
if (gProvisionStatus.setup.active || gProvisionStatus.restore.active) return callback(new ProvisionError(ProvisionError.BAD_STATE, 'Already setting up or restoring'));
if (gProvisionStatus.setup.active || gProvisionStatus.restore.active) return callback(new BoxError(BoxError.BAD_STATE, 'Already setting up or restoring'));
gProvisionStatus.setup = { active: true, errorMessage: '', message: 'Adding domain' };
@@ -139,11 +106,11 @@ function setup(dnsConfig, backupConfig, auditSource, callback) {
}
users.isActivated(function (error, activated) {
if (error) return done(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (activated) return done(new ProvisionError(ProvisionError.ALREADY_SETUP));
if (error) return done(error);
if (activated) return done(new BoxError(BoxError.CONFLICT, 'Already activated', { activate: true }));
unprovision(function (error) {
if (error) return done(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (error) return done(error);
const domain = dnsConfig.domain.toLowerCase();
const zoneName = dnsConfig.zoneName ? dnsConfig.zoneName : (tld.getDomain(domain) || domain);
@@ -159,23 +126,25 @@ function setup(dnsConfig, backupConfig, auditSource, callback) {
};
domains.add(domain, data, auditSource, function (error) {
if (error && error.reason === DomainsError.BAD_FIELD) return done(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
if (error && error.reason === DomainsError.ALREADY_EXISTS) return done(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
if (error) return done(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (error) return done(error);
callback(); // now that args are validated run the task in the background
sysinfo.testConfig(sysinfoConfig, function (error) {
if (error) return done(error);
async.series([
autoRegister.bind(null, domain),
domains.prepareDashboardDomain.bind(null, domain, auditSource, (progress) => setProgress('setup', progress.message, NOOP_CALLBACK)),
cloudron.setDashboardDomain.bind(null, domain, auditSource),
mail.addDomain.bind(null, domain), // this relies on settings.mailFqdn() and settings.adminDomain()
(next) => { if (!backupConfig) return next(); settings.setBackupConfig(backupConfig, next); },
setProgress.bind(null, 'setup', 'Done'),
eventlog.add.bind(null, eventlog.ACTION_PROVISION, auditSource, { })
], function (error) {
gProvisionStatus.setup.active = false;
gProvisionStatus.setup.errorMessage = error ? error.message : '';
callback(); // now that args are validated run the task in the background
async.series([
autoRegister.bind(null, domain),
settings.setSysinfoConfig.bind(null, sysinfoConfig),
domains.prepareDashboardDomain.bind(null, domain, auditSource, (progress) => setProgress('setup', progress.message, NOOP_CALLBACK)),
cloudron.setDashboardDomain.bind(null, domain, auditSource),
mail.addDomain.bind(null, domain), // this relies on settings.mailFqdn() and settings.adminDomain()
setProgress.bind(null, 'setup', 'Done'),
eventlog.add.bind(null, eventlog.ACTION_PROVISION, auditSource, { })
], function (error) {
gProvisionStatus.setup.active = false;
gProvisionStatus.setup.errorMessage = error ? error.message : '';
});
});
});
});
@@ -221,12 +190,11 @@ function activate(username, password, email, displayName, ip, auditSource, callb
setTimeZone(ip, function () { }); // TODO: get this from user. note that timezone is detected based on the browser location and not the cloudron region
users.createOwner(username, password, email, displayName, auditSource, function (error, userObject) {
if (error && error.reason === UsersError.ALREADY_EXISTS) return callback(new ProvisionError(ProvisionError.ALREADY_PROVISIONED, 'Already activated'));
if (error && error.reason === UsersError.BAD_FIELD) return callback(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (error && error.reason === BoxError.ALREADY_EXISTS) return callback(new BoxError(BoxError.CONFLICT, 'Already activated'));
if (error) return callback(error);
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (error) return callback(error);
eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { });
@@ -241,17 +209,18 @@ function activate(username, password, email, displayName, ip, auditSource, callb
});
}
function restore(backupConfig, backupId, version, auditSource, callback) {
function restore(backupConfig, backupId, version, sysinfoConfig, auditSource, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof version, 'string');
assert.strictEqual(typeof sysinfoConfig, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
if (!semver.valid(version)) return callback(new ProvisionError(ProvisionError.BAD_STATE, 'version is not a valid semver'));
if (semver.major(constants.VERSION) !== semver.major(version) || semver.minor(constants.VERSION) !== semver.minor(version)) return callback(new ProvisionError(ProvisionError.BAD_STATE, `Run cloudron-setup with --version ${version} to restore from this backup`));
if (!semver.valid(version)) return callback(new BoxError(BoxError.BAD_FIELD, 'version is not a valid semver', { field: 'version' }));
if (semver.major(constants.VERSION) !== semver.major(version) || semver.minor(constants.VERSION) !== semver.minor(version)) return callback(new BoxError(BoxError.BAD_STATE, `Run cloudron-setup with --version ${version} to restore from this backup`));
if (gProvisionStatus.setup.active || gProvisionStatus.restore.active) return callback(new ProvisionError(ProvisionError.BAD_STATE, 'Already setting up or restoring'));
if (gProvisionStatus.setup.active || gProvisionStatus.restore.active) return callback(new BoxError(BoxError.BAD_STATE, 'Already setting up or restoring'));
gProvisionStatus.restore = { active: true, errorMessage: '', message: 'Testing backup config' };
@@ -262,29 +231,32 @@ function restore(backupConfig, backupId, version, auditSource, callback) {
}
users.isActivated(function (error, activated) {
if (error) return done(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (activated) return done(new ProvisionError(ProvisionError.ALREADY_PROVISIONED, 'Already activated. Restore with a fresh Cloudron installation.'));
if (error) return done(error);
if (activated) return done(new BoxError(BoxError.CONFLICT, 'Already activated. Restore with a fresh Cloudron installation.'));
backups.testConfig(backupConfig, function (error) {
if (error && error.reason === BackupsError.BAD_FIELD) return done(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return done(new ProvisionError(ProvisionError.EXTERNAL_ERROR, error.message));
if (error) return done(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (error) return done(error);
debug(`restore: restoring from ${backupId} from provider ${backupConfig.provider} with format ${backupConfig.format}`);
sysinfo.testConfig(sysinfoConfig, function (error) {
if (error) return done(error);
callback(); // now that the fields are validated, continue task in the background
debug(`restore: restoring from ${backupId} from provider ${backupConfig.provider} with format ${backupConfig.format}`);
async.series([
setProgress.bind(null, 'restore', 'Downloading backup'),
backups.restore.bind(null, backupConfig, backupId, (progress) => setProgress('restore', progress.message, NOOP_CALLBACK)),
cloudron.setupDashboard.bind(null, auditSource, (progress) => setProgress('restore', progress.message, NOOP_CALLBACK)),
settings.setBackupConfig.bind(null, backupConfig), // update with the latest backupConfig
eventlog.add.bind(null, eventlog.ACTION_RESTORE, auditSource, { backupId }),
], function (error) {
gProvisionStatus.restore.active = false;
gProvisionStatus.restore.errorMessage = error ? error.message : '';
callback(); // now that the fields are validated, continue task in the background
if (!error) cloudron.onActivated(NOOP_CALLBACK);
async.series([
setProgress.bind(null, 'restore', 'Downloading backup'),
backups.restore.bind(null, backupConfig, backupId, (progress) => setProgress('restore', progress.message, NOOP_CALLBACK)),
settings.setSysinfoConfig.bind(null, sysinfoConfig),
cloudron.setupDashboard.bind(null, auditSource, (progress) => setProgress('restore', progress.message, NOOP_CALLBACK)),
settings.setBackupConfig.bind(null, backupConfig), // update with the latest backupConfig
eventlog.add.bind(null, eventlog.ACTION_RESTORE, auditSource, { backupId }),
], function (error) {
gProvisionStatus.restore.active = false;
gProvisionStatus.restore.errorMessage = error ? error.message : '';
if (!error) cloudron.onActivated(NOOP_CALLBACK);
});
});
});
});
@@ -294,15 +266,15 @@ function getStatus(callback) {
assert.strictEqual(typeof callback, 'function');
users.isActivated(function (error, activated) {
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (error) return callback(error);
settings.getCloudronName(function (error, cloudronName) {
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback(null, _.extend({
version: constants.VERSION,
apiServerOrigin: settings.apiServerOrigin(), // used by CaaS tool
provider: sysinfo.provider(),
provider: settings.provider(),
cloudronName: cloudronName,
adminFqdn: settings.adminDomain() ? settings.adminFqdn() : null,
activated: activated,
+90 -71
View File
@@ -1,8 +1,6 @@
'use strict';
exports = module.exports = {
ReverseProxyError: ReverseProxyError,
setFallbackCertificate: setFallbackCertificate,
getFallbackCertificate: getFallbackCertificate,
@@ -16,16 +14,16 @@ exports = module.exports = {
renewCerts: renewCerts,
// the 'configure' functions always ensure a certificate
configureDefaultServer: configureDefaultServer,
// the 'configure' ensure a certificate and generate nginx config
configureAdmin: configureAdmin,
configureApp: configureApp,
unconfigureApp: unconfigureApp,
// these only generate nginx config
writeDefaultConfig: writeDefaultConfig,
writeAdminConfig: writeAdminConfig,
writeAppConfig: writeAppConfig,
reload: reload,
removeAppConfigs: removeAppConfigs,
// exported for testing
@@ -36,6 +34,7 @@ var acme2 = require('./cert/acme2.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
caas = require('./cert/caas.js'),
constants = require('./constants.js'),
crypto = require('crypto'),
@@ -57,32 +56,9 @@ var acme2 = require('./cert/acme2.js'),
users = require('./users.js'),
util = require('util');
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/appconfig.ejs', { encoding: 'utf8' }),
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/nginxconfig.ejs', { encoding: 'utf8' }),
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh');
function ReverseProxyError(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(ReverseProxyError, Error);
ReverseProxyError.INTERNAL_ERROR = 'Internal Error';
ReverseProxyError.INVALID_CERT = 'Invalid certificate';
ReverseProxyError.NOT_FOUND = 'Not Found';
function getCertApi(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -164,29 +140,29 @@ function validateCertificate(location, domainObject, certificate) {
const cert = certificate.cert, key = certificate.key;
// check for empty cert and key strings
if (!cert && key) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'missing cert');
if (cert && !key) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'missing key');
if (!cert && key) return new BoxError(BoxError.BAD_FIELD, 'missing cert', { field: 'cert' });
if (cert && !key) return new BoxError(BoxError.BAD_FIELD, 'missing key', { field: 'key' });
// -checkhost checks for SAN or CN exclusively. SAN takes precedence and if present, ignores the CN.
const fqdn = domains.fqdn(location, domainObject);
var result = safe.child_process.execSync(`openssl x509 -noout -checkhost "${fqdn}"`, { encoding: 'utf8', input: cert });
if (result === null) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Unable to get certificate subject:' + safe.error.message);
if (result === null) return new BoxError(BoxError.BAD_FIELD, 'Unable to get certificate subject:' + safe.error.message, { field: 'cert' });
if (result.indexOf('does match certificate') === -1) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, `Certificate is not valid for this domain. Expecting ${fqdn}`);
if (result.indexOf('does match certificate') === -1) return new BoxError(BoxError.BAD_FIELD, `Certificate is not valid for this domain. Expecting ${fqdn}`, { field: 'cert' });
// http://httpd.apache.org/docs/2.0/ssl/ssl_faq.html#verify
var certModulus = safe.child_process.execSync('openssl x509 -noout -modulus', { encoding: 'utf8', input: cert });
if (certModulus === null) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, `Unable to get cert modulus: ${safe.error.message}`);
if (certModulus === null) return new BoxError(BoxError.BAD_FIELD, `Unable to get cert modulus: ${safe.error.message}`, { field: 'cert' });
var keyModulus = safe.child_process.execSync('openssl rsa -noout -modulus', { encoding: 'utf8', input: key });
if (keyModulus === null) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, `Unable to get key modulus: ${safe.error.message}`);
if (keyModulus === null) return new BoxError(BoxError.BAD_FIELD, `Unable to get key modulus: ${safe.error.message}`, { field: 'cert' });
if (certModulus !== keyModulus) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Key does not match the certificate.');
if (certModulus !== keyModulus) return new BoxError(BoxError.BAD_FIELD, 'Key does not match the certificate.', { field: 'cert' });
// check expiration
result = safe.child_process.execSync('openssl x509 -checkend 0', { encoding: 'utf8', input: cert });
if (!result) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Certificate has expired.');
if (!result) return new BoxError(BoxError.BAD_FIELD, 'Certificate has expired.', { field: 'cert' });
return null;
}
@@ -194,7 +170,11 @@ function validateCertificate(location, domainObject, certificate) {
function reload(callback) {
if (process.env.BOX_ENV === 'test') return callback();
shell.sudo('reload', [ RELOAD_NGINX_CMD ], {}, callback);
shell.sudo('reload', [ RELOAD_NGINX_CMD ], {}, function (error) {
if (error) return callback(new BoxError(BoxError.NGINX_ERROR, error));
callback();
});
}
function generateFallbackCertificateSync(domainObject) {
@@ -215,15 +195,15 @@ function generateFallbackCertificateSync(domainObject) {
let configFile = path.join(os.tmpdir(), 'openssl-' + crypto.randomBytes(4).readUInt32LE(0) + '.conf');
safe.fs.writeFileSync(configFile, opensslConfWithSan, 'utf8');
let certCommand = util.format(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=*.${cn} -extensions SAN -config ${configFile} -nodes`);
if (!safe.child_process.execSync(certCommand)) return { error: new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message) };
if (!safe.child_process.execSync(certCommand)) return { error: new BoxError(BoxError.OPENSSL_ERROR, safe.error.message) };
safe.fs.unlinkSync(configFile);
const cert = safe.fs.readFileSync(certFilePath, 'utf8');
if (!cert) return { error: safe.error };
if (!cert) return { error: new BoxError(BoxError.FS_ERROR, safe.error.message) };
safe.fs.unlinkSync(certFilePath);
const key = safe.fs.readFileSync(keyFilePath, 'utf8');
if (!key) return { error: safe.error };
if (!key) return { error: new BoxError(BoxError.FS_ERROR, safe.error.message) };
safe.fs.unlinkSync(keyFilePath);
return { cert: cert, key: key, error: null };
@@ -237,17 +217,17 @@ function setFallbackCertificate(domain, fallback, callback) {
if (fallback.restricted) { // restricted certs are not backed up
debug(`setFallbackCertificate: setting restricted certs for domain ${domain}`);
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`), fallback.cert)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`), fallback.key)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`), fallback.cert)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`), fallback.key)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message));
} else {
debug(`setFallbackCertificate: setting certs for domain ${domain}`);
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`), fallback.cert)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.key`), fallback.key)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`), fallback.cert)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.key`), fallback.key)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message));
}
// TODO: maybe the cert is being used by the mail container
reload(function (error) {
if (error) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.NGINX_ERROR, error));
return callback(null);
});
@@ -261,13 +241,13 @@ function getFallbackCertificate(domain, callback) {
var certFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`);
var keyFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath, type: 'provisioned' });
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
// check for auto-generated or user set fallback certs
certFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`);
keyFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.key`);
callback(null, { certFilePath, keyFilePath, type: 'fallback' });
callback(null, { certFilePath, keyFilePath });
}
function setAppCertificateSync(location, domainObject, certificate) {
@@ -333,6 +313,8 @@ function notifyCertChanged(vhost, callback) {
assert.strictEqual(typeof vhost, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`notifyCertChanged: vhost: ${vhost} mailFqdn: ${settings.mailFqdn()}`);
if (vhost !== settings.mailFqdn()) return callback();
mail.handleCertChanged(callback);
@@ -350,12 +332,12 @@ function ensureCertificate(vhost, domain, auditSource, callback) {
getCertApi(domainObject, function (error, api, apiOptions) {
if (error) return callback(error);
getCertificateByHostname(vhost, domainObject, function (error, currentBundle) {
getCertificateByHostname(vhost, domainObject, function (_error, currentBundle) {
if (currentBundle) {
debug(`ensureCertificate: ${vhost} certificate already exists at ${currentBundle.keyFilePath}`);
if (currentBundle.certFilePath.endsWith('.user.cert')) return callback(null, currentBundle); // user certs cannot be renewed
if (!isExpiringSync(currentBundle.certFilePath, 24 * 30) && providerMatchesSync(domainObject, currentBundle.certFilePath, apiOptions)) return callback(null, currentBundle);
if (currentBundle.certFilePath.endsWith('.user.cert')) return callback(null, currentBundle, { renewed: false }); // user certs cannot be renewed
if (!isExpiringSync(currentBundle.certFilePath, 24 * 30) && providerMatchesSync(domainObject, currentBundle.certFilePath, apiOptions)) return callback(null, currentBundle, { renewed: false });
debug(`ensureCertificate: ${vhost} cert require renewal`);
} else {
debug(`ensureCertificate: ${vhost} cert does not exist`);
@@ -364,15 +346,28 @@ function ensureCertificate(vhost, domain, auditSource, callback) {
debug('ensureCertificate: getting certificate for %s with options %j', vhost, apiOptions);
api.getCertificate(vhost, domain, apiOptions, function (error, certFilePath, keyFilePath) {
debug(`ensureCertificate: error: ${error ? error.message : 'null'} cert: ${certFilePath}`);
eventlog.add(currentBundle ? eventlog.ACTION_CERTIFICATE_RENEWAL : eventlog.ACTION_CERTIFICATE_NEW, auditSource, { domain: vhost, errorMessage: error ? error.message : '' });
if (error && currentBundle && !isExpiringSync(currentBundle.certFilePath, 0)) {
debug('ensureCertificate: continue using existing bundle since renewal failed');
return callback(null, currentBundle, { renewed: false });
}
notifyCertChanged(vhost, function (error) {
if (error) return callback(error);
// if no cert was returned use fallback. the fallback/caas provider will not provide any for example
if (!certFilePath || !keyFilePath) return getFallbackCertificate(domain, callback);
if (certFilePath && keyFilePath) return callback(null, { certFilePath, keyFilePath }, { renewed: true });
callback(null, { certFilePath, keyFilePath });
debug(`ensureCertificate: renewal of ${vhost} failed. using fallback certificates for ${domain}`);
// if no cert was returned use fallback. the fallback/caas provider will not provide any for example
getFallbackCertificate(domain, function (error, bundle) {
if (error) return callback(error);
callback(null, bundle, { renewed: false });
});
});
});
});
@@ -399,9 +394,7 @@ function writeAdminNginxConfig(bundle, configFileName, vhost, callback) {
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, configFileName);
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) return callback(safe.error);
if (vhost) safe.fs.unlinkSync(path.join(paths.NGINX_APPCONFIG_DIR, 'admin.conf')); // remove legacy admin.conf. remove after 3.5
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) return callback(new BoxError(BoxError.FS_ERROR, safe.error));
reload(callback);
}
@@ -428,6 +421,8 @@ function writeAdminConfig(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`writeAdminConfig: writing admin config for ${domain}`);
domains.get(domain, function (error, domainObject) {
if (error) return callback(error);
@@ -449,6 +444,15 @@ function writeAppNginxConfig(app, bundle, callback) {
var sourceDir = path.resolve(__dirname, '..');
var endpoint = 'app';
let robotsTxtQuoted = null, hideHeaders = [], cspQuoted = null;
const reverseProxyConfig = app.reverseProxyConfig || {}; // some of our code uses fake app objects
if (reverseProxyConfig.robotsTxt) robotsTxtQuoted = JSON.stringify(app.reverseProxyConfig.robotsTxt);
if (reverseProxyConfig.csp) {
cspQuoted = `"${app.reverseProxyConfig.csp}"`;
hideHeaders = [ 'Content-Security-Policy' ];
if (reverseProxyConfig.csp.includes('frame-ancestors ')) hideHeaders.push('X-Frame-Options');
}
var data = {
sourceDir: sourceDir,
adminOrigin: settings.adminOrigin(),
@@ -458,16 +462,18 @@ function writeAppNginxConfig(app, bundle, callback) {
endpoint: endpoint,
certFilePath: bundle.certFilePath,
keyFilePath: bundle.keyFilePath,
robotsTxtQuoted: app.robotsTxt ? JSON.stringify(app.robotsTxt) : null
robotsTxtQuoted,
cspQuoted,
hideHeaders
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
debug('writing config for "%s" to %s with options %j', app.fqdn, nginxConfigFilename, data);
debug('writeAppNginxConfig: writing config for "%s" to %s with options %j', app.fqdn, nginxConfigFilename, data);
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) {
debug('Error creating nginx config for "%s" : %s', app.fqdn, safe.error.message);
return callback(safe.error);
return callback(new BoxError(BoxError.FS_ERROR, safe.error));
}
reload(callback);
@@ -487,7 +493,9 @@ function writeAppRedirectNginxConfig(app, fqdn, bundle, callback) {
endpoint: 'redirect',
certFilePath: bundle.certFilePath,
keyFilePath: bundle.keyFilePath,
robotsTxtQuoted: null
robotsTxtQuoted: null,
cspQuoted: null,
hideHeaders: []
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
@@ -497,7 +505,7 @@ function writeAppRedirectNginxConfig(app, fqdn, bundle, callback) {
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) {
debug('Error creating nginx redirect config for "%s" : %s', app.fqdn, safe.error.message);
return callback(safe.error);
return callback(new BoxError(BoxError.FS_ERROR, safe.error));
}
reload(callback);
@@ -584,14 +592,17 @@ function renewCerts(options, auditSource, progressCallback, callback) {
if (options.domain) appDomains = appDomains.filter(function (appDomain) { return appDomain.domain === options.domain; });
let progress = 1;
let progress = 1, renewed = [];
async.eachSeries(appDomains, function (appDomain, iteratorCallback) {
progressCallback({ percent: progress, message: `Renewing certs of ${appDomain.fqdn}` });
progress += Math.round(100/appDomains.length);
ensureCertificate(appDomain.fqdn, appDomain.domain, auditSource, function (error, bundle) {
ensureCertificate(appDomain.fqdn, appDomain.domain, auditSource, function (error, bundle, state) {
if (error) return iteratorCallback(error); // this can happen if cloudron is not setup yet
if (state.renewed) renewed.push(appDomain.fqdn);
// hack to check if the app's cert changed or not. this doesn't handle prod/staging le change since they use same file name
let currentNginxConfig = safe.fs.readFileSync(appDomain.nginxConfigFilename, 'utf8') || '';
if (currentNginxConfig.includes(bundle.certFilePath)) return iteratorCallback();
@@ -603,11 +614,19 @@ function renewCerts(options, auditSource, progressCallback, callback) {
if (appDomain.type === 'webadmin') configureFunc = writeAdminNginxConfig.bind(null, bundle, `${settings.adminFqdn()}.conf`, settings.adminFqdn());
else if (appDomain.type === 'main') configureFunc = writeAppNginxConfig.bind(null, appDomain.app, bundle);
else if (appDomain.type === 'alternate') configureFunc = writeAppRedirectNginxConfig.bind(null, appDomain.app, appDomain.fqdn, bundle);
else return iteratorCallback(new Error(`Unknown domain type for ${appDomain.fqdn}. This should never happen`));
else return iteratorCallback(new BoxError(BoxError.INTERNAL_ERROR, `Unknown domain type for ${appDomain.fqdn}. This should never happen`));
configureFunc(iteratorCallback);
});
}, callback);
}, function (error) {
if (error) return callback(error);
debug(`renewCerts: Renewed certs of ${JSON.stringify(renewed)}`);
if (renewed.length === 0) return callback(null);
// reload nginx if any certs were updated but the config was not rewritten
reload(callback);
});
});
}
@@ -619,26 +638,26 @@ function removeAppConfigs() {
}
}
function configureDefaultServer(callback) {
function writeDefaultConfig(callback) {
assert.strictEqual(typeof callback, 'function');
var certFilePath = path.join(paths.NGINX_CERT_DIR, 'default.cert');
var keyFilePath = path.join(paths.NGINX_CERT_DIR, 'default.key');
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
debug('configureDefaultServer: create new cert');
debug('writeDefaultConfig: create new cert');
var cn = 'cloudron-' + (new Date()).toISOString(); // randomize date a bit to keep firefox happy
if (!safe.child_process.execSync(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=${cn} -nodes`)) {
debug(`configureDefaultServer: could not generate certificate: ${safe.error.message}`);
return callback(safe.error);
debug(`writeDefaultConfig: could not generate certificate: ${safe.error.message}`);
return callback(new BoxError(BoxError.OPENSSL_ERROR, safe.error));
}
}
writeAdminNginxConfig({ certFilePath, keyFilePath }, constants.NGINX_DEFAULT_CONFIG_FILE_NAME, '', function (error) {
if (error) return callback(error);
debug('configureDefaultServer: done');
debug('writeDefaultConfig: done');
callback(null);
});
+8 -9
View File
@@ -12,14 +12,13 @@ var accesscontrol = require('../accesscontrol.js'),
assert = require('assert'),
BasicStrategy = require('passport-http').BasicStrategy,
BearerStrategy = require('passport-http-bearer').Strategy,
BoxError = require('../boxerror.js'),
clients = require('../clients.js'),
ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy,
ClientsError = clients.ClientsError,
HttpError = require('connect-lastmile').HttpError,
LocalStrategy = require('passport-local').Strategy,
passport = require('passport'),
users = require('../users.js'),
UsersError = users.UsersError;
users = require('../users.js');
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -42,16 +41,16 @@ function initialize(callback) {
passport.use(new LocalStrategy(function (username, password, callback) {
if (username.indexOf('@') === -1) {
users.verifyWithUsername(username, password, function (error, result) {
if (error && error.reason === UsersError.NOT_FOUND) return callback(null, false);
if (error && error.reason === UsersError.WRONG_PASSWORD) return callback(null, false);
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, false);
if (error && error.reason === BoxError.INVALID_CREDENTIALS) return callback(null, false);
if (error) return callback(error);
if (!result) return callback(null, false);
callback(null, result);
});
} else {
users.verifyWithEmail(username, password, function (error, result) {
if (error && error.reason === UsersError.NOT_FOUND) return callback(null, false);
if (error && error.reason === UsersError.WRONG_PASSWORD) return callback(null, false);
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, false);
if (error && error.reason === BoxError.INVALID_CREDENTIALS) return callback(null, false);
if (error) return callback(error);
if (!result) return callback(null, false);
callback(null, result);
@@ -62,7 +61,7 @@ function initialize(callback) {
// Used to authenticate a OAuth2 client which uses clientId and clientSecret in the Authorization header
passport.use(new BasicStrategy(function (clientId, clientSecret, callback) {
clients.get(clientId, function (error, client) {
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
if (client.clientSecret !== clientSecret) return callback(null, false);
callback(null, client);
@@ -72,7 +71,7 @@ function initialize(callback) {
// Used to authenticate a OAuth2 client which uses clientId and clientSecret in the request body (client_id, client_secret)
passport.use(new ClientPasswordStrategy(function (clientId, clientSecret, callback) {
clients.get(clientId, function(error, client) {
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, false);
if (error) { return callback(error); }
if (client.clientSecret !== clientSecret) { return callback(null, false); }
callback(null, client);
+62 -64
View File
@@ -7,6 +7,7 @@ exports = module.exports = {
installApp: installApp,
uninstallApp: uninstallApp,
restoreApp: restoreApp,
importApp: importApp,
backupApp: backupApp,
updateApp: updateApp,
getLogs: getLogs,
@@ -21,7 +22,7 @@ exports = module.exports = {
setMemoryLimit: setMemoryLimit,
setAutomaticBackup: setAutomaticBackup,
setAutomaticUpdate: setAutomaticUpdate,
setRobotsTxt: setRobotsTxt,
setReverseProxyConfig: setReverseProxyConfig,
setCertificate: setCertificate,
setDebugMode: setDebugMode,
setEnvironment: setEnvironment,
@@ -41,9 +42,9 @@ exports = module.exports = {
};
var apps = require('../apps.js'),
AppsError = apps.AppsError,
assert = require('assert'),
auditSource = require('../auditsource.js'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:routes/apps'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
@@ -51,30 +52,11 @@ var apps = require('../apps.js'),
util = require('util'),
WebSocket = require('ws');
function toHttpError(appError) {
switch (appError.reason) {
case AppsError.NOT_FOUND:
return new HttpError(404, appError);
case AppsError.ALREADY_EXISTS:
case AppsError.BAD_STATE:
return new HttpError(409, appError);
case AppsError.BAD_FIELD:
return new HttpError(400, appError);
case AppsError.PLAN_LIMIT:
return new HttpError(402, appError);
case AppsError.EXTERNAL_ERROR:
return new HttpError(424, appError);
case AppsError.INTERNAL_ERROR:
default:
return new HttpError(500, appError);
}
}
function getApp(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
apps.get(req.params.id, function (error, app) {
if (error) return next(toHttpError(error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, apps.removeInternalFields(app)));
});
@@ -84,7 +66,7 @@ function getApps(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
apps.getAllByUser(req.user, function (error, allApps) {
if (error) return next(toHttpError(error));
if (error) return next(BoxError.toHttpError(error));
allApps = allApps.map(apps.removeRestrictedFields);
@@ -96,7 +78,7 @@ function getAppIcon(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
apps.getIconPath(req.params.id, { original: req.query.original }, function (error, iconPath) {
if (error) return next(toHttpError(error));
if (error) return next(BoxError.toHttpError(error));
res.sendFile(iconPath);
});
@@ -121,9 +103,6 @@ function installApp(req, res, next) {
if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
if (data.backupId && typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be string or null'));
if (data.backupFormat && typeof data.backupFormat !== 'string') return next(new HttpError(400, 'backupFormat must be string or null'));
if ('label' in data && typeof data.label !== 'string') return next(new HttpError(400, 'label must be a string'));
// falsy values in cert and key unset the cert
@@ -140,8 +119,6 @@ function installApp(req, res, next) {
if (('debugMode' in data) && typeof data.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object'));
if (data.robotsTxt && typeof data.robotsTxt !== 'string') return next(new HttpError(400, 'robotsTxt must be a string'));
if ('alternateDomains' in data) {
if (!Array.isArray(data.alternateDomains)) return next(new HttpError(400, 'alternateDomains must be an array'));
if (data.alternateDomains.some(function (d) { return (typeof d.domain !== 'string' || typeof d.subdomain !== 'string'); })) return next(new HttpError(400, 'alternateDomains array must contain objects with domain and subdomain strings'));
@@ -157,7 +134,7 @@ function installApp(req, res, next) {
debug('Installing app :%j', data);
apps.install(data, req.user, auditSource.fromRequest(req), function (error, result) {
if (error) return next(toHttpError(error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { id: result.id, taskId: result.taskId }));
});
@@ -170,7 +147,7 @@ function setAccessRestriction(req, res, next) {
if (typeof req.body.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction must be an object'));
apps.setAccessRestriction(req.params.id, req.body.accessRestriction, auditSource.fromRequest(req), function (error) {
if (error) return next(toHttpError(error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
});
@@ -183,7 +160,7 @@ function setLabel(req, res, next) {
if (typeof req.body.label !== 'string') return next(new HttpError(400, 'label must be a string'));
apps.setLabel(req.params.id, req.body.label, auditSource.fromRequest(req), function (error) {
if (error) return next(toHttpError(error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
});
@@ -197,7 +174,7 @@ function setTags(req, res, next) {
if (req.body.tags.some((t) => typeof t !== 'string')) return next(new HttpError(400, 'tags array must contain strings'));
apps.setTags(req.params.id, req.body.tags, auditSource.fromRequest(req), function (error) {
if (error) return next(toHttpError(error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
});
@@ -210,7 +187,7 @@ function setIcon(req, res, next) {
if (req.body.icon !== null && typeof req.body.icon !== 'string') return next(new HttpError(400, 'icon is null or a base-64 image string'));
apps.setIcon(req.params.id, req.body.icon, auditSource.fromRequest(req), function (error) {
if (error) return next(toHttpError(error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
});
@@ -223,7 +200,7 @@ function setMemoryLimit(req, res, next) {
if (typeof req.body.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
apps.setMemoryLimit(req.params.id, req.body.memoryLimit, auditSource.fromRequest(req), function (error, result) {
if (error) return next(toHttpError(error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
@@ -236,7 +213,7 @@ function setAutomaticBackup(req, res, next) {
if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enable must be a boolean'));
apps.setAutomaticBackup(req.params.id, req.body.enable, auditSource.fromRequest(req), function (error) {
if (error) return next(toHttpError(error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
});
@@ -249,20 +226,22 @@ function setAutomaticUpdate(req, res, next) {
if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enable must be a boolean'));
apps.setAutomaticUpdate(req.params.id, req.body.enable, auditSource.fromRequest(req), function (error) {
if (error) return next(toHttpError(error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
});
}
function setRobotsTxt(req, res, next) {
function setReverseProxyConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (req.body.robotsTxt !== null && typeof req.body.robotsTxt !== 'string') return next(new HttpError(400, 'robotsTxt is not a string'));
apps.setRobotsTxt(req.params.id, req.body.robotsTxt, auditSource.fromRequest(req), function (error) {
if (error) return next(toHttpError(error));
if (req.body.csp !== null && typeof req.body.csp !== 'string') return next(new HttpError(400, 'csp is not a string'));
apps.setReverseProxyConfig(req.params.id, req.body, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
});
@@ -278,7 +257,7 @@ function setCertificate(req, res, next) {
if (!req.body.cert && req.body.key) return next(new HttpError(400, 'cert must be provided'));
apps.setCertificate(req.params.id, req.body, auditSource.fromRequest(req), function (error) {
if (error) return next(toHttpError(error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
});
@@ -292,7 +271,7 @@ function setEnvironment(req, res, next) {
if (Object.keys(req.body.env).some((key) => typeof req.body.env[key] !== 'string')) return next(new HttpError(400, 'env must contain values as strings'));
apps.setEnvironment(req.params.id, req.body.env, auditSource.fromRequest(req), function (error, result) {
if (error) return next(toHttpError(error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
@@ -305,7 +284,7 @@ function setDebugMode(req, res, next) {
if (req.body.debugMode !== null && typeof req.body.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object'));
apps.setDebugMode(req.params.id, req.body.debugMode, auditSource.fromRequest(req), function (error, result) {
if (error) return next(toHttpError(error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
@@ -316,9 +295,10 @@ function setMailbox(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
if (req.body.mailboxName !== null && typeof req.body.mailboxName !== 'string') return next(new HttpError(400, 'mailboxName must be a string'));
if (typeof req.body.mailboxDomain !== 'string') return next(new HttpError(400, 'mailboxDomain must be a string'));
apps.setMailbox(req.params.id, req.body.mailboxName, auditSource.fromRequest(req), function (error, result) {
if (error) return next(toHttpError(error));
apps.setMailbox(req.params.id, req.body.mailboxName, req.body.mailboxDomain, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
@@ -328,8 +308,7 @@ function setLocation(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (!req.body.location) return next(new HttpError(400, 'location is required'));
if (typeof req.body.location !== 'string') return next(new HttpError(400, 'location must be string'));
if (typeof req.body.location !== 'string') return next(new HttpError(400, 'location must be string')); // location may be an empty string
if (!req.body.domain) return next(new HttpError(400, 'domain is required'));
if (typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be string'));
@@ -343,7 +322,7 @@ function setLocation(req, res, next) {
if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean'));
apps.setLocation(req.params.id, req.body, auditSource.fromRequest(req), function (error, result) {
if (error) return next(toHttpError(error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
@@ -356,7 +335,7 @@ function setDataDir(req, res, next) {
if (req.body.dataDir !== null && typeof req.body.dataDir !== 'string') return next(new HttpError(400, 'dataDir must be a string'));
apps.setDataDir(req.params.id, req.body.dataDir, auditSource.fromRequest(req), function (error, result) {
if (error) return next(toHttpError(error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
@@ -384,7 +363,7 @@ function repairApp(req, res, next) {
if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean'));
apps.repair(req.params.id, data, auditSource.fromRequest(req), function (error, result) {
if (error) return next(toHttpError(error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
@@ -402,7 +381,25 @@ function restoreApp(req, res, next) {
if (data.backupId !== null && typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be string or null'));
apps.restore(req.params.id, data, auditSource.fromRequest(req), function (error, result) {
if (error) return next(toHttpError(error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function importApp(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
var data = req.body;
debug('Importing app id:%s', req.params.id);
if (typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be string'));
if (typeof data.backupFormat !== 'string') return next(new HttpError(400, 'backupFormat must be string'));
apps.importApp(req.params.id, data, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
@@ -424,7 +421,7 @@ function cloneApp(req, res, next) {
if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean'));
apps.clone(req.params.id, data, req.user, auditSource.fromRequest(req), function (error, result) {
if (error) return next(toHttpError(error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201, { id: result.id, taskId: result.taskId }));
});
@@ -436,7 +433,7 @@ function backupApp(req, res, next) {
debug('Backup app id:%s', req.params.id);
apps.backup(req.params.id, function (error, result) {
if (error) return next(toHttpError(error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
@@ -448,7 +445,7 @@ function uninstallApp(req, res, next) {
debug('Uninstalling app id:%s', req.params.id);
apps.uninstall(req.params.id, auditSource.fromRequest(req), function (error, result) {
if (error) return next(toHttpError(error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
@@ -460,7 +457,7 @@ function startApp(req, res, next) {
debug('Start app id:%s', req.params.id);
apps.start(req.params.id, function (error, result) {
if (error) return next(toHttpError(error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
@@ -472,7 +469,7 @@ function stopApp(req, res, next) {
debug('Stop app id:%s', req.params.id);
apps.stop(req.params.id, function (error, result) {
if (error) return next(toHttpError(error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
@@ -489,12 +486,13 @@ function updateApp(req, res, next) {
if ('appStoreId' in data && typeof data.appStoreId !== 'string') return next(new HttpError(400, 'appStoreId must be a string'));
if (!data.manifest && !data.appStoreId) return next(new HttpError(400, 'appStoreId or manifest is required'));
if ('skipBackup' in data && typeof data.skipBackup !== 'boolean') return next(new HttpError(400, 'skipBackup must be a boolean'));
if ('force' in data && typeof data.force !== 'boolean') return next(new HttpError(400, 'force must be a boolean'));
debug('Update app id:%s to manifest:%j', req.params.id, data.manifest);
apps.update(req.params.id, req.body, auditSource.fromRequest(req), function (error, result) {
if (error) return next(toHttpError(error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
@@ -520,7 +518,7 @@ function getLogStream(req, res, next) {
};
apps.getLogs(req.params.id, options, function (error, logStream) {
if (error) return next(toHttpError(error));
if (error) return next(BoxError.toHttpError(error));
res.writeHead(200, {
'Content-Type': 'text/event-stream',
@@ -555,7 +553,7 @@ function getLogs(req, res, next) {
};
apps.getLogs(req.params.id, options, function (error, logStream) {
if (error) return next(toHttpError(error));
if (error) return next(BoxError.toHttpError(error));
res.writeHead(200, {
'Content-Type': 'application/x-logs',
@@ -609,7 +607,7 @@ function exec(req, res, next) {
var tty = req.query.tty === 'true' ? true : false;
apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
if (error) return next(toHttpError(error));
if (error) return next(BoxError.toHttpError(error));
if (req.headers['upgrade'] !== 'tcp') return next(new HttpError(404, 'exec requires TCP upgrade'));
@@ -649,7 +647,7 @@ function execWebSocket(req, res, next) {
var tty = req.query.tty === 'true' ? true : false;
apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
if (error) return next(toHttpError(error));
if (error) return next(BoxError.toHttpError(error));
debug('Connected to terminal');
@@ -689,7 +687,7 @@ function listBackups(req, res, next) {
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
apps.listBackups(page, perPage, req.params.id, function (error, result) {
if (error) return next(toHttpError(error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { backups: result }));
});
@@ -704,7 +702,7 @@ function uploadFile(req, res, next) {
if (!req.files.file) return next(new HttpError(400, 'file must be provided as multipart'));
apps.uploadFile(req.params.id, req.files.file.path, req.query.file, function (error) {
if (error) return next(toHttpError(error));
if (error) return next(BoxError.toHttpError(error));
debug('uploadFile: done');
@@ -720,7 +718,7 @@ function downloadFile(req, res, next) {
if (typeof req.query.file !== 'string' || !req.query.file) return next(new HttpError(400, 'file query argument must be provided'));
apps.downloadFile(req.params.id, req.query.file, function (error, stream, info) {
if (error) return next(toHttpError(error));
if (error) return next(BoxError.toHttpError(error));
var headers = {
'Content-Type': 'application/octet-stream',
+6 -24
View File
@@ -10,8 +10,8 @@ exports = module.exports = {
};
var appstore = require('../appstore.js'),
AppstoreError = appstore.AppstoreError,
assert = require('assert'),
BoxError = require('../boxerror.js'),
custom = require('../custom.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
@@ -27,10 +27,7 @@ function isAppAllowed(appstoreId) {
function getApps(req, res, next) {
appstore.getApps(function (error, apps) {
if (error && error.reason === AppstoreError.INVALID_TOKEN) return next(new HttpError(402, error.message));
if (error && error.reason === AppstoreError.LICENSE_ERROR) return next(new HttpError(402, error.message));
if (error && error.reason === AppstoreError.NOT_REGISTERED) return next(new HttpError(412, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
let filteredApps = apps.filter((app) => !custom.spec().appstore.blacklist.includes(app.id));
if (custom.spec().appstore.whitelist) filteredApps = filteredApps.filter((app) => app.id in custom.spec().appstore.whitelist);
@@ -45,11 +42,7 @@ function getApp(req, res, next) {
if (!isAppAllowed(req.params.appstoreId)) return next(new HttpError(405, 'feature disabled by admin'));
appstore.getApp(req.params.appstoreId, function (error, app) {
if (error && error.reason === AppstoreError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppstoreError.INVALID_TOKEN) return next(new HttpError(402, error.message));
if (error && error.reason === AppstoreError.LICENSE_ERROR) return next(new HttpError(402, error.message));
if (error && error.reason === AppstoreError.NOT_REGISTERED) return next(new HttpError(412, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, app));
});
@@ -62,11 +55,7 @@ function getAppVersion(req, res, next) {
if (!isAppAllowed(req.params.appstoreId)) return next(new HttpError(405, 'feature disabled by admin'));
appstore.getAppVersion(req.params.appstoreId, req.params.versionId, function (error, manifest) {
if (error && error.reason === AppstoreError.NOT_FOUND) return next(new HttpError(404, 'No such app or version'));
if (error && error.reason === AppstoreError.INVALID_TOKEN) return next(new HttpError(402, error.message));
if (error && error.reason === AppstoreError.LICENSE_ERROR) return next(new HttpError(402, error.message));
if (error && error.reason === AppstoreError.NOT_REGISTERED) return next(new HttpError(412, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, manifest));
});
@@ -81,11 +70,7 @@ function registerCloudron(req, res, next) {
if (typeof req.body.signup !== 'boolean') return next(new HttpError(400, 'signup must be a boolean'));
appstore.registerWithLoginCredentials(req.body, function (error) {
if (error && error.reason === AppstoreError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === AppstoreError.ACCESS_DENIED) return next(new HttpError(412, error.message));
if (error && error.reason === AppstoreError.ALREADY_REGISTERED) return next(new HttpError(422, error.message));
if (error && error.reason === AppstoreError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201, {}));
});
@@ -95,10 +80,7 @@ function getSubscription(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
appstore.getSubscription(function (error, result) {
if (error && error.reason === AppstoreError.INVALID_TOKEN) return next(new HttpError(402, error.message));
if (error && error.reason === AppstoreError.NOT_REGISTERED) return next(new HttpError(412, error.message));
if (error && error.reason === AppstoreError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, result)); // { email, cloudronId, plan, cancel_at, status }
});
+4 -6
View File
@@ -9,7 +9,7 @@ exports = module.exports = {
let auditSource = require('../auditsource.js'),
backupdb = require('../backupdb.js'),
backups = require('../backups.js'),
BackupsError = require('../backups.js').BackupsError,
BoxError = require('../boxerror.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
@@ -21,8 +21,7 @@ function list(req, res, next) {
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
backups.getByStatePaged(backupdb.BACKUP_STATE_NORMAL, page, perPage, function (error, result) {
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { backups: result }));
});
@@ -30,8 +29,7 @@ function list(req, res, next) {
function startBackup(req, res, next) {
backups.startBackupTask(auditSource.fromRequest(req), function (error, taskId) {
if (error && error.reason === BackupsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId }));
});
@@ -39,7 +37,7 @@ function startBackup(req, res, next) {
function cleanup(req, res, next) {
backups.startCleanupTask(auditSource.fromRequest(req), function (error, taskId) {
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId }));
});
+16 -21
View File
@@ -12,8 +12,8 @@ exports = module.exports = {
};
var assert = require('assert'),
BoxError = require('../boxerror.js'),
clients = require('../clients.js'),
ClientsError = clients.ClientsError,
constants = require('../constants.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
@@ -29,9 +29,8 @@ function add(req, res, next) {
if (!validUrl.isWebUri(data.redirectURI)) return next(new HttpError(400, 'redirectURI must be a valid uri'));
clients.add(data.appId, clients.TYPE_EXTERNAL, data.redirectURI, data.scope, function (error, result) {
if (error && error.reason === ClientsError.INVALID_SCOPE) return next(new HttpError(400, error.message));
if (error && error.reason === ClientsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201, result));
});
}
@@ -40,8 +39,8 @@ function get(req, res, next) {
assert.strictEqual(typeof req.params.clientId, 'string');
clients.get(req.params.clientId, function (error, result) {
if (error && error.reason === ClientsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, result));
});
}
@@ -50,16 +49,14 @@ function del(req, res, next) {
assert.strictEqual(typeof req.params.clientId, 'string');
clients.get(req.params.clientId, function (error, result) {
if (error && error.reason === ClientsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
// we do not allow to use the REST API to delete addon clients
if (result.type !== clients.TYPE_EXTERNAL) return next(new HttpError(405, 'Deleting app addon clients is not allowed.'));
clients.del(req.params.clientId, function (error, result) {
if (error && error.reason === ClientsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === ClientsError.NOT_ALLOWED) return next(new HttpError(405, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204, result));
});
});
@@ -67,7 +64,8 @@ function del(req, res, next) {
function getAll(req, res, next) {
clients.getAll(function (error, result) {
if (error && error.reason !== ClientsError.NOT_FOUND) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { clients: result }));
});
}
@@ -83,8 +81,8 @@ function addToken(req, res, next) {
if ('name' in req.body && typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be a string'));
clients.addTokenByUserId(req.params.clientId, req.user.id, expiresAt, { name: req.body.name || '' }, function (error, result) {
if (error && error.reason === ClientsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201, { token: result }));
});
}
@@ -94,8 +92,7 @@ function getTokens(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
clients.getTokensByUserId(req.params.clientId, req.user.id, function (error, result) {
if (error && error.reason === ClientsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
result = result.map(clients.removeTokenPrivateFields);
@@ -108,8 +105,8 @@ function delTokens(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
clients.delTokensByUserId(req.params.clientId, req.user.id, function (error) {
if (error && error.reason === ClientsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204));
});
}
@@ -120,9 +117,7 @@ function delToken(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
clients.delToken(req.params.clientId, req.params.tokenId, function (error) {
if (error && error.reason === ClientsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === ClientsError.INVALID_TOKEN) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204));
});
+25 -21
View File
@@ -13,22 +13,23 @@ exports = module.exports = {
setDashboardAndMailDomain: setDashboardAndMailDomain,
prepareDashboardDomain: prepareDashboardDomain,
renewCerts: renewCerts,
getServerIp: getServerIp,
syncExternalLdap: syncExternalLdap
};
let assert = require('assert'),
async = require('async'),
auditSource = require('../auditsource.js'),
BoxError = require('../boxerror.js'),
cloudron = require('../cloudron.js'),
CloudronError = cloudron.CloudronError,
custom = require('../custom.js'),
disks = require('../disks.js'),
externalldap = require('../externalldap.js'),
externalLdap = require('../externalldap.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
sysinfo = require('../sysinfo.js'),
updater = require('../updater.js'),
updateChecker = require('../updatechecker.js'),
UpdaterError = require('../updater.js').UpdaterError;
updateChecker = require('../updatechecker.js');
function reboot(req, res, next) {
// Finish the request, to let the appstore know we triggered the reboot
@@ -39,7 +40,7 @@ function reboot(req, res, next) {
function isRebootRequired(req, res, next) {
cloudron.isRebootRequired(function (error, result) {
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { rebootRequired: result }));
});
@@ -47,7 +48,7 @@ function isRebootRequired(req, res, next) {
function getConfig(req, res, next) {
cloudron.getConfig(function (error, cloudronConfig) {
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, cloudronConfig));
});
@@ -55,7 +56,8 @@ function getConfig(req, res, next) {
function getDisks(req, res, next) {
disks.getDisks(function (error, result) {
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, result));
});
}
@@ -65,8 +67,8 @@ function update(req, res, next) {
// this only initiates the update, progress can be checked via the progress route
updater.updateToLatest(req.body, auditSource.fromRequest(req), function (error, taskId) {
if (error && error.reason === UpdaterError.ALREADY_UPTODATE) return next(new HttpError(422, error.message));
if (error && error.reason === UpdaterError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === BoxError.NOT_FOUND) return next(new HttpError(422, error.message));
if (error && error.reason === BoxError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, { taskId }));
@@ -102,8 +104,7 @@ function getLogs(req, res, next) {
};
cloudron.getLogs(req.params.unit, options, function (error, logStream) {
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(404, 'Invalid type'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
res.writeHead(200, {
'Content-Type': 'application/x-logs',
@@ -132,8 +133,7 @@ function getLogStream(req, res, next) {
};
cloudron.getLogs(req.params.unit, options, function (error, logStream) {
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(404, 'Invalid type'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
res.writeHead(200, {
'Content-Type': 'text/event-stream',
@@ -159,8 +159,7 @@ function setDashboardAndMailDomain(req, res, next) {
if (!custom.spec().domains.changeDashboardDomain) return next(new HttpError(405, 'feature disabled by admin'));
cloudron.setDashboardAndMailDomain(req.body.domain, auditSource.fromRequest(req), function (error) {
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204, {}));
});
@@ -172,9 +171,7 @@ function prepareDashboardDomain(req, res, next) {
if (!custom.spec().domains.changeDashboardDomain) return next(new HttpError(405, 'feature disabled by admin'));
cloudron.prepareDashboardDomain(req.body.domain, auditSource.fromRequest(req), function (error, taskId) {
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(404, error.message));
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId }));
});
@@ -182,17 +179,24 @@ function prepareDashboardDomain(req, res, next) {
function renewCerts(req, res, next) {
cloudron.renewCerts({ domain: req.body.domain || null }, auditSource.fromRequest(req), function (error, taskId) {
if (error && error.reason === CloudronError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId }));
});
}
function syncExternalLdap(req, res, next) {
externalldap.startSyncer(function (error, taskId) {
externalLdap.startSyncer(function (error, taskId) {
if (error) return next(new HttpError(500, error.message));
next(new HttpSuccess(202, { taskId: taskId }));
});
}
function getServerIp(req, res, next) {
sysinfo.getServerIp(function (error, ip) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { ip }));
});
}
+8 -20
View File
@@ -14,8 +14,8 @@ exports = module.exports = {
var assert = require('assert'),
auditSource = require('../auditsource.js'),
BoxError = require('../boxerror.js'),
domains = require('../domains.js'),
DomainsError = domains.DomainsError,
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
@@ -23,8 +23,7 @@ function verifyDomainLock(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
domains.get(req.params.domain, function (error, domain) {
if (error && error.reason === DomainsError.NOT_FOUND) return next(new HttpError(404, 'No such domain'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
if (domain.locked) return next(new HttpError(423, 'This domain is locked'));
@@ -68,10 +67,7 @@ function add(req, res, next) {
};
domains.add(req.body.domain, data, auditSource.fromRequest(req), function (error) {
if (error && error.reason === DomainsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === DomainsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === DomainsError.INVALID_PROVIDER) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201, { domain: req.body.domain, config: req.body.config }));
});
@@ -81,8 +77,7 @@ function get(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
domains.get(req.params.domain, function (error, result) {
if (error && error.reason === DomainsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, domains.removePrivateFields(result)));
});
@@ -134,10 +129,7 @@ function update(req, res, next) {
};
domains.update(req.params.domain, data, auditSource.fromRequest(req), function (error) {
if (error && error.reason === DomainsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === DomainsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === DomainsError.INVALID_PROVIDER) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204, {}));
});
@@ -147,9 +139,7 @@ function del(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
domains.del(req.params.domain, auditSource.fromRequest(req), function (error) {
if (error && error.reason === DomainsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === DomainsError.IN_USE) return next(new HttpError(409, 'Domain is still in use. Remove all apps and mailboxes using this domain'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204));
});
@@ -164,10 +154,8 @@ function checkDnsRecords(req, res, next) {
req.clearTimeout();
domains.checkDnsRecords(req.query.subdomain, req.params.domain, function (error, result) {
if (error && error.reason === DomainsError.NOT_FOUND) return next(new HttpError(404, error.message)); // domain (and not record!) not found
if (error && error.reason === DomainsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error && error.reason === DomainsError.ACCESS_DENIED) return next(new HttpSuccess(200, { error: { reason: error.reason, message: error.message }}));
if (error) return next(new HttpError(500, error));
if (error && error.reason === BoxError.ACCESS_DENIED) return next(new HttpSuccess(200, { error: { reason: error.reason, message: error.message }}));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { needsOverwrite: result.needsOverwrite }));
});
+4 -5
View File
@@ -5,15 +5,14 @@ exports = module.exports = {
list: list
};
var eventlog = require('../eventlog.js'),
EventLogError = eventlog.EventLogError,
var BoxError = require('../boxerror.js'),
eventlog = require('../eventlog.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
function get(req, res, next) {
eventlog.get(req.params.eventId, function (error, result) {
if (error && error.reason === EventLogError.NOT_FOUND) return next(new HttpError(404, 'no such eventlog'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { event: result }));
});
@@ -34,7 +33,7 @@ function list(req, res, next) {
if (req.query.action) actions.push(req.query.action);
eventlog.getAllPaged(actions, req.query.search || null, page, perPage, function (error, result) {
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { eventlogs: result }));
});
+8 -15
View File
@@ -10,10 +10,10 @@ exports = module.exports = {
};
var assert = require('assert'),
BoxError = require('../boxerror.js'),
groups = require('../groups.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
GroupsError = groups.GroupsError;
HttpSuccess = require('connect-lastmile').HttpSuccess;
function create(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
@@ -21,9 +21,7 @@ function create(req, res, next) {
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be string'));
groups.create(req.body.name, function (error, group) {
if (error && error.reason === GroupsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === GroupsError.ALREADY_EXISTS) return next(new HttpError(409, 'Already exists'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
var groupInfo = {
id: group.id,
@@ -38,8 +36,7 @@ function get(req, res, next) {
assert.strictEqual(typeof req.params.groupId, 'string');
groups.getWithMembers(req.params.groupId, function (error, result) {
if (error && error.reason === GroupsError.NOT_FOUND) return next(new HttpError(404, 'No such group'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, result));
});
@@ -52,8 +49,7 @@ function update(req, res, next) {
if ('name' in req.body && typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be a string'));
groups.update(req.params.groupId, req.body, function (error) {
if (error && error.reason === GroupsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { }));
});
@@ -66,8 +62,7 @@ function updateMembers(req, res, next) {
if (!Array.isArray(req.body.userIds)) return next(new HttpError(404, 'userIds must be an array'));
groups.setMembers(req.params.groupId, req.body.userIds, function (error) {
if (error && error.reason === GroupsError.NOT_FOUND) return next(new HttpError(404, 'Invalid group or user id'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { }));
});
@@ -75,7 +70,7 @@ function updateMembers(req, res, next) {
function list(req, res, next) {
groups.getAll(function (error, result) {
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { groups: result }));
});
@@ -85,9 +80,7 @@ function remove(req, res, next) {
assert.strictEqual(typeof req.params.groupId, 'string');
groups.remove(req.params.groupId, function (error) {
if (error && error.reason === GroupsError.NOT_FOUND) return next(new HttpError(404, 'Group not found'));
if (error && error.reason === GroupsError.NOT_ALLOWED) return next(new HttpError(409, 'Group deletion not allowed'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204));
});
+38 -65
View File
@@ -36,8 +36,8 @@ exports = module.exports = {
var assert = require('assert'),
auditSource = require('../auditsource.js'),
BoxError = require('../boxerror.js'),
mail = require('../mail.js'),
MailError = mail.MailError,
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
middleware = require('../middleware/index.js'),
@@ -49,8 +49,7 @@ function getDomain(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.getDomain(req.params.domain, function (error, result) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, mail.removePrivateFields(result)));
});
@@ -62,9 +61,7 @@ function addDomain(req, res, next) {
if (typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
mail.addDomain(req.body.domain, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.ALREADY_EXISTS) return next(new HttpError(409, 'domain already exists'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201, { domain: req.body.domain }));
});
@@ -92,9 +89,7 @@ function setDnsRecords(req, res, next) {
req.clearTimeout();
mail.setDnsRecords(req.params.domain, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201));
});
@@ -104,9 +99,7 @@ function removeDomain(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.removeDomain(req.params.domain, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.IN_USE) return next(new HttpError(409, 'Mail domain is still in use. Remove existing mailboxes'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204));
});
@@ -119,8 +112,7 @@ function getStatus(req, res, next) {
req.clearTimeout();
mail.getStatus(req.params.domain, function (error, records) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, records));
});
@@ -133,9 +125,7 @@ function setMailFromValidation(req, res, next) {
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled is required'));
mail.setMailFromValidation(req.params.domain, req.body.enabled, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202));
});
@@ -153,9 +143,7 @@ function setCatchAllAddress(req, res, next) {
}
mail.setCatchAllAddress(req.params.domain, req.body.addresses, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202));
});
@@ -173,9 +161,7 @@ function setMailRelay(req, res, next) {
if ('acceptSelfSignedCerts' in req.body && typeof req.body.acceptSelfSignedCerts !== 'boolean') return next(new HttpError(400, 'acceptSelfSignedCerts must be a boolean'));
mail.setMailRelay(req.params.domain, req.body, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202));
});
@@ -188,9 +174,7 @@ function setMailEnabled(req, res, next) {
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled is required'));
mail.setMailEnabled(req.params.domain, !!req.body.enabled, auditSource.fromRequest(req), function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202));
});
@@ -203,9 +187,7 @@ function sendTestMail(req, res, next) {
if (!req.body.to || typeof req.body.to !== 'string') return next(new HttpError(400, 'to must be a non-empty string'));
mail.sendTestMail(req.params.domain, req.body.to, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202));
});
@@ -214,9 +196,14 @@ function sendTestMail(req, res, next) {
function listMailboxes(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.listMailboxes(req.params.domain, function (error, result) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
mail.listMailboxes(req.params.domain, page, perPage, function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { mailboxes: result }));
});
@@ -227,8 +214,7 @@ function getMailbox(req, res, next) {
assert.strictEqual(typeof req.params.name, 'string');
mail.getMailbox(req.params.name, req.params.domain, function (error, result) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { mailbox: result }));
});
@@ -241,10 +227,7 @@ function addMailbox(req, res, next) {
if (typeof req.body.userId !== 'string') return next(new HttpError(400, 'userId must be a string'));
mail.addMailbox(req.body.name, req.params.domain, req.body.userId, auditSource.fromRequest(req), function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201, {}));
});
@@ -257,9 +240,7 @@ function updateMailbox(req, res, next) {
if (typeof req.body.userId !== 'string') return next(new HttpError(400, 'userId must be a string'));
mail.updateMailboxOwner(req.params.name, req.params.domain, req.body.userId, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204));
});
@@ -270,8 +251,7 @@ function removeMailbox(req, res, next) {
assert.strictEqual(typeof req.params.name, 'string');
mail.removeMailbox(req.params.name, req.params.domain, auditSource.fromRequest(req), function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201, {}));
});
@@ -280,9 +260,14 @@ function removeMailbox(req, res, next) {
function listAliases(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.listAliases(req.params.domain, function (error, result) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
mail.listAliases(req.params.domain, page, perPage, function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { aliases: result }));
});
@@ -293,8 +278,7 @@ function getAliases(req, res, next) {
assert.strictEqual(typeof req.params.name, 'string');
mail.getAliases(req.params.name, req.params.domain, function (error, result) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { aliases: result }));
});
@@ -312,10 +296,7 @@ function setAliases(req, res, next) {
}
mail.setAliases(req.params.name, req.params.domain, req.body.aliases, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202));
});
@@ -325,8 +306,7 @@ function getLists(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.getLists(req.params.domain, function (error, result) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { lists: result }));
});
@@ -337,8 +317,7 @@ function getList(req, res, next) {
assert.strictEqual(typeof req.params.name, 'string');
mail.getList(req.params.domain, req.params.name, function (error, result) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { list: result }));
});
@@ -357,10 +336,7 @@ function addList(req, res, next) {
}
mail.addList(req.body.name, req.params.domain, req.body.members, auditSource.fromRequest(req), function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.ALREADY_EXISTS) return next(new HttpError(409, 'list already exists'));
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201, {}));
});
@@ -378,9 +354,7 @@ function updateList(req, res, next) {
}
mail.updateList(req.params.name, req.params.domain, req.body.members, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204));
});
@@ -391,8 +365,7 @@ function removeList(req, res, next) {
assert.strictEqual(typeof req.params.name, 'string');
mail.removeList(req.params.name, req.params.domain, auditSource.fromRequest(req), function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204));
});
+5 -7
View File
@@ -8,17 +8,16 @@ exports = module.exports = {
};
let assert = require('assert'),
BoxError = require('../boxerror.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
notifications = require('../notifications.js'),
NotificationsError = notifications.NotificationsError;
notifications = require('../notifications.js');
function verifyOwnership(req, res, next) {
if (!req.params.notificationId) return next(); // skip for listing
notifications.get(req.params.notificationId, function (error, result) {
if (error && error.reason === NotificationsError.NOT_FOUND) return next(new HttpError(404, 'No such notification'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
if (result.userId !== req.user.id) return next(new HttpError(403, 'User is not owner'));
@@ -46,7 +45,7 @@ function list(req, res, next) {
else if (req.query.acknowledged) acknowledged = req.query.acknowledged === 'true' ? true : false;
notifications.getAllPaged(req.user.id, acknowledged, page, perPage, function (error, result) {
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { notifications: result }));
});
@@ -56,8 +55,7 @@ function ack(req, res, next) {
assert.strictEqual(typeof req.params.notificationId, 'string');
notifications.ack(req.params.notificationId, function (error) {
if (error && error.reason === NotificationsError.NOT_FOUND) return next(new HttpError(404, 'No such notification'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204, {}));
});
+51 -15
View File
@@ -21,11 +21,11 @@ exports = module.exports = {
var apps = require('../apps.js'),
assert = require('assert'),
async = require('async'),
authcodedb = require('../authcodedb.js'),
BoxError = require('../boxerror.js'),
clients = require('../clients'),
ClientsError = clients.ClientsError,
constants = require('../constants.js'),
DatabaseError = require('../databaseerror.js'),
debug = require('debug')('box:routes/oauth2'),
eventlog = require('../eventlog.js'),
hat = require('../hat.js'),
@@ -39,7 +39,6 @@ var apps = require('../apps.js'),
speakeasy = require('speakeasy'),
url = require('url'),
users = require('../users.js'),
UsersError = users.UsersError,
util = require('util'),
_ = require('underscore');
@@ -89,7 +88,7 @@ function initialize() {
// exchange authorization codes for access tokens. this is used by external oauth clients
gServer.exchange(oauth2orize.exchange.code(function (client, code, redirectURI, callback) {
authcodedb.get(code, function (error, authCode) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
if (client.id !== authCode.clientId) return callback(null, false);
@@ -255,7 +254,9 @@ function login(req, res) {
var failureQuery = querystring.stringify({ error: 'Invalid username or password', returnTo: returnTo });
passport.authenticate('local', {
failureRedirect: '/api/v1/session/login?' + failureQuery
})(req, res, function () {
})(req, res, function (error) {
if (error) return res.redirect('/api/v1/session/login?' + failureQuery); // on some exception in the handlers
if (!req.user.ghost && req.user.twoFactorAuthenticationEnabled) {
if (!req.body.totpToken) {
let failureQuery = querystring.stringify({ error: 'A 2FA token is required', returnTo: returnTo });
@@ -275,10 +276,45 @@ function login(req, res) {
// -> GET /api/v1/session/logout
function logout(req, res) {
req.logout();
function done() {
req.logout();
if (req.query && req.query.redirect) res.redirect(req.query.redirect);
else res.redirect('/');
if (req.query && req.query.redirect) res.redirect(req.query.redirect);
else res.redirect('/');
}
if (!req.query.all) return done();
// find and destroy all login sessions by this user - this got rather complex quickly
req.sessionStore.list(function (error, result) {
if (error) {
console.error('Error listing sessions', error);
return done();
}
// WARNING fix this if we change the storage backend - Great stuff!
var sessionIds = result.map(function(s) { return s.replace('.json', ''); });
async.each(sessionIds, function (id, callback) {
req.sessionStore.get(id, function (error, result) {
if (error) {
console.error(`Error getting session ${id}`, error);
return callback();
}
// ignore empty or non passport sessions
if (!result || !result.passport || !result.passport.user) return callback();
// not this user
if (result.passport.user !== req.user.id) return callback();
req.sessionStore.destroy(id, function (error) {
if (error) console.error(`Unable to destroy session ${id}`, error);
callback();
});
});
}, done);
});
}
// Form to enter email address to send a password reset request mail
@@ -302,7 +338,7 @@ function passwordResetRequest(req, res, next) {
debug('passwordResetRequest: email or username %s.', req.body.identifier);
users.resetPasswordByIdentifier(req.body.identifier, function (error) {
if (error && error.reason !== UsersError.NOT_FOUND) {
if (error && error.reason !== BoxError.NOT_FOUND) {
console.error(error);
return sendErrorPageOrRedirect(req, res, 'User not found');
}
@@ -356,9 +392,9 @@ function accountSetup(req, res, next) {
var data = _.pick(req.body, 'username', 'displayName');
users.update(userObject.id, data, auditSource(req), function (error) {
if (error && error.reason === UsersError.ALREADY_EXISTS) return renderAccountSetupSite(res, req, userObject, 'Username already exists');
if (error && error.reason === UsersError.BAD_FIELD) return renderAccountSetupSite(res, req, userObject, error.message);
if (error && error.reason === UsersError.NOT_FOUND) return renderAccountSetupSite(res, req, userObject, 'No such user');
if (error && error.reason === BoxError.ALREADY_EXISTS) return renderAccountSetupSite(res, req, userObject, 'Username already exists');
if (error && error.reason === BoxError.BAD_FIELD) return renderAccountSetupSite(res, req, userObject, error.message);
if (error && error.reason === BoxError.NOT_FOUND) return renderAccountSetupSite(res, req, userObject, 'No such user');
if (error) return next(new HttpError(500, error));
userObject.username = req.body.username;
@@ -366,7 +402,7 @@ function accountSetup(req, res, next) {
// setPassword clears the resetToken
users.setPassword(userObject.id, req.body.password, function (error) {
if (error && error.reason === UsersError.BAD_FIELD) return renderAccountSetupSite(res, req, userObject, error.message);
if (error && error.reason === BoxError.BAD_FIELD) return renderAccountSetupSite(res, req, userObject, error.message);
if (error) return next(new HttpError(500, error));
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
@@ -414,7 +450,7 @@ function passwordReset(req, res, next) {
// setPassword clears the resetToken
users.setPassword(userObject.id, req.body.password, function (error) {
if (error && error.reason === UsersError.BAD_FIELD) return next(new HttpError(406, error.message));
if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(406, error.message));
if (error) return next(new HttpError(500, error));
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
@@ -463,7 +499,7 @@ function authorization() {
debug('authorization: client %s with callback to %s.', clientId, redirectURI);
clients.get(clientId, function (error, client) {
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
// ignore the origin passed into form the client, but use the one from the clientdb
+6 -15
View File
@@ -11,10 +11,10 @@ exports = module.exports = {
var assert = require('assert'),
auditSource = require('../auditsource.js'),
BoxError = require('../boxerror.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
users = require('../users.js'),
UsersError = users.UsersError,
_ = require('underscore');
function get(req, res, next) {
@@ -43,10 +43,7 @@ function update(req, res, next) {
var data = _.pick(req.body, 'email', 'fallbackEmail', 'displayName');
users.update(req.user.id, data, auditSource.fromRequest(req), function (error) {
if (error && error.reason === UsersError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === UsersError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204));
});
@@ -59,9 +56,7 @@ function changePassword(req, res, next) {
if (typeof req.body.newPassword !== 'string') return next(new HttpError(400, 'newPassword must be a string'));
users.setPassword(req.user.id, req.body.newPassword, function (error) {
if (error && error.reason === UsersError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204));
});
@@ -71,8 +66,7 @@ function setTwoFactorAuthenticationSecret(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
users.setTwoFactorAuthenticationSecret(req.user.id, function (error, result) {
if (error && error.reason === UsersError.ALREADY_EXISTS) return next(new HttpError(409, 'TwoFactor Authentication is enabled, disable first'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201, { secret: result.secret, qrcode: result.qrcode }));
});
@@ -85,10 +79,7 @@ function enableTwoFactorAuthentication(req, res, next) {
if (!req.body.totpToken || typeof req.body.totpToken !== 'string') return next(new HttpError(400, 'totpToken must be a nonempty string'));
users.enableTwoFactorAuthentication(req.user.id, req.body.totpToken, function (error) {
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
if (error && error.reason === UsersError.BAD_TOKEN) return next(new HttpError(412, 'Invalid token'));
if (error && error.reason === UsersError.ALREADY_EXISTS) return next(new HttpError(409, 'TwoFactor Authentication is already enabled'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {}));
});
@@ -98,7 +89,7 @@ function disableTwoFactorAuthentication(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
users.disableTwoFactorAuthentication(req.user.id, function (error) {
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {}));
});
+13 -20
View File
@@ -9,19 +9,19 @@ exports = module.exports = {
};
var assert = require('assert'),
auditSource = require('../auditsource'),
auditSource = require('../auditsource.js'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:routes/setup'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
provision = require('../provision.js'),
ProvisionError = require('../provision.js').ProvisionError,
sysinfo = require('../sysinfo.js'),
settings = require('../settings.js'),
superagent = require('superagent');
function providerTokenAuth(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (sysinfo.provider() === 'ami') {
if (settings.provider() === 'ami') {
if (typeof req.body.providerToken !== 'string' || !req.body.providerToken) return next(new HttpError(400, 'providerToken must be a non empty string'));
superagent.get('http://169.254.169.254/latest/meta-data/instance-id').timeout(30 * 1000).end(function (error, result) {
@@ -53,16 +53,13 @@ function setup(req, res, next) {
if ('tlsConfig' in dnsConfig && typeof dnsConfig.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be an object'));
if (dnsConfig.tlsConfig && (!dnsConfig.tlsConfig.provider || typeof dnsConfig.tlsConfig.provider !== 'string')) return next(new HttpError(400, 'tlsConfig.provider must be a string'));
if ('backupConfig' in req.body && typeof req.body.backupConfig !== 'object') return next(new HttpError(400, 'backupConfig must be an object'));
if ('sysinfoConfig' in req.body && typeof req.body.sysinfoConfig !== 'object') return next(new HttpError(400, 'sysinfoConfig must be an object'));
// it can take sometime to setup DNS, register cloudron
req.clearTimeout();
provision.setup(dnsConfig, req.body.backupConfig || null, auditSource.fromRequest(req), function (error) {
if (error && error.reason === ProvisionError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === ProvisionError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === ProvisionError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
provision.setup(dnsConfig, req.body.sysinfoConfig || { provider: 'generic' }, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
});
@@ -85,9 +82,7 @@ function activate(req, res, next) {
debug('activate: username:%s ip:%s', username, ip);
provision.activate(username, password, email, displayName, ip, auditSource.fromRequest(req), function (error, info) {
if (error && error.reason === ProvisionError.ALREADY_PROVISIONED) return next(new HttpError(409, 'Already setup'));
if (error && error.reason === ProvisionError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201, info));
});
@@ -107,12 +102,10 @@ function restore(req, res, next) {
if (typeof req.body.backupId !== 'string') return next(new HttpError(400, 'backupId must be a string or null'));
if (typeof req.body.version !== 'string') return next(new HttpError(400, 'version must be a string'));
provision.restore(backupConfig, req.body.backupId, req.body.version, auditSource.fromRequest(req), function (error) {
if (error && error.reason === ProvisionError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === ProvisionError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === ProvisionError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === ProvisionError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
if ('sysinfoConfig' in req.body && typeof req.body.sysinfoConfig !== 'object') return next(new HttpError(400, 'sysinfoConfig must be an object'));
provision.restore(backupConfig, req.body.backupId, req.body.version, req.body.sysinfoConfig || { provider: 'generic' }, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
});
@@ -120,7 +113,7 @@ function restore(req, res, next) {
function getStatus(req, res, next) {
provision.getStatus(function (error, status) {
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, status));
});
+7 -11
View File
@@ -10,8 +10,8 @@ exports = module.exports = {
};
var addons = require('../addons.js'),
AddonsError = addons.AddonsError,
assert = require('assert'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:routes/addons'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
@@ -20,7 +20,7 @@ function getAll(req, res, next) {
req.clearTimeout(); // can take a while to get status of all services
addons.getServices(function (error, result) {
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { services: result }));
});
@@ -30,8 +30,7 @@ function get(req, res, next) {
assert.strictEqual(typeof req.params.service, 'string');
addons.getService(req.params.service, function (error, result) {
if (error && error.reason === AddonsError.NOT_FOUND) return next(new HttpError(404, 'No such service'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { service: result }));
});
@@ -48,8 +47,7 @@ function configure(req, res, next) {
};
addons.configureService(req.params.service, data, function (error) {
if (error && error.reason === AddonsError.NOT_FOUND) return next(new HttpError(404, 'No such service'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {}));
});
@@ -70,8 +68,7 @@ function getLogs(req, res, next) {
};
addons.getServiceLogs(req.params.service, options, function (error, logStream) {
if (error && error.reason === AddonsError.NOT_FOUND) return next(new HttpError(404, 'No such service'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
res.writeHead(200, {
'Content-Type': 'application/x-logs',
@@ -103,8 +100,7 @@ function getLogStream(req, res, next) {
};
addons.getServiceLogs(req.params.service, options, function (error, logStream) {
if (error && error.reason === AddonsError.NOT_FOUND) return next(new HttpError(404, 'No such service'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
res.writeHead(200, {
'Content-Type': 'text/event-stream',
@@ -130,7 +126,7 @@ function restart(req, res, next) {
debug(`Restarting service ${req.params.service}`);
addons.restartService(req.params.service, function (error) {
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {}));
});
+66 -45
View File
@@ -9,18 +9,18 @@ exports = module.exports = {
var assert = require('assert'),
backups = require('../backups.js'),
BoxError = require('../boxerror.js'),
custom = require('../custom.js'),
docker = require('../docker.js'),
BoxError = require('../boxerror.js'),
externalLdap = require('../externalldap.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
safe = require('safetydance'),
settings = require('../settings.js'),
SettingsError = settings.SettingsError;
settings = require('../settings.js');
function getAppAutoupdatePattern(req, res, next) {
settings.getAppAutoupdatePattern(function (error, pattern) {
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { pattern: pattern }));
});
@@ -32,8 +32,7 @@ function setAppAutoupdatePattern(req, res, next) {
if (typeof req.body.pattern !== 'string') return next(new HttpError(400, 'pattern is required'));
settings.setAppAutoupdatePattern(req.body.pattern, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
});
@@ -41,7 +40,7 @@ function setAppAutoupdatePattern(req, res, next) {
function getBoxAutoupdatePattern(req, res, next) {
settings.getBoxAutoupdatePattern(function (error, pattern) {
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { pattern: pattern }));
});
@@ -53,8 +52,7 @@ function setBoxAutoupdatePattern(req, res, next) {
if (typeof req.body.pattern !== 'string') return next(new HttpError(400, 'pattern is required'));
settings.setBoxAutoupdatePattern(req.body.pattern, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
});
@@ -66,8 +64,7 @@ function setCloudronName(req, res, next) {
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name is required'));
settings.setCloudronName(req.body.name, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {}));
});
@@ -75,7 +72,7 @@ function setCloudronName(req, res, next) {
function getCloudronName(req, res, next) {
settings.getCloudronName(function (error, name) {
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { name: name }));
});
@@ -83,7 +80,7 @@ function getCloudronName(req, res, next) {
function getTimeZone(req, res, next) {
settings.getTimeZone(function (error, tz) {
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { timeZone: tz }));
});
@@ -95,8 +92,7 @@ function setTimeZone(req, res, next) {
if (typeof req.body.timeZone !== 'string') return next(new HttpError(400, 'timeZone is required'));
settings.setTimeZone(req.body.timeZone, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
});
@@ -109,7 +105,7 @@ function setCloudronAvatar(req, res, next) {
var avatar = safe.fs.readFileSync(req.files.avatar.path);
settings.setCloudronAvatar(avatar, function (error) {
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {}));
});
@@ -117,7 +113,7 @@ function setCloudronAvatar(req, res, next) {
function getCloudronAvatar(req, res, next) {
settings.getCloudronAvatar(function (error, avatar) {
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
// avoid caching the avatar on the client to see avatar changes immediately
res.set('Cache-Control', 'no-cache');
@@ -129,7 +125,7 @@ function getCloudronAvatar(req, res, next) {
function getBackupConfig(req, res, next) {
settings.getBackupConfig(function (error, backupConfig) {
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
// always send provider as it is used by the UI to figure if backups are disabled ('noop' backend)
if (!custom.spec().backups.configurable) {
@@ -160,9 +156,7 @@ function setBackupConfig(req, res, next) {
req.clearTimeout();
settings.setBackupConfig(req.body, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === SettingsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
});
@@ -170,7 +164,7 @@ function setBackupConfig(req, res, next) {
function getPlatformConfig(req, res, next) {
settings.getPlatformConfig(function (error, config) {
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, config));
});
@@ -188,9 +182,7 @@ function setPlatformConfig(req, res, next) {
}
settings.setPlatformConfig(req.body, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === SettingsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
});
@@ -198,26 +190,25 @@ function setPlatformConfig(req, res, next) {
function getExternalLdapConfig(req, res, next) {
settings.getExternalLdapConfig(function (error, config) {
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, config));
next(new HttpSuccess(200, externalLdap.removePrivateFields(config)));
});
}
function setExternalLdapConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled must be a boolean'));
if (typeof req.body.url !== 'string' || req.body.url === '') return next(new HttpError(400, 'url must be a non empty string'));
if (typeof req.body.baseDn !== 'string' || req.body.baseDn === '') return next(new HttpError(400, 'baseDn must be a non empty string'));
if (typeof req.body.filter !== 'string' || req.body.filter === '') return next(new HttpError(400, 'filter must be a non empty string'));
if ('bindDn' in req.body && (typeof req.body.bindDn !== 'string' || req.body.bindDn === '')) return next(new HttpError(400, 'bindDn must be a non empty string'));
if (!req.body.provider || typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider must be a string'));
if ('url' in req.body && typeof req.body.url !== 'string') return next(new HttpError(400, 'url must be a string'));
if ('baseDn' in req.body && typeof req.body.baseDn !== 'string') return next(new HttpError(400, 'baseDn must be a string'));
if ('usernameField' in req.body && typeof req.body.usernameField !== 'string') return next(new HttpError(400, 'usernameField must be a string'));
if ('filter' in req.body && typeof req.body.filter !== 'string') return next(new HttpError(400, 'filter must be a string'));
if ('bindDn' in req.body && typeof req.body.bindDn !== 'string') return next(new HttpError(400, 'bindDn must be a non empty string'));
if ('bindPassword' in req.body && typeof req.body.bindPassword !== 'string') return next(new HttpError(400, 'bindPassword must be a string'));
settings.setExternalLdapConfig(req.body, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === SettingsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
});
@@ -225,7 +216,7 @@ function setExternalLdapConfig(req, res, next) {
function getDynamicDnsConfig(req, res, next) {
settings.getDynamicDnsConfig(function (error, enabled) {
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { enabled: enabled }));
});
@@ -239,8 +230,7 @@ function setDynamicDnsConfig(req, res, next) {
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled boolean is required'));
settings.setDynamicDnsConfig(req.body.enabled, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
});
@@ -248,7 +238,7 @@ function setDynamicDnsConfig(req, res, next) {
function getUnstableAppsConfig(req, res, next) {
settings.getUnstableAppsConfig(function (error, enabled) {
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { enabled: enabled }));
});
@@ -260,28 +250,55 @@ function setUnstableAppsConfig(req, res, next) {
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled boolean is required'));
settings.setUnstableAppsConfig(req.body.enabled, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
});
}
function getRegistryConfig(req, res, next) {
settings.getRegistryConfig(function (error, registryConfig) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, docker.removePrivateFields(registryConfig)));
});
}
function setRegistryConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.serveraddress !== 'string') return next(new HttpError(400, 'serveraddress is required'));
if (typeof req.body.serverAddress !== 'string') return next(new HttpError(400, 'serverAddress is required'));
if ('username' in req.body && typeof req.body.username !== 'string') return next(new HttpError(400, 'username is required'));
if ('email' in req.body && typeof req.body.email !== 'string') return next(new HttpError(400, 'email is required'));
if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password is required'));
docker.setRegistryConfig(req.body, function (error) {
if (error && error.reason === BoxError.ACCESS_DENIED) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
settings.setRegistryConfig(req.body, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200));
});
}
function getSysinfoConfig(req, res, next) {
settings.getSysinfoConfig(function (error, sysinfoConfig) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, sysinfoConfig));
});
}
function setSysinfoConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.provider || typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
settings.setSysinfoConfig(req.body, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
});
}
function get(req, res, next) {
assert.strictEqual(typeof req.params.setting, 'string');
@@ -291,6 +308,8 @@ function get(req, res, next) {
case settings.PLATFORM_CONFIG_KEY: return getPlatformConfig(req, res, next);
case settings.EXTERNAL_LDAP_KEY: return getExternalLdapConfig(req, res, next);
case settings.UNSTABLE_APPS_KEY: return getUnstableAppsConfig(req, res, next);
case settings.REGISTRY_CONFIG_KEY: return getRegistryConfig(req, res, next);
case settings.SYSINFO_CONFIG_KEY: return getSysinfoConfig(req, res, next);
case settings.APP_AUTOUPDATE_PATTERN_KEY: return getAppAutoupdatePattern(req, res, next);
case settings.BOX_AUTOUPDATE_PATTERN_KEY: return getBoxAutoupdatePattern(req, res, next);
@@ -312,6 +331,8 @@ function set(req, res, next) {
case settings.PLATFORM_CONFIG_KEY: return setPlatformConfig(req, res, next);
case settings.EXTERNAL_LDAP_KEY: return setExternalLdapConfig(req, res, next);
case settings.UNSTABLE_APPS_KEY: return setUnstableAppsConfig(req, res, next);
case settings.REGISTRY_CONFIG_KEY: return setRegistryConfig(req, res, next);
case settings.SYSINFO_CONFIG_KEY: return setSysinfoConfig(req, res, next);
case settings.APP_AUTOUPDATE_PATTERN_KEY: return setAppAutoupdatePattern(req, res, next);
case settings.BOX_AUTOUPDATE_PATTERN_KEY: return setBoxAutoupdatePattern(req, res, next);
+3 -2
View File
@@ -20,13 +20,14 @@ function createTicket(req, res, next) {
if (!custom.spec().support.submitTickets) return next(new HttpError(405, 'feature disabled by admin'));
const VALID_TYPES = [ 'feedback', 'ticket', 'app_missing', 'app_error', 'upgrade_request' ];
const VALID_TYPES = [ 'feedback', 'ticket', 'app_missing', 'app_error', 'upgrade_request', 'email_error' ];
if (typeof req.body.type !== 'string' || !req.body.type) return next(new HttpError(400, 'type must be string'));
if (VALID_TYPES.indexOf(req.body.type) === -1) return next(new HttpError(400, 'unknown type'));
if (typeof req.body.subject !== 'string' || !req.body.subject) return next(new HttpError(400, 'subject must be string'));
if (typeof req.body.description !== 'string' || !req.body.description) return next(new HttpError(400, 'description must be string'));
if (req.body.appId && typeof req.body.appId !== 'string') return next(new HttpError(400, 'appId must be string'));
if (req.body.altEmail && typeof req.body.altEmail !== 'string') return next(new HttpError(400, 'altEmail must be string'));
appstore.createTicket(_.extend({ }, req.body, { email: req.user.email, displayName: req.user.displayName }), function (error) {
if (error) return next(new HttpError(503, `Error contacting cloudron.io: ${error.message}. Please email ${custom.spec().support.email}`));
@@ -43,7 +44,7 @@ function enableRemoteSupport(req, res, next) {
if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enabled is required'));
support.enableRemoteSupport(req.body.enable, function (error) {
if (error) return next(new HttpError(500, error));
if (error) return next(new HttpError(503, 'Error enabling remote support. Try running "cloudron-support --enable-ssh" on the server'));
next(new HttpSuccess(202, {}));
});
+6 -11
View File
@@ -10,18 +10,16 @@ exports = module.exports = {
};
let assert = require('assert'),
BoxError = require('../boxerror.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
TaskError = require('../tasks.js').TaskError,
tasks = require('../tasks.js');
function stopTask(req, res, next) {
assert.strictEqual(typeof req.params.taskId, 'string');
tasks.stopTask(req.params.taskId, function (error) {
if (error && error.reason === TaskError.NOT_FOUND) return next(new HttpError(404, 'No such task'));
if (error && error.reason === TaskError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204, {}));
});
@@ -31,8 +29,7 @@ function get(req, res, next) {
assert.strictEqual(typeof req.params.taskId, 'string');
tasks.get(req.params.taskId, function (error, task) {
if (error && error.reason === TaskError.NOT_FOUND) return next(new HttpError(404, 'No such task'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, tasks.removePrivateFields(task)));
});
@@ -48,7 +45,7 @@ function list(req, res, next) {
if (req.query.type && typeof req.query.type !== 'string') return next(new HttpError(400, 'type must be a string'));
tasks.listByTypePaged(req.query.type || null, page, perPage, function (error, result) {
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
result = result.map(tasks.removePrivateFields);
@@ -69,8 +66,7 @@ function getLogs(req, res, next) {
};
tasks.getLogs(req.params.taskId, options, function (error, logStream) {
if (error && error.reason === TaskError.NOT_FOUND) return next(new HttpError(404, 'No such task'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
res.writeHead(200, {
'Content-Type': 'application/x-logs',
@@ -100,8 +96,7 @@ function getLogStream(req, res, next) {
};
tasks.getLogs(req.params.taskId, options, function (error, logStream) {
if (error && error.reason === TaskError.NOT_FOUND) return next(new HttpError(404, 'No such task'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
res.writeHead(200, {
'Content-Type': 'text/event-stream',
+43 -20
View File
@@ -211,7 +211,7 @@ function startBox(done) {
token_1 = hat(8 * 32);
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add({ id: 'tid-1', accessToken: token_1, identifier: user_1_id, clientId: 'cid-sdk', expires: Date.now() + 1000000, scope: 'apps', name: '' }, callback); // cid-sdk means we don't need to send password
tokendb.add({ id: 'tid-1', accessToken: token_1, identifier: user_1_id, clientId: clients.ID_SDK, expires: Date.now() + 1000000, scope: 'apps', name: '' }, callback);
});
},
@@ -330,7 +330,7 @@ describe('App API', function () {
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: 'some', accessRestriction: null, domain: 'doesnotexist.com' })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('No such domain');
done();
});
@@ -432,7 +432,7 @@ describe('App API', function () {
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, domain: DOMAIN_0.domain, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(424);
expect(res.statusCode).to.equal(402);
expect(fake1.isDone()).to.be.ok();
done();
});
@@ -480,6 +480,7 @@ describe('App API', function () {
expect(res.body.id).to.eql(APP_ID);
expect(res.body.installationState).to.be.ok();
expect(res.body.mailboxName).to.be(APP_LOCATION + '.app');
expect(res.body.mailboxDomain).to.be(DOMAIN_0.domain);
done();
});
});
@@ -1040,18 +1041,9 @@ describe('App API', function () {
});
});
describe('configure robotsTxt', function () {
it('fails with missing robotsTxt', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/robots_txt')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
describe('configure reverseProxy - robotsTxt', function () {
it('fails with bad robotsTxt', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/robots_txt')
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/reverse_proxy')
.query({ access_token: token })
.send({ robotsTxt: 34 })
.end(function (err, res) {
@@ -1061,9 +1053,9 @@ describe('App API', function () {
});
it('can set robotsTxt', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/robots_txt')
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/reverse_proxy')
.query({ access_token: token })
.send({ robotsTxt: 'any string is good' })
.send({ robotsTxt: 'any string is good', csp: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
done();
@@ -1071,9 +1063,39 @@ describe('App API', function () {
});
it('can reset robotsTxt', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/robots_txt')
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/reverse_proxy')
.query({ access_token: token })
.send({ robotsTxt: null })
.send({ robotsTxt: null, csp: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
done();
});
});
it('fails with bad csp', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/reverse_proxy')
.query({ access_token: token })
.send({ robotsTxt: null, csp: 34 })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('can set frame-ancestors', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/reverse_proxy')
.query({ access_token: token })
.send({ robotsTxt: null, csp: 'frame-ancestors \'self\'' })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
done();
});
});
it('can reset frame-ancestors', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/reverse_proxy')
.query({ access_token: token })
.send({ robotsTxt: null, csp: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
done();
@@ -1132,6 +1154,7 @@ describe('App API', function () {
if (error) return done(error);
expect(app.mailboxName).to.be(APP_LOCATION_NEW + '.app'); // must follow location change
expect(app.mailboxDomain).to.be(DOMAIN_0.domain);
docker.getContainer(app.containerId).inspect(function (error, data) {
expect(error).to.not.be.ok();
@@ -1343,7 +1366,7 @@ describe('App API', function () {
it('can set mailbox', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/mailbox')
.query({ access_token: token })
.send({ mailboxName: 'genos' })
.send({ mailboxName: 'genos', mailboxDomain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
taskId = res.body.taskId;
@@ -1371,7 +1394,7 @@ describe('App API', function () {
it('can reset mailbox', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/mailbox')
.query({ access_token: token })
.send({ mailboxName: null })
.send({ mailboxName: null, mailboxDomain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
taskId = res.body.taskId;
+4 -4
View File
@@ -63,7 +63,7 @@ describe('Appstore Apps API', function () {
superagent.get(SERVER_URL + '/api/v1/appstore/apps')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(412); // not registered yet
expect(result.statusCode).to.equal(402); // not registered yet; invalid credentials
done();
});
});
@@ -72,7 +72,7 @@ describe('Appstore Apps API', function () {
superagent.get(SERVER_URL + '/api/v1/appstore/apps/org.wordpress.cloudronapp')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(412); // not registered yet
expect(result.statusCode).to.equal(402); // not registered yet; invalid credentials
done();
});
});
@@ -99,7 +99,7 @@ describe('Appstore Apps API', function () {
it('can list apps', function (done) {
var scope1 = nock(settings.apiServerOrigin())
.get(`/api/v1/apps?accessToken=CLOUDRON_TOKEN&boxVersion=${constants.VERSION}&unstable=false`, () => true)
.get(`/api/v1/apps?accessToken=CLOUDRON_TOKEN&boxVersion=${constants.VERSION}&unstable=true`, () => true)
.reply(200, { apps: [] });
superagent.get(SERVER_URL + '/api/v1/appstore/apps')
@@ -170,7 +170,7 @@ describe('Subscription API - no signup', function () {
.send({ email: 'test@cloudron.io', password: 'secret', signup: false })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(422);
expect(result.statusCode).to.equal(409);
done();
});
});
-48
View File
@@ -245,52 +245,4 @@ describe('Developer API', function () {
});
});
});
describe('sdk tokens are valid without password checks', function () {
var token_normal, token_sdk;
before(function (done) {
async.series([
setup,
function (callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
token_normal = result.body.accessToken;
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME, password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(new Date(result.body.expires).toString()).to.not.be('Invalid Date');
expect(result.body.accessToken).to.be.a('string');
token_sdk = result.body.accessToken;
callback();
});
});
},
], done);
});
after(cleanup);
it('fails with non sdk token', function (done) {
superagent.post(SERVER_URL + '/api/v1/profile/password').query({ access_token: token_normal }).send({ newPassword: 'Some?$123' }).end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/profile/password').query({ access_token: token_sdk }).send({ newPassword: 'Some?$123' }).end(function (error, result) {
expect(result.statusCode).to.equal(204);
done();
});
});
});
});
+12 -30
View File
@@ -15,10 +15,10 @@ exports = module.exports = {
var assert = require('assert'),
auditSource = require('../auditsource.js'),
BoxError = require('../boxerror.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
users = require('../users.js'),
UsersError = users.UsersError;
users = require('../users.js');
function create(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
@@ -35,9 +35,7 @@ function create(req, res, next) {
var displayName = req.body.displayName || '';
users.create(username, password, email, displayName, { invitor: req.user, admin: req.body.admin }, auditSource.fromRequest(req), function (error, user) {
if (error && error.reason === UsersError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === UsersError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
var userInfo = {
id: user.id,
@@ -72,10 +70,7 @@ function update(req, res, next) {
if ('active' in req.body && typeof req.body.active !== 'boolean') return next(new HttpError(400, 'active must be a boolean'));
users.update(req.params.userId, req.body, auditSource.fromRequest(req), function (error) {
if (error && error.reason === UsersError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === UsersError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204));
});
@@ -91,7 +86,7 @@ function list(req, res, next) {
if (req.query.search && typeof req.query.search !== 'string') return next(new HttpError(400, 'search must be a string'));
users.getAllPaged(req.query.search || null, page, perPage, function (error, results) {
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
results = results.map(users.removeRestrictedFields);
@@ -104,8 +99,7 @@ function get(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
users.get(req.params.userId, function (error, result) {
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, users.removePrivateFields(result)));
});
@@ -117,9 +111,7 @@ function remove(req, res, next) {
if (req.user.id === req.params.userId) return next(new HttpError(409, 'Not allowed to remove yourself.'));
users.remove(req.params.userId, auditSource.fromRequest(req), function (error) {
if (error && error.reason === UsersError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204));
});
@@ -128,14 +120,10 @@ function remove(req, res, next) {
function verifyPassword(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (req.authInfo.skipPasswordVerification) return next(); // using an 'sdk' token we skip password checks
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'API call requires user password'));
users.verifyWithUsername(req.user.username, req.body.password, function (error) {
if (error && error.reason === UsersError.WRONG_PASSWORD) return next(new HttpError(412, 'Password incorrect'));
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
req.body.password = '<redacted>'; // this will prevent logs from displaying plain text password
@@ -147,8 +135,7 @@ function createInvite(req, res, next) {
assert.strictEqual(typeof req.params.userId, 'string');
users.createInvite(req.params.userId, function (error, resetToken) {
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { resetToken: resetToken }));
});
@@ -158,9 +145,7 @@ function sendInvite(req, res, next) {
assert.strictEqual(typeof req.params.userId, 'string');
users.sendInvite(req.params.userId, { invitor: req.user }, function (error) {
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
if (error && error.reason === UsersError.BAD_FIELD) return next(new HttpError(409, 'Call createInvite API first'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { }));
});
@@ -173,8 +158,7 @@ function setGroups(req, res, next) {
if (!Array.isArray(req.body.groupIds)) return next(new HttpError(400, 'API call requires a groups array.'));
users.setMembership(req.params.userId, req.body.groupIds, function (error) {
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(404, 'One or more groups not found'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204));
});
@@ -187,9 +171,7 @@ function changePassword(req, res, next) {
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be a string'));
users.setPassword(req.params.userId, req.body.password, function (error) {
if (error && error.reason === UsersError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
if (error) return next(new HttpError(500, error));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204));
});
+3 -1
View File
@@ -149,6 +149,7 @@ function initializeExpressSync() {
router.get ('/api/v1/cloudron/eventlog', cloudronScope, routes.eventlog.list);
router.get ('/api/v1/cloudron/eventlog/:eventId', cloudronScope, routes.eventlog.get);
router.post('/api/v1/cloudron/sync_external_ldap', cloudronScope, routes.cloudron.syncExternalLdap);
router.get ('/api/v1/cloudron/server_ip', cloudronScope, routes.cloudron.getServerIp);
// tasks
router.get ('/api/v1/tasks', settingsScope, routes.tasks.list);
@@ -247,7 +248,7 @@ function initializeExpressSync() {
router.post('/api/v1/apps/:id/configure/memory_limit', appsManageScope, routes.apps.setMemoryLimit);
router.post('/api/v1/apps/:id/configure/automatic_backup', appsManageScope, routes.apps.setAutomaticBackup);
router.post('/api/v1/apps/:id/configure/automatic_update', appsManageScope, routes.apps.setAutomaticUpdate);
router.post('/api/v1/apps/:id/configure/robots_txt', appsManageScope, routes.apps.setRobotsTxt);
router.post('/api/v1/apps/:id/configure/reverse_proxy', appsManageScope, routes.apps.setReverseProxyConfig);
router.post('/api/v1/apps/:id/configure/cert', appsManageScope, routes.apps.setCertificate);
router.post('/api/v1/apps/:id/configure/debug_mode', appsManageScope, routes.apps.setDebugMode);
router.post('/api/v1/apps/:id/configure/mailbox', appsManageScope, routes.apps.setMailbox);
@@ -258,6 +259,7 @@ function initializeExpressSync() {
router.post('/api/v1/apps/:id/update', appsManageScope, routes.apps.updateApp);
router.post('/api/v1/apps/:id/restore', appsManageScope, routes.apps.restoreApp);
router.post('/api/v1/apps/:id/import', appsManageScope, routes.apps.importApp);
router.post('/api/v1/apps/:id/backup', appsManageScope, routes.apps.backupApp);
router.get ('/api/v1/apps/:id/backups', appsManageScope, routes.apps.listBackups);
router.post('/api/v1/apps/:id/stop', appsManageScope, routes.apps.stopApp);
+150 -93
View File
@@ -1,8 +1,6 @@
'use strict';
exports = module.exports = {
SettingsError: SettingsError,
getAppAutoupdatePattern: getAppAutoupdatePattern,
setAppAutoupdatePattern: setAppAutoupdatePattern,
@@ -33,6 +31,9 @@ exports = module.exports = {
getExternalLdapConfig: getExternalLdapConfig,
setExternalLdapConfig: setExternalLdapConfig,
getRegistryConfig: getRegistryConfig,
setRegistryConfig: setRegistryConfig,
getLicenseKey: getLicenseKey,
setLicenseKey: setLicenseKey,
@@ -42,6 +43,11 @@ exports = module.exports = {
getCloudronToken: getCloudronToken,
setCloudronToken: setCloudronToken,
getSysinfoConfig: getSysinfoConfig,
setSysinfoConfig: setSysinfoConfig,
provider: provider,
getAll: getAll,
initCache: initCache,
@@ -67,6 +73,8 @@ exports = module.exports = {
BACKUP_CONFIG_KEY: 'backup_config',
PLATFORM_CONFIG_KEY: 'platform_config',
EXTERNAL_LDAP_KEY: 'external_ldap_config',
REGISTRY_CONFIG_KEY: 'registry_config',
SYSINFO_CONFIG_KEY: 'sysinfo_config',
// strings
APP_AUTOUPDATE_PATTERN_KEY: 'app_autoupdate_pattern',
@@ -93,18 +101,18 @@ exports = module.exports = {
var addons = require('./addons.js'),
assert = require('assert'),
backups = require('./backups.js'),
BackupsError = backups.BackupsError,
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
cron = require('./cron.js'),
CronJob = require('cron').CronJob,
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:settings'),
externalldap = require('./externalldap.js'),
ExternalLdapError = externalldap.ExternalLdapError,
docker = require('./docker.js'),
externalLdap = require('./externalldap.js'),
moment = require('moment-timezone'),
paths = require('./paths.js'),
safe = require('safetydance'),
settingsdb = require('./settingsdb.js'),
sysinfo = require('./sysinfo.js'),
util = require('util'),
_ = require('underscore');
@@ -115,7 +123,7 @@ let gDefaults = (function () {
result[exports.TIME_ZONE_KEY] = 'America/Los_Angeles';
result[exports.CLOUDRON_NAME_KEY] = 'Cloudron';
result[exports.DYNAMIC_DNS_KEY] = false;
result[exports.UNSTABLE_APPS_KEY] = false;
result[exports.UNSTABLE_APPS_KEY] = true;
result[exports.LICENSE_KEY] = '';
result[exports.CLOUDRON_ID_KEY] = '';
result[exports.CLOUDRON_TOKEN_KEY] = '';
@@ -128,7 +136,13 @@ let gDefaults = (function () {
intervalSecs: 24 * 60 * 60 // ~1 day
};
result[exports.PLATFORM_CONFIG_KEY] = {};
result[exports.EXTERNAL_LDAP_KEY] = {};
result[exports.EXTERNAL_LDAP_KEY] = {
provider: 'noop'
};
result[exports.REGISTRY_CONFIG_KEY] = {};
result[exports.SYSINFO_CONFIG_KEY] = {
provider: 'generic'
};
result[exports.ADMIN_DOMAIN_KEY] = '';
result[exports.ADMIN_FQDN_KEY] = '';
result[exports.API_SERVER_ORIGIN_KEY] = 'https://api.cloudron.io';
@@ -140,31 +154,6 @@ let gDefaults = (function () {
let gCache = {};
function SettingsError(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(SettingsError, Error);
SettingsError.INTERNAL_ERROR = 'Internal Error';
SettingsError.EXTERNAL_ERROR = 'External Error';
SettingsError.NOT_FOUND = 'Not Found';
SettingsError.BAD_FIELD = 'Bad Field';
function notifyChange(key, value) {
assert.strictEqual(typeof key, 'string');
// value is a variant
@@ -177,11 +166,11 @@ function setAppAutoupdatePattern(pattern, callback) {
if (pattern !== constants.AUTOUPDATE_PATTERN_NEVER) { // check if pattern is valid
var job = safe.safeCall(function () { return new CronJob(pattern); });
if (!job) return callback(new SettingsError(SettingsError.BAD_FIELD, 'Invalid pattern'));
if (!job) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid pattern', { field: 'pattern' }));
}
settingsdb.set(exports.APP_AUTOUPDATE_PATTERN_KEY, pattern, function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (error) return callback(error);
notifyChange(exports.APP_AUTOUPDATE_PATTERN_KEY, pattern);
@@ -193,8 +182,8 @@ function getAppAutoupdatePattern(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.APP_AUTOUPDATE_PATTERN_KEY, function (error, pattern) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.APP_AUTOUPDATE_PATTERN_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.APP_AUTOUPDATE_PATTERN_KEY]);
if (error) return callback(error);
callback(null, pattern);
});
@@ -206,11 +195,11 @@ function setBoxAutoupdatePattern(pattern, callback) {
if (pattern !== constants.AUTOUPDATE_PATTERN_NEVER) { // check if pattern is valid
var job = safe.safeCall(function () { return new CronJob(pattern); });
if (!job) return callback(new SettingsError(SettingsError.BAD_FIELD, 'Invalid pattern'));
if (!job) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid pattern', { field: 'pattern' }));
}
settingsdb.set(exports.BOX_AUTOUPDATE_PATTERN_KEY, pattern, function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (error) return callback(error);
notifyChange(exports.BOX_AUTOUPDATE_PATTERN_KEY, pattern);
@@ -222,8 +211,8 @@ function getBoxAutoupdatePattern(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.BOX_AUTOUPDATE_PATTERN_KEY, function (error, pattern) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.BOX_AUTOUPDATE_PATTERN_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.BOX_AUTOUPDATE_PATTERN_KEY]);
if (error) return callback(error);
callback(null, pattern);
});
@@ -233,10 +222,10 @@ function setTimeZone(tz, callback) {
assert.strictEqual(typeof tz, 'string');
assert.strictEqual(typeof callback, 'function');
if (moment.tz.names().indexOf(tz) === -1) return callback(new SettingsError(SettingsError.BAD_FIELD, 'Bad timeZone'));
if (moment.tz.names().indexOf(tz) === -1) return callback(new BoxError(BoxError.BAD_FIELD, 'Bad timeZone', { field: 'timezone' }));
settingsdb.set(exports.TIME_ZONE_KEY, tz, function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (error) return callback(error);
notifyChange(exports.TIME_ZONE_KEY, tz);
@@ -248,8 +237,8 @@ function getTimeZone(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.TIME_ZONE_KEY, function (error, tz) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.TIME_ZONE_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.TIME_ZONE_KEY]);
if (error) return callback(error);
callback(null, tz);
});
@@ -259,8 +248,9 @@ function getCloudronName(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.CLOUDRON_NAME_KEY, function (error, name) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.CLOUDRON_NAME_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.CLOUDRON_NAME_KEY]);
if (error) return callback(error);
callback(null, name);
});
}
@@ -269,13 +259,13 @@ function setCloudronName(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
if (!name) return callback(new SettingsError(SettingsError.BAD_FIELD, 'name is empty'));
if (!name) return callback(new BoxError(BoxError.BAD_FIELD, 'name is empty', { field: 'name' }));
// some arbitrary restrictions (for sake of ui layout)
if (name.length > 32) return callback(new SettingsError(SettingsError.BAD_FIELD, 'name cannot exceed 32 characters'));
if (name.length > 32) return callback(new BoxError(BoxError.BAD_FIELD, 'name cannot exceed 32 characters', { field: 'name' }));
settingsdb.set(exports.CLOUDRON_NAME_KEY, name, function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (error) return callback(error);
notifyChange(exports.CLOUDRON_NAME_KEY, name);
@@ -293,7 +283,7 @@ function getCloudronAvatar(callback) {
avatar = safe.fs.readFileSync(paths.CLOUDRON_DEFAULT_AVATAR_FILE);
if (avatar) return callback(null, avatar);
callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error));
callback(new BoxError(BoxError.FS_ERROR, safe.error));
}
function setCloudronAvatar(avatar, callback) {
@@ -301,7 +291,7 @@ function setCloudronAvatar(avatar, callback) {
assert.strictEqual(typeof callback, 'function');
if (!safe.fs.writeFileSync(paths.CLOUDRON_AVATAR_FILE, avatar)) {
return callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error));
return callback(new BoxError(BoxError.FS_ERROR, safe.error));
}
return callback(null);
@@ -311,8 +301,8 @@ function getDynamicDnsConfig(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.DYNAMIC_DNS_KEY, function (error, enabled) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.DYNAMIC_DNS_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.DYNAMIC_DNS_KEY]);
if (error) return callback(error);
callback(null, !!enabled); // settingsdb holds string values only
});
@@ -324,7 +314,7 @@ function setDynamicDnsConfig(enabled, callback) {
// settingsdb takes string values only
settingsdb.set(exports.DYNAMIC_DNS_KEY, enabled ? 'enabled' : '', function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (error) return callback(error);
notifyChange(exports.DYNAMIC_DNS_KEY, enabled);
@@ -336,8 +326,8 @@ function getUnstableAppsConfig(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.UNSTABLE_APPS_KEY, function (error, enabled) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.UNSTABLE_APPS_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.UNSTABLE_APPS_KEY]);
if (error) return callback(error);
callback(null, !!enabled); // settingsdb holds string values only
});
@@ -349,7 +339,7 @@ function setUnstableAppsConfig(enabled, callback) {
// settingsdb takes string values only
settingsdb.set(exports.UNSTABLE_APPS_KEY, enabled ? 'enabled' : '', function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (error) return callback(error);
notifyChange(exports.UNSTABLE_APPS_KEY, enabled);
@@ -361,8 +351,8 @@ function getBackupConfig(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.BACKUP_CONFIG_KEY, function (error, value) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.BACKUP_CONFIG_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.BACKUP_CONFIG_KEY]);
if (error) return callback(error);
callback(null, JSON.parse(value)); // provider, token, key, region, prefix, bucket
});
@@ -378,14 +368,12 @@ function setBackupConfig(backupConfig, callback) {
backups.injectPrivateFields(backupConfig, curentConfig);
backups.testConfig(backupConfig, function (error) {
if (error && error.reason === BackupsError.BAD_FIELD) return callback(new SettingsError(SettingsError.BAD_FIELD, error.message));
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new SettingsError(SettingsError.EXTERNAL_ERROR, error.message));
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (error) return callback(error);
backups.cleanupCacheFilesSync();
settingsdb.set(exports.BACKUP_CONFIG_KEY, JSON.stringify(backupConfig), function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (error) return callback(error);
notifyChange(exports.BACKUP_CONFIG_KEY, backupConfig);
@@ -399,8 +387,8 @@ function getPlatformConfig(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.PLATFORM_CONFIG_KEY, function (error, value) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.PLATFORM_CONFIG_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.PLATFORM_CONFIG_KEY]);
if (error) return callback(error);
callback(null, JSON.parse(value));
});
@@ -411,11 +399,11 @@ function setPlatformConfig(platformConfig, callback) {
for (let addon of [ 'mysql', 'postgresql', 'mail', 'mongodb' ]) {
if (!platformConfig[addon]) continue;
if (platformConfig[addon].memorySwap < platformConfig[addon].memory) return callback(new SettingsError(SettingsError.BAD_FIELD, 'memorySwap must be larger than memory'));
if (platformConfig[addon].memorySwap < platformConfig[addon].memory) return callback(new BoxError(BoxError.BAD_FIELD, 'memorySwap must be larger than memory', { field: 'memory', addon }));
}
settingsdb.set(exports.PLATFORM_CONFIG_KEY, JSON.stringify(platformConfig), function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (error) return callback(error);
addons.updateServiceConfig(platformConfig, callback);
});
@@ -425,8 +413,8 @@ function getExternalLdapConfig(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.EXTERNAL_LDAP_KEY, function (error, value) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.EXTERNAL_LDAP_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.EXTERNAL_LDAP_KEY]);
if (error) return callback(error);
callback(null, JSON.parse(value));
});
@@ -436,16 +424,81 @@ function setExternalLdapConfig(externalLdapConfig, callback) {
assert.strictEqual(typeof externalLdapConfig, 'object');
assert.strictEqual(typeof callback, 'function');
externalldap.testConfig(externalLdapConfig, function (error) {
if (error && error.reason === ExternalLdapError.BAD_FIELD) return callback(new SettingsError(SettingsError.BAD_FIELD, error.message));
if (error && error.reason === ExternalLdapError.EXTERNAL_ERROR) return callback(new SettingsError(SettingsError.EXTERNAL_ERROR, error.message));
if (error && error.reason === ExternalLdapError.INVALID_CREDENTIALS) return callback(new SettingsError(SettingsError.BAD_FIELD, 'invalid bind credentials'));
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
getExternalLdapConfig(function (error, curentConfig) {
if (error) return callback(error);
settingsdb.set(exports.EXTERNAL_LDAP_KEY, JSON.stringify(externalLdapConfig), function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
externalLdap.injectPrivateFields(externalLdapConfig, curentConfig);
notifyChange(exports.EXTERNAL_LDAP_KEY, externalLdapConfig);
externalLdap.testConfig(externalLdapConfig, function (error) {
if (error) return callback(error);
settingsdb.set(exports.EXTERNAL_LDAP_KEY, JSON.stringify(externalLdapConfig), function (error) {
if (error) return callback(error);
notifyChange(exports.EXTERNAL_LDAP_KEY, externalLdapConfig);
callback(null);
});
});
});
}
function getRegistryConfig(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.REGISTRY_CONFIG_KEY, function (error, value) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.REGISTRY_CONFIG_KEY]);
if (error) return callback(error);
callback(null, JSON.parse(value));
});
}
function setRegistryConfig(registryConfig, callback) {
assert.strictEqual(typeof registryConfig, 'object');
assert.strictEqual(typeof callback, 'function');
getRegistryConfig(function (error, curentConfig) {
if (error) return callback(error);
docker.injectPrivateFields(registryConfig, curentConfig);
docker.testRegistryConfig(registryConfig, function (error) {
if (error) return callback(error);
settingsdb.set(exports.REGISTRY_CONFIG_KEY, JSON.stringify(registryConfig), function (error) {
if (error) return callback(error);
notifyChange(exports.REGISTRY_CONFIG_KEY, registryConfig);
callback(null);
});
});
});
}
function getSysinfoConfig(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.SYSINFO_CONFIG_KEY, function (error, value) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.SYSINFO_CONFIG_KEY]);
if (error) return callback(error);
callback(null, JSON.parse(value));
});
}
function setSysinfoConfig(sysinfoConfig, callback) {
assert.strictEqual(typeof sysinfoConfig, 'object');
assert.strictEqual(typeof callback, 'function');
sysinfo.testConfig(sysinfoConfig, function (error) {
if (error) return callback(error);
settingsdb.set(exports.SYSINFO_CONFIG_KEY, JSON.stringify(sysinfoConfig), function (error) {
if (error) return callback(error);
notifyChange(exports.SYSINFO_CONFIG_KEY, sysinfoConfig);
callback(null);
});
@@ -456,8 +509,8 @@ function getLicenseKey(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.LICENSE_KEY, function (error, value) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.LICENSE_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.LICENSE_KEY]);
if (error) return callback(error);
callback(null, value);
});
@@ -468,7 +521,7 @@ function setLicenseKey(licenseKey, callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.set(exports.LICENSE_KEY, licenseKey, function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (error) return callback(error);
notifyChange(exports.LICENSE_KEY, licenseKey);
@@ -480,8 +533,8 @@ function getCloudronId(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.CLOUDRON_ID_KEY, function (error, value) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.CLOUDRON_ID_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.CLOUDRON_ID_KEY]);
if (error) return callback(error);
callback(null, value);
});
@@ -492,7 +545,7 @@ function setCloudronId(cid, callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.set(exports.CLOUDRON_ID_KEY, cid, function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (error) return callback(error);
notifyChange(exports.CLOUDRON_ID_KEY, cid);
@@ -504,8 +557,8 @@ function getCloudronToken(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.CLOUDRON_TOKEN_KEY, function (error, value) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.CLOUDRON_TOKEN_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.CLOUDRON_TOKEN_KEY]);
if (error) return callback(error);
callback(null, value);
});
@@ -516,7 +569,7 @@ function setCloudronToken(token, callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.set(exports.CLOUDRON_TOKEN_KEY, token, function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (error) return callback(error);
notifyChange(exports.CLOUDRON_TOKEN_KEY, token);
@@ -528,7 +581,7 @@ function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.getAll(function (error, settings) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (error) return callback(error);
var result = _.extend({ }, gDefaults);
settings.forEach(function (setting) { result[setting.name] = setting.value; });
@@ -539,7 +592,7 @@ function getAll(callback) {
result[exports.DEMO_KEY] = !!result[exports.DEMO_KEY];
// convert JSON objects
[exports.BACKUP_CONFIG_KEY, exports.PLATFORM_CONFIG_KEY, exports.EXTERNAL_LDAP_KEY ].forEach(function (key) {
[exports.BACKUP_CONFIG_KEY, exports.PLATFORM_CONFIG_KEY, exports.EXTERNAL_LDAP_KEY, exports.REGISTRY_CONFIG_KEY, exports.SYSINFO_CONFIG_KEY ].forEach(function (key) {
result[key] = typeof result[key] === 'object' ? result[key] : safe.JSON.parse(result[key]);
});
@@ -553,12 +606,15 @@ function initCache(callback) {
getAll(function (error, allSettings) {
if (error) return callback(error);
const provider = safe.fs.readFileSync(paths.PROVIDER_FILE, 'utf8');
gCache = {
apiServerOrigin: allSettings[exports.API_SERVER_ORIGIN_KEY],
webServerOrigin: allSettings[exports.WEB_SERVER_ORIGIN_KEY],
adminDomain: allSettings[exports.ADMIN_DOMAIN_KEY],
adminFqdn: allSettings[exports.ADMIN_FQDN_KEY],
isDemo: allSettings[exports.DEMO_KEY]
isDemo: allSettings[exports.DEMO_KEY],
provider: provider ? provider.trim() : 'generic'
};
callback();
@@ -572,10 +628,10 @@ function setAdmin(adminDomain, adminFqdn, callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.set(exports.ADMIN_DOMAIN_KEY, adminDomain, function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (error) return callback(error);
settingsdb.set(exports.ADMIN_FQDN_KEY, adminFqdn, function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (error) return callback(error);
gCache.adminDomain = adminDomain;
gCache.adminFqdn = adminFqdn;
@@ -590,7 +646,7 @@ function setApiServerOrigin(origin, callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.set(exports.API_SERVER_ORIGIN_KEY, origin, function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (error) return callback(error);
gCache.apiServerOrigin = origin;
notifyChange(exports.API_SERVER_ORIGIN_KEY, origin);
@@ -599,6 +655,7 @@ function setApiServerOrigin(origin, callback) {
});
}
function provider() { return gCache.provider; }
function apiServerOrigin() { return gCache.apiServerOrigin; }
function webServerOrigin() { return gCache.webServerOrigin; }
function adminDomain() { return gCache.adminDomain; }
+7 -7
View File
@@ -10,8 +10,8 @@ exports = module.exports = {
};
var assert = require('assert'),
database = require('./database.js'),
DatabaseError = require('./databaseerror');
BoxError = require('./boxerror.js'),
database = require('./database.js');
const SETTINGS_FIELDS = [ 'name', 'value' ].join(',');
@@ -20,8 +20,8 @@ function get(key, callback) {
assert.strictEqual(typeof callback, 'function');
database.query(`SELECT ${SETTINGS_FIELDS} FROM settings WHERE name = ?`, [ key ], 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, 'Setting not found'));
callback(null, result[0].value);
});
@@ -29,7 +29,7 @@ function get(key, callback) {
function getAll(callback) {
database.query(`SELECT ${SETTINGS_FIELDS} FROM settings ORDER BY name`, 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);
});
@@ -41,7 +41,7 @@ function set(key, value, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO settings (name, value) VALUES (?, ?) ON DUPLICATE KEY UPDATE value=VALUES(value)', [ key, value ], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error)); // don't rely on affectedRows here since it gives 2
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); // don't rely on affectedRows here since it gives 2
callback(null);
});
@@ -49,7 +49,7 @@ function set(key, value, callback) {
function clear(callback) {
database.query('DELETE FROM settings', function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(error);
});
+19 -19
View File
@@ -17,7 +17,7 @@ exports = module.exports = {
};
var assert = require('assert'),
BackupsError = require('../backups.js').BackupsError,
BoxError = require('../boxerror.js'),
debug = require('debug')('box:storage/filesystem'),
EventEmitter = require('events'),
fs = require('fs'),
@@ -35,7 +35,7 @@ function upload(apiConfig, backupFilePath, sourceStream, callback) {
assert.strictEqual(typeof callback, 'function');
mkdirp(path.dirname(backupFilePath), function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
safe.fs.unlinkSync(backupFilePath); // remove any hardlink
@@ -48,15 +48,15 @@ function upload(apiConfig, backupFilePath, sourceStream, callback) {
fileStream.on('error', function (error) {
debug('[%s] upload: out stream error.', backupFilePath, error);
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
});
fileStream.on('finish', function () {
// in test, upload() may or may not be called via sudo script
const BACKUP_UID = parseInt(process.env.SUDO_UID, 10) || process.getuid();
if (!safe.fs.chownSync(backupFilePath, BACKUP_UID, BACKUP_UID)) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Unable to chown:' + safe.error.message));
if (!safe.fs.chownSync(path.dirname(backupFilePath), BACKUP_UID, BACKUP_UID)) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Unable to chown:' + safe.error.message));
if (!safe.fs.chownSync(backupFilePath, BACKUP_UID, BACKUP_UID)) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Unable to chown:' + safe.error.message));
if (!safe.fs.chownSync(path.dirname(backupFilePath), BACKUP_UID, BACKUP_UID)) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Unable to chown:' + safe.error.message));
debug('upload %s: done.', backupFilePath);
@@ -72,7 +72,7 @@ function download(apiConfig, sourceFilePath, callback) {
debug(`download: ${sourceFilePath}`);
if (!safe.fs.existsSync(sourceFilePath)) return callback(new BackupsError(BackupsError.NOT_FOUND, `File not found: ${sourceFilePath}`));
if (!safe.fs.existsSync(sourceFilePath)) return callback(new BoxError(BoxError.NOT_FOUND, `File not found: ${sourceFilePath}`));
var fileStream = fs.createReadStream(sourceFilePath);
callback(null, fileStream);
@@ -116,14 +116,14 @@ function copy(apiConfig, oldFilePath, newFilePath) {
var events = new EventEmitter();
mkdirp(path.dirname(newFilePath), function (error) {
if (error) return events.emit('done', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
if (error) return events.emit('done', new BoxError(BoxError.EXTERNAL_ERROR, error.message));
events.emit('progress', `Copying ${oldFilePath} to ${newFilePath}`);
// this will hardlink backups saving space
var cpOptions = apiConfig.noHardlinks ? '-a' : '-al';
shell.spawn('copy', '/bin/cp', [ cpOptions, oldFilePath, newFilePath ], { }, function (error) {
if (error) return events.emit('done', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
if (error) return events.emit('done', new BoxError(BoxError.EXTERNAL_ERROR, error.message));
events.emit('done', null);
});
@@ -141,9 +141,9 @@ function remove(apiConfig, filename, callback) {
if (!stat) return callback();
if (stat.isFile()) {
if (!safe.fs.unlinkSync(filename)) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, safe.error.message));
if (!safe.fs.unlinkSync(filename)) return callback(new BoxError(BoxError.EXTERNAL_ERROR, safe.error.message));
} else if (stat.isDirectory()) {
if (!safe.fs.rmdirSync(filename)) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, safe.error.message));
if (!safe.fs.rmdirSync(filename)) return callback(new BoxError(BoxError.EXTERNAL_ERROR, safe.error.message));
}
callback(null);
@@ -158,7 +158,7 @@ function removeDir(apiConfig, pathPrefix) {
process.nextTick(() => events.emit('progress', `Removing directory ${pathPrefix}`));
shell.spawn('removeDir', '/bin/rm', [ '-rf', pathPrefix ], { }, function (error) {
if (error) return events.emit('done', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
if (error) return events.emit('done', new BoxError(BoxError.EXTERNAL_ERROR, error.message));
events.emit('done', null);
});
@@ -170,21 +170,21 @@ function testConfig(apiConfig, callback) {
assert.strictEqual(typeof apiConfig, 'object');
assert.strictEqual(typeof callback, 'function');
if (typeof apiConfig.backupFolder !== 'string') return callback(new BackupsError(BackupsError.BAD_FIELD, 'backupFolder must be string'));
if (typeof apiConfig.backupFolder !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'backupFolder must be string', { field: 'backupFolder' }));
if (!apiConfig.backupFolder) return callback(new BackupsError(BackupsError.BAD_FIELD, 'backupFolder is required'));
if (!apiConfig.backupFolder) return callback(new BoxError(BoxError.BAD_FIELD, 'backupFolder is required', { field: 'backupFolder' }));
if ('noHardlinks' in apiConfig && typeof apiConfig.noHardlinks !== 'boolean') return callback(new BackupsError(BackupsError.BAD_FIELD, 'noHardlinks must be boolean'));
if ('noHardlinks' in apiConfig && typeof apiConfig.noHardlinks !== 'boolean') return callback(new BoxError(BoxError.BAD_FIELD, 'noHardlinks must be boolean', { field: 'noHardLinks' }));
if ('externalDisk' in apiConfig && typeof apiConfig.externalDisk !== 'boolean') return callback(new BackupsError(BackupsError.BAD_FIELD, 'externalDisk must be boolean'));
if ('externalDisk' in apiConfig && typeof apiConfig.externalDisk !== 'boolean') return callback(new BoxError(BoxError.BAD_FIELD, 'externalDisk must be boolean', { field: 'externalDisk' }));
fs.stat(apiConfig.backupFolder, function (error, result) {
if (error) return callback(new BackupsError(BackupsError.BAD_FIELD, 'Directory does not exist or cannot be accessed: ' + error.message));
if (!result.isDirectory()) return callback(new BackupsError(BackupsError.BAD_FIELD, 'Backup location is not a directory'));
if (error) return callback(new BoxError(BoxError.BAD_FIELD, 'Directory does not exist or cannot be accessed: ' + error.message), { field: 'backupFolder' });
if (!result.isDirectory()) return callback(new BoxError(BoxError.BAD_FIELD, 'Backup location is not a directory', { field: 'backupFolder' }));
mkdirp(path.join(apiConfig.backupFolder, 'snapshot'), function (error) {
if (error && error.code === 'EACCES') return callback(new BackupsError(BackupsError.BAD_FIELD, `Access denied. Run "chown yellowtent:yellowtent ${apiConfig.backupFolder}" on the server`));
if (error) return callback(new BackupsError(BackupsError.BAD_FIELD, error.message));
if (error && error.code === 'EACCES') return callback(new BoxError(BoxError.BAD_FIELD, `Access denied. Run "chown yellowtent:yellowtent ${apiConfig.backupFolder}" on the server`, { field: 'backupFolder' }));
if (error) return callback(new BoxError(BoxError.BAD_FIELD, error.message, { field: 'backupFolder' }));
callback(null);
});
+23 -29
View File
@@ -22,7 +22,7 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
backups = require('../backups.js'),
BackupsError = require('../backups.js').BackupsError,
BoxError = require('../boxerror.js'),
debug = require('debug')('box:storage/gcs'),
EventEmitter = require('events'),
GCS = require('@google-cloud/storage').Storage,
@@ -68,7 +68,7 @@ function upload(apiConfig, backupFilePath, sourceStream, callback) {
function done(error) {
if (error) {
debug('[%s] upload: gcp upload error.', backupFilePath, error);
return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, `Error uploading ${backupFilePath}. Message: ${error.message} HTTP Code: ${error.code}`));
return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Error uploading ${backupFilePath}. Message: ${error.message} HTTP Code: ${error.code}`));
}
callback(null);
@@ -95,10 +95,10 @@ function download(apiConfig, backupFilePath, callback) {
var readStream = file.createReadStream()
.on('error', function(error) {
if (error && error.code == 404){
ps.emit('error', new BackupsError(BackupsError.NOT_FOUND));
ps.emit('error', new BoxError(BoxError.NOT_FOUND));
} else {
debug('[%s] download: gcp stream error.', backupFilePath, error);
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error));
ps.emit('error', new BoxError(BoxError.EXTERNAL_ERROR, error));
}
})
;
@@ -143,7 +143,7 @@ function copy(apiConfig, oldFilePath, newFilePath) {
assert.strictEqual(typeof oldFilePath, 'string');
assert.strictEqual(typeof newFilePath, 'string');
var events = new EventEmitter(), retryCount = 0;
var events = new EventEmitter();
function copyFile(entry, iteratorCallback) {
var relativePath = path.relative(oldFilePath, entry.fullPath);
@@ -151,24 +151,20 @@ function copy(apiConfig, oldFilePath, newFilePath) {
getBucket(apiConfig).file(entry.fullPath).copy(path.join(newFilePath, relativePath), function(error) {
if (error) debug('copyBackup: gcs copy error', error);
if (error && error.code === 404) return iteratorCallback(new BackupsError(BackupsError.NOT_FOUND, 'Old backup not found'));
if (error) return iteratorCallback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
if (error && error.code === 404) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, 'Old backup not found'));
if (error) return iteratorCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
iteratorCallback(null);
});
events.emit('progress', `Copying ${relativePath}...`);
}
const batchSize = -1;
var total = 0, concurrency = 4;
const batchSize = 1000, concurrency = 10;
var total = 0;
listDir(apiConfig, oldFilePath, batchSize, function (entries, done) {
total += entries.length;
if (retryCount === 0) concurrency = Math.min(concurrency + 1, 10); else concurrency = Math.max(concurrency - 1, 5);
events.emit('progress', `${retryCount} errors. concurrency set to ${concurrency}`);
retryCount = 0;
events.emit('progress', `Copying ${entries.length} files from ${entries[0].fullPath} to ${entries[entries.length-1].fullPath}. total: ${total}`);
async.eachLimit(entries, concurrency, copyFile, done);
}, function (error) {
@@ -197,17 +193,15 @@ function removeDir(apiConfig, pathPrefix) {
assert.strictEqual(typeof apiConfig, 'object');
assert.strictEqual(typeof pathPrefix, 'string');
var events = new EventEmitter(), retryCount = 0;
var events = new EventEmitter();
const batchSize = 1;
var total = 0, concurrency = 4;
const batchSize = 1000, concurrency = 10; // https://googleapis.dev/nodejs/storage/latest/Bucket.html#deleteFiles
var total = 0;
listDir(apiConfig, pathPrefix, batchSize, function (entries, done) {
total += entries.length;
if (retryCount === 0) concurrency = Math.min(concurrency + 1, 10); else concurrency = Math.max(concurrency - 1, 5);
events.emit('progress', `${retryCount} errors. concurrency set to ${concurrency}`);
retryCount = 0;
events.emit('progress', `Removing ${entries.length} files from ${entries[0].fullPath} to ${entries[entries.length-1].fullPath}. total: ${total}`);
async.eachLimit(entries, concurrency, function (entry, iteratorCallback) {
remove(apiConfig, entry.fullPath, iteratorCallback);
@@ -225,13 +219,13 @@ function testConfig(apiConfig, callback) {
assert.strictEqual(typeof apiConfig, 'object');
assert.strictEqual(typeof callback, 'function');
if (typeof apiConfig.projectId !== 'string') return callback(new BackupsError(BackupsError.BAD_FIELD, 'projectId must be a string'));
if (!apiConfig.credentials || typeof apiConfig.credentials !== 'object') return callback(new BackupsError(BackupsError.BAD_FIELD, 'credentials must be an object'));
if (typeof apiConfig.credentials.client_email !== 'string') return callback(new BackupsError(BackupsError.BAD_FIELD, 'credentials.client_email must be a string'));
if (typeof apiConfig.credentials.private_key !== 'string') return callback(new BackupsError(BackupsError.BAD_FIELD, 'credentials.private_key must be a string'));
if (typeof apiConfig.projectId !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'projectId must be a string'));
if (!apiConfig.credentials || typeof apiConfig.credentials !== 'object') return callback(new BoxError(BoxError.BAD_FIELD, 'credentials must be an object'));
if (typeof apiConfig.credentials.client_email !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'credentials.client_email must be a string'));
if (typeof apiConfig.credentials.private_key !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'credentials.private_key must be a string'));
if (typeof apiConfig.bucket !== 'string') return callback(new BackupsError(BackupsError.BAD_FIELD, 'bucket must be a string'));
if (typeof apiConfig.prefix !== 'string') return callback(new BackupsError(BackupsError.BAD_FIELD, 'prefix must be a string'));
if (typeof apiConfig.bucket !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'bucket must be a string'));
if (typeof apiConfig.prefix !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'prefix must be a string'));
// attempt to upload and delete a file with new credentials
var bucket = getBucket(apiConfig);
@@ -245,16 +239,16 @@ function testConfig(apiConfig, callback) {
uploadStream.on('error', function(error) {
debug('testConfig: failed uploading cloudron-testfile', error);
if (error && error.code && (error.code == 403 || error.code == 404)) {
return callback(new BackupsError(BackupsError.BAD_FIELD, error.message));
return callback(new BoxError(BoxError.BAD_FIELD, error.message));
}
return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
});
uploadStream.on('finish', function() {
debug('testConfig: uploaded cloudron-testfile ' + JSON.stringify(arguments));
bucket.file(path.join(apiConfig.prefix, 'cloudron-testfile')).delete(function(error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
debug('testConfig: deleted cloudron-testfile');
callback();
});
+18 -15
View File
@@ -23,7 +23,7 @@ var assert = require('assert'),
async = require('async'),
AWS = require('aws-sdk'),
backups = require('../backups.js'),
BackupsError = require('../backups.js').BackupsError,
BoxError = require('../boxerror.js'),
chunk = require('lodash.chunk'),
debug = require('debug')('box:storage/s3'),
EventEmitter = require('events'),
@@ -95,10 +95,13 @@ function upload(apiConfig, backupFilePath, sourceStream, callback) {
// s3.upload automatically does a multi-part upload. we set queueSize to 1 to reduce memory usage
// uploader will buffer at most queueSize * partSize bytes into memory at any given time.
s3.upload(params, { partSize: 10 * 1024 * 1024, queueSize: 1 }, function (error, data) {
// scaleway only supports 1000 parts per object (https://www.scaleway.com/en/docs/s3-multipart-upload/)
const partSize = apiConfig.provider === 'scaleway-objectstorage' ? 100 * 1024 * 1024 : 10 * 1024 * 1024;
s3.upload(params, { partSize, queueSize: 1 }, function (error, data) {
if (error) {
debug('Error uploading [%s]: s3 upload error.', backupFilePath, error);
return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, `Error uploading ${backupFilePath}. Message: ${error.message} HTTP Code: ${error.code}`));
return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Error uploading ${backupFilePath}. Message: ${error.message} HTTP Code: ${error.code}`));
}
debug(`Uploaded ${backupFilePath}: ${JSON.stringify(data)}`);
@@ -128,10 +131,10 @@ function download(apiConfig, backupFilePath, callback) {
multipartDownload.on('error', function (error) {
if (S3_NOT_FOUND(error)) {
ps.emit('error', new BackupsError(BackupsError.NOT_FOUND, `Backup not found: ${backupFilePath}`));
ps.emit('error', new BoxError(BoxError.NOT_FOUND, `Backup not found: ${backupFilePath}`));
} else {
debug(`download: ${apiConfig.bucket}:${backupFilePath} s3 stream error.`, error);
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message || error.code)); // DO sets 'code'
ps.emit('error', new BoxError(BoxError.EXTERNAL_ERROR, error.message || error.code)); // DO sets 'code'
}
});
@@ -216,8 +219,8 @@ function copy(apiConfig, oldFilePath, newFilePath) {
function done(error) {
if (error) debug(`copy: s3 copy error when copying ${entry.fullPath}: ${error}`);
if (error && S3_NOT_FOUND(error)) return iteratorCallback(new BackupsError(BackupsError.NOT_FOUND, `Old backup not found: ${entry.fullPath}`));
if (error) return iteratorCallback(new BackupsError(BackupsError.EXTERNAL_ERROR, `Error copying ${entry.fullPath} : ${error.code} ${error}`));
if (error && S3_NOT_FOUND(error)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `Old backup not found: ${entry.fullPath}`));
if (error) return iteratorCallback(new BoxError(BoxError.EXTERNAL_ERROR, `Error copying ${entry.fullPath} : ${error.code} ${error}`));
iteratorCallback(null);
}
@@ -402,13 +405,13 @@ function testConfig(apiConfig, callback) {
assert.strictEqual(typeof apiConfig, 'object');
assert.strictEqual(typeof callback, 'function');
if (typeof apiConfig.accessKeyId !== 'string') return callback(new BackupsError(BackupsError.BAD_FIELD, 'accessKeyId must be a string'));
if (typeof apiConfig.secretAccessKey !== 'string') return callback(new BackupsError(BackupsError.BAD_FIELD, 'secretAccessKey must be a string'));
if (typeof apiConfig.accessKeyId !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'accessKeyId must be a string', { field: 'accessKeyId' }));
if (typeof apiConfig.secretAccessKey !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'secretAccessKey must be a string', { field: 'secretAccessKey' }));
if (typeof apiConfig.bucket !== 'string') return callback(new BackupsError(BackupsError.BAD_FIELD, 'bucket must be a string'));
if (typeof apiConfig.prefix !== 'string') return callback(new BackupsError(BackupsError.BAD_FIELD, 'prefix must be a string'));
if ('signatureVersion' in apiConfig && typeof apiConfig.signatureVersion !== 'string') return callback(new BackupsError(BackupsError.BAD_FIELD, 'signatureVersion must be a string'));
if ('endpoint' in apiConfig && typeof apiConfig.endpoint !== 'string') return callback(new BackupsError(BackupsError.BAD_FIELD, 'endpoint must be a string'));
if (typeof apiConfig.bucket !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'bucket must be a string', { field: 'bucket' }));
if (typeof apiConfig.prefix !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'prefix must be a string', { field: 'prefix' }));
if ('signatureVersion' in apiConfig && typeof apiConfig.signatureVersion !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'signatureVersion must be a string', { field: 'signatureVersion' }));
if ('endpoint' in apiConfig && typeof apiConfig.endpoint !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'endpoint must be a string', { field: 'endpoint' }));
// attempt to upload and delete a file with new credentials
getS3Config(apiConfig, function (error, credentials) {
@@ -422,7 +425,7 @@ function testConfig(apiConfig, callback) {
var s3 = new AWS.S3(credentials);
s3.putObject(params, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message || error.code)); // DO sets 'code'
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message || error.code)); // DO sets 'code'
var params = {
Bucket: apiConfig.bucket,
@@ -430,7 +433,7 @@ function testConfig(apiConfig, callback) {
};
s3.deleteObject(params, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message || error.code)); // DO sets 'code'
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message || error.code)); // DO sets 'code'
callback();
});
+21 -30
View File
@@ -2,47 +2,37 @@
exports = module.exports = {
getRemoteSupport: getRemoteSupport,
enableRemoteSupport: enableRemoteSupport,
SupportError: SupportError
enableRemoteSupport: enableRemoteSupport
};
let assert = require('assert'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
shell = require('./shell.js'),
once = require('once'),
path = require('path'),
paths = require('./paths.js'),
sysinfo = require('./sysinfo.js'),
util = require('util');
settings = require('./settings.js');
// the logic here is also used in the cloudron-support tool
var AUTHORIZED_KEYS_FILEPATH = constants.TEST ? path.join(paths.baseDir(), 'authorized_keys') : ((sysinfo.provider() === 'ec2' || sysinfo.provider() === 'lightsail' || sysinfo.provider() === 'ami') ? '/home/ubuntu/.ssh/authorized_keys' : '/root/.ssh/authorized_keys'),
AUTHORIZED_KEYS_USER = constants.TEST ? process.getuid() : ((sysinfo.provider() === 'ec2' || sysinfo.provider() === 'lightsail' || sysinfo.provider() === 'ami') ? 'ubuntu' : 'root'),
AUTHORIZED_KEYS_CMD = path.join(__dirname, 'scripts/remotesupport.sh');
const AUTHORIZED_KEYS_CMD = path.join(__dirname, 'scripts/remotesupport.sh');
function SupportError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
function sshInfo() {
let filePath, user;
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;
if (constants.TEST) {
filePath = path.join(paths.baseDir(), 'authorized_keys');
user = process.getuid();
} else if (settings.provider() === 'ec2' || settings.provider() === 'lightsail' || settings.provider() === 'ami') {
filePath = '/home/ubuntu/.ssh/authorized_keys';
user = 'ubuntu';
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
filePath = '/root/.ssh/authorized_keys';
user = 'root';
}
return { filePath, user };
}
util.inherits(SupportError, Error);
SupportError.NOT_FOUND = 'Not found';
SupportError.INVALID_KEY = 'Invalid key';
SupportError.INTERNAL_ERROR = 'Internal Error';
function getRemoteSupport(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -50,8 +40,8 @@ function getRemoteSupport(callback) {
callback = once(callback); // exit may or may not be called after an 'error'
let result = '';
let cp = shell.sudo('support', [ AUTHORIZED_KEYS_CMD, 'is-enabled', AUTHORIZED_KEYS_FILEPATH ], {}, function (error) {
if (error) callback(new SupportError(SupportError.INTERNAL_ERROR, error));
let cp = shell.sudo('support', [ AUTHORIZED_KEYS_CMD, 'is-enabled', sshInfo().filePath ], {}, function (error) {
if (error) callback(new BoxError(BoxError.FS_ERROR, error));
callback(null, { enabled: result.trim() === 'true' });
});
@@ -61,8 +51,9 @@ function getRemoteSupport(callback) {
function enableRemoteSupport(enable, callback) {
assert.strictEqual(typeof callback, 'function');
shell.sudo('support', [ AUTHORIZED_KEYS_CMD, enable ? 'enable' : 'disable', AUTHORIZED_KEYS_FILEPATH, AUTHORIZED_KEYS_USER ], {}, function (error) {
if (error) callback(new SupportError(SupportError.INTERNAL_ERROR, error));
let si = sshInfo();
shell.sudo('support', [ AUTHORIZED_KEYS_CMD, enable ? 'enable' : 'disable', si.filePath, si.user ], {}, function (error) {
if (error) callback(new BoxError(BoxError.FS_ERROR, error));
callback();
});
+20 -56
View File
@@ -1,76 +1,33 @@
'use strict';
exports = module.exports = {
SysInfoError: SysInfoError,
getServerIp: getServerIp,
testConfig: testConfig,
getPublicIp: getPublicIp,
hasIPv6: hasIPv6,
provider: provider
hasIPv6: hasIPv6
};
var assert = require('assert'),
ec2 = require('./sysinfo/ec2.js'),
fs = require('fs'),
generic = require('./sysinfo/generic.js'),
paths = require('./paths.js'),
scaleway = require('./sysinfo/scaleway.js'),
safe = require('safetydance'),
util = require('util');
settings = require('./settings.js');
function SysInfoError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
function api(provider) {
assert.strictEqual(typeof provider, 'string');
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(SysInfoError, Error);
SysInfoError.INTERNAL_ERROR = 'Internal Error';
SysInfoError.EXTERNAL_ERROR = 'External Error';
let gProvider = null;
function provider() {
if (gProvider) return gProvider;
gProvider = safe.fs.readFileSync(paths.PROVIDER_FILE, 'utf8');
if (!gProvider) return gProvider = 'generic';
return gProvider.trim();
}
function getApi(callback) {
assert.strictEqual(typeof callback, 'function');
switch (provider()) {
case 'ec2': return callback(null, ec2);
case 'lightsail': return callback(null, ec2);
case 'ami': return callback(null, ec2);
case 'scaleway': return callback(null, scaleway);
default: return callback(null, generic);
switch (provider) {
case 'fixed': return require('./sysinfo/fixed.js');
case 'network-interface': return require('./sysinfo/network-interface.js');
default: return require('./sysinfo/generic.js');
}
}
function getPublicIp(callback) {
function getServerIp(callback) {
assert.strictEqual(typeof callback, 'function');
getApi(function (error, api) {
settings.getSysinfoConfig(function (error, config) {
if (error) return callback(error);
api.getPublicIp(callback);
api(config.provider).getServerIp(config, callback);
});
}
@@ -79,3 +36,10 @@ function hasIPv6() {
// 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;
}
function testConfig(config, callback) {
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof callback, 'function');
api(config.provider).testConfig(config, callback);
}
-21
View File
@@ -1,21 +0,0 @@
'use strict';
exports = module.exports = {
getPublicIp: getPublicIp
};
var assert = require('assert'),
superagent = require('superagent'),
SysInfoError = require('../sysinfo.js').SysInfoError,
util = require('util');
function getPublicIp(callback) {
assert.strictEqual(typeof callback, 'function');
superagent.get('http://169.254.169.254/latest/meta-data/public-ipv4').timeout(30 * 1000).end(function (error, result) {
if (error) return callback(new SysInfoError(SysInfoError.INTERNAL_ERROR, error.status ? 'Request failed: ' + error.status : 'Network failure'));
if (result.statusCode !== 200) return callback(new SysInfoError(SysInfoError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
callback(null, result.text);
});
}
+27
View File
@@ -0,0 +1,27 @@
'use strict';
exports = module.exports = {
getServerIp,
testConfig
};
var assert = require('assert'),
BoxError = require('../boxerror.js'),
validator = require('validator');
function getServerIp(config, callback) {
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof callback, 'function');
callback(null, config.ip);
}
function testConfig(config, callback) {
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof callback, 'function');
if (typeof config.ip !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'ip must be a string'));
if (!validator.isIP(config.ip, 4)) return callback(new BoxError(BoxError.BAD_FIELD, 'ip is not a valid ipv4'));
callback(null);
}
+15 -6
View File
@@ -1,15 +1,17 @@
'use strict';
exports = module.exports = {
getPublicIp: getPublicIp
getServerIp,
testConfig
};
var assert = require('assert'),
async = require('async'),
superagent = require('superagent'),
SysInfoError = require('../sysinfo.js').SysInfoError;
BoxError = require('../boxerror.js'),
superagent = require('superagent');
function getPublicIp(callback) {
function getServerIp(config, callback) {
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof callback, 'function');
if (process.env.BOX_ENV === 'test') return callback(null, '127.0.0.1');
@@ -18,11 +20,11 @@ function getPublicIp(callback) {
superagent.get('https://api.cloudron.io/api/v1/helper/public_ip').timeout(30 * 1000).end(function (error, result) {
if (error || result.statusCode !== 200) {
console.error('Error getting IP', error);
return callback(new SysInfoError(SysInfoError.EXTERNAL_ERROR, 'Unable to detect IP. API server unreachable'));
return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Unable to detect IP. API server unreachable'));
}
if (!result.body && !result.body.ip) {
console.error('Unexpected answer. No "ip" found in response body.', result.body);
return callback(new SysInfoError(SysInfoError.EXTERNAL_ERROR, 'Unable to detect IP. No IP found in response'));
return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Unable to detect IP. No IP found in response'));
}
callback(null, result.body.ip);
@@ -33,3 +35,10 @@ function getPublicIp(callback) {
callback(null, result);
});
}
function testConfig(config, callback) {
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof callback, 'function');
callback(null);
}

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