Compare commits

..

383 Commits

Author SHA1 Message Date
Girish Ramakrishnan 70128458b2 Fix crash when renewAll is called when cloudron is not setup yet 2018-06-05 21:27:32 -07:00
Girish Ramakrishnan 900225957e typo: code should return SetupError 2018-06-05 21:19:47 -07:00
Girish Ramakrishnan fd8f5e3c71 Return error for trailing dot instead 2018-06-05 21:09:07 -07:00
Girish Ramakrishnan 7382ea2b04 Handle my subdomain already existing 2018-06-05 20:53:28 -07:00
Girish Ramakrishnan 09163b8a2b strip any trailing dot in the domain and zoneName 2018-06-05 20:33:14 -07:00
Girish Ramakrishnan 953398c427 lint 2018-06-05 20:02:47 -07:00
Girish Ramakrishnan 9f7406c235 cloudron-activate: Add option to setup backup dir 2018-06-05 19:40:46 -07:00
Girish Ramakrishnan 2e427aa60e Add 2.3.2 changes 2018-06-05 09:51:56 -07:00
Girish Ramakrishnan ab80cc9ea1 Add username to the TOTP secret name
This works around issue in FreeOTP app which crashed when
the same name is used.

https://github.com/freeotp/freeotp-ios/issues/69
https://github.com/freeotp/freeotp-android/issues/69
2018-06-04 16:08:03 -07:00
Girish Ramakrishnan 321f11c644 mysql: _ prefix is hardcoded in mysql addon already
Fixes #560
2018-06-04 12:31:40 -07:00
Girish Ramakrishnan 47f85434db cloudron-activate: always login since activate return token and not accessToken 2018-06-01 00:12:19 -07:00
Girish Ramakrishnan 7717c7b1cd Add cloudron-activate script to automate activation from VM image 2018-05-31 23:46:44 -07:00
Johannes Zellner 7618aa786c Handle AppstoreError properly when no appstore account was set 2018-05-30 20:33:58 +02:00
Girish Ramakrishnan f752cb368c Remove spamcannibal
Fixes #559
2018-05-30 11:07:17 -07:00
Girish Ramakrishnan ca500e2165 mailer: do not send notifications to fallback email 2018-05-30 09:26:59 -07:00
Johannes Zellner 371f81b980 Add test for mail enabling without a subscription 2018-05-30 00:02:18 +02:00
Johannes Zellner c68cca9a54 Fixup mail test, which requires a subscription 2018-05-29 23:59:53 +02:00
Johannes Zellner 9194be06c3 Fix app purchase test 2018-05-29 23:24:08 +02:00
Johannes Zellner 9eb58cdfe5 Check for plan when enabling email 2018-05-29 13:31:43 +02:00
Johannes Zellner 99be89012d No need to check for active subscription state, as the appstore already does this 2018-05-29 13:31:43 +02:00
Johannes Zellner 541fabcb2e Add convenience function to determine if subscription is 'free' or not 2018-05-29 13:31:43 +02:00
Johannes Zellner 915e04eb08 We do not have an 'undecided' plan state anymore 2018-05-29 13:31:43 +02:00
Girish Ramakrishnan 48896d4e50 more changes 2018-05-28 10:06:46 -07:00
Johannes Zellner 29682c0944 Only allow max of 2 apps on the free plan 2018-05-26 18:53:20 +02:00
Girish Ramakrishnan 346b1cb91c more changes 2018-05-26 08:11:19 -07:00
Girish Ramakrishnan e552821c01 Add 2.3.1 changes 2018-05-25 11:44:04 -07:00
Girish Ramakrishnan bac3ba101e Add mailboxName to app configure route
Fixes #558
2018-05-24 16:26:34 -07:00
Girish Ramakrishnan 87c46fe3ea apps: return mailbox name as part of app
part of cloudron/box#558
2018-05-24 15:50:46 -07:00
Girish Ramakrishnan f9763b1ad3 namecom: MX record not set properly 2018-05-24 09:41:52 -07:00
Girish Ramakrishnan f1e6116b83 Fix copyright years 2018-05-23 20:02:33 -07:00
Girish Ramakrishnan 273948c3c7 Fix tests 2018-05-22 13:22:48 -07:00
Girish Ramakrishnan 9c073e7bee Preserve addons credentials when restoring 2018-05-22 13:07:58 -07:00
Girish Ramakrishnan 8b3edf6efc Bump mail container for managesieve fix 2018-05-18 18:26:19 -07:00
Girish Ramakrishnan 07e649a2d3 Add more changes 2018-05-17 20:17:24 -07:00
Girish Ramakrishnan 8c63b6716d Trigger a re-configure 2018-05-17 20:16:51 -07:00
Girish Ramakrishnan 6fd314fe82 Do not change password on app update
Fixes #554
2018-05-17 19:48:57 -07:00
Girish Ramakrishnan 0c7eaf09a9 bump container versions 2018-05-17 10:00:00 -07:00
Girish Ramakrishnan d0988e2d61 Generate password for mongodb on platform side
Part of #554
2018-05-17 10:00:00 -07:00
Girish Ramakrishnan 4bedbd7167 Generate password for postgresql on platform side
Part of #554
2018-05-17 10:00:00 -07:00
Girish Ramakrishnan 7ca7901a73 Generate password for mysql on platform side
Part of #554
2018-05-17 09:59:57 -07:00
Girish Ramakrishnan d28dfdbd03 Add 2.3.0 changes 2018-05-17 09:24:47 -07:00
Girish Ramakrishnan c85ca3c6e2 account setup simply redirects to main page now 2018-05-17 09:17:08 -07:00
Girish Ramakrishnan da934d26af call callback 2018-05-17 09:16:32 -07:00
Girish Ramakrishnan f7cc49c5f4 move platform config to db
this way it can be tied up to some REST API later

part of #555
2018-05-16 17:34:56 -07:00
Girish Ramakrishnan 27e263e7fb lint 2018-05-16 14:08:54 -07:00
Girish Ramakrishnan 052050f48b Add a way to persist addon memory configuration
Fixes #555
2018-05-16 14:00:55 -07:00
Girish Ramakrishnan 81e29c7c2b Make the INFRA_VERSION_FILE more readable 2018-05-16 09:54:42 -07:00
Girish Ramakrishnan c3fbead658 Allow zoneName to be changed in domain update route 2018-05-15 15:39:30 -07:00
Girish Ramakrishnan 36f5b6d678 manual dns: handle ENOTFOUND
Fixes #548
2018-05-15 15:39:18 -07:00
Girish Ramakrishnan a45b1449de Allow ghost users to skip 2fa 2018-05-14 15:07:01 -07:00
Girish Ramakrishnan a1020ec6b8 remove /user from profile route 2018-05-13 21:53:06 -07:00
Johannes Zellner d384284ec8 Add name.com DNS provider in the CHANGES file 2018-05-11 10:03:58 +02:00
Girish Ramakrishnan bd29447a7f gcdns: Fix typo 2018-05-10 10:05:42 -07:00
Johannes Zellner aa5952fe0b Wait longer for dns in apptask
name.com often takes longer to sync all nameservers, which means we
timeout too early for them
2018-05-10 15:37:47 +02:00
Johannes Zellner 39dc5da05a We have to return a value on dns record upserting 2018-05-09 18:58:09 +02:00
Johannes Zellner d0e07d995a Add name.com dns tests 2018-05-09 18:13:21 +02:00
Johannes Zellner 94408c1c3d Add name.com DNS provider 2018-05-09 18:13:14 +02:00
Girish Ramakrishnan 66f032a7ee route53: use credentials instead of dnsConfig 2018-05-07 23:41:03 -07:00
Girish Ramakrishnan 4356df3676 bump timeout 2018-05-07 16:28:11 -07:00
Girish Ramakrishnan 1e730d2fc0 route53: more test fixing 2018-05-07 16:20:03 -07:00
Girish Ramakrishnan e8875ccd2e godaddy: add tests 2018-05-07 16:09:00 -07:00
Girish Ramakrishnan 2b3656404b route53: fix tests 2018-05-07 15:53:08 -07:00
Girish Ramakrishnan 60b5e6f711 gandi: add tests 2018-05-07 15:51:51 -07:00
Girish Ramakrishnan b9166b382d route53: set listHostedZonesByName for new/updated domains 2018-05-07 13:42:10 -07:00
Girish Ramakrishnan d0c427b0df Add more 2.2 changes 2018-05-07 11:46:27 -07:00
Girish Ramakrishnan da5d0c61b4 godaddy: workaround issue where there is no del record API 2018-05-07 11:41:37 -07:00
Girish Ramakrishnan 1f75c2cc48 route53: add backward compat for pre-2.2 IAM perms
backward compat for 2.2, where we only required access to "listHostedZones"
2018-05-07 11:24:34 -07:00
Girish Ramakrishnan d0197aab15 Revert "No need to iterate over the hosted zones anymore"
This reverts commit e4a70b95f5.

We will add backward compat route for pre-2.2 cloudrons
2018-05-07 11:23:28 -07:00
Johannes Zellner e4a70b95f5 No need to iterate over the hosted zones anymore 2018-05-07 16:35:32 +02:00
Johannes Zellner f4d3d79922 Query only requested Route53 zone
Fixes #550
2018-05-07 16:30:42 +02:00
Girish Ramakrishnan e3827ee25f Add more 2.2 changes 2018-05-06 23:52:02 -07:00
Girish Ramakrishnan 9981ff2495 Add GoDaddy Domain API 2018-05-06 23:07:52 -07:00
Girish Ramakrishnan 722b14b13d Add note on MX records 2018-05-06 22:14:39 -07:00
Girish Ramakrishnan eb2fb6491c gandi: more fixes 2018-05-06 21:16:47 -07:00
Girish Ramakrishnan a53afbce91 Add Gandi LiveDNS backend 2018-05-06 19:48:51 -07:00
Girish Ramakrishnan 31af6c64d0 Expire existing webadmin token so that the UI gets a new token 2018-05-06 13:08:22 -07:00
Girish Ramakrishnan e8efc5a1b2 Fix test 2018-05-06 12:58:39 -07:00
Girish Ramakrishnan 0c07c6e4d0 Allow "-" in usernames
now that username and mailboxes are totally separate, we can allow '-'.
'+' is still reserved because LDAP it.

Fixes #509
2018-05-05 09:56:21 -07:00
Girish Ramakrishnan da5fd71aaa Bump mail container for CRAM-MD5 login fix 2018-05-04 21:57:26 -07:00
Girish Ramakrishnan d57d590363 2.2.0 changes 2018-05-04 10:30:24 -07:00
Johannes Zellner d6e49415d4 Only list user mailboxes in ldap search 2018-05-04 17:02:04 +02:00
Johannes Zellner cb73eb61d4 Allow binds against mailboxes 2018-05-04 17:02:04 +02:00
Johannes Zellner 4ce3a262a3 Allow search for mailboxes over ldap for a specific domain 2018-05-04 17:02:04 +02:00
Girish Ramakrishnan d18d1a977a Add duplicate profile route for compat with old apps using oauth addon 2018-05-03 09:29:46 -07:00
Girish Ramakrishnan 616e38189c Revert "x"
This reverts commit 182ea3dac3.
2018-05-03 09:29:23 -07:00
Johannes Zellner 726cafcee4 Rollback appdb record on clone and install if appstore purchase fails 2018-05-03 13:20:34 +02:00
Girish Ramakrishnan e5c43e9acd Remove debug 2018-05-02 12:41:22 -07:00
Girish Ramakrishnan f09e8664d1 Return canonical scope in REST responses
The '*' scope is purely an implementation detail. It cannot
be requested as such.
2018-05-02 12:36:41 -07:00
Girish Ramakrishnan 182ea3dac3 x 2018-05-01 15:17:48 -07:00
Girish Ramakrishnan 97acd40829 remove obsolete roleSdk from the database
the file is renamed because cloudron.io was patched by mistake and we want to run this
migration there
2018-05-01 14:53:46 -07:00
Girish Ramakrishnan f1abb2149d gravatar url is already generated client side 2018-05-01 14:30:48 -07:00
Girish Ramakrishnan 8c4015851a merge auth.js into accesscontrol.js 2018-05-01 14:03:10 -07:00
Girish Ramakrishnan a545bdd574 merge developer.js into clients.js 2018-05-01 14:02:59 -07:00
Girish Ramakrishnan d1135accbd lint 2018-05-01 13:58:13 -07:00
Girish Ramakrishnan d5b594fade return the scope as part of the user profile
send canonical scope in the profile response
2018-05-01 13:25:47 -07:00
Girish Ramakrishnan c5ffb65563 Fix usage of normalizeScope 2018-05-01 13:21:53 -07:00
Girish Ramakrishnan f76a5a7ba7 Move the clients API out of oauth prefix 2018-05-01 11:30:51 -07:00
Girish Ramakrishnan 17bcd95961 typo: return the scope as the scope 2018-05-01 10:59:46 -07:00
Girish Ramakrishnan 23bc0e8db7 Remove SDK Role
Just compare with the token's clientId instead
2018-04-30 23:03:30 -07:00
Girish Ramakrishnan 240ee5f563 Ensure we hand out max user.scope
The token.scope was valid at token creation time. The user's scope
could since have changed (maybe we got kicked out of a group).
2018-04-30 22:51:57 -07:00
Girish Ramakrishnan 200f43a58e lint 2018-04-30 22:41:23 -07:00
Girish Ramakrishnan 61d803f528 Use SCOPE_ANY everywhere 2018-04-30 21:44:24 -07:00
Girish Ramakrishnan e7c8791356 lint 2018-04-30 21:41:09 -07:00
Girish Ramakrishnan bc4f9cf596 Remove redundant requireAdmin
We already hand out scopes based on the user's access control
2018-04-30 21:38:48 -07:00
Girish Ramakrishnan 9789966017 Set the scope for a token basedon what the user has access to 2018-04-30 21:21:18 -07:00
Girish Ramakrishnan 1432d90f37 lint 2018-04-30 21:13:23 -07:00
Girish Ramakrishnan 68317a89cb remove the analytics hooks 2018-04-30 15:33:25 -07:00
Girish Ramakrishnan c84f984205 No need to create a token on password reset 2018-04-30 15:15:05 -07:00
Johannes Zellner 6e19153350 Remove now unused result argument 2018-04-30 20:37:12 +02:00
Johannes Zellner 4dc778f7c2 Change reset password button text 2018-04-30 20:17:56 +02:00
Johannes Zellner c5c3748aa9 Do not auto login on password reset 2018-04-30 19:55:59 +02:00
Girish Ramakrishnan f809e359c9 refactor the global variables as functions 2018-04-29 20:17:45 -07:00
Girish Ramakrishnan 91e846d976 Add SCOPE_DOMAINS 2018-04-29 18:11:33 -07:00
Girish Ramakrishnan b5f8ca6c16 Fix nasssty typo 2018-04-29 17:50:12 -07:00
Girish Ramakrishnan 922ab3bde1 lint 2018-04-29 17:50:12 -07:00
Girish Ramakrishnan 3b7bcc1f61 refactor scopes into accesscontrol.js
this will be our authorization layer for oauth and non-oauth tokens.
2018-04-29 17:50:07 -07:00
Girish Ramakrishnan 6e3b060615 Use SCOPE_PROFILE constant 2018-04-29 17:12:30 -07:00
Girish Ramakrishnan cc113d0bb5 Add SCOPE_CLIENTS for oauth clients API 2018-04-29 17:03:10 -07:00
Girish Ramakrishnan 3e22d513eb Add SCOPE_MAIL for mail APIs 2018-04-29 17:01:12 -07:00
Girish Ramakrishnan 9cf51ef680 Update cid-webadmin to have all the scopes possible 2018-04-29 16:57:49 -07:00
Girish Ramakrishnan 1c55a3e310 typo 2018-04-29 11:29:21 -07:00
Girish Ramakrishnan d8acf92929 UserError -> UsersError 2018-04-29 11:22:15 -07:00
Girish Ramakrishnan 7bb8d059b5 GroupError -> GroupsError 2018-04-29 11:21:01 -07:00
Girish Ramakrishnan 863afc68cb DomainError -> DomainsError 2018-04-29 11:20:12 -07:00
Girish Ramakrishnan 4fd58fb46b Rename user.js to users.js 2018-04-29 11:19:04 -07:00
Girish Ramakrishnan b1b664ceca Move removeInternalAppFields to model code 2018-04-29 10:47:34 -07:00
Girish Ramakrishnan 1a27009fb5 Make expiresAt a body parameter 2018-04-28 22:02:07 -07:00
Girish Ramakrishnan 6c8c206e89 Move input validation logic to routes 2018-04-28 21:58:56 -07:00
Girish Ramakrishnan 82207c3ccd Keep naming consistent with delToken 2018-04-28 21:55:57 -07:00
Girish Ramakrishnan 6768994bbe Capitalize the 2fa text 2018-04-28 10:20:33 -07:00
Girish Ramakrishnan b72efb1018 Remove private fields when listing domains
Currently, domains list route does not return the fallback cert.
make it future proof, just in case.
2018-04-27 11:41:30 -07:00
Johannes Zellner 7a8c525beb Add 2fa Oauth route tests 2018-04-27 14:08:22 +02:00
Johannes Zellner 9372d8797a Add 2fa tests for developer login api 2018-04-27 12:29:11 +02:00
Johannes Zellner faeb89b258 Add 2fa token login to oauth login form 2018-04-27 11:46:41 +02:00
Johannes Zellner 50d7ade0d9 Remove unused property on set 2fa secret 2018-04-27 08:20:12 +02:00
Johannes Zellner 497c76a905 Add 2fa token check to developer login 2018-04-27 08:18:13 +02:00
Johannes Zellner bbc434dc21 Attach the whole user object also for basic authed routes 2018-04-27 08:18:13 +02:00
Girish Ramakrishnan a7bb5d6b5c add route to query apps specific to user
it's not nice to overload a route to mean different things depending
on who queries it.
2018-04-26 20:07:12 -07:00
Girish Ramakrishnan e0da6679e9 Move user routes to /api/v1/user 2018-04-26 19:57:44 -07:00
Girish Ramakrishnan 561d2d9f8b admin column is no more in users table 2018-04-26 19:55:26 -07:00
Girish Ramakrishnan 7549b3e837 enhance user creation API to take a password 2018-04-26 14:13:40 -07:00
Girish Ramakrishnan 7756c07bc6 Add better text for the secret 2018-04-26 09:39:35 -07:00
Johannes Zellner 0d58a6bf33 Send 2fa auth status with profile info 2018-04-26 16:29:40 +02:00
Johannes Zellner fbba636fb3 Handle more 2fa route errors 2018-04-26 16:14:37 +02:00
Johannes Zellner 9cd6333cf7 2fa routest work with the req.user object 2018-04-26 15:12:14 +02:00
Johannes Zellner eb02c182e5 Avoid linter issue 2018-04-26 14:45:12 +02:00
Johannes Zellner 6574b22cf6 Add 2fa routest and business logic 2018-04-26 08:46:35 +02:00
Girish Ramakrishnan d1ed2aa2ce remove untested route set_admin 2018-04-25 21:43:21 -07:00
Girish Ramakrishnan c2a762cb29 Do not reserve mailbox names
Now that user management is split from mailboxes, we don't need to
reserve mailbox names anymore.
2018-04-25 21:14:33 -07:00
Girish Ramakrishnan 34d40edef4 Fix issue where docker needs more recent packages
The following packages have unmet dependencies:
 docker-ce : Depends: libseccomp2 (>= 2.3.0) but 2.2.3-3ubuntu3 is to be installed
E: Unable to correct problems, you have held broken packages.
2018-04-25 13:44:53 -07:00
Johannes Zellner 5ceb14cbca Add required 2fa node modules qrcode and speakeasy 2018-04-25 17:03:16 +02:00
Johannes Zellner 38668937ad Fixup database tests 2018-04-25 17:03:16 +02:00
Johannes Zellner 0167f83d4a Handle 2fa fields in userdb code 2018-04-25 17:03:16 +02:00
Johannes Zellner 9e66adb6d0 Add 2fa db record fields to users table 2018-04-25 17:03:16 +02:00
Girish Ramakrishnan 0a537029bc add mailbox domain to the constraint 2018-04-23 10:14:16 -07:00
Girish Ramakrishnan c0716e86a7 Remove the "or Email"
The email here can be confused with the Cloudron email which it is not.
The preferred approach is to login via username anyway.
2018-04-22 18:29:10 -07:00
Girish Ramakrishnan 50185adcf4 Add 2.1.1 changes 2018-04-18 12:49:11 -07:00
Johannes Zellner 0c728c6af5 Fix mail rest api tests 2018-04-13 12:54:40 +02:00
Johannes Zellner 34d3d79b12 Improve error message when alias name is already taken 2018-04-13 12:37:27 +02:00
Johannes Zellner ff856a5978 Rename 'address' catchall property to 'addresses' to better indiciate this being an array 2018-04-13 12:15:15 +02:00
Johannes Zellner c4dad2f55f Fix address property error response in catchall 2018-04-13 12:15:15 +02:00
Girish Ramakrishnan 734286ba2e Add support for installing private docker images 2018-04-12 11:43:57 -07:00
Girish Ramakrishnan 0f7f8af4b2 Use docker 18.03.0-ce
17.12.0-ce has strange issues like https://github.com/moby/moby/issues/34097
2018-04-11 18:25:19 -07:00
Johannes Zellner 60381d938e Fix search and replace mistake 2018-04-11 15:29:37 +02:00
Johannes Zellner ddaa52163b Update ssl ciphers according to mozillas recommendation 2018-04-11 15:15:29 +02:00
Johannes Zellner 799c1ba05d Improve on the csp header restriction 2018-04-11 13:00:08 +02:00
Johannes Zellner 838838b90d nginx would drop other headers if add_header is defined in the location section 2018-04-11 12:29:57 +02:00
Girish Ramakrishnan 4554d9f2f8 Add more changes 2018-04-10 15:13:04 -07:00
Johannes Zellner 573d0e993e Add CSP header for dashboard 2018-04-10 17:59:06 +02:00
Johannes Zellner 97313fe1c8 Remove other unused assets from the release tarball 2018-04-10 14:08:13 +02:00
Johannes Zellner 944f743438 Use the node modules defined in the dashboard repo 2018-04-10 13:51:01 +02:00
Johannes Zellner 96a5b0e6ba Remove dashboard related node modules 2018-04-10 13:12:42 +02:00
Girish Ramakrishnan 95f7e50065 bump mail container 2018-04-10 00:00:27 -07:00
Girish Ramakrishnan d6a8837716 mail: verify with the owner id 2018-04-09 13:17:07 -07:00
Johannes Zellner cc759e3550 set the mailbox record type for apps 2018-04-09 15:39:36 +02:00
Girish Ramakrishnan bf0dd935e5 mail: add type field 2018-04-07 21:29:44 -07:00
Girish Ramakrishnan 1d761deec0 Fix test 2018-04-07 18:39:17 -07:00
Girish Ramakrishnan b6335a327c Rename TYPE_* to OWNER_TYPE_* 2018-04-07 18:33:30 -07:00
Johannes Zellner 55d53ef311 Do not succeed if mailbox name is already taken 2018-04-06 16:55:01 +02:00
Johannes Zellner 878940edae Fix sql syntax 2018-04-06 15:54:55 +02:00
Johannes Zellner 15648a3ab2 fix typo name -> username 2018-04-06 14:53:20 +02:00
Johannes Zellner 2fae98dd5b pass the dashboard version as a revision to the gulp file 2018-04-06 07:47:42 +02:00
Girish Ramakrishnan 9beeb33090 mail: validate list and mailbox names 2018-04-05 17:49:16 -07:00
Girish Ramakrishnan 605dc00422 mail: add members field for lists
we have to track the members of a list in the mail app separately
from groups. this is required because users can now have multiple
mailboxes. and because of that we cannot do a 1-1 mapping of group
members to mailboxes anymore. the ui is changed to select mailboxes
when creating a list.
2018-04-05 16:07:38 -07:00
Girish Ramakrishnan 2c8fa01d6d mail: split the functions to add list and mailbox 2018-04-05 15:01:28 -07:00
Girish Ramakrishnan 467bfa2859 remove mailboxdb from groups code 2018-04-04 20:08:52 -07:00
Girish Ramakrishnan affb420181 cloudron-setup: highlight reboot in red 2018-04-04 09:55:22 -07:00
Girish Ramakrishnan e7b26e5655 Add note on accepting self-signed cert 2018-04-04 09:54:14 -07:00
Girish Ramakrishnan 5af657ee22 rename mail crud functions 2018-04-03 15:06:14 -07:00
Girish Ramakrishnan 7fac92c519 validate user id when adding mailbox 2018-04-03 14:27:09 -07:00
Girish Ramakrishnan f8a731f63a Add routes to change the mailbox and list owner 2018-04-03 14:12:43 -07:00
Girish Ramakrishnan a1f4a4d614 mail: make mailbox API based on mailbox name
this decouples mail API from users
2018-04-03 13:59:03 -07:00
Girish Ramakrishnan 696e864459 mail: make list API based on list name
this decouples mail API from groups
2018-04-03 12:06:22 -07:00
Girish Ramakrishnan 678ea50f87 validateAlias -> validateName 2018-04-03 09:47:15 -07:00
Girish Ramakrishnan 69d3b3cac8 2.0.2 -> 2.1.0 2018-04-02 13:37:06 -07:00
Girish Ramakrishnan 76915b99a8 Fix linter 2018-04-02 09:46:30 -07:00
Girish Ramakrishnan 255a5a12a5 Decouple mailbox deletion from user delete 2018-04-02 09:45:46 -07:00
Johannes Zellner 602291895c Mention which alias is reserved 2018-04-02 14:59:10 +02:00
Johannes Zellner 045ea4681a Do not return an error on mailinglist listing if none exists
We usually return the empty array, to avoid the need for specific error
handling
2018-04-01 21:51:56 +02:00
Johannes Zellner e364661813 Send correct status code if mail alias already exists 2018-04-01 19:29:47 +02:00
Johannes Zellner df9a191434 Add rest api to list all aliases for a given domain 2018-04-01 18:23:54 +02:00
Johannes Zellner b4aac42032 Add more changes for 2.0.2 2018-04-01 15:15:52 +02:00
Johannes Zellner 2a8be279e7 The package lock now uses sha512 for checksum 2018-04-01 13:15:05 +02:00
Johannes Zellner 4af69fb8c8 Do not show a warning like log, but just dump the tag and detail 2018-03-29 17:36:00 +02:00
Girish Ramakrishnan cbc98a48ef Slight wording change 2018-03-28 10:17:17 -07:00
Girish Ramakrishnan 874541b988 Add issue templates 2018-03-28 10:14:35 -07:00
Girish Ramakrishnan 0aa1b758ec Update docker to 17.12.0-ce 2018-03-26 16:34:33 -07:00
Girish Ramakrishnan 2e0c632942 Do not crash if mail alias does not validate 2018-03-25 21:08:15 -07:00
Girish Ramakrishnan 82a593e82a Forward stats calls to mail container 2018-03-23 10:52:07 -07:00
Girish Ramakrishnan e33ebe7304 Revert "mysql: increase max_allowed_packet"
This reverts commit 9123ea7016.

Not needed. This was a db corruption issue
2018-03-22 21:49:08 -07:00
Girish Ramakrishnan d81930be72 add note on conn limit 2018-03-22 21:07:06 -07:00
Girish Ramakrishnan aac914182f remove options from database.initialize 2018-03-22 20:34:49 -07:00
Girish Ramakrishnan 26d4a11c44 cleanup eventlog more aggressively
Those login entries are really adding up on old cloudrons
2018-03-22 20:31:32 -07:00
Girish Ramakrishnan f498443cae remove unused exports 2018-03-22 20:29:26 -07:00
Girish Ramakrishnan d84d761bad Remove unused export 2018-03-22 19:40:38 -07:00
Girish Ramakrishnan 07601d1292 Fix schema 2018-03-22 18:41:10 -07:00
Girish Ramakrishnan 6cbe964301 Add note 2018-03-22 17:13:32 -07:00
Girish Ramakrishnan 84dcdbba33 Re-assign 2020 to mail server 2018-03-21 23:15:30 -07:00
Girish Ramakrishnan 9123ea7016 mysql: increase max_allowed_packet
some cloudrons are reporting some errors after 2.0. maybe all those
additional joins/fields we put in is causing this
2018-03-21 17:52:22 -07:00
Girish Ramakrishnan 2a18070016 do-spaces: Force retry of 4xx error codes when copying 2018-03-21 15:41:21 -07:00
Girish Ramakrishnan e0ece06b26 s3: improved copy logging 2018-03-21 14:22:41 -07:00
Girish Ramakrishnan 83d2eb31dd clarify debug 2018-03-21 11:39:16 -07:00
Girish Ramakrishnan c6b8ad88dd 2.0.2 changes 2018-03-20 20:04:35 -07:00
Girish Ramakrishnan 6adf88a6e5 Make uploads work with very slow upload speeds
chunk uploads get a timeout of 2mins (derived from http.timeout).
On servers like kimsufi, uploads takes forever (100 MB/sec limit).
Currently, our upload code does not dynamically adapt itself to
changing the concurrency when network is slow.
2018-03-20 19:37:45 -07:00
Girish Ramakrishnan 7699f6721d Add hack to figure out the position in the queue
this helps us track the progress a bit in the logs
2018-03-20 19:37:35 -07:00
Girish Ramakrishnan ce33681c37 Dump etag info 2018-03-20 18:19:14 -07:00
Girish Ramakrishnan 565eed015f Add better backup logs 2018-03-20 16:41:45 -07:00
Girish Ramakrishnan dd296544be Remove extra prefix 2018-03-15 14:30:10 -07:00
Girish Ramakrishnan a07c4423c4 Rename webadmin to dashboard
The box nginx config has to be re-generated but this is always
done at box restart time
2018-03-15 14:14:23 -07:00
Girish Ramakrishnan 65f07cb7c0 Add more changes 2018-03-14 09:15:58 -07:00
Girish Ramakrishnan 8d1a6cb06b Add more changes 2018-03-14 09:14:45 -07:00
Girish Ramakrishnan 873ea0fecd Restart mail server after DKIM keys are generated
Haraka won't do change notification on those
2018-03-13 09:53:41 -07:00
Girish Ramakrishnan ace1f36f9c 2.0.1 changes 2018-03-13 00:36:58 -07:00
Girish Ramakrishnan 4cc9818139 remove error prone short-circuit update
when we do pre-releases, there really is no way for us to update
all the cloudrons. this worked when everything was managed cloudron.
2018-03-13 00:36:03 -07:00
Girish Ramakrishnan 390639bac0 Bump mail container
This fixes delivery of incoming mail from an outbound only domain
2018-03-13 00:20:48 -07:00
Girish Ramakrishnan 830c685ead recreate mail configs when mail domain is added 2018-03-12 21:14:45 -07:00
Girish Ramakrishnan 65b174f950 Domain removal can fail because of mailbox as well 2018-03-12 09:54:16 -07:00
Girish Ramakrishnan 331ed4e6b9 Pass on any appstore purchase error 2018-03-11 12:43:24 -07:00
Girish Ramakrishnan afef548097 cloudron-setup: make sure --help runs as non-root 2018-03-09 10:37:18 -08:00
Johannes Zellner 60e924d5b8 We do require a domain entry in the mails table always 2018-03-09 14:26:33 +01:00
Johannes Zellner c0ea91a688 We have to parse the JSON data from the raw db results 2018-03-09 10:27:13 +01:00
Girish Ramakrishnan ecf1f9255d relay: cloudron-smtp can always be set 2018-03-08 23:23:02 -08:00
Girish Ramakrishnan 1125643a80 Add Haraka 2.8.18 to changelog 2018-03-08 20:12:34 -08:00
Girish Ramakrishnan 61243f6a09 Wait for DNS records call to finish 2018-03-08 20:08:01 -08:00
Girish Ramakrishnan 2e156aa34a simplify the configureWebadmin logic 2018-03-08 18:26:44 -08:00
Girish Ramakrishnan 440629530f remove redundant check for "test" mode 2018-03-08 18:15:09 -08:00
Girish Ramakrishnan 3922824dc6 no reason to keep retrying 2018-03-08 18:00:16 -08:00
Girish Ramakrishnan 6bc5add023 Add a way to re-sync mail DNS records
Also, make restore resync the admin domain record which gets messed
up by the dns setup
2018-03-08 17:59:53 -08:00
Girish Ramakrishnan f284245e16 dkim keys are needed for the test 2018-03-08 16:10:33 -08:00
Girish Ramakrishnan ac62ee5a16 better debug 2018-03-08 15:29:18 -08:00
Girish Ramakrishnan 66f251be06 dnsSetup must fail if domain already exists 2018-03-08 15:10:38 -08:00
Girish Ramakrishnan ab932c4f5c Do not regenerate domain key if domain already exists 2018-03-08 15:01:08 -08:00
Girish Ramakrishnan 074c6fdba3 More renaming to forum 2018-03-08 10:50:18 -08:00
Girish Ramakrishnan b36f4becbc remove bad changelog 2018-03-08 10:22:46 -08:00
Girish Ramakrishnan ac69b96f92 clear timeout when getting mail status 2018-03-08 09:32:06 -08:00
Girish Ramakrishnan 6da7a7d2f4 clear the request timeout when adding/updating domain
DO API takes very long to respond at times :/ Currently, there is
no easy way to reset the timeout middleware timeout. We should add
this feature upstream (https://github.com/expressjs/timeout/issues/26)
2018-03-08 09:27:56 -08:00
Girish Ramakrishnan 22c54ced05 email: add MAIL_DOMAINS 2018-03-07 20:39:58 -08:00
Girish Ramakrishnan c7b1d49de6 cloudron-setup: add some color 2018-03-07 15:51:00 -08:00
Girish Ramakrishnan b7bf5b180c Display the format as well 2018-03-07 13:59:17 -08:00
Girish Ramakrishnan 12aba46893 use apps.getAll since app.fqdn is otherwise undefined 2018-03-07 13:39:40 -08:00
Johannes Zellner 9d4eee0dfe reword error message, all apps using a domain have to be deleted 2018-03-07 10:45:03 +01:00
Girish Ramakrishnan d69c8f49e5 Migrate daily update pattern 2018-03-06 21:29:08 -08:00
Girish Ramakrishnan dd5f41aee8 Fix failing test 2018-03-06 01:35:38 -08:00
Girish Ramakrishnan 0b20b265de Do not crash if domain is not found 2018-03-06 01:30:40 -08:00
Girish Ramakrishnan ac94d0b5c7 Make apps also auto-update like before by default 2018-03-05 21:33:18 -08:00
Girish Ramakrishnan c5a70d10d7 Add robotsTxt to appdb.add 2018-03-05 16:14:23 -08:00
Girish Ramakrishnan b83eb993d8 Add sso and robotsTxt to app backup config.json 2018-03-05 15:03:03 -08:00
Girish Ramakrishnan 6cadaca307 clone: copy enableBackup and robotsTxt 2018-03-05 14:56:24 -08:00
Girish Ramakrishnan 36b91ae7db Add PSBL 2018-03-05 14:26:53 -08:00
Johannes Zellner 3115432309 Fix missed eventlog.getAllPaged() usage 2018-03-05 17:53:18 +01:00
Johannes Zellner 8340f77e20 Fixup the database tests 2018-03-05 17:17:01 +01:00
Johannes Zellner 75932e2805 Collect app information for feedback email 2018-03-05 17:03:54 +01:00
Johannes Zellner ff6d468604 Support multiple actions for eventlog api 2018-03-05 11:46:06 +01:00
Girish Ramakrishnan 161b2ac6f5 clone: Fix crash where port conflict is not handled 2018-03-02 19:37:15 -08:00
Johannes Zellner 9775ab5e8e make correct use of eventlog for developer/cli login 2018-03-02 19:26:55 +01:00
Johannes Zellner 726202b040 Amend app object where applicable to login event 2018-03-02 19:21:11 +01:00
Johannes Zellner 39d6ec96b7 amend full user object to login action 2018-03-02 19:21:11 +01:00
Johannes Zellner 87fedb71b7 Use shared function to remove private user fields for api 2018-03-02 11:24:27 +01:00
Johannes Zellner 8424e687cb Amend full user object to user action eventlog entries 2018-03-02 11:24:06 +01:00
Johannes Zellner c0d030c978 Amend full user object to user eventlog action entries 2018-03-02 11:02:32 +01:00
Johannes Zellner 53470e286f Use app model code to get all amended properties 2018-03-02 10:58:05 +01:00
Girish Ramakrishnan e22c17eabe Fix issue where new package versions are getting skipped 2018-03-01 11:39:10 -08:00
Girish Ramakrishnan 5ac1fccb98 mail: Fix crashes when user has no username yet 2018-02-28 13:31:28 -08:00
Girish Ramakrishnan 0cc58fafd6 Do not crash if user does not have username 2018-02-28 13:18:41 -08:00
Girish Ramakrishnan 98e19e6df5 fix upload errors causing double callback 2018-02-27 19:16:03 -08:00
Girish Ramakrishnan 441e514119 scheduler: give scheduler tasks twice the memory by default 2018-02-27 15:03:09 -08:00
Girish Ramakrishnan ff4b09a342 Use the container StartedAt instead of lastDate
CronJob.lastDate keeps resetting on every tick. Also, it doesn't
work across box code restarts.
2018-02-27 14:26:40 -08:00
Girish Ramakrishnan f8c8133148 scheduler: better debugs 2018-02-27 13:54:38 -08:00
Girish Ramakrishnan 938a41e12c scheduler: give cron jobs a grace period of 30 mins to complete 2018-02-27 13:28:42 -08:00
Girish Ramakrishnan 5d231f4fef scheduler: do no start all cronjobs at once 2018-02-27 12:44:11 -08:00
Girish Ramakrishnan a4e6181edf Fix tests 2018-02-27 11:59:15 -08:00
Girish Ramakrishnan 6685118b03 Use safe.JSON.parse instead
safe.require() caches the credentials which is annoying
2018-02-27 11:24:08 -08:00
Girish Ramakrishnan 4c9919a98b Drop the "your" 2018-02-27 09:22:43 -08:00
Girish Ramakrishnan 470c9971f8 mail exchange does not have trailing dot 2018-02-23 17:26:28 -08:00
Girish Ramakrishnan b6fb49956f s3: better debug output when copying 2018-02-22 12:41:18 -08:00
Girish Ramakrishnan 0bba985ff1 storage: Add implementation note 2018-02-22 12:30:55 -08:00
Girish Ramakrishnan 3c8c15db01 s3: use a constant backoff since it takes forever to fail otherwise 2018-02-22 12:30:44 -08:00
Girish Ramakrishnan c8a6294772 lint 2018-02-22 12:24:16 -08:00
Girish Ramakrishnan cea83889ec s3: Fix issue where it takes forever to timeout if the backend is down 2018-02-22 12:19:23 -08:00
Girish Ramakrishnan 2ecb66afd7 s3: cleanup code 2018-02-22 12:16:01 -08:00
Girish Ramakrishnan f5d426fd69 debug out the progress message 2018-02-22 11:11:36 -08:00
Girish Ramakrishnan e6c07fc148 merge the done callback into the main code 2018-02-22 11:06:28 -08:00
Girish Ramakrishnan 1f30a4f3ea Make s3.deleteObjects return error 2018-02-22 11:05:29 -08:00
Girish Ramakrishnan 0bfdaeb2fb rename to chunkSize 2018-02-22 11:01:04 -08:00
Girish Ramakrishnan e022dbf8a6 Revert "merge the done callback into the main code"
This reverts commit c39bec8cc1.

This was committed with extra stuff by mistake
2018-02-22 10:58:56 -08:00
Girish Ramakrishnan 0e7e672dd2 Update node modules 2018-02-22 10:52:42 -08:00
Girish Ramakrishnan 6075a7a890 typo 2018-02-22 10:34:48 -08:00
Girish Ramakrishnan 28b864c346 sos: Copy in 96M chunks as recommended by exoscale 2018-02-22 10:31:56 -08:00
Girish Ramakrishnan e9437131ff mail: set domain_selector to be mail_from 2018-02-21 20:46:32 -08:00
Girish Ramakrishnan c39bec8cc1 merge the done callback into the main code 2018-02-21 20:17:58 -08:00
Girish Ramakrishnan 727a25f491 DO Spaces: multipart copy now works 2018-02-20 14:48:03 -08:00
Johannes Zellner 26bacfcbd6 Allow partial match of eventlog actions 2018-02-20 11:20:17 -08:00
Johannes Zellner a777e7aeb3 add full app object to app related eventlog actions 2018-02-20 10:34:09 -08:00
Johannes Zellner 676625a3f6 Add more appstore tests 2018-02-18 22:43:11 -08:00
Johannes Zellner f41603ea94 Add appstore.sendAliveStatus() tests 2018-02-18 21:42:37 -08:00
Johannes Zellner 18ae958e87 Send all domains with provider with the alive post 2018-02-18 21:36:21 -08:00
Johannes Zellner d68d4295de Remove unused require 2018-02-18 20:16:17 -08:00
Girish Ramakrishnan 0244529b45 Add more changelog 2018-02-18 02:45:46 -08:00
Girish Ramakrishnan 1d044a7392 Bump mail container for multi-domain support 2018-02-18 00:54:41 -08:00
Girish Ramakrishnan 06eab93f0e restart mail container when mail.ini changes 2018-02-18 00:54:11 -08:00
Girish Ramakrishnan 84b7672509 caas can be a provider 2018-02-17 10:28:03 -08:00
Girish Ramakrishnan c9cd4ed363 Fix changelog version 2018-02-16 16:43:04 -08:00
Girish Ramakrishnan 05c98ccadb Enable auto-updates for major versions
Cloudron is always rolling releases and we never break compat
2018-02-16 16:01:10 -08:00
Johannes Zellner cb62cdcfa1 Report dependency error for clone if backup or domain was not found 2018-02-16 10:45:06 -08:00
Girish Ramakrishnan c0fddf5d8a Version 1.11.0 changes 2018-02-11 01:22:26 -08:00
Girish Ramakrishnan bcf3e71979 Add API to remove mailboxes by domain 2018-02-11 01:18:29 -08:00
Girish Ramakrishnan baf5cae58a Fix tests 2018-02-11 00:04:41 -08:00
Girish Ramakrishnan 5c1f9d5686 typo 2018-02-11 00:04:28 -08:00
Girish Ramakrishnan 4d89340c7d Handle FK error when deleting mail domain 2018-02-10 22:49:35 -08:00
Girish Ramakrishnan 0b6846787e The mailboxes domain column must reference the mail domain column 2018-02-10 21:31:50 -08:00
Girish Ramakrishnan 79976cd29d add an extra newline in config 2018-02-10 21:29:00 -08:00
Girish Ramakrishnan 574cf1057e mail: ensure mail is disabled when deleting mail domain 2018-02-10 10:38:45 -08:00
Johannes Zellner 1b3450e3a2 update the altDomain migration to also generate certs with SAN 2018-02-10 15:55:22 +01:00
Girish Ramakrishnan bec032702d Remove SAN check
-checkhost already checks the SAN. It is implementation dependent
as to whether the CN is checked for.
2018-02-09 14:20:03 -08:00
Girish Ramakrishnan fc79047bbf Generate fallback cert to contain naked domain in SAN 2018-02-09 13:44:29 -08:00
Girish Ramakrishnan 5263ea860d Add cert tests 2018-02-09 11:19:47 -08:00
Johannes Zellner 5140dee81d Generate a fallback cert for domains added during altDomain migration 2018-02-09 13:08:45 +01:00
Johannes Zellner 24d3195660 Add dns setup and activation route tests 2018-02-09 12:43:20 +01:00
Johannes Zellner 721a4c4349 Validate the adminFqdn in dns setup route 2018-02-09 12:43:03 +01:00
Girish Ramakrishnan 83ff295f6d debug: authenticateMailbox 2018-02-08 18:49:27 -08:00
Girish Ramakrishnan 6decc790d6 Follow CNAME records
DNS records can now be a A record or a CNAME record. All we care
about is them resolving to the public IP of the server somehow.

The main reason for this change is that altDomain is migrated into
domains table and the DNS propagation checks have to work after that.
(previously, the 'altDomain' was a signal for a CNAME check which now
cannot be done post-migration).

In the future, we can make this more sophisticated to instead maybe
do a well-known URI query. That way it will work even if there is
some proxy like Cloudflare in the middle.

Fixes #503
2018-02-08 15:43:31 -08:00
Girish Ramakrishnan 459cf8d0cd Add note on unbound at 127.0.0.1 2018-02-08 14:43:49 -08:00
Girish Ramakrishnan 58386b0c54 remove resolveNs 2018-02-08 14:39:35 -08:00
Girish Ramakrishnan 101c1bda25 translate cancelled errors to timeout errors 2018-02-08 14:27:02 -08:00
Girish Ramakrishnan d31c948d3e Remove type argument from waitForDns
The function is going to be changed to handle only A/CNAME records
2018-02-08 14:24:11 -08:00
Girish Ramakrishnan 0927c8161c Add note on return value of dns.resolve 2018-02-08 14:10:53 -08:00
Girish Ramakrishnan 4d92aea2f3 Fix usage of callback 2018-02-08 14:10:32 -08:00
Girish Ramakrishnan 0ca2451eaa fix tests 2018-02-08 12:09:06 -08:00
Girish Ramakrishnan 3b987f1970 DNS -> Dns 2018-02-08 12:05:29 -08:00
Girish Ramakrishnan a7b0ba2178 PTR must be resolved by the domain and not IP 2018-02-08 11:56:25 -08:00
Girish Ramakrishnan 744e6b8af0 replace the verizon smtp 2018-02-08 11:48:55 -08:00
Girish Ramakrishnan 8254e795be add missing export 2018-02-08 11:42:45 -08:00
Girish Ramakrishnan 26c95a25b6 Use the native dns resolver
it now supports cancel()

also, fixes #514
2018-02-08 11:37:58 -08:00
Girish Ramakrishnan 209f37312b createReleaseTarball must use the local branch for master branch 2018-02-08 08:58:20 -08:00
Johannes Zellner 5bd218b3b6 Fix intrinsicFqdn removal breakage 2018-02-08 15:23:38 +01:00
Johannes Zellner d57b772ada We can use js multiline strings 2018-02-08 15:19:00 +01:00
Johannes Zellner b6384d5025 Remove intrinsicFqdn 2018-02-08 15:07:49 +01:00
Johannes Zellner fa65576688 Remove unused require 2018-02-08 15:04:13 +01:00
Johannes Zellner 3572b4eb91 Do not crash if certs cannot be found. Error object does not exist 2018-02-08 10:27:30 +01:00
Johannes Zellner e710a210fd Fixup the unit tests 2018-02-08 09:00:31 +01:00
Johannes Zellner 265db7d0f7 Fix typo in appdb 2018-02-08 09:00:31 +01:00
Johannes Zellner b1939e73f4 Remove all occurances of altDomain in the code
Tests are pending
2018-02-08 09:00:31 +01:00
Johannes Zellner 28f5f62414 Add altDomain migration script 2018-02-08 09:00:31 +01:00
Girish Ramakrishnan ff577a8ed5 stop and disable postfix for good measure 2018-02-07 09:08:04 -08:00
Johannes Zellner 63d06d7024 Use fresh settings key for app autoupdate pattern 2018-02-07 16:51:53 +01:00
Johannes Zellner 4d4b77d6fb Add 1.10.2 changes 2018-02-07 16:27:54 +01:00
Johannes Zellner 3b4ff18881 Keep the invite email for users, which have not yet setup a username 2018-02-07 16:27:49 +01:00
Girish Ramakrishnan d65cb93158 Remove obsolete action 2018-02-06 23:14:02 -08:00
Girish Ramakrishnan e00f98884c setup SPF record of non-primary domain correctly 2018-02-06 23:11:47 -08:00
Girish Ramakrishnan 21016cc2e0 createReleaseTarball: Make sure we pick the current branch on webadmin 2018-02-06 16:20:29 -08:00
Girish Ramakrishnan d12803bb9d Add 1.10.1 changes 2018-02-06 16:11:06 -08:00
Girish Ramakrishnan 039a31318a Generate per-domain enable_outbound relay settings 2018-02-06 14:43:14 -08:00
Johannes Zellner 3eb11ee20a Fixup updatechecker tests 2018-02-06 19:25:03 +01:00
Johannes Zellner 11d740682e Split box and app autoupdate pattern settings 2018-02-06 19:25:03 +01:00
Johannes Zellner 09b33e7ef9 Disable autoupdates by default 2018-02-06 19:25:03 +01:00
Johannes Zellner 19fafca9df Drop users email unique constraint for the migration timeframe 2018-02-06 12:14:11 +01:00
Girish Ramakrishnan da29c69be4 generate per-domain mail configuration 2018-02-05 15:13:35 -08:00
Johannes Zellner c4531e32d5 Fix all app route tests 2018-02-05 22:17:16 +01:00
Johannes Zellner 8f74cacfd0 Remove unused require 2018-02-05 20:45:53 +01:00
Girish Ramakrishnan 9ba830ab21 "installing" is easier to understand 2018-02-05 11:13:51 -08:00
Girish Ramakrishnan ad152bacdd Do not allow dns setup and restore to run in parallel
In the e2e, we did not check the webadminStatus after a dnsSetup
and immediately rushed into restore. This ended up mangling the
cert/key files of the admin domain.
2018-02-05 09:35:16 -08:00
Johannes Zellner 89673fa7f0 Make more of the app route tests work 2018-02-05 17:28:30 +01:00
Johannes Zellner c8613e646b Show more descriptive error message if minBoxVersion blocks update 2018-02-05 15:20:42 +01:00
142 changed files with 9333 additions and 10721 deletions
+2 -1
View File
@@ -1,6 +1,7 @@
# following files are skipped when exporting using git archive
test export-ignore
docs export-ignore
.jshintrc export-ignore
.gitlab export-ignore
.gitattributes export-ignore
.gitignore export-ignore
+6
View File
@@ -0,0 +1,6 @@
Please do not use this issue tracker for support requests and bug reports.
This issue tracker is used by the Cloudron development team to track actual
bugs in the code.
Please use the forum at https://forum.cloudron.io to report bugs. For
confidential issues, please email us at support@cloudron.io.
+7
View File
@@ -0,0 +1,7 @@
Please do not use this issue tracker for support requests and feature reports.
This issue tracker is used by the Cloudron development team to track issues in
the code.
Please use the forum at https://forum.cloudron.io to report bugs. For
confidential issues, please email us at support@cloudron.io.
+1
View File
@@ -2,6 +2,7 @@
"node": true,
"browser": true,
"unused": true,
"multistr": true,
"globalstrict": true,
"predef": [ "angular", "$" ],
"esnext": true
+93
View File
@@ -1205,3 +1205,96 @@
* Configure Exoscale SOS to use new SOS NG endpoint
* Fix S3 storage backend CopySource encoding rules
[1.11.0]
* Update Haraka to 2.8.17 to fix various crashes
* Report dependency error for clone if backup or domain was not found
* Enable auto-updates for major versions
[2.0.0]
* Multi-domain support
* Update Haraka to 2.8.18
* Split box and app autoupdate pattern settings
* Stop and disable any pre-installed postfix server
* Migrate altDomain as a manual DNS provider
* Use node's native dns resolve instead of dig
* DNS records can now be a A record or a CNAME record
* Fix generation of fallback certificates to include naked domain
* Merge multi-string DKIM records
* scheduler: do not start cron jobs all at once
* scheduler: give cron jobs a grace period of 30 minutes to complete
[2.0.1]
* Multi-domain support
* Update Haraka to 2.8.18
* Split box and app autoupdate pattern settings
* Stop and disable any pre-installed postfix server
* Migrate altDomain as a manual DNS provider
* Use node's native dns resolve instead of dig
* DNS records can now be a A record or a CNAME record
* Fix generation of fallback certificates to include naked domain
* Merge multi-string DKIM records
* scheduler: do not start cron jobs all at once
* scheduler: give cron jobs a grace period of 30 minutes to complete
* Rework the eventlog view
* App clone now clones the robotsTxt and backup settings
[2.1.0]
* Make S3 backend work reliably with slow internet connections
* Update docker to 18.03.0-ce
* Finalize the Email and Mailbox API
* Move mailbox settings from users to email view
* mail: fix issue where hosts with valid SPF for a Cloudron domain are unable to send mail to Cloudron
* mail: fix crash when bounce emails have a null sender
* Add CSP header for dashboard
* Add support for installing private docker images
[2.1.1]
* Make S3 backend work reliably with slow internet connections
* Update docker to 18.03.0-ce
* Finalize the Email and Mailbox API
* Move mailbox settings from users to email view
* mail: fix issue where hosts with valid SPF for a Cloudron domain are unable to send mail to Cloudron
* mail: fix crash when bounce emails have a null sender
* Add CSP header for dashboard
* Add support for installing private docker images
[2.2.0]
* Add 2FA support for the admin dashboard
* Cleanup scope management in REST API
* Enhance user creation API to take a password
* Relax restriction on mailbox names now that it is decoupled from user management
[2.2.1]
* Add 2FA support for the admin dashboard
* Add Gandi & GoDaddy DNS providers
* Fix zone detection logic on Route53 accounts with more than 100 zones
* Warn using when disabling email
* Cleanup scope management in REST API
* Enhance user creation API to take a password
* Relax restriction on mailbox names now that it is decoupled from user management
* Fix issue where mail container incorrectly advertised CRAM-MD5 support
[2.3.0]
* Add Name.com DNS provider
* Fix issue where account setup page was crashing
* Add advanced DNS configuration UI
* Preserve addon/database configuration across app updates and restores
* ManageSieve port now offers STARTTLS
[2.3.1]
* Add Name.com DNS provider
* Fix issue where account setup page was crashing
* Add advanced DNS configuration UI
* Preserve addon/database configuration across app updates and restores
* ManageSieve port now offers STARTTLS
* Allow mailbox name to be set for apps
* Rework the Email server UI
* Add the ability to manually trigger a backup of an application
* Enable/disable mail from validation within UI
* Allow setting app visibility for non-SSO apps
* Add Clone UI
[2.3.2]
* Fix issue where multi-db apps were not provisioned correctly
* Improve setup, restore views to have field labels
+1 -1
View File
@@ -630,7 +630,7 @@ state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
box
Copyright (C) 2016,2017 Cloudron UG
Copyright (C) 2016,2017,2018 Cloudron UG
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
+6 -1
View File
@@ -48,6 +48,11 @@ apps up-to-date and secure.
* [Selfhosting](https://cloudron.io/documentation/installation/) - [Pricing](https://cloudron.io/pricing.html)
* [Managed Hosting](https://cloudron.io/managed.html)
**Note:** This repo is a small part of what gets installed on your server - there is
the dashboard, database addons, graph container, base image etc. Cloudron also relies
on external services such as the App Store for apps to be installed. As such, don't
clone this repo and npm install and expect something to work.
## Documentation
* [Documentation](https://cloudron.io/documentation/)
@@ -59,6 +64,6 @@ the containers in the Cloudron.
## Community
* [Chat](https://chat.cloudron.io/)
* [Forum](https://forum.cloudron.io/)
* [Support](mailto:support@cloudron.io)
+4
View File
@@ -105,3 +105,7 @@ systemctl disable bind9 || true
systemctl stop dnsmasq || true
systemctl disable dnsmasq || true
# on ssdnodes postfix seems to run by default
systemctl stop postfix || true
systemctl disable postfix || true
@@ -0,0 +1,24 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.runSql('SELECT * FROM settings WHERE name=?', ['autoupdate_pattern'], function (error, results) {
if (error || results.length === 0) return callback(error); // will use defaults from box code
// migrate the 'daily' update pattern
var appUpdatePattern = results[0].value;
if (appUpdatePattern === '00 00 1,3,5,23 * * *') appUpdatePattern = '00 30 1,3,5,23 * * *';
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'DELETE FROM settings WHERE name=?', ['autoupdate_pattern']),
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', ['app_autoupdate_pattern', appUpdatePattern]),
db.runSql.bind(db, 'COMMIT')
], callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,121 @@
'use strict';
var async = require('async'),
crypto = require('crypto'),
fs = require('fs'),
os = require('os'),
path = require('path'),
safe = require('safetydance'),
tldjs = require('tldjs');
exports.up = function(db, callback) {
db.all('SELECT * FROM apps', function (error, apps) {
if (error) return callback(error);
async.eachSeries(apps, function (app, callback) {
if (!app.altDomain) {
console.log('App %s does not use altDomain, skip', app.id);
return callback();
}
const domain = tldjs.getDomain(app.altDomain);
const subdomain = tldjs.getSubdomain(app.altDomain);
const mailboxName = (subdomain ? subdomain : JSON.parse(app.manifestJson).title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
console.log('App %s is on domain %s and subdomain %s with mailbox', app.id, domain, subdomain, mailboxName);
async.series([
// Add domain if not exists
function (callback) {
const query = 'INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson) VALUES (?, ?, ?, ?, ?)';
const args = [ domain, domain, 'manual', JSON.stringify({}), JSON.stringify({ provider: 'letsencrypt-prod' }) ];
db.runSql(query, args, function (error) {
if (error && error.code !== 'ER_DUP_ENTRY') return callback(error);
console.log('Added domain %s', domain);
// ensure we have a fallback cert for the newly added domain. This is the same as in reverseproxy.js
// WARNING this will only work on the cloudron itself not during local testing!
const certFilePath = `/home/yellowtent/boxdata/certs/${domain}.host.cert`;
const keyFilePath = `/home/yellowtent/boxdata/certs/${domain}.host.key`;
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) { // generate it
let opensslConf = safe.fs.readFileSync('/etc/ssl/openssl.cnf', 'utf8');
let opensslConfWithSan = `${opensslConf}\n[SAN]\nsubjectAltName=DNS:${domain}\n`;
let configFile = path.join(os.tmpdir(), 'openssl-' + crypto.randomBytes(4).readUInt32LE(0) + '.conf');
let certCommand = `openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=*.${domain} -extensions SAN -config ${configFile} -nodes`;
safe.fs.writeFileSync(configFile, opensslConfWithSan, 'utf8');
if (!safe.child_process.execSync(certCommand)) return callback(safe.error.message);
safe.fs.unlinkSync(configFile);
}
callback();
});
},
// Add domain to mail table if not exists
function (callback) {
const query = 'INSERT INTO mail (domain, enabled, mailFromValidation, catchAllJson, relayJson) VALUES (?, ?, ?, ?, ?)';
const args = [ domain, 0, 1, '[]', JSON.stringify({ provider: 'cloudron-smtp' }) ];
db.runSql(query, args, function (error) {
if (error && error.code !== 'ER_DUP_ENTRY') return callback(error);
console.log('Added domain %s to mail table', domain);
callback();
});
},
// Remove old mailbox record if any
function (callback) {
const query = 'DELETE FROM mailboxes WHERE ownerId=?';
const args = [ app.id ];
db.runSql(query, args, function (error) {
if (error) return callback(error);
console.log('Cleaned up mailbox record for app %s', app.id);
callback();
});
},
// Add new mailbox record
function (callback) {
const query = 'INSERT INTO mailboxes (name, domain, ownerId, ownerType) VALUES (?, ?, ?, ?)';
const args = [ mailboxName, domain, app.id, 'app' /* mailboxdb.TYPE_APP */ ];
db.runSql(query, args, function (error) {
if (error) return callback(error);
console.log('Added mailbox record for app %s', app.id);
callback();
});
},
// Update app record
function (callback) {
const query = 'UPDATE apps SET location=?, domain=?, altDomain=? WHERE id=?';
const args = [ subdomain, domain, '', app.id ];
db.runSql(query, args, function (error) {
if (error) return error;
console.log('Updated app %s with new domain', app.id);
callback();
});
}
], callback);
}, function (error) {
if (error) return callback(error);
// finally drop the altDomain db field
db.runSql('ALTER TABLE apps DROP COLUMN altDomain', [], callback);
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN altDomain VARCHAR(256)', [], callback);
};
@@ -0,0 +1,19 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE mailboxes DROP FOREIGN KEY mailboxes_domain_constraint'),
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD CONSTRAINT mailboxes_domain_constraint FOREIGN KEY(domain) REFERENCES mail(domain)'),
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE mailboxes DROP FOREIGN KEY mailboxes_domain_constraint', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,51 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
var users = { }, groupMembers = { };
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD COLUMN membersJson TEXT'),
function getUsers(done) {
db.all('SELECT * from users', [ ], function (error, results) {
if (error) return done(error);
results.forEach(function (result) { users[result.id] = result; });
done();
});
},
function getGroups(done) {
db.all('SELECT id, name, GROUP_CONCAT(groupMembers.userId) AS userIds ' +
' FROM groups LEFT OUTER JOIN groupMembers ON groups.id = groupMembers.groupId ' +
' GROUP BY groups.id', [ ], function (error, results) {
if (error) return done(error);
results.forEach(function (result) {
var userIds = result.userIds ? result.userIds.split(',') : [];
var members = userIds.map(function (id) { return users[id].username; });
groupMembers[result.id] = members;
});
done();
});
},
function removeGroupIdAndSetMembers(done) {
async.eachSeries(Object.keys(groupMembers), function (gid, iteratorDone) {
console.log(`Migrating group id ${gid} to ${JSON.stringify(groupMembers[gid])}`);
db.runSql('UPDATE mailboxes SET membersJson = ?, ownerId = ? WHERE ownerId = ?', [ JSON.stringify(groupMembers[gid]), 'admin', gid ], iteratorDone);
}, done);
},
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE mailboxes DROP COLUMN membersJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,34 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD COLUMN type VARCHAR(16)'),
function addMailboxType(done) {
db.all('SELECT * from mailboxes', [ ], function (error, results) {
if (error) return done(error);
async.eachSeries(results, function (mailbox, iteratorCallback) {
let type = 'mailbox';
if (mailbox.aliasTarget) {
type = 'alias';
} else if (mailbox.membersJson) {
type = 'list';
}
db.runSql('UPDATE mailboxes SET type = ? WHERE name = ? AND domain = ?', [ type, mailbox.name, mailbox.domain ], iteratorCallback);
}, done);
});
},
db.runSql.bind(db, 'ALTER TABLE mailboxes MODIFY type VARCHAR(16) NOT NULL'),
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE mailboxes DROP COLUMN membersJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN twoFactorAuthenticationSecret VARCHAR(128) DEFAULT "", ADD COLUMN twoFactorAuthenticationEnabled BOOLEAN DEFAULT false', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP twoFactorAuthenticationSecret, DROP twoFactorAuthenticationEnabled', function (error) {
if (error) console.error(error);
callback(error);
});
};
+21
View File
@@ -0,0 +1,21 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('UPDATE clients SET scope=? WHERE id=? OR id=? OR id=?', ['*', 'cid-webadmin', 'cid-sdk', 'cid-cli'], function (error) {
if (error) console.error(error);
db.runSql('UPDATE tokens SET scope=? WHERE scope LIKE ?', ['*', '%*%'], function (error) { // remove the roleSdk
if (error) console.error(error);
db.runSql('UPDATE tokens SET expires=? WHERE clientId=?', [ 1525636734905, 'cid-webadmin' ], function (error) { // force webadmin to get a new token
if (error) console.error(error);
callback(error);
});
});
});
};
exports.down = function(db, callback) {
callback();
};
+23 -22
View File
@@ -21,9 +21,10 @@ CREATE TABLE IF NOT EXISTS users(
salt VARCHAR(512) NOT NULL,
createdAt VARCHAR(512) NOT NULL,
modifiedAt VARCHAR(512) NOT NULL,
admin INTEGER NOT NULL,
displayName VARCHAR(512) DEFAULT '',
fallbackEmail VARCHAR(512) DEFAULT ""
displayName VARCHAR(512) DEFAULT "",
fallbackEmail VARCHAR(512) DEFAULT "",
twoFactorAuthenticationSecret VARCHAR(128) DEFAULT "",
twoFactorAuthenticationEnabled BOOLEAN DEFAULT false,
PRIMARY KEY(id));
@@ -72,7 +73,6 @@ CREATE TABLE IF NOT EXISTS apps(
createdAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
updatedAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
memoryLimit BIGINT DEFAULT 0,
altDomain VARCHAR(256),
xFrameOptions VARCHAR(512),
sso BOOLEAN DEFAULT 1, // whether user chose to enable SSO
debugModeJson TEXT, // options for development mode
@@ -81,8 +81,8 @@ CREATE TABLE IF NOT EXISTS apps(
// the following fields do not belong here, they can be removed when we use a queue for apptask
restoreConfigJson VARCHAR(256), // used to pass backupId to restore from to apptask
oldConfigJson TEXT, // used to pass old config for apptask (configure, restore)
updateConfigJson TEXT, // used to pass new config for apptask (update)
oldConfigJson TEXT, // used to pass old config to apptask (configure, restore)
updateConfigJson TEXT, // used to pass new config to apptask (update)
FOREIGN KEY(domain) REFERENCES domains(domain),
PRIMARY KEY(id));
@@ -130,25 +130,10 @@ CREATE TABLE IF NOT EXISTS eventlog(
action VARCHAR(128) NOT NULL,
source TEXT, /* { userId, username, ip }. userId can be null for cron,sysadmin */
data TEXT, /* free flowing json based on action */
creationTime TIMESTAMP, /* FIXME: precision must be TIMESTAMP(2) */
createdAt TIMESTAMP(2) NOT NULL,
PRIMARY KEY (id));
/* Future fields:
* accessRestriction - to determine who can access it. So this has foreign keys
* quota - per mailbox quota
*/
CREATE TABLE IF NOT EXISTS mailboxes(
name VARCHAR(128) NOT NULL,
ownerId VARCHAR(128) NOT NULL, /* app id or user id or group id */
ownerType VARCHAR(16) NOT NULL, /* 'app' or 'user' or 'group' */
aliasTarget VARCHAR(128), /* the target name type is an alias */
creationTime TIMESTAMP,
domain VARCHAR(128),
FOREIGN KEY(domain) REFERENCES domains(domain),
UNIQUE (name, domain));
CREATE TABLE IF NOT EXISTS domains(
domain VARCHAR(128) NOT NULL UNIQUE, /* if this needs to be larger, InnoDB has a limit of 767 bytes for PRIMARY KEY values! */
zoneName VARCHAR(128) NOT NULL, /* this mostly contains the domain itself again */
@@ -174,4 +159,20 @@ CREATE TABLE IF NOT EXISTS mail(
CHARACTER SET utf8 COLLATE utf8_bin;
/* Future fields:
* accessRestriction - to determine who can access it. So this has foreign keys
* quota - per mailbox quota
*/
CREATE TABLE IF NOT EXISTS mailboxes(
name VARCHAR(128) NOT NULL,
type VARCHAR(16) NOT NULL, /* 'mailbox', 'alias', 'list' */
ownerId VARCHAR(128) NOT NULL, /* app id or user id or group id */
ownerType VARCHAR(16) NOT NULL, /* 'app' or 'user' or 'group' */
aliasTarget VARCHAR(128), /* the target name type is an alias */
membersJson TEXT, /* members of a group */
creationTime TIMESTAMP,
domain VARCHAR(128),
FOREIGN KEY(domain) REFERENCES mail(domain),
UNIQUE (name, domain));
+1758 -6160
View File
File diff suppressed because it is too large Load Diff
+18 -26
View File
@@ -14,11 +14,11 @@
"node": ">=4.0.0 <=4.1.1"
},
"dependencies": {
"@google-cloud/dns": "^0.7.0",
"@google-cloud/storage": "^1.2.1",
"@google-cloud/dns": "^0.7.1",
"@google-cloud/storage": "^1.6.0",
"@sindresorhus/df": "^2.1.0",
"async": "^2.6.0",
"aws-sdk": "^2.151.0",
"aws-sdk": "^2.201.0",
"body-parser": "^1.18.2",
"cloudron-manifestformat": "^2.11.0",
"connect-ensure-login": "^0.1.1",
@@ -28,24 +28,24 @@
"cookie-session": "^1.3.2",
"cron": "^1.3.0",
"csurf": "^1.6.6",
"db-migrate": "^0.10.0-beta.24",
"db-migrate": "^0.10.5",
"db-migrate-mysql": "^1.1.10",
"debug": "^3.1.0",
"dockerode": "^2.5.3",
"dockerode": "^2.5.4",
"ejs": "^2.5.7",
"ejs-cli": "^2.0.0",
"express": "^4.16.2",
"express-session": "^1.15.6",
"hat": "0.0.3",
"json": "^9.0.3",
"ldapjs": "^1.0.0",
"ldapjs": "^1.0.2",
"lodash.chunk": "^4.2.0",
"mime": "^2.0.3",
"mime": "^2.2.0",
"moment-timezone": "^0.5.14",
"morgan": "^1.9.0",
"multiparty": "^4.1.2",
"mysql": "^2.15.0",
"nodemailer": "^4.4.0",
"nodemailer": "^4.6.0",
"nodemailer-smtp-transport": "^2.7.4",
"oauth2orize": "^1.11.0",
"once": "^1.3.2",
@@ -58,45 +58,37 @@
"password-generator": "^2.2.0",
"progress-stream": "^2.0.0",
"proxy-middleware": "^0.15.0",
"qrcode": "^1.2.0",
"recursive-readdir": "^2.2.1",
"request": "^2.83.0",
"s3-block-read-stream": "^0.2.0",
"safetydance": "^0.7.1",
"semver": "^5.4.1",
"semver": "^5.5.0",
"showdown": "^1.8.2",
"speakeasy": "^2.0.0",
"split": "^1.0.0",
"superagent": "^3.8.1",
"supererror": "^0.7.1",
"tar-fs": "^1.16.0",
"tar-stream": "^1.5.5",
"tldjs": "^2.2.0",
"tldjs": "^2.3.1",
"underscore": "^1.7.0",
"uuid": "^3.1.0",
"uuid": "^3.2.1",
"valid-url": "^1.0.9",
"validator": "^9.1.1",
"ws": "^3.3.1"
"validator": "^9.4.1",
"ws": "^3.3.3"
},
"devDependencies": {
"bootstrap-sass": "^3.3.3",
"expect.js": "*",
"gulp": "^3.9.1",
"gulp-autoprefixer": "^4.0.0",
"gulp-concat": "^2.4.3",
"gulp-cssnano": "^2.1.0",
"gulp-ejs": "^3.1.0",
"gulp-sass": "^3.1.0",
"gulp-serve": "^1.0.0",
"gulp-sourcemaps": "^2.6.1",
"gulp-uglify": "^3.0.0",
"hock": "^1.3.2",
"istanbul": "*",
"js2xmlparser": "^3.0.0",
"mocha": "*",
"mocha": "^5.0.1",
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
"nock": "^9.0.14",
"node-sass": "^4.6.1",
"readdirp": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz",
"yargs": "^10.0.3"
"rimraf": "^2.6.2"
},
"scripts": {
"migrate_local": "DATABASE_URL=mysql://root:@localhost/box node_modules/.bin/db-migrate up",
@@ -106,6 +98,6 @@
"postmerge": "/bin/true",
"precommit": "/bin/true",
"prepush": "npm test",
"webadmin": "node_modules/.bin/gulp"
"dashboard": "node_modules/.bin/gulp"
}
}
+122
View File
@@ -0,0 +1,122 @@
#!/bin/bash
set -eu -o pipefail
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
function get_status() {
key="$1"
if status=$($curl -q -f "http://localhost:3000/api/v1/cloudron/status" 2>/dev/null); then
currentValue=$(echo "${status}" | python3 -c 'import sys, json; print(json.dumps(json.load(sys.stdin)[sys.argv[1]]))' "${key}")
echo "${currentValue}"
return 0
fi
return 1
}
function wait_for_status() {
key="$1"
expectedValue="$2"
echo "wait_for_status: $key to be $expectedValue"
while true; do
if currentValue=$(get_status "${key}"); then
echo "wait_for_status: $key is current: $currentValue expecting: $expectedValue"
if [[ "${currentValue}" == $expectedValue ]]; then
break
fi
fi
sleep 3
done
}
domain=""
domainProvider=""
domainConfigJson="{}"
domainTlsProvider="letsencrypt-prod"
adminUsername="superadmin"
adminPassword="Secret123#"
adminEmail="admin@server.local"
appstoreUserId=""
appstoreToken=""
backupDir="/var/backups"
args=$(getopt -o "" -l "domain:,domain-provider:,domain-tls-provider:,admin-username:,admin-password:,admin-email:,appstore-user:,appstore-token:,backup-dir:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--domain) domain="$2"; shift 2;;
--domain-provider) domainProvider="$2"; shift 2;;
--domain-tls-provider) domainTlsProvider="$2"; shift 2;;
--admin-username) adminUsername="$2"; shift 2;;
--admin-password) adminPassword="$2"; shift 2;;
--admin-email) adminEmail="$2"; shift 2;;
--appstore-user) appstoreUser="$2"; shift 2;;
--appstore-token) appstoreToken="$2"; shift 2;;
--backup-dir) backupDir="$2"; shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
echo "=> Waiting for cloudron to be ready"
wait_for_status "version" '*'
if [[ $(get_status "webadminStatus") != *'"tls": true'* ]]; then
echo "=> Domain setup"
dnsSetupData=$(printf '{ "domain": "%s", "adminFqdn": "%s", "provider": "%s", "config": %s, "tlsConfig": { "provider": "%s" } }' "${domain}" "my.${domain}" "${domainProvider}" "$domainConfigJson" "${domainTlsProvider}")
if ! $curl -X POST -H "Content-Type: application/json" -d "${dnsSetupData}" http://localhost:3000/api/v1/cloudron/dns_setup; then
echo "DNS Setup Failed"
exit 1
fi
wait_for_status "webadminStatus" '*"tls": true*'
else
echo "=> Skipping Domain setup"
fi
activationData=$(printf '{"username": "%s", "password":"%s", "email": "%s" }' "${adminUsername}" "${adminPassword}" "${adminEmail}")
if [[ $(get_status "activated") == "false" ]]; then
echo "=> Activating"
if ! activationResult=$($curl -X POST -H "Content-Type: application/json" -d "${activationData}" http://localhost:3000/api/v1/cloudron/activate); then
echo "Failed to activate with ${activationData}: ${activationResult}"
exit 1
fi
wait_for_status "activated" "true"
else
echo "=> Skipping Activation"
fi
echo "=> Getting token"
if ! activationResult=$($curl -X POST -H "Content-Type: application/json" -d "${activationData}" http://localhost:3000/api/v1/developer/login); then
echo "Failed to login with ${activationData}: ${activationResult}"
exit 1
fi
accessToken=$(echo "${activationResult}" | python3 -c 'import sys, json; print(json.load(sys.stdin)[sys.argv[1]])' "accessToken")
echo "=> Setting up App Store account with accessToken ${accessToken}"
appstoreData=$(printf '{"userId":"%s", "token":"%s" }' "${appstoreUser}" "${appstoreToken}")
if ! appstoreResult=$($curl -X POST -H "Content-Type: application/json" -d "${appstoreData}" "http://localhost:3000/api/v1/settings/appstore_config?access_token=${accessToken}"); then
echo "Failed to setup Appstore account with ${appstoreData}: ${appstoreResult}"
exit 1
fi
echo "=> Setting up Backup Directory with accessToken ${accessToken}"
backupData=$(printf '{"provider":"filesystem", "key":"", "backupFolder":"%s", "retentionSecs": 864000, "format": "tgz"}' "${backupDir}")
chown -R yellowtent:yellowtent "${backupDir}"
if ! backupResult=$($curl -X POST -H "Content-Type: application/json" -d "${backupData}" "http://localhost:3000/api/v1/settings/backup_config?access_token=${accessToken}"); then
echo "Failed to setup backup configuration with ${backupDir}: ${backupResult}"
exit 1
fi
echo "=> Done!"
+20 -13
View File
@@ -2,16 +2,6 @@
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
if [[ $(lsb_release -rs) != "16.04" ]]; then
echo "Cloudron requires Ubuntu 16.04" > /dev/stderr
exit 1
fi
# change this to a hash when we make a upgrade release
readonly LOG_FILE="/var/log/cloudron-setup.log"
readonly DATA_FILE="/root/cloudron-install-data.json"
@@ -26,6 +16,10 @@ readonly physical_memory=$(LC_ALL=C free -m | awk '/Mem:/ { print $2 }')
readonly disk_size_bytes=$(LC_ALL=C df --output=size / | tail -n1)
readonly disk_size_gb=$((${disk_size_bytes}/1024/1024))
readonly RED='\033[31m'
readonly GREEN='\033[32m'
readonly DONE='\033[m'
# verify the system has minimum requirements met
if [[ "${rootfs_type}" != "ext4" ]]; then
echo "Error: Cloudron requires '/' to be ext4" # see #364
@@ -81,6 +75,18 @@ while true; do
esac
done
# Only --help works as non-root
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
# Only --help works with mismatched ubuntu
if [[ $(lsb_release -rs) != "16.04" ]]; then
echo "Cloudron requires Ubuntu 16.04" > /dev/stderr
exit 1
fi
# validate arguments in the absence of data
if [[ -z "${provider}" ]]; then
echo "--provider is required (azure, cloudscale, digitalocean, ec2, exoscale, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
@@ -88,6 +94,7 @@ if [[ -z "${provider}" ]]; then
elif [[ \
"${provider}" != "ami" && \
"${provider}" != "azure" && \
"${provider}" != "caas" && \
"${provider}" != "cloudscale" && \
"${provider}" != "digitalocean" && \
"${provider}" != "ec2" && \
@@ -119,7 +126,7 @@ echo ""
echo " Follow setup logs in a second terminal with:"
echo " $ tail -f ${LOG_FILE}"
echo ""
echo " Join us at https://chat.cloudron.io for any questions."
echo " Join us at https://forum.cloudron.io for any questions."
echo ""
if [[ "${initBaseImage}" == "true" ]]; then
@@ -198,10 +205,10 @@ while true; do
sleep 10
done
echo -e "\n\nVisit https://<IP> to finish setup once the server has rebooted.\n"
echo -e "\n\n${GREEN}Visit https://<IP> and accept the self-signed certificate to finish setup.${DONE}"
if [[ "${rebootServer}" == "true" ]]; then
echo -e "\n\nRebooting this server now to let bootloader changes take effect.\n"
echo -e "\n${RED}Rebooting this server now to let changes take effect.${DONE}\n"
systemctl stop mysql # sometimes mysql ends up having corrupt privilege tables
systemctl reboot
fi
+26 -39
View File
@@ -29,8 +29,8 @@ if ! $(cd "${SOURCE_DIR}" && git diff --exit-code >/dev/null); then
exit 1
fi
if ! $(cd "${SOURCE_DIR}/../webadmin" && git diff --exit-code >/dev/null); then
echo "You have local changes in webadmin, stash or commit them to proceed"
if ! $(cd "${SOURCE_DIR}/../dashboard" && git diff --exit-code >/dev/null); then
echo "You have local changes in dashboard, stash or commit them to proceed"
exit 1
fi
@@ -41,53 +41,40 @@ fi
box_version=$(cd "${SOURCE_DIR}" && git rev-parse "HEAD")
branch=$(git rev-parse --abbrev-ref HEAD)
webadmin_version=$(cd "${SOURCE_DIR}/../webadmin" && git fetch && git rev-parse "origin/${branch}")
if [[ "${branch}" == "master" ]]; then
dashboard_version=$(cd "${SOURCE_DIR}/../dashboard" && git rev-parse "${branch}")
else
dashboard_version=$(cd "${SOURCE_DIR}/../dashboard" && git fetch && git rev-parse "origin/${branch}")
fi
bundle_dir=$(mktemp -d -t box 2>/dev/null || mktemp -d box-XXXXXXXXXX --tmpdir=$TMPDIR)
[[ -z "$bundle_file" ]] && bundle_file="${TMPDIR}/box-${box_version:0:10}-${webadmin_version:0:10}.tar.gz"
[[ -z "$bundle_file" ]] && bundle_file="${TMPDIR}/box-${box_version:0:10}-${dashboard_version:0:10}.tar.gz"
chmod "o+rx,g+rx" "${bundle_dir}" # otherwise extracted tarball director won't be readable by others/group
echo "Checking out code box version [${box_version}] and webadmin version [${webadmin_version}] into ${bundle_dir}"
echo "==> Checking out code box version [${box_version}] and dashboard version [${dashboard_version}] into ${bundle_dir}"
(cd "${SOURCE_DIR}" && git archive --format=tar ${box_version} | (cd "${bundle_dir}" && tar xf -))
(cd "${SOURCE_DIR}/../webadmin" && git archive --format=tar ${webadmin_version} | (cd "${bundle_dir}" && tar xf -))
(cp "${SOURCE_DIR}/../webadmin/LICENSE" "${bundle_dir}")
(cd "${SOURCE_DIR}/../dashboard" && git archive --format=tar ${dashboard_version} | (mkdir -p "${bundle_dir}/dashboard.build" && cd "${bundle_dir}/dashboard.build" && tar xf -))
(cp "${SOURCE_DIR}/../dashboard/LICENSE" "${bundle_dir}")
if diff "${TMPDIR}/boxtarball.cache/package-lock.json.all" "${bundle_dir}/package-lock.json" >/dev/null 2>&1; then
echo "Reusing dev modules from cache"
cp -r "${TMPDIR}/boxtarball.cache/node_modules-all/." "${bundle_dir}/node_modules"
else
echo "Installing modules with dev dependencies"
(cd "${bundle_dir}" && npm install)
echo "==> Installing modules for dashboard asset generation"
(cd "${bundle_dir}/dashboard.build" && npm install --production)
echo "Caching dev dependencies"
mkdir -p "${TMPDIR}/boxtarball.cache/node_modules-all"
rsync -a --delete "${bundle_dir}/node_modules/" "${TMPDIR}/boxtarball.cache/node_modules-all/"
cp "${bundle_dir}/package-lock.json" "${TMPDIR}/boxtarball.cache/package-lock.json.all"
fi
echo "==> Building dashboard assets"
(cd "${bundle_dir}/dashboard.build" && ./node_modules/.bin/gulp --revision ${dashboard_version})
echo "Building webadmin assets"
(cd "${bundle_dir}" && ./node_modules/.bin/gulp)
echo "==> Move built dashboard assets into destination"
mkdir -p "${bundle_dir}/dashboard"
mv "${bundle_dir}/dashboard.build/dist" "${bundle_dir}/dashboard/"
echo "Remove intermediate files required at build-time only"
rm -rf "${bundle_dir}/node_modules/"
rm -rf "${bundle_dir}/webadmin/src"
rm -rf "${bundle_dir}/gulpfile.js"
echo "==> Cleanup dashboard build artifacts"
rm -rf "${bundle_dir}/dashboard.build"
if diff "${TMPDIR}/boxtarball.cache/package-lock.json.prod" "${bundle_dir}/package-lock.json" >/dev/null 2>&1; then
echo "Reusing prod modules from cache"
cp -r "${TMPDIR}/boxtarball.cache/node_modules-prod/." "${bundle_dir}/node_modules"
else
echo "Installing modules for production"
(cd "${bundle_dir}" && npm install --production --no-optional)
echo "==> Installing toplevel node modules"
(cd "${bundle_dir}" && npm install --production --no-optional)
echo "Caching prod dependencies"
mkdir -p "${TMPDIR}/boxtarball.cache/node_modules-prod"
rsync -a --delete "${bundle_dir}/node_modules/" "${TMPDIR}/boxtarball.cache/node_modules-prod/"
cp "${bundle_dir}/package-lock.json" "${TMPDIR}/boxtarball.cache/package-lock.json.prod"
fi
echo "Create final tarball"
echo "==> Create final tarball"
(cd "${bundle_dir}" && tar czf "${bundle_file}" .)
echo "Cleaning up ${bundle_dir}"
echo "==> Cleaning up ${bundle_dir}"
rm -rf "${bundle_dir}"
echo "Tarball saved at ${bundle_file}"
echo "==> Tarball saved at ${bundle_file}"
+9 -3
View File
@@ -35,11 +35,11 @@ while true; do
done
echo "==> installer: updating docker"
if [[ $(docker version --format {{.Client.Version}}) != "17.09.0-ce" ]]; then
$curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_17.09.0~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
if [[ $(docker version --format {{.Client.Version}}) != "18.03.0-ce" ]]; then
$curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_18.03.0~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
# https://download.docker.com/linux/ubuntu/dists/xenial/stable/binary-amd64/Packages
if [[ $(sha256sum /tmp/docker.deb | cut -d' ' -f1) != "d33f6eb134f0ab0876148bd96de95ea47d583d7f2cddfdc6757979453f9bd9bf" ]]; then
if [[ $(sha256sum /tmp/docker.deb | cut -d' ' -f1) != "1f7315b5723b849fe542fe973b0edb4164a0200e926d386ac14363a968f9e4fc" ]]; then
echo "==> installer: docker binary download is corrupt"
exit 5
fi
@@ -54,6 +54,12 @@ if [[ $(docker version --format {{.Client.Version}}) != "17.09.0-ce" ]]; then
sleep 1
done
# the latest docker might need newer packages
while ! apt update -y; do
echo "==> installer: Failed to update packages. Retry"
sleep 1
done
while ! apt install -y /tmp/docker.deb; do
echo "==> installer: Failed to install docker. Retry"
sleep 1
+2 -2
View File
@@ -214,8 +214,8 @@ cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
}
CONF_END
echo "==> Creating config.json for webadmin"
cat > "${BOX_SRC_DIR}/webadmin/dist/config.json" <<CONF_END
echo "==> Creating config.json for dashboard"
cat > "${BOX_SRC_DIR}/dashboard/dist/config.json" <<CONF_END
{
"webServerOrigin": "${arg_web_server_origin}"
}
+10 -4
View File
@@ -66,8 +66,9 @@ server {
# https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
ssl_prefer_server_ciphers on;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # don't use SSLv3 ref: POODLE
# ciphers according to https://weakdh.org/sysadmin.html
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
# ciphers according to https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=nginx-1.10.3&openssl=1.0.2g&hsts=yes&profile=modern
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
ssl_dhparam /home/yellowtent/boxdata/dhparams.pem;
add_header Strict-Transport-Security "max-age=15768000";
@@ -89,6 +90,11 @@ server {
add_header Referrer-Policy "no-referrer-when-downgrade";
proxy_hide_header Referrer-Policy;
# CSP headers for the admin/dashboard resources
<% if ( endpoint === 'admin' ) { -%>
add_header Content-Security-Policy "default-src 'none'; connect-src wss: https: 'self' *.cloudron.io; script-src https: 'self' 'unsafe-inline' 'unsafe-eval'; img-src * data:; style-src https: 'unsafe-inline'; object-src 'none'; font-src https: 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self';";
<% } -%>
proxy_http_version 1.1;
proxy_intercept_errors on;
proxy_read_timeout 3500;
@@ -106,7 +112,7 @@ server {
proxy_set_header Connection $connection_upgrade;
# only serve up the status page if we get proxy gateway errors
root <%= sourceDir %>/webadmin/dist;
root <%= sourceDir %>/dashboard/dist;
error_page 502 503 504 /appstatus.html;
location /appstatus.html {
internal;
@@ -160,7 +166,7 @@ server {
# }
location / {
root <%= sourceDir %>/webadmin/dist;
root <%= sourceDir %>/dashboard/dist;
index index.html index.htm;
}
<% } else if ( endpoint === 'app' ) { %>
+190
View File
@@ -0,0 +1,190 @@
'use strict';
exports = module.exports = {
SCOPE_APPS: 'apps',
SCOPE_CLIENTS: 'clients',
SCOPE_CLOUDRON: 'cloudron',
SCOPE_DOMAINS: 'domains',
SCOPE_MAIL: 'mail',
SCOPE_PROFILE: 'profile',
SCOPE_SETTINGS: 'settings',
SCOPE_USERS: 'users',
VALID_SCOPES: [ 'apps', 'clients', 'cloudron', 'domains', 'mail', 'profile', 'settings', 'users' ],
SCOPE_ANY: '*',
initialize: initialize,
uninitialize: uninitialize,
accessTokenAuth: accessTokenAuth,
validateScope: validateScope,
validateRequestedScopes: validateRequestedScopes,
normalizeScope: normalizeScope,
canonicalScope: canonicalScope
};
var assert = require('assert'),
BasicStrategy = require('passport-http').BasicStrategy,
BearerStrategy = require('passport-http-bearer').Strategy,
clients = require('./clients'),
ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy,
ClientsError = clients.ClientsError,
DatabaseError = require('./databaseerror'),
debug = require('debug')('box:accesscontrol'),
LocalStrategy = require('passport-local').Strategy,
passport = require('passport'),
tokendb = require('./tokendb'),
users = require('./users.js'),
UsersError = users.UsersError,
_ = require('underscore');
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
passport.serializeUser(function (user, callback) {
callback(null, user.id);
});
passport.deserializeUser(function(userId, callback) {
users.get(userId, function (error, result) {
if (error) return callback(error);
callback(null, result);
});
});
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) 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) return callback(error);
if (!result) return callback(null, false);
callback(null, result);
});
}
}));
passport.use(new BasicStrategy(function (username, password, callback) {
if (username.indexOf('cid-') === 0) {
debug('BasicStrategy: detected client id %s instead of username:password', username);
// username is actually client id here
// password is client secret
clients.get(username, function (error, client) {
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
if (client.clientSecret != password) return callback(null, false);
return callback(null, client);
});
} else {
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) return callback(error);
if (!result) return callback(null, false);
callback(null, result);
});
}
}));
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) { return callback(error); }
if (client.clientSecret != clientSecret) { return callback(null, false); }
return callback(null, client);
});
}));
passport.use(new BearerStrategy(accessTokenAuth));
callback(null);
}
function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
callback(null);
}
function canonicalScope(scope) {
return scope.replace(exports.SCOPE_ANY, exports.VALID_SCOPES.join(','));
}
function normalizeScope(allowedScope, wantedScope) {
assert.strictEqual(typeof allowedScope, 'string');
assert.strictEqual(typeof wantedScope, 'string');
const allowedScopes = allowedScope.split(',');
const wantedScopes = wantedScope.split(',');
if (allowedScopes.indexOf(exports.SCOPE_ANY) !== -1) return canonicalScope(wantedScope);
if (wantedScopes.indexOf(exports.SCOPE_ANY) !== -1) return canonicalScope(allowedScope);
return _.intersection(allowedScopes, wantedScopes).join(',');
}
function accessTokenAuth(accessToken, callback) {
assert.strictEqual(typeof accessToken, 'string');
assert.strictEqual(typeof callback, 'function');
tokendb.get(accessToken, function (error, token) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
users.get(token.identifier, function (error, user) {
if (error && error.reason === UsersError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
// scopes here can define what capabilities that token carries
// passport put the 'info' object into req.authInfo, where we can further validate the scopes
var scope = normalizeScope(user.scope, token.scope);
var info = { scope: scope, clientId: token.clientId };
callback(null, user, info);
});
});
}
function validateScope(scope) {
assert.strictEqual(typeof scope, 'string');
if (scope === '') return new Error('Empty scope not allowed');
// 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) !== -1; });
if (!allValid) return new Error('Invalid scope. Available scopes are ' + exports.VALID_SCOPES.join(', '));
return null;
}
// tests if all requestedScopes are attached to the request
function validateRequestedScopes(authInfo, requestedScopes) {
assert.strictEqual(typeof authInfo, 'object');
assert(Array.isArray(requestedScopes));
if (!authInfo || !authInfo.scope) return new Error('No scope found');
var scopes = authInfo.scope.split(',');
if (scopes.indexOf(exports.SCOPE_ANY) !== -1) return null;
for (var i = 0; i < requestedScopes.length; ++i) {
if (scopes.indexOf(requestedScopes[i]) === -1) {
debug('scope: missing scope "%s".', requestedScopes[i]);
return new Error('Missing required scope "' + requestedScopes[i] + '"');
}
}
return null;
}
+242 -124
View File
@@ -15,19 +15,22 @@ exports = module.exports = {
_teardownOauth: teardownOauth
};
var appdb = require('./appdb.js'),
var accesscontrol = require('./accesscontrol.js'),
appdb = require('./appdb.js'),
assert = require('assert'),
async = require('async'),
clients = require('./clients.js'),
config = require('./config.js'),
ClientsError = clients.ClientsError,
crypto = require('crypto'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:addons'),
docker = require('./docker.js'),
dockerConnection = docker.connection,
fs = require('fs'),
generatePassword = require('password-generator'),
hat = require('hat'),
infra = require('./infra_version.js'),
mail = require('./mail.js'),
mailboxdb = require('./mailboxdb.js'),
once = require('once'),
path = require('path'),
@@ -112,10 +115,9 @@ var KNOWN_ADDONS = {
var RMAPPDIR_CMD = path.join(__dirname, 'scripts/rmappdir.sh');
function debugApp(app, args) {
assert(!app || typeof app === 'object');
assert(typeof app === 'object');
var prefix = app ? app.intrinsicFqdn : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function setupAddons(app, addons, callback) {
@@ -250,8 +252,8 @@ function setupOauth(app, options, callback) {
if (!app.sso) return callback(null);
var appId = app.id;
var redirectURI = 'https://' + (app.altDomain || app.intrinsicFqdn);
var scope = 'profile';
var redirectURI = 'https://' + app.fqdn;
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);
@@ -291,20 +293,27 @@ function setupEmail(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
// note that "external" access info can be derived from MAIL_DOMAIN (since it's part of user documentation)
var env = [
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
{ name: 'MAIL_SMTP_PORT', value: '2525' },
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
{ name: 'MAIL_IMAP_PORT', value: '9993' },
{ name: 'MAIL_SIEVE_SERVER', value: 'mail' },
{ name: 'MAIL_SIEVE_PORT', value: '4190' },
{ name: 'MAIL_DOMAIN', value: app.domain }
];
mail.getDomains(function (error, mailDomains) {
if (error) return callback(error);
debugApp(app, 'Setting up Email');
const mailInDomains = mailDomains.filter(function (d) { return d.enabled; }).map(function (d) { return d.domain; }).join(',');
appdb.setAddonConfig(app.id, 'email', env, callback);
// note that "external" access info can be derived from MAIL_DOMAIN (since it's part of user documentation)
var env = [
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
{ name: 'MAIL_SMTP_PORT', value: '2525' },
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
{ name: 'MAIL_IMAP_PORT', value: '9993' },
{ name: 'MAIL_SIEVE_SERVER', value: 'mail' },
{ name: 'MAIL_SIEVE_PORT', value: '4190' },
{ name: 'MAIL_DOMAIN', value: app.domain },
{ name: 'MAIL_DOMAINS', value: mailInDomains }
];
debugApp(app, 'Setting up Email');
appdb.setAddonConfig(app.id, 'email', env, callback);
});
}
function teardownEmail(app, options, callback) {
@@ -356,23 +365,28 @@ function setupSendMail(app, options, callback) {
debugApp(app, 'Setting up SendMail');
mailboxdb.getByOwnerId(app.id, function (error, results) {
if (error) return callback(error);
appdb.getAddonConfigByName(app.id, 'sendmail', 'MAIL_SMTP_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
var password = generatePassword(128, false /* memorable */, /[\w\d_]/);
var password = error ? hat(4 * 128) : existingPassword;
var env = [
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
{ name: 'MAIL_SMTP_PORT', value: '2525' },
{ name: 'MAIL_SMTPS_PORT', value: '2465' },
{ name: 'MAIL_SMTP_USERNAME', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_SMTP_PASSWORD', value: password },
{ name: 'MAIL_FROM', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_DOMAIN', value: app.domain }
];
debugApp(app, 'Setting sendmail addon config to %j', env);
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
mailboxdb.getByOwnerId(app.id, function (error, results) {
if (error) return callback(error);
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
var env = [
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
{ name: 'MAIL_SMTP_PORT', value: '2525' },
{ name: 'MAIL_SMTPS_PORT', value: '2465' },
{ name: 'MAIL_SMTP_USERNAME', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_SMTP_PASSWORD', value: password },
{ name: 'MAIL_FROM', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_DOMAIN', value: app.domain }
];
debugApp(app, 'Setting sendmail addon config to %j', env);
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
});
});
}
@@ -393,23 +407,28 @@ function setupRecvMail(app, options, callback) {
debugApp(app, 'Setting up recvmail');
mailboxdb.getByOwnerId(app.id, function (error, results) {
if (error) return callback(error);
appdb.getAddonConfigByName(app.id, 'recvmail', 'MAIL_IMAP_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
var password = generatePassword(128, false /* memorable */, /[\w\d_]/);
var password = error ? hat(4 * 128) : existingPassword;
var env = [
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
{ name: 'MAIL_IMAP_PORT', value: '9993' },
{ name: 'MAIL_IMAP_USERNAME', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_IMAP_PASSWORD', value: password },
{ name: 'MAIL_TO', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_DOMAIN', value: app.domain }
];
mailboxdb.getByOwnerId(app.id, function (error, results) {
if (error) return callback(error);
debugApp(app, 'Setting sendmail addon config to %j', env);
appdb.setAddonConfig(app.id, 'recvmail', env, callback);
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
var env = [
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
{ name: 'MAIL_IMAP_PORT', value: '9993' },
{ name: 'MAIL_IMAP_USERNAME', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_IMAP_PASSWORD', value: password },
{ name: 'MAIL_TO', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_DOMAIN', value: app.domain }
];
debugApp(app, 'Setting sendmail addon config to %j', env);
appdb.setAddonConfig(app.id, 'recvmail', env, callback);
});
});
}
@@ -423,6 +442,14 @@ function teardownRecvMail(app, options, callback) {
appdb.unsetAddonConfig(app.id, 'recvmail', callback);
}
function mysqlDatabaseName(appId) {
assert.strictEqual(typeof appId, 'string');
var md5sum = crypto.createHash('md5'); // get rid of "-"
md5sum.update(appId);
return md5sum.digest('hex').substring(0, 16); // max length of mysql usernames is 16
}
function setupMySql(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
@@ -430,16 +457,36 @@ function setupMySql(app, options, callback) {
debugApp(app, 'Setting up mysql');
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'add-prefix' : 'add', app.id ];
appdb.getAddonConfigByName(app.id, 'mysql', 'MYSQL_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
docker.execContainer('mysql', cmd, { bufferStdout: true }, function (error, stdout) {
if (error) return callback(error);
const dbname = mysqlDatabaseName(app.id);
const password = error ? hat(4 * 48) : existingPassword; // see box#362 for password length
var result = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
var env = result.map(function (r) { var idx = r.indexOf('='); return { name: r.substr(0, idx), value: r.substr(idx + 1) }; });
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'add-prefix' : 'add', dbname, password ];
debugApp(app, 'Setting mysql addon config to %j', env);
appdb.setAddonConfig(app.id, 'mysql', env, callback);
docker.execContainer('mysql', cmd, { bufferStdout: true }, function (error) {
if (error) return callback(error);
var env = [
{ name: 'MYSQL_USERNAME', value: dbname },
{ name: 'MYSQL_PASSWORD', value: password },
{ name: 'MYSQL_HOST', value: 'mysql' },
{ name: 'MYSQL_PORT', value: '3306' }
];
if (options.multipleDatabases) {
env = env.concat({ name: 'MYSQL_DATABASE_PREFIX', value: `${dbname}_` });
} else {
env = env.concat(
{ name: 'MYSQL_URL', value: `mysql://${dbname}:${password}@mysql/${dbname}` },
{ name: 'MYSQL_DATABASE', value: dbname }
);
}
debugApp(app, 'Setting mysql addon config to %j', env);
appdb.setAddonConfig(app.id, 'mysql', env, callback);
});
});
}
@@ -448,7 +495,8 @@ function teardownMySql(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'remove-prefix' : 'remove', app.id ];
const dbname = mysqlDatabaseName(app.id);
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'remove-prefix' : 'remove', dbname ];
debugApp(app, 'Tearing down mysql');
@@ -460,6 +508,10 @@ function teardownMySql(app, options, callback) {
}
function backupMySql(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Backing up mysql');
callback = once(callback); // ChildProcess exit may or may not be called after error
@@ -467,12 +519,17 @@ function backupMySql(app, options, callback) {
var output = fs.createWriteStream(path.join(paths.APPS_DATA_DIR, app.id, 'mysqldump'));
output.on('error', callback);
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'backup-prefix' : 'backup', app.id ];
const dbname = mysqlDatabaseName(app.id);
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'backup-prefix' : 'backup', dbname ];
docker.execContainer('mysql', cmd, { stdout: output }, callback);
}
function restoreMySql(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
callback = once(callback); // ChildProcess exit may or may not be called after error
setupMySql(app, options, function (error) {
@@ -483,7 +540,8 @@ function restoreMySql(app, options, callback) {
var input = fs.createReadStream(path.join(paths.APPS_DATA_DIR, app.id, 'mysqldump'));
input.on('error', callback);
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'restore-prefix' : 'restore', app.id ];
const dbname = mysqlDatabaseName(app.id);
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'restore-prefix' : 'restore', dbname ];
docker.execContainer('mysql', cmd, { stdin: input }, callback);
});
}
@@ -495,16 +553,29 @@ function setupPostgreSql(app, options, callback) {
debugApp(app, 'Setting up postgresql');
var cmd = [ '/addons/postgresql/service.sh', 'add', app.id ];
appdb.getAddonConfigByName(app.id, 'postgresql', 'POSTGRESQL_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
docker.execContainer('postgresql', cmd, { bufferStdout: true }, function (error, stdout) {
if (error) return callback(error);
const password = error ? hat(4 * 128) : existingPassword;
const appId = app.id.replace(/-/g, '');
var result = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
var env = result.map(function (r) { var idx = r.indexOf('='); return { name: r.substr(0, idx), value: r.substr(idx + 1) }; });
var cmd = [ '/addons/postgresql/service.sh', 'add', appId, password ];
debugApp(app, 'Setting postgresql addon config to %j', env);
appdb.setAddonConfig(app.id, 'postgresql', env, callback);
docker.execContainer('postgresql', cmd, { bufferStdout: true }, function (error) {
if (error) return callback(error);
var env = [
{ name: 'POSTGRESQL_URL', value: `postgres://user${appId}:${password}@postgresql/db${appId}` },
{ name: 'POSTGRESQL_USERNAME', value: `user${appId}` },
{ name: 'POSTGRESQL_PASSWORD', value: password },
{ name: 'POSTGRESQL_HOST', value: 'postgresql' },
{ name: 'POSTGRESQL_PORT', value: '5432' },
{ name: 'POSTGRESQL_DATABASE', value: `db${appId}` }
];
debugApp(app, 'Setting postgresql addon config to %j', env);
appdb.setAddonConfig(app.id, 'postgresql', env, callback);
});
});
}
@@ -513,7 +584,9 @@ function teardownPostgreSql(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var cmd = [ '/addons/postgresql/service.sh', 'remove', app.id ];
const appId = app.id.replace(/-/g, '');
var cmd = [ '/addons/postgresql/service.sh', 'remove', appId ];
debugApp(app, 'Tearing down postgresql');
@@ -525,6 +598,10 @@ function teardownPostgreSql(app, options, callback) {
}
function backupPostgreSql(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Backing up postgresql');
callback = once(callback); // ChildProcess exit may or may not be called after error
@@ -532,12 +609,17 @@ function backupPostgreSql(app, options, callback) {
var output = fs.createWriteStream(path.join(paths.APPS_DATA_DIR, app.id, 'postgresqldump'));
output.on('error', callback);
var cmd = [ '/addons/postgresql/service.sh', 'backup', app.id ];
const appId = app.id.replace(/-/g, '');
var cmd = [ '/addons/postgresql/service.sh', 'backup', appId ];
docker.execContainer('postgresql', cmd, { stdout: output }, callback);
}
function restorePostgreSql(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
callback = once(callback);
setupPostgreSql(app, options, function (error) {
@@ -548,7 +630,8 @@ function restorePostgreSql(app, options, callback) {
var input = fs.createReadStream(path.join(paths.APPS_DATA_DIR, app.id, 'postgresqldump'));
input.on('error', callback);
var cmd = [ '/addons/postgresql/service.sh', 'restore', app.id ];
const appId = app.id.replace(/-/g, '');
var cmd = [ '/addons/postgresql/service.sh', 'restore', appId ];
docker.execContainer('postgresql', cmd, { stdin: input }, callback);
});
@@ -561,16 +644,30 @@ function setupMongoDb(app, options, callback) {
debugApp(app, 'Setting up mongodb');
var cmd = [ '/addons/mongodb/service.sh', 'add', app.id ];
appdb.getAddonConfigByName(app.id, 'mongodb', 'MONGODB_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
docker.execContainer('mongodb', cmd, { bufferStdout: true }, function (error, stdout) {
if (error) return callback(error);
const password = error ? hat(4 * 128) : existingPassword;
var result = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
var env = result.map(function (r) { var idx = r.indexOf('='); return { name: r.substr(0, idx), value: r.substr(idx + 1) }; });
const dbname = app.id;
debugApp(app, 'Setting mongodb addon config to %j', env);
appdb.setAddonConfig(app.id, 'mongodb', env, callback);
var cmd = [ '/addons/mongodb/service.sh', 'add', dbname, password ];
docker.execContainer('mongodb', cmd, { bufferStdout: true }, function (error) {
if (error) return callback(error);
var env = [
{ name: 'MONGODB_URL', value : `mongodb://${dbname}:${password}@mongodb/${dbname}` },
{ name: 'MONGODB_USERNAME', value : dbname },
{ name: 'MONGODB_PASSWORD', value: password },
{ name: 'MONGODB_HOST', value : 'mongodb' },
{ name: 'MONGODB_PORT', value : '27017' },
{ name: 'MONGODB_DATABASE', value : dbname }
];
debugApp(app, 'Setting mongodb addon config to %j', env);
appdb.setAddonConfig(app.id, 'mongodb', env, callback);
});
});
}
@@ -579,7 +676,8 @@ function teardownMongoDb(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var cmd = [ '/addons/mongodb/service.sh', 'remove', app.id ];
const dbname = app.id;
var cmd = [ '/addons/mongodb/service.sh', 'remove', dbname ];
debugApp(app, 'Tearing down mongodb');
@@ -591,6 +689,10 @@ function teardownMongoDb(app, options, callback) {
}
function backupMongoDb(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Backing up mongodb');
callback = once(callback); // ChildProcess exit may or may not be called after error
@@ -598,12 +700,17 @@ function backupMongoDb(app, options, callback) {
var output = fs.createWriteStream(path.join(paths.APPS_DATA_DIR, app.id, 'mongodbdump'));
output.on('error', callback);
var cmd = [ '/addons/mongodb/service.sh', 'backup', app.id ];
const dbname = app.id;
var cmd = [ '/addons/mongodb/service.sh', 'backup', dbname ];
docker.execContainer('mongodb', cmd, { stdout: output }, callback);
}
function restoreMongoDb(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
callback = once(callback); // ChildProcess exit may or may not be called after error
setupMongoDb(app, options, function (error) {
@@ -614,7 +721,9 @@ function restoreMongoDb(app, options, callback) {
var input = fs.createReadStream(path.join(paths.APPS_DATA_DIR, app.id, 'mongodbdump'));
input.on('error', callback);
var cmd = [ '/addons/mongodb/service.sh', 'restore', app.id ];
const dbname = app.id;
var cmd = [ '/addons/mongodb/service.sh', 'restore', dbname ];
docker.execContainer('mongodb', cmd, { stdin: input }, callback);
});
}
@@ -625,58 +734,63 @@ function setupRedis(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var redisPassword = generatePassword(128, false /* memorable */, /[\w\d_]/); // ensure no / in password for being sed friendly (and be uri friendly)
var redisVarsFile = path.join(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
var redisDataDir = path.join(paths.APPS_DATA_DIR, app.id + '/redis');
appdb.getAddonConfigByName(app.id, 'redis', 'REDIS_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
if (!safe.fs.writeFileSync(redisVarsFile, 'REDIS_PASSWORD=' + redisPassword)) {
return callback(new Error('Error writing redis config'));
}
const redisPassword = error ? hat(4 * 48) : existingPassword; // see box#362 for password length
if (!safe.fs.mkdirSync(redisDataDir) && safe.error.code !== 'EEXIST') return callback(new Error('Error creating redis data dir:' + safe.error));
var redisVarsFile = path.join(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
var redisDataDir = path.join(paths.APPS_DATA_DIR, app.id + '/redis');
// Compute redis memory limit based on app's memory limit (this is arbitrary)
var memoryLimit = app.memoryLimit || app.manifest.memoryLimit || 0;
if (!safe.fs.writeFileSync(redisVarsFile, 'REDIS_PASSWORD=' + redisPassword)) {
return callback(new Error('Error writing redis config'));
}
if (memoryLimit === -1) { // unrestricted (debug mode)
memoryLimit = 0;
} else if (memoryLimit === 0 || memoryLimit <= (2 * 1024 * 1024 * 1024)) { // less than 2G (ram+swap)
memoryLimit = 150 * 1024 * 1024; // 150m
} else {
memoryLimit = 600 * 1024 * 1024; // 600m
}
if (!safe.fs.mkdirSync(redisDataDir) && safe.error.code !== 'EEXIST') return callback(new Error('Error creating redis data dir:' + safe.error));
const tag = infra.images.redis.tag, redisName = 'redis-' + app.id;
const label = app.intrinsicFqdn;
// note that we do not add appId label because this interferes with the stop/start app logic
const cmd = `docker run --restart=always -d --name=${redisName} \
--label=location=${label} \
--net cloudron \
--net-alias ${redisName} \
-m ${memoryLimit/2} \
--memory-swap ${memoryLimit} \
--dns 172.18.0.1 \
--dns-search=. \
-v ${redisVarsFile}:/etc/redis/redis_vars.sh:ro \
-v ${redisDataDir}:/var/lib/redis:rw \
--read-only -v /tmp -v /run ${tag}`;
// Compute redis memory limit based on app's memory limit (this is arbitrary)
var memoryLimit = app.memoryLimit || app.manifest.memoryLimit || 0;
var env = [
{ name: 'REDIS_URL', value: 'redis://redisuser:' + redisPassword + '@redis-' + app.id },
{ name: 'REDIS_PASSWORD', value: redisPassword },
{ name: 'REDIS_HOST', value: redisName },
{ name: 'REDIS_PORT', value: '6379' }
];
if (memoryLimit === -1) { // unrestricted (debug mode)
memoryLimit = 0;
} else if (memoryLimit === 0 || memoryLimit <= (2 * 1024 * 1024 * 1024)) { // less than 2G (ram+swap)
memoryLimit = 150 * 1024 * 1024; // 150m
} else {
memoryLimit = 600 * 1024 * 1024; // 600m
}
async.series([
// stop so that redis can flush itself with SIGTERM
shell.execSync.bind(null, 'stopRedis', `docker stop --time=10 ${redisName} 2>/dev/null || true`),
shell.execSync.bind(null, 'stopRedis', `docker rm --volumes ${redisName} 2>/dev/null || true`),
shell.execSync.bind(null, 'startRedis', cmd),
appdb.setAddonConfig.bind(null, app.id, 'redis', env)
], function (error) {
if (error) debug('Error setting up redis: ', error);
callback(error);
const tag = infra.images.redis.tag, redisName = 'redis-' + app.id;
const label = app.fqdn;
// note that we do not add appId label because this interferes with the stop/start app logic
const cmd = `docker run --restart=always -d --name=${redisName} \
--label=location=${label} \
--net cloudron \
--net-alias ${redisName} \
-m ${memoryLimit/2} \
--memory-swap ${memoryLimit} \
--dns 172.18.0.1 \
--dns-search=. \
-v ${redisVarsFile}:/etc/redis/redis_vars.sh:ro \
-v ${redisDataDir}:/var/lib/redis:rw \
--read-only -v /tmp -v /run ${tag}`;
var env = [
{ name: 'REDIS_URL', value: 'redis://redisuser:' + redisPassword + '@redis-' + app.id },
{ name: 'REDIS_PASSWORD', value: redisPassword },
{ name: 'REDIS_HOST', value: redisName },
{ name: 'REDIS_PORT', value: '6379' }
];
async.series([
// stop so that redis can flush itself with SIGTERM
shell.execSync.bind(null, 'stopRedis', `docker stop --time=10 ${redisName} 2>/dev/null || true`),
shell.execSync.bind(null, 'stopRedis', `docker rm --volumes ${redisName} 2>/dev/null || true`),
shell.execSync.bind(null, 'startRedis', cmd),
appdb.setAddonConfig.bind(null, app.id, 'redis', env)
], function (error) {
if (error) debug('Error setting up redis: ', error);
callback(error);
});
});
}
@@ -697,7 +811,7 @@ function teardownRedis(app, options, callback) {
safe.fs.unlinkSync(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
shell.sudo('teardownRedis', [ RMAPPDIR_CMD, app.id + '/redis', true /* delete directory */ ], function (error, stdout, stderr) {
shell.sudo('teardownRedis', [ RMAPPDIR_CMD, app.id + '/redis', true /* delete directory */ ], function (error /* ,stdout , stderr*/) {
if (error) return callback(new Error('Error removing redis data:' + error));
appdb.unsetAddonConfig(app.id, 'redis', callback);
@@ -706,6 +820,10 @@ function teardownRedis(app, options, callback) {
}
function backupRedis(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Backing up redis');
var cmd = [ '/addons/redis/service.sh', 'backup' ]; // the redis dir is volume mounted
+7 -6
View File
@@ -61,7 +61,7 @@ var assert = require('assert'),
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.installationProgress', 'apps.runState',
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'apps.location', 'apps.domain', 'apps.dnsRecordId',
'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit',
'apps.altDomain', 'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
'apps.creationTime', 'apps.updateTime' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
@@ -196,17 +196,18 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal
var accessRestriction = data.accessRestriction || null;
var accessRestrictionJson = JSON.stringify(accessRestriction);
var memoryLimit = data.memoryLimit || 0;
var altDomain = data.altDomain || null;
var xFrameOptions = data.xFrameOptions || '';
var installationState = data.installationState || exports.ISTATE_PENDING_INSTALL;
var restoreConfigJson = data.restoreConfig ? JSON.stringify(data.restoreConfig) : null; // used when cloning
var sso = 'sso' in data ? data.sso : null;
var robotsTxt = 'robotsTxt' in data ? data.robotsTxt : null;
var debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null;
var queries = [];
queries.push({
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, domain, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, restoreConfigJson, sso, debugModeJson) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, location, domain, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, restoreConfigJson, sso, debugModeJson ]
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, domain, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt) ' +
' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, location, domain, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt ]
});
Object.keys(portBindings).forEach(function (env) {
@@ -219,8 +220,8 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal
// only allocate a mailbox if mailboxName is set
if (data.mailboxName) {
queries.push({
query: 'INSERT INTO mailboxes (name, domain, ownerId, ownerType) VALUES (?, ?, ?, ?)',
args: [ data.mailboxName, domain, id, mailboxdb.TYPE_APP ]
query: 'INSERT INTO mailboxes (name, type, domain, ownerId, ownerType) VALUES (?, ?, ?, ?, ?)',
args: [ data.mailboxName, mailboxdb.TYPE_MAILBOX, domain, id, mailboxdb.OWNER_TYPE_APP ]
});
}
+5 -7
View File
@@ -5,7 +5,6 @@ var appdb = require('./appdb.js'),
assert = require('assert'),
async = require('async'),
DatabaseError = require('./databaseerror.js'),
config = require('./config.js'),
debug = require('debug')('box:apphealthmonitor'),
docker = require('./docker.js').connection,
mailer = require('./mailer.js'),
@@ -24,13 +23,9 @@ var gRunTimeout = null;
var gDockerEventStream = null;
function debugApp(app) {
assert(!app || typeof app === 'object');
assert(typeof app === 'object');
var prefix = app ? app.intrinsicFqdn : '(no app)';
var manifestAppId = app ? app.manifest.id : '';
var id = app ? app.id : '';
debug(prefix + ' ' + manifestAppId + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)) + ' - ' + id);
debug(app.fqdn + ' ' + app.manifest.id + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)) + ' - ' + app.id);
}
function setHealth(app, health, callback) {
@@ -71,6 +66,9 @@ function setHealth(app, health, callback) {
// callback is called with error for fatal errors and not if health check failed
function checkAppHealth(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
debugApp(app, 'skipped. istate:%s rstate:%s', app.installationState, app.runState);
return callback(null);
+170 -124
View File
@@ -4,6 +4,7 @@ exports = module.exports = {
AppsError: AppsError,
hasAccessTo: hasAccessTo,
removeInternalAppFields: removeInternalAppFields,
get: get,
getByIpAddress: getByIpAddress,
@@ -61,10 +62,11 @@ var addons = require('./addons.js'),
docker = require('./docker.js'),
domaindb = require('./domaindb.js'),
domains = require('./domains.js'),
DomainError = require('./domains.js').DomainError,
DomainsError = require('./domains.js').DomainsError,
eventlog = require('./eventlog.js'),
fs = require('fs'),
groups = require('./groups.js'),
mail = require('./mail.js'),
mailboxdb = require('./mailboxdb.js'),
manifestFormat = require('cloudron-manifestformat'),
os = require('os'),
@@ -83,7 +85,8 @@ var addons = require('./addons.js'),
url = require('url'),
util = require('util'),
uuid = require('uuid'),
validator = require('validator');
validator = require('validator'),
_ = require('underscore');
// http://dustinsenos.com/articles/customErrorsInNode
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
@@ -172,14 +175,14 @@ function validatePortBindings(portBindings, tcpPorts) {
993, /* imaps */
2003, /* graphite (lo) */
2004, /* graphite (lo) */
2020, /* install server */
2020, /* mail server */
config.get('port'), /* app server (lo) */
config.get('sysadminPort'), /* sysadmin app server (lo) */
config.get('smtpPort'), /* internal smtp port (lo) */
config.get('ldapPort'), /* ldap server (lo) */
3306, /* mysql (lo) */
4190, /* managesieve */
8000 /* graphite (lo) */
8000, /* graphite (lo) */
];
if (!portBindings) return null;
@@ -306,20 +309,29 @@ function getDuplicateErrorDetails(location, portBindings, error) {
return new AppsError(AppsError.ALREADY_EXISTS);
}
// app configs that is useful for 'archival' into the app backup config.json
function getAppConfig(app) {
return {
manifest: app.manifest,
location: app.location,
domain: app.domain,
intrinsicFqdn: app.intrinsicFqdn,
accessRestriction: app.accessRestriction,
portBindings: app.portBindings,
memoryLimit: app.memoryLimit,
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN',
altDomain: app.altDomain
robotsTxt: app.robotsTxt,
sso: app.sso
};
}
function removeInternalAppFields(app) {
return _.pick(app,
'id', 'appStoreId', 'installationState', 'installationProgress', 'runState', 'health',
'location', 'domain', 'fqdn', 'mailboxName',
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'xFrameOptions',
'sso', 'debugMode', 'robotsTxt', 'enableBackup', 'creationTime', 'updateTime');
}
function getIconUrlSync(app) {
var iconPath = paths.APP_ICONS_DIR + '/' + app.id + '.png';
return fs.existsSync(iconPath) ? '/api/v1/apps/' + app.id + '/icon' : null;
@@ -362,12 +374,16 @@ function get(appId, callback) {
domaindb.get(app.domain, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
app.intrinsicFqdn = domains.fqdn(app.location, app.domain, result.provider);
app.iconUrl = getIconUrlSync(app);
app.fqdn = app.altDomain || app.intrinsicFqdn;
app.cnameTarget = app.altDomain ? app.intrinsicFqdn : null;
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
callback(null, app);
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!error) app.mailboxName = mailboxes[0].name;
callback(null, app);
});
});
});
}
@@ -386,12 +402,16 @@ function getByIpAddress(ip, callback) {
domaindb.get(app.domain, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
app.intrinsicFqdn = domains.fqdn(app.location, app.domain, result.provider);
app.iconUrl = getIconUrlSync(app);
app.fqdn = app.altDomain || app.intrinsicFqdn;
app.cnameTarget = app.altDomain ? app.intrinsicFqdn : null;
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
callback(null, app);
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!error) app.mailboxName = mailboxes[0].name;
callback(null, app);
});
});
});
});
@@ -407,12 +427,16 @@ function getAll(callback) {
domaindb.get(app.domain, function (error, result) {
if (error) return iteratorDone(new AppsError(AppsError.INTERNAL_ERROR, error));
app.intrinsicFqdn = domains.fqdn(app.location, app.domain, result.provider);
app.iconUrl = getIconUrlSync(app);
app.fqdn = app.altDomain || app.intrinsicFqdn;
app.cnameTarget = app.altDomain ? app.intrinsicFqdn : null;
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
iteratorDone();
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!error) app.mailboxName = mailboxes[0].name;
iteratorDone(null, app);
});
});
}, function (error) {
if (error) return callback(error);
@@ -468,7 +492,6 @@ function install(data, auditSource, callback) {
cert = data.cert || null,
key = data.key || null,
memoryLimit = data.memoryLimit || 0,
altDomain = data.altDomain || null,
xFrameOptions = data.xFrameOptions || 'SAMEORIGIN',
sso = 'sso' in data ? data.sso : null,
debugMode = data.debugMode || null,
@@ -513,8 +536,6 @@ function install(data, auditSource, callback) {
// if sso was unspecified, enable it by default if possible
if (sso === null) sso = !!manifest.addons['ldap'] || !!manifest.addons['oauth'];
if (altDomain !== null && !validator.isFQDN(altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid external domain'));
var appId = uuid.v4();
if (icon) {
@@ -526,56 +547,70 @@ function install(data, auditSource, callback) {
}
domains.get(domain, function (error, domainObject) {
if (error && error.reason === DomainError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
var intrinsicFqdn = domains.fqdn(location, domain, domainObject.provider);
var fqdn = domains.fqdn(location, domain, domainObject.provider);
error = validateHostname(location, domain, intrinsicFqdn);
error = validateHostname(location, domain, fqdn);
if (error) return callback(error);
if (cert && key) {
error = reverseProxy.validateCertificate(intrinsicFqdn, cert, key);
error = reverseProxy.validateCertificate(fqdn, cert, key);
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
}
debug('Will install app with id : ' + appId);
appstore.purchase(appId, appStoreId, function (error) {
if (error && error.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error && error.reason === AppstoreError.BILLING_REQUIRED) return callback(new AppsError(AppsError.BILLING_REQUIRED, error.message));
if (error && error.reason === AppstoreError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
var data = {
accessRestriction: accessRestriction,
memoryLimit: memoryLimit,
xFrameOptions: xFrameOptions,
sso: sso,
debugMode: debugMode,
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app',
restoreConfig: backupId ? { backupId: backupId, backupFormat: backupFormat } : null,
enableBackup: enableBackup,
robotsTxt: robotsTxt
};
appdb.add(appId, appStoreId, manifest, location, domain, portBindings, data, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, error.message));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
var data = {
accessRestriction: accessRestriction,
memoryLimit: memoryLimit,
altDomain: altDomain,
xFrameOptions: xFrameOptions,
sso: sso,
debugMode: debugMode,
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app',
restoreConfig: backupId ? { backupId: backupId, backupFormat: backupFormat } : null,
enableBackup: enableBackup,
robotsTxt: robotsTxt,
intrinsicFqdn: intrinsicFqdn
};
appstore.purchase(appId, appStoreId, function (appstoreError) {
// if purchase failed, rollback the appdb record
if (appstoreError) {
appdb.del(appId, function (error) {
if (error) console.error('Failed to rollback app installation.', error);
appdb.add(appId, appStoreId, manifest, location, domain, portBindings, data, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (appstoreError.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, appstoreError.message));
if (appstoreError && appstoreError.reason === AppstoreError.BILLING_REQUIRED) return callback(new AppsError(AppsError.BILLING_REQUIRED, appstoreError.message));
if (appstoreError && appstoreError.reason === AppstoreError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, appstoreError.message));
callback(new AppsError(AppsError.INTERNAL_ERROR, appstoreError));
});
return;
}
// save cert to boxdata/certs
if (cert && key) {
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, intrinsicFqdn + '.user.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, intrinsicFqdn + '.user.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.user.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.user.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
}
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, location: location, domain: domain, manifest: manifest, backupId: backupId });
// fetch fresh app object for eventlog
get(appId, function (error, result) {
if (error) return callback(error);
callback(null, { id : appId });
eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, app: result });
callback(null, { id : appId });
});
});
});
});
@@ -588,9 +623,8 @@ function configure(appId, data, auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
get(appId, function (error, app) {
if (error) return callback(error);
var domain, location, portBindings, values = { };
if ('location' in data) location = values.location = data.location.toLowerCase();
@@ -605,11 +639,6 @@ function configure(appId, data, auditSource, callback) {
if (error) return callback(error);
}
if ('altDomain' in data) {
values.altDomain = data.altDomain;
if (values.altDomain !== null && !validator.isFQDN(values.altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid external domain'));
}
if ('portBindings' in data) {
portBindings = values.portBindings = data.portBindings;
error = validatePortBindings(values.portBindings, app.manifest.tcpPorts);
@@ -642,28 +671,31 @@ function configure(appId, data, auditSource, callback) {
if (error) return callback(error);
}
if ('mailboxName' in data) {
error = mail.validateName(data.mailboxName);
if (error) return callback(error);
}
domains.get(domain, function (error, domainObject) {
if (error && error.reason === DomainError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
var intrinsicFqdn = domains.fqdn(location, domain, domainObject.provider);
var fqdn = domains.fqdn(location, domain, domainObject.provider);
error = validateHostname(location, domain, intrinsicFqdn);
error = validateHostname(location, domain, fqdn);
if (error) return callback(error);
// save cert to boxdata/certs. TODO: move this to apptask when we have a real task queue
if ('cert' in data && 'key' in data) {
if (data.cert && data.key) {
var vhost = values.altDomain || intrinsicFqdn;
error = reverseProxy.validateCertificate(vhost, data.cert, data.key);
error = reverseProxy.validateCertificate(fqdn, data.cert, data.key);
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${vhost}.user.cert`), data.cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${vhost}.user.key`), data.key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`), data.cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.key`), data.key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
} else { // remove existing cert/key
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${vhost}.user.cert`))) debug('Error removing cert: ' + safe.error.message);
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${vhost}..user.key`))) debug('Error removing key: ' + safe.error.message);
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`))) debug('Error removing cert: ' + safe.error.message);
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}..user.key`))) debug('Error removing key: ' + safe.error.message);
}
}
@@ -673,8 +705,8 @@ function configure(appId, data, auditSource, callback) {
debug('Will configure app with id:%s values:%j', appId, values);
var oldName = (app.location ? app.location : app.manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
var newName = (location ? location : app.manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
var oldName = app.mailboxName;
var newName = data.mailboxName || app.mailboxName;
mailboxdb.updateName(oldName, values.oldConfig.domain, newName, domain, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new AppsError(AppsError.ALREADY_EXISTS, 'This mailbox is already taken'));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
@@ -687,9 +719,14 @@ function configure(appId, data, auditSource, callback) {
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId });
// fetch fresh app object for eventlog
get(appId, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
callback(null);
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: result });
callback(null);
});
});
});
});
@@ -729,9 +766,8 @@ function update(appId, data, auditSource, callback) {
}
}
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
get(appId, function (error, app) {
if (error) return callback(error);
// prevent user from installing a app with different manifest id over an existing app
// this allows cloudron install -f --app <appid> for an app installed from the appStore
@@ -756,7 +792,7 @@ function update(appId, data, auditSource, callback) {
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId: appId, toManifest: manifest, fromManifest: app.manifest, force: data.force });
eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId: appId, toManifest: manifest, fromManifest: app.manifest, force: data.force, app: app });
// clear update indicator, if update fails, it will come back through the update checker
updateChecker.resetAppUpdateInfo(appId);
@@ -780,10 +816,8 @@ function getLogs(appId, options, callback) {
debug('Getting logs for %s', appId);
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
get(appId, function (error, app) {
if (error) return callback(error);
var lines = options.lines || 100,
follow = !!options.follow,
@@ -827,9 +861,8 @@ function restore(appId, data, auditSource, callback) {
debug('Will restore app with id:%s', appId);
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
get(appId, function (error, app) {
if (error) return callback(error);
// for empty or null backupId, use existing manifest to mimic a reinstall
var func = data.backupId ? backups.get.bind(null, data.backupId) : function (next) { return next(null, { manifest: app.manifest }); };
@@ -858,7 +891,7 @@ function restore(appId, data, auditSource, callback) {
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { appId: appId });
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { appId: appId, app: app });
callback(null);
});
@@ -884,13 +917,12 @@ function clone(appId, data, auditSource, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof portBindings, 'object');
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
get(appId, function (error, app) {
if (error) return callback(error);
backups.get(backupId, function (error, backupInfo) {
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
if (error && error.reason === BackupsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
if (error && error.reason === BackupsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Backup not found'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!backupInfo.manifest) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore config'));
@@ -903,41 +935,56 @@ function clone(appId, data, auditSource, callback) {
if (error) return callback(error);
domains.get(domain, function (error, domainObject) {
if (error && error.reason === DomainError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
var intrinsicFqdn = domains.fqdn(location, domain, domainObject.provider);
error = validateHostname(location, domain, intrinsicFqdn);
error = validateHostname(location, domain, domains.fqdn(location, domain, domainObject.provider));
if (error) return callback(error);
var newAppId = uuid.v4(), manifest = backupInfo.manifest;
appstore.purchase(newAppId, app.appStoreId, function (error) {
if (error && error.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error && error.reason === AppstoreError.BILLING_REQUIRED) return callback(new AppsError(AppsError.BILLING_REQUIRED, error.message));
if (error && error.reason === AppstoreError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
var data = {
installationState: appdb.ISTATE_PENDING_CLONE,
memoryLimit: app.memoryLimit,
accessRestriction: app.accessRestriction,
xFrameOptions: app.xFrameOptions,
restoreConfig: { backupId: backupId, backupFormat: backupInfo.format },
sso: !!app.sso,
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app',
enableBackup: app.enableBackup,
robotsTxt: app.robotsTxt
};
appdb.add(newAppId, app.appStoreId, manifest, location, domain, portBindings, data, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
var data = {
installationState: appdb.ISTATE_PENDING_CLONE,
memoryLimit: app.memoryLimit,
accessRestriction: app.accessRestriction,
xFrameOptions: app.xFrameOptions,
restoreConfig: { backupId: backupId, backupFormat: backupInfo.format },
sso: !!app.sso,
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app'
};
appstore.purchase(newAppId, app.appStoreId, function (appstoreError) {
// if purchase failed, rollback the appdb record
if (appstoreError) {
appdb.del(newAppId, function (error) {
if (error) console.error('Failed to rollback app installation.', error);
appdb.add(newAppId, app.appStoreId, manifest, location, domain, portBindings, data, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (appstoreError.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, appstoreError.message));
if (appstoreError && appstoreError.reason === AppstoreError.BILLING_REQUIRED) return callback(new AppsError(AppsError.BILLING_REQUIRED, appstoreError.message));
if (appstoreError && appstoreError.reason === AppstoreError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, appstoreError.message));
callback(new AppsError(AppsError.INTERNAL_ERROR, appstoreError));
});
return;
}
taskmanager.restartAppTask(newAppId);
eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, location: location, manifest: manifest });
// fetch fresh app object for eventlog
get(appId, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
callback(null, { id : newAppId });
eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, oldApp: app, newApp: result });
callback(null, { id : newAppId });
});
});
});
});
@@ -952,10 +999,10 @@ function uninstall(appId, auditSource, callback) {
debug('Will uninstall app with id:%s', appId);
get(appId, function (error, result) {
get(appId, function (error, app) {
if (error) return callback(error);
appstore.unpurchase(appId, result.appStoreId, function (error) {
appstore.unpurchase(appId, app.appStoreId, function (error) {
if (error && error.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error && error.reason === AppstoreError.BILLING_REQUIRED) return callback(new AppsError(AppsError.BILLING_REQUIRED, error.message));
if (error && error.reason === AppstoreError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
@@ -966,7 +1013,7 @@ function uninstall(appId, auditSource, callback) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId });
eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId, app: app });
taskmanager.startAppTask(appId, callback);
});
@@ -1017,7 +1064,7 @@ function checkManifestConstraints(manifest) {
}
if (semver.valid(manifest.minBoxVersion) && semver.gt(manifest.minBoxVersion, config.version())) {
return new AppsError(AppsError.BAD_FIELD, 'minBoxVersion exceeds Box version');
return new AppsError(AppsError.BAD_FIELD, 'App version requires a new platform version');
}
return null;
@@ -1031,9 +1078,8 @@ function exec(appId, options, callback) {
var cmd = options.cmd || [ '/bin/bash' ];
assert(util.isArray(cmd) && cmd.length > 0);
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
get(appId, function (error, app) {
if (error) return callback(error);
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
return callback(new AppsError(AppsError.BAD_STATE, 'App not installed or running'));
@@ -1172,17 +1218,17 @@ function listBackups(page, perPage, appId, callback) {
function restoreInstalledApps(callback) {
assert.strictEqual(typeof callback, 'function');
appdb.getAll(function (error, apps) {
getAll(function (error, apps) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
async.map(apps, function (app, iteratorDone) {
debug('marking %s for restore', app.intrinsicFqdn);
backups.getByAppIdPaged(1, 1, app.id, function (error, results) {
var restoreConfig = !error && results.length ? { backupId: results[0].id, backupFormat: results[0].format } : null;
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_RESTORE, { restoreConfig: restoreConfig, oldConfig: null }, function (error) {
if (error) debug('did not mark %s for restore', app.intrinsicFqdn, error);
debug(`marking ${app.fqdn} for restore using restore config ${JSON.stringify(restoreConfig)}`);
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_RESTORE, { restoreConfig: restoreConfig, oldConfig: getAppConfig(app) }, function (error) {
if (error) debug(`Error marking ${app.fqdn} for restore: ${JSON.stringify(error)}`);
iteratorDone(); // always succeed
});
@@ -1194,14 +1240,14 @@ function restoreInstalledApps(callback) {
function configureInstalledApps(callback) {
assert.strictEqual(typeof callback, 'function');
appdb.getAll(function (error, apps) {
getAll(function (error, apps) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
async.map(apps, function (app, iteratorDone) {
debug('marking %s for reconfigure', app.intrinsicFqdn);
debug(`marking ${app.fqdn} for reconfigure`);
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_CONFIGURE, { oldConfig: null }, function (error) {
if (error) debug('did not mark %s for reconfigure', app.intrinsicFqdn, error);
if (error) debug(`Error marking ${app.fqdn} for reconfigure: ${JSON.stringify(error)}`);
iteratorDone(); // always succeed
});
+134 -66
View File
@@ -5,6 +5,7 @@ exports = module.exports = {
unpurchase: unpurchase,
getSubscription: getSubscription,
isFreePlan: isFreePlan,
sendAliveStatus: sendAliveStatus,
@@ -18,9 +19,13 @@ exports = module.exports = {
AppstoreError: AppstoreError
};
var assert = require('assert'),
var appdb = require('./appdb.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
config = require('./config.js'),
debug = require('debug')('box:appstore'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
mail = require('./mail.js'),
os = require('os'),
@@ -86,6 +91,10 @@ function getSubscription(callback) {
});
}
function isFreePlan(subscription) {
return !subscription || subscription.plan.id === 'free';
}
function purchase(appId, appstoreId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof appstoreId, 'string');
@@ -93,20 +102,41 @@ function purchase(appId, appstoreId, callback) {
if (appstoreId === '') return callback(null);
getAppstoreConfig(function (error, appstoreConfig) {
function doThePurchase() {
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/apps/' + appId;
var data = { appstoreId: appstoreId };
superagent.post(url).send(data).query({ accessToken: appstoreConfig.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));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
if (result.statusCode === 402) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED, result.body.message));
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)));
callback(null);
});
});
}
getSubscription(function (error, result) {
if (error) return callback(error);
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/apps/' + appId;
var data = { appstoreId: appstoreId };
// only check for app install count if on the free plan
if (result.id !== 'free') return doThePurchase();
superagent.post(url).send(data).query({ accessToken: appstoreConfig.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));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
if (result.statusCode === 402) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED, result.body.message));
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)));
appdb.getAppStoreIds(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
callback(null);
var count = result.filter(function (a) { return !!a.appStoreId; }).length;
// we only allow max of 2 app installations without a subscription
// WARNING install and clone in apps.js will first add the db record and then call purchase() so we test for more than 2 here
if (count > 2) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED, 'Too many apps installed'));
doThePurchase();
});
});
}
@@ -127,7 +157,7 @@ function unpurchase(appId, appstoreId, callback) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
if (result.statusCode === 404) return callback(null); // was never 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 AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
superagent.del(url).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
@@ -140,62 +170,87 @@ function unpurchase(appId, appstoreId, callback) {
});
}
function sendAliveStatus(data, callback) {
function sendAliveStatus(callback) {
callback = callback || NOOP_CALLBACK;
settings.getAll(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
var allSettings, allDomains, mailDomains, loginEvents;
mail.getAll(function (error, mailDomains) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
eventlog.getAllPaged(eventlog.ACTION_USER_LOGIN, null, 1, 1, function (error, loginEvents) {
async.series([
function (callback) {
settings.getAll(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
allSettings = result;
callback();
});
},
function (callback) {
domains.getAll(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
allDomains = result;
callback();
});
},
function (callback) {
mail.getDomains(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, 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));
loginEvents = result;
callback();
});
}
], function (error) {
if (error) return callback(error);
var backendSettings = {
backupConfig: {
provider: result[settings.BACKUP_CONFIG_KEY].provider,
hardlinks: !result[settings.BACKUP_CONFIG_KEY].noHardlinks
},
domainConfig: {
count: mailDomains.length
},
mailConfig: {
outboundCount: mailDomains.length,
inboundCount: mailDomains.filter(function (d) { return d.enabled; }).length,
catchAllCount: mailDomains.filter(function (d) { return d.catchAll.length !== 0; }).length,
relayProviders: Array.from(new Set(mailDomains.map(function (d) { return d.relay.provider; })))
},
autoupdatePattern: result[settings.AUTOUPDATE_PATTERN_KEY],
timeZone: result[settings.TIME_ZONE_KEY],
};
var backendSettings = {
backupConfig: {
provider: allSettings[settings.BACKUP_CONFIG_KEY].provider,
hardlinks: !allSettings[settings.BACKUP_CONFIG_KEY].noHardlinks
},
domainConfig: {
count: allDomains.length,
domains: Array.from(new Set(allDomains.map(function (d) { return { domain: d.domain, provider: d.provider }; })))
},
mailConfig: {
outboundCount: mailDomains.length,
inboundCount: mailDomains.filter(function (d) { return d.enabled; }).length,
catchAllCount: mailDomains.filter(function (d) { return d.catchAll.length !== 0; }).length,
relayProviders: Array.from(new Set(mailDomains.map(function (d) { return d.relay.provider; })))
},
appAutoupdatePattern: allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY],
boxAutoupdatePattern: allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY],
timeZone: allSettings[settings.TIME_ZONE_KEY],
};
var data = {
version: config.version(),
adminFqdn: config.adminFqdn(),
provider: config.provider(),
backendSettings: backendSettings,
machine: {
cpus: os.cpus(),
totalmem: os.totalmem()
},
events: {
lastLogin: loginEvents[0] ? (new Date(loginEvents[0].creationTime).getTime()) : 0
}
};
var data = {
version: config.version(),
adminFqdn: config.adminFqdn(),
provider: config.provider(),
backendSettings: backendSettings,
machine: {
cpus: os.cpus(),
totalmem: os.totalmem()
},
events: {
lastLogin: loginEvents[0] ? (new Date(loginEvents[0].creationTime).getTime()) : 0
}
};
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/alive';
superagent.post(url).send(data).query({ accessToken: appstoreConfig.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 !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Sending alive status failed. %s %j', result.status, result.body)));
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/alive';
superagent.post(url).send(data).query({ accessToken: appstoreConfig.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 !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Sending alive status failed. %s %j', result.status, result.body)));
callback(null);
});
});
callback(null);
});
});
});
@@ -242,9 +297,12 @@ function getAppUpdate(app, callback) {
const updateInfo = result.body;
// for the appstore, x.y.z is the same as x.y.z-0 but in semver, x.y.z > x.y.z-0
const curAppVersion = semver.prerelease(app.manifest.version) ? app.manifest.version : `${app.manifest.version}-0`;
// do some sanity checks
if (!safe.query(updateInfo, 'manifest.version') || semver.gt(app.manifest.version, safe.query(updateInfo, 'manifest.version'))) {
debug('Skipping malformed update of app %s version: %s. got %j', app.id, app.manifest.version, updateInfo);
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)));
}
@@ -281,16 +339,26 @@ function sendFeedback(info, callback) {
assert.strictEqual(typeof info.description, 'string');
assert.strictEqual(typeof callback, 'function');
function collectAppInfoIfNeeded(callback) {
if (!info.appId) return callback();
apps.get(info.appId, callback);
}
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/feedback';
collectAppInfoIfNeeded(function (error, result) {
if (error) console.error('Unable to get app info', error);
if (result) info.app = result;
superagent.post(url).query({ accessToken: appstoreConfig.token }).send(info).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/feedback';
callback(null);
superagent.post(url).query({ accessToken: appstoreConfig.token }).send(info).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
callback(null);
});
});
});
}
+21 -40
View File
@@ -15,8 +15,7 @@ exports = module.exports = {
_verifyManifest: verifyManifest,
_registerSubdomain: registerSubdomain,
_unregisterSubdomain: unregisterSubdomain,
_waitForDnsPropagation: waitForDnsPropagation,
_waitForAltDomainDnsPropagation: waitForAltDomainDnsPropagation
_waitForDnsPropagation: waitForDnsPropagation
};
require('supererror')({ splatchError: true });
@@ -38,7 +37,7 @@ var addons = require('./addons.js'),
debug = require('debug')('box:apptask'),
docker = require('./docker.js'),
domains = require('./domains.js'),
DomainError = domains.DomainError,
DomainsError = domains.DomainsError,
ejs = require('ejs'),
fs = require('fs'),
manifestFormat = require('cloudron-manifestformat'),
@@ -71,8 +70,7 @@ function initialize(callback) {
function debugApp(app) {
assert.strictEqual(typeof app, 'object');
var prefix = app ? (app.intrinsicFqdn || '(bare)') : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
// updates the app object and the database
@@ -266,17 +264,17 @@ function registerSubdomain(app, overwrite, callback) {
if (error) return callback(error);
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Registering subdomain location [%s] overwrite: %s', app.intrinsicFqdn, overwrite);
debugApp(app, 'Registering subdomain location [%s] overwrite: %s', app.fqdn, overwrite);
// get the current record before updating it
domains.getDNSRecords(app.location, app.domain, 'A', function (error, values) {
domains.getDnsRecords(app.location, app.domain, 'A', function (error, values) {
if (error) return retryCallback(error);
// refuse to update any existing DNS record for custom domains that we did not create
if (values.length !== 0 && !overwrite) return retryCallback(null, new Error('DNS Record already exists'));
domains.upsertDNSRecords(app.location, app.domain, 'A', [ ip ], function (error, changeId) {
if (error && (error.reason === DomainError.STILL_BUSY || error.reason === DomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
domains.upsertDnsRecords(app.location, app.domain, 'A', [ ip ], function (error, changeId) {
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(null, error || changeId);
});
@@ -305,11 +303,11 @@ function unregisterSubdomain(app, location, domain, callback) {
if (error) return callback(error);
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Unregistering subdomain: %s', app.intrinsicFqdn);
debugApp(app, 'Unregistering subdomain: %s', app.fqdn);
domains.removeDNSRecords(location, domain, 'A', [ ip ], function (error) {
if (error && error.reason === DomainError.NOT_FOUND) return retryCallback(null, null); // domain can be not found if oldConfig.domain or restoreConfig.domain was removed
if (error && (error.reason === DomainError.STILL_BUSY || error.reason === DomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
domains.removeDnsRecords(location, domain, 'A', [ ip ], function (error) {
if (error && error.reason === DomainsError.NOT_FOUND) return retryCallback(null, null); // domain can be not found if oldConfig.domain or restoreConfig.domain was removed
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(null, error);
});
@@ -343,27 +341,10 @@ function waitForDnsPropagation(app, callback) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
domains.waitForDNSRecord(app.intrinsicFqdn, app.domain, ip, 'A', { interval: 5000, times: 120 }, callback);
domains.waitForDnsRecord(app.fqdn, app.domain, ip, { interval: 5000, times: 240 }, callback);
});
}
function waitForAltDomainDnsPropagation(app, callback) {
if (!app.altDomain) return callback(null);
// try for 10 minutes before giving up. this allows the user to "reconfigure" the app in the case where
// an app has an external domain and cloudron is migrated to custom domain.
var isNakedDomain = tld.getDomain(app.altDomain) === app.altDomain;
if (isNakedDomain) { // check naked domains with A record since CNAME records don't work there
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
domains.waitForDNSRecord(app.altDomain, tld.getDomain(app.altDomain), ip, 'A', { interval: 10000, times: 60 }, callback);
});
} else {
domains.waitForDNSRecord(app.altDomain, tld.getDomain(app.altDomain), app.intrinsicFqdn + '.', 'CNAME', { interval: 10000, times: 60 }, callback);
}
}
// Ordering is based on the following rationale:
// - configure nginx, icon, oauth
// - register subdomain.
@@ -393,8 +374,14 @@ function install(app, callback) {
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
deleteContainers.bind(null, app),
// oldConfig can be null during upgrades
addons.teardownAddons.bind(null, app, app.oldConfig ? app.oldConfig.manifest.addons : app.manifest.addons),
function teardownAddons(next) {
// when restoring, app does not require these addons anymore. remove carefully to preserve the db passwords
var addonsToRemove = !isRestoring
? app.manifest.addons
: _.omit(app.oldConfig.manifest.addons, Object.keys(app.manifest.addons));
addons.teardownAddons(app, addonsToRemove, next);
},
deleteVolume.bind(null, app, { removeDirectory: false }), // do not remove any symlinked volume
// for restore case
@@ -446,9 +433,6 @@ function install(app, callback) {
updateApp.bind(null, app, { installationProgress: '85, Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Waiting for External Domain setup' }),
exports._waitForAltDomainDnsPropagation.bind(null, app), // required when restoring and !restoreConfig
updateApp.bind(null, app, { installationProgress: '95, Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
@@ -494,7 +478,7 @@ function configure(app, callback) {
assert.strictEqual(typeof callback, 'function');
// oldConfig can be null during an infra update
var locationChanged = app.oldConfig && (app.oldConfig.intrinsicFqdn !== app.intrinsicFqdn);
var locationChanged = app.oldConfig && (app.oldConfig.fqdn !== app.fqdn);
async.series([
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
@@ -541,9 +525,6 @@ function configure(app, callback) {
updateApp.bind(null, app, { installationProgress: '80, Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '85, Waiting for External Domain setup' }),
exports._waitForAltDomainDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
-125
View File
@@ -1,125 +0,0 @@
'use strict';
exports = module.exports = {
initialize: initialize,
uninitialize: uninitialize,
accessTokenAuth: accessTokenAuth
};
var assert = require('assert'),
BasicStrategy = require('passport-http').BasicStrategy,
BearerStrategy = require('passport-http-bearer').Strategy,
clients = require('./clients'),
ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy,
ClientsError = clients.ClientsError,
DatabaseError = require('./databaseerror'),
debug = require('debug')('box:auth'),
LocalStrategy = require('passport-local').Strategy,
crypto = require('crypto'),
passport = require('passport'),
tokendb = require('./tokendb'),
user = require('./user'),
UserError = user.UserError,
_ = require('underscore');
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
passport.serializeUser(function (user, callback) {
callback(null, user.id);
});
passport.deserializeUser(function(userId, callback) {
user.get(userId, function (error, result) {
if (error) return callback(error);
var md5 = crypto.createHash('md5').update(result.email).digest('hex');
result.gravatar = 'https://www.gravatar.com/avatar/' + md5 + '.jpg?s=24&d=mm';
callback(null, result);
});
});
passport.use(new LocalStrategy(function (username, password, callback) {
if (username.indexOf('@') === -1) {
user.verifyWithUsername(username, password, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
if (error && error.reason === UserError.WRONG_PASSWORD) return callback(null, false);
if (error) return callback(error);
if (!result) return callback(null, false);
callback(null, _.pick(result, 'id', 'username', 'email', 'admin'));
});
} else {
user.verifyWithEmail(username, password, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
if (error && error.reason === UserError.WRONG_PASSWORD) return callback(null, false);
if (error) return callback(error);
if (!result) return callback(null, false);
callback(null, _.pick(result, 'id', 'username', 'email', 'admin'));
});
}
}));
passport.use(new BasicStrategy(function (username, password, callback) {
if (username.indexOf('cid-') === 0) {
debug('BasicStrategy: detected client id %s instead of username:password', username);
// username is actually client id here
// password is client secret
clients.get(username, function (error, client) {
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
if (client.clientSecret != password) return callback(null, false);
return callback(null, client);
});
} else {
user.verifyWithUsername(username, password, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
if (error && error.reason === UserError.WRONG_PASSWORD) return callback(null, false);
if (error) return callback(error);
if (!result) return callback(null, false);
callback(null, result);
});
}
}));
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) { return callback(error); }
if (client.clientSecret != clientSecret) { return callback(null, false); }
return callback(null, client);
});
}));
passport.use(new BearerStrategy(accessTokenAuth));
callback(null);
}
function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
callback(null);
}
function accessTokenAuth(accessToken, callback) {
assert.strictEqual(typeof accessToken, 'string');
assert.strictEqual(typeof callback, 'function');
tokendb.get(accessToken, function (error, token) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
// scopes here can define what capabilities that token carries
// passport put the 'info' object into req.authInfo, where we can further validate the scopes
var info = { scope: token.scope };
user.get(token.identifier, function (error, user) {
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
callback(null, user, info);
});
});
}
+27 -18
View File
@@ -68,10 +68,9 @@ var NOOP_CALLBACK = function (error) { if (error) debug(error); };
var BACKUPTASK_CMD = path.join(__dirname, 'backuptask.js');
function debugApp(app) {
assert(!app || typeof app === 'object');
assert(typeof app === 'object');
var prefix = app ? app.intrinsicFqdn : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function BackupsError(reason, errorOrMessage) {
@@ -232,6 +231,11 @@ function sync(backupConfig, backupId, dataDir, callback) {
assert.strictEqual(typeof dataDir, 'string');
assert.strictEqual(typeof callback, 'function');
function setBackupProgress(message) {
debug('%s: %s', (new Date()).toISOString(), message);
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, message);
}
syncer.sync(dataDir, function processTask(task, iteratorCallback) {
debug('sync: processing task: %j', task);
var backupFilePath = path.join(getBackupFilePath(backupConfig, backupId, backupConfig.format), task.path);
@@ -239,28 +243,33 @@ function sync(backupConfig, backupId, dataDir, callback) {
if (task.operation === 'removedir') {
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, `Removing directory ${task.path}`);
return api(backupConfig.provider).removeDir(backupConfig, backupFilePath)
.on('progress', function (detail) {
debug(`sync: ${detail}`);
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, detail);
})
.on('progress', setBackupProgress)
.on('done', iteratorCallback);
} else if (task.operation === 'remove') {
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, `Removing ${task.path}`);
setBackupProgress(`Removing ${task.path}`);
return api(backupConfig.provider).remove(backupConfig, backupFilePath, iteratorCallback);
}
var retryCount = 0;
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
retryCallback = once(retryCallback); // protect again upload() erroring much later after read stream error
++retryCount;
debug(`${task.operation} ${task.path} try ${retryCount}`);
if (task.operation === 'add') {
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, `Adding ${task.path}`);
setBackupProgress(`Adding ${task.path} position ${task.position} try ${retryCount}`);
var stream = fs.createReadStream(path.join(dataDir, task.path));
stream.on('error', function () { return retryCallback(); }); // ignore error if file disappears
api(backupConfig.provider).upload(backupConfig, backupFilePath, stream, retryCallback);
stream.on('error', function (error) {
setBackupProgress(`read stream error for ${task.path}: ${error.message}`);
retryCallback();
}); // ignore error if file disappears
api(backupConfig.provider).upload(backupConfig, backupFilePath, stream, function (error) {
setBackupProgress(error ? `Error uploading ${task.path} try ${retryCount}: ${error.message}` : `Uploaded ${task.path}`);
retryCallback(error);
});
}
}, iteratorCallback);
}, 10 /* concurrency */, function (error) {
}, backupConfig.syncConcurrency || 10 /* concurrency */, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
callback();
@@ -294,8 +303,6 @@ function upload(backupId, format, dataDir, callback) {
assert.strictEqual(typeof dataDir, 'string');
assert.strictEqual(typeof callback, 'function');
callback = once(callback);
debug('upload: id %s format %s dataDir %s', backupId, format, dataDir);
settings.getBackupConfig(function (error, backupConfig) {
@@ -303,6 +310,8 @@ function upload(backupId, format, dataDir, callback) {
if (format === 'tgz') {
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
retryCallback = once(retryCallback); // protect again upload() erroring much later after tar stream error
var tarStream = createTarPackStream(dataDir, backupConfig.key || null);
tarStream.on('error', retryCallback); // already returns BackupsError
@@ -720,7 +729,7 @@ function backupApp(app, callback) {
const timestamp = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
safe.fs.unlinkSync(paths.BACKUP_LOG_FILE); // start fresh log file
progress.set(progress.BACKUP, 10, 'Backing up ' + (app.altDomain || app.intrinsicFqdn));
progress.set(progress.BACKUP, 10, 'Backing up ' + app.fqdn);
backupAppWithTimestamp(app, timestamp, function (error) {
progress.set(progress.BACKUP, 100, error ? error.message : '');
@@ -747,12 +756,12 @@ function backupBoxAndApps(auditSource, callback) {
var step = 100/(allApps.length+2);
async.mapSeries(allApps, function iterator(app, iteratorCallback) {
progress.set(progress.BACKUP, step * processed, 'Backing up ' + (app.altDomain || app.intrinsicFqdn));
progress.set(progress.BACKUP, step * processed, 'Backing up ' + app.fqdn);
++processed;
if (!app.enableBackup) {
progress.set(progress.BACKUP, step * processed, 'Skipped backup ' + (app.altDomain || app.intrinsicFqdn));
progress.set(progress.BACKUP, step * processed, 'Skipped backup ' + app.fqdn);
return iteratorCallback(null, null); // nothing to backup
}
@@ -762,7 +771,7 @@ function backupBoxAndApps(auditSource, callback) {
return iteratorCallback(error);
}
progress.set(progress.BACKUP, step * processed, 'Backed up ' + (app.altDomain || app.intrinsicFqdn));
progress.set(progress.BACKUP, step * processed, 'Backed up ' + app.fqdn);
iteratorCallback(null, backupId || null); // clear backupId if is in BAD_STATE and never backed up
});
+2 -2
View File
@@ -44,9 +44,9 @@ initialize(function (error) {
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, '');
backups.upload(backupId, format, dataDir, function resultHandler(error) {
if (error) debug('completed with error', error);
if (error) debug('upload completed with error', error);
debug('completed');
debug('upload completed');
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, error ? error.message : '');
+3 -3
View File
@@ -182,8 +182,8 @@ function clear(callback) {
function addDefaultClients(callback) {
async.series([
add.bind(null, 'cid-webadmin', 'Settings', 'built-in', 'secret-webadmin', 'https://admin-localhost', 'cloudron,profile,users,apps,settings'),
add.bind(null, 'cid-sdk', 'SDK', 'built-in', 'secret-sdk', 'https://admin-localhost', '*,roleSdk'),
add.bind(null, 'cid-cli', 'Cloudron Tool', 'built-in', 'secret-cli', 'https://admin-localhost', '*,roleSdk')
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);
}
+38 -55
View File
@@ -8,26 +8,16 @@ exports = module.exports = {
del: del,
getAll: getAll,
getByAppIdAndType: getByAppIdAndType,
getClientTokensByUserId: getClientTokensByUserId,
delClientTokensByUserId: delClientTokensByUserId,
getTokensByUserId: getTokensByUserId,
delTokensByUserId: delTokensByUserId,
delByAppIdAndType: delByAppIdAndType,
addClientTokenByUserId: addClientTokenByUserId,
addTokenByUserId: addTokenByUserId,
delToken: delToken,
issueDeveloperToken: issueDeveloperToken,
addDefaultClients: addDefaultClients,
// keep this in sync with start.sh ADMIN_SCOPES that generates the cid-webadmin
SCOPE_APPS: 'apps',
SCOPE_DEVELOPER: 'developer', // obsolete
SCOPE_PROFILE: 'profile',
SCOPE_CLOUDRON: 'cloudron',
SCOPE_SETTINGS: 'settings',
SCOPE_USERS: 'users',
// roles are handled just like the above scopes, they are parallel to scopes
// scopes enclose API groups, roles specify the usage role
SCOPE_ROLE_SDK: 'roleSdk',
// client type enums
TYPE_EXTERNAL: 'external',
TYPE_BUILT_IN: 'built-in',
@@ -39,10 +29,14 @@ var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
clientdb = require('./clientdb.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:clients'),
eventlog = require('./eventlog.js'),
hat = require('hat'),
accesscontrol = require('./accesscontrol.js'),
tokendb = require('./tokendb.js'),
users = require('./users.js'),
util = require('util'),
uuid = require('uuid');
@@ -84,28 +78,6 @@ function validateName(name) {
return null;
}
function validateScope(scope) {
assert.strictEqual(typeof scope, 'string');
var VALID_SCOPES = [
exports.SCOPE_APPS,
exports.SCOPE_DEVELOPER,
exports.SCOPE_PROFILE,
exports.SCOPE_CLOUDRON,
exports.SCOPE_SETTINGS,
exports.SCOPE_USERS,
'*', // includes all scopes, but not roles
exports.SCOPE_ROLE_SDK
];
if (scope === '') return new ClientsError(ClientsError.INVALID_SCOPE, 'Empty scope not allowed');
var allValid = scope.split(',').every(function (s) { return VALID_SCOPES.indexOf(s) !== -1; });
if (!allValid) return new ClientsError(ClientsError.INVALID_SCOPE, 'Invalid scope. Available scopes are ' + VALID_SCOPES.join(', '));
return null;
}
function add(appId, type, redirectURI, scope, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof type, 'string');
@@ -113,13 +85,9 @@ function add(appId, type, redirectURI, scope, callback) {
assert.strictEqual(typeof scope, 'string');
assert.strictEqual(typeof callback, 'function');
// allow whitespace
scope = scope.split(',').map(function (s) { return s.trim(); }).join(',');
var error = accesscontrol.validateScope(scope);
if (error) return callback(new ClientsError(ClientsError.INVALID_SCOPE, error.message));
var error = validateScope(scope);
if (error) return callback(error);
// appId is also client name
error = validateName(appId);
if (error) return callback(error);
@@ -191,7 +159,7 @@ function getAll(callback) {
if (record.type === exports.TYPE_PROXY) record.name = result.manifest.title + ' Website Proxy';
if (record.type === exports.TYPE_OAUTH) record.name = result.manifest.title + ' OAuth';
record.domain = result.altDomain || result.intrinsicFqdn;
record.domain = result.fqdn;
tmp.push(record);
@@ -216,7 +184,7 @@ function getByAppIdAndType(appId, type, callback) {
});
}
function getClientTokensByUserId(clientId, userId, callback) {
function getTokensByUserId(clientId, userId, callback) {
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -235,7 +203,7 @@ function getClientTokensByUserId(clientId, userId, callback) {
});
}
function delClientTokensByUserId(clientId, userId, callback) {
function delTokensByUserId(clientId, userId, callback) {
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -275,7 +243,7 @@ function delByAppIdAndType(appId, type, callback) {
});
}
function addClientTokenByUserId(clientId, userId, expiresAt, callback) {
function addTokenByUserId(clientId, userId, expiresAt, callback) {
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof expiresAt, 'number');
@@ -285,21 +253,39 @@ function addClientTokenByUserId(clientId, userId, expiresAt, callback) {
if (error) return callback(error);
var token = tokendb.generateToken();
var scope = accesscontrol.canonicalScope(result.scope);
tokendb.add(token, userId, result.id, expiresAt, result.scope, function (error) {
tokendb.add(token, userId, result.id, expiresAt, scope, function (error) {
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
callback(null, {
accessToken: token,
identifier: userId,
clientId: result.id,
scope: result.id,
scope: result.scope,
expires: expiresAt
});
});
});
}
// this issues a cid-cli token that does not require a password in various routes
function issueDeveloperToken(userObject, ip, callback) {
assert.strictEqual(typeof userObject, 'object');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
const expiresAt = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
addTokenByUserId('cid-cli', userObject.id, expiresAt, function (error, result) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'cli', ip: ip }, { userId: userObject.id, user: users.removePrivateFields(userObject) });
callback(null, result);
});
}
function delToken(clientId, tokenId, callback) {
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof tokenId, 'string');
@@ -324,13 +310,10 @@ function addDefaultClients(origin, callback) {
debug('Adding default clients');
// The domain might have changed, therefor we have to update the record
// !!! This needs to be in sync with the webadmin, specifically login_callback.js
const ADMIN_SCOPES = 'cloudron,developer,profile,users,apps,settings';
// id, appId, type, clientSecret, redirectURI, scope
async.series([
clientdb.upsert.bind(null, 'cid-webadmin', 'Settings', 'built-in', 'secret-webadmin', origin, ADMIN_SCOPES),
clientdb.upsert.bind(null, 'cid-sdk', 'SDK', 'built-in', 'secret-sdk', origin, '*,roleSdk'),
clientdb.upsert.bind(null, 'cid-cli', 'Cloudron Tool', 'built-in', 'secret-cli', origin, '*, roleSdk')
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, '*')
], callback);
}
+3 -22
View File
@@ -40,7 +40,7 @@ var assert = require('assert'),
spawn = require('child_process').spawn,
split = require('split'),
updateChecker = require('./updatechecker.js'),
user = require('./user.js'),
users = require('./users.js'),
util = require('util'),
_ = require('underscore');
@@ -103,7 +103,7 @@ function onActivated(callback) {
// Starting the platform after a user is available means:
// 1. mail bounces can now be sent to the cloudron owner
// 2. the restore code path can run without sudo (since mail/ is non-root)
user.count(function (error, count) {
users.count(function (error, count) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
if (!count) return callback(); // not activated
@@ -234,30 +234,11 @@ function updateToLatest(auditSource, callback) {
if (!boxUpdateInfo) return callback(new CloudronError(CloudronError.ALREADY_UPTODATE, 'No update available'));
if (!boxUpdateInfo.sourceTarballUrl) return callback(new CloudronError(CloudronError.BAD_STATE, 'No automatic update available'));
// check if this is just a version number change
if (config.version().match(/[-+]/) !== null && config.version().replace(/[-+].*/, '') === boxUpdateInfo.version) {
doShortCircuitUpdate(boxUpdateInfo, function (error) {
if (error) debug('Short-circuit update failed', error);
});
return callback(null);
}
if (boxUpdateInfo.upgrade && config.provider() !== 'caas') return callback(new CloudronError(CloudronError.SELF_UPGRADE_NOT_SUPPORTED));
update(boxUpdateInfo, auditSource, callback);
}
function doShortCircuitUpdate(boxUpdateInfo, callback) {
assert(boxUpdateInfo !== null && typeof boxUpdateInfo === 'object');
debug('Starting short-circuit from prerelease version %s to release version %s', config.version(), boxUpdateInfo.version);
config.setVersion(boxUpdateInfo.version);
progress.clear(progress.UPDATE);
updateChecker.resetUpdateInfo();
callback();
}
function doUpdate(boxUpdateInfo, callback) {
assert(boxUpdateInfo && typeof boxUpdateInfo === 'object');
@@ -294,7 +275,7 @@ function doUpdate(boxUpdateInfo, callback) {
debug('updating box %s %j', boxUpdateInfo.sourceTarballUrl, _.omit(data, 'tlsCert', 'tlsKey', 'token', 'appstore', 'caas'));
progress.set(progress.UPDATE, 5, 'Downloading and extracting new version');
progress.set(progress.UPDATE, 5, 'Downloading and installing new version');
shell.sudo('update', [ UPDATE_CMD, boxUpdateInfo.sourceTarballUrl, JSON.stringify(data) ], function (error) {
if (error) return updateError(error);
+39 -17
View File
@@ -22,12 +22,12 @@ var apps = require('./apps.js'),
reverseProxy = require('./reverseproxy.js'),
scheduler = require('./scheduler.js'),
settings = require('./settings.js'),
semver = require('semver'),
updateChecker = require('./updatechecker.js');
var gJobs = {
alive: null, // send periodic stats
autoUpdater: null,
appAutoUpdater: null,
boxAutoUpdater: null,
appUpdateChecker: null,
backup: null,
boxUpdateChecker: null,
@@ -78,14 +78,16 @@ function initialize(callback) {
});
settings.events.on(settings.TIME_ZONE_KEY, recreateJobs);
settings.events.on(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
settings.events.on(settings.APP_AUTOUPDATE_PATTERN_KEY, appAutoupdatePatternChanged);
settings.events.on(settings.BOX_AUTOUPDATE_PATTERN_KEY, boxAutoupdatePatternChanged);
settings.events.on(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
settings.getAll(function (error, allSettings) {
if (error) return callback(error);
recreateJobs(allSettings[settings.TIME_ZONE_KEY]);
autoupdatePatternChanged(allSettings[settings.AUTOUPDATE_PATTERN_KEY]);
appAutoupdatePatternChanged(allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY]);
boxAutoupdatePatternChanged(allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY]);
dynamicDnsChanged(allSettings[settings.DYNAMIC_DNS_KEY]);
callback();
@@ -189,32 +191,51 @@ function recreateJobs(tz) {
});
}
function autoupdatePatternChanged(pattern) {
function boxAutoupdatePatternChanged(pattern) {
assert.strictEqual(typeof pattern, 'string');
assert(gJobs.boxUpdateCheckerJob);
debug('Auto update pattern changed to %s', pattern);
debug('Box auto update pattern changed to %s', pattern);
if (gJobs.autoUpdater) gJobs.autoUpdater.stop();
if (gJobs.boxAutoUpdater) gJobs.boxAutoUpdater.stop();
if (pattern === constants.AUTOUPDATE_PATTERN_NEVER) return;
gJobs.autoUpdater = new CronJob({
gJobs.boxAutoUpdater = new CronJob({
cronTime: pattern,
onTick: function() {
var updateInfo = updateChecker.getUpdateInfo();
if (updateInfo.box) {
if (semver.major(updateInfo.box.version) === semver.major(config.version())) {
debug('Starting autoupdate to %j', updateInfo.box);
cloudron.updateToLatest(AUDIT_SOURCE, NOOP_CALLBACK);
} else {
debug('Block automatic update for major version');
}
} else if (updateInfo.apps) {
debug('Starting autoupdate to %j', updateInfo.box);
cloudron.updateToLatest(AUDIT_SOURCE, NOOP_CALLBACK);
} else {
debug('No box auto updates available');
}
},
start: true,
timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack
});
}
function appAutoupdatePatternChanged(pattern) {
assert.strictEqual(typeof pattern, 'string');
assert(gJobs.boxUpdateCheckerJob);
debug('Apps auto update pattern changed to %s', pattern);
if (gJobs.appAutoUpdater) gJobs.appAutoUpdater.stop();
if (pattern === constants.AUTOUPDATE_PATTERN_NEVER) return;
gJobs.appAutoUpdater = new CronJob({
cronTime: pattern,
onTick: function() {
var updateInfo = updateChecker.getUpdateInfo();
if (updateInfo.apps) {
debug('Starting app update to %j', updateInfo.apps);
apps.autoupdateApps(updateInfo.apps, AUDIT_SOURCE, NOOP_CALLBACK);
} else {
debug('No auto updates available');
debug('No app auto updates available');
}
},
start: true,
@@ -245,7 +266,8 @@ function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
settings.events.removeListener(settings.TIME_ZONE_KEY, recreateJobs);
settings.events.removeListener(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
settings.events.removeListener(settings.APP_AUTOUPDATE_PATTERN_KEY, appAutoupdatePatternChanged);
settings.events.removeListener(settings.BOX_AUTOUPDATE_PATTERN_KEY, boxAutoupdatePatternChanged);
settings.events.removeListener(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
for (var job in gJobs) {
+2 -14
View File
@@ -6,10 +6,6 @@ exports = module.exports = {
query: query,
transaction: transaction,
beginTransaction: beginTransaction,
rollback: rollback,
commit: commit,
importFromFile: importFromFile,
exportToFile: exportToFile,
@@ -27,21 +23,13 @@ var assert = require('assert'),
var gConnectionPool = null,
gDefaultConnection = null;
function initialize(options, callback) {
if (typeof options === 'function') {
callback = options;
options = {
connectionLimit: 5
};
}
assert.strictEqual(typeof options.connectionLimit, 'number');
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
if (gConnectionPool !== null) return callback(null);
gConnectionPool = mysql.createPool({
connectionLimit: options.connectionLimit,
connectionLimit: 5, // this has to be > 1 since we store one connection as 'default'. the rest for transactions
host: config.database().hostname,
user: config.database().username,
password: config.database().password,
-56
View File
@@ -1,56 +0,0 @@
/* jslint node: true */
'use strict';
exports = module.exports = {
DeveloperError: DeveloperError,
issueDeveloperToken: issueDeveloperToken
};
var assert = require('assert'),
clients = require('./clients.js'),
constants = require('./constants.js'),
eventlog = require('./eventlog.js'),
tokendb = require('./tokendb.js'),
util = require('util');
function DeveloperError(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(DeveloperError, Error);
DeveloperError.INTERNAL_ERROR = 'Internal Error';
DeveloperError.EXTERNAL_ERROR = 'External Error';
function issueDeveloperToken(user, auditSource, callback) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var token = tokendb.generateToken();
var expiresAt = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
var scopes = '*,' + clients.SCOPE_ROLE_SDK;
tokendb.add(token, user.id, 'cid-cli', expiresAt, scopes, function (error) {
if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { authType: 'cli', userId: user.id, username: user.username });
callback(null, { token: token, expiresAt: new Date(expiresAt).toISOString() });
});
}
-46
View File
@@ -1,46 +0,0 @@
'use strict';
exports = module.exports = {
resolve: resolve
};
var assert = require('assert'),
child_process = require('child_process'),
debug = require('debug')('box:dig');
function resolve(domain, type, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
// dig @server cloudron.io TXT +short
var args = [ ];
if (options.server) args.push('@' + options.server);
if (type === 'PTR') {
args.push('-x', domain);
} else {
args.push(domain, type);
}
args.push('+short');
child_process.execFile('/usr/bin/dig', args, { encoding: 'utf8', killSignal: 'SIGKILL', timeout: options.timeout || 0 }, function (error, stdout, stderr) {
if (error && error.killed) error.code = 'ETIMEDOUT';
if (error || stderr) debug('resolve error (%j): %j %s %s', args, error, stdout, stderr);
if (error) return callback(error);
debug('resolve (%j): %s', args, stdout);
if (!stdout) return callback(); // timeout or no result
var lines = stdout.trim().split('\n');
if (type === 'MX') {
lines = lines.map(function (line) {
var parts = line.split(' ');
return { priority: parts[0], exchange: parts[1] };
});
}
return callback(null, lines);
});
}
+2 -4
View File
@@ -28,11 +28,9 @@ function maybeSend(callback) {
var pendingAppUpdates = updateInfo.apps || {};
pendingAppUpdates = Object.keys(pendingAppUpdates).map(function (key) { return pendingAppUpdates[key]; });
appstore.getSubscription(function (error, result) {
appstore.getSubscription(function (error, subscription) {
if (error) debug('Error getting subscription:', error);
var hasSubscription = result && result.plan.id !== 'free' && result.plan.id !== 'undecided';
eventlog.getByCreationTime(new Date(new Date() - 7*86400000), function (error, events) {
if (error) return callback(error);
@@ -46,7 +44,7 @@ function maybeSend(callback) {
if (error) return callback(error);
var info = {
hasSubscription: hasSubscription,
hasSubscription: appstore.isFreePlan(subscription),
pendingAppUpdates: pendingAppUpdates,
pendingBoxUpdate: updateInfo.box || null,
+12 -12
View File
@@ -11,7 +11,7 @@ exports = module.exports = {
var assert = require('assert'),
config = require('../config.js'),
debug = require('debug')('box:dns/caas'),
DomainError = require('../domains.js').DomainError,
DomainsError = require('../domains.js').DomainsError,
superagent = require('superagent'),
util = require('util');
@@ -45,10 +45,10 @@ function add(dnsConfig, zoneName, subdomain, type, values, callback) {
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 400) return callback(new DomainError(DomainError.BAD_FIELD, result.body.message));
if (result.statusCode === 420) return callback(new DomainError(DomainError.STILL_BUSY));
if (result.statusCode !== 201) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
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)));
return callback(null, result.body.changeId);
});
@@ -70,8 +70,8 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
.query({ token: dnsConfig.token, type: type })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode !== 200) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
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)));
return callback(null, result.body.values);
});
@@ -109,11 +109,11 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 400) return callback(new DomainError(DomainError.BAD_FIELD, result.body.message));
if (result.statusCode === 420) return callback(new DomainError(DomainError.STILL_BUSY));
if (result.statusCode === 404) return callback(new DomainError(DomainError.NOT_FOUND));
if (result.statusCode !== 204) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
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)));
return callback(null);
});
+17 -17
View File
@@ -11,8 +11,8 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
debug = require('debug')('box:dns/cloudflare'),
dns = require('dns'),
DomainError = require('../domains.js').DomainError,
dns = require('../native-dns.js'),
DomainsError = require('../domains.js').DomainsError,
superagent = require('superagent'),
util = require('util'),
_ = require('underscore');
@@ -24,8 +24,8 @@ function translateRequestError(result, callback) {
assert.strictEqual(typeof result, 'object');
assert.strictEqual(typeof callback, 'function');
if (result.statusCode === 404) return callback(new DomainError(DomainError.NOT_FOUND, util.format('%s %j', result.statusCode, 'API does not exist')));
if (result.statusCode === 422) return callback(new DomainError(DomainError.BAD_FIELD, result.body.message));
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 === 400 || result.statusCode === 401 || result.statusCode === 403) && result.body.errors.length > 0) {
let error = result.body.errors[0];
let message = error.message;
@@ -34,10 +34,10 @@ function translateRequestError(result, callback) {
else message = 'Invalid credentials';
}
return callback(new DomainError(DomainError.ACCESS_DENIED, message));
return callback(new DomainsError(DomainsError.ACCESS_DENIED, message));
}
callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
}
function getZoneByName(dnsConfig, zoneName, callback) {
@@ -52,13 +52,13 @@ 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 DomainError(DomainError.NOT_FOUND, util.format('%s %j', result.statusCode, result.body)));
if (!result.body.result.length) return callback(new DomainsError(DomainsError.NOT_FOUND, util.format('%s %j', result.statusCode, result.body)));
callback(null, result.body.result[0]);
});
}
function getDNSRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, callback) {
function getDnsRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneId, 'string');
assert.strictEqual(typeof zoneName, 'string');
@@ -100,7 +100,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
var zoneId = result.id;
getDNSRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, function (error, result) {
getDnsRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
var dnsRecords = result;
@@ -171,7 +171,7 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
getZoneByName(dnsConfig, zoneName, function(error, result){
if (error) return callback(error);
getDNSRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
getDnsRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
if (error) return callback(error);
var tmp = result.map(function (record) { return record.content; });
@@ -193,7 +193,7 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
getZoneByName(dnsConfig, zoneName, function(error, result){
if (error) return callback(error);
getDNSRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
getDnsRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
if (error) return callback(error);
if (result.length === 0) return callback(null);
@@ -233,8 +233,8 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainError(DomainError.BAD_FIELD, 'token must be a non-empty string'));
if (!dnsConfig.email || typeof dnsConfig.email !== 'string') return callback(new DomainError(DomainError.BAD_FIELD, 'email 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.email || typeof dnsConfig.email !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'email must be a non-empty string'));
var credentials = {
token: dnsConfig.token,
@@ -243,16 +243,16 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolveNs(zoneName, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainError(DomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
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'));
getZoneByName(dnsConfig, zoneName, function(error, result) {
if (error) return callback(error);
if (!_.isEqual(result.name_servers.sort(), nameservers.sort())) {
debug('verifyDnsConfig: %j and %j do not match', nameservers, result.name_servers);
return callback(new DomainError(DomainError.BAD_FIELD, 'Domain nameservers are not set to Cloudflare'));
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Cloudflare'));
}
const testSubdomain = 'cloudrontestdns';
+21 -21
View File
@@ -11,8 +11,8 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
debug = require('debug')('box:dns/digitalocean'),
dns = require('dns'),
DomainError = require('../domains.js').DomainError,
dns = require('../native-dns.js'),
DomainsError = require('../domains.js').DomainsError,
safe = require('safetydance'),
superagent = require('superagent'),
util = require('util');
@@ -39,10 +39,10 @@ function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
.set('Authorization', 'Bearer ' + dnsConfig.token)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 404) return callback(new DomainError(DomainError.NOT_FOUND, formatError(result)));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainError(DomainError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return callback(new DomainError(DomainError.EXTERNAL_ERROR, formatError(result)));
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 404) return callback(new DomainsError(DomainsError.NOT_FOUND, formatError(result)));
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)));
matchingRecords = matchingRecords.concat(result.body.domain_records.filter(function (record) {
return (record.type === type && record.name === subdomain);
@@ -101,10 +101,10 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return iteratorCallback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new DomainError(DomainError.ACCESS_DENIED, formatError(result)));
if (result.statusCode === 422) return iteratorCallback(new DomainError(DomainError.BAD_FIELD, result.body.message));
if (result.statusCode !== 201) return iteratorCallback(new DomainError(DomainError.EXTERNAL_ERROR, formatError(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)));
recordIds.push(safe.query(result.body, 'domain_record.id'));
@@ -119,10 +119,10 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
// increment, as we have consumed the record
++i;
if (error && !error.response) return iteratorCallback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new DomainError(DomainError.ACCESS_DENIED, formatError(result)));
if (result.statusCode === 422) return iteratorCallback(new DomainError(DomainError.BAD_FIELD, result.body.message));
if (result.statusCode !== 200) return iteratorCallback(new DomainError(DomainError.EXTERNAL_ERROR, formatError(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 !== 200) return iteratorCallback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
recordIds.push(safe.query(result.body, 'domain_record.id'));
@@ -185,10 +185,10 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
.set('Authorization', 'Bearer ' + dnsConfig.token)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 404) return callback(null);
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainError(DomainError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 204) return callback(new DomainError(DomainError.EXTERNAL_ERROR, formatError(result)));
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)));
debug('del: done');
@@ -210,13 +210,13 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolveNs(zoneName, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainError(DomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
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 (nameservers.map(function (n) { return n.toLowerCase(); }).indexOf('ns1.digitalocean.com') === -1) {
debug('verifyDnsConfig: %j does not contains DO NS', nameservers);
return callback(new DomainError(DomainError.BAD_FIELD, 'Domain nameservers are not set to Digital Ocean'));
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Digital Ocean'));
}
const testSubdomain = 'cloudrontestdns';
+146
View File
@@ -0,0 +1,146 @@
'use strict';
exports = module.exports = {
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
debug = require('debug')('box:dns/gandi'),
dns = require('../native-dns.js'),
DomainsError = require('../domains.js').DomainsError,
superagent = require('superagent'),
util = require('util');
var GANDI_API = 'https://dns.api.gandi.net/api/v5';
function formatError(response) {
return util.format(`Gandi DNS error [${response.statusCode}] ${response.body.message}`);
}
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
debug(`upsert: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
var data = {
'rrset_ttl': 300, // this is the minimum allowed
'rrset_values': values // for mx records, value is already of the '<priority> <server>' format
};
superagent.put(`${GANDI_API}/domains/${zoneName}/records/${subdomain}/${type}`)
.set('X-Api-Key', dnsConfig.token)
.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)));
return callback(null, 'unused-id');
});
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
debug(`get: ${subdomain} in zone ${zoneName} of type ${type}`);
superagent.get(`${GANDI_API}/domains/${zoneName}/records/${subdomain}/${type}`)
.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 (result.statusCode === 404) return callback(null, [ ]);
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
debug('get: %j', result.body);
return callback(null, result.body.rrset_values);
});
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
debug(`del: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
superagent.del(`${GANDI_API}/domains/${zoneName}/records/${subdomain}/${type}`)
.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 === 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)));
debug('del: done');
return callback(null);
});
}
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
var credentials = {
token: dnsConfig.token
};
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new 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 (!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'));
}
const testSubdomain = 'cloudrontestdns';
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
callback(null, credentials);
});
});
});
}
+26 -26
View File
@@ -10,8 +10,8 @@ exports = module.exports = {
var assert = require('assert'),
debug = require('debug')('box:dns/gcdns'),
dns = require('dns'),
DomainError = require('../domains.js').DomainError,
dns = require('../native-dns.js'),
DomainsError = require('../domains.js').DomainsError,
GCDNS = require('@google-cloud/dns'),
util = require('util'),
_ = require('underscore');
@@ -42,20 +42,20 @@ function getZoneByName(dnsConfig, zoneName, callback) {
var gcdns = GCDNS(getDnsCredentials(dnsConfig));
gcdns.getZones(function (error, zones) {
if (error && error.message === 'invalid_grant') return callback(new DomainError(DomainError.ACCESS_DENIED, 'The key was probably revoked'));
if (error && error.reason === 'No such domain') return callback(new DomainError(DomainError.NOT_FOUND, error.message));
if (error && error.code === 403) return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
if (error && error.code === 404) return callback(new DomainError(DomainError.NOT_FOUND, error.message));
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) {
debug('gcdns.getZones', error);
return callback(new DomainError(DomainError.EXTERNAL_ERROR, error));
return callback(new DomainsError(DomainsError.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 DomainError(DomainError.NOT_FOUND, 'no such zone'));
if (!zone) return callback(new DomainsError(DomainsError.NOT_FOUND, 'no such zone'));
callback(null, zone); //zone.metadata ~= {name="", dnsName="", nameServers:[]}
});
@@ -77,10 +77,10 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
var domain = (subdomain ? subdomain + '.' : '') + zoneName + '.';
zone.getRecords({ type: type, name: domain }, function (error, oldRecords) {
if (error && error.code === 403) return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
if (error && error.code === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error) {
debug('upsert->zone.getRecords', error);
return callback(new DomainError(DomainError.EXTERNAL_ERROR, error.message));
return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
}
var newRecord = zone.record(type, {
@@ -90,11 +90,11 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
});
zone.createChange({ delete: oldRecords, add: newRecord }, function(error, change) {
if (error && error.code === 403) return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
if (error && error.code === 412) return callback(new DomainError(DomainError.STILL_BUSY, error.message));
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) {
debug('upsert->zone.createChange', error);
return callback(new DomainError(DomainError.EXTERNAL_ERROR, error.message));
return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
}
callback(null, change.id);
@@ -119,8 +119,8 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
};
zone.getRecords(params, function (error, records) {
if (error && error.code === 403) return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
if (error) return callback(new DomainError(DomainError.EXTERNAL_ERROR, error));
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 (records.length === 0) return callback(null, [ ]);
return callback(null, records[0].data);
@@ -142,18 +142,18 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
var domain = (subdomain ? subdomain + '.' : '') + zoneName + '.';
zone.getRecords({ type: type, name: domain }, function(error, oldRecords) {
if (error && error.code === 403) return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
if (error && error.code === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error) {
debug('del->zone.getRecords', error);
return callback(new DomainError(DomainError.EXTERNAL_ERROR, error.message));
return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
}
zone.deleteRecords(oldRecords, function (error, change) {
if (error && error.code === 403) return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
if (error && error.code === 412) return callback(new DomainError(DomainError.STILL_BUSY, error.message));
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) {
debug('del->zone.createChange', error);
return callback(new DomainError(DomainError.EXTERNAL_ERROR, error.message));
return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
}
callback(null, change.id);
@@ -172,17 +172,17 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
var credentials = getDnsCredentials(dnsConfig);
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolveNs(zoneName, function (error, resolvedNS) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !resolvedNS) return callback(new DomainError(DomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
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'));
getZoneByName(credentials, zoneName, function (error, zone) {
if (error) return callback(error);
var definedNS = zone.metadata.nameServers.sort().map(function(r) { return r.replace(/\.$/, ''); });
if (!_.isEqual(definedNS, resolvedNS.sort())) {
debug('verifyDnsConfig: %j and %j do not match', resolvedNS, definedNS);
return callback(new DomainError(DomainError.BAD_FIELD, 'Domain nameservers are not set to Google Cloud DNS'));
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'));
}
const testSubdomain = 'cloudrontestdns';
+181
View File
@@ -0,0 +1,181 @@
'use strict';
exports = module.exports = {
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
debug = require('debug')('box:dns/godaddy'),
dns = require('../native-dns.js'),
DomainsError = require('../domains.js').DomainsError,
superagent = require('superagent'),
util = require('util');
// const GODADDY_API_OTE = 'https://api.ote-godaddy.com/v1/domains';
const GODADDY_API = 'https://api.godaddy.com/v1/domains';
// this is a workaround for godaddy not having a delete API
// https://stackoverflow.com/questions/39347464/delete-record-libcloud-godaddy-api
const GODADDY_INVALID_IP = '0.0.0.0';
function formatError(response) {
return util.format(`GoDaddy DNS error [${response.statusCode}] ${response.body.message}`);
}
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
debug(`upsert: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
var records = [ ];
values.forEach(function (value) {
var record = { ttl: 600 }; // 600 is the min ttl
if (type === 'MX') {
record.priority = parseInt(value.split(' ')[0], 10);
record.data = value.split(' ')[1];
} else {
record.data = value;
}
records.push(record);
});
superagent.put(`${GODADDY_API}/${zoneName}/records/${type}/${subdomain}`)
.set('Authorization', `sso-key ${dnsConfig.apiKey}:${dnsConfig.apiSecret}`)
.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)));
return callback(null, 'unused-id');
});
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
debug(`get: ${subdomain} in zone ${zoneName} of type ${type}`);
superagent.get(`${GODADDY_API}/${zoneName}/records/${type}/${subdomain}`)
.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 (result.statusCode === 404) return callback(null, [ ]);
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
debug('get: %j', result.body);
var values = result.body.map(function (record) { return record.data; });
if (values.length === 1 && values[0] === GODADDY_INVALID_IP) return callback(null, [ ]); // pretend this record doesn't exist
return callback(null, values);
});
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
debug(`get: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
if (type !== 'A') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, new Error('Not supported by GoDaddy API'))); // can never happen
// check if the record exists at all so that we don't insert the "Dead" record for no reason
get(dnsConfig, zoneName, subdomain, type, function (error, values) {
if (error) return callback(error);
if (values.length === 0) return callback();
// godaddy does not have a delete API. so fill it up with an invalid IP that we can ignore in future get()
var records = [{
ttl: 600,
data: GODADDY_INVALID_IP
}];
superagent.put(`${GODADDY_API}/${zoneName}/records/${type}/${subdomain}`)
.set('Authorization', `sso-key ${dnsConfig.apiKey}:${dnsConfig.apiSecret}`)
.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 (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)));
debug('del: done');
return callback(null);
});
});
}
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
var credentials = {
apiKey: dnsConfig.apiKey,
apiSecret: dnsConfig.apiSecret
};
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 (!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'));
}
const testSubdomain = 'cloudrontestdns';
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
callback(null, credentials);
});
});
});
}
+1 -1
View File
@@ -15,7 +15,7 @@ exports = module.exports = {
};
var assert = require('assert'),
DomainError = require('../domains.js').DomainError,
DomainsError = require('../domains.js').DomainsError,
util = require('util');
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
+5 -4
View File
@@ -10,8 +10,8 @@ exports = module.exports = {
var assert = require('assert'),
debug = require('debug')('box:dns/manual'),
dns = require('dns'),
DomainError = require('../domains.js').DomainError,
dns = require('../native-dns.js'),
DomainsError = require('../domains.js').DomainsError,
util = require('util');
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
@@ -56,8 +56,9 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
assert.strictEqual(typeof callback, 'function');
// Very basic check if the nameservers can be fetched
dns.resolveNs(zoneName, function (error, nameservers) {
if (error || !nameservers) return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to get nameservers'));
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'));
callback(null, { wildcard: !!dnsConfig.wildcard });
});
+243
View File
@@ -0,0 +1,243 @@
'use strict';
exports = module.exports = {
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
debug = require('debug')('box:dns/namecom'),
dns = require('../native-dns.js'),
safe = require('safetydance'),
DomainsError = require('../domains.js').DomainsError,
superagent = require('superagent');
const NAMECOM_API = 'https://api.name.com/v4';
function formatError(response) {
return `Name.com DNS error [${response.statusCode}] ${response.text}`;
}
function addRecord(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug(`add: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
var data = {
host: subdomain,
type: type,
ttl: 300 // 300 is the lowest
};
if (type === 'MX') {
data.priority = parseInt(values[0].split(' ')[0], 10);
data.answer = values[0].split(' ')[1];
} else {
data.answer = values[0];
}
superagent.post(`${NAMECOM_API}/domains/${zoneName}/records`)
.auth(dnsConfig.username, dnsConfig.token)
.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)));
return callback(null, 'unused-id');
});
}
function updateRecord(dnsConfig, zoneName, recordId, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof recordId, 'number');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug(`update:${recordId} on ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
var data = {
host: subdomain,
type: type,
ttl: 300 // 300 is the lowest
};
if (type === 'MX') {
data.priority = parseInt(values[0].split(' ')[0], 10);
data.answer = values[0].split(' ')[1];
} else {
data.answer = values[0];
}
superagent.put(`${NAMECOM_API}/domains/${zoneName}/records/${recordId}`)
.auth(dnsConfig.username, dnsConfig.token)
.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)));
return callback(null, 'unused-id');
});
}
function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
debug(`getInternal: ${subdomain} in zone ${zoneName} of type ${type}`);
superagent.get(`${NAMECOM_API}/domains/${zoneName}/records`)
.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)));
// name.com does not return the correct content-type
result.body = safe.JSON.parse(result.text);
if (!result.body.records) result.body.records = [];
result.body.records.forEach(function (r) {
// name.com api simply strips empty properties
r.host = r.host || '@';
});
var results = result.body.records.filter(function (r) {
return (r.host === subdomain && r.type === type);
});
debug('getInternal: %j', results);
return callback(null, results);
});
}
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
debug(`upsert: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
if (result.length === 0) return addRecord(dnsConfig, zoneName, subdomain, type, values, callback);
return updateRecord(dnsConfig, zoneName, result[0].id, subdomain, type, values, callback);
});
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
var tmp = result.map(function (record) { return record.answer; });
debug('get: %j', tmp);
return callback(null, tmp);
});
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
debug(`del: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
if (result.length === 0) return callback();
superagent.del(`${NAMECOM_API}/domains/${zoneName}/records/${result[0].id}`)
.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)));
return callback(null);
});
});
}
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
var credentials = {
username: dnsConfig.username,
token: dnsConfig.token
};
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new 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 (!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'));
}
const testSubdomain = 'cloudrontestdns';
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
callback(null, credentials);
});
});
});
}
+2 -3
View File
@@ -46,11 +46,10 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
return callback();
}
function waitForDns(domain, zoneName, value, type, options, callback) {
function waitForDns(domain, zoneName, value, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert(typeof value === 'string' || util.isRegExp(value));
assert(type === 'A' || type === 'CNAME' || type === 'TXT');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
+43 -34
View File
@@ -13,10 +13,9 @@ exports = module.exports = {
var assert = require('assert'),
AWS = require('aws-sdk'),
config = require('../config.js'),
debug = require('debug')('box:dns/route53'),
dns = require('dns'),
DomainError = require('../domains.js').DomainError,
dns = require('../native-dns.js'),
DomainsError = require('../domains.js').DomainsError,
util = require('util'),
_ = require('underscore');
@@ -40,16 +39,25 @@ function getZoneByName(dnsConfig, zoneName, callback) {
assert.strictEqual(typeof callback, 'function');
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
route53.listHostedZones({}, function (error, result) {
if (error && error.code === 'AccessDenied') return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
if (error && error.code === 'InvalidClientTokenId') return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
if (error) return callback(new DomainError(DomainError.EXTERNAL_ERROR, error.message));
// backward compat for 2.2, where we only required access to "listHostedZones"
let listHostedZones;
if (dnsConfig.listHostedZonesByName) {
listHostedZones = route53.listHostedZonesByName.bind(route53, { MaxItems: '1', DNSName: zoneName + '.' });
} else {
listHostedZones = route53.listHostedZones.bind(route53, {}); // currently, this route does not support > 100 zones
}
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));
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 DomainError(DomainError.NOT_FOUND, 'no such zone'));
if (!zone) return callback(new DomainsError(DomainsError.NOT_FOUND, 'no such zone'));
callback(null, zone);
});
@@ -65,9 +73,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 DomainError(DomainError.ACCESS_DENIED, error.message));
if (error && error.code === 'InvalidClientTokenId') return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
if (error) return callback(new DomainError(DomainError.EXTERNAL_ERROR, error.message));
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));
callback(null, result);
});
@@ -88,7 +96,7 @@ function add(dnsConfig, zoneName, subdomain, type, values, callback) {
if (error) return callback(error);
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName;
var records = values.map(function (v) { return { Value: v }; });
var records = values.map(function (v) { return { Value: v }; }); // for mx records, value is already of the '<priority> <server>' format
var params = {
ChangeBatch: {
@@ -107,11 +115,11 @@ function add(dnsConfig, zoneName, subdomain, type, values, callback) {
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
route53.changeResourceRecordSets(params, function(error, result) {
if (error && error.code === 'AccessDenied') return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
if (error && error.code === 'InvalidClientTokenId') return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
if (error && error.code === 'PriorRequestNotComplete') return callback(new DomainError(DomainError.STILL_BUSY, error.message));
if (error && error.code === 'InvalidChangeBatch') return callback(new DomainError(DomainError.BAD_FIELD, error.message));
if (error) return callback(new DomainError(DomainError.EXTERNAL_ERROR, error.message));
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));
callback(null, result.ChangeInfo.Id);
});
@@ -148,9 +156,9 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
route53.listResourceRecordSets(params, function (error, result) {
if (error && error.code === 'AccessDenied') return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
if (error && error.code === 'InvalidClientTokenId') return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
if (error) return callback(new DomainError(DomainError.EXTERNAL_ERROR, error.message));
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 (result.ResourceRecordSets.length === 0) return callback(null, [ ]);
if (result.ResourceRecordSets[0].Name !== params.StartRecordName || result.ResourceRecordSets[0].Type !== params.StartRecordType) return callback(null, [ ]);
@@ -193,24 +201,24 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
};
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
route53.changeResourceRecordSets(params, function(error, result) {
if (error && error.code === 'AccessDenied') return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
if (error && error.code === 'InvalidClientTokenId') return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
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.message && error.message.indexOf('it was not found') !== -1) {
debug('del: resource record set not found.', error);
return callback(new DomainError(DomainError.NOT_FOUND, error.message));
return callback(new DomainsError(DomainsError.NOT_FOUND, error.message));
} else if (error && error.code === 'NoSuchHostedZone') {
debug('del: hosted zone not found.', error);
return callback(new DomainError(DomainError.NOT_FOUND, error.message));
return callback(new DomainsError(DomainsError.NOT_FOUND, error.message));
} else if (error && error.code === 'PriorRequestNotComplete') {
debug('del: resource is still busy', error);
return callback(new DomainError(DomainError.STILL_BUSY, error.message));
return callback(new DomainsError(DomainsError.STILL_BUSY, error.message));
} else if (error && error.code === 'InvalidChangeBatch') {
debug('del: invalid change batch. No such record to be deleted.');
return callback(new DomainError(DomainError.NOT_FOUND, error.message));
return callback(new DomainsError(DomainsError.NOT_FOUND, error.message));
} else if (error) {
debug('del: error', error);
return callback(new DomainError(DomainError.EXTERNAL_ERROR, error.message));
return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
}
callback(null);
@@ -229,21 +237,22 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
accessKeyId: dnsConfig.accessKeyId,
secretAccessKey: dnsConfig.secretAccessKey,
region: dnsConfig.region || 'us-east-1',
endpoint: dnsConfig.endpoint || null
endpoint: dnsConfig.endpoint || null,
listHostedZonesByName: true // new/updated creds require this perm
};
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolveNs(zoneName, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainError(DomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
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'));
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 DomainError(DomainError.BAD_FIELD, 'Domain nameservers are not set to Route53'));
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Route53'));
}
const testSubdomain = 'cloudrontestdns';
@@ -253,7 +262,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
del(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
+47 -46
View File
@@ -5,53 +5,59 @@ exports = module.exports = waitForDns;
var assert = require('assert'),
async = require('async'),
debug = require('debug')('box:dns/waitfordns'),
dig = require('../dig.js'),
dns = require('dns'),
DomainError = require('../domains.js').DomainError,
util = require('util');
dns = require('../native-dns.js'),
DomainsError = require('../domains.js').DomainsError;
function isChangeSynced(domain, value, type, nameserver, callback) {
function resolveIp(hostname, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
// try A record at authoritative server
debug(`resolveIp: Checking if ${hostname} has A record at ${options.server}`);
dns.resolve(hostname, 'A', options, function (error, results) {
if (!error && results.length !== 0) return callback(null, results);
// try CNAME record at authoritative server
debug(`resolveIp: Checking if ${hostname} has CNAME record at ${options.server}`);
dns.resolve(hostname, 'CNAME', options, function (error, results) {
if (error || results.length === 0) return callback(error, results);
// recurse lookup the CNAME record
debug(`resolveIp: Resolving ${hostname}'s CNAME record ${results[0]}`);
dns.resolve(results[0], 'A', { server: '127.0.0.1', timeout: options.timeout }, callback);
});
});
}
function isChangeSynced(domain, value, nameserver, callback) {
assert.strictEqual(typeof domain, 'string');
assert(util.isRegExp(value));
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert.strictEqual(typeof nameserver, 'string');
assert.strictEqual(typeof callback, 'function');
// ns records cannot have cname
dns.resolve4(nameserver, function (error, nsIps) {
dns.resolve(nameserver, 'A', { timeout: 5000 }, function (error, nsIps) {
if (error || !nsIps || nsIps.length === 0) {
debug('nameserver %s does not resolve. assuming it stays bad.', nameserver); // it's fine if one or more ns are dead
return callback(true);
debug(`isChangeSynced: cannot resolve NS ${nameserver}`); // it's fine if one or more ns are dead
return callback(null, true);
}
async.every(nsIps, function (nsIp, iteratorCallback) {
dig.resolve(domain, type, { server: nsIp, timeout: 5000 }, function (error, answer) {
if (error && error.code === 'ETIMEDOUT') {
debug('nameserver %s (%s) timed out when trying to resolve %s', nameserver, nsIp, domain);
resolveIp(domain, { server: nsIp, timeout: 5000 }, function (error, answer) {
if (error && error.code === 'TIMEOUT') {
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) timed out when resolving ${domain}`);
return iteratorCallback(null, true); // should be ok if dns server is down
}
if (error) {
debug('nameserver %s (%s) returned error trying to resolve %s: %s', nameserver, nsIp, domain, error);
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) errored when resolve ${domain}: ${error}`);
return iteratorCallback(null, false);
}
if (!answer || answer.length === 0) {
debug('bad answer from nameserver %s (%s) resolving %s (%s)', nameserver, nsIp, domain, type);
return iteratorCallback(null, false);
}
debug(`isChangeSynced: ${domain} was resolved to ${answer} at NS ${nameserver} (${nsIp}). Expecting ${value}`);
debug('isChangeSynced: ns: %s (%s), name:%s Actual:%j Expecting:%s', nameserver, nsIp, domain, answer, value);
var match = answer.some(function (a) {
return ((type === 'A' && value.test(a)) ||
(type === 'CNAME' && value.test(a)) ||
(type === 'TXT' && value.test(a)));
});
if (match) return iteratorCallback(null, true); // done!
iteratorCallback(null, false);
iteratorCallback(null, answer.length === 1 && answer[0] === value);
});
}, callback);
@@ -59,38 +65,33 @@ function isChangeSynced(domain, value, type, nameserver, callback) {
}
// check if IP change has propagated to every nameserver
function waitForDns(domain, zoneName, value, type, options, callback) {
function waitForDns(domain, zoneName, value, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert(typeof value === 'string' || util.isRegExp(value));
assert(type === 'A' || type === 'CNAME' || type === 'TXT');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
if (typeof value === 'string') {
// http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
value = new RegExp('^' + value.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '$');
}
debug('waitForDns: domain %s to be %s in zone %s.', domain, value, zoneName);
debug('waitForIp: domain %s to be %s in zone %s.', domain, value, zoneName);
var attempt = 1;
var attempt = 0;
async.retry(options, function (retryCallback) {
debug('waitForDNS: %s (zone: %s) attempt %s.', domain, zoneName, attempt++);
++attempt;
debug(`waitForDns (try ${attempt}): ${domain} to be ${value} in zone ${zoneName}`);
dns.resolveNs(zoneName, function (error, nameservers) {
if (error || !nameservers) return retryCallback(error || new DomainError(DomainError.EXTERNAL_ERROR, 'Unable to get nameservers'));
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error || !nameservers) return retryCallback(error || new DomainsError(DomainsError.EXTERNAL_ERROR, 'Unable to get nameservers'));
async.every(nameservers, isChangeSynced.bind(null, domain, value, type), function (error, synced) {
debug('waitForIp: %s %s ns: %j', domain, synced ? 'done' : 'not done', nameservers);
async.every(nameservers, isChangeSynced.bind(null, domain, value), function (error, synced) {
debug('waitForDns: %s %s ns: %j', domain, synced ? 'done' : 'not done', nameservers);
retryCallback(synced ? null : new DomainError(DomainError.EXTERNAL_ERROR, 'ETRYAGAIN'));
retryCallback(synced ? null : new DomainsError(DomainsError.EXTERNAL_ERROR, 'ETRYAGAIN'));
});
});
}, function retryDone(error) {
if (error) return callback(error);
debug('waitForDNS: %s done.', domain);
debug(`waitForDns: ${domain} has propagated`);
callback(null);
});
+24 -37
View File
@@ -15,6 +15,7 @@ exports = module.exports = {
createSubcontainer: createSubcontainer,
getContainerIdByIp: getContainerIdByIp,
inspect: inspect,
inspectByName: inspect,
execContainer: execContainer
};
@@ -44,56 +45,38 @@ var addons = require('./addons.js'),
debug = require('debug')('box:docker.js'),
once = require('once'),
safe = require('safetydance'),
shell = require('./shell.js'),
spawn = child_process.spawn,
util = require('util'),
_ = require('underscore');
function debugApp(app, args) {
assert(!app || typeof app === 'object');
assert(typeof app === 'object');
var prefix = app ? app.intrinsicFqdn : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function pullImage(manifest, callback) {
var docker = exports.connection;
docker.pull(manifest.dockerImage, function (err, stream) {
if (err) return callback(new Error('Error connecting to docker. statusCode: ' + err.statusCode));
// Use docker CLI here to support downloading of private repos. for dockerode, we have to use
// https://github.com/apocas/dockerode#pull-from-private-repos
shell.exec('pullImage', '/usr/bin/docker', [ 'pull', manifest.dockerImage ], { }, function (error) {
if (error) {
debug(`pullImage: Error pulling image ${manifest.dockerImage} of ${manifest.id}: ${error.message}`);
return callback(new Error('Failed to pull image'));
}
// 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);
var image = docker.getImage(manifest.dockerImage);
// The information here is useless because this is per layer as opposed to per image
if (data.status) {
} else if (data.error) {
debug('pullImage error %s: %s', manifest.id, data.errorDetail.message);
}
});
image.inspect(function (err, data) {
if (err) return callback(new Error('Error inspecting image:' + err.message));
if (!data || !data.Config) return callback(new Error('Missing Config in image:' + JSON.stringify(data, null, 4)));
if (!data.Config.Entrypoint && !data.Config.Cmd) return callback(new Error('Only images with entry point are allowed'));
stream.on('end', function () {
debug('downloaded image %s of %s successfully', manifest.dockerImage, manifest.id);
if (data.Config.ExposedPorts) debug('This image of %s exposes ports: %j', manifest.id, data.Config.ExposedPorts);
var image = docker.getImage(manifest.dockerImage);
image.inspect(function (err, data) {
if (err) return callback(new Error('Error inspecting image:' + err.message));
if (!data || !data.Config) return callback(new Error('Missing Config in image:' + JSON.stringify(data, null, 4)));
if (!data.Config.Entrypoint && !data.Config.Cmd) return callback(new Error('Only images with entry point are allowed'));
if (data.Config.ExposedPorts) debug('This image of %s exposes ports: %j', manifest.id, data.Config.ExposedPorts);
callback(null);
});
});
stream.on('error', function (error) {
debug('error pulling image %s of %s: %j', manifest.dockerImage, manifest.id, error);
callback(error);
callback(null);
});
});
}
@@ -129,7 +112,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
var manifest = app.manifest;
var exposedPorts = {}, dockerPortBindings = { };
var domain = app.altDomain || app.intrinsicFqdn;
var domain = app.fqdn;
var stdEnv = [
'CLOUDRON=1',
'WEBADMIN_ORIGIN=' + config.adminOrigin(),
@@ -163,6 +146,10 @@ function createSubcontainer(app, name, cmd, options, callback) {
memoryLimit = constants.DEFAULT_MEMORY_LIMIT;
}
// give scheduler tasks twice the memory limit since background jobs take more memory
// if required, we can make this a manifest and runtime argument later
if (!isAppContainer) memoryLimit *= 2;
// apparmor is disabled on few servers
var enableSecurityOpt = config.CLOUDRON && safe(function () { return child_process.spawnSync('aa-enabled').status === 0; }, false);
@@ -186,7 +173,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
'/run': {}
},
Labels: {
'fqdn': app.intrinsicFqdn,
'fqdn': app.fqdn,
'appId': app.id,
'isSubcontainer': String(!isAppContainer)
},
+91 -75
View File
@@ -10,13 +10,15 @@ module.exports = exports = {
fqdn: fqdn,
setAdmin: setAdmin,
getDNSRecords: getDNSRecords,
upsertDNSRecords: upsertDNSRecords,
removeDNSRecords: removeDNSRecords,
getDnsRecords: getDnsRecords,
upsertDnsRecords: upsertDnsRecords,
removeDnsRecords: removeDnsRecords,
waitForDNSRecord: waitForDNSRecord,
waitForDnsRecord: waitForDnsRecord,
DomainError: DomainError
removePrivateFields: removePrivateFields,
DomainsError: DomainsError
};
var assert = require('assert'),
@@ -32,12 +34,13 @@ var assert = require('assert'),
shell = require('./shell.js'),
sysinfo = require('./sysinfo.js'),
tld = require('tldjs'),
util = require('util');
util = require('util'),
_ = require('underscore');
var RESTART_CMD = path.join(__dirname, 'scripts/restart.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function DomainError(reason, errorOrMessage) {
function DomainsError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
@@ -55,17 +58,17 @@ function DomainError(reason, errorOrMessage) {
this.nestedError = errorOrMessage;
}
}
util.inherits(DomainError, Error);
util.inherits(DomainsError, Error);
DomainError.NOT_FOUND = 'No such domain';
DomainError.ALREADY_EXISTS = 'Domain already exists';
DomainError.EXTERNAL_ERROR = 'External error';
DomainError.BAD_FIELD = 'Bad Field';
DomainError.STILL_BUSY = 'Still busy';
DomainError.IN_USE = 'In Use';
DomainError.INTERNAL_ERROR = 'Internal error';
DomainError.ACCESS_DENIED = 'Access denied';
DomainError.INVALID_PROVIDER = 'provider must be route53, gcdns, digitalocean, cloudflare, noop, manual or caas';
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, manual or caas';
// choose which subdomain backend we use for test purpose we use route53
function api(provider) {
@@ -77,6 +80,9 @@ function api(provider) {
case 'route53': return require('./dns/route53.js');
case 'gcdns': return require('./dns/gcdns.js');
case 'digitalocean': return require('./dns/digitalocean.js');
case 'gandi': return require('./dns/gandi.js');
case 'godaddy': return require('./dns/godaddy.js');
case 'namecom': return require('./dns/namecom.js');
case 'noop': return require('./dns/noop.js');
case 'manual': return require('./dns/manual.js');
default: return null;
@@ -92,7 +98,7 @@ function verifyDnsConfig(config, domain, zoneName, provider, ip, callback) {
assert.strictEqual(typeof callback, 'function');
var backend = api(provider);
if (!backend) return callback(new DomainError(DomainError.INVALID_PROVIDER));
if (!backend) return callback(new DomainsError(DomainsError.INVALID_PROVIDER));
api(provider).verifyDnsConfig(config, domain, zoneName, ip, callback);
}
@@ -107,40 +113,42 @@ function add(domain, zoneName, provider, config, fallbackCertificate, tlsConfig,
assert.strictEqual(typeof tlsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
if (!tld.isValid(domain)) return callback(new DomainError(DomainError.BAD_FIELD, 'Invalid domain'));
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 (zoneName) {
if (!tld.isValid(zoneName)) return callback(new DomainError(DomainError.BAD_FIELD, 'Invalid 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'));
} else {
zoneName = tld.getDomain(domain) || domain;
}
if (fallbackCertificate) {
let error = reverseProxy.validateCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain);
if (error) return callback(new DomainError(DomainError.BAD_FIELD, error.message));
let error = reverseProxy.validateCertificate(`test.${domain}`, fallbackCertificate.cert, fallbackCertificate.key);
if (error) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
}
if (tlsConfig.provider !== 'fallback' && tlsConfig.provider !== 'caas' && tlsConfig.provider.indexOf('letsencrypt-') !== 0) {
return callback(new DomainError(DomainError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback or le-*'));
return callback(new DomainsError(DomainsError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback or le-*'));
}
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
verifyDnsConfig(config, domain, zoneName, provider, ip, function (error, result) {
if (error && error.reason === DomainError.ACCESS_DENIED) return callback(new DomainError(DomainError.BAD_FIELD, 'Error adding A record. Access denied'));
if (error && error.reason === DomainError.NOT_FOUND) return callback(new DomainError(DomainError.BAD_FIELD, 'Zone not found'));
if (error && error.reason === DomainError.EXTERNAL_ERROR) return callback(new DomainError(DomainError.BAD_FIELD, 'Error adding A record:' + error.message));
if (error && error.reason === DomainError.BAD_FIELD) return callback(new DomainError(DomainError.BAD_FIELD, error.message));
if (error && error.reason === DomainError.INVALID_PROVIDER) return callback(new DomainError(DomainError.BAD_FIELD, error.message));
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
if (error && error.reason === DomainsError.ACCESS_DENIED) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record. 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, 'Error adding A record: ' + 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));
domaindb.add(domain, { zoneName: zoneName, provider: provider, config: result, tlsConfig: tlsConfig }, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new DomainError(DomainError.ALREADY_EXISTS));
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, 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));
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
callback();
});
@@ -155,16 +163,16 @@ 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 DomainError(DomainError.NOT_FOUND));
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, 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));
reverseProxy.getFallbackCertificate(domain, function (error, bundle) {
if (error && error.reason !== ReverseProxyError.NOT_FOUND) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
if (error && error.reason !== ReverseProxyError.NOT_FOUND) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, 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 DomainError(DomainError.INTERNAL_ERROR, error));
if (!cert || !key) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, 'unable to read certificates from disk'));
result.fallbackCertificate = { cert: cert, key: key };
@@ -177,14 +185,15 @@ function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
domaindb.getAll(function (error, result) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
return callback(null, result);
});
}
function update(domain, provider, config, fallbackCertificate, tlsConfig, callback) {
function update(domain, zoneName, provider, config, fallbackCertificate, tlsConfig, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof fallbackCertificate, 'object');
@@ -192,37 +201,43 @@ function update(domain, provider, config, fallbackCertificate, tlsConfig, callba
assert.strictEqual(typeof callback, 'function');
domaindb.get(domain, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainError(DomainError.NOT_FOUND));
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, 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 (zoneName) {
if (!tld.isValid(zoneName)) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid zoneName'));
} else {
zoneName = result.zoneName;
}
if (fallbackCertificate) {
let error = reverseProxy.validateCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain);
if (error) return callback(new DomainError(DomainError.BAD_FIELD, error.message));
let error = reverseProxy.validateCertificate(`test.${domain}`, fallbackCertificate.cert, fallbackCertificate.key);
if (error) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
}
if (tlsConfig.provider !== 'fallback' && tlsConfig.provider !== 'caas' && tlsConfig.provider.indexOf('letsencrypt-') !== 0) {
return callback(new DomainError(DomainError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback or letsencrypt-*'));
return callback(new DomainsError(DomainsError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback or letsencrypt-*'));
}
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
verifyDnsConfig(config, domain, result.zoneName, provider, ip, function (error, result) {
if (error && error.reason === DomainError.ACCESS_DENIED) return callback(new DomainError(DomainError.BAD_FIELD, 'Error adding A record. Access denied'));
if (error && error.reason === DomainError.NOT_FOUND) return callback(new DomainError(DomainError.BAD_FIELD, 'Zone not found'));
if (error && error.reason === DomainError.EXTERNAL_ERROR) return callback(new DomainError(DomainError.BAD_FIELD, 'Error adding A record:' + error.message));
if (error && error.reason === DomainError.BAD_FIELD) return callback(new DomainError(DomainError.BAD_FIELD, error.message));
if (error && error.reason === DomainError.INVALID_PROVIDER) return callback(new DomainError(DomainError.BAD_FIELD, error.message));
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
verifyDnsConfig(config, domain, zoneName, provider, ip, function (error, result) {
if (error && error.reason === DomainsError.ACCESS_DENIED) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record. 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, 'Error adding A record:' + 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));
domaindb.update(domain, { provider: provider, config: result, tlsConfig: tlsConfig }, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainError(DomainError.NOT_FOUND));
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
domaindb.update(domain, { zoneName: zoneName, provider: provider, config: result, tlsConfig: tlsConfig }, 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 (!fallbackCertificate) return callback();
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
callback();
});
@@ -237,9 +252,9 @@ function del(domain, callback) {
assert.strictEqual(typeof callback, 'function');
domaindb.del(domain, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainError(DomainError.NOT_FOUND));
if (error && error.reason === DatabaseError.IN_USE) return callback(new DomainError(DomainError.IN_USE));
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, 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));
return callback(null);
});
@@ -256,14 +271,14 @@ function getName(domain, subdomain) {
return subdomain === '' ? part : subdomain + '.' + part;
}
function getDNSRecords(subdomain, domain, type, callback) {
function getDnsRecords(subdomain, domain, type, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
get(domain, function (error, result) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
api(result.provider).get(result.config, result.zoneName, getName(result, subdomain), type, function (error, values) {
if (error) return callback(error);
@@ -273,7 +288,7 @@ function getDNSRecords(subdomain, domain, type, callback) {
});
}
function upsertDNSRecords(subdomain, domain, type, values, callback) {
function upsertDnsRecords(subdomain, domain, type, values, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
@@ -283,7 +298,7 @@ function upsertDNSRecords(subdomain, domain, type, values, callback) {
debug('upsertDNSRecord: %s on %s type %s values', subdomain, domain, type, values);
get(domain, function (error, result) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
api(result.provider).upsert(result.config, result.zoneName, getName(result, subdomain), type, values, function (error, changeId) {
if (error) return callback(error);
@@ -293,7 +308,7 @@ function upsertDNSRecords(subdomain, domain, type, values, callback) {
});
}
function removeDNSRecords(subdomain, domain, type, values, callback) {
function removeDnsRecords(subdomain, domain, type, values, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
@@ -306,29 +321,25 @@ function removeDNSRecords(subdomain, domain, type, values, callback) {
if (error) return callback(error);
api(result.provider).del(result.config, result.zoneName, getName(result, subdomain), type, values, function (error) {
if (error && error.reason !== DomainError.NOT_FOUND) return callback(error);
if (error && error.reason !== DomainsError.NOT_FOUND) return callback(error);
callback(null);
});
});
}
function waitForDNSRecord(fqdn, domain, value, type, options, callback) {
// only wait for A record
function waitForDnsRecord(fqdn, domain, value, options, callback) {
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof domain, 'string');
assert(typeof value === 'string' || util.isRegExp(value));
assert(type === 'A' || type === 'CNAME' || type === 'TXT');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
get(domain, function (error, result) {
// domain can be not found when waiting for altDomain. When we migrate altDomain, this can never happen
if (error && error.reason !== DomainError.NOT_FOUND) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
if (error) return callback(error);
// hack for lack of provider with altDomain. When we migrate altDomain, this will be automatically "manual"
const provider = result ? result.provider : 'manual';
api(provider).waitForDns(fqdn, result ? result.zoneName : domain, value, type, options, callback);
api(result.provider).waitForDns(fqdn, result ? result.zoneName : domain, value, options, callback);
});
}
@@ -344,7 +355,7 @@ function setAdmin(domain, callback) {
var setPtrRecord = config.provider() === 'caas' ? caas.setPtrRecord : function (d, next) { next(); };
setPtrRecord(domain, function (error) {
if (error) return callback(new DomainError(DomainError.EXTERNAL_ERROR, 'Error setting PTR record:' + error.message));
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Error setting PTR record:' + error.message));
config.setAdminDomain(result.domain);
config.setAdminLocation('my');
@@ -361,3 +372,8 @@ function fqdn(location, domain, provider) {
return location + (location ? (provider !== 'caas' ? '.' : '-') : '') + domain;
}
function removePrivateFields(domain) {
var result = _.pick(domain, 'domain', 'zoneName', 'provider', 'config', 'tlsConfig', 'fallbackCertificate');
if (result.fallbackCertificate) delete result.fallbackCertificate.key; // do not return the 'key'. in caas, this is private
return result;
}
+2 -2
View File
@@ -23,7 +23,7 @@ function sync(callback) {
debug('refreshDNS: current ip %s', ip);
domains.upsertDNSRecords(config.adminLocation(), config.adminDomain(), 'A', [ ip ], function (error) {
domains.upsertDnsRecords(config.adminLocation(), config.adminDomain(), 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('refreshDNS: done for admin location');
@@ -35,7 +35,7 @@ function sync(callback) {
// do not change state of installing apps since apptask will error if dns record already exists
if (app.installationState !== appdb.ISTATE_INSTALLED) return callback();
domains.upsertDNSRecords(app.location, app.domain, 'A', [ ip ], callback);
domains.upsertDnsRecords(app.location, app.domain, 'A', [ ip ], callback);
}, function (error) {
if (error) return callback(error);
+4 -12
View File
@@ -22,7 +22,6 @@ exports = module.exports = {
ACTION_BACKUP_START: 'backup.start',
ACTION_BACKUP_CLEANUP: 'backup.cleanup',
ACTION_CERTIFICATE_RENEWAL: 'certificate.renew',
ACTION_CLI_MODE: 'settings.climode',
ACTION_START: 'cloudron.start',
ACTION_UPDATE: 'cloudron.update',
ACTION_USER_ADD: 'user.add',
@@ -91,14 +90,14 @@ function get(id, callback) {
});
}
function getAllPaged(action, search, page, perPage, callback) {
assert(typeof action === 'string' || action === null);
function getAllPaged(actions, search, page, perPage, callback) {
assert(Array.isArray(actions));
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
eventlogdb.getAllPaged(action, search, page, perPage, function (error, events) {
eventlogdb.getAllPaged(actions, search, page, perPage, function (error, events) {
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
callback(null, events);
@@ -122,14 +121,7 @@ function cleanup(callback) {
var d = new Date();
d.setDate(d.getDate() - 10); // 10 days ago
// only cleanup high frequency events
var actions = [
exports.ACTION_USER_LOGIN,
exports.ACTION_BACKUP_START,
exports.ACTION_BACKUP_FINISH
];
eventlogdb.delByCreationTime(d, actions, function (error) {
eventlogdb.delByCreationTime(d, function (error) {
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
callback(null);
+12 -13
View File
@@ -40,8 +40,8 @@ function get(eventId, callback) {
});
}
function getAllPaged(action, search, page, perPage, callback) {
assert(typeof action === 'string' || action === null);
function getAllPaged(actions, search, page, perPage, callback) {
assert(Array.isArray(actions));
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
@@ -50,14 +50,15 @@ function getAllPaged(action, search, page, perPage, callback) {
var data = [];
var query = 'SELECT ' + EVENTLOGS_FIELDS + ' FROM eventlog';
if (action || search) query += ' WHERE';
if (actions.length || search) query += ' WHERE';
if (search) query += ' (source LIKE ' + mysql.escape('%' + search + '%') + ' OR data LIKE ' + mysql.escape('%' + search + '%') + ')';
if (action && search) query += ' AND ';
if (action) {
query += ' action=?';
data.push(action);
}
if (actions.length && search) query += ' AND ( ';
actions.forEach(function (action, i) {
query += ' (action LIKE ' + mysql.escape(`%${action}%`) + ') ';
if (i < actions.length-1) query += ' OR ';
});
if (actions.length && search) query += ' ) ';
query += ' ORDER BY creationTime DESC LIMIT ?,?';
@@ -120,15 +121,13 @@ function clear(callback) {
});
}
function delByCreationTime(creationTime, actions, callback) {
function delByCreationTime(creationTime, callback) {
assert(util.isDate(creationTime));
assert(Array.isArray(actions));
assert.strictEqual(typeof callback, 'function');
var query = 'DELETE FROM eventlog WHERE creationTime < ? ';
if (actions.length) query += ' AND ( ' + actions.map(function () { return 'action != ?'; }).join(' AND ') + ' ) ';
var query = 'DELETE FROM eventlog WHERE creationTime < ?';
database.query(query, [ creationTime ].concat(actions), function (error) {
database.query(query, [ creationTime ], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(error);
+41 -46
View File
@@ -1,7 +1,7 @@
'use strict';
exports = module.exports = {
GroupError: GroupError,
GroupsError: GroupsError,
create: create,
remove: remove,
@@ -24,13 +24,12 @@ var assert = require('assert'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
groupdb = require('./groupdb.js'),
mailboxdb = require('./mailboxdb.js'),
util = require('util'),
uuid = require('uuid');
// http://dustinsenos.com/articles/customErrorsInNode
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
function GroupError(reason, errorOrMessage) {
function GroupsError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
@@ -48,28 +47,28 @@ function GroupError(reason, errorOrMessage) {
this.nestedError = errorOrMessage;
}
}
util.inherits(GroupError, Error);
GroupError.INTERNAL_ERROR = 'Internal Error';
GroupError.ALREADY_EXISTS = 'Already Exists';
GroupError.NOT_FOUND = 'Not Found';
GroupError.BAD_FIELD = 'Field error';
GroupError.NOT_EMPTY = 'Not Empty';
GroupError.NOT_ALLOWED = 'Not Allowed';
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 GroupError(GroupError.BAD_FIELD, 'name must be atleast 1 char');
if (name.length >= 200) return new GroupError(GroupError.BAD_FIELD, 'name too long');
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 (constants.RESERVED_NAMES.indexOf(name) !== -1) return new GroupError(GroupError.BAD_FIELD, 'name is reserved');
if (constants.RESERVED_NAMES.indexOf(name) !== -1) return new GroupsError(GroupsError.BAD_FIELD, 'name is reserved');
// +/- can be tricky in emails. also need to consider valid LDAP characters here (e.g '+' is reserved)
if (/[^a-zA-Z0-9.]/.test(name)) return new GroupError(GroupError.BAD_FIELD, 'name can only contain alphanumerals and dot');
if (/[^a-zA-Z0-9.]/.test(name)) return new GroupsError(GroupsError.BAD_FIELD, 'name can only contain alphanumerals and dot');
// app emails are sent using the .app suffix
if (name.indexOf('.app') !== -1) return new GroupError(GroupError.BAD_FIELD, 'name pattern is reserved for apps');
if (name.indexOf('.app') !== -1) return new GroupsError(GroupsError.BAD_FIELD, 'name pattern is reserved for apps');
return null;
}
@@ -86,8 +85,8 @@ 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 GroupError(GroupError.ALREADY_EXISTS));
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, 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));
callback(null, { id: id, name: name });
});
@@ -98,17 +97,13 @@ function remove(id, callback) {
assert.strictEqual(typeof callback, 'function');
// never allow admin group to be deleted
if (id === constants.ADMIN_GROUP_ID) return callback(new GroupError(GroupError.NOT_ALLOWED));
if (id === constants.ADMIN_GROUP_ID) return callback(new GroupsError(GroupsError.NOT_ALLOWED));
mailboxdb.delByOwnerId(id, function (error) {
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
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));
groupdb.del(id, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
callback(null);
});
callback(null);
});
}
@@ -117,8 +112,8 @@ 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 GroupError(GroupError.NOT_FOUND));
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, 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));
return callback(null, result);
});
@@ -129,8 +124,8 @@ 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 GroupError(GroupError.NOT_FOUND));
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, 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));
return callback(null, result);
});
@@ -140,7 +135,7 @@ function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
groupdb.getAll(function (error, result) {
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
return callback(null, result);
});
@@ -150,7 +145,7 @@ function getAllWithMembers(callback) {
assert.strictEqual(typeof callback, 'function');
groupdb.getAllWithMembers(function (error, result) {
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
return callback(null, result);
});
@@ -161,8 +156,8 @@ 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 GroupError(GroupError.NOT_FOUND));
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, 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));
return callback(null, result);
});
@@ -173,8 +168,8 @@ function getGroups(userId, callback) {
assert.strictEqual(typeof callback, 'function');
groupdb.getGroups(userId, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, 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));
return callback(null, result);
});
@@ -186,8 +181,8 @@ function setGroups(userId, groupIds, callback) {
assert.strictEqual(typeof callback, 'function');
groupdb.setGroups(userId, groupIds, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, 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));
return callback(null);
});
@@ -199,8 +194,8 @@ 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 GroupError(GroupError.NOT_FOUND));
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, 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));
return callback(null);
});
@@ -212,8 +207,8 @@ 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 GroupError(GroupError.NOT_FOUND, 'Invalid group or user id'));
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, 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));
return callback(null);
});
@@ -225,8 +220,8 @@ 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 GroupError(GroupError.NOT_FOUND));
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, 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));
return callback(null);
});
@@ -238,8 +233,8 @@ 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 GroupError(GroupError.NOT_FOUND));
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, 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));
return callback(null, result);
});
+5 -5
View File
@@ -7,18 +7,18 @@
exports = module.exports = {
// a major version makes all apps restore from backup. #451 must be fixed before we do this.
// a minor version makes all apps re-configure themselves
'version': '48.9.0',
'version': '48.10.0',
'baseImages': [ 'cloudron/base:0.10.0' ],
// Note that if any of the databases include an upgrade, bump the infra version above
// This is because we upgrade using dumps instead of mysql_upgrade, pg_upgrade etc
'images': {
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:1.0.0' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:1.0.0' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:1.0.1' },
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:1.1.0' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:1.1.0' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:1.1.0' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:1.0.0' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:1.0.0' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:1.3.0' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:1.0.0' }
}
};
+91 -43
View File
@@ -13,8 +13,8 @@ var assert = require('assert'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:ldap'),
eventlog = require('./eventlog.js'),
user = require('./user.js'),
UserError = user.UserError,
users = require('./users.js'),
UsersError = users.UsersError,
ldap = require('ldapjs'),
mail = require('./mail.js'),
MailError = mail.MailError,
@@ -51,7 +51,7 @@ function getUsersWithAccessToApp(req, callback) {
getAppByRequest(req, function (error, app) {
if (error) return callback(error);
user.list(function (error, result){
users.list(function (error, result){
if (error) return callback(new ldap.OperationsError(error.toString()));
async.filter(result, apps.hasAccessTo.bind(null, app), function (error, result) {
@@ -258,38 +258,83 @@ function groupAdminsCompare(req, res, next) {
function mailboxSearch(req, res, next) {
debug('mailbox search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id);
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
// if cn is set we only search for one mailbox specifically
if (req.dn.rdns[0].attrs.cn) {
var email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
var parts = email.split('@');
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
var email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
var parts = email.split('@');
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) return next(new ldap.OperationsError(error.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) return next(new ldap.OperationsError(error.toString()));
var obj = {
dn: req.dn.toString(),
attributes: {
objectclass: ['mailbox'],
objectcategory: 'mailbox',
cn: `${mailbox.name}@${mailbox.domain}`,
uid: `${mailbox.name}@${mailbox.domain}`,
mail: `${mailbox.name}@${mailbox.domain}`,
ownerType: mailbox.ownerType,
displayname: 'Max Mustermann',
givenName: 'Max',
username: 'mmustermann',
samaccountname: 'mmustermann'
}
};
var obj = {
dn: req.dn.toString(),
attributes: {
objectclass: ['mailbox'],
objectcategory: 'mailbox',
cn: `${mailbox.name}@${mailbox.domain}`,
uid: `${mailbox.name}@${mailbox.domain}`,
mail: `${mailbox.name}@${mailbox.domain}`,
ownerType: mailbox.ownerType
// ensure all filter values are also lowercase
var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null);
if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString()));
if (lowerCaseFilter.matches(obj.attributes)) {
finalSend([ obj ], req, res, next);
} else {
res.end();
}
};
});
} else if (req.dn.rdns[0].attrs.domain) {
var domain = req.dn.rdns[0].attrs.domain.value.toLowerCase();
// ensure all filter values are also lowercase
var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null);
if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString()));
mailboxdb.listMailboxes(domain, function (error, result) {
if (error) return next(new ldap.OperationsError(error.toString()));
if (lowerCaseFilter.matches(obj.attributes)) {
finalSend([ obj ], req, res, next);
} else {
res.end();
}
});
var results = [];
// only send user mailboxes
result = result.filter(function (m) { return m.ownerType === mailboxdb.OWNER_TYPE_USER; });
// send mailbox objects
result.forEach(function (mailbox) {
var dn = ldap.parseDN(`cn=${mailbox.name}@${domain},domain=${domain},ou=mailboxes,dc=cloudron`);
var obj = {
dn: dn.toString(),
attributes: {
objectclass: ['mailbox'],
objectcategory: 'mailbox',
cn: `${mailbox.name}@${domain}`,
uid: `${mailbox.name}@${domain}`,
mail: `${mailbox.name}@${domain}`,
ownerType: mailbox.ownerType
}
};
// ensure all filter values are also lowercase
var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null);
if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString()));
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) {
results.push(obj);
}
});
finalSend(results, req, res, next);
});
} else {
return next(new ldap.NoSuchObjectError(req.dn.toString()));
}
}
function mailAliasSearch(req, res, next) {
@@ -378,18 +423,18 @@ function authenticateUser(req, res, next) {
var api;
if (attributeName === 'mail') {
api = user.verifyWithEmail;
api = users.verifyWithEmail;
} else if (commonName.indexOf('@') !== -1) { // if mail is specified, enforce mail check
api = user.verifyWithEmail;
api = users.verifyWithEmail;
} else if (commonName.indexOf('uid-') === 0) {
api = user.verify;
api = users.verify;
} else {
api = user.verifyWithUsername;
api = users.verifyWithUsername;
}
api(commonName, req.credentials || '', function (error, user) {
if (error && error.reason === UserError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error && error.reason === UserError.WRONG_PASSWORD) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
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) return next(new ldap.OperationsError(error.message));
req.user = user;
@@ -410,7 +455,7 @@ function authorizeUserForApp(req, res, next) {
// we return no such object, to avoid leakage of a users existence
if (!result) return next(new ldap.NoSuchObjectError(req.dn.toString()));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: app.id }, { userId: req.user.id });
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: app.id, app: app }, { userId: req.user.id, user: users.removePrivateFields(req.user) });
res.end();
});
@@ -418,6 +463,8 @@ function authorizeUserForApp(req, res, next) {
}
function authenticateMailbox(req, res, next) {
debug('mailbox auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
var email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
@@ -428,11 +475,11 @@ function authenticateMailbox(req, res, next) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
mail.get(parts[1], function (error, domain) {
mail.getDomain(parts[1], function (error, domain) {
if (error && error.reason === MailError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
if (mailbox.ownerType === mailboxdb.TYPE_APP) {
if (mailbox.ownerType === mailboxdb.OWNER_TYPE_APP) {
var addonId = req.dn.rdns[1].attrs.ou.value.toLowerCase(); // 'sendmail' or 'recvmail'
var name;
if (addonId === 'sendmail') name = 'MAIL_SMTP_PASSWORD';
@@ -446,15 +493,15 @@ function authenticateMailbox(req, res, next) {
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: name }, { appId: mailbox.ownerId, addonId: addonId });
return res.end();
});
} else if (mailbox.ownerType === mailboxdb.TYPE_USER) {
} else if (mailbox.ownerType === mailboxdb.OWNER_TYPE_USER) {
if (!domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
user.verifyWithUsername(parts[0], req.credentials || '', function (error, user) {
if (error && error.reason === UserError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error && error.reason === UserError.WRONG_PASSWORD) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
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) return next(new ldap.OperationsError(error.message));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: user.username });
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) });
res.end();
});
} else {
@@ -487,6 +534,7 @@ function start(callback) {
gServer.search('ou=mailaliases,dc=cloudron', mailAliasSearch);
gServer.search('ou=mailinglists,dc=cloudron', mailingListSearch);
gServer.bind('ou=mailboxes,dc=cloudron', authenticateMailbox);
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailbox);
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailbox);
+360 -260
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -9,7 +9,7 @@ This is most likely a problem in the application.
To resolve this, you can try the following:
* Restart the app in the app configuration dialog
* Restore the app to the latest backup
* Contact us via support@cloudron.io or https://chat.cloudron.io
* Contact us via support@cloudron.io or https://forum.cloudron.io
Powered by https://cloudron.io
@@ -46,6 +46,4 @@ Sent at: <%= new Date().toUTCString() %>
</center>
<img src="https://analytics.cloudron.io/piwik.php?idsite=2&rec=1&e_c=CloudronEmail&e_a=update" style="border:0" alt="" />
<% } %>
@@ -52,7 +52,5 @@ Sent at: <%= new Date().toUTCString() %>
</center>
<img src="https://analytics.cloudron.io/piwik.php?idsite=2&rec=1&e_c=CloudronEmail&e_a=update" style="border:0" alt="" />
<% } %>
-1
View File
@@ -174,5 +174,4 @@ Sent at: <%= new Date().toUTCString() %>
</div>
</center>
<img src="https://analytics.cloudron.io/piwik.php?idsite=2&rec=1&e_c=CloudronEmail&e_a=digest" style="border:0" alt="" />
<% } %>
-2
View File
@@ -38,6 +38,4 @@ Powered by https://cloudron.io
</center>
<img src="https://analytics.cloudron.io/piwik.php?idsite=2&rec=1&e_c=CloudronEmail&e_a=passwordReset" style="border:0" alt="" />
<% } %>
-2
View File
@@ -44,6 +44,4 @@ Powered by https://cloudron.io
</center>
<img src="https://analytics.cloudron.io/piwik.php?idsite=2&rec=1&e_c=CloudronEmail&e_a=userAdded" style="border:0" alt="" />
<% } %>
-2
View File
@@ -43,6 +43,4 @@ Powered by https://cloudron.io
</center>
<img src="https://analytics.cloudron.io/piwik.php?idsite=2&rec=1&e_c=CloudronEmail&e_a=welcomeUser" style="border:0" alt="" />
<% } %>
+131 -55
View File
@@ -1,7 +1,11 @@
'use strict';
exports = module.exports = {
add: add,
addMailbox: addMailbox,
addGroup: addGroup,
updateMailbox: updateMailbox,
updateList: updateList,
del: del,
listAliases: listAliases,
@@ -17,31 +21,44 @@ exports = module.exports = {
getByOwnerId: getByOwnerId,
delByOwnerId: delByOwnerId,
delByDomain: delByDomain,
updateName: updateName,
_clear: clear,
TYPE_USER: 'user',
TYPE_APP: 'app',
TYPE_GROUP: 'group'
TYPE_MAILBOX: 'mailbox',
TYPE_LIST: 'list',
TYPE_ALIAS: 'alias',
OWNER_TYPE_USER: 'user',
OWNER_TYPE_APP: 'app',
OWNER_TYPE_GROUP: 'group' // obsolete
};
var assert = require('assert'),
database = require('./database.js'),
DatabaseError = require('./databaseerror.js'),
safe = require('safetydance'),
util = require('util');
var MAILBOX_FIELDS = [ 'name', 'ownerId', 'ownerType', 'aliasTarget', 'creationTime', 'domain' ].join(',');
var MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'ownerType', 'aliasTarget', 'creationTime', 'membersJson', 'domain' ].join(',');
function add(name, domain, ownerId, ownerType, callback) {
function postProcess(data) {
data.members = safe.JSON.parse(data.membersJson) || [ ];
delete data.membersJson;
return data;
}
function addMailbox(name, domain, ownerId, ownerType, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof ownerType, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO mailboxes (name, domain, ownerId, ownerType) VALUES (?, ?, ?, ?)', [ name, domain, ownerId, ownerType ], function (error) {
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, ownerType) VALUES (?, ?, ?, ?, ?)', [ name, exports.TYPE_MAILBOX, domain, ownerId, ownerType ], 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));
@@ -49,6 +66,51 @@ function add(name, domain, ownerId, ownerType, callback) {
});
}
function updateMailbox(name, domain, ownerId, ownerType, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof ownerType, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('UPDATE mailboxes SET ownerId = ? WHERE name = ? AND domain = ? AND ownerType = ?', [ ownerId, name, domain, ownerType ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null);
});
}
function addGroup(name, domain, members, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(members));
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, ownerType, membersJson) VALUES (?, ?, ?, ?, ?, ?)',
[ name, exports.TYPE_LIST, domain, 'admin', exports.OWNER_TYPE_GROUP, 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));
callback(null);
});
}
function updateList(name, domain, members, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(members));
assert.strictEqual(typeof callback, 'function');
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));
callback(null);
});
}
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -72,11 +134,21 @@ function del(name, domain, callback) {
});
}
function delByDomain(domain, callback) {
assert.strictEqual(typeof domain, 'string');
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));
callback(null);
});
}
function delByOwnerId(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
// deletes aliases as well
database.query('DELETE FROM mailboxes WHERE ownerId=?', [ id ], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -108,35 +180,41 @@ function getMailbox(name, domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND domain = ? AND (ownerType = ? OR ownerType = ?) AND aliasTarget IS NULL', [ name, domain, exports.TYPE_APP, exports.TYPE_USER ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
database.query('SELECT ' + 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));
callback(null, results[0]);
});
callback(null, postProcess(results[0]));
});
}
function listMailboxes(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE domain = ? AND (ownerType = ? OR ownerType = ?) AND aliasTarget IS NULL ORDER BY name', [ domain, exports.TYPE_APP, exports.TYPE_USER ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE type = ? AND domain = ? ORDER BY name',
[ exports.TYPE_MAILBOX, domain ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, results);
});
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
}
function listGroups(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE domain = ? AND ownerType = ? AND aliasTarget IS NULL', [ domain, exports.TYPE_GROUP ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
database.query('SELECT ' + 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));
callback(null, results);
});
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
}
function getGroup(name, domain, callback) {
@@ -144,25 +222,13 @@ function getGroup(name, domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
// This can be merged into a single query but cannot get 'not found' information
// SELECT users.username FROM mailboxes
// INNER JOIN groupMembers ON mailboxes.ownerId = groupMembers.groupId
// INNER JOIN users ON groupMembers.userId = users.id
// WHERE mailboxes.name = <name>
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND domain = ? AND ownerType = ? AND aliasTarget IS NULL', [ name, domain, exports.TYPE_GROUP ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
// username can be null if the user has not signed up with the invite yet
database.query('SELECT users.username FROM groupMembers INNER JOIN users ON groupMembers.userId = users.id WHERE groupMembers.groupId = ? AND users.username IS NOT NULL', [ results[0].ownerId ], function (error, memberList) {
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));
results[0].members = memberList.map(function (m) { return m.username; });
callback(null, results[0]);
callback(null, postProcess(results[0]));
});
});
}
function getByOwnerId(ownerId, callback) {
@@ -173,6 +239,8 @@ function getByOwnerId(ownerId, callback) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
}
@@ -188,10 +256,11 @@ function setAliasesForName(name, domain, aliases, callback) {
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
var queries = [];
queries.push({ query: 'DELETE FROM mailboxes WHERE aliasTarget = ? AND domain = ?', args: [ name, domain ] });
// clear existing aliases
queries.push({ query: 'DELETE FROM mailboxes WHERE aliasTarget = ? AND domain = ? AND type = ?', args: [ name, domain, exports.TYPE_ALIAS ] });
aliases.forEach(function (alias) {
queries.push({ query: 'INSERT INTO mailboxes (name, domain, aliasTarget, ownerId, ownerType) VALUES (?, ?, ?, ?, ?)',
args: [ alias, domain, name, results[0].ownerId, results[0].ownerType ] });
queries.push({ query: 'INSERT INTO mailboxes (name, type, domain, aliasTarget, ownerId, ownerType) VALUES (?, ?, ?, ?, ?, ?)',
args: [ alias, exports.TYPE_ALIAS, domain, name, results[0].ownerId, results[0].ownerType ] });
});
database.transaction(queries, function (error) {
@@ -208,23 +277,27 @@ function getAliasesForName(name, domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT name FROM mailboxes WHERE aliasTarget = ? AND domain = ? ORDER BY name', [ name, domain ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
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));
results = results.map(function (r) { return r.name; });
callback(null, results);
});
results = results.map(function (r) { return r.name; });
callback(null, results);
});
}
function listAliases(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE domain = ? AND aliasTarget IS NOT NULL ORDER BY name', [ domain ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE domain = ? AND type = ? ORDER BY name',
[ domain, exports.TYPE_ALIAS ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, results);
});
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
}
function getAlias(name, domain, callback) {
@@ -232,10 +305,13 @@ function getAlias(name, domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND domain = ? AND aliasTarget IS NOT NULL', [ 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));
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));
callback(null, results[0]);
});
results.forEach(function (result) { postProcess(result); });
callback(null, results[0]);
});
}
+1
View File
@@ -61,6 +61,7 @@ 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));
+15 -16
View File
@@ -39,7 +39,7 @@ var assert = require('assert'),
settings = require('./settings.js'),
showdown = require('showdown'),
smtpTransport = require('nodemailer-smtp-transport'),
users = require('./user.js'),
users = require('./users.js'),
util = require('util'),
_ = require('underscore');
@@ -60,6 +60,19 @@ function splatchError(error) {
return util.inspect(result, { depth: null, showHidden: true });
}
function getAdminEmails(callback) {
users.getAllAdmins(function (error, admins) {
if (error) return callback(error);
if (admins.length === 0) return callback(new Error('No admins on this cloudron')); // box not activated yet
var adminEmails = [ ];
admins.forEach(function (admin) { adminEmails.push(admin.email); });
callback(null, adminEmails);
});
}
// This will collect the most common details required for notification emails
function getMailConfig(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -74,7 +87,7 @@ function getMailConfig(callback) {
cloudronName = 'Cloudron';
}
mail.getAll(function (error, domains) {
mail.getDomains(function (error, domains) {
if (error) return callback(error);
if (domains.length === 0) return callback('No domains configured');
@@ -157,20 +170,6 @@ function render(templateFile, params) {
return content;
}
function getAdminEmails(callback) {
users.getAllAdmins(function (error, admins) {
if (error) return callback(error);
if (admins.length === 0) return callback(new Error('No admins on this cloudron')); // box not activated yet
var adminEmails = [ ];
adminEmails.push(admins[0].fallbackEmail);
admins.forEach(function (admin) { adminEmails.push(admin.email); });
callback(null, adminEmails);
});
}
function mailUserEventToAdmins(user, event) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof event, 'string');
+35
View File
@@ -0,0 +1,35 @@
'use strict';
exports = module.exports = {
resolve: resolve
};
var assert = require('assert'),
dns = require('dns');
// a note on TXT records. It doesn't have quotes ("") at the DNS level. Those quotes
// are added for DNS server software to enclose spaces. Such quotes may also be returned
// by the DNS REST API of some providers
function resolve(hostname, rrtype, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof rrtype, 'string');
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
const resolver = new dns.Resolver();
if (options.server) resolver.setServers([ options.server ]);
// should callback with ECANCELLED but looks like we might hit https://github.com/nodejs/node/issues/14814
const timerId = setTimeout(resolver.cancel.bind(resolver), options.timeout || 5000);
resolver.resolve(hostname, rrtype, function (error, result) {
clearTimeout(timerId);
if (error && error.code === 'ECANCELLED') error.code = 'TIMEOUT';
// result is an empty array if there was no error but there is no record. when you query a random
// domain, it errors with ENOTFOUND. But if you query an existing domain (A record) but with different
// type (CNAME) it is not an error and empty array
callback(error, result);
});
}
+1 -1
View File
@@ -1,6 +1,6 @@
<footer class="text-center">
<span class="text-muted">&copy; 2017 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
<span class="text-muted">&copy; 2016-18 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fa fa-twitter"></i></a></span>
<span class="text-muted"><a href="https://chat.cloudron.io" target="_blank">Chat <i class="fa fa-comments"></i></a></span>
</footer>
+6 -2
View File
@@ -24,16 +24,20 @@
<form id="loginForm" action="" method="post">
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
<div class="form-group">
<label class="control-label" for="inputUsername">Username or Email</label>
<label class="control-label" for="inputUsername">Username</label>
<input type="text" class="form-control" id="inputUsername" name="username" value="<%= username %>" autofocus required>
</div>
<div class="form-group">
<label class="control-label" for="inputPassword">Password</label>
<input type="password" class="form-control" name="password" id="inputPassword" value="<%= password %>" required>
</div>
<div class="form-group">
<label class="control-label" for="inputPassword">2FA Token (if enabled)</label>
<input type="text" class="form-control" name="totpToken" id="inputTotpToken" value="">
</div>
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Sign in"/>
</form>
<a href="/api/v1/session/password/resetRequest.html">Reset your password</a>
<a href="/api/v1/session/password/resetRequest.html">Reset password</a>
</div>
</div>
</div>
+1 -1
View File
@@ -42,7 +42,7 @@ app.controller('Controller', [function () {}]);
</div>
<input type="password" class="form-control" id="inputPasswordRepeat" ng-model="passwordRepeat" name="passwordRepeat" required>
</div>
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Create" ng-disabled="resetForm.$invalid || password !== passwordRepeat"/>
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Set New Password" ng-disabled="resetForm.$invalid || password !== passwordRepeat"/>
</form>
</div>
</div>
+2 -2
View File
@@ -5,7 +5,7 @@
<div class="layout-content">
<center>
<h2>Reset your password</h2>
<h2>Reset password</h2>
</center>
<br/>
@@ -16,7 +16,7 @@
<form action="/api/v1/session/password/resetRequest" method="post" autocomplete="off">
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
<div class="form-group">
<label class="control-label" for="inputIdentifier">Username or Email</label>
<label class="control-label" for="inputIdentifier">Username</label>
<input type="text" class="form-control" id="inputIdentifier" name="identifier" autofocus required>
</div>
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Reset"/>
+30 -3
View File
@@ -22,6 +22,7 @@ var apps = require('./apps.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
shell = require('./shell.js'),
taskmanager = require('./taskmanager.js'),
util = require('util'),
@@ -47,8 +48,15 @@ function start(callback) {
// short-circuit for the restart case
if (_.isEqual(infra, existingInfra)) {
debug('platform is uptodate at version %s', infra.version);
emitPlatformReady();
return callback();
updateAddons(function (error) {
if (error) return callback(error);
emitPlatformReady();
callback();
});
return;
}
debug('Updating infrastructure from %s to %s', existingInfra.version, infra.version);
@@ -61,7 +69,8 @@ function start(callback) {
startAddons.bind(null, existingInfra),
removeOldImages,
startApps.bind(null, existingInfra),
fs.writeFile.bind(fs, paths.INFRA_VERSION_FILE, JSON.stringify(infra))
fs.writeFile.bind(fs, paths.INFRA_VERSION_FILE, JSON.stringify(infra, null, 4)),
updateAddons
], function (error) {
if (error) return callback(error);
@@ -80,6 +89,24 @@ function stop(callback) {
taskmanager.pauseTasks(callback);
}
function updateAddons(callback) {
settings.getPlatformConfig(function (error, platformConfig) {
if (error) return callback(error);
for (var containerName of [ 'mysql', 'postgresql', 'mail', 'mongodb' ]) {
const containerConfig = platformConfig[containerName];
if (!containerConfig) continue;
if (!containerConfig.memory || !containerConfig.memorySwap) continue;
const cmd = `docker update --memory ${containerConfig.memory} --memory-swap ${containerConfig.memorySwap} ${containerName}`;
shell.execSync(`update${containerName}`, cmd);
}
callback();
});
}
function emitPlatformReady() {
// give some time for the platform to "settle". For example, mysql might still be initing the
// database dir and we cannot call service scripts until that's done.
+1 -1
View File
@@ -41,7 +41,7 @@ function setDetail(tag, detail) {
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof detail, 'string');
if (!progress[tag]) return debug('unable to set detail %s', detail);
if (!progress[tag]) return debug('[%s] %s', tag, detail);
progress[tag].detail = detail;
}
+31 -40
View File
@@ -32,6 +32,7 @@ var acme = require('./cert/acme.js'),
caas = require('./cert/caas.js'),
config = require('./config.js'),
constants = require('./constants.js'),
crypto = require('crypto'),
debug = require('debug')('box:certificates'),
domains = require('./domains.js'),
ejs = require('ejs'),
@@ -39,13 +40,13 @@ var acme = require('./cert/acme.js'),
fallback = require('./cert/fallback.js'),
fs = require('fs'),
mailer = require('./mailer.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
platform = require('./platform.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
tld = require('tldjs'),
user = require('./user.js'),
users = require('./users.js'),
util = require('util');
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/../setup/start/nginx/appconfig.ejs', { encoding: 'utf8' }),
@@ -84,12 +85,11 @@ function getApi(app, callback) {
if (domain.tlsConfig.provider === 'fallback') return callback(null, fallback, {});
// use acme if we have altDomain or the tlsConfig is not caas
var api = (app.altDomain || domain.tlsConfig.provider !== 'caas') ? acme : caas;
var api = domain.tlsConfig.provider === 'caas' ? caas : acme;
var options = { };
if (domain.tlsConfig.provider === 'caas') {
options.prod = true; // with altDomain, we will choose acme setting based on this
options.prod = true;
} else { // acme
options.prod = domain.tlsConfig.provider.match(/.*-prod/) !== null; // matches 'le-prod' or 'letsencrypt-prod'
}
@@ -98,7 +98,7 @@ function getApi(app, callback) {
// we cannot use admin@fqdn because the user might not have set it up.
// we simply update the account with the latest email we have each time when getting letsencrypt certs
// https://github.com/ietf-wg-acme/acme/issues/30
user.getOwner(function (error, owner) {
users.getOwner(function (error, owner) {
options.email = error ? 'support@cloudron.io' : (owner.fallbackEmail || owner.email); // can error if not activated yet
callback(null, api, options);
@@ -138,22 +138,11 @@ function validateCertificate(domain, cert, key) {
if (!cert && key) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'missing cert');
if (cert && !key) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'missing key');
var result = safe.child_process.execSync('openssl x509 -noout -checkhost "' + domain + '"', { encoding: 'utf8', input: cert });
// -checkhost checks for SAN or CN exclusively. SAN takes precedence and if present, ignores the CN.
var result = safe.child_process.execSync(`openssl x509 -noout -checkhost "${domain}"`, { encoding: 'utf8', input: cert });
if (!result) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Unable to get certificate subject.');
// if no match, check alt names
if (result.indexOf('does match certificate') === -1) {
// https://github.com/drwetter/testssl.sh/pull/383
var cmd = 'openssl x509 -noout -text | grep -A3 "Subject Alternative Name" | \
grep "DNS:" | \
sed -e "s/DNS://g" -e "s/ //g" -e "s/,/ /g" -e "s/othername:<unsupported>//g"';
result = safe.child_process.execSync(cmd, { encoding: 'utf8', input: cert });
var altNames = result ? [ ] : result.trim().split(' '); // might fail if cert has no SAN
debug('validateCertificate: detected altNames as %j', altNames);
// check altNames
if (!altNames.some(matchesDomain)) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, util.format('Certificate is not valid for this domain. Expecting %s in %j', domain, altNames));
}
if (result.indexOf('does match certificate') === -1) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, `Certificate is not valid for this domain. Expecting ${domain}`);
// 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 });
@@ -186,8 +175,14 @@ function setFallbackCertificate(domain, fallback, callback) {
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));
} else if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) { // generate it
var certCommand = util.format('openssl req -x509 -newkey rsa:2048 -keyout %s -out %s -days 3650 -subj /CN=*.%s -nodes', keyFilePath, certFilePath, domain);
let opensslConf = safe.fs.readFileSync('/etc/ssl/openssl.cnf', 'utf8');
// SAN must contain all the domains since CN check is based on implementation if SAN is found. -checkhost also checks only SAN if present!
let opensslConfWithSan = `${opensslConf}\n[SAN]\nsubjectAltName=DNS:${domain},DNS:*.${domain}\n`;
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=*.${domain} -extensions SAN -config ${configFile} -nodes`);
if (!safe.child_process.execSync(certCommand)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
safe.fs.unlinkSync(configFile);
}
platform.handleCertChanged('*.' + domain);
@@ -220,15 +215,13 @@ function getCertificate(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var vhost = app.altDomain || app.intrinsicFqdn;
var certFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.user.cert`);
var keyFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.user.key`);
var certFilePath = path.join(paths.APP_CERTS_DIR, `${app.fqdn}.user.cert`);
var keyFilePath = path.join(paths.APP_CERTS_DIR, `${app.fqdn}.user.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
certFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.cert`);
keyFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.key`);
certFilePath = path.join(paths.APP_CERTS_DIR, `${app.fqdn}.cert`);
keyFilePath = path.join(paths.APP_CERTS_DIR, `${app.fqdn}.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
@@ -240,7 +233,7 @@ function ensureCertificate(app, auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var vhost = app.altDomain || app.intrinsicFqdn;
const vhost = app.fqdn;
var certFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.user.cert`);
var keyFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.user.key`);
@@ -278,7 +271,7 @@ function ensureCertificate(app, auditSource, callback) {
eventlog.add(eventlog.ACTION_CERTIFICATE_RENEWAL, auditSource, { domain: vhost, errorMessage: errorMessage });
// if no cert was returned use fallback. the fallback/caas provider will not provide any for example
if (!certFilePath || !keyFilePath) return getFallbackCertificate(app.altDomain ? tld.getDomain(app.altDomain) : app.domain, callback);
if (!certFilePath || !keyFilePath) return getFallbackCertificate(app.domain, callback);
callback(null, { certFilePath, keyFilePath, reason: 'new-le' });
});
@@ -314,7 +307,7 @@ function configureAdmin(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var adminApp = { domain: config.adminDomain(), intrinsicFqdn: config.adminFqdn() };
var adminApp = { domain: config.adminDomain(), fqdn: config.adminFqdn() };
ensureCertificate(adminApp, auditSource, function (error, bundle) {
if (error) return callback(error);
@@ -329,12 +322,11 @@ function configureAppInternal(app, bundle, callback) {
var sourceDir = path.resolve(__dirname, '..');
var endpoint = 'app';
var vhost = app.altDomain || app.intrinsicFqdn;
var data = {
sourceDir: sourceDir,
adminOrigin: config.adminOrigin(),
vhost: vhost,
vhost: app.fqdn,
hasIPv6: config.hasIPv6(),
port: app.httpPort,
endpoint: endpoint,
@@ -346,10 +338,10 @@ function configureAppInternal(app, bundle, callback) {
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', vhost, nginxConfigFilename, data);
debug('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', vhost, safe.error.message);
debug('Error creating nginx config for "%s" : %s', app.fqdn, safe.error.message);
return callback(safe.error);
}
@@ -372,11 +364,9 @@ function unconfigureApp(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var vhost = app.altDomain || app.intrinsicFqdn;
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
if (!safe.fs.unlinkSync(nginxConfigFilename)) {
if (safe.error.code !== 'ENOENT') debug('Error removing nginx configuration of "%s": %s', vhost, safe.error.message);
if (safe.error.code !== 'ENOENT') debug('Error removing nginx configuration of "%s": %s', app.fqdn, safe.error.message);
return callback(null);
}
@@ -392,21 +382,22 @@ function renewAll(auditSource, callback) {
apps.getAll(function (error, allApps) {
if (error) return callback(error);
allApps.push({ domain: config.adminDomain(), intrinsicFqdn: config.adminFqdn() }); // inject fake webadmin app
allApps.push({ domain: config.adminDomain(), fqdn: config.adminFqdn() }); // inject fake webadmin app
async.eachSeries(allApps, function (app, iteratorCallback) {
ensureCertificate(app, auditSource, function (error, bundle) {
if (error) return iteratorCallback(error); // this can happen if cloudron is not setup yet
if (bundle.reason !== 'new-le' && bundle.reason !== 'fallback') return iteratorCallback();
// reconfigure for the case where we got a renewed cert after fallback
var configureFunc = app.intrinsicFqdn === config.adminFqdn() ?
var configureFunc = app.fqdn === config.adminFqdn() ?
configureAdminInternal.bind(null, bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn())
: configureAppInternal.bind(null, app, bundle);
configureFunc(function (ignoredError) {
if (ignoredError) debug('fallbackExpiredCertificates: error reconfiguring app', ignoredError);
platform.handleCertChanged(app.intrinsicFqdn);
platform.handleCertChanged(app.fqdn);
iteratorCallback(); // move to next app
});
+56
View File
@@ -0,0 +1,56 @@
'use strict';
exports = module.exports = {
scope: scope,
websocketAuth: websocketAuth
};
var accesscontrol = require('../accesscontrol.js'),
assert = require('assert'),
HttpError = require('connect-lastmile').HttpError,
passport = require('passport');
// The scope middleware provides an auth middleware for routes.
//
// It is used for API routes, which are authenticated using accesstokens.
// Those accesstokens carry OAuth scopes and the middleware takes the required
// scope as an argument and will verify the accesstoken against it.
//
// See server.js:
// var profileScope = routes.oauth2.scope('profile');
//
function scope(requestedScope) {
assert.strictEqual(typeof requestedScope, 'string');
var requestedScopes = requestedScope.split(',');
return [
passport.authenticate(['bearer'], { session: false }),
function (req, res, next) {
var error = accesscontrol.validateRequestedScopes(req.authInfo || null, requestedScopes);
if (error) return next(new HttpError(403, error.message));
next();
}
];
}
function websocketAuth(requestedScopes, req, res, next) {
assert(Array.isArray(requestedScopes));
if (typeof req.query.access_token !== 'string') return next(new HttpError(401, 'Unauthorized'));
accesscontrol.accessTokenAuth(req.query.access_token, function (error, user, info) {
if (error) return next(new HttpError(500, error.message));
if (!user) return next(new HttpError(401, 'Unauthorized'));
req.user = user;
req.authInfo = info;
var e = accesscontrol.validateRequestedScopes(req.authInfo, requestedScopes);
if (e) return next(new HttpError(401, e.message));
next();
});
}
+22 -36
View File
@@ -3,6 +3,7 @@
exports = module.exports = {
getApp: getApp,
getApps: getApps,
getAllByUser: getAllByUser,
getAppIcon: getAppIcon,
installApp: installApp,
configureApp: configureApp,
@@ -42,34 +43,6 @@ function auditSource(req) {
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
function removeInternalAppFields(app) {
return {
id: app.id,
appStoreId: app.appStoreId,
installationState: app.installationState,
installationProgress: app.installationProgress,
runState: app.runState,
health: app.health,
location: app.location,
domain: app.domain,
accessRestriction: app.accessRestriction,
manifest: app.manifest,
portBindings: app.portBindings,
iconUrl: app.iconUrl,
fqdn: app.fqdn,
memoryLimit: app.memoryLimit,
altDomain: app.altDomain,
cnameTarget: app.cnameTarget,
xFrameOptions: app.xFrameOptions,
sso: app.sso,
debugMode: app.debugMode,
robotsTxt: app.robotsTxt,
enableBackup: app.enableBackup,
creationTime: app.creationTime.toISOString(),
updateTime: app.updateTime.toISOString()
};
}
function getApp(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
@@ -77,18 +50,29 @@ function getApp(req, res, next) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, removeInternalAppFields(app)));
next(new HttpSuccess(200, apps.removeInternalAppFields(app)));
});
}
function getApps(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
var func = req.user.admin ? apps.getAll : apps.getAllByUser.bind(null, req.user);
func(function (error, allApps) {
apps.getAll(function (error, allApps) {
if (error) return next(new HttpError(500, error));
allApps = allApps.map(removeInternalAppFields);
allApps = allApps.map(apps.removeInternalAppFields);
next(new HttpSuccess(200, { apps: allApps }));
});
}
function getAllByUser(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
apps.getAllByUser(req.user, function (error, allApps) {
if (error) return next(new HttpError(500, error));
allApps = allApps.map(apps.removeInternalAppFields);
next(new HttpSuccess(200, { apps: allApps }));
});
@@ -134,9 +118,6 @@ function installApp(req, res, next) {
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
// falsy value in altDomain unsets it
if (data.altDomain && typeof data.altDomain !== 'string') return next(new HttpError(400, 'altDomain must be a string'));
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
if ('sso' in data && typeof data.sso !== 'boolean') return next(new HttpError(400, 'sso must be a boolean'));
@@ -181,7 +162,6 @@ function configureApp(req, res, next) {
if (!data.cert && data.key) return next(new HttpError(400, 'cert must be provided'));
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
if (data.altDomain && typeof data.altDomain !== 'string') return next(new HttpError(400, 'altDomain must be a string'));
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
if ('enableBackup' in data && typeof data.enableBackup !== 'boolean') return next(new HttpError(400, 'enableBackup must be a boolean'));
@@ -190,6 +170,8 @@ function configureApp(req, res, next) {
if (data.robotsTxt && typeof data.robotsTxt !== 'string') return next(new HttpError(400, 'robotsTxt must be a string'));
if ('mailboxName' in data && typeof data.mailboxName !== 'string') return next(new HttpError(400, 'mailboxName must be a string'));
debug('Configuring app id:%s data:%j', req.params.id, data);
apps.configure(req.params.id, data, auditSource(req), function (error) {
@@ -243,9 +225,13 @@ function cloneApp(req, res, next) {
apps.clone(req.params.id, data, auditSource(req), function (error, result) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.BILLING_REQUIRED) return next(new HttpError(402, 'Billing required'));
if (error && error.reason === AppsError.BAD_CERTIFICATE) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
+12 -10
View File
@@ -5,9 +5,9 @@ exports = module.exports = {
get: get,
del: del,
getAll: getAll,
addClientToken: addClientToken,
getClientTokens: getClientTokens,
delClientTokens: delClientTokens,
addToken: addToken,
getTokens: getTokens,
delTokens: delTokens,
delToken: delToken
};
@@ -72,36 +72,38 @@ function getAll(req, res, next) {
});
}
function addClientToken(req, res, next) {
function addToken(req, res, next) {
assert.strictEqual(typeof req.params.clientId, 'string');
assert.strictEqual(typeof req.user, 'object');
assert.strictEqual(typeof req.body, 'object');
var expiresAt = req.query.expiresAt ? parseInt(req.query.expiresAt, 10) : Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
var data = req.body;
var expiresAt = data.expiresAt ? parseInt(data.expiresAt, 10) : Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
if (isNaN(expiresAt) || expiresAt <= Date.now()) return next(new HttpError(400, 'expiresAt must be a timestamp in the future'));
clients.addClientTokenByUserId(req.params.clientId, req.user.id, expiresAt, function (error, result) {
clients.addTokenByUserId(req.params.clientId, req.user.id, expiresAt, 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));
next(new HttpSuccess(201, { token: result }));
});
}
function getClientTokens(req, res, next) {
function getTokens(req, res, next) {
assert.strictEqual(typeof req.params.clientId, 'string');
assert.strictEqual(typeof req.user, 'object');
clients.getClientTokensByUserId(req.params.clientId, req.user.id, function (error, result) {
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));
next(new HttpSuccess(200, { tokens: result }));
});
}
function delClientTokens(req, res, next) {
function delTokens(req, res, next) {
assert.strictEqual(typeof req.params.clientId, 'string');
assert.strictEqual(typeof req.user, 'object');
clients.delClientTokensByUserId(req.params.clientId, req.user.id, function (error) {
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));
next(new HttpSuccess(204));
+1 -4
View File
@@ -44,10 +44,6 @@ function getConfig(req, res, next) {
cloudron.getConfig(function (error, cloudronConfig) {
if (error) return next(new HttpError(500, error));
if (!req.user.admin) {
cloudronConfig = _.pick(cloudronConfig, 'apiServerOrigin', 'webServerOrigin', 'fqdn', 'adminFqdn', 'version', 'progress', 'isDemo', 'cloudronName', 'provider');
}
next(new HttpSuccess(200, cloudronConfig));
});
}
@@ -89,6 +85,7 @@ function feedback(req, res, next) {
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'));
appstore.sendFeedback(_.extend(req.body, { email: req.user.email, displayName: req.user.displayName }), function (error) {
if (error && error.reason === AppstoreError.BILLING_REQUIRED) return next(new HttpError(402, 'Login to App Store to create support tickets. You can also email support@cloudron.io'));
+15 -10
View File
@@ -4,26 +4,31 @@ exports = module.exports = {
login: login
};
var developer = require('../developer.js'),
var clients = require('../clients.js'),
passport = require('passport'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
HttpSuccess = require('connect-lastmile').HttpSuccess,
speakeasy = require('speakeasy');
function login(req, res, next) {
passport.authenticate('local', function (error, user) {
if (error) return next(new HttpError(500, error));
if (!user) return next(new HttpError(401, 'Invalid credentials'));
developer.issueDeveloperToken(user, auditSource(req), function (error, result) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
if (!user.ghost && user.twoFactorAuthenticationEnabled) {
if (!req.body.totpToken) return next(new HttpError(401, 'A totpToken must be provided'));
let verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken });
if (!verified) return next(new HttpError(401, 'Invalid totpToken'));
}
clients.issueDeveloperToken(user, ip, function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { token: result.token, expiresAt: result.expiresAt }));
next(new HttpSuccess(200, result));
});
})(req, res, next);
})(req, res, next);
}
+22 -27
View File
@@ -5,14 +5,12 @@ exports = module.exports = {
get: get,
getAll: getAll,
update: update,
del: del,
setAdmin: setAdmin
del: del
};
var assert = require('assert'),
domains = require('../domains.js'),
DomainError = domains.DomainError,
DomainsError = domains.DomainsError,
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
@@ -29,10 +27,13 @@ function add(req, res, next) {
if ('tlsConfig' in req.body && typeof req.body.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be a object with a provider string property'));
if (req.body.tlsConfig && (!req.body.tlsConfig.provider || typeof req.body.tlsConfig.provider !== 'string')) return next(new HttpError(400, 'tlsConfig.provider must be a string'));
// some DNS providers like DigitalOcean take a really long time to verify credentials (https://github.com/expressjs/timeout/issues/26)
req.clearTimeout();
domains.add(req.body.domain, req.body.zoneName || '', req.body.provider, req.body.config, req.body.fallbackCertificate || null, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
if (error && error.reason === DomainError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === DomainError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === DomainError.INVALID_PROVIDER) return next(new HttpError(400, error.message));
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));
next(new HttpSuccess(201, { domain: req.body.domain, config: req.body.config }));
@@ -43,12 +44,10 @@ function get(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
domains.get(req.params.domain, function (error, result) {
if (error && error.reason === DomainError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === DomainsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
delete result.fallbackCertificate.key; // do not return the 'key'. in caas, this is private
next(new HttpSuccess(200, result));
next(new HttpSuccess(200, domains.removePrivateFields(result)));
});
}
@@ -56,6 +55,8 @@ function getAll(req, res, next) {
domains.getAll(function (error, result) {
if (error) return next(new HttpError(500, error));
result = result.map(domains.removePrivateFields);
next(new HttpSuccess(200, { domains: result }));
});
}
@@ -66,16 +67,20 @@ function update(req, res, next) {
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider must be an object'));
if (typeof req.body.config !== 'object') return next(new HttpError(400, 'config must be an object'));
if ('zoneName' in req.body && typeof req.body.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be a string'));
if ('fallbackCertificate' in req.body && typeof req.body.fallbackCertificate !== 'object') return next(new HttpError(400, 'fallbackCertificate must be a object with cert and key strings'));
if (req.body.fallbackCertificate && (!req.body.fallbackCertificate.cert || typeof req.body.fallbackCertificate.cert !== 'string')) return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
if (req.body.fallbackCertificate && (!req.body.fallbackCertificate.key || typeof req.body.fallbackCertificate.key !== 'string')) return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
if ('tlsConfig' in req.body && typeof req.body.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be a object with a provider string property'));
if (req.body.tlsConfig && (!req.body.tlsConfig.provider || typeof req.body.tlsConfig.provider !== 'string')) return next(new HttpError(400, 'tlsConfig.provider must be a string'));
domains.update(req.params.domain, req.body.provider, req.body.config, req.body.fallbackCertificate || null, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
if (error && error.reason === DomainError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === DomainError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === DomainError.INVALID_PROVIDER) return next(new HttpError(400, error.message));
// some DNS providers like DigitalOcean take a really long time to verify credentials (https://github.com/expressjs/timeout/issues/26)
req.clearTimeout();
domains.update(req.params.domain, req.body.zoneName || '', req.body.provider, req.body.config, req.body.fallbackCertificate || null, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, 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));
next(new HttpSuccess(204, {}));
@@ -86,21 +91,11 @@ function del(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
domains.del(req.params.domain, function (error) {
if (error && error.reason === DomainError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === DomainError.IN_USE) return next(new HttpError(409, 'Domain is still in use'));
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));
next(new HttpSuccess(204));
});
}
function setAdmin(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
domains.setAdmin(req.params.domain.toLowerCase(), function (error) {
if (error && error.reason === DomainError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}
+5 -1
View File
@@ -15,10 +15,14 @@ function get(req, res, next) {
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
if (req.query.actions && typeof req.query.actions !== 'string') return next(new HttpError(400, 'actions must be a comma separated string'));
if (req.query.action && typeof req.query.action !== 'string') return next(new HttpError(400, 'action must be a string'));
if (req.query.search && typeof req.query.search !== 'string') return next(new HttpError(400, 'search must be a string'));
eventlog.getAllPaged(req.query.action || null, req.query.search || null, page, perPage, function (error, result) {
var actions = req.query.actions ? req.query.actions.split(',').map(function (s) { return s.trim(); }) : [];
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));
next(new HttpSuccess(200, { eventlogs: result }));
+7 -7
View File
@@ -13,7 +13,7 @@ var assert = require('assert'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
groups = require('../groups.js'),
GroupError = groups.GroupError;
GroupsError = groups.GroupsError;
function create(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
@@ -21,8 +21,8 @@ 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 === GroupError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === GroupError.ALREADY_EXISTS) return next(new HttpError(409, 'Already exists'));
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));
var groupInfo = {
@@ -38,7 +38,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 === GroupError.NOT_FOUND) return next(new HttpError(404, 'No such group'));
if (error && error.reason === GroupsError.NOT_FOUND) return next(new HttpError(404, 'No such group'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, result));
@@ -52,7 +52,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 === GroupError.NOT_FOUND) return next(new HttpError(404, 'Invalid group or user id'));
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));
next(new HttpSuccess(200));
@@ -71,8 +71,8 @@ function remove(req, res, next) {
assert.strictEqual(typeof req.params.groupId, 'string');
groups.remove(req.params.groupId, function (error) {
if (error && error.reason === GroupError.NOT_FOUND) return next(new HttpError(404, 'Group not found'));
if (error && error.reason === GroupError.NOT_ALLOWED) return next(new HttpError(409, 'Group deletion not allowed'));
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));
next(new HttpSuccess(204));
+3 -1
View File
@@ -1,6 +1,7 @@
'use strict';
exports = module.exports = {
accesscontrol: require('./accesscontrol.js'),
apps: require('./apps.js'),
backups: require('./backups.js'),
caas: require('./caas.js'),
@@ -18,5 +19,6 @@ exports = module.exports = {
sysadmin: require('./sysadmin.js'),
settings: require('./settings.js'),
ssh: require('./ssh.js'),
user: require('./user.js')
user: require('./user.js'),
users: require('./users.js')
};
+134 -39
View File
@@ -1,10 +1,11 @@
'use strict';
exports = module.exports = {
get: get,
add: add,
del: del,
getDomain: getDomain,
addDomain: addDomain,
getDomainStats: getDomainStats,
updateDomain: updateDomain,
removeDomain: removeDomain,
getStatus: getStatus,
@@ -16,16 +17,19 @@ exports = module.exports = {
sendTestMail: sendTestMail,
getMailboxes: getMailboxes,
getUserMailbox: getUserMailbox,
enableUserMailbox: enableUserMailbox,
disableUserMailbox: disableUserMailbox,
getMailbox: getMailbox,
addMailbox: addMailbox,
updateMailbox: updateMailbox,
removeMailbox: removeMailbox,
listAliases: listAliases,
getAliases: getAliases,
setAliases: setAliases,
getLists: getLists,
getList: getList,
addList: addList,
updateList: updateList,
removeList: removeList
};
@@ -33,12 +37,16 @@ var assert = require('assert'),
mail = require('../mail.js'),
MailError = mail.MailError,
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
HttpSuccess = require('connect-lastmile').HttpSuccess,
middleware = require('../middleware/index.js'),
url = require('url');
function get(req, res, next) {
var mailProxy = middleware.proxy(url.parse('http://127.0.0.1:2020'));
function getDomain(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.get(req.params.domain, function (error, result) {
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));
@@ -46,12 +54,12 @@ function get(req, res, next) {
});
}
function add(req, res, next) {
function addDomain(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
mail.add(req.body.domain, function (error) {
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));
@@ -60,13 +68,39 @@ function add(req, res, next) {
});
}
function del(req, res, next) {
function getDomainStats(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.del(req.params.domain, function (error) {
var parsedUrl = url.parse(req.url, true /* parseQueryString */);
delete parsedUrl.query['access_token'];
delete req.headers['authorization'];
delete req.headers['cookies'];
req.url = url.format({ pathname: req.params.domain, query: parsedUrl.query });
mailProxy(req, res, next);
}
function updateDomain(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.domain, 'string');
mail.updateDomain(req.params.domain, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
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));
next(new HttpSuccess(204));
});
}
@@ -74,6 +108,9 @@ function del(req, res, next) {
function getStatus(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
// can take a while to query all the DNS entries
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));
@@ -101,13 +138,14 @@ function setCatchAllAddress(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.body, 'object');
if (!req.body.address || !Array.isArray(req.body.address)) return next(new HttpError(400, 'address array is required'));
if (!req.body.addresses) return next(new HttpError(400, 'addresses is required'));
if (!Array.isArray(req.body.addresses)) return next(new HttpError(400, 'addresses must be an array of strings'));
for (var i = 0; i < req.body.address.length; i++) {
if (typeof req.body.address[i] !== 'string') return next(new HttpError(400, 'address must be an array of string'));
for (var i = 0; i < req.body.addresses.length; i++) {
if (typeof req.body.addresses[i] !== 'string') return next(new HttpError(400, 'addresses must be an array of strings'));
}
mail.setCatchAllAddress(req.params.domain, req.body.address, function (error) {
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));
@@ -144,6 +182,7 @@ function setMailEnabled(req, res, next) {
mail.setMailEnabled(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 && error.reason === MailError.BILLING_REQUIRED) return next(new HttpError(402, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
@@ -175,11 +214,11 @@ function getMailboxes(req, res, next) {
});
}
function getUserMailbox(req, res, next) {
function getMailbox(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.userId, 'string');
assert.strictEqual(typeof req.params.name, 'string');
mail.getUserMailbox(req.params.domain, req.params.userId, function (error, result) {
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));
@@ -187,36 +226,65 @@ function getUserMailbox(req, res, next) {
});
}
function enableUserMailbox(req, res, next) {
function addMailbox(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.userId, 'string');
mail.enableUserMailbox(req.params.domain, req.params.userId, function (error) {
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be a string'));
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, 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 HttpSuccess(201, {}));
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));
next(new HttpSuccess(201, {}));
});
}
function disableUserMailbox(req, res, next) {
function updateMailbox(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.userId, 'string');
assert.strictEqual(typeof req.params.name, 'string');
mail.disableUserMailbox(req.params.domain, req.params.userId, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpSuccess(201, {}));
if (typeof req.body.userId !== 'string') return next(new HttpError(400, 'userId must be a string'));
mail.updateMailbox(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));
next(new HttpSuccess(204));
});
}
function removeMailbox(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.name, 'string');
mail.removeMailbox(req.params.name, req.params.domain, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, {}));
});
}
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));
next(new HttpSuccess(200, { aliases: result }));
});
}
function getAliases(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.userId, 'string');
assert.strictEqual(typeof req.params.name, 'string');
mail.getAliases(req.params.domain, req.params.userId, function (error, result) {
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));
@@ -226,7 +294,7 @@ function getAliases(req, res, next) {
function setAliases(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.userId, 'string');
assert.strictEqual(typeof req.params.name, 'string');
assert.strictEqual(typeof req.body, 'object');
if (!Array.isArray(req.body.aliases)) return next(new HttpError(400, 'aliases must be an array'));
@@ -235,8 +303,10 @@ function setAliases(req, res, next) {
if (typeof req.body.aliases[i] !== 'string') return next(new HttpError(400, 'alias must be a string'));
}
mail.setAliases(req.params.domain, req.params.userId, req.body.aliases, function (error) {
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));
next(new HttpSuccess(202));
@@ -256,9 +326,9 @@ function getLists(req, res, next) {
function getList(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.groupId, 'string');
assert.strictEqual(typeof req.params.name, 'string');
mail.getList(req.params.domain, req.params.groupId, function (error, result) {
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));
@@ -270,22 +340,47 @@ function addList(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.groupId !== 'string') return next(new HttpError(400, 'groupId must be a string'));
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be a string'));
if (!Array.isArray(req.body.members)) return next(new HttpError(400, 'members must be a string'));
mail.addList(req.params.domain, req.body.groupId, function (error) {
for (var i = 0; i < req.body.members.length; i++) {
if (typeof req.body.members[i] !== 'string') return next(new HttpError(400, 'member must be a string'));
}
mail.addList(req.body.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.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));
next(new HttpSuccess(201, {}));
});
}
function updateList(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.name, 'string');
if (!Array.isArray(req.body.members)) return next(new HttpError(400, 'members must be a string'));
for (var i = 0; i < req.body.members.length; i++) {
if (typeof req.body.members[i] !== 'string') return next(new HttpError(400, 'member must be a string'));
}
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));
next(new HttpSuccess(204));
});
}
function removeList(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.groupId, 'string');
assert.strictEqual(typeof req.params.name, 'string');
mail.removeList(req.params.domain, req.params.groupId, function (error) {
mail.removeList(req.params.domain, req.params.name, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
+223 -262
View File
@@ -1,14 +1,33 @@
'use strict';
var apps = require('../apps'),
exports = module.exports = {
initialize: initialize,
uninitialize: uninitialize,
loginForm: loginForm,
login: login,
logout: logout,
sessionCallback: sessionCallback,
passwordResetRequestSite: passwordResetRequestSite,
passwordResetRequest: passwordResetRequest,
passwordSentSite: passwordSentSite,
passwordResetSite: passwordResetSite,
passwordReset: passwordReset,
accountSetupSite: accountSetupSite,
accountSetup: accountSetup,
authorization: authorization,
token: token,
csrf: csrf
};
var accesscontrol = require('../accesscontrol.js'),
apps = require('../apps.js'),
assert = require('assert'),
auth = require('../auth.js'),
authcodedb = require('../authcodedb'),
authcodedb = require('../authcodedb.js'),
clients = require('../clients'),
ClientsError = clients.ClientsError,
config = require('../config.js'),
constants = require('../constants'),
DatabaseError = require('../databaseerror'),
constants = require('../constants.js'),
DatabaseError = require('../databaseerror.js'),
debug = require('debug')('box:routes/oauth2'),
eventlog = require('../eventlog.js'),
hat = require('hat'),
@@ -18,124 +37,138 @@ var apps = require('../apps'),
passport = require('passport'),
querystring = require('querystring'),
session = require('connect-ensure-login'),
settings = require('../settings'),
tokendb = require('../tokendb'),
settings = require('../settings.js'),
speakeasy = require('speakeasy'),
tokendb = require('../tokendb.js'),
url = require('url'),
user = require('../user.js'),
UserError = user.UserError,
users = require('../users.js'),
UsersError = users.UsersError,
util = require('util'),
_ = require('underscore');
function auditSource(req, appId) {
// appObject is optional here
function auditSource(req, appId, appObject) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
return { authType: 'oauth', ip: ip, appId: appId };
return { authType: 'oauth', ip: ip, appId: appId, app: appObject };
}
// create OAuth 2.0 server
var gServer = oauth2orize.createServer();
var gServer = null;
// Register serialialization and deserialization functions.
//
// The client id is stored in the session and can thus be retrieved for each
// step in the oauth flow transaction, which involves multiple http requests.
function initialize() {
assert(gServer === null);
gServer.serializeClient(function (client, callback) {
return callback(null, client.id);
});
gServer = oauth2orize.createServer();
gServer.deserializeClient(function (id, callback) {
clients.get(id, callback);
});
// Register serialialization and deserialization functions.
//
// The client id is stored in the session and can thus be retrieved for each
// step in the oauth flow transaction, which involves multiple http requests.
// Register supported grant types.
// Grant authorization codes. The callback takes the `client` requesting
// authorization, the `redirectURI` (which is used as a verifier in the
// subsequent exchange), the authenticated `user` granting access, and
// their response, which contains approved scope, duration, etc. as parsed by
// the application. The application issues a code, which is bound to these
// values, and will be exchanged for an access token.
gServer.grant(oauth2orize.grant.code({ scopeSeparator: ',' }, function (client, redirectURI, user, ares, callback) {
debug('grant code:', client.id, redirectURI, user.id, ares);
var code = hat(256);
var expiresAt = Date.now() + 60 * 60000; // 1 hour
authcodedb.add(code, client.id, user.id, expiresAt, function (error) {
if (error) return callback(error);
debug('grant code: new auth code for client %s code %s', client.id, code);
callback(null, code);
gServer.serializeClient(function (client, callback) {
return callback(null, client.id);
});
}));
gServer.grant(oauth2orize.grant.token({ scopeSeparator: ',' }, function (client, user, ares, callback) {
debug('grant token:', client.id, user.id, ares);
var token = tokendb.generateToken();
var expires = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
tokendb.add(token, user.id, client.id, expires, client.scope, function (error) {
if (error) return callback(error);
debug('grant token: new access token for client %s token %s', client.id, token);
callback(null, token);
gServer.deserializeClient(function (id, callback) {
clients.get(id, callback);
});
}));
// Exchange authorization codes for access tokens. The callback accepts the
// `client`, which is exchanging `code` and any `redirectURI` from the
// authorization request for verification. If these values are validated, the
// application issues an access token on behalf of the user who authorized the
// code.
// Register supported grant types.
gServer.exchange(oauth2orize.exchange.code(function (client, code, redirectURI, callback) {
debug('exchange:', client, code, redirectURI);
// Grant authorization codes. The callback takes the `client` requesting
// authorization, the `redirectURI` (which is used as a verifier in the
// subsequent exchange), the authenticated `user` granting access, and
// their response, which contains approved scope, duration, etc. as parsed by
// the application. The application issues a code, which is bound to these
// values, and will be exchanged for an access token.
authcodedb.get(code, function (error, authCode) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
if (client.id !== authCode.clientId) return callback(null, false);
gServer.grant(oauth2orize.grant.code({ scopeSeparator: ',' }, function (client, redirectURI, user, ares, callback) {
debug('grant code:', client.id, redirectURI, user.id, ares);
authcodedb.del(code, function (error) {
if(error) return callback(error);
var code = hat(256);
var expiresAt = Date.now() + 60 * 60000; // 1 hour
var token = tokendb.generateToken();
var expires = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
authcodedb.add(code, client.id, user.id, expiresAt, function (error) {
if (error) return callback(error);
tokendb.add(token, authCode.userId, authCode.clientId, expires, client.scope, function (error) {
if (error) return callback(error);
debug('grant code: new auth code for client %s code %s', client.id, code);
debug('exchange: new access token for client %s token %s', client.id, token);
callback(null, code);
});
}));
callback(null, token);
gServer.grant(oauth2orize.grant.token({ scopeSeparator: ',' }, function (client, user, ares, callback) {
debug('grant token:', client.id, user.id, ares);
var token = tokendb.generateToken();
var expires = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
var scope = accesscontrol.normalizeScope(user.scope, client.scope);
tokendb.add(token, user.id, client.id, expires, scope, function (error) {
if (error) return callback(error);
debug('grant token: new access token for client %s token %s', client.id, token);
callback(null, token);
});
}));
// Exchange authorization codes for access tokens. The callback accepts the
// `client`, which is exchanging `code` and any `redirectURI` from the
// authorization request for verification. If these values are validated, the
// application issues an access token on behalf of the user who authorized the
// code.
gServer.exchange(oauth2orize.exchange.code(function (client, code, redirectURI, callback) {
debug('exchange:', client, code, redirectURI);
authcodedb.get(code, function (error, authCode) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
if (client.id !== authCode.clientId) return callback(null, false);
authcodedb.del(code, function (error) {
if(error) return callback(error);
var token = tokendb.generateToken();
var expires = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
var scope = accesscontrol.canonicalScope(client.scope);
tokendb.add(token, authCode.userId, authCode.clientId, expires, client.scope, function (error) {
if (error) return callback(error);
debug('exchange: new access token for client %s token %s', client.id, token);
callback(null, token);
});
});
});
});
}));
}));
// overwrite the session.ensureLoggedIn to not use res.redirect() due to a chrome bug not sending cookies on redirects
session.ensureLoggedIn = function (redirectTo) {
assert.strictEqual(typeof redirectTo, 'string');
// overwrite the session.ensureLoggedIn to not use res.redirect() due to a chrome bug not sending cookies on redirects
session.ensureLoggedIn = function (redirectTo) {
assert.strictEqual(typeof redirectTo, 'string');
return function (req, res, next) {
if (!req.isAuthenticated || !req.isAuthenticated()) {
if (req.session) {
req.session.returnTo = req.originalUrl || req.url;
return function (req, res, next) {
if (!req.isAuthenticated || !req.isAuthenticated()) {
if (req.session) {
req.session.returnTo = req.originalUrl || req.url;
}
res.status(200).send(util.format('<script>window.location.href = "%s";</script>', redirectTo));
} else {
next();
}
res.status(200).send(util.format('<script>window.location.href = "%s";</script>', redirectTo));
} else {
next();
}
};
};
};
}
function uninitialize() {
gServer = null;
}
function renderTemplate(res, template, data) {
assert.strictEqual(typeof res, 'object');
@@ -231,15 +264,15 @@ function loginForm(req, res) {
if (error) return sendError(req, res, 'Unknown OAuth client');
switch (result.type) {
case clients.TYPE_BUILT_IN: return renderBuiltIn();
case clients.TYPE_EXTERNAL: return render(result.appId, '/api/v1/cloudron/avatar');
default: break;
case clients.TYPE_BUILT_IN: return renderBuiltIn();
case clients.TYPE_EXTERNAL: return render(result.appId, '/api/v1/cloudron/avatar');
default: break;
}
apps.get(result.appId, function (error, result) {
if (error) return sendErrorPageOrRedirect(req, res, 'Unknown Application for those OAuth credentials');
var applicationName = result.altDomain || result.intrinsicFqdn;
var applicationName = result.fqdn;
render(applicationName, '/api/v1/apps/' + result.id + '/icon');
});
});
@@ -253,6 +286,19 @@ function login(req, res) {
passport.authenticate('local', {
failureRedirect: '/api/v1/session/login?' + failureQuery
})(req, res, function () {
if (!req.user.ghost && req.user.twoFactorAuthenticationEnabled) {
if (!req.body.totpToken) {
let failureQuery = querystring.stringify({ error: 'A 2FA token is required', returnTo: returnTo });
return res.redirect('/api/v1/session/login?' + failureQuery);
}
let verified = speakeasy.totp.verify({ secret: req.user.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken });
if (!verified) {
let failureQuery = querystring.stringify({ error: 'The 2FA token is invalid', returnTo: returnTo });
return res.redirect('/api/v1/session/login?' + failureQuery);
}
}
res.redirect(returnTo);
});
}
@@ -285,8 +331,8 @@ function passwordResetRequest(req, res, next) {
debug('passwordResetRequest: email or username %s.', req.body.identifier);
user.resetPasswordByIdentifier(req.body.identifier, function (error) {
if (error && error.reason !== UserError.NOT_FOUND) {
users.resetPasswordByIdentifier(req.body.identifier, function (error) {
if (error && error.reason !== UsersError.NOT_FOUND) {
console.error(error);
return sendErrorPageOrRedirect(req, res, 'User not found');
}
@@ -314,7 +360,7 @@ function renderAccountSetupSite(res, req, userObject, error) {
function accountSetupSite(req, res) {
if (!req.query.reset_token) return sendError(req, res, 'Missing Reset Token');
user.getByResetToken(req.query.reset_token, function (error, userObject) {
users.getByResetToken(req.query.reset_token, function (error, userObject) {
if (error) return sendError(req, res, 'Invalid Reset Token');
renderAccountSetupSite(res, req, userObject, '');
@@ -332,26 +378,26 @@ function accountSetup(req, res, next) {
debug('acountSetup: with token %s.', req.body.resetToken);
user.getByResetToken(req.body.resetToken, function (error, userObject) {
users.getByResetToken(req.body.resetToken, function (error, userObject) {
if (error) return sendError(req, res, 'Invalid Reset Token');
var data = _.pick(req.body, 'username', 'displayName');
user.update(userObject.id, data, auditSource(req), function (error) {
if (error && error.reason === UserError.ALREADY_EXISTS) return renderAccountSetupSite(res, req, userObject, 'Username already exists');
if (error && error.reason === UserError.BAD_FIELD) return renderAccountSetupSite(res, req, userObject, error.message);
if (error && error.reason === UserError.NOT_FOUND) return renderAccountSetupSite(res, req, userObject, 'No such user');
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) return next(new HttpError(500, error));
userObject.username = req.body.username;
userObject.displayName = req.body.displayName;
// setPassword clears the resetToken
user.setPassword(userObject.id, req.body.password, function (error, result) {
if (error && error.reason === UserError.BAD_FIELD) return renderAccountSetupSite(res, req, userObject, error.message);
users.setPassword(userObject.id, req.body.password, function (error, result) {
if (error && error.reason === UsersError.BAD_FIELD) return renderAccountSetupSite(res, req, userObject, error.message);
if (error) return next(new HttpError(500, error));
res.redirect(util.format('%s?accessToken=%s&expiresAt=%s', config.adminOrigin(), result.token, result.expiresAt));
res.redirect(config.adminOrigin());
});
});
});
@@ -361,7 +407,7 @@ function accountSetup(req, res, next) {
function passwordResetSite(req, res, next) {
if (!req.query.reset_token) return next(new HttpError(400, 'Missing reset_token'));
user.getByResetToken(req.query.reset_token, function (error, user) {
users.getByResetToken(req.query.reset_token, function (error, user) {
if (error) return next(new HttpError(401, 'Invalid reset_token'));
renderTemplate(res, 'password_reset', {
@@ -382,17 +428,17 @@ function passwordReset(req, res, next) {
debug('passwordReset: with token %s.', req.body.resetToken);
user.getByResetToken(req.body.resetToken, function (error, userObject) {
users.getByResetToken(req.body.resetToken, function (error, userObject) {
if (error) return next(new HttpError(401, 'Invalid resetToken'));
if (!userObject.username) return next(new HttpError(401, 'No username set'));
// setPassword clears the resetToken
user.setPassword(userObject.id, req.body.password, function (error, result) {
if (error && error.reason === UserError.BAD_FIELD) return next(new HttpError(406, error.message));
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) return next(new HttpError(500, error));
res.redirect(util.format('%s?accessToken=%s&expiresAt=%s', config.adminOrigin(), result.token, result.expiresAt));
res.redirect(config.adminOrigin());
});
});
}
@@ -401,13 +447,14 @@ function passwordReset(req, res, next) {
// The callback page takes the redirectURI and the authCode and redirects the browser accordingly
//
// -> GET /api/v1/session/callback
var callback = [
session.ensureLoggedIn('/api/v1/session/login'),
function (req, res) {
renderTemplate(res, 'callback', { callbackServer: req.query.redirectURI });
}
];
function sessionCallback() {
return [
session.ensureLoggedIn('/api/v1/session/login'),
function (req, res) {
renderTemplate(res, 'callback', { callbackServer: req.query.redirectURI });
}
];
}
// The authorization endpoint is the entry point for an OAuth login.
//
@@ -419,54 +466,55 @@ var callback = [
// - Then it will redirect the browser to the given <callbackURL> containing the authcode in the query
//
// -> GET /api/v1/oauth/dialog/authorize
var authorization = [
function (req, res, next) {
if (!req.query.redirect_uri) return sendErrorPageOrRedirect(req, res, 'Invalid request. redirect_uri query param is not set.');
if (!req.query.client_id) return sendErrorPageOrRedirect(req, res, 'Invalid request. client_id query param is not set.');
if (!req.query.response_type) return sendErrorPageOrRedirect(req, res, 'Invalid request. response_type query param is not set.');
if (req.query.response_type !== 'code' && req.query.response_type !== 'token') return sendErrorPageOrRedirect(req, res, 'Invalid request. Only token and code response types are supported.');
function authorization() {
return [
function (req, res, next) {
if (!req.query.redirect_uri) return sendErrorPageOrRedirect(req, res, 'Invalid request. redirect_uri query param is not set.');
if (!req.query.client_id) return sendErrorPageOrRedirect(req, res, 'Invalid request. client_id query param is not set.');
if (!req.query.response_type) return sendErrorPageOrRedirect(req, res, 'Invalid request. response_type query param is not set.');
if (req.query.response_type !== 'code' && req.query.response_type !== 'token') return sendErrorPageOrRedirect(req, res, 'Invalid request. Only token and code response types are supported.');
session.ensureLoggedIn('/api/v1/session/login?returnTo=' + req.query.redirect_uri)(req, res, next);
},
gServer.authorization({}, function (clientId, redirectURI, callback) {
debug('authorization: client %s with callback to %s.', clientId, redirectURI);
session.ensureLoggedIn('/api/v1/session/login?returnTo=' + req.query.redirect_uri)(req, res, next);
},
gServer.authorization({}, function (clientId, redirectURI, callback) {
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) return callback(error);
clients.get(clientId, function (error, client) {
if (error && error.reason === ClientsError.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
var redirectPath = url.parse(redirectURI).path;
var redirectOrigin = client.redirectURI;
// ignore the origin passed into form the client, but use the one from the clientdb
var redirectPath = url.parse(redirectURI).path;
var redirectOrigin = client.redirectURI;
callback(null, client, '/api/v1/session/callback?redirectURI=' + encodeURIComponent(url.resolve(redirectOrigin, redirectPath)));
});
}),
function (req, res, next) {
// Handle our different types of oauth clients
var type = req.oauth2.client.type;
if (type === clients.TYPE_EXTERNAL || type === clients.TYPE_BUILT_IN) {
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, req.oauth2.client.appId), { userId: req.oauth2.user.id });
return next();
}
apps.get(req.oauth2.client.appId, function (error, appObject) {
if (error) return sendErrorPageOrRedirect(req, res, 'Invalid request. Unknown app for this client_id.');
apps.hasAccessTo(appObject, req.oauth2.user, function (error, access) {
if (error) return sendError(req, res, 'Internal error');
if (!access) return sendErrorPageOrRedirect(req, res, 'No access to this app.');
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, appObject.id), { userId: req.oauth2.user.id });
next();
callback(null, client, '/api/v1/session/callback?redirectURI=' + encodeURIComponent(url.resolve(redirectOrigin, redirectPath)));
});
});
},
gServer.decision({ loadTransaction: false })
];
}),
function (req, res, next) {
// Handle our different types of oauth clients
var type = req.oauth2.client.type;
if (type === clients.TYPE_EXTERNAL || type === clients.TYPE_BUILT_IN) {
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, req.oauth2.client.appId), { userId: req.oauth2.user.id, user: users.removePrivateFields(req.oauth2.user) });
return next();
}
apps.get(req.oauth2.client.appId, function (error, appObject) {
if (error) return sendErrorPageOrRedirect(req, res, 'Invalid request. Unknown app for this client_id.');
apps.hasAccessTo(appObject, req.oauth2.user, function (error, access) {
if (error) return sendError(req, res, 'Internal error');
if (!access) return sendErrorPageOrRedirect(req, res, 'No access to this app.');
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, appObject.id, appObject), { userId: req.oauth2.user.id, user: users.removePrivateFields(req.oauth2.user) });
next();
});
});
},
gServer.decision({ loadTransaction: false })
];
}
// The token endpoint allows an OAuth client to exchange an authcode with an accesstoken.
//
@@ -475,109 +523,22 @@ var authorization = [
// An authcode is only good for one such exchange to an accesstoken.
//
// -> POST /api/v1/oauth/token
var token = [
passport.authenticate(['basic', 'oauth2-client-password'], { session: false }),
gServer.token(),
gServer.errorHandler()
];
// tests if all requestedScopes are attached to the request
function validateRequestedScopes(req, requestedScopes) {
assert.strictEqual(typeof req, 'object');
assert(Array.isArray(requestedScopes));
if (!req.authInfo || !req.authInfo.scope) return new Error('No scope found');
var scopes = req.authInfo.scope.split(',');
// check for roles separately
if (requestedScopes.indexOf(clients.SCOPE_ROLE_SDK) !== -1 && scopes.indexOf(clients.SCOPE_ROLE_SDK) === -1) {
return new Error('Missing required scope role "' + clients.SCOPE_ROLE_SDK + '"');
}
if (scopes.indexOf('*') !== -1) return null;
for (var i = 0; i < requestedScopes.length; ++i) {
if (scopes.indexOf(requestedScopes[i]) === -1) {
debug('scope: missing scope "%s".', requestedScopes[i]);
return new Error('Missing required scope "' + requestedScopes[i] + '"');
}
}
return null;
}
// The scope middleware provides an auth middleware for routes.
//
// It is used for API routes, which are authenticated using accesstokens.
// Those accesstokens carry OAuth scopes and the middleware takes the required
// scope as an argument and will verify the accesstoken against it.
//
// See server.js:
// var profileScope = routes.oauth2.scope('profile');
//
function scope(requestedScope) {
assert.strictEqual(typeof requestedScope, 'string');
var requestedScopes = requestedScope.split(',');
debug('scope: add routes with requested scopes', requestedScopes);
function token() {
return [
passport.authenticate(['bearer'], { session: false }),
function (req, res, next) {
var error = validateRequestedScopes(req, requestedScopes);
if (error) return next(new HttpError(401, error.message));
next();
}
passport.authenticate(['basic', 'oauth2-client-password'], { session: false }),
gServer.token(),
gServer.errorHandler()
];
}
function websocketAuth(requestedScopes, req, res, next) {
assert(Array.isArray(requestedScopes));
if (typeof req.query.access_token !== 'string') return next(new HttpError(401, 'Unauthorized'));
auth.accessTokenAuth(req.query.access_token, function (error, user, info) {
if (error) return next(new HttpError(500, error.message));
if (!user) return next(new HttpError(401, 'Unauthorized'));
req.user = user;
req.authInfo = info;
var error = validateRequestedScopes(req, requestedScopes);
if (error) return next(new HttpError(401, error.message));
next();
});
}
// Cross-site request forgery protection middleware for login form
var csrf = [
middleware.csrf(),
function (err, req, res, next) {
if (err.code !== 'EBADCSRFTOKEN') return next(err);
function csrf() {
return [
middleware.csrf(),
function (err, req, res, next) {
if (err.code !== 'EBADCSRFTOKEN') return next(err);
sendErrorPageOrRedirect(req, res, 'Form expired');
}
];
exports = module.exports = {
loginForm: loginForm,
login: login,
logout: logout,
callback: callback,
passwordResetRequestSite: passwordResetRequestSite,
passwordResetRequest: passwordResetRequest,
passwordSentSite: passwordSentSite,
passwordResetSite: passwordResetSite,
passwordReset: passwordReset,
accountSetupSite: accountSetupSite,
accountSetup: accountSetup,
authorization: authorization,
token: token,
validateRequestedScopes: validateRequestedScopes,
scope: scope,
websocketAuth: websocketAuth,
csrf: csrf
};
sendErrorPageOrRedirect(req, res, 'Form expired');
}
];
}
+55 -12
View File
@@ -3,14 +3,18 @@
exports = module.exports = {
get: get,
update: update,
changePassword: changePassword
changePassword: changePassword,
setTwoFactorAuthenticationSecret: setTwoFactorAuthenticationSecret,
enableTwoFactorAuthentication: enableTwoFactorAuthentication,
disableTwoFactorAuthentication: disableTwoFactorAuthentication
};
var assert = require('assert'),
var accesscontrol = require('../accesscontrol.js'),
assert = require('assert'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
user = require('../user.js'),
UserError = user.UserError,
users = require('../users.js'),
UsersError = users.UsersError,
_ = require('underscore');
function auditSource(req) {
@@ -27,7 +31,9 @@ function get(req, res, next) {
email: req.user.email,
fallbackEmail: req.user.fallbackEmail,
admin: req.user.admin,
displayName: req.user.displayName
scope: accesscontrol.canonicalScope(req.authInfo.scope), // this returns the token scope and not the user's scope
displayName: req.user.displayName,
twoFactorAuthenticationEnabled: req.user.twoFactorAuthenticationEnabled
}));
}
@@ -41,10 +47,10 @@ function update(req, res, next) {
var data = _.pick(req.body, 'email', 'fallbackEmail', 'displayName');
user.update(req.user.id, data, auditSource(req), function (error) {
if (error && error.reason === UserError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === UserError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
users.update(req.user.id, data, auditSource(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));
next(new HttpSuccess(204));
@@ -57,11 +63,48 @@ function changePassword(req, res, next) {
if (typeof req.body.newPassword !== 'string') return next(new HttpError(400, 'newPassword must be a string'));
user.setPassword(req.user.id, req.body.newPassword, function (error) {
if (error && error.reason === UserError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(403, 'Wrong password'));
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(403, 'Wrong password'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
});
}
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));
next(new HttpSuccess(201, { secret: result.secret, qrcode: result.qrcode }));
});
}
function enableTwoFactorAuthentication(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.user, 'object');
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(403, '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));
next(new HttpSuccess(202, {}));
});
}
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));
next(new HttpSuccess(202, {}));
});
}
+34 -6
View File
@@ -1,8 +1,11 @@
'use strict';
exports = module.exports = {
getAutoupdatePattern: getAutoupdatePattern,
setAutoupdatePattern: setAutoupdatePattern,
getAppAutoupdatePattern: getAppAutoupdatePattern,
setAppAutoupdatePattern: setAppAutoupdatePattern,
getBoxAutoupdatePattern: getBoxAutoupdatePattern,
setBoxAutoupdatePattern: setBoxAutoupdatePattern,
getCloudronName: getCloudronName,
setCloudronName: setCloudronName,
@@ -27,20 +30,41 @@ var assert = require('assert'),
settings = require('../settings.js'),
SettingsError = settings.SettingsError;
function getAutoupdatePattern(req, res, next) {
settings.getAutoupdatePattern(function (error, pattern) {
function getAppAutoupdatePattern(req, res, next) {
settings.getAppAutoupdatePattern(function (error, pattern) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { pattern: pattern }));
});
}
function setAutoupdatePattern(req, res, next) {
function setAppAutoupdatePattern(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.pattern !== 'string') return next(new HttpError(400, 'pattern is required'));
settings.setAutoupdatePattern(req.body.pattern, function (error) {
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));
next(new HttpSuccess(200));
});
}
function getBoxAutoupdatePattern(req, res, next) {
settings.getBoxAutoupdatePattern(function (error, pattern) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { pattern: pattern }));
});
}
function setBoxAutoupdatePattern(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
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));
@@ -129,6 +153,10 @@ function setBackupConfig(req, res, next) {
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if (typeof req.body.retentionSecs !== 'number') return next(new HttpError(400, 'retentionSecs is required'));
if ('key' in req.body && typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if ('syncConcurrency' in req.body) {
if (typeof req.body.syncConcurrency !== 'number') return next(new HttpError(400, 'syncConcurrency must be a positive integer'));
if (req.body.syncConcurrency < 1) return next(new HttpError(400, 'syncConcurrency must be a positive integer'));
}
if (typeof req.body.format !== 'string') return next(new HttpError(400, 'format must be a string'));
if ('acceptSelfSignedCerts' in req.body && typeof req.body.acceptSelfSignedCerts !== 'boolean') return next(new HttpError(400, 'format must be a boolean'));
+1
View File
@@ -78,6 +78,7 @@ function dnsSetup(req, res, next) {
setup.dnsSetup(req.body.adminFqdn.toLowerCase(), req.body.domain.toLowerCase(), req.body.zoneName || '', req.body.provider, req.body.config, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
if (error && error.reason === SetupError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === SetupError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === SetupError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
+117
View File
@@ -0,0 +1,117 @@
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
var accesscontrol = require('../accesscontrol.js'),
expect = require('expect.js'),
HttpError = require('connect-lastmile').HttpError,
passport = require('passport');
describe('scopes middleware', function () {
var passportAuthenticateSave = null;
before(function () {
passportAuthenticateSave = passport.authenticate;
passport.authenticate = function () {
return function (req, res, next) { next(); };
};
});
after(function () {
passport.authenticate = passportAuthenticateSave;
});
it('fails due to missing authInfo', function (done) {
var mw = accesscontrol.scope('admin')[1];
var req = {};
mw(req, null, function (error) {
expect(error).to.be.a(HttpError);
done();
});
});
it('fails due to missing scope property in authInfo', function (done) {
var mw = accesscontrol.scope('admin')[1];
var req = { authInfo: {} };
mw(req, null, function (error) {
expect(error).to.be.a(HttpError);
done();
});
});
it('fails due to missing scope in request', function (done) {
var mw = accesscontrol.scope('admin')[1];
var req = { authInfo: { scope: '' } };
mw(req, null, function (error) {
expect(error).to.be.a(HttpError);
done();
});
});
it('fails due to wrong scope in request', function (done) {
var mw = accesscontrol.scope('admin')[1];
var req = { authInfo: { scope: 'foobar,something' } };
mw(req, null, function (error) {
expect(error).to.be.a(HttpError);
done();
});
});
it('fails due to wrong scope in request', function (done) {
var mw = accesscontrol.scope('admin,users')[1];
var req = { authInfo: { scope: 'foobar,admin' } };
mw(req, null, function (error) {
expect(error).to.be.a(HttpError);
done();
});
});
it('succeeds with one requested scope and one provided scope', function (done) {
var mw = accesscontrol.scope('admin')[1];
var req = { authInfo: { scope: 'admin' } };
mw(req, null, function (error) {
expect(error).to.not.be.ok();
done();
});
});
it('succeeds with one requested scope and two provided scopes', function (done) {
var mw = accesscontrol.scope('admin')[1];
var req = { authInfo: { scope: 'foobar,admin' } };
mw(req, null, function (error) {
expect(error).to.not.be.ok();
done();
});
});
it('succeeds with two requested scope and two provided scopes', function (done) {
var mw = accesscontrol.scope('admin,foobar')[1];
var req = { authInfo: { scope: 'foobar,admin' } };
mw(req, null, function (error) {
expect(error).to.not.be.ok();
done();
});
});
it('succeeds with two requested scope and provided wildcard scope', function (done) {
var mw = accesscontrol.scope('admin,foobar')[1];
var req = { authInfo: { scope: '*' } };
mw(req, null, function (error) {
expect(error).to.not.be.ok();
done();
});
});
});
File diff suppressed because it is too large Load Diff
+58 -55
View File
@@ -5,7 +5,8 @@
/* global before:false */
/* global after:false */
var async = require('async'),
var accesscontrol = require('../../accesscontrol.js'),
async = require('async'),
config = require('../../config.js'),
clients = require('../../clients.js'),
database = require('../../database.js'),
@@ -60,8 +61,8 @@ describe('OAuth Clients API', function () {
after(cleanup);
it('fails without token', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile' })
superagent.post(SERVER_URL + '/api/v1/clients')
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: accesscontrol.SCOPE_PROFILE })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
@@ -69,9 +70,9 @@ describe('OAuth Clients API', function () {
});
it('fails without appId', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
superagent.post(SERVER_URL + '/api/v1/clients')
.query({ access_token: token })
.send({ redirectURI: 'http://foobar.com', scope: 'profile' })
.send({ redirectURI: 'http://foobar.com', scope: accesscontrol.SCOPE_PROFILE })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
@@ -79,9 +80,9 @@ describe('OAuth Clients API', function () {
});
it('fails with empty appId', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
superagent.post(SERVER_URL + '/api/v1/clients')
.query({ access_token: token })
.send({ appId: '', redirectURI: 'http://foobar.com', scope: 'profile' })
.send({ appId: '', redirectURI: 'http://foobar.com', scope: accesscontrol.SCOPE_PROFILE })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
@@ -89,7 +90,7 @@ describe('OAuth Clients API', function () {
});
it('fails without scope', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
superagent.post(SERVER_URL + '/api/v1/clients')
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'http://foobar.com' })
.end(function (error, result) {
@@ -99,7 +100,7 @@ describe('OAuth Clients API', function () {
});
it('fails with empty scope', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
superagent.post(SERVER_URL + '/api/v1/clients')
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: '' })
.end(function (error, result) {
@@ -109,9 +110,9 @@ describe('OAuth Clients API', function () {
});
it('fails without redirectURI', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
superagent.post(SERVER_URL + '/api/v1/clients')
.query({ access_token: token })
.send({ appId: 'someApp', scope: 'profile' })
.send({ appId: 'someApp', scope: accesscontrol.SCOPE_PROFILE })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
@@ -119,9 +120,9 @@ describe('OAuth Clients API', function () {
});
it('fails with empty redirectURI', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
superagent.post(SERVER_URL + '/api/v1/clients')
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: '', scope: 'profile' })
.send({ appId: 'someApp', redirectURI: '', scope: accesscontrol.SCOPE_PROFILE })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
@@ -129,9 +130,9 @@ describe('OAuth Clients API', function () {
});
it('fails with malformed redirectURI', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
superagent.post(SERVER_URL + '/api/v1/clients')
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'foobar', scope: 'profile' })
.send({ appId: 'someApp', redirectURI: 'foobar', scope: accesscontrol.SCOPE_PROFILE })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
@@ -139,9 +140,9 @@ describe('OAuth Clients API', function () {
});
it('fails with invalid name', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
superagent.post(SERVER_URL + '/api/v1/clients')
.query({ access_token: token })
.send({ appId: '$"$%^45asdfasdfadf.adf.', redirectURI: 'http://foobar.com', scope: 'profile' })
.send({ appId: '$"$%^45asdfasdfadf.adf.', redirectURI: 'http://foobar.com', scope: accesscontrol.SCOPE_PROFILE })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
@@ -149,9 +150,9 @@ describe('OAuth Clients API', function () {
});
it('succeeds with dash', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
superagent.post(SERVER_URL + '/api/v1/clients')
.query({ access_token: token })
.send({ appId: 'fo-1234-bar', redirectURI: 'http://foobar.com', scope: 'profile' })
.send({ appId: 'fo-1234-bar', redirectURI: 'http://foobar.com', scope: accesscontrol.SCOPE_PROFILE })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
done();
@@ -159,9 +160,9 @@ describe('OAuth Clients API', function () {
});
it('succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
superagent.post(SERVER_URL + '/api/v1/clients')
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile' })
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: accesscontrol.SCOPE_PROFILE })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
expect(result.body.id).to.be.a('string');
@@ -181,7 +182,7 @@ describe('OAuth Clients API', function () {
id: '',
appId: 'someAppId-0',
redirectURI: 'http://some.callback0',
scope: 'profile'
scope: accesscontrol.SCOPE_PROFILE
};
before(function (done) {
@@ -189,7 +190,7 @@ describe('OAuth Clients API', function () {
setup,
function (callback) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
superagent.post(SERVER_URL + '/api/v1/clients')
.query({ access_token: token })
.send({ appId: CLIENT_0.appId, redirectURI: CLIENT_0.redirectURI, scope: CLIENT_0.scope })
.end(function (error, result) {
@@ -206,7 +207,7 @@ describe('OAuth Clients API', function () {
after(cleanup);
it('fails without token', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
superagent.get(SERVER_URL + '/api/v1/clients/' + CLIENT_0.id)
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
@@ -215,7 +216,7 @@ describe('OAuth Clients API', function () {
it('fails with unknown id', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id.toUpperCase())
superagent.get(SERVER_URL + '/api/v1/clients/' + CLIENT_0.id.toUpperCase())
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
@@ -224,7 +225,7 @@ describe('OAuth Clients API', function () {
});
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
superagent.get(SERVER_URL + '/api/v1/clients/' + CLIENT_0.id)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
@@ -239,14 +240,14 @@ describe('OAuth Clients API', function () {
id: '',
appId: 'someAppId-0',
redirectURI: 'http://some.callback0',
scope: 'profile'
scope: accesscontrol.SCOPE_PROFILE
};
var CLIENT_1 = {
id: '',
appId: 'someAppId-1',
redirectURI: 'http://some.callback1',
scope: 'profile',
scope: accesscontrol.SCOPE_PROFILE,
type: clients.TYPE_OAUTH
};
@@ -255,7 +256,7 @@ describe('OAuth Clients API', function () {
setup,
function (callback) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
superagent.post(SERVER_URL + '/api/v1/clients')
.query({ access_token: token })
.send({ appId: CLIENT_0.appId, redirectURI: CLIENT_0.redirectURI, scope: CLIENT_0.scope })
.end(function (error, result) {
@@ -272,7 +273,7 @@ describe('OAuth Clients API', function () {
after(cleanup);
it('fails without token', function (done) {
superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
superagent.del(SERVER_URL + '/api/v1/clients/' + CLIENT_0.id)
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
@@ -281,7 +282,7 @@ describe('OAuth Clients API', function () {
it('fails with unknown id', function (done) {
superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id.toUpperCase())
superagent.del(SERVER_URL + '/api/v1/clients/' + CLIENT_0.id.toUpperCase())
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
@@ -290,12 +291,12 @@ describe('OAuth Clients API', function () {
});
it('succeeds', function (done) {
superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
superagent.del(SERVER_URL + '/api/v1/clients/' + CLIENT_0.id)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
superagent.get(SERVER_URL + '/api/v1/clients/' + CLIENT_0.id)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
@@ -306,12 +307,12 @@ describe('OAuth Clients API', function () {
});
it('fails for cid-webadmin', function (done) {
superagent.del(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin')
superagent.del(SERVER_URL + '/api/v1/clients/cid-webadmin')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(405);
superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin')
superagent.get(SERVER_URL + '/api/v1/clients/cid-webadmin')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
@@ -327,12 +328,12 @@ describe('OAuth Clients API', function () {
CLIENT_1.id = result.id;
superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_1.id)
superagent.del(SERVER_URL + '/api/v1/clients/' + CLIENT_1.id)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(405);
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_1.id)
superagent.get(SERVER_URL + '/api/v1/clients/' + CLIENT_1.id)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
@@ -359,9 +360,11 @@ describe('Clients', function () {
};
// make csrf always succeed for testing
oauth2.csrf = function (req, res, next) {
req.csrfToken = function () { return hat(256); };
next();
oauth2.csrf = function () {
return function (req, res, next) {
req.csrfToken = function () { return hat(256); };
next();
};
};
function setup2(done) {
@@ -388,7 +391,7 @@ describe('Clients', function () {
after(cleanup);
it('fails due to missing token', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients')
superagent.get(SERVER_URL + '/api/v1/clients')
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
@@ -396,7 +399,7 @@ describe('Clients', function () {
});
it('fails due to empty token', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients')
superagent.get(SERVER_URL + '/api/v1/clients')
.query({ access_token: '' })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
@@ -405,7 +408,7 @@ describe('Clients', function () {
});
it('fails due to wrong token', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients')
superagent.get(SERVER_URL + '/api/v1/clients')
.query({ access_token: token.toUpperCase() })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
@@ -414,7 +417,7 @@ describe('Clients', function () {
});
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients')
superagent.get(SERVER_URL + '/api/v1/clients')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
@@ -431,7 +434,7 @@ describe('Clients', function () {
after(cleanup);
it('fails due to missing token', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
superagent.get(SERVER_URL + '/api/v1/clients/cid-webadmin/tokens')
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
@@ -439,7 +442,7 @@ describe('Clients', function () {
});
it('fails due to empty token', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
superagent.get(SERVER_URL + '/api/v1/clients/cid-webadmin/tokens')
.query({ access_token: '' })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
@@ -448,7 +451,7 @@ describe('Clients', function () {
});
it('fails due to wrong token', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
superagent.get(SERVER_URL + '/api/v1/clients/cid-webadmin/tokens')
.query({ access_token: token.toUpperCase() })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
@@ -457,7 +460,7 @@ describe('Clients', function () {
});
it('fails due to unkown client', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/CID-WEBADMIN/tokens')
superagent.get(SERVER_URL + '/api/v1/clients/CID-WEBADMIN/tokens')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
@@ -466,7 +469,7 @@ describe('Clients', function () {
});
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
superagent.get(SERVER_URL + '/api/v1/clients/cid-webadmin/tokens')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
@@ -484,7 +487,7 @@ describe('Clients', function () {
after(cleanup);
it('fails due to missing token', function (done) {
superagent.del(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
superagent.del(SERVER_URL + '/api/v1/clients/cid-webadmin/tokens')
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
@@ -492,7 +495,7 @@ describe('Clients', function () {
});
it('fails due to empty token', function (done) {
superagent.del(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
superagent.del(SERVER_URL + '/api/v1/clients/cid-webadmin/tokens')
.query({ access_token: '' })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
@@ -501,7 +504,7 @@ describe('Clients', function () {
});
it('fails due to wrong token', function (done) {
superagent.del(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
superagent.del(SERVER_URL + '/api/v1/clients/cid-webadmin/tokens')
.query({ access_token: token.toUpperCase() })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
@@ -510,7 +513,7 @@ describe('Clients', function () {
});
it('fails due to unkown client', function (done) {
superagent.del(SERVER_URL + '/api/v1/oauth/clients/CID-WEBADMIN/tokens')
superagent.del(SERVER_URL + '/api/v1/clients/CID-WEBADMIN/tokens')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
@@ -519,7 +522,7 @@ describe('Clients', function () {
});
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
superagent.get(SERVER_URL + '/api/v1/clients/cid-webadmin/tokens')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
@@ -527,7 +530,7 @@ describe('Clients', function () {
expect(result.body.tokens.length).to.eql(1);
expect(result.body.tokens[0].identifier).to.eql(USER_0.id);
superagent.del(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
superagent.del(SERVER_URL + '/api/v1/clients/cid-webadmin/tokens')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
+5 -18
View File
@@ -5,7 +5,8 @@
/* global before:false */
/* global after:false */
var async = require('async'),
var accesscontrol = require('../../accesscontrol.js'),
async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
@@ -166,7 +167,7 @@ describe('Cloudron', function () {
userId_1 = result.body.id;
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, userId_1, 'test-client-id', Date.now() + 100000, '*', callback);
tokendb.add(token_1, userId_1, 'test-client-id', Date.now() + 100000, accesscontrol.SCOPE_ANY, callback);
});
}
], done);
@@ -202,25 +203,11 @@ describe('Cloudron', function () {
});
});
it('succeeds (non-admin)', function (done) {
it('fails (non-admin)', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/config')
.query({ access_token: token_1 })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.apiServerOrigin).to.eql('http://localhost:6060');
expect(result.body.webServerOrigin).to.eql(null);
expect(result.body.adminFqdn).to.eql(config.adminFqdn());
expect(result.body.progress).to.be.an('object');
expect(result.body.version).to.eql(config.version());
expect(result.body.cloudronName).to.be.a('string');
expect(result.body.provider).to.be.a('string');
expect(result.body.update).to.be(undefined);
expect(result.body.size).to.be(undefined);
expect(result.body.region).to.be(undefined);
expect(result.body.memory).to.be(undefined);
expect(result.statusCode).to.equal(403);
done();
});
});
+93 -13
View File
@@ -10,6 +10,7 @@ var async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
speakeasy = require('speakeasy'),
superagent = require('superagent'),
server = require('../../server.js');
@@ -36,6 +37,8 @@ function cleanup(done) {
}
describe('Developer API', function () {
this.timeout(20000);
describe('login', function () {
before(function (done) {
async.series([
@@ -131,8 +134,8 @@ describe('Developer API', function () {
.send({ username: USERNAME, password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(new Date(result.body.expiresAt).toString()).to.not.be('Invalid Date');
expect(result.body.token).to.be.a('string');
expect(new Date(result.body.expires).toString()).to.not.be('Invalid Date');
expect(result.body.accessToken).to.be.a('string');
done();
});
});
@@ -142,8 +145,8 @@ describe('Developer API', function () {
.send({ username: USERNAME.toUpperCase(), password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(new Date(result.body.expiresAt).toString()).to.not.be('Invalid Date');
expect(result.body.token).to.be.a('string');
expect(new Date(result.body.expires).toString()).to.not.be('Invalid Date');
expect(result.body.accessToken).to.be.a('string');
done();
});
});
@@ -153,8 +156,8 @@ describe('Developer API', function () {
.send({ username: EMAIL, password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(new Date(result.body.expiresAt).toString()).to.not.be('Invalid Date');
expect(result.body.token).to.be.a('string');
expect(new Date(result.body.expires).toString()).to.not.be('Invalid Date');
expect(result.body.accessToken).to.be.a('string');
done();
});
});
@@ -164,13 +167,90 @@ describe('Developer API', function () {
.send({ username: EMAIL.toUpperCase(), password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(new Date(result.body.expiresAt).toString()).to.not.be('Invalid Date');
expect(result.body.token).to.be.a('string');
expect(new Date(result.body.expires).toString()).to.not.be('Invalid Date');
expect(result.body.accessToken).to.be.a('string');
done();
});
});
});
describe('2fa login', function () {
var secret, accessToken;
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) {
callback(error);
});
},
function (callback) {
superagent.post(`${SERVER_URL}/api/v1/developer/login`).send({ username: USERNAME, password: PASSWORD }).end(function (error, result) {
accessToken = result.body.accessToken;
callback(error);
});
},
function (callback) {
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication`).query({ access_token: accessToken }).end(function (error, result) {
secret = result.body.secret;
callback(error);
});
},
function (callback) {
var totpToken = speakeasy.totp({
secret: secret,
encoding: 'base32'
});
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication/enable`).query({ access_token: accessToken }).send({ totpToken: totpToken }).end(function (error, result) {
callback(error);
});
}
], done);
});
after(function (done) {
async.series([
function (callback) {
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication/disable`).query({ access_token: accessToken }).send({ password: PASSWORD }).end(function (error, result) {
callback(error);
});
},
cleanup
], done);
});
it('fails due to missing token', function (done) {
superagent.post(`${SERVER_URL}/api/v1/developer/login`).send({ username: USERNAME, password: PASSWORD }).end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails due to wrong token', function (done) {
superagent.post(`${SERVER_URL}/api/v1/developer/login`).send({ username: USERNAME, password: PASSWORD }).send({ totpToken: 'wrongtoken' }).end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('succeeds', function (done) {
var totpToken = speakeasy.totp({
secret: secret,
encoding: 'base32'
});
superagent.post(`${SERVER_URL}/api/v1/developer/login`).send({ username: USERNAME, password: PASSWORD }).send({ totpToken: totpToken }).end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(200);
expect(result.body).to.be.an(Object);
expect(result.body.accessToken).to.be.a('string');
done();
});
});
});
describe('sdk tokens are valid without password checks', function () {
var token_normal, token_sdk;
@@ -184,16 +264,16 @@ describe('Developer API', function () {
.end(function (error, result) {
expect(result).to.be.ok();
token_normal = result.body.token;
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.expiresAt).toString()).to.not.be('Invalid Date');
expect(result.body.token).to.be.a('string');
expect(new Date(result.body.expires).toString()).to.not.be('Invalid Date');
expect(result.body.accessToken).to.be.a('string');
token_sdk = result.body.token;
token_sdk = result.body.accessToken;
callback();
});
@@ -206,7 +286,7 @@ describe('Developer API', function () {
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(400);
expect(result.statusCode).to.equal(401);
done();
});
});

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