Compare commits

...

858 Commits
1.9.3 ... 3.0

Author SHA1 Message Date
Girish Ramakrishnan
51aaa8f304 More changes 2018-08-16 20:14:03 -07:00
Girish Ramakrishnan
0c2e200176 3.0.2 changes 2018-08-16 20:11:58 -07:00
Girish Ramakrishnan
8d7ba5cc26 Fix issue where normal users are shown all apps 2018-08-16 20:10:57 -07:00
Girish Ramakrishnan
8671c4c015 3.0.1 changes 2018-08-06 19:45:46 -07:00
Girish Ramakrishnan
2612cc18fa Expire existing webadmin token so that the UI gets a new token
This is because we added a new appstore scope
2018-08-06 19:40:46 -07:00
Girish Ramakrishnan
36a21acae8 skip verification for prereleases because we remove it from release.json 2018-08-06 16:23:23 -07:00
Girish Ramakrishnan
1ed4710c68 Better error message 2018-08-05 23:38:49 -07:00
Girish Ramakrishnan
75b6688734 Clear timeout when setting backup config 2018-08-05 22:29:27 -07:00
Girish Ramakrishnan
f7a7e4e95a Fix text in account setup screen 2018-08-05 21:58:07 -07:00
Girish Ramakrishnan
a8ba0b91f7 Allow subdomain in the location field
This allows one to easily add "dev.staging@domain.com" etc without having to create
yet another domain. This plays well with the concept that we have a
mail domain for every domain. So we get mails from @domain.com working for
these subdomain installations.
2018-08-04 09:37:18 -07:00
Girish Ramakrishnan
95540e8cbc Do ownership check in exec route 2018-08-03 22:19:42 -07:00
Girish Ramakrishnan
9ebd22d6f7 add api note 2018-08-03 22:19:36 -07:00
Girish Ramakrishnan
1cf5807fb9 Send spaces config in config route
This is here because normal users cannot access settings route
2018-08-03 18:26:16 -07:00
Girish Ramakrishnan
cc7824681b Return ownerId in apps response 2018-08-03 18:15:22 -07:00
Girish Ramakrishnan
a0a523ae71 spaces: verify app ownership in app management routes 2018-08-03 17:35:58 -07:00
Girish Ramakrishnan
fba70d888b Remove tokenScopes
decided that the UI won't use this any more and will just the admin flag
2018-08-03 10:07:23 -07:00
Girish Ramakrishnan
ce9fc7b3f7 Better error message 2018-08-03 09:46:22 -07:00
Girish Ramakrishnan
3d4df8e26c Add test for spaces config 2018-08-03 09:46:16 -07:00
Girish Ramakrishnan
d3f9647cd5 Add settings for Spaces 2018-08-03 09:40:57 -07:00
Girish Ramakrishnan
2a49569805 Send admin flag in the profile 2018-08-03 09:34:25 -07:00
Girish Ramakrishnan
47c8700d42 make scopesForUser async 2018-08-03 09:34:19 -07:00
Girish Ramakrishnan
d302dbc634 Fix tests 2018-08-02 14:59:50 -07:00
Girish Ramakrishnan
eab3cd6b2b ownerId for domains is not implemented yet 2018-08-01 18:53:49 -07:00
Girish Ramakrishnan
92151b1e42 Add note on npm rebuild 2018-08-01 18:37:40 -07:00
Girish Ramakrishnan
621d00a5c6 GPG verify releases
Part of #189
2018-08-01 17:16:42 -07:00
Girish Ramakrishnan
5bd7cd6749 Check if syslog binary exists first
The command fails in a subshell and strange things happen

==> installer: update cloudron-syslog
/tmp/box-src-hotfix/scripts/installer.sh: line 99: /usr/local/cloudron-syslog/bin/cloudron-syslog: No such file or directory
/tmp/box-src-hotfix/scripts/installer.sh: line 99: : command not found
2018-08-01 12:18:56 -07:00
Girish Ramakrishnan
5fb525f011 Commit binary public key
GPG's --verify option can only use binary format
2018-07-31 20:25:46 -07:00
Girish Ramakrishnan
d8257c4745 refactor updater logic into updater.js 2018-07-31 20:20:10 -07:00
Girish Ramakrishnan
ef5dc7311f Do not encrypt empty path 2018-07-31 19:41:03 -07:00
Girish Ramakrishnan
498642b670 Fix debugs 2018-07-31 19:32:56 -07:00
Girish Ramakrishnan
daa8514654 Pass the message and not error object 2018-07-31 10:46:35 -07:00
Girish Ramakrishnan
608de479fb Add releases public key
Part of #189
2018-07-31 10:34:01 -07:00
Johannes Zellner
51f7a47ea6 Fix db migration script 2018-07-30 22:58:09 +02:00
Girish Ramakrishnan
480aed9f33 Update mail addon 2018-07-30 12:37:37 -07:00
Girish Ramakrishnan
74ae0a1787 More changes 2018-07-30 10:46:14 -07:00
Girish Ramakrishnan
ed8351b0dc Add re-installation check
Fixes #536
2018-07-30 10:10:30 -07:00
Girish Ramakrishnan
a1070b7da3 storage: fix file not found message 2018-07-30 07:47:52 -07:00
Girish Ramakrishnan
3067d87ca9 handle decryption errors 2018-07-29 21:01:20 -07:00
Girish Ramakrishnan
56ca6f449f Set any restore errors in status object
Fixes #505
2018-07-29 20:31:58 -07:00
Girish Ramakrishnan
99ad3e499d s3: do not rely on entry.s3 2018-07-29 09:00:57 -07:00
Girish Ramakrishnan
7182ad4205 s3: Remove usage of entries.s3 2018-07-29 07:51:12 -07:00
Girish Ramakrishnan
0b10e2b332 s3: add listDir
Fixes #440
2018-07-28 09:05:44 -07:00
Girish Ramakrishnan
f546d53ca2 gcs: add listDir
part of #440
2018-07-28 03:51:32 -07:00
Girish Ramakrishnan
2bcc0eef96 fs: downloadDir is unused 2018-07-27 16:56:30 -07:00
Girish Ramakrishnan
a5daad2e1a rsync: encrypted download (fs backend)
Part of #440
2018-07-27 16:06:13 -07:00
Girish Ramakrishnan
b3c8767d79 rsync: encrypted upload (fs backend)
Part of #440
2018-07-27 15:34:48 -07:00
Girish Ramakrishnan
f97f528f05 backup: Implement downloadDir using listDir 2018-07-27 15:24:35 -07:00
Girish Ramakrishnan
ba8a549235 fs: open the source stream using download() 2018-07-27 15:23:48 -07:00
Girish Ramakrishnan
737541f707 storage: standardize the listDir API 2018-07-27 15:23:26 -07:00
Girish Ramakrishnan
94cb222869 filesystem: Use listDir
We can now refactor all the iterating/fs write logic into backups.js
(required for restoring encrypted backups)
2018-07-27 14:09:52 -07:00
Girish Ramakrishnan
df98847535 Remove backupDone from storage interface
This is only used by caas
2018-07-27 12:08:19 -07:00
Girish Ramakrishnan
3d22458f9b Add the 3.0 UI changes 2018-07-27 00:08:08 -07:00
Girish Ramakrishnan
d76381fa26 More 3.0 changes 2018-07-27 00:04:31 -07:00
Girish Ramakrishnan
606cd4da36 Allow - in mailbox names (for no-reply) 2018-07-26 23:48:21 -07:00
Girish Ramakrishnan
554006683e Only unset of admin flag is disallowed 2018-07-26 23:43:44 -07:00
Girish Ramakrishnan
0966edd8fe Fix missing require 2018-07-26 22:36:20 -07:00
Girish Ramakrishnan
78a2176d1d Make admin simply a boolean instead of group
This simplifies a lot of logic. Keeping an admin group has no benefit
2018-07-26 22:29:57 -07:00
Girish Ramakrishnan
39848a25a8 Refactor a bit 2018-07-26 15:35:41 -07:00
Girish Ramakrishnan
ea946396e7 Use users.isAdmin in all places 2018-07-26 13:23:06 -07:00
Girish Ramakrishnan
b4d5def56d Revert role support 2018-07-26 13:23:06 -07:00
Johannes Zellner
477abf53f3 Add initial 3.0.0 changes 2018-07-26 21:36:43 +02:00
Girish Ramakrishnan
0cb03e3789 Add REST route for platform config
Fixes #566
2018-07-25 14:09:15 -07:00
Girish Ramakrishnan
f4d7d4e7f2 Set the domain field 2018-07-25 11:41:42 -07:00
Girish Ramakrishnan
c09ae963e9 merge set/addDnsRecords
This also makes the update mail domain route redundant
2018-07-25 10:51:21 -07:00
Girish Ramakrishnan
fa30312cea lint 2018-07-25 10:34:57 -07:00
Girish Ramakrishnan
c063267c72 Split setting of dns records from mail config route
This allows us to enable email server but not setup the dns records
yet. Useful for importing existing mailboxes from another server
without adjusting the DNS.
2018-07-25 10:29:28 -07:00
Girish Ramakrishnan
589602cdb0 Add note 2018-07-25 08:29:39 -07:00
Girish Ramakrishnan
6be062f8fd Return empty object for angular 2018-07-24 22:19:34 -07:00
Girish Ramakrishnan
837ec4eb12 Send subdomain field in mail status 2018-07-24 14:25:21 -07:00
Girish Ramakrishnan
4a4166764a relay: Add a connect timeout 2018-07-23 17:05:15 -07:00
Girish Ramakrishnan
7654f36e23 Do temporary redirect instead
Permanent redirect can be cached forever in the browser which
means that changing redirect settings appear to not work.
2018-07-15 17:43:36 -07:00
Girish Ramakrishnan
6810c61e58 Add audit event for ownership transfer 2018-07-05 13:51:22 -07:00
Johannes Zellner
75f9b19db2 Ensure we uri encode the email query arguments for invite, reset and setup links 2018-07-04 11:09:29 +02:00
Johannes Zellner
17410c9432 Remove notes about updates only on paid plan for the digest 2018-07-04 10:59:17 +02:00
Johannes Zellner
8a1de81284 Cleanup sysadmin backup trigger test to only test for backup start event 2018-06-30 14:33:51 +02:00
Johannes Zellner
7b540a1d2d Fixup database tests 2018-06-30 14:12:35 +02:00
Johannes Zellner
8e8488a8e6 Lets be explicit about the error 2018-06-30 14:02:40 +02:00
Johannes Zellner
b1b843fdd8 Fix dns tests 2018-06-30 13:59:23 +02:00
Johannes Zellner
c13c4d0b28 Ensure we purge all nginx configs of an app 2018-06-29 23:14:06 +02:00
Johannes Zellner
2371c8053f We have added a query for removing subdomains in the transaction 2018-06-29 23:14:06 +02:00
Johannes Zellner
7dc2f3cb5b Also wait for alternateDomains to be in sync 2018-06-29 23:14:06 +02:00
Johannes Zellner
163563f400 Remove now unused dnsRecordId 2018-06-29 23:14:06 +02:00
Johannes Zellner
868ed977b3 Cleanup alternateDomain dns records and nginx config 2018-06-29 23:14:06 +02:00
Johannes Zellner
262fe18fb2 Set dns record id for alternateDomains 2018-06-29 23:14:06 +02:00
Johannes Zellner
1eba79660e Move dnsRecordId to subdomains table 2018-06-29 23:14:06 +02:00
Johannes Zellner
3088ac098f Put redirect label into alternateDomain nginx configs 2018-06-29 23:14:06 +02:00
Johannes Zellner
45a41ea161 Fix typo 2018-06-29 23:14:06 +02:00
Johannes Zellner
6c17709d2a Fix fqdn building for alternateDomains 2018-06-29 23:14:06 +02:00
Johannes Zellner
2a52543087 The property is called subdomain not location 2018-06-29 23:14:06 +02:00
Johannes Zellner
f4f6f4e7e0 Use business logic for getting all apps in taskmanager 2018-06-29 23:14:06 +02:00
Johannes Zellner
f53c526677 Remove superflous . for user cert key file location 2018-06-29 23:14:06 +02:00
Johannes Zellner
1aa58a3905 ignore alternateDomains field for appdb updates 2018-06-29 23:14:06 +02:00
Johannes Zellner
2d58a6bdff Fix typo 2018-06-29 23:14:06 +02:00
Johannes Zellner
40c22a1ad7 send alternateDomains over the wire 2018-06-29 23:14:06 +02:00
Johannes Zellner
3d0da34960 Create nginx configs for app redirects 2018-06-29 23:14:06 +02:00
Johannes Zellner
a6e53e3617 Ensure certificates for alternateDomains 2018-06-29 23:14:06 +02:00
Johannes Zellner
8efab41d37 Amend the alternateDomains property to app objects 2018-06-29 23:14:06 +02:00
Johannes Zellner
9af456cc7d Fix appdb.add as location and domain columns are gone 2018-06-29 23:14:06 +02:00
Johannes Zellner
9ba78b5b87 Setup alternate domains in apptask configure 2018-06-29 23:14:06 +02:00
Johannes Zellner
b1b848de21 Support configuring alternateDomains for apps 2018-06-29 23:14:06 +02:00
Johannes Zellner
5497a7d4d8 Do not handle location and domain field for apps table updates 2018-06-29 23:14:06 +02:00
Johannes Zellner
18887b27e6 Fix migration script typo 2018-06-29 23:14:06 +02:00
Johannes Zellner
fb42b54210 Remove apps.location and apps.domain
This is now managed in the subdomains table
2018-06-29 23:14:06 +02:00
Johannes Zellner
4d2ba2adaa Use subdomains table in appdb 2018-06-29 23:14:06 +02:00
Johannes Zellner
c97e8d6bd4 add subdomains table with migration scripts 2018-06-29 23:14:06 +02:00
Girish Ramakrishnan
b15029de11 Send tokenScopes as an array 2018-06-29 09:53:12 -07:00
Girish Ramakrishnan
9aa74c99fc Make setup route return tokenScopes
This is used by the dashboard logic during initial activation
2018-06-29 08:27:34 -07:00
Girish Ramakrishnan
35c9e99102 Move the update info into separate route (since it is sensitive) 2018-06-28 17:50:27 -07:00
Girish Ramakrishnan
cab9bc3a61 Unify the config routes into /api/v1/config
No more separate config routes for different types of user
2018-06-28 17:40:57 -07:00
Girish Ramakrishnan
712c920b86 Move caas config to separate route 2018-06-28 17:28:46 -07:00
Girish Ramakrishnan
9978dff627 Add API to set and transfer ownership 2018-06-28 16:48:04 -07:00
Girish Ramakrishnan
ff5bd42bef remove mostly dead comment 2018-06-28 14:19:56 -07:00
Girish Ramakrishnan
dfa318e898 Add ownerId for apps
This tracks who installed the app.
2018-06-28 14:13:12 -07:00
Girish Ramakrishnan
38977858aa When issuing token intersect with the existing user roles
Also:
* Move token validation to accesscontrol.js
* Use clients.addTokenByUserId everywhere
2018-06-28 00:07:43 -07:00
Girish Ramakrishnan
6510240c0a Fix accesscontrol.intersectScopes 2018-06-27 18:08:38 -07:00
Girish Ramakrishnan
d66dc11f01 Make canonicalScopeString return sorted array 2018-06-27 14:07:25 -07:00
Girish Ramakrishnan
ce4424d115 debug out the scope 2018-06-27 13:54:10 -07:00
Girish Ramakrishnan
a958c01974 digest: drop the Cloudron prefix in the subject 2018-06-27 11:00:41 -07:00
Girish Ramakrishnan
877f181f8d Put ts in app listing as well 2018-06-26 19:58:21 -07:00
Girish Ramakrishnan
02c0137dc1 Add a timestamp column to apps table
this field can be used in UI to see if more detailed app information
has to be fetched (since it calls the list apps REST API periodically)
2018-06-26 18:16:39 -07:00
Girish Ramakrishnan
d0b34cc43e Fix schema 2018-06-26 18:05:34 -07:00
Girish Ramakrishnan
93a2cab355 Ignore internal mailbox conflict errors 2018-06-26 17:44:14 -07:00
Girish Ramakrishnan
6907475f7a Add app management scope
This splits the apps API into those who have just 'read' access
and those who have 'manage' access.
2018-06-26 08:56:30 -07:00
Girish Ramakrishnan
9bf93b026b rename to removeInternalFields 2018-06-25 16:40:16 -07:00
Girish Ramakrishnan
f932f8b3d3 Add user management scope
This splits the user and groups API into those who have just 'read' access
and those who have 'manage' access.
2018-06-25 16:10:00 -07:00
Girish Ramakrishnan
7ab5d5e50d Add domain management scope
This splits the domains API into those who have just 'read' access
(i.e without configuration details) and those who have 'manage' access.
2018-06-25 15:12:22 -07:00
Johannes Zellner
5028230354 Update cloudron-syslog
Fixes #564
2018-06-25 19:35:13 +02:00
Girish Ramakrishnan
80e9214f5b Reduce password length for sendmail/recvmail
Fixes #565
2018-06-22 16:37:34 -07:00
Girish Ramakrishnan
5ca64dd642 Normalize host path to prevent access to arbitrary paths 2018-06-22 08:29:08 -07:00
Girish Ramakrishnan
24d9d3063b Allow specifying bindMounts array to localstorage addon 2018-06-21 22:04:06 -07:00
Girish Ramakrishnan
74b1df17c0 Fix groups test and route to make name optional 2018-06-20 09:31:50 -07:00
Girish Ramakrishnan
7880a2f9c3 API returns 403 for incorrect password 2018-06-20 09:27:24 -07:00
Girish Ramakrishnan
8a84872704 Wrong password logs out the user 2018-06-18 18:52:35 -07:00
Girish Ramakrishnan
5d13cc363f Allow group name to be changed 2018-06-18 18:30:40 -07:00
Girish Ramakrishnan
987a42b448 Fix syntax 2018-06-18 17:58:35 -07:00
Girish Ramakrishnan
3601e4f8a6 if -> while 2018-06-18 17:39:15 -07:00
Girish Ramakrishnan
60ed290179 validate role names against existing roles 2018-06-18 17:32:07 -07:00
Girish Ramakrishnan
ff73bc121f Make tokenScope plural 2018-06-18 15:10:02 -07:00
Girish Ramakrishnan
6cd0601629 Map group roles to scopes 2018-06-18 14:52:39 -07:00
Girish Ramakrishnan
b5c8e7a52a Rename to getGroups 2018-06-18 14:10:29 -07:00
Girish Ramakrishnan
7f3114e67d Rename to get/setMembership (to indicate IDs and not group objects 2018-06-18 13:57:17 -07:00
Girish Ramakrishnan
1dbcf2a46a Rename to groups.update 2018-06-18 13:41:27 -07:00
Girish Ramakrishnan
898cbd01b3 tokens table always has canonical scope 2018-06-17 23:11:36 -07:00
Girish Ramakrishnan
b6b7d08af3 Rename to accesscontrol.canonicalScopeString 2018-06-17 22:43:42 -07:00
Girish Ramakrishnan
6a2dacb08a Make intersectScopes take an array 2018-06-17 22:39:33 -07:00
Girish Ramakrishnan
1015b0ad9c validateScope -> validateScopeString 2018-06-17 22:29:17 -07:00
Girish Ramakrishnan
106e17f7ff caas: verify dns config
this is so that it fails if someone gives invalid token via API call
2018-06-17 22:22:05 -07:00
Girish Ramakrishnan
6ca28d9a58 validate dns config parameters 2018-06-17 22:21:51 -07:00
Girish Ramakrishnan
ad6bc191f9 Make hasScopes take an array 2018-06-17 21:06:17 -07:00
Girish Ramakrishnan
682f7a710c Add an appstore scope for subscription settings 2018-06-17 18:09:13 -07:00
Girish Ramakrishnan
f24a099e79 Remove user.admin property
The UI will now base itself entirely off the scopes of the token
2018-06-17 16:49:56 -07:00
Girish Ramakrishnan
156ffb40c9 Remove scope from users.get 2018-06-17 16:07:20 -07:00
Girish Ramakrishnan
db8b6838bb Move skip password verification logic to accesscontrol.js 2018-06-17 15:20:27 -07:00
Girish Ramakrishnan
c3631350cf We can skip user.scope here since we will intersect at access time anyway 2018-06-17 15:11:10 -07:00
Girish Ramakrishnan
669a1498aa Do not dump token in logs 2018-06-17 15:01:42 -07:00
Girish Ramakrishnan
12e55d1fab Use _.flatten instead 2018-06-16 13:03:52 -07:00
Girish Ramakrishnan
ca9cd2cf0f Add groups.getRoles 2018-06-16 00:29:56 -07:00
Girish Ramakrishnan
e8d9597345 Fix various error codes
401 - bad password/wrong password
403 - authenticated but not authorized
409 - conflict
2018-06-15 23:15:30 -07:00
Girish Ramakrishnan
24b0a96f07 Move passport logic to routes 2018-06-15 17:32:40 -07:00
Girish Ramakrishnan
858ffcec72 Add note 2018-06-15 17:04:18 -07:00
Girish Ramakrishnan
05a8911cca condense the comments 2018-06-15 16:56:35 -07:00
Girish Ramakrishnan
89b41b11a4 Remove bogus accesscontrol logic 2018-06-15 16:26:14 -07:00
Girish Ramakrishnan
491d1c1273 getByResetToken already has the user object 2018-06-15 16:22:28 -07:00
Girish Ramakrishnan
0a0884bf93 lint 2018-06-15 16:10:06 -07:00
Girish Ramakrishnan
a1ac7f2ef9 Remove support for authenticating non-oauth2 clients via BasicStrategy
This is not used anywhere
2018-06-15 15:38:58 -07:00
Girish Ramakrishnan
6aef9213aa Add notes on the various strategies 2018-06-15 15:38:53 -07:00
Girish Ramakrishnan
2e92172794 Do not dump the entire token 2018-06-15 14:56:52 -07:00
Girish Ramakrishnan
c210359046 Create the backup logs dir 2018-06-15 14:56:32 -07:00
Girish Ramakrishnan
042ea081a0 Typo 2018-06-15 14:35:52 -07:00
Girish Ramakrishnan
1c32224a8a Move backup logs to make the REST API work 2018-06-15 09:47:34 -07:00
Johannes Zellner
b3fa5afe3a First show apptask logs, then app logs 2018-06-15 14:58:07 +02:00
Johannes Zellner
843fec9dcb Fix wrong hasSubscription logic for digest email 2018-06-15 13:44:33 +02:00
Johannes Zellner
35d9cc3c02 Update cloudron-syslog 2018-06-15 10:20:03 +02:00
Girish Ramakrishnan
02d5d2f808 Add API to add and update the group roles 2018-06-15 00:28:27 -07:00
Girish Ramakrishnan
a77d45f5de Add rolesJson to groups table
This will contain the roles ('role definition') of a group of
users. We will internally map these to our API scopes.
2018-06-14 22:54:52 -07:00
Girish Ramakrishnan
5e09f3dcb2 Group names can contain - and end with .app
now that we have decoupled it from mailbox names, this restriction
can be removed
2018-06-14 22:22:09 -07:00
Girish Ramakrishnan
eb566d28e7 Remove groupdb._addDefaultGroups
Putting this in db code causes issues when we merge in the
accesscontrol/roles variables (groupdb needs to source those
variables)
2018-06-14 21:56:34 -07:00
Girish Ramakrishnan
8795da5d20 Allow subscopes
We can now have scopes as apps:read, apps:write etc
2018-06-14 20:56:04 -07:00
Girish Ramakrishnan
a9ec46c97e Add test for accesscontrol.validateScope 2018-06-14 20:51:22 -07:00
Girish Ramakrishnan
dc86b0f319 validateRequestedScopes -> hasScopes 2018-06-14 20:31:48 -07:00
Girish Ramakrishnan
f7089c52ff normalizeScope -> intersectScope 2018-06-14 20:23:56 -07:00
Girish Ramakrishnan
62793ca7b3 Add accesscontrol.canonicalScope tests 2018-06-14 20:17:59 -07:00
Girish Ramakrishnan
92e6909567 Move feedback route to cloudronScope 2018-06-14 20:03:51 -07:00
Girish Ramakrishnan
55e5c319fe Fix failing log test 2018-06-14 13:43:44 -07:00
Johannes Zellner
1f8451fedb Do not print tail file headers in the logs 2018-06-14 12:53:49 +02:00
Johannes Zellner
cdc78936b5 Ignore faulty empty log lines 2018-06-14 12:53:19 +02:00
Girish Ramakrishnan
eaf0b4e56e mail api: handle not found errors 2018-06-13 07:51:22 -07:00
Girish Ramakrishnan
7339c37b98 Fix invite link in userAdded notification 2018-06-12 20:08:59 -07:00
Girish Ramakrishnan
3176938ea0 Add 2.4.1 changes 2018-06-12 19:25:24 -07:00
Girish Ramakrishnan
c3c77c5a97 Fix wording 2018-06-12 18:18:41 -07:00
Girish Ramakrishnan
32e6b9024c Add email query param to reset code path
This reduces any attack surface
2018-06-12 17:56:41 -07:00
Girish Ramakrishnan
5a6ea33694 Display apptask logs 2018-06-12 14:55:58 -07:00
Girish Ramakrishnan
60bff95d9f Add journalctl fallback for box logs 2018-06-12 14:39:33 -07:00
Girish Ramakrishnan
0cc2838b8b lint 2018-06-12 13:49:52 -07:00
Girish Ramakrishnan
0fc4f4bbff Explicitly pass port and logdir 2018-06-11 22:53:12 -07:00
Girish Ramakrishnan
0b82146b3e Install cloudron-syslog service file from box repo 2018-06-11 22:42:49 -07:00
Girish Ramakrishnan
4369b3046e Make options non-optional 2018-06-11 15:14:59 -07:00
Girish Ramakrishnan
ac75b60f47 Fix password validation text in setup and reset forms 2018-06-11 14:01:40 -07:00
Girish Ramakrishnan
d752ef5fad Move password generation logic to model code 2018-06-11 13:06:08 -07:00
Girish Ramakrishnan
c099d5d3fa Make password strength be 8 chars
Fixes #434
2018-06-11 12:55:24 -07:00
Girish Ramakrishnan
6534297a5d Remove hat module
It's not been updated for 6 years!
2018-06-11 12:38:29 -07:00
Girish Ramakrishnan
2aa6350c94 Change the function names
We had these because user and mailbox code used to be mixed
2018-06-11 11:43:41 -07:00
Girish Ramakrishnan
8b4a399b8f More changes 2018-06-11 11:43:41 -07:00
Johannes Zellner
177243b7f2 Support new platform/addon log style 2018-06-11 20:09:45 +02:00
Girish Ramakrishnan
c2ca827458 Add 2.4.0 changes 2018-06-11 10:13:48 -07:00
Girish Ramakrishnan
90d7dc893c Fix test 2018-06-11 10:01:54 -07:00
Girish Ramakrishnan
eeaaa95ca3 Put email on free plan 2018-06-09 18:26:00 -07:00
Girish Ramakrishnan
04be582573 make the mailbox name follow the apps new location, if the user did not set it explicitly 2018-06-09 11:05:54 -07:00
Girish Ramakrishnan
0953787559 Fix docker exec terminal resize issue
Fixes #549
2018-06-08 11:44:24 -07:00
Girish Ramakrishnan
3bd8a58ea5 Update docker to 18.03.1 2018-06-08 09:13:46 -07:00
Girish Ramakrishnan
275181824f Not sure why package lock keeps changing 2018-06-07 18:45:30 -07:00
Girish Ramakrishnan
f814ffb14f Update node version 2018-06-07 18:43:52 -07:00
Girish Ramakrishnan
95ae948fce more package lock changes 2018-06-07 18:42:47 -07:00
Girish Ramakrishnan
9debf1f6c6 Update some packages 2018-06-07 18:36:36 -07:00
Girish Ramakrishnan
0e583b5afe Update node to 8.11.2 2018-06-07 17:06:47 -07:00
Girish Ramakrishnan
fa47031a63 cloudron-activate: Set externalDisk to true 2018-06-07 11:38:07 -07:00
Girish Ramakrishnan
7fd1bb8597 backup: Add externalDisk option to fs backend
This merely confirms from the user understands that backups have to
stored on an external disk.
2018-06-07 11:14:13 -07:00
Johannes Zellner
8c5b550caa Explicitly use cloudron-syslog 1.0.0 instead of moving master 2018-06-07 16:38:44 +02:00
Johannes Zellner
3d57c32853 Explicitly send empty object for successful post to avoid angular warnings 2018-06-07 16:10:47 +02:00
Johannes Zellner
898d928dd6 logrotate files under platformdata must be owned by root 2018-06-06 18:45:54 +02:00
Johannes Zellner
c578a048dd Ensure app logrotate file is owned by root 2018-06-06 18:39:52 +02:00
Johannes Zellner
2a475c1199 Add logrotate for app and addon logs 2018-06-06 17:36:48 +02:00
Johannes Zellner
57e195883c Use plain syslog tags to be compatible with the format 2018-06-06 14:09:50 +02:00
Johannes Zellner
f2178d9b81 Setup addons to log to cloudron-syslog 2018-06-06 14:09:50 +02:00
Johannes Zellner
df1ac43f40 Use subshells correctly 2018-06-06 14:09:50 +02:00
Johannes Zellner
39059c627b Add --unsafe-perm for cloudron-syslog installation 2018-06-06 14:09:50 +02:00
Johannes Zellner
d942c77ceb Bump infra version to reconfigure the container 2018-06-06 14:09:50 +02:00
Johannes Zellner
c39240c518 Install cloudron-syslog 2018-06-06 14:09:50 +02:00
Johannes Zellner
fd0e2782d8 Deliver the correct utc timestamp instead of the ISO string 2018-06-06 14:09:50 +02:00
Johannes Zellner
36aaa0406e Fix comment about firewall rule setup 2018-06-06 14:09:50 +02:00
Johannes Zellner
17ecb366af Bring back json log format for now 2018-06-06 14:09:50 +02:00
Johannes Zellner
1a83281e16 use port 2514 for syslog 2018-06-06 14:09:50 +02:00
Johannes Zellner
ec41e0eef5 Use tail instead of journalctl to deliver logs 2018-06-06 14:09:50 +02:00
Johannes Zellner
d4097ed4e0 Move logs into platformdata/logs 2018-06-06 14:09:50 +02:00
Johannes Zellner
8fa99fae1a Put all apptask logs of an app in the same log file 2018-06-06 14:09:50 +02:00
Johannes Zellner
e9400e5dce support test usecase to not put logs in /var/log 2018-06-06 14:09:50 +02:00
Johannes Zellner
372a17dc37 Cleanup logs on app uninstall 2018-06-06 14:09:50 +02:00
Johannes Zellner
5ca60b2d3c Since we use log files now, lets keep the apptask timestamp for debug() 2018-06-06 14:09:50 +02:00
Johannes Zellner
1dc649b7a2 Put apptask logs alongside the app logs 2018-06-06 14:09:50 +02:00
Johannes Zellner
74437db740 Use syslog logging backend for apps and addons 2018-06-06 14:09:50 +02:00
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
Girish Ramakrishnan
faef3114f5 Add more 1.10 changes 2018-02-04 09:33:44 -08:00
Girish Ramakrishnan
087f14643a catch_all_address in settings table is JSON 2018-02-04 01:27:40 -08:00
Girish Ramakrishnan
77fe595970 lint 2018-02-04 00:15:41 -08:00
Girish Ramakrishnan
14529d313a sos: CopySource requires fancier encoding rules 2018-02-04 00:12:28 -08:00
Girish Ramakrishnan
72f56ff91c rename the gcdns key file 2018-02-03 22:18:04 -08:00
Girish Ramakrishnan
2a7eabfa68 fix test mail route 2018-02-03 18:34:11 -08:00
Girish Ramakrishnan
d18fe0a40c app: Check altDomain when saving certs 2018-02-03 01:03:23 -08:00
Girish Ramakrishnan
8f5105388f Fix crash when cert renewal fails 2018-02-02 21:21:51 -08:00
Girish Ramakrishnan
4c0da7a8c9 use fallback cert of altDomain 2018-02-02 20:29:04 -08:00
Girish Ramakrishnan
f607010396 sos: remove exoscale specific hacks which are obsolete now 2018-02-02 20:13:51 -08:00
Girish Ramakrishnan
909db5b80e put the ui version in the string as well 2018-02-02 19:57:55 -08:00
Girish Ramakrishnan
7563dd4ac8 remove ununsed require 2018-02-02 18:49:55 -08:00
Girish Ramakrishnan
de1af3ac72 typo 2018-02-02 16:01:20 -08:00
Girish Ramakrishnan
2b9e90397d Do not delete the old conf dir (migration might use it) 2018-02-02 15:44:49 -08:00
Girish Ramakrishnan
8e258f11ec caas: import appstore and caas configs for existing caas 2018-02-02 13:29:57 -08:00
Johannes Zellner
2e818fd689 Make first half of the app route tests pass 2018-02-02 14:06:01 +01:00
Girish Ramakrishnan
f85b7a4336 How many commits to fix a typo? 2018-02-01 22:24:41 -08:00
Girish Ramakrishnan
f4a021b751 Fix path to nginx cert 2018-02-01 21:58:42 -08:00
Johannes Zellner
272b0489ff Fixup the app route test startup sequence 2018-02-01 18:04:43 +01:00
Johannes Zellner
1b25a0d7b7 Fix copy'n'paste error 2018-02-01 17:28:04 +01:00
Johannes Zellner
d8b62f95be Fix domains tests 2018-02-01 11:19:42 +01:00
Johannes Zellner
b337300a7b Do not use removed setTlsConfig in apptask tests 2018-02-01 11:16:55 +01:00
Girish Ramakrishnan
2083b035e8 create images in sfo2 now 2018-01-31 22:36:00 -08:00
Girish Ramakrishnan
2873793e7b typo 2018-01-31 22:31:19 -08:00
Girish Ramakrishnan
17128f0b56 Start the platform code only after 3 secs 2018-01-31 22:16:06 -08:00
Girish Ramakrishnan
1f5ecd5ff8 Set default for CAAS_CONFIG_KEY 2018-01-31 21:48:33 -08:00
Girish Ramakrishnan
52e23c1299 createReleaseTarball: warn about uncommitted changes in webadmin 2018-01-31 20:48:12 -08:00
Girish Ramakrishnan
298a2d2f0f cloudron-setup: remove unused source-url 2018-01-31 20:14:31 -08:00
Girish Ramakrishnan
38b6e49d44 cloudron-setup: remove unused dns-provider 2018-01-31 20:11:13 -08:00
Girish Ramakrishnan
d915ea348f cloudron-setup: tls-provider is obsolete 2018-01-31 20:10:47 -08:00
Girish Ramakrishnan
8014cc8ae1 le -> letsencrypt 2018-01-31 18:53:29 -08:00
Girish Ramakrishnan
7dc7c56e97 Fix tests 2018-01-31 18:23:49 -08:00
Girish Ramakrishnan
a5af87e47a caas: never return key 2018-01-31 18:22:33 -08:00
Girish Ramakrishnan
ab7448926f Fix use of fallback certs
We used to always use nginx cert dir. When custom fallback certs
were set, we used to copy it in boxdata cert dir and then nginx cert dir.

The issue is then that we have to copy all certs to nginx cert dir on
cloudron restore.

To fix this, we simply give priority to nginx cert dir and not copy
around certs anymore. caas cert will reside in nginx cert dir and
not get backed up, as expected.
2018-01-31 18:20:29 -08:00
Johannes Zellner
a727fc5efa Use tlsConfig from domain, not settings in reverseproxy 2018-01-31 18:37:08 +01:00
Johannes Zellner
0b31568c14 Remove tlsConfig from the settings code 2018-01-31 18:27:31 +01:00
Johannes Zellner
9b21167a8d Use tlsConfig from the domain, not from settings 2018-01-31 18:27:18 +01:00
Johannes Zellner
3c198550be Do not send tlsConfig with alive status 2018-01-31 18:27:02 +01:00
Johannes Zellner
31be178210 Add tlsConfig provider validation 2018-01-31 18:20:11 +01:00
Johannes Zellner
d1ef35ae1d Fixup all the tests to use domain tlsConfig 2018-01-31 18:09:38 +01:00
Johannes Zellner
1ec294a04b Add tlsConfig to domains and setup rest apis 2018-01-31 17:42:26 +01:00
Johannes Zellner
75775fa192 Add tlsConfig in domain model code 2018-01-31 16:57:59 +01:00
Johannes Zellner
5db1716664 Add tlsConfig handling in domaindb.js 2018-01-31 16:56:22 +01:00
Johannes Zellner
2db35e42de Add migration script for tlsConfig in domains 2018-01-31 16:53:00 +01:00
Johannes Zellner
5521e17313 Remove progress.json generation in start.sh 2018-01-31 11:56:51 +01:00
Johannes Zellner
35d2755cfb Remove splashpage logic 2018-01-31 09:24:43 +01:00
Johannes Zellner
8ee1c87c45 Do not setup the splashscreen anymore 2018-01-31 09:11:04 +01:00
Johannes Zellner
e1533ccd54 Prefix all installer log output for better tracking 2018-01-31 09:10:51 +01:00
Girish Ramakrishnan
7907dd5c4f migrate any existing host.cert/key to domain based host.cert/key
we don't remove the old one's yet since existing nginx config
might still be referencing them (not sure)
2018-01-30 22:09:55 -08:00
Girish Ramakrishnan
be66d1ff4d arg_fqdn is no more 2018-01-30 21:27:27 -08:00
Girish Ramakrishnan
686a01b3e6 autoprovision: Save tlsKey/tlsCert with domain prefix 2018-01-30 20:41:52 -08:00
Girish Ramakrishnan
3299efc113 typos 2018-01-30 20:38:58 -08:00
Girish Ramakrishnan
7a15777ca5 1.9.4 changes 2018-01-30 19:54:04 -08:00
Girish Ramakrishnan
a553a5de79 Copy over the correct license file 2018-01-30 19:52:03 -08:00
Girish Ramakrishnan
21f11c4136 cloudron-setup: remove many obsolete args 2018-01-30 19:51:58 -08:00
Girish Ramakrishnan
cd31ed23bc rewrite renewAll to use existing functions 2018-01-30 16:34:26 -08:00
Girish Ramakrishnan
639a0eb43b Move the BOX_ENV check for more test coverage 2018-01-30 16:14:05 -08:00
Girish Ramakrishnan
86cf8bf9e7 Add auditSource to ensureCertificate 2018-01-30 15:16:34 -08:00
Girish Ramakrishnan
8e500e0243 caas: make the cert provider use domain fallback certs 2018-01-30 14:18:34 -08:00
Girish Ramakrishnan
781cc3b67a Merge ensureCertificate and configuring nginx 2018-01-30 13:54:13 -08:00
Girish Ramakrishnan
f379724128 merge certificates.js and nginx.js to reverseproxy.js
when certs change, we have to call into nginx anyway. since they
go hand in hand, just merge those files. modern reverse proxies
do this job integrated already.
2018-01-30 12:26:09 -08:00
Girish Ramakrishnan
8e63d63509 Move configureDefaultServer to nginx.js 2018-01-30 12:01:53 -08:00
Girish Ramakrishnan
c84f84b9fe host.cert and host.key are not used anymore 2018-01-30 11:58:26 -08:00
Girish Ramakrishnan
fd913de913 Fallback certs are in host.cert/host.key 2018-01-30 11:43:51 -08:00
Girish Ramakrishnan
3336614702 Make getFallbackCertificate return path like getCertificate 2018-01-30 11:30:35 -08:00
Girish Ramakrishnan
f2372c2c75 Fallback certs are named host.cert and host.key 2018-01-30 11:08:58 -08:00
Girish Ramakrishnan
4a4f1b883a Rename getAdminCerticate to getCertificate 2018-01-30 11:04:13 -08:00
Johannes Zellner
79f2709f3a Ensure certificates and nginx configs on startup 2018-01-30 16:55:13 +01:00
Johannes Zellner
8dea0f71f3 Fix typo with intrinsicFqdn 2018-01-30 16:46:51 +01:00
Johannes Zellner
28cffbb168 fix config-test, fqdn is gone 2018-01-30 14:09:59 +01:00
Johannes Zellner
a662362df7 Add mailinglist rest api tests 2018-01-30 12:23:08 +01:00
Johannes Zellner
f54197afe4 return 204 on mailinglist removal api 2018-01-30 12:22:55 +01:00
Johannes Zellner
2745511e67 Fix typo when using HttpSuccess instead of HttpError 2018-01-30 12:14:08 +01:00
Girish Ramakrishnan
2c60c4eb82 move all setup logic to setup.js 2018-01-29 15:51:36 -08:00
Girish Ramakrishnan
c57c372adf remove config.fqdn() completely 2018-01-29 15:11:43 -08:00
Girish Ramakrishnan
1791617f33 typo 2018-01-29 15:11:43 -08:00
Girish Ramakrishnan
e844e1400e zoneName is gone from config 2018-01-29 15:11:43 -08:00
Girish Ramakrishnan
1f2cfc45b5 adminFqdn is only valid if we have a admin domain set 2018-01-29 15:11:43 -08:00
Johannes Zellner
3c3d44e7f8 Add email alias route tests 2018-01-29 19:39:07 +01:00
Johannes Zellner
6470803604 Do not check if email is enabled when an app tries to do sendmail auth through ldap 2018-01-29 19:29:04 +01:00
Johannes Zellner
edb02c859b Add mailbox rest api tests 2018-01-29 18:53:51 +01:00
Girish Ramakrishnan
351b5fcd70 Use config.adminDomain in tests 2018-01-29 09:28:26 -08:00
Girish Ramakrishnan
4c78a2933f config.setZoneName is gone 2018-01-29 09:06:01 -08:00
Johannes Zellner
9041da62e7 fix email route tests 2018-01-29 17:45:10 +01:00
Johannes Zellner
1e9b37053d Add email domain rest api CRUD testing 2018-01-29 17:18:01 +01:00
Johannes Zellner
6115b1cecf return 409 when an email domain conflict is found 2018-01-29 17:17:47 +01:00
Johannes Zellner
ec7b550ca6 Fixup the ldap tests 2018-01-29 13:35:22 +01:00
Johannes Zellner
66ece2243b Actually return an error if we try to update a non existing email domain 2018-01-29 13:28:11 +01:00
Johannes Zellner
77961e51ec mail.get() returns a MailError 2018-01-29 13:14:08 +01:00
Johannes Zellner
c95de547eb Fix config tests to match the removal of zoneName and repurpose of fqdn 2018-01-29 10:11:09 +01:00
Girish Ramakrishnan
b2363271aa add config.adminDomain 2018-01-28 14:26:41 -08:00
Johannes Zellner
3b2f286ac5 certificates init/uninitialize are gone 2018-01-28 20:59:20 +01:00
Girish Ramakrishnan
9ff1b19c3f Remove unused config.zoneName 2018-01-27 09:26:17 -08:00
Girish Ramakrishnan
64f90abac7 Remove onDomainConfigured 2018-01-26 22:56:15 -08:00
Girish Ramakrishnan
7ce79505ee remove overcomplicated certificate events 2018-01-26 22:47:05 -08:00
Girish Ramakrishnan
b4f945f977 do not regenerate fallback certificate 2018-01-26 22:47:05 -08:00
Girish Ramakrishnan
c2d348fe72 remove unused /api/v1/settings/admin_certificate 2018-01-26 20:49:48 -08:00
Girish Ramakrishnan
44324f4501 make certificates.validateCertificate return CertificatesError 2018-01-26 20:39:58 -08:00
Girish Ramakrishnan
6789e9cfe7 change order of validateCertificate args 2018-01-26 20:36:31 -08:00
Girish Ramakrishnan
4d72dfd3da remove ensureFallbackCertificate 2018-01-26 20:31:48 -08:00
Girish Ramakrishnan
66d90c36fc generate fallback cert for domains if not provided 2018-01-26 20:30:37 -08:00
Girish Ramakrishnan
d0bf315859 certificates: cert/key cannot be null 2018-01-26 20:06:17 -08:00
Girish Ramakrishnan
f49ff2985c certificates: setFallbackCertificate does not validate anymore 2018-01-26 20:03:24 -08:00
Girish Ramakrishnan
ca839ea5cb remove dead comment 2018-01-26 19:55:01 -08:00
Girish Ramakrishnan
ce2d39d54c cloudron-setup: add hetzner in the help text 2018-01-26 18:53:50 -08:00
Johannes Zellner
6ef57d3f23 Fix various route handling tests 2018-01-26 21:31:22 +01:00
Girish Ramakrishnan
de8f7415c3 use config.adminFqdn instead of config.fqdn 2018-01-26 12:12:22 -08:00
Johannes Zellner
7441e11c2d Further test fixes to oauth, ldap and mail tests 2018-01-26 21:10:53 +01:00
Johannes Zellner
c3211c7603 Add domain and enable mail on it for many tests 2018-01-26 20:36:27 +01:00
Johannes Zellner
f028b4a232 Remove mailing list logic from groups tests 2018-01-26 20:07:23 +01:00
Johannes Zellner
004211a683 Remove mailbox and alias related tests in the user logic testing 2018-01-26 20:05:09 +01:00
Johannes Zellner
7d65f341db Ensure user tests have the domain setup correctly and remove implicit mailbox check 2018-01-26 18:47:51 +01:00
Johannes Zellner
7e378b426e Fixup the database tests 2018-01-26 18:32:13 +01:00
Johannes Zellner
3135c227d7 Do not add default domain records when we clear the db for testing 2018-01-26 18:32:01 +01:00
Johannes Zellner
3948cfc33b Parse sql error message to deliver correct conflict errors 2018-01-26 17:56:07 +01:00
Johannes Zellner
ccdf926976 move mailbox cleanup code from userdb to user businesslogic 2018-01-26 12:26:03 +01:00
Johannes Zellner
40f73f6c4b Cleanup the mailboxes for groups on deletion 2018-01-26 11:40:28 +01:00
Johannes Zellner
50e0856803 Do not automatically create mailboxes for groups 2018-01-26 11:36:26 +01:00
Johannes Zellner
d95a670dd7 Add mailing lists routes and logic 2018-01-26 11:25:45 +01:00
Girish Ramakrishnan
16b1b27bfb readDkimPublicKeySync has moved 2018-01-25 15:38:29 -08:00
Girish Ramakrishnan
15fbfd3042 create maildb entry in database._clear 2018-01-25 15:20:53 -08:00
Girish Ramakrishnan
aabd1e7df6 Fix crash when adding a domain 2018-01-25 14:51:07 -08:00
Girish Ramakrishnan
9059a30b89 refactor dns logic in cloudron.js 2018-01-25 14:46:48 -08:00
Girish Ramakrishnan
669b94b0d1 test: remove used of config.fqdn 2018-01-25 14:11:52 -08:00
Girish Ramakrishnan
6cb9779537 mail: Add DNS records when mail domain is created 2018-01-25 14:11:46 -08:00
Girish Ramakrishnan
caf8da331c Do not export configureWebadmin 2018-01-25 13:40:06 -08:00
Girish Ramakrishnan
67eb7a290f Fix mail domain route 2018-01-25 13:40:02 -08:00
Girish Ramakrishnan
af6d8f41ee create dkim key when mail domain is added 2018-01-25 10:38:59 -08:00
Girish Ramakrishnan
7c361a87b0 validate appstore update responses 2018-01-25 09:39:34 -08:00
Johannes Zellner
aab175ea05 Remove alias handling from user logic to mail logic 2018-01-25 18:03:26 +01:00
Johannes Zellner
647582a246 Add alias route handler and logic to mail.js 2018-01-25 18:03:02 +01:00
Johannes Zellner
7bba63d911 rename mailbox routes to match overall scheme of plurals 2018-01-25 12:27:28 +01:00
Girish Ramakrishnan
b71c0bde55 add routes to create and delete mail domain 2018-01-25 12:09:30 +01:00
Girish Ramakrishnan
ef3ab44199 update cloudron-manifestformat (for prerelease in semver) 2018-01-24 20:09:14 -08:00
Girish Ramakrishnan
ed3f128bcd Send relay, catchall and domain info from mail table 2018-01-24 15:38:45 -08:00
Girish Ramakrishnan
2f5ab98284 remove config.zoneName() use 2018-01-24 15:01:54 -08:00
Girish Ramakrishnan
ee66893875 more config.fqdn() removal in tests 2018-01-24 14:58:37 -08:00
Girish Ramakrishnan
45456f2cf7 Remove unused maildb.addDefaultDomain 2018-01-24 14:42:45 -08:00
Girish Ramakrishnan
df3c127584 fqdn -> domain 2018-01-24 14:28:35 -08:00
Girish Ramakrishnan
9d409a67fd Default to domain because tld.getDomain returns null for some tld's 2018-01-24 14:17:26 -08:00
Girish Ramakrishnan
2e05483d54 refactor: addDnsRecords now takes a domain 2018-01-24 14:08:25 -08:00
Girish Ramakrishnan
4e267c7cd1 mail: remove config.fqdn use 2018-01-24 12:28:47 -08:00
Girish Ramakrishnan
efc6a5acd0 Return NOT_FOUND if app is missing in appstore 2018-01-24 10:56:26 -08:00
Johannes Zellner
962ebc835d Do not error if mailbox state is already correct 2018-01-24 15:44:05 +01:00
Johannes Zellner
c7282e861c Add missing MailError type 2018-01-24 15:38:19 +01:00
Johannes Zellner
358048e02b Add domain specific mailbox routes and logic 2018-01-24 13:11:35 +01:00
Johannes Zellner
666f42f4ef Do not automatically create mailboxes for users 2018-01-24 12:56:12 +01:00
Johannes Zellner
aca07765c9 remove unused require 2018-01-24 12:55:44 +01:00
Johannes Zellner
a3caad46a2 Add unique constraint to our schema description file 2018-01-24 10:51:46 +01:00
Girish Ramakrishnan
5e688944e8 dns: compute subdomain correctly when zone name and domain differs 2018-01-23 20:25:45 -08:00
Girish Ramakrishnan
ed75364e2b Default to tld.getDomain if zone name is not provided 2018-01-23 18:54:05 -08:00
Girish Ramakrishnan
d33e35fda2 Move send_test_mail under mail domain api 2018-01-23 16:10:23 -08:00
Girish Ramakrishnan
ccaf687e91 remove config.fqdn from settings-test 2018-01-23 15:47:41 -08:00
Girish Ramakrishnan
ab447120dc cloudflare: del returns 200 now 2018-01-23 14:52:41 -08:00
Girish Ramakrishnan
9e0e99cb0c Use new mail.ini parameters 2018-01-23 14:25:15 -08:00
Girish Ramakrishnan
992a32a8d9 add hetzner as supported provider 2018-01-23 11:37:15 -08:00
Johannes Zellner
4a7b26f940 mail_relay from settings table is also JSON 2018-01-23 16:29:15 +01:00
Johannes Zellner
147c728743 Rename maildb table to mail 2018-01-23 15:45:30 +01:00
Johannes Zellner
ec910e8ca1 Fix hotfix to be able to deal with different webadmin and box code revisions 2018-01-23 12:15:38 +01:00
Johannes Zellner
681813eddd Ensure the mail status check api has consitent toplevel properties 2018-01-23 12:01:00 +01:00
Girish Ramakrishnan
e6f4a9e4a8 Use domain in mail test 2018-01-22 14:49:30 -08:00
Johannes Zellner
27bd0be1fc Remove config.fqdn() from ldap tests 2018-01-22 20:35:21 +01:00
Johannes Zellner
f152dbefad Also check if the domain has mail enabled for ldap sendmail auth 2018-01-22 20:35:08 +01:00
Johannes Zellner
687ba0e248 Verify mailbox against username instead of email 2018-01-22 20:06:18 +01:00
Girish Ramakrishnan
61b5d3e60d createReleaseTarball: Use HEAD instead of --revision 2018-01-22 11:02:21 -08:00
Girish Ramakrishnan
b69d6c42e1 Get webadmin from a level below 2018-01-22 11:02:21 -08:00
Girish Ramakrishnan
924e35294f move ui to separate repo
This separates the platform code from the ui
2018-01-22 11:01:42 -08:00
Johannes Zellner
a6f79854db Further ldap test fixes 2018-01-22 17:53:17 +01:00
Johannes Zellner
591f01bb45 Do not automatically generate the user's email for password verification 2018-01-22 16:12:13 +01:00
Johannes Zellner
8bcd807010 Use main email address for avatar 2018-01-22 16:09:25 +01:00
Johannes Zellner
14dcd71429 Add ui components for fallbackEmail property 2018-01-22 16:06:35 +01:00
Johannes Zellner
9f29438b34 Allow changing fallbackEmail via the profile api 2018-01-22 15:55:55 +01:00
Johannes Zellner
cf94f26d62 Fixup the test using fallbackEmail 2018-01-22 11:52:58 +01:00
Girish Ramakrishnan
6fdb093595 add and remove maildb entries when domain is created and removed 2018-01-21 20:33:21 -08:00
Girish Ramakrishnan
93d5ce63ae fix indent 2018-01-21 15:34:35 -08:00
Johannes Zellner
32152a8b88 Add missing migration file 2018-01-21 14:50:55 +01:00
Johannes Zellner
48d557b242 Replace alternateEmail with fallbackEmail 2018-01-21 14:50:24 +01:00
Johannes Zellner
1e8aa209b1 Add fallbackEmail to user data model 2018-01-21 14:25:39 +01:00
Girish Ramakrishnan
00c1c42b58 Fix mail route tests 2018-01-21 00:59:20 -08:00
Girish Ramakrishnan
f4e1b8874c add maildb entry when domain entry is created 2018-01-21 00:27:28 -08:00
Girish Ramakrishnan
f5b685465f make ldap test use maildb 2018-01-21 00:27:13 -08:00
Girish Ramakrishnan
f49a36f667 Use mail.get() 2018-01-21 00:17:25 -08:00
Girish Ramakrishnan
70fecb8a75 Add mail test 2018-01-21 00:06:08 -08:00
Girish Ramakrishnan
04868f0983 maildb tests 2018-01-20 23:52:16 -08:00
Girish Ramakrishnan
16ac205c7f make mail routes domain based 2018-01-20 23:47:12 -08:00
Girish Ramakrishnan
3ed794e486 Add a single getter for all mail settings 2018-01-20 23:47:12 -08:00
Girish Ramakrishnan
f93963540e Add maildb
also, migrate values from settings table to maildb
2018-01-20 23:47:08 -08:00
Girish Ramakrishnan
777269810f Move mail container creation to mail.js
At this point, mail.js is like an app of it's own
2018-01-20 20:38:35 -08:00
Girish Ramakrishnan
a7de17a160 Move mail related settings to new mail route
there is quite a bit of circular dep between settings, platform and
mail code. this will be removed in future commits.
2018-01-20 20:02:01 -08:00
Girish Ramakrishnan
e724913b6c Move email_status into mail route 2018-01-20 18:37:15 -08:00
Girish Ramakrishnan
b68db9bf05 email -> mail 2018-01-20 18:22:43 -08:00
Girish Ramakrishnan
8da04f6f51 make domaindb.update take object 2018-01-20 10:24:11 -08:00
Girish Ramakrishnan
b64c41758e domaindb.upsert is not used 2018-01-20 10:18:06 -08:00
Girish Ramakrishnan
0eaea12818 make domaindb.add take an object 2018-01-20 10:17:01 -08:00
Girish Ramakrishnan
b098db16cf domaindb: add DOMAINS_FIELDS 2018-01-20 10:05:31 -08:00
Girish Ramakrishnan
dc952f1dd8 remove config.fqdn() from caas-test 2018-01-20 09:50:30 -08:00
Girish Ramakrishnan
ee733d54ea Add mail_domains and mail_default_domain to mail.ini 2018-01-19 23:17:00 -08:00
Girish Ramakrishnan
0e4a0658b2 Remove postman location (unused by dovecot) 2018-01-19 22:10:10 -08:00
Girish Ramakrishnan
20166cd41c make catch_all fully qualified 2018-01-19 20:02:56 -08:00
Girish Ramakrishnan
98d493b2d0 ldap: make mailbox search return fully qualified names 2018-01-19 12:14:43 -08:00
Girish Ramakrishnan
af25485fa0 ldap: Make alias return fully qualified alias 2018-01-19 12:11:33 -08:00
Girish Ramakrishnan
2015e7bce9 ldap: make mailing list search return fully qualified members 2018-01-19 12:11:26 -08:00
Johannes Zellner
2370b12795 Use the db also for crashnotifier to obtain a valid server domain for sending the mail 2018-01-19 19:26:45 +01:00
Johannes Zellner
18a781b956 Collect common mail sending configs in one function 2018-01-19 18:58:34 +01:00
Johannes Zellner
77206a9d3c Remove fqdn usage from all email templates 2018-01-19 18:56:52 +01:00
Girish Ramakrishnan
73800ac6a7 Linter fixes 2018-01-19 09:55:27 -08:00
Girish Ramakrishnan
27dfd1d6c1 Set the dkim selector when generating keys
The box code owns the dkim dir and selector can change per domain
2018-01-18 21:49:45 -08:00
Girish Ramakrishnan
0833f8830c retire script does not require fqdn 2018-01-18 19:44:43 -08:00
Girish Ramakrishnan
28a240a701 send adminFqdn instead of domain in alive route 2018-01-18 19:44:34 -08:00
Girish Ramakrishnan
5be827cd4e remove use of config.fqdn() in ldap test 2018-01-18 19:35:05 -08:00
Girish Ramakrishnan
0f47dcfae6 ldap: mailbox routes now require the cn to be fully qualified 2018-01-18 19:33:38 -08:00
Girish Ramakrishnan
614f13ffd0 move caas tests to separate file 2018-01-18 14:22:07 -08:00
Girish Ramakrishnan
a850c0813b caas: use caas_config for token verification 2018-01-18 13:51:02 -08:00
Girish Ramakrishnan
4b642a407f remove caas dep from tests 2018-01-18 13:50:57 -08:00
Girish Ramakrishnan
6a87558b52 set default provider to generic 2018-01-18 13:41:40 -08:00
Girish Ramakrishnan
606efb8038 remove caas related logic from oauth test 2018-01-18 13:31:08 -08:00
Girish Ramakrishnan
cfa523f3c1 Remove config.fqdn() fallback in apptask 2018-01-18 12:06:20 -08:00
Girish Ramakrishnan
0e7ebc9637 Move SMTPS port to 2465
This curious change is because gogs and gitea have no way to configure
the SMTPS port and rely on port number ending with 465 to determine
SMTPS (!)
2018-01-18 10:38:39 -08:00
Johannes Zellner
94f7b90705 Add 1.9.3 changes 2018-01-18 15:45:17 +01:00
Johannes Zellner
876b4feb49 The DNS provider property moved to the root dns config object 2018-01-18 12:15:44 +01:00
Girish Ramakrishnan
27252fb2cc 1.10.0 changes 2018-01-17 21:10:11 -08:00
Girish Ramakrishnan
6513aebba6 MAIL_IMAP_USERNAME and MAIL_SMTP_USERNAME should be fully qualified
Re-configure the whole infra to propagate the changes
2018-01-17 21:07:53 -08:00
Girish Ramakrishnan
e9be2a7fb7 Remove is_custom_domain use (use adminFqdn instead) 2018-01-17 21:07:13 -08:00
Girish Ramakrishnan
28dac3fdb3 Fix indentation 2018-01-17 21:05:43 -08:00
Girish Ramakrishnan
f4c3ae639e Use app.domain instead of config.fqdn when setting up mail addon 2018-01-17 21:05:22 -08:00
Girish Ramakrishnan
8c8e387012 bump addons 2018-01-17 20:25:57 -08:00
Girish Ramakrishnan
9e8be3fa50 do basic backupId validation 2018-01-17 16:22:50 -08:00
Girish Ramakrishnan
21058f8b61 exchangeBoxTokenWithUserToken is obsolete 2018-01-17 15:59:33 -08:00
Girish Ramakrishnan
cef0cd4b25 config.token() and arg_token are not used anymore 2018-01-17 15:39:32 -08:00
Girish Ramakrishnan
9cd690e8b4 Do not cache box and user config
this is not needed anymore since webadmin does not keep polling this
2018-01-17 15:38:24 -08:00
Girish Ramakrishnan
116befd111 fix test 2018-01-17 15:13:53 -08:00
Girish Ramakrishnan
fe0ff45c37 caas: autoprovision and use caas and appstore configs 2018-01-17 14:11:19 -08:00
Girish Ramakrishnan
337f919451 dnsConfig is unused 2018-01-17 12:25:33 -08:00
Girish Ramakrishnan
03d2e74e1d use constants 2018-01-17 12:25:14 -08:00
Girish Ramakrishnan
2c42653c24 1.9.2 changes 2018-01-17 10:53:00 -08:00
339 changed files with 22051 additions and 64514 deletions

3
.gitattributes vendored
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

1
.gitignore vendored
View File

@@ -1,7 +1,6 @@
node_modules/
coverage/
webadmin/dist/
setup/splash/website/
installer/src/certs/server.key
# vim swap files

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.

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.

View File

@@ -2,6 +2,7 @@
"node": true,
"browser": true,
"unused": true,
"multistr": true,
"globalstrict": true,
"predef": [ "angular", "$" ],
"esnext": true

192
CHANGES
View File

@@ -1161,3 +1161,195 @@
* Set max email recepient limit (in outgoing emails) to 500
* Put terminal and app logs viewer to separate window
[1.9.2]
* Prepare Cloudron for supporting multiple domains
* Add Cloudron restore UI
* Do not put app in errored state if backup fails
* Display backup progress in CaaS
* Add Google Cloud Storage backend for backups
* Update node to 8.9.3 LTS
* Set max email recepient limit (in outgoing emails) to 500
* Put terminal and app logs viewer to separate window
[1.9.3]
* Prepare Cloudron for supporting multiple domains
* Add Cloudron restore UI
* Do not put app in errored state if backup fails
* Display backup progress in CaaS
* Add Google Cloud Storage backend for backups
* Update node to 8.9.3 LTS
* Set max email recepient limit (in outgoing emails) to 500
* Put terminal and app logs viewer to separate window
[1.9.4]
* Fix typo causing LE cert renewals to fail
[1.10.0]
* Migrate mailboxes to support multiple domains
* Update addon containers to latest versions
* Add DigitalOcean Spaces region Singapore 1 (SGP1)
* Configure Exoscale SOS to use new SOS NG endpoint
* Fix S3 storage backend CopySource encoding rules
[1.10.1]
* Migrate mailboxes to support multiple domains
* Update addon containers to latest versions
* Add DigitalOcean Spaces region Singapore 1 (SGP1)
* Configure Exoscale SOS to use new SOS NG endpoint
* Fix S3 storage backend CopySource encoding rules
[1.10.2]
* Migrate mailboxes to support multiple domains
* Update addon containers to latest versions
* Add DigitalOcean Spaces region Singapore 1 (SGP1)
* 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
[2.4.0]
* Use custom logging backend to have more control over log rotation
* Make user explicitly confirm that fs backup dir is on external storage
* Update node to 8.11.2
* Update docker to 18.03.1
* Fix docker exec terminal resize issue
* Make the mailbox name follow the apps new location, if the user did not set it explicitly
* Add backups view
[2.4.1]
* Use custom logging backend to have more control over log rotation
* Mail logs and box logs UI
* Make user explicitly confirm that fs backup dir is on external storage
* Update node to 8.11.2
* Update docker to 18.03.1
* Fix docker exec terminal resize issue
* Make the mailbox name follow the apps new location, if the user did not set it explicitly
* Add backups view
[3.0.0]
* Support alternate app domains with redirects
* Allow hyphen in mailbox names
* Fix issue where the UI timesout when relay server is not reachable
* Add support for personal spaces
* Add UI to edit users in the groups dialog
* Add UI to set groups when creating a user
* Open logs and terminal in a new tab instead of a window
* Add button to view backup logs
* Add Mailjet mail relay support
* Encryption support for incremental backups
* Display restore errors in the UI
* Update Haraka to 2.8.19
* GPG verify releases
* Allow subdomains in location field
[3.0.1]
* Support alternate app domains with redirects
* Allow hyphen in mailbox names
* Fix issue where the UI timesout when relay server is not reachable
* Add support for personal spaces
* Add UI to edit users in the groups dialog
* Add UI to set groups when creating a user
* Open logs and terminal in a new tab instead of a window
* Add button to view backup logs
* Add Mailjet mail relay support
* Encryption support for incremental backups
* Display restore errors in the UI
* Update Haraka to 2.8.19
* GPG verify releases
* Allow subdomains in location field
[3.0.2]
* Fix issue where normal users are shown apps they don't have access to
* Re-configure mail apps when mail is enabled/disabled

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

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)

View File

@@ -29,7 +29,7 @@ function create_droplet() {
local ssh_key_id="$1"
local box_name="$2"
local image_region="sfo1"
local image_region="sfo2"
local ubuntu_image_slug="ubuntu-16-04-x64"
local box_size="1gb"

View File

@@ -61,7 +61,7 @@ echo "==> Installing Docker"
mkdir -p /etc/systemd/system/docker.service.d
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=overlay2" > /etc/systemd/system/docker.service.d/cloudron.conf
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
curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_18.03.1~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
# apt install with install deps (as opposed to dpkg -i)
apt install -y /tmp/docker.deb
rm /tmp/docker.deb
@@ -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

View File

@@ -2,6 +2,8 @@
'use strict';
var database = require('./src/database.js');
var sendFailureLogs = require('./src/logcollector').sendFailureLogs;
function main() {
@@ -10,7 +12,12 @@ function main() {
var processName = process.argv[2];
console.log('Started crash notifier for', processName);
sendFailureLogs(processName, { unit: processName });
// mailer needs the db
database.initialize(function (error) {
if (error) return console.error('Cannot connect to database. Unable to send crash log.', error);
sendFailureLogs(processName, { unit: processName });
});
}
main();

View File

@@ -1,257 +0,0 @@
/* jslint node:true */
'use strict';
var argv = require('yargs').argv,
autoprefixer = require('gulp-autoprefixer'),
concat = require('gulp-concat'),
cssnano = require('gulp-cssnano'),
ejs = require('gulp-ejs'),
gulp = require('gulp'),
rimraf = require('rimraf'),
sass = require('gulp-sass'),
serve = require('gulp-serve'),
sourcemaps = require('gulp-sourcemaps'),
uglify = require('gulp-uglify'),
url = require('url');
gulp.task('3rdparty', function () {
gulp.src([
'webadmin/src/3rdparty/**/*.js',
'webadmin/src/3rdparty/**/*.map',
'webadmin/src/3rdparty/**/*.css',
'webadmin/src/3rdparty/**/*.otf',
'webadmin/src/3rdparty/**/*.eot',
'webadmin/src/3rdparty/**/*.svg',
'webadmin/src/3rdparty/**/*.gif',
'webadmin/src/3rdparty/**/*.ttf',
'webadmin/src/3rdparty/**/*.woff',
'webadmin/src/3rdparty/**/*.woff2'
])
.pipe(gulp.dest('webadmin/dist/3rdparty/'))
.pipe(gulp.dest('setup/splash/website/3rdparty'));
gulp.src('node_modules/bootstrap-sass/assets/javascripts/bootstrap.min.js')
.pipe(gulp.dest('webadmin/dist/3rdparty/js'))
.pipe(gulp.dest('setup/splash/website/3rdparty/js'));
});
// --------------
// JavaScript
// --------------
if (argv.help || argv.h) {
console.log('Supported arguments for "gulp develop":');
console.log(' --client-id <clientId>');
console.log(' --client-secret <clientSecret>');
console.log(' --api-origin <cloudron api uri>');
process.exit(1);
}
gulp.task('js', ['js-index', 'js-logs', 'js-terminal', 'js-setup', 'js-setupdns', 'js-restore', 'js-update'], function () {});
var oauth = {
clientId: argv.clientId || 'cid-webadmin',
clientSecret: argv.clientSecret || 'unused',
apiOrigin: argv.apiOrigin || '',
apiOriginHostname: argv.apiOrigin ? url.parse(argv.apiOrigin).hostname : ''
};
console.log();
console.log('Using OAuth credentials:');
console.log(' ClientId: %s', oauth.clientId);
console.log(' ClientSecret: %s', oauth.clientSecret);
console.log(' Cloudron API: %s', oauth.apiOrigin || 'default');
console.log(' Cloudron Host: %s', oauth.apiOriginHostname);
console.log();
gulp.task('js-index', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src([
'webadmin/src/js/index.js',
'webadmin/src/js/client.js',
'webadmin/src/js/appstore.js',
'webadmin/src/js/main.js',
'webadmin/src/views/*.js'
])
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('index.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist/js'));
});
gulp.task('js-logs', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['webadmin/src/js/logs.js', 'webadmin/src/js/client.js'])
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('logs.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist/js'));
});
gulp.task('js-terminal', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['webadmin/src/js/terminal.js', 'webadmin/src/js/client.js'])
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('terminal.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist/js'));
});
gulp.task('js-setup', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['webadmin/src/js/setup.js', 'webadmin/src/js/client.js'])
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('setup.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist/js'));
});
gulp.task('js-setupdns', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['webadmin/src/js/setupdns.js', 'webadmin/src/js/client.js'])
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('setupdns.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist/js'));
});
gulp.task('js-restore', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['webadmin/src/js/restore.js', 'webadmin/src/js/client.js'])
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('restore.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist/js'));
});
gulp.task('js-update', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['webadmin/src/js/update.js'])
.pipe(sourcemaps.init())
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist/js'))
.pipe(gulp.dest('setup/splash/website/js'));
});
// --------------
// HTML
// --------------
gulp.task('html', ['html-views', 'html-update', 'html-templates'], function () {
return gulp.src('webadmin/src/*.html').pipe(ejs({ apiOriginHostname: oauth.apiOriginHostname }, {}, { ext: '.html' })).pipe(gulp.dest('webadmin/dist'));
});
gulp.task('html-update', function () {
return gulp.src(['webadmin/src/update.html']).pipe(gulp.dest('setup/splash/website'));
});
gulp.task('html-views', function () {
return gulp.src('webadmin/src/views/**/*.html').pipe(gulp.dest('webadmin/dist/views'));
});
gulp.task('html-templates', function () {
return gulp.src('webadmin/src/templates/**/*.html').pipe(gulp.dest('webadmin/dist/templates'));
});
// --------------
// CSS
// --------------
gulp.task('css', function () {
return gulp.src('webadmin/src/*.scss')
.pipe(sourcemaps.init())
.pipe(sass({ includePaths: ['node_modules/bootstrap-sass/assets/stylesheets/'] }).on('error', sass.logError))
.pipe(autoprefixer())
.pipe(cssnano())
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist'))
.pipe(gulp.dest('setup/splash/website'));
});
gulp.task('images', function () {
return gulp.src('webadmin/src/img/**')
.pipe(gulp.dest('webadmin/dist/img'));
});
// --------------
// Utilities
// --------------
gulp.task('watch', ['default'], function () {
gulp.watch(['webadmin/src/*.scss'], ['css']);
gulp.watch(['webadmin/src/img/*'], ['images']);
gulp.watch(['webadmin/src/**/*.html'], ['html']);
gulp.watch(['webadmin/src/views/*.html'], ['html-views']);
gulp.watch(['webadmin/src/templates/*.html'], ['html-templates']);
gulp.watch(['webadmin/src/js/update.js'], ['js-update']);
gulp.watch(['webadmin/src/js/setup.js', 'webadmin/src/js/client.js'], ['js-setup']);
gulp.watch(['webadmin/src/js/setupdns.js', 'webadmin/src/js/client.js'], ['js-setupdns']);
gulp.watch(['webadmin/src/js/restore.js', 'webadmin/src/js/client.js'], ['js-restore']);
gulp.watch(['webadmin/src/js/logs.js', 'webadmin/src/js/client.js'], ['js-logs']);
gulp.watch(['webadmin/src/js/terminal.js', 'webadmin/src/js/client.js'], ['js-terminal']);
gulp.watch(['webadmin/src/js/index.js', 'webadmin/src/js/client.js', 'webadmin/src/js/appstore.js', 'webadmin/src/js/main.js', 'webadmin/src/views/*.js'], ['js-index']);
gulp.watch(['webadmin/src/3rdparty/**/*'], ['3rdparty']);
});
gulp.task('clean', function () {
rimraf.sync('webadmin/dist');
rimraf.sync('setup/splash/website');
});
gulp.task('default', ['clean', 'html', 'js', '3rdparty', 'images', 'css'], function () {});
gulp.task('develop', ['watch'], serve({ root: 'webadmin/dist', port: 4000 }));

View File

@@ -0,0 +1,24 @@
'use strict';
exports.up = function(db, callback) {
var cmd = 'CREATE TABLE IF NOT EXISTS mail(' +
'domain VARCHAR(128) NOT NULL UNIQUE,' +
'enabled BOOLEAN DEFAULT 0,' +
'mailFromValidation BOOLEAN DEFAULT 1,' +
'catchAllJson TEXT,' +
'relayJson TEXT,' +
'FOREIGN KEY(domain) REFERENCES domains(domain),' +
'PRIMARY KEY(domain)) CHARACTER SET utf8 COLLATE utf8_bin';
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE mail', function (error) {
if (error) console.error(error);
callback(error);
});
};

View File

@@ -0,0 +1,34 @@
'use strict';
exports.up = function(db, callback) {
db.all('SELECT * FROM domains', function (error, domains) {
if (error) return callback(error);
if (domains.length === 0) return callback();
db.all('SELECT * FROM settings', function (error, allSettings) {
if (error) return callback(error);
// defaults
var mailFromValidation = true;
var catchAll = [ ];
var relay = { provider: 'cloudron-smtp' };
var mailEnabled = false;
allSettings.forEach(function (setting) {
switch (setting.name) {
case 'mail_from_validation': mailFromValidation = !!setting.value; break;
case 'catch_all_address': catchAll = JSON.parse(setting.value); break;
case 'mail_relay': relay = JSON.parse(setting.value); break;
case 'mail_config': mailEnabled = JSON.parse(setting.value).enabled; break;
}
});
db.runSql('INSERT INTO mail (domain, enabled, mailFromValidation, catchAllJson, relayJson) VALUES (?, ?, ?, ?, ?)',
[ domains[0].domain, mailEnabled, mailFromValidation, JSON.stringify(catchAll), JSON.stringify(relay) ], callback);
});
});
};
exports.down = function(db, callback) {
callback();
};

View File

@@ -0,0 +1,44 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.all('SELECT * FROM users', [ ], function (error, users) {
if (error) return callback(error);
db.all('SELECT * FROM mail WHERE enabled=1', [ ], function (error, mailDomains) {
if (error) return callback(error);
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE users DROP INDEX users_email'),
db.runSql.bind(db, 'ALTER TABLE users ADD COLUMN fallbackEmail VARCHAR(512) DEFAULT ""'),
function setDefaults(done) {
async.eachSeries(users, function (user, iteratorCallback) {
var defaultEmail = '';
var fallbackEmail = '';
if (mailDomains.length === 0) {
defaultEmail = user.email;
fallbackEmail = user.email;
} else {
defaultEmail = user.username ? (user.username + '@' + mailDomains[0].domain) : user.email;
fallbackEmail = user.email;
}
db.runSql('UPDATE users SET email = ?, fallbackEmail = ? WHERE id = ?', [ defaultEmail, fallbackEmail, user.id ], iteratorCallback);
}, done);
},
db.runSql.bind(db, 'ALTER TABLE users ADD UNIQUE users_email (email)'),
db.runSql.bind(db, 'COMMIT')
], callback);
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN fallbackEmail', function (error) {
if (error) console.error(error);
callback(error);
});
};

View File

@@ -0,0 +1,26 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.all('SELECT * FROM settings WHERE name = ?', [ 'tls_config' ], function (error, result) {
if (error) return callback(error);
var tlsConfig = (result[0] && result[0].value) ? JSON.parse(result[0].value) : { provider: 'letsencrypt-prod'};
tlsConfig.provider = tlsConfig.provider.replace(/$le\-/, 'letsencrypt-'); // old cloudrons had le-prod/le-staging
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE domains ADD COLUMN tlsConfigJson TEXT'),
db.runSql.bind(db, 'UPDATE domains SET tlsConfigJson = ?', [ JSON.stringify(tlsConfig) ]),
db.runSql.bind(db, 'COMMIT')
], callback);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE domains DROP COLUMN tlsConfigJson', function (error) {
if (error) console.error(error);
callback(error);
});
};

View File

@@ -0,0 +1,47 @@
'use strict';
var async = require('async'),
fs = require('fs'),
superagent = require('superagent');
exports.up = function(db, callback) {
if (!fs.existsSync('/home/yellowtent/configs/cloudron.conf')) {
console.log('Unable to locate cloudron.conf');
return callback();
}
var config = JSON.parse(fs.readFileSync('/home/yellowtent/configs/cloudron.conf', 'utf8'));
if (config.provider !== 'caas' || !config.fqdn) {
console.log('Not caas (%s) or no fqdn', config.provider, config.fqdn);
return callback();
}
db.runSql('SELECT COUNT(*) AS total FROM users', function (error, result) {
if (error) return callback(error);
if (result[0].total === 0) {
console.log('This cloudron is not activated. It will automatically get appstore and caas configs from autoprovision logic');
return callback();
}
console.log('Downloading appstore and caas config');
superagent.get(config.apiServerOrigin + `/api/v1/boxes/${config.fqdn}/config`)
.query({ token: config.token })
.timeout(30 * 1000).end(function (error, result) {
if (error) return callback(error);
console.log('Adding %j config', result.body);
async.series([
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', [ 'appstore_config', JSON.stringify(result.body.appstoreConfig) ]),
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', [ 'caas_config', JSON.stringify(result.body.caasConfig) ])
], callback);
});
});
};
exports.down = function(db, callback) {
callback();
};

View File

@@ -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();
};

View File

@@ -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);
};

View File

@@ -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);
});
};

View File

@@ -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);
});
};

View File

@@ -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);
});
};

View File

@@ -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);
});
};

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();
};

View File

@@ -0,0 +1,28 @@
'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 apps ADD COLUMN ownerId VARCHAR(128)'),
function (next) {
db.all('SELECT id FROM users ORDER BY createdAt LIMIT 1', [ ], function (error, results) {
if (error || results.length === 0) return next(error);
var ownerId = results[0].id;
db.runSql('UPDATE apps SET ownerId=?', [ ownerId ], next);
});
},
db.runSql.bind(db, 'ALTER TABLE apps MODIFY ownerId VARCHAR(128) NOT NULL'),
db.runSql.bind(db, 'ALTER TABLE apps ADD CONSTRAINT apps_owner_constraint FOREIGN KEY(ownerId) REFERENCES users(id)'),
db.runSql.bind(db, 'COMMIT'),
], callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN ownerId', function (error) {
if (error) console.error(error);
callback(error);
});
};

View File

@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN ts TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN ts ', function (error) {
if (error) console.error(error);
callback(error);
});
};

View File

@@ -0,0 +1,25 @@
'use strict';
exports.up = function(db, callback) {
var cmd = 'CREATE TABLE IF NOT EXISTS subdomains(' +
'appId VARCHAR(128) NOT NULL,' +
'domain VARCHAR(128) NOT NULL,' +
'subdomain VARCHAR(128) NOT NULL,' +
'type VARCHAR(128) NOT NULL,' +
'dnsRecordId VARCHAR(512),' +
'FOREIGN KEY(domain) REFERENCES domains(domain),' +
'FOREIGN KEY(appId) REFERENCES apps(id),' +
'UNIQUE (subdomain, domain)) CHARACTER SET utf8 COLLATE utf8_bin';
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE subdomains', function (error) {
if (error) console.error(error);
callback(error);
});
};

View File

@@ -0,0 +1,28 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.all('SELECT * from apps', [ ], function (error, results) {
if (error) return done(error);
var queries = [
db.runSql.bind(db, 'START TRANSACTION;')
];
results.forEach(function (app) {
queries.push(db.runSql.bind(db, 'INSERT INTO subdomains (appId, domain, subdomain, type, dnsRecordId) VALUES (?, ?, ?, ?, ?)', [ app.id, app.domain, app.location, 'primary', app.dnsRecordId ]));
});
queries.push(db.runSql.bind(db, 'COMMIT'));
async.series(queries, callback);
});
};
exports.down = function(db, callback) {
db.runSql('DELETE FROM subdomains', function (error) {
if (error) console.error(error);
callback(error);
});
};

View File

@@ -0,0 +1,41 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps DROP INDEX location_domain_unique_index, DROP FOREIGN KEY apps_domain_constraint, DROP COLUMN domain, DROP COLUMN location, DROP COLUMN dnsRecordId', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.all('SELECT * from subdomains WHERE type = ?', [ 'primary' ], function (error, results) {
if (error) return callback(error);
var cmd = 'ALTER TABLE apps'
+ ' ADD COLUMN location VARCHAR(128),'
+ ' ADD COLUMN domain VARCHAR(128),'
+ ' ADD COLUMN dnsRecordId VARCHAR(512)';
db.runSql(cmd, function (error) {
if (error) return callback(error);
var queries = [ db.runSql.bind(db, 'START TRANSACTION;') ];
results.forEach(function (d) {
queries.push(db.runSql.bind(db, 'UPDATE apps SET domain = ?, location = ?, dnsRecordId = ? WHERE id = ?', [ d.domain, d.subdomain, d.appId, d.dnsRecordId ]));
});
queries.push(db.runSql.bind(db, 'COMMIT'));
async.series(queries, function (error) {
if (error) return callback(error);
var cmd = 'ALTER TABLE apps'
+ ' ADD CONSTRAINT apps_domain_constraint FOREIGN KEY(domain) REFERENCES domains(domain),'
+ ' ADD UNIQUE location_domain_unique_index (location, domain)';
db.runSql(cmd, callback);
});
});
});
};

View File

@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE subdomains DROP COLUMN dnsRecordId', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE subdomains ADD COLUMN dnsRecordId VARCHAR(512)', function (error) {
if (error) return callback(error);
callback();
});
};

View File

@@ -0,0 +1,34 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN admin BOOLEAN DEFAULT 0', function (error) {
if (error) return callback(error);
db.all('SELECT userId FROM groupMembers WHERE groupId=?', [ 'admin' ], function (error, results) {
if (error) return callback(error);
if (results.length === 0) return callback();
async.eachSeries(results, function (result, iteratorDone) {
db.runSql('UPDATE users SET admin=1 WHERE id=?', [ result.userId ], iteratorDone);
}, function (error) {
if (error) return callback(error);
async.series([
db.runSql.bind(db, 'DELETE FROM groupMembers WHERE groupId=?', [ 'admin' ]),
db.runSql.bind(db, 'DELETE FROM groups WHERE id=?', [ 'admin' ])
], callback);
});
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN admin', function (error) {
if (error) console.error(error);
callback(error);
});
};

View File

@@ -0,0 +1,13 @@
'use strict';
exports.up = function(db, callback) {
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();
};

View File

@@ -21,8 +21,12 @@ 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 '',
displayName VARCHAR(512) DEFAULT "",
fallbackEmail VARCHAR(512) DEFAULT "",
twoFactorAuthenticationSecret VARCHAR(128) DEFAULT "",
twoFactorAuthenticationEnabled BOOLEAN DEFAULT false,
admin BOOLEAN DEFAULT false,
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS groups(
@@ -65,12 +69,11 @@ CREATE TABLE IF NOT EXISTS apps(
httpPort INTEGER, // this is the nginx proxy port and not manifest.httpPort
location VARCHAR(128) NOT NULL,
domain VARCHAR(128) NOT NULL,
dnsRecordId VARCHAR(512), // tracks any id that we got back to track dns updates
accessRestrictionJson TEXT, // { users: [ ], groups: [ ] }
createdAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
updatedAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
creationTime TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app was installed
updateTime TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the last app update was done
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, // when this db record was updated (useful for UI caching)
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
@@ -79,10 +82,12 @@ 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),
ownerId VARCHAR(128),
FOREIGN KEY(ownerId) REFERENCES users(id),
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS appPortBindings(
@@ -128,32 +133,60 @@ 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));
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 */
provider VARCHAR(16) NOT NULL,
configJson TEXT, /* JSON containing the dns backend provider config */
tlsConfigJson TEXT, /* JSON containing the tls provider config */
PRIMARY KEY (domain))
/* the default db collation is utf8mb4_unicode_ci but for the app table domain constraint we have to use the old one */
CHARACTER SET utf8 COLLATE utf8_bin;
CREATE TABLE IF NOT EXISTS mail(
domain VARCHAR(128) NOT NULL UNIQUE,
enabled BOOLEAN DEFAULT 0, /* MDA enabled */
mailFromValidation BOOLEAN DEFAULT 1,
catchAllJson TEXT,
relayJson TEXT,
FOREIGN KEY(domain) REFERENCES domains(domain),
PRIMARY KEY(domain))
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));
CREATE TABLE IF NOT EXISTS subdomains(
appId VARCHAR(128) NOT NULL,
domain VARCHAR(128) NOT NULL,
subdomain VARCHAR(128) NOT NULL,
type VARCHAR(128) NOT NULL,
FOREIGN KEY(domain) REFERENCES domains(domain),
PRIMARY KEY (name));
FOREIGN KEY(appId) REFERENCES apps(id),
UNIQUE (subdomain, 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 */
provider VARCHAR(16) NOT NULL,
configJson TEXT, /* JSON containing the dns backend provider config */
PRIMARY KEY (domain))
/* the default db collation is utf8mb4_unicode_ci but for the app table domain constraint we have to use the old one */
CHARACTER SET utf8 COLLATE utf8_bin;

12102
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,13 +14,13 @@
"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.2",
"@google-cloud/storage": "^1.7.0",
"@sindresorhus/df": "^2.1.0",
"async": "^2.6.0",
"aws-sdk": "^2.151.0",
"body-parser": "^1.18.2",
"cloudron-manifestformat": "^2.10.0",
"async": "^2.6.1",
"aws-sdk": "^2.253.1",
"body-parser": "^1.18.3",
"cloudron-manifestformat": "^2.11.0",
"connect-ensure-login": "^0.1.1",
"connect-lastmile": "^1.0.2",
"connect-timeout": "^1.9.0",
@@ -28,24 +28,23 @@
"cookie-session": "^1.3.2",
"cron": "^1.3.0",
"csurf": "^1.6.6",
"db-migrate": "^0.10.0-beta.24",
"db-migrate": "^0.11.1",
"db-migrate-mysql": "^1.1.10",
"debug": "^3.1.0",
"dockerode": "^2.5.3",
"ejs": "^2.5.7",
"ejs-cli": "^2.0.0",
"express": "^4.16.2",
"dockerode": "^2.5.5",
"ejs": "^2.6.1",
"ejs-cli": "^2.0.1",
"express": "^4.16.3",
"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",
"moment-timezone": "^0.5.14",
"mime": "^2.3.1",
"moment-timezone": "^0.5.17",
"morgan": "^1.9.0",
"multiparty": "^4.1.2",
"multiparty": "^4.1.4",
"mysql": "^2.15.0",
"nodemailer": "^4.4.0",
"nodemailer": "^4.6.5",
"nodemailer-smtp-transport": "^2.7.4",
"oauth2orize": "^1.11.0",
"once": "^1.3.2",
@@ -55,48 +54,39 @@
"passport-http-bearer": "^1.0.1",
"passport-local": "^1.0.0",
"passport-oauth2-client-password": "^0.1.2",
"password-generator": "^2.2.0",
"progress-stream": "^2.0.0",
"proxy-middleware": "^0.15.0",
"recursive-readdir": "^2.2.1",
"request": "^2.83.0",
"s3-block-read-stream": "^0.2.0",
"qrcode": "^1.2.0",
"readdirp": "^2.1.0",
"request": "^2.87.0",
"rimraf": "^2.6.2",
"s3-block-read-stream": "^0.5.0",
"safetydance": "^0.7.1",
"semver": "^5.4.1",
"showdown": "^1.8.2",
"semver": "^5.5.0",
"showdown": "^1.8.6",
"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",
"underscore": "^1.7.0",
"uuid": "^3.1.0",
"superagent": "^3.8.3",
"supererror": "^0.7.2",
"tar-fs": "^1.16.2",
"tar-stream": "^1.6.1",
"tldjs": "^2.3.1",
"underscore": "^1.9.1",
"uuid": "^3.2.1",
"valid-url": "^1.0.9",
"validator": "^9.1.1",
"ws": "^3.3.1"
"validator": "^10.3.0",
"ws": "^5.2.0"
},
"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.2.0",
"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"
"recursive-readdir": "^2.2.2"
},
"scripts": {
"migrate_local": "DATABASE_URL=mysql://root:@localhost/box node_modules/.bin/db-migrate up",
@@ -106,6 +96,6 @@
"postmerge": "/bin/true",
"precommit": "/bin/true",
"prepush": "npm test",
"webadmin": "node_modules/.bin/gulp"
"dashboard": "node_modules/.bin/gulp"
}
}

122
scripts/cloudron-activate Executable file
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", "externalDisk": true}' "${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!"

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
@@ -42,97 +36,91 @@ if [[ "${disk_size_gb}" -lt "${MINIMUM_DISK_SIZE_GB}" ]]; then
exit 1
fi
if systemctl -q is-active box; then
echo "Error: Cloudron is already installed. To reinstall, start afresh"
exit 1
fi
initBaseImage="true"
# provisioning data
domain=""
adminLocation="my"
zoneName=""
provider=""
tlsProvider="le-prod"
requestedVersion=""
apiServerOrigin="https://api.cloudron.io"
webServerOrigin="https://cloudron.io"
dataJson=""
prerelease="false"
sourceTarballUrl=""
rebootServer="true"
baseDataDir=""
# these are here for pre-1.9 compat
encryptionKey=""
restoreUrl=""
args=$(getopt -o "" -l "domain:,help,skip-baseimage-init,data:,data-dir:,provider:,encryption-key:,restore-url:,tls-provider:,version:,dns-provider:,env:,admin-location:,prerelease,skip-reboot,source-url:" -n "$0" -- "$@")
args=$(getopt -o "" -l "help,skip-baseimage-init,data-dir:,provider:,version:,env:,prerelease,skip-reboot" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--domain) domain="$2"; shift 2;;
--admin-location) adminLocation="$2"; shift 2;;
--help) echo "See https://cloudron.io/documentation/installation/ on how to install Cloudron"; exit 0;;
--provider) provider="$2"; shift 2;;
--encryption-key) encryptionKey="$2"; shift 2;;
--restore-url) restoreUrl="$2"; shift 2;;
--tls-provider) tlsProvider="$2"; shift 2;;
--version) requestedVersion="$2"; shift 2;;
--env)
if [[ "$2" == "dev" ]]; then
apiServerOrigin="https://api.dev.cloudron.io"
webServerOrigin="https://dev.cloudron.io"
tlsProvider="le-staging"
prerelease="true"
elif [[ "$2" == "staging" ]]; then
apiServerOrigin="https://api.staging.cloudron.io"
webServerOrigin="https://staging.cloudron.io"
tlsProvider="le-staging"
prerelease="true"
fi
shift 2;;
--skip-baseimage-init) initBaseImage="false"; shift;;
--skip-reboot) rebootServer="false"; shift;;
--data) dataJson="$2"; shift 2;;
--prerelease) prerelease="true"; shift;;
--source-url) sourceTarballUrl="$2"; version="0.0.1+custom"; shift 2;;
--data-dir) baseDataDir=$(realpath "$2"); shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
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 "${dataJson}" ]]; then
if [[ -z "${provider}" ]]; then
echo "--provider is required (azure, cloudscale.ch, digitalocean, ec2, exoscale, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
exit 1
elif [[ \
"${provider}" != "ami" && \
"${provider}" != "azure" && \
"${provider}" != "cloudscale.ch" && \
"${provider}" != "digitalocean" && \
"${provider}" != "ec2" && \
"${provider}" != "exoscale" && \
"${provider}" != "gce" && \
"${provider}" != "lightsail" && \
"${provider}" != "linode" && \
"${provider}" != "ovh" && \
"${provider}" != "rosehosting" && \
"${provider}" != "scaleway" && \
"${provider}" != "vultr" && \
"${provider}" != "generic" \
]]; then
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, gce, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
exit 1
fi
if [[ -z "${provider}" ]]; then
echo "--provider is required (azure, cloudscale, digitalocean, ec2, exoscale, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
exit 1
elif [[ \
"${provider}" != "ami" && \
"${provider}" != "azure" && \
"${provider}" != "caas" && \
"${provider}" != "cloudscale" && \
"${provider}" != "digitalocean" && \
"${provider}" != "ec2" && \
"${provider}" != "exoscale" && \
"${provider}" != "gce" && \
"${provider}" != "hetzner" && \
"${provider}" != "lightsail" && \
"${provider}" != "linode" && \
"${provider}" != "ovh" && \
"${provider}" != "rosehosting" && \
"${provider}" != "scaleway" && \
"${provider}" != "vultr" && \
"${provider}" != "generic" \
]]; then
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, gce, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
exit 1
fi
if [[ "${tlsProvider}" != "fallback" && "${tlsProvider}" != "le-prod" && "${tlsProvider}" != "le-staging" ]]; then
echo "--tls-provider must be one of: le-prod, le-staging, fallback"
exit 1
fi
if [[ -n "${baseDataDir}" && ! -d "${baseDataDir}" ]]; then
echo "${baseDataDir} does not exist"
exit 1
fi
if [[ -n "${baseDataDir}" && ! -d "${baseDataDir}" ]]; then
echo "${baseDataDir} does not exist"
exit 1
fi
echo ""
@@ -143,7 +131,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
@@ -160,74 +148,33 @@ if [[ "${initBaseImage}" == "true" ]]; then
fi
echo "=> Checking version"
if [[ "${sourceTarballUrl}" == "" ]]; then
if ! releaseJson=$($curl -s "${apiServerOrigin}/api/v1/releases?prerelease=${prerelease}&boxVersion=${requestedVersion}"); then
echo "Failed to get release information"
exit 1
fi
if ! releaseJson=$($curl -s "${apiServerOrigin}/api/v1/releases?prerelease=${prerelease}&boxVersion=${requestedVersion}"); then
echo "Failed to get release information"
exit 1
fi
if [[ "$requestedVersion" == "" ]]; then
version=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["version"])')
else
version="${requestedVersion}"
fi
if [[ "$requestedVersion" == "" ]]; then
version=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["version"])')
else
version="${requestedVersion}"
fi
if ! sourceTarballUrl=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["info"]["sourceTarballUrl"])'); then
echo "No source code for version '${requestedVersion:-latest}'"
exit 1
fi
if ! sourceTarballUrl=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["info"]["sourceTarballUrl"])'); then
echo "No source code for version '${requestedVersion:-latest}'"
exit 1
fi
# Build data
# tlsConfig, dnsConfig, backupConfig are here for backward compat with < 1.9
# from 1.9, we use autoprovision.json
if [[ -z "${dataJson}" ]]; then
if [[ -z "${restoreUrl}" ]]; then
data=$(cat <<EOF
{
"fqdn": "${domain}",
"adminLocation": "${adminLocation}",
"adminFqdn": "${adminLocation}.${domain}",
"zoneName": "${zoneName}",
"provider": "${provider}",
"apiServerOrigin": "${apiServerOrigin}",
"webServerOrigin": "${webServerOrigin}",
"version": "${version}",
"tlsConfig": {
"provider": "${tlsProvider}"
},
"backupConfig" : {
"provider": "filesystem",
"backupFolder": "/var/backups",
"key": "${encryptionKey}",
"format": "tgz",
"retentionSecs": 172800
}
}
data=$(cat <<EOF
{
"provider": "${provider}",
"apiServerOrigin": "${apiServerOrigin}",
"webServerOrigin": "${webServerOrigin}",
"version": "${version}"
}
EOF
)
else
data=$(cat <<EOF
{
"fqdn": "${domain}",
"adminLocation": "${adminLocation}",
"adminFqdn": "${adminLocation}.${domain}",
"zoneName": "${zoneName}",
"provider": "${provider}",
"apiServerOrigin": "${apiServerOrigin}",
"webServerOrigin": "${webServerOrigin}",
"restore": {
"url": "${restoreUrl}",
"key": "${encryptionKey}"
},
"version": "${version}"
}
EOF
)
fi
else
data="${dataJson}"
fi
)
echo "=> Downloading version ${version} ..."
box_src_tmp_dir=$(mktemp -dt box-src-XXXXXX)
@@ -258,31 +205,15 @@ echo -n "=> Waiting for cloudron to be ready (this takes some time) ..."
while true; do
echo -n "."
if status=$($curl -q -f "http://localhost:3000/api/v1/cloudron/status" 2>/dev/null); then
[[ -z "$domain" ]] && break # with no domain, we are up and running
[[ "$status" == *"\"tls\": true"* ]] && break # with a domain, wait for the cert
break # we are up and running
fi
sleep 10
done
autoprovision_data=$(cat <<EOF
{
"tlsConfig": {
"provider": "${tlsProvider}"
}
}
EOF
)
echo "${autoprovision_data}" > /home/yellowtent/configs/autoprovision.json
if [[ -n "${domain}" ]]; then
echo -e "\n\nVisit https://my.${domain} to finish setup once the server has rebooted.\n"
else
echo -e "\n\nVisit https://<IP> to finish setup once the server has rebooted.\n"
fi
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

View File

@@ -7,17 +7,15 @@ set -eu
[[ $(uname -s) == "Darwin" ]] && GNU_GETOPT="/usr/local/opt/gnu-getopt/bin/getopt" || GNU_GETOPT="getopt"
readonly GNU_GETOPT
args=$(${GNU_GETOPT} -o "" -l "revision:,output:" -n "$0" -- "$@")
args=$(${GNU_GETOPT} -o "" -l "output:" -n "$0" -- "$@")
eval set -- "${args}"
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
commitish="HEAD"
bundle_file=""
while true; do
case "$1" in
--revision) commitish="$2"; shift 2;;
--output) bundle_file="$2"; shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
@@ -27,60 +25,56 @@ done
readonly TMPDIR=${TMPDIR:-/tmp} # why is this not set on mint?
if ! $(cd "${SOURCE_DIR}" && git diff --exit-code >/dev/null); then
echo "You have local changes, stash or commit them to proceed"
echo "You have local changes in box, stash or commit them to proceed"
exit 1
fi
if [[ "$(node --version)" != "v8.9.3" ]]; then
echo "This script requires node 8.9.3"
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
version=$(cd "${SOURCE_DIR}" && git rev-parse "${commitish}")
if [[ "$(node --version)" != "v8.11.2" ]]; then
echo "This script requires node 8.11.2"
exit 1
fi
box_version=$(cd "${SOURCE_DIR}" && git rev-parse "HEAD")
branch=$(git rev-parse --abbrev-ref HEAD)
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-${version}.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 [${version}] into ${bundle_dir}"
(cd "${SOURCE_DIR}" && git archive --format=tar ${version} | (cd "${bundle_dir}" && tar xf -))
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}/../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}"

View File

@@ -35,27 +35,33 @@ 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.1-ce" ]]; then
$curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_18.03.1~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
echo "docker binary download is corrupt"
if [[ $(sha256sum /tmp/docker.deb | cut -d' ' -f1) != "54f4c9268492a4fd2ec2e6bcc95553855b025f35dcc8b9f60ac34e0aa307279b" ]]; then
echo "==> installer: docker binary download is corrupt"
exit 5
fi
echo "Waiting for all dpkg tasks to finish..."
echo "==> installer: Waiting for all dpkg tasks to finish..."
while fuser /var/lib/dpkg/lock; do
sleep 1
done
while ! dpkg --force-confold --configure -a; do
echo "Failed to fix packages. Retry"
echo "==> installer: Failed to fix packages. Retry"
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 "Failed to install docker. Retry"
echo "==> installer: Failed to install docker. Retry"
sleep 1
done
@@ -63,14 +69,15 @@ if [[ $(docker version --format {{.Client.Version}}) != "17.09.0-ce" ]]; then
fi
echo "==> installer: updating node"
if [[ "$(node --version)" != "v8.9.3" ]]; then
mkdir -p /usr/local/node-8.9.3
$curl -sL https://nodejs.org/dist/v8.9.3/node-v8.9.3-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-8.9.3
ln -sf /usr/local/node-8.9.3/bin/node /usr/bin/node
ln -sf /usr/local/node-8.9.3/bin/npm /usr/bin/npm
if [[ "$(node --version)" != "v8.11.2" ]]; then
mkdir -p /usr/local/node-8.11.2
$curl -sL https://nodejs.org/dist/v8.11.2/node-v8.11.2-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-8.11.2
ln -sf /usr/local/node-8.11.2/bin/node /usr/bin/node
ln -sf /usr/local/node-8.11.2/bin/npm /usr/bin/npm
rm -rf /usr/local/node-6.11.5
fi
# this is here (and not in updater.js) because rebuild requires the above node
for try in `seq 1 10`; do
# for reasons unknown, the dtrace package will fail. but rebuilding second time will work
@@ -78,23 +85,34 @@ for try in `seq 1 10`; do
# however by default npm drops privileges for npm rebuild
# https://docs.npmjs.com/misc/config#unsafe-perm
if cd "${box_src_tmp_dir}" && npm rebuild --unsafe-perm; then break; fi
echo "Failed to rebuild, trying again"
echo "==> installer: Failed to rebuild, trying again"
sleep 5
done
if [[ ${try} -eq 10 ]]; then
echo "npm rebuild failed"
echo "==> installer: npm rebuild failed, giving up"
exit 4
fi
echo "==> installer: update cloudron-syslog"
CLOUDRON_SYSLOG_DIR=/usr/local/cloudron-syslog
CLOUDRON_SYSLOG="${CLOUDRON_SYSLOG_DIR}/bin/cloudron-syslog"
CLOUDRON_SYSLOG_VERSION="1.0.3"
while [[ ! -f "${CLOUDRON_SYSLOG}" || "$(${CLOUDRON_SYSLOG} --version)" != ${CLOUDRON_SYSLOG_VERSION} ]]; do
rm -rf "${CLOUDRON_SYSLOG_DIR}"
mkdir -p "${CLOUDRON_SYSLOG_DIR}"
if npm install --unsafe-perm -g --prefix "${CLOUDRON_SYSLOG_DIR}" cloudron-syslog@${CLOUDRON_SYSLOG_VERSION}; then break; fi
echo "===> installer: Failed to install cloudron-syslog, trying again"
sleep 5
done
if ! id "${USER}" 2>/dev/null; then
useradd "${USER}" -m
fi
if [[ "${is_update}" == "yes" ]]; then
echo "Setting up update splash screen"
"${box_src_tmp_dir}/setup/splashpage.sh" --data "${arg_data}" || true # show splash from new code
${BOX_SRC_DIR}/setup/stop.sh # stop the old code
echo "==> installer: stop cloudron.target service for update"
${BOX_SRC_DIR}/setup/stop.sh
fi
# setup links to data directory

View File

@@ -3,16 +3,13 @@
source_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
json="${source_dir}/../node_modules/.bin/json"
# IMPORTANT: Fix cloudron.js:doUpdate if you add/remove any arg. keep these sorted for readability
arg_api_server_origin=""
arg_fqdn=""
arg_fqdn="" # remove after 1.10
arg_admin_domain=""
arg_admin_location=""
arg_admin_fqdn=""
arg_zone_name=""
arg_is_custom_domain="false" # can be removed after 1.9
arg_retire_reason=""
arg_retire_info=""
arg_token=""
arg_version=""
arg_web_server_origin=""
arg_provider=""
@@ -35,16 +32,13 @@ while true; do
# these params must be valid in all cases
arg_fqdn=$(echo "$2" | $json fqdn)
arg_admin_fqdn=$(echo "$2" | $json adminFqdn)
arg_zone_name=$(echo "$2" | $json zoneName)
[[ "${arg_zone_name}" == "" ]] && arg_zone_name="${arg_fqdn}"
# can be removed after 1.9
arg_is_custom_domain=$(echo "$2" | $json isCustomDomain)
[[ "${arg_is_custom_domain}" == "" ]] && arg_is_custom_domain="true"
arg_admin_location=$(echo "$2" | $json adminLocation)
[[ "${arg_admin_location}" == "" ]] && arg_admin_location="my"
arg_admin_domain=$(echo "$2" | $json adminDomain)
[[ "${arg_admin_domain}" == "" ]] && arg_admin_domain="${arg_fqdn}"
# only update/restore have this valid (but not migrate)
arg_api_server_origin=$(echo "$2" | $json apiServerOrigin)
[[ "${arg_api_server_origin}" == "" ]] && arg_api_server_origin="https://api.cloudron.io"
@@ -58,8 +52,6 @@ while true; do
arg_is_demo=$(echo "$2" | $json isDemo)
[[ "${arg_is_demo}" == "" ]] && arg_is_demo="false"
arg_token=$(echo "$2" | $json token)
arg_provider=$(echo "$2" | $json provider)
[[ "${arg_provider}" == "" ]] && arg_provider="generic"
@@ -72,10 +64,8 @@ done
echo "Parsed arguments:"
echo "api server: ${arg_api_server_origin}"
echo "admin fqdn: ${arg_admin_fqdn}"
echo "fqdn: ${arg_fqdn}"
echo "custom domain: ${arg_is_custom_domain}"
# do not dump these as they might become available via logs API
#echo "token: ${arg_token}"
echo "version: ${arg_version}"
echo "web server: ${arg_web_server_origin}"
echo "provider: ${arg_provider}"

View File

@@ -1,48 +0,0 @@
#!/bin/bash
set -eu -o pipefail
readonly SETUP_WEBSITE_DIR="/home/yellowtent/setup/website"
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly box_src_dir="$(realpath ${script_dir}/..)"
readonly PLATFORM_DATA_DIR="/home/yellowtent/platformdata"
echo "Setting up nginx update page"
if [[ ! -f "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf" ]]; then
echo "No admin.conf found. This Cloudron has no domain yet. Skip splash setup"
exit
fi
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
# keep this is sync with config.js appFqdn()
admin_origin="https://${arg_admin_fqdn}"
# copy the website
rm -rf "${SETUP_WEBSITE_DIR}" && mkdir -p "${SETUP_WEBSITE_DIR}"
cp -r "${script_dir}/splash/website/"* "${SETUP_WEBSITE_DIR}"
# create nginx config
readonly current_infra=$(node -e "console.log(require('${script_dir}/../src/infra_version.js').version);")
existing_infra="none"
[[ -f "${PLATFORM_DATA_DIR}/INFRA_VERSION" ]] && existing_infra=$(node -e "console.log(JSON.parse(require('fs').readFileSync('${PLATFORM_DATA_DIR}/INFRA_VERSION', 'utf8')).version);")
if [[ "${arg_retire_reason}" != "" || "${existing_infra}" != "${current_infra}" ]]; then
echo "Showing progress bar on all subdomains in retired mode or infra update. retire: ${arg_retire_reason} existing: ${existing_infra} current: ${current_infra}"
rm -f ${PLATFORM_DATA_DIR}/nginx/applications/*
${box_src_dir}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
-O "{ \"vhost\": \"~^(.+)\$\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\", \"robotsTxtQuoted\": null, \"hasIPv6\": false }" > "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf"
else
echo "Show progress bar only on admin domain for normal update"
${box_src_dir}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
-O "{ \"vhost\": \"${arg_admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\", \"robotsTxtQuoted\": null, \"hasIPv6\": false }" > "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf"
fi
if [[ "${arg_retire_reason}" == "migrate" ]]; then
echo "{ \"migrate\": { \"percent\": \"10\", \"message\": \"Migrating cloudron. This could take up to 15 minutes.\", \"info\": ${arg_retire_info} }, \"backup\": null, \"apiServerOrigin\": \"${arg_api_server_origin}\" }" > "${SETUP_WEBSITE_DIR}/progress.json"
else
echo '{ "update": { "percent": "10", "message": "Updating cloudron software" }, "backup": null }' > "${SETUP_WEBSITE_DIR}/progress.json"
fi
nginx -s reload

View File

@@ -11,7 +11,6 @@ readonly PLATFORM_DATA_DIR="${HOME_DIR}/platformdata" # platform data
readonly APPS_DATA_DIR="${HOME_DIR}/appsdata" # app data
readonly BOX_DATA_DIR="${HOME_DIR}/boxdata" # box data
readonly CONFIG_DIR="${HOME_DIR}/configs"
readonly SETUP_PROGRESS_JSON="${HOME_DIR}/setup/website/progress.json"
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
@@ -19,19 +18,11 @@ readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
set_progress() {
local percent="$1"
local message="$2"
echo "==> ${percent} - ${message}"
(echo "{ \"update\": { \"percent\": \"${percent}\", \"message\": \"${message}\" }, \"backup\": {} }" > "${SETUP_PROGRESS_JSON}") 2> /dev/null || true # as this will fail in non-update mode
}
set_progress "20" "Configuring host"
echo "==> Configuring host"
sed -e 's/^#NTP=/NTP=0.ubuntu.pool.ntp.org 1.ubuntu.pool.ntp.org 2.ubuntu.pool.ntp.org 3.ubuntu.pool.ntp.org/' -i /etc/systemd/timesyncd.conf
timedatectl set-ntp 1
timedatectl set-timezone UTC
hostnamectl set-hostname "${arg_fqdn}"
hostnamectl set-hostname "${arg_admin_fqdn}"
echo "==> Configuring docker"
cp "${script_dir}/start/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
@@ -85,6 +76,8 @@ mkdir -p "${PLATFORM_DATA_DIR}/collectd/collectd.conf.d"
mkdir -p "${PLATFORM_DATA_DIR}/logrotate.d"
mkdir -p "${PLATFORM_DATA_DIR}/acme"
mkdir -p "${PLATFORM_DATA_DIR}/backup"
mkdir -p "${PLATFORM_DATA_DIR}/logs/backup"
mkdir -p "${PLATFORM_DATA_DIR}/update"
mkdir -p "${BOX_DATA_DIR}/appicons"
mkdir -p "${BOX_DATA_DIR}/certs"
@@ -114,7 +107,7 @@ systemctl restart systemd-journald
setfacl -n -m u:${USER}:r /var/log/journal/*/system.journal
echo "==> Creating config directory"
rm -rf "${CONFIG_DIR}" && mkdir "${CONFIG_DIR}"
mkdir -p "${CONFIG_DIR}"
echo "==> Setting up unbound"
# DO uses Google nameservers by default. This causes RBL queries to fail (host 2.0.0.127.zen.spamhaus.org)
@@ -129,6 +122,7 @@ echo "==> Adding systemd services"
cp -r "${script_dir}/start/systemd/." /etc/systemd/system/
systemctl daemon-reload
systemctl enable unbound
systemctl enable cloudron-syslog
systemctl enable cloudron.target
systemctl enable cloudron-firewall
@@ -141,6 +135,9 @@ systemctl enable --now cron
# ensure unbound runs
systemctl restart unbound
# ensure cloudron-syslog runs
systemctl restart cloudron-syslog
echo "==> Configuring sudoers"
rm -f /etc/sudoers.d/${USER}
cp "${script_dir}/start/sudoers" /etc/sudoers.d/${USER}
@@ -155,6 +152,8 @@ echo "==> Configuring logrotate"
if ! grep -q "^include ${PLATFORM_DATA_DIR}/logrotate.d" /etc/logrotate.conf; then
echo -e "\ninclude ${PLATFORM_DATA_DIR}/logrotate.d\n" >> /etc/logrotate.conf
fi
cp "${script_dir}/start/app-logrotate" "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate"
chown root:root "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate"
echo "==> Adding motd message for admins"
cp "${script_dir}/start/cloudron-motd" /etc/update-motd.d/92-cloudron
@@ -172,6 +171,9 @@ if ! grep -q "^Restart=" /etc/systemd/system/multi-user.target.wants/nginx.servi
echo -e "\n[Service]\nRestart=always\n" >> /etc/systemd/system/multi-user.target.wants/nginx.service
systemctl daemon-reload
fi
# remove this migration after 1.10
[[ -f /etc/nginx/cert/host.cert ]] && cp /etc/nginx/cert/host.cert "/etc/nginx/cert/${arg_admin_domain}.host.cert"
[[ -f /etc/nginx/cert/host.key ]] && cp /etc/nginx/cert/host.key "/etc/nginx/cert/${arg_admin_domain}.host.key"
systemctl start nginx
# bookkeep the version as part of data
@@ -199,38 +201,29 @@ readonly mysql_root_password="password"
mysqladmin -u root -ppassword password password # reset default root password
mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
set_progress "40" "Migrating data"
echo "==> Migrating data"
sudo -u "${USER}" -H bash <<EOF
set -eu
cd "${BOX_SRC_DIR}"
BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up
EOF
if [[ -z "${arg_admin_fqdn:-}" ]]; then
# can be removed after 1.9
admin_fqdn=$([[ "${arg_is_custom_domain}" == "true" ]] && echo "${arg_admin_location}.${arg_fqdn}" || echo "${arg_admin_location}-${arg_fqdn}")
else
admin_fqdn="${arg_admin_fqdn}"
fi
echo "==> Creating cloudron.conf"
cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
{
"version": "${arg_version}",
"token": "${arg_token}",
"apiServerOrigin": "${arg_api_server_origin}",
"webServerOrigin": "${arg_web_server_origin}",
"fqdn": "${arg_fqdn}",
"adminFqdn": "${admin_fqdn}",
"adminDomain": "${arg_admin_domain}",
"adminFqdn": "${arg_admin_fqdn}",
"adminLocation": "${arg_admin_location}",
"zoneName": "${arg_zone_name}",
"provider": "${arg_provider}",
"isDemo": ${arg_is_demo}
}
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}"
}
@@ -246,19 +239,22 @@ fi
echo "==> Changing ownership"
chown "${USER}:${USER}" -R "${CONFIG_DIR}"
chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/nginx" "${PLATFORM_DATA_DIR}/collectd" "${PLATFORM_DATA_DIR}/logrotate.d" "${PLATFORM_DATA_DIR}/addons" "${PLATFORM_DATA_DIR}/acme" "${PLATFORM_DATA_DIR}/backup"
chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/nginx" "${PLATFORM_DATA_DIR}/collectd" "${PLATFORM_DATA_DIR}/addons" "${PLATFORM_DATA_DIR}/acme" "${PLATFORM_DATA_DIR}/backup" "${PLATFORM_DATA_DIR}/logs" "${PLATFORM_DATA_DIR}/update"
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}/INFRA_VERSION" 2>/dev/null || true
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}"
# logrotate files have to be owned by root, this is here to fixup existing installations where we were resetting the owner to yellowtent
chown root:root -R "${PLATFORM_DATA_DIR}/logrotate.d"
# do not chown the boxdata/mail directory; dovecot gets upset
chown "${USER}:${USER}" "${BOX_DATA_DIR}"
find "${BOX_DATA_DIR}" -mindepth 1 -maxdepth 1 -not -path "${BOX_DATA_DIR}/mail" -exec chown -R "${USER}:${USER}" {} \;
chown "${USER}:${USER}" "${BOX_DATA_DIR}/mail"
chown "${USER}:${USER}" -R "${BOX_DATA_DIR}/mail/dkim" # this is owned by box currently since it generates the keys
set_progress "60" "Starting Cloudron"
echo "==> Starting Cloudron"
systemctl start cloudron.target
sleep 2 # give systemd sometime to start the processes
set_progress "90" "Almost done"
echo "==> Almost done"

10
setup/start/app-logrotate Normal file
View File

@@ -0,0 +1,10 @@
# logrotate config for app logs
/home/yellowtent/platformdata/logs/*/*.log {
# only keep one rotated file, we currently do not send that over the api
rotate 1
size 10M
# we never compress so we can simply tail the files
nocompress
copytruncate
}

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;
@@ -154,51 +160,22 @@ server {
}
# graphite paths (uncomment block below and visit /graphite/index.html)
# remember to comment out the CSP policy as well to access the graphite dashboard
# location ~ ^/(graphite|content|metrics|dashboard|render|browser|composer)/ {
# proxy_pass http://127.0.0.1:8000;
# client_max_body_size 1m;
# }
location / {
root <%= sourceDir %>/webadmin/dist;
root <%= sourceDir %>/dashboard/dist;
index index.html index.htm;
}
<% } else if ( endpoint === 'app' ) { %>
proxy_pass http://127.0.0.1:<%= port %>;
<% } else if ( endpoint === 'splash' ) { %>
root <%= sourceDir %>;
error_page 503 /update.html;
location /update.html {
add_header Cache-Control no-cache;
}
location /theme.css {
add_header Cache-Control no-cache;
}
location /3rdparty/ {
add_header Cache-Control no-cache;
}
location /js/ {
add_header Cache-Control no-cache;
}
location /progress.json {
add_header Cache-Control no-cache;
}
location /api/v1/cloudron/progress {
add_header Cache-Control no-cache;
default_type application/json;
alias <%= sourceDir %>/progress.json;
}
location / {
return 503;
}
<% } else if ( endpoint === 'redirect' ) { %>
# redirect everything to the app. this is temporary because there is no way
# to clear a permanent redirect on the browser
return 302 https://<%= redirectTo %>$request_uri;
<% } %>
}
}

View File

@@ -0,0 +1,14 @@
[Unit]
Description=Cloudron Syslog
After=network.target
[Service]
ExecStart=/usr/local/cloudron-syslog/bin/cloudron-syslog --port 2514 --logdir /home/yellowtent/platformdata/logs
WorkingDirectory=/usr/local/cloudron-syslog
Environment="NODE_ENV=production"
Restart=always
User=yellowtent
Group=yellowtent
[Install]
WantedBy=multi-user.target

147
src/accesscontrol.js Normal file
View File

@@ -0,0 +1,147 @@
'use strict';
exports = module.exports = {
SCOPE_APPS_READ: 'apps:read',
SCOPE_APPS_MANAGE: 'apps:manage',
SCOPE_CLIENTS: 'clients',
SCOPE_CLOUDRON: 'cloudron',
SCOPE_DOMAINS_READ: 'domains:read',
SCOPE_DOMAINS_MANAGE: 'domains:manage',
SCOPE_MAIL: 'mail',
SCOPE_PROFILE: 'profile',
SCOPE_SETTINGS: 'settings',
SCOPE_USERS_READ: 'users:read',
SCOPE_USERS_MANAGE: 'users:manage',
SCOPE_APPSTORE: 'appstore',
VALID_SCOPES: [ 'apps', 'appstore', 'clients', 'cloudron', 'domains', 'mail', 'profile', 'settings', 'users' ], // keep this sorted
SCOPE_ANY: '*',
validateScopeString: validateScopeString,
hasScopes: hasScopes,
canonicalScopeString: canonicalScopeString,
intersectScopes: intersectScopes,
validateToken: validateToken,
scopesForUser: scopesForUser
};
var assert = require('assert'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:accesscontrol'),
settings = require('./settings.js'),
tokendb = require('./tokendb.js'),
users = require('./users.js'),
UsersError = users.UsersError,
_ = require('underscore');
// returns scopes that does not have wildcards and is sorted
function canonicalScopeString(scope) {
if (scope === exports.SCOPE_ANY) return exports.VALID_SCOPES.join(',');
return scope.split(',').sort().join(',');
}
function intersectScopes(allowedScopes, wantedScopes) {
assert(Array.isArray(allowedScopes), 'Expecting sorted array');
assert(Array.isArray(wantedScopes), 'Expecting sorted array');
if (_.isEqual(allowedScopes, wantedScopes)) return allowedScopes; // quick path
let wantedScopesMap = new Map();
let results = [];
// make a map of scope -> [ subscopes ]
for (let w of wantedScopes) {
let parts = w.split(':');
let subscopes = wantedScopesMap.get(parts[0]) || new Set();
subscopes.add(parts[1] || '*');
wantedScopesMap.set(parts[0], subscopes);
}
for (let a of allowedScopes) {
let parts = a.split(':');
let as = parts[1] || '*';
let subscopes = wantedScopesMap.get(parts[0]);
if (!subscopes) continue;
if (subscopes.has('*') || subscopes.has(as)) {
results.push(a);
} else if (as === '*') {
results = results.concat(Array.from(subscopes).map(function (ss) { return `${a}:${ss}`; }));
}
}
return results;
}
function validateScopeString(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.split(':')[0]) !== -1; });
if (!allValid) return new Error('Invalid scope. Available scopes are ' + exports.VALID_SCOPES.join(', '));
return null;
}
// tests if all requiredScopes are attached to the request
function hasScopes(authorizedScopes, requiredScopes) {
assert(Array.isArray(authorizedScopes), 'Expecting array');
assert(Array.isArray(requiredScopes), 'Expecting array');
if (authorizedScopes.indexOf(exports.SCOPE_ANY) !== -1) return null;
for (var i = 0; i < requiredScopes.length; ++i) {
const scopeParts = requiredScopes[i].split(':');
// this allows apps:write if the token has a higher apps scope
if (authorizedScopes.indexOf(requiredScopes[i]) === -1 && authorizedScopes.indexOf(scopeParts[0]) === -1) {
debug('scope: missing scope "%s".', requiredScopes[i]);
return new Error('Missing required scope "' + requiredScopes[i] + '"');
}
}
return null;
}
function scopesForUser(user, callback) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof callback, 'function');
if (user.admin) return callback(null, exports.VALID_SCOPES);
settings.getSpacesConfig(function (error, spaces) {
if (error) return callback(error);
callback(null, spaces.enabled ? [ 'profile', 'apps', 'domains:read', 'users:read' ] : [ 'profile', 'apps:read' ]);
});
}
function validateToken(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, null /* user */, 'Invalid Token'); // will end up as a 401
if (error) return callback(error); // this triggers 'internal error' in passport
users.get(token.identifier, function (error, user) {
if (error && error.reason === UsersError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
if (error) return callback(error);
scopesForUser(user, function (error, userScopes) {
if (error) return callback(error);
var authorizedScopes = intersectScopes(userScopes, token.scope.split(','));
const skipPasswordVerification = token.clientId === 'cid-sdk' || token.clientId === 'cid-cli'; // these clients do not require password checks unlike UI
var info = { authorizedScopes: authorizedScopes, skipPasswordVerification: skipPasswordVerification }; // ends up in req.authInfo
callback(null, user, info);
});
});
});
}

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'),
hat = require('./hat.js'),
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) {
@@ -205,13 +207,21 @@ function getBindsSync(app, addons) {
assert.strictEqual(typeof app, 'object');
assert(!addons || typeof addons === 'object');
var binds = [ ];
let binds = [ ];
if (!addons) return binds;
for (var addon in addons) {
for (let addon in addons) {
switch (addon) {
case 'localstorage': binds.push(path.join(paths.APPS_DATA_DIR, app.id, 'data') + ':/app/data:rw'); break;
case 'localstorage':
binds.push(path.join(paths.APPS_DATA_DIR, app.id, 'data') + ':/app/data:rw');
if (!Array.isArray(addons[addon].bindMounts)) break;
for (let mount of addons[addon].bindMounts) {
let [ host, container ] = mount.split(':');
binds.push(path.join(paths.APPS_DATA_DIR, app.id, 'data', path.normalize(host)) + ':' + container);
}
break;
default: break;
}
}
@@ -250,8 +260,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 +301,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: config.fqdn() }
];
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 +373,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 * 48) : existingPassword; // see box#565 for password length
var env = [
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
{ name: 'MAIL_SMTP_PORT', value: '2525' },
{ name: 'MAIL_SMTPS_PORT', value: '4650' },
{ name: 'MAIL_SMTP_USERNAME', value: mailbox.name },
{ name: 'MAIL_SMTP_PASSWORD', value: password },
{ name: 'MAIL_FROM', value: mailbox.name + '@' + config.fqdn() },
{ name: 'MAIL_DOMAIN', value: config.fqdn() }
];
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 +415,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 * 48) : existingPassword; // see box#565 for password length
var env = [
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
{ name: 'MAIL_IMAP_PORT', value: '9993' },
{ name: 'MAIL_IMAP_USERNAME', value: mailbox.name },
{ name: 'MAIL_IMAP_PASSWORD', value: password },
{ name: 'MAIL_TO', value: mailbox.name + '@' + config.fqdn() },
{ name: 'MAIL_DOMAIN', value: config.fqdn() }
];
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 +450,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 +465,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 +503,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 +516,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 +527,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 +548,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 +561,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 +592,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 +606,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 +617,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 +638,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 +652,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 +684,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 +697,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 +708,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 +729,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 +742,67 @@ 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} \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag="${redisName}" \
-m ${memoryLimit/2} \
--memory-swap ${memoryLimit} \
--dns 172.18.0.1 \
--dns-search=. \
-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);
});
});
}
@@ -685,27 +811,31 @@ function teardownRedis(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var container = dockerConnection.getContainer('redis-' + app.id);
var container = dockerConnection.getContainer('redis-' + app.id);
var removeOptions = {
force: true, // kill container if it's running
v: true // removes volumes associated with the container
};
var removeOptions = {
force: true, // kill container if it's running
v: true // removes volumes associated with the container
};
container.remove(removeOptions, function (error) {
if (error && error.statusCode !== 404) return callback(new Error('Error removing container:' + error));
container.remove(removeOptions, function (error) {
if (error && error.statusCode !== 404) return callback(new Error('Error removing container:' + error));
safe.fs.unlinkSync(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
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);
});
});
});
}
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

View File

@@ -24,6 +24,9 @@ exports = module.exports = {
setRunCommand: setRunCommand,
getAppStoreIds: getAppStoreIds,
setOwner: setOwner,
transferOwnership: transferOwnership,
// installation codes (keep in sync in UI)
ISTATE_PENDING_INSTALL: 'pending_install', // installs and fresh reinstalls
ISTATE_PENDING_CLONE: 'pending_clone', // clone
@@ -47,6 +50,10 @@ exports = module.exports = {
HEALTH_ERROR: 'error',
HEALTH_DEAD: 'dead',
// subdomain table types
SUBDOMAIN_TYPE_PRIMARY: 'primary',
SUBDOMAIN_TYPE_REDIRECT: 'redirect',
_clear: clear
};
@@ -59,10 +66,10 @@ var assert = require('assert'),
util = require('util');
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.creationTime', 'apps.updateTime' ].join(',');
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'subdomains.subdomain AS location', 'subdomains.domain',
'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit',
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.ts' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
@@ -113,6 +120,12 @@ function postProcess(result) {
assert(result.debugModeJson === null || typeof result.debugModeJson === 'string');
result.debugMode = safe.JSON.parse(result.debugModeJson);
delete result.debugModeJson;
result.alternateDomains = result.alternateDomains || [];
result.alternateDomains.forEach(function (d) {
delete d.appId;
delete d.type;
});
}
function get(id, callback) {
@@ -121,13 +134,22 @@ function get(id, callback) {
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId WHERE apps.id = ? GROUP BY apps.id', [ id ], function (error, result) {
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND type = ?'
+ ' WHERE apps.id = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, id ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
postProcess(result[0]);
database.query('SELECT * FROM subdomains WHERE appId = ? AND type = ?', [ id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, result[0]);
result[0].alternateDomains = alternateDomains;
postProcess(result[0]);
callback(null, result[0]);
})
});
}
@@ -137,13 +159,21 @@ function getByHttpPort(httpPort, callback) {
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId WHERE httpPort = ? GROUP BY apps.id', [ httpPort ], function (error, result) {
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND type = ?'
+ ' WHERE httpPort = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, httpPort ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
postProcess(result[0]);
database.query('SELECT * FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, result[0]);
result[0].alternateDomains = alternateDomains;
postProcess(result[0]);
callback(null, result[0]);
});
});
}
@@ -153,13 +183,21 @@ function getByContainerId(containerId, callback) {
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId WHERE containerId = ? GROUP BY apps.id', [ containerId ], function (error, result) {
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND type = ?'
+ ' WHERE containerId = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, containerId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
postProcess(result[0]);
database.query('SELECT * FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, result[0]);
result[0].alternateDomains = alternateDomains;
postProcess(result[0]);
callback(null, result[0]);
});
});
}
@@ -168,23 +206,38 @@ function getAll(callback) {
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' GROUP BY apps.id ORDER BY apps.id', function (error, results) {
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND type = ?'
+ ' GROUP BY apps.id ORDER BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(postProcess);
database.query('SELECT * FROM subdomains WHERE type = ?', [ exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, results);
alternateDomains.forEach(function (d) {
var domain = results.find(function (a) { return d.appId === a.id; });
if (!domain) return;
domain.alternateDomains = domain.alternateDomains || [];
domain.alternateDomains.push(d);
});
results.forEach(postProcess);
callback(null, results);
});
});
}
function add(id, appStoreId, manifest, location, domain, portBindings, data, callback) {
function add(id, appStoreId, manifest, location, domain, ownerId, portBindings, data, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof appStoreId, 'string');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof manifest.version, 'string');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof portBindings, 'object');
assert(data && typeof data === 'object');
assert.strictEqual(typeof callback, 'function');
@@ -196,17 +249,24 @@ 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, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId) ' +
' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId ]
});
queries.push({
query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)',
args: [ id, domain, location, exports.SUBDOMAIN_TYPE_PRIMARY ]
});
Object.keys(portBindings).forEach(function (env) {
@@ -219,13 +279,23 @@ 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 ]
});
}
if (data.alternateDomains) {
data.alternateDomains.forEach(function (d) {
queries.push({
query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)',
args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_REDIRECT ]
});
});
}
database.transaction(queries, function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error.message));
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'no such domain'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
@@ -276,6 +346,7 @@ function del(id, callback) {
assert.strictEqual(typeof callback, 'function');
var queries = [
{ query: 'DELETE FROM subdomains WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM mailboxes WHERE ownerId=?', args: [ id ] },
{ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM apps WHERE id = ?', args: [ id ] }
@@ -283,7 +354,7 @@ function del(id, callback) {
database.transaction(queries, function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results[2].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (results[3].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null);
});
@@ -293,6 +364,7 @@ function clear(callback) {
assert.strictEqual(typeof callback, 'function');
async.series([
database.query.bind(null, 'DELETE FROM subdomains'),
database.query.bind(null, 'DELETE FROM appPortBindings'),
database.query.bind(null, 'DELETE FROM appAddonConfigs'),
database.query.bind(null, 'DELETE FROM apps')
@@ -313,6 +385,7 @@ function updateWithConstraints(id, app, constraints, callback) {
assert.strictEqual(typeof callback, 'function');
assert(!('portBindings' in app) || typeof app.portBindings === 'object');
assert(!('accessRestriction' in app) || typeof app.accessRestriction === 'object' || app.accessRestriction === '');
assert(!('alternateDomains' in app) || Array.isArray(app.alternateDomains));
var queries = [ ];
@@ -326,12 +399,27 @@ function updateWithConstraints(id, app, constraints, callback) {
});
}
if ('location' in app) {
queries.push({ query: 'UPDATE subdomains SET subdomain = ? WHERE appId = ? AND type = ?', args: [ app.location, id, exports.SUBDOMAIN_TYPE_PRIMARY ]});
}
if ('domain' in app) {
queries.push({ query: 'UPDATE subdomains SET domain = ? WHERE appId = ? AND type = ?', args: [ app.domain, id, exports.SUBDOMAIN_TYPE_PRIMARY ]});
}
if ('alternateDomains' in app) {
queries.push({ query: 'DELETE FROM subdomains WHERE appId = ? AND type = ?', args: [ id, exports.SUBDOMAIN_TYPE_REDIRECT ]});
app.alternateDomains.forEach(function (d) {
queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_REDIRECT ]});
});
}
var fields = [ ], values = [ ];
for (var p in app) {
if (p === 'manifest' || p === 'oldConfig' || p === 'updateConfig' || p === 'restoreConfig' || p === 'accessRestriction' || p === 'debugMode') {
fields.push(`${p}Json = ?`);
values.push(JSON.stringify(app[p]));
} else if (p !== 'portBindings') {
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains') {
fields.push(p + ' = ?');
values.push(app[p]);
}
@@ -503,3 +591,31 @@ function getAddonConfigByName(appId, addonId, name, callback) {
callback(null, results[0].value);
});
}
function setOwner(appId, ownerId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('UPDATE apps SET ownerId=? WHERE appId=?', [ ownerId, appId ], function (error, results) {
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such user'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such app'));
callback(null);
});
}
function transferOwnership(oldOwnerId, newOwnerId, callback) {
assert.strictEqual(typeof oldOwnerId, 'string');
assert.strictEqual(typeof newOwnerId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('UPDATE apps SET ownerId=? WHERE ownerId=?', [ newOwnerId, oldOwnerId ], function (error, results) {
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such user'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}

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);

View File

@@ -4,6 +4,8 @@ exports = module.exports = {
AppsError: AppsError,
hasAccessTo: hasAccessTo,
removeInternalFields: removeInternalFields,
removeRestrictedFields: removeRestrictedFields,
get: get,
getByIpAddress: getByIpAddress,
@@ -40,21 +42,22 @@ exports = module.exports = {
downloadFile: downloadFile,
uploadFile: uploadFile,
setOwner: setOwner,
transferOwnership: transferOwnership,
// exported for testing
_validateHostname: validateHostname,
_validatePortBindings: validatePortBindings,
_validateAccessRestriction: validateAccessRestriction
};
var addons = require('./addons.js'),
appdb = require('./appdb.js'),
var appdb = require('./appdb.js'),
appstore = require('./appstore.js'),
AppstoreError = require('./appstore.js').AppstoreError,
assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
BackupsError = backups.BackupsError,
certificates = require('./certificates.js'),
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
@@ -62,15 +65,16 @@ 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'),
path = require('path'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
semver = require('semver'),
spawn = require('child_process').spawn,
@@ -83,7 +87,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
@@ -130,8 +135,7 @@ function validateHostname(location, domain, hostname) {
const RESERVED_LOCATIONS = [
constants.API_LOCATION,
constants.SMTP_LOCATION,
constants.IMAP_LOCATION,
constants.POSTMAN_LOCATION
constants.IMAP_LOCATION
];
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new AppsError(AppsError.BAD_FIELD, location + ' is reserved');
@@ -145,9 +149,9 @@ function validateHostname(location, domain, hostname) {
if (location) {
// label validation
if (location.length > 63) return new AppsError(AppsError.BAD_FIELD, 'Subdomain exceeds 63 characters');
if (location.match(/^[A-Za-z0-9-]+$/) === null) return new AppsError(AppsError.BAD_FIELD, 'Subdomain can only contain alphanumerics and hyphen');
if (location.startsWith('-') || location.endsWith('-')) return new AppsError(AppsError.BAD_FIELD, 'Subdomain cannot start or end with hyphen');
if (location.split('.').some(function (p) { return p.length > 63 || p.length < 1; })) return new AppsError(AppsError.BAD_FIELD, 'Invalid subdomain length');
if (location.match(/^[A-Za-z0-9-.]+$/) === null) return new AppsError(AppsError.BAD_FIELD, 'Subdomain can only contain alphanumeric, hyphen and dot');
if (/^[-.]/.test(location)) return new AppsError(AppsError.BAD_FIELD, 'Subdomain cannot start or end with hyphen or dot');
}
return null;
@@ -157,7 +161,7 @@ function validateHostname(location, domain, hostname) {
function validatePortBindings(portBindings, tcpPorts) {
assert.strictEqual(typeof portBindings, 'object');
// keep the public ports in sync with firewall rules in scripts/initializeBaseUbuntuImage.sh
// keep the public ports in sync with firewall rules in setup/start/cloudron-firewall.sh
// these ports are reserved even if we listen only on 127.0.0.1 because we setup HostIp to be 127.0.0.1
// for custom tcp ports
var RESERVED_PORTS = [
@@ -173,14 +177,15 @@ function validatePortBindings(portBindings, tcpPorts) {
993, /* imaps */
2003, /* graphite (lo) */
2004, /* graphite (lo) */
2020, /* install server */
2020, /* mail server */
2514, /* cloudron-syslog (lo) */
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;
@@ -307,20 +312,37 @@ 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,
alternateDomains: app.alternateDomains || []
};
}
function removeInternalFields(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', 'ts',
'alternateDomains', 'ownerId');
}
function removeRestrictedFields(app) {
return _.pick(app,
'id', 'appStoreId', 'installationState', 'installationProgress', 'runState', 'health', 'ownerId',
'location', 'domain', 'fqdn', 'manifest', 'portBindings', 'iconUrl', 'creationTime', 'ts');
}
function getIconUrlSync(app) {
var iconPath = paths.APP_ICONS_DIR + '/' + app.id + '.png';
return fs.existsSync(iconPath) ? '/api/v1/apps/' + app.id + '/icon' : null;
@@ -336,20 +358,13 @@ function hasAccessTo(app, user, callback) {
// check user access
if (app.accessRestriction.users.some(function (e) { return e === user.id; })) return callback(null, true);
// check group access
groups.getGroups(user.id, function (error, groupIds) {
if (error) return callback(null, false);
if (user.admin) return callback(null, true); // admins can always access any app
const isAdmin = groupIds.indexOf(constants.ADMIN_GROUP_ID) !== -1;
if (!app.accessRestriction.groups) return callback(null, false);
if (isAdmin) return callback(null, true); // admins can always access any app
if (app.accessRestriction.groups.some(function (gid) { return user.groupIds.indexOf(gid) !== -1; })) return callback(null, true);
if (!app.accessRestriction.groups) return callback(null, false);
if (app.accessRestriction.groups.some(function (gid) { return groupIds.indexOf(gid) !== -1; })) return callback(null, true);
callback(null, false);
});
callback(null, false);
}
function get(appId, callback) {
@@ -363,12 +378,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);
});
});
});
}
@@ -387,12 +406,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);
});
});
});
});
@@ -408,12 +431,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);
@@ -450,12 +477,16 @@ function downloadManifest(appStoreId, manifest, callback) {
superagent.get(url).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Network error downloading manifest:' + error.message));
if (result.statusCode !== 200) return callback(new AppsError(AppsError.EXTERNAL_ERROR, util.format('Failed to get app info from store.', result.statusCode, result.text)));
if (result.statusCode !== 200) return callback(new AppsError(AppsError.NOT_FOUND, util.format('Failed to get app info from store.', result.statusCode, result.text)));
callback(null, parts[0], result.body.manifest);
});
}
function mailboxNameForLocation(location, manifest) {
return (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
}
function install(data, auditSource, callback) {
assert(data && typeof data === 'object');
assert.strictEqual(typeof auditSource, 'object');
@@ -469,14 +500,14 @@ 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,
robotsTxt = data.robotsTxt || null,
enableBackup = 'enableBackup' in data ? data.enableBackup : true,
backupId = data.backupId || null,
backupFormat = data.backupFormat || 'tgz';
backupFormat = data.backupFormat || 'tgz',
ownerId = data.ownerId;
assert(data.appStoreId || data.manifest); // atleast one of them is required
@@ -514,8 +545,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) {
@@ -527,54 +556,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);
error = certificates.validateCertificate(cert, key, intrinsicFqdn);
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
if (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: mailboxNameForLocation(location, manifest),
restoreConfig: backupId ? { backupId: backupId, backupFormat: backupFormat } : null,
enableBackup: enableBackup,
robotsTxt: robotsTxt
};
appdb.add(appId, appStoreId, manifest, location, domain, ownerId, 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 });
});
});
});
});
@@ -587,9 +632,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();
@@ -604,11 +648,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);
@@ -641,26 +680,36 @@ 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);
}
if ('alternateDomains' in data) {
// TODO validate all subdomains [{ domain: '', subdomain: ''}]
values.alternateDomains = data.alternateDomains;
}
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) {
error = certificates.validateCertificate(data.cert, data.key, intrinsicFqdn);
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, intrinsicFqdn + '.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, intrinsicFqdn + '.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, intrinsicFqdn + '.user.cert'))) debug('Error removing cert: ' + safe.error.message);
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, intrinsicFqdn + '.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);
}
}
@@ -670,9 +719,12 @@ 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';
// make the mailbox name follow the apps new location, if the user did not set it explicitly
var oldName = app.mailboxName;
var newName = data.mailboxName || (app.mailboxName.endsWith('.app') ? mailboxNameForLocation(location, app.manifest) : app.mailboxName);
mailboxdb.updateName(oldName, values.oldConfig.domain, newName, domain, function (error) {
if (newName.endsWith('.app')) error = null; // ignore internal mailbox conflict errors since we want to show location conflict errors in the UI
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));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -684,9 +736,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);
});
});
});
});
@@ -726,9 +783,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
@@ -753,7 +809,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);
@@ -764,12 +820,6 @@ function update(appId, data, auditSource, callback) {
});
}
function appLogFilter(app) {
var names = [ app.id ].concat(addons.getContainerNamesSync(app, app.manifest.addons));
return names.map(function (name) { return 'CONTAINER_NAME=' + name; });
}
function getLogs(appId, options, callback) {
assert.strictEqual(typeof appId, 'string');
assert(options && typeof options === 'object');
@@ -777,34 +827,38 @@ 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,
format = options.format || 'json';
format = options.format || 'json',
follow = !!options.follow;
var args = [ '--no-pager', '--lines=' + lines ];
if (follow) args.push('--follow');
if (format == 'short') args.push('--output=short', '-a'); else args.push('--output=json');
args = args.concat(appLogFilter(app));
assert.strictEqual(typeof lines, 'number');
assert.strictEqual(typeof format, 'string');
var cp = spawn('/bin/journalctl', args);
var args = [ '--lines=' + lines ];
if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs
args.push(path.join(paths.LOG_DIR, appId, 'apptask.log'));
args.push(path.join(paths.LOG_DIR, appId, 'app.log'));
var cp = spawn('/usr/bin/tail', args);
var transformStream = split(function mapper(line) {
if (format !== 'json') return line + '\n';
var obj = safe.JSON.parse(line);
if (!obj) return undefined;
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
var timestamp = (new Date(data[0])).getTime();
if (isNaN(timestamp)) timestamp = 0;
var message = line.slice(data[0].length+1);
// ignore faulty empty logs
if (!timestamp && !message) return;
var source = obj.CONTAINER_NAME.slice(app.id.length + 1);
return JSON.stringify({
realtimeTimestamp: obj.__REALTIME_TIMESTAMP,
monotonicTimestamp: obj.__MONOTONIC_TIMESTAMP,
message: obj.MESSAGE,
source: source || 'main'
realtimeTimestamp: timestamp * 1000,
message: message,
source: appId
}) + '\n';
});
@@ -824,9 +878,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 }); };
@@ -855,7 +908,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);
});
@@ -874,20 +927,21 @@ function clone(appId, data, auditSource, callback) {
var location = data.location.toLowerCase(),
domain = data.domain.toLowerCase(),
portBindings = data.portBindings || null,
backupId = data.backupId;
backupId = data.backupId,
ownerId = data.ownerId;
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof portBindings, 'object');
assert(ownerId === null || typeof ownerId === 'string');
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'));
@@ -900,41 +954,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, ownerId, 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 });
});
});
});
});
@@ -949,10 +1018,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));
@@ -963,7 +1032,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);
});
@@ -1014,7 +1083,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;
@@ -1028,9 +1097,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'));
@@ -1070,7 +1138,11 @@ function exec(appId, options, callback) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (options.rows && options.columns) {
exec.resize({ h: options.rows, w: options.columns }, function (error) { if (error) debug('Error resizing console', error); });
// there is a race where resizing too early results in a 404 "no such exec"
// https://git.cloudron.io/cloudron/box/issues/549
setTimeout(function () {
exec.resize({ h: options.rows, w: options.columns }, function (error) { if (error) debug('Error resizing console', error); });
}, 2000);
}
return callback(null, stream);
@@ -1169,17 +1241,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
});
@@ -1191,14 +1263,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
});
@@ -1287,3 +1359,28 @@ function uploadFile(appId, sourceFilePath, destFilePath, callback) {
callback(null);
});
}
function setOwner(appId, ownerId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
appdb.setOwner(appId, ownerId, function (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));
callback();
});
}
function transferOwnership(oldOwnerId, newOwnerId, callback) {
assert.strictEqual(typeof oldOwnerId, 'string');
assert.strictEqual(typeof newOwnerId, 'string');
assert.strictEqual(typeof callback, 'function');
appdb.transferOwnership(oldOwnerId, newOwnerId, function (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));
callback();
});
}

View File

@@ -5,6 +5,7 @@ exports = module.exports = {
unpurchase: unpurchase,
getSubscription: getSubscription,
isFreePlan: isFreePlan,
sendAliveStatus: sendAliveStatus,
@@ -18,11 +19,18 @@ 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'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
superagent = require('superagent'),
util = require('util');
@@ -56,23 +64,12 @@ var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function getAppstoreConfig(callback) {
assert.strictEqual(typeof callback, 'function');
// Caas Cloudrons do not store appstore credentials in their local database
if (config.provider() === 'caas') {
var url = config.apiServerOrigin() + '/api/v1/exchangeBoxTokenWithUserToken';
superagent.post(url).query({ token: config.token() }).timeout(30 * 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('App unpurchase failed. %s %j', result.status, result.body)));
settings.getAppstoreConfig(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
if (!result.token) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
callback(null, result.body);
});
} else {
settings.getAppstoreConfig(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
if (!result.token) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
callback(null, result);
});
}
callback(null, result);
});
}
function getSubscription(callback) {
@@ -94,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');
@@ -101,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();
});
});
}
@@ -135,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));
@@ -148,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;
eventlog.getAllPaged(eventlog.ACTION_USER_LOGIN, null, 1, 1, function (error, loginEvents) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
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 = {
tlsConfig: {
provider: result[settings.TLS_CONFIG_KEY].provider
},
backupConfig: {
provider: result[settings.BACKUP_CONFIG_KEY].provider,
hardlinks: !result[settings.BACKUP_CONFIG_KEY].noHardlinks
},
mailConfig: {
enabled: result[settings.MAIL_CONFIG_KEY].enabled
},
mailRelay: {
provider: result[settings.MAIL_RELAY_KEY].provider
},
mailCatchAll: {
count: result[settings.CATCH_ALL_ADDRESS_KEY].length
},
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 = {
domain: config.fqdn(),
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);
});
});
});
@@ -218,12 +265,18 @@ function getBoxUpdate(callback) {
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/boxupdate';
superagent.get(url).query({ accessToken: appstoreConfig.token, boxVersion: config.version() }).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode === 204) return callback(null); // no update
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
if (result.statusCode !== 200 || !result.body) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
// { version, changelog, upgrade, sourceTarballUrl}
callback(null, result.body);
var updateInfo = result.body;
if (!semver.valid(updateInfo.version) || semver.gt(config.version(), updateInfo.version)) {
return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Invalid update version: %s %s', result.statusCode, result.text)));
}
// updateInfo: { version, changelog, upgrade, sourceTarballUrl, sourceTarballSigUrl, boxVersionsUrl, boxVersionsSigUrl }
callback(null, updateInfo);
});
});
}
@@ -240,10 +293,21 @@ function getAppUpdate(app, callback) {
superagent.get(url).query({ accessToken: appstoreConfig.token, boxVersion: config.version(), appId: app.appStoreId, appVersion: app.manifest.version }).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 204) return callback(null); // no update
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
if (result.statusCode !== 200 || !result.body) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
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(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)));
}
// { id, creationDate, manifest }
callback(null, result.body);
callback(null, updateInfo);
});
});
}
@@ -275,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);
});
});
});
}

View File

@@ -8,46 +8,40 @@ exports = module.exports = {
// exported for testing
_reserveHttpPort: reserveHttpPort,
_configureNginx: configureNginx,
_unconfigureNginx: unconfigureNginx,
_configureReverseProxy: configureReverseProxy,
_unconfigureReverseProxy: unconfigureReverseProxy,
_createVolume: createVolume,
_deleteVolume: deleteVolume,
_verifyManifest: verifyManifest,
_registerSubdomain: registerSubdomain,
_unregisterSubdomain: unregisterSubdomain,
_waitForDnsPropagation: waitForDnsPropagation,
_waitForAltDomainDnsPropagation: waitForAltDomainDnsPropagation
_waitForDnsPropagation: waitForDnsPropagation
};
require('supererror')({ splatchError: true });
// remove timestamp from debug() based output
require('debug').formatArgs = function formatArgs(args) {
args[0] = this.namespace + ' ' + args[0];
};
var addons = require('./addons.js'),
appdb = require('./appdb.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
certificates = require('./certificates.js'),
config = require('./config.js'),
database = require('./database.js'),
DatabaseError = require('./databaseerror.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'),
net = require('net'),
nginx = require('./nginx.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
rimraf = require('rimraf'),
safe = require('safetydance'),
shell = require('./shell.js'),
superagent = require('superagent'),
@@ -72,8 +66,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
@@ -113,23 +106,19 @@ function reserveHttpPort(app, callback) {
});
}
function configureNginx(app, callback) {
function configureReverseProxy(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
certificates.ensureCertificate(app, function (error, certFilePath, keyFilePath) {
if (error) return callback(error);
nginx.configureApp(app, certFilePath, keyFilePath, callback);
});
reverseProxy.configureApp(app, { userId: null, username: 'apptask' }, callback);
}
function unconfigureNginx(app, callback) {
function unconfigureReverseProxy(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
// TODO: maybe revoke the cert
nginx.unconfigureApp(app, callback);
reverseProxy.unconfigureApp(app, callback);
}
function createContainer(app, callback) {
@@ -271,26 +260,24 @@ 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) {
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(null, error || changeId);
retryCallback(null, error);
});
});
}, function (error, result) {
if (error || result instanceof Error) return callback(error || result);
// dnsRecordId tracks whether we created this DNS record so that we can unregister later
updateApp(app, { dnsRecordId: result }, callback);
callback(null);
});
});
}
@@ -301,31 +288,90 @@ function unregisterSubdomain(app, location, domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
if (!app.dnsRecordId) {
debugApp(app, 'Skip unregister of record not created by cloudron');
return callback(null);
}
sysinfo.getPublicIp(function (error, ip) {
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);
});
}, function (error, result) {
if (error || result instanceof Error) return callback(error || result);
updateApp(app, { dnsRecordId: null }, callback);
callback(null);
});
});
}
function registerAlternateDomains(app, overwrite, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof overwrite, 'boolean');
assert.strictEqual(typeof callback, 'function');
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
async.eachSeries(app.alternateDomains, function (domain, callback) {
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Registering alternate subdomain [%s] overwrite: %s', (domain.subdomain ? (domain.subdomain + '.') : '') + domain.domain, overwrite);
// get the current record before updating it
domains.getDnsRecords(domain.subdomain, domain.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(domain.subdomain, domain.domain, 'A', [ ip ], function (error) {
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(null, error);
});
});
}, function (error, result) {
if (error || result instanceof Error) return callback(error || result);
callback();
});
}, callback);
});
}
function unregisterAlternateDomains(app, all, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof all, 'boolean');
assert.strictEqual(typeof callback, 'function');
var obsoleteDomains
if (all) obsoleteDomains = app.alternateDomains;
else obsoleteDomains = app.oldConfig.alternateDomains.filter(function (o) { return !app.alternateDomains.some(function (n) { return n.subdomain === o.subdomain && n.domain === o.domain; }); });
if (obsoleteDomains.length === 0) return callback();
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
async.eachSeries(obsoleteDomains, function (domain, callback) {
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Unregistering subdomain: %s%s', domain.subdomain ? (domain.subdomain + '.') : '', domain.domain);
domains.removeDnsRecords(domain.subdomain, domain.domain, 'A', [ ip ], function (error) {
if (error && error.reason === DomainsError.NOT_FOUND) return retryCallback(null, null);
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(null, error);
});
}, function (error, result) {
if (error || result instanceof Error) return callback(error || result);
callback();
});
}, callback);
});
}
function removeIcon(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -336,6 +382,16 @@ function removeIcon(app, callback) {
});
}
function cleanupLogs(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
rimraf(path.join(paths.LOG_DIR, app.id), function (error) {
if (error) debugApp(app, 'cannot cleanup logs: %s', error);
callback(null);
});
}
function waitForDnsPropagation(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -348,25 +404,16 @@ 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);
});
}
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) {
domains.waitForDnsRecord(app.fqdn, app.domain, ip, { interval: 5000, times: 240 }, function (error) {
if (error) return callback(error);
domains.waitForDNSRecord(app.altDomain, tld.getDomain(app.altDomain), ip, 'A', { interval: 10000, times: 60 }, callback);
// now wait for alternateDomains, if any
async.eachSeries(app.alternateDomains, function (domain, callback) {
var fqdn = (domain.subdomain ? (domain.subdomain + '.') : '') + domain.domain;
domains.waitForDnsRecord(fqdn, domain.domain, ip, { interval: 5000, times: 240 }, callback);
}, 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:
@@ -393,13 +440,19 @@ function install(app, callback) {
// teardown for re-installs
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
unconfigureNginx.bind(null, app),
unconfigureReverseProxy.bind(null, app),
removeCollectdProfile.bind(null, app),
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
@@ -451,11 +504,8 @@ 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, Configure nginx' }),
configureNginx.bind(null, app),
updateApp.bind(null, app, { installationProgress: '95, Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
// done!
function (callback) {
@@ -499,30 +549,34 @@ 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' }),
unconfigureNginx.bind(null, app),
unconfigureReverseProxy.bind(null, app),
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
deleteContainers.bind(null, app),
unregisterAlternateDomains.bind(null, app, false /* all */),
function (next) {
if (!locationChanged) return next();
// the config.fqdn() fallback can be removed after 1.9
unregisterSubdomain(app, app.oldConfig.location, app.oldConfig.domain || config.fqdn(), next);
unregisterSubdomain(app, app.oldConfig.location, app.oldConfig.domain, next);
},
reserveHttpPort.bind(null, app),
updateApp.bind(null, app, { installationProgress: '20, Downloading icon' }),
downloadIcon.bind(null, app),
updateApp.bind(null, app, { installationProgress: '35, Registering subdomain' }),
updateApp.bind(null, app, { installationProgress: '30, Registering subdomain' }),
registerSubdomain.bind(null, app, !locationChanged /* overwrite */), // if location changed, do not overwrite to detect conflicts
updateApp.bind(null, app, { installationProgress: '35, Registering alternate domains'}),
registerAlternateDomains.bind(null, app, true /* overwrite */), // figure out when to overwrite
updateApp.bind(null, app, { installationProgress: '40, Downloading image' }),
docker.downloadImage.bind(null, app.manifest),
@@ -547,11 +601,8 @@ 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 Nginx' }),
configureNginx.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
// done!
function (callback) {
@@ -704,14 +755,18 @@ function uninstall(app, callback) {
updateApp.bind(null, app, { installationProgress: '50, Deleting image' }),
docker.deleteImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '60, Unregistering subdomain' }),
updateApp.bind(null, app, { installationProgress: '60, Unregistering domains' }),
unregisterAlternateDomains.bind(null, app, true /* all */),
unregisterSubdomain.bind(null, app, app.location, app.domain),
updateApp.bind(null, app, { installationProgress: '80, Cleanup icon' }),
updateApp.bind(null, app, { installationProgress: '70, Cleanup icon' }),
removeIcon.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Unconfiguring Nginx' }),
unconfigureNginx.bind(null, app),
updateApp.bind(null, app, { installationProgress: '80, Unconfiguring reverse proxy' }),
unconfigureReverseProxy.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Cleanup logs' }),
cleanupLogs.bind(null, app),
updateApp.bind(null, app, { installationProgress: '95, Remove app from database' }),
appdb.del.bind(null, app.id)
@@ -801,6 +856,8 @@ function startTask(appId, callback) {
if (require.main === module) {
assert.strictEqual(process.argv.length, 3, 'Pass the appid as argument');
// add a separator for the log file
debug('------------------------------------------------------------');
debug('Apptask for %s', process.argv[2]);
process.on('SIGTERM', function () {

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.alternateEmail || 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);
});
});
}

View File

@@ -58,6 +58,7 @@ var addons = require('./addons.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
settings = require('./settings.js'),
superagent = require('superagent'),
syncer = require('./syncer.js'),
tar = require('tar-fs'),
util = require('util'),
@@ -68,10 +69,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) {
@@ -158,7 +158,7 @@ function get(backupId, callback) {
assert.strictEqual(typeof callback, 'function');
backupdb.get(backupId, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new BackupsError(BackupsError.NOT_FOUND, error));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new BackupsError(BackupsError.NOT_FOUND));
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
callback(null, result);
@@ -183,6 +183,92 @@ function log(detail) {
progress.setDetail(progress.BACKUP, detail);
}
function encryptFilePath(filePath, key) {
assert.strictEqual(typeof filePath, 'string');
assert.strictEqual(typeof key, 'string');
var encryptedParts = filePath.split('/').map(function (part) {
const cipher = crypto.createCipher('aes-256-cbc', key);
let crypt = cipher.update(part);
crypt = Buffer.concat([ crypt, cipher.final() ]);
return crypt.toString('base64') // ensures path is valid
.replace(/\//g, '-') // replace '/' of base64 since it conflicts with path separator
.replace(/=/g,''); // strip trailing = padding. this is only needed if we concat base64 strings, which we don't
});
return encryptedParts.join('/');
}
function decryptFilePath(filePath, key) {
assert.strictEqual(typeof filePath, 'string');
assert.strictEqual(typeof key, 'string');
let decryptedParts = [];
for (let part of filePath.split('/')) {
part = part + Array(part.length % 4).join('='); // add back = padding
part = part.replace(/-/g, '/'); // replace with '/'
try {
let decrypt = crypto.createDecipher('aes-256-cbc', key);
let text = decrypt.update(Buffer.from(part, 'base64'));
text = Buffer.concat([ text, decrypt.final() ]);
decryptedParts.push(text.toString('utf8'));
} catch (error) {
debug(`Error decrypting file ${filePath} part ${part}:`, error);
return null;
}
}
return decryptedParts.join('/');
}
function createReadStream(sourceFile, key) {
assert.strictEqual(typeof sourceFile, 'string');
assert(key === null || typeof key === 'string');
var stream = fs.createReadStream(sourceFile);
var ps = progressStream({ time: 10000 }); // display a progress every 10 seconds
stream.on('error', function (error) {
debug('createReadStream: tar stream error.', error);
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
ps.on('progress', function(progress) {
debug('createReadStream: %s@%s (%s)', Math.round(progress.transferred/1024/1024) + 'M', Math.round(progress.speed/1024/1024) + 'Mbps', sourceFile);
});
if (key !== null) {
var encrypt = crypto.createCipher('aes-256-cbc', key);
encrypt.on('error', function (error) {
debug('createReadStream: encrypt stream error.', error);
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
return stream.pipe(encrypt).pipe(ps);
} else {
return stream.pipe(ps);
}
}
function createWriteStream(destFile, key) {
assert.strictEqual(typeof destFile, 'string');
assert(key === null || typeof key === 'string');
var stream = fs.createWriteStream(destFile);
if (key !== null) {
var decrypt = crypto.createDecipher('aes-256-cbc', key);
decrypt.on('error', function (error) {
debug('createWriteStream: decrypt stream error.', error);
});
decrypt.pipe(stream);
return decrypt;
} else {
return stream;
}
}
function createTarPackStream(sourceDir, key) {
assert.strictEqual(typeof sourceDir, 'string');
assert(key === null || typeof key === 'string');
@@ -232,35 +318,47 @@ function sync(backupConfig, backupId, dataDir, callback) {
assert.strictEqual(typeof dataDir, 'string');
assert.strictEqual(typeof callback, 'function');
function setBackupProgress(message) {
debug('%s', 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);
// the empty task.path is special to signify the directory
const destPath = task.path && backupConfig.key ? encryptFilePath(task.path, backupConfig.key) : task.path;
const backupFilePath = path.join(getBackupFilePath(backupConfig, backupId, backupConfig.format), destPath);
if (task.operation === 'removedir') {
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, `Removing directory ${task.path}`);
setBackupProgress(`Removing directory ${backupFilePath}`);
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 ${backupFilePath}`);
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}`);
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);
setBackupProgress(`Adding ${task.path} position ${task.position} try ${retryCount}`);
var stream = createReadStream(path.join(dataDir, task.path), backupConfig.key || null);
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 +392,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 +399,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
@@ -357,7 +455,7 @@ function tarExtract(inStream, destination, key, callback) {
var decrypt = crypto.createDecipher('aes-256-cbc', key);
decrypt.on('error', function (error) {
debug('tarExtract: decrypt stream error.', error);
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, `Failed to decrypt: ${error.message}`));
});
inStream.pipe(ps).pipe(decrypt).pipe(gunzip).pipe(extract);
} else {
@@ -391,6 +489,45 @@ function restoreFsMetadata(appDataDir, callback) {
});
}
function downloadDir(backupConfig, backupFilePath, destDir, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupFilePath, 'string');
assert.strictEqual(typeof destDir, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`downloadDir: ${backupFilePath} to ${destDir}`);
function downloadFile(entry, callback) {
let relativePath = path.relative(backupFilePath, entry.fullPath);
if (backupConfig.key) {
relativePath = decryptFilePath(relativePath, backupConfig.key);
if (!relativePath) return callback(new BackupsError(BackupsError.BAD_STATE, 'Unable to decrypt file'));
}
const destFilePath = path.join(destDir, relativePath);
mkdirp(path.dirname(destFilePath), function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
api(backupConfig.provider).download(backupConfig, entry.fullPath, function (error, sourceStream) {
if (error) return callback(error);
sourceStream.on('error', callback);
let destStream = createWriteStream(destFilePath, backupConfig.key || null);
destStream.on('error', callback);
debug(`downloadDir: Copying ${entry.fullPath} to ${destFilePath}`);
sourceStream.pipe(destStream, { end: true }).on('finish', callback);
});
});
}
api(backupConfig.provider).listDir(backupConfig, backupFilePath, 1000, function (entries, done) {
async.each(entries, downloadFile, done);
}, callback);
}
function download(backupConfig, backupId, format, dataDir, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
@@ -409,9 +546,7 @@ function download(backupConfig, backupId, format, dataDir, callback) {
tarExtract(sourceStream, dataDir, backupConfig.key || null, callback);
});
} else {
var events = api(backupConfig.provider).downloadDir(backupConfig, getBackupFilePath(backupConfig, backupId, format), dataDir);
events.on('progress', log);
events.on('done', function (error) {
downloadDir(backupConfig, getBackupFilePath(backupConfig, backupId, format), dataDir, function (error) {
if (error) return callback(error);
restoreFsMetadata(dataDir, callback);
@@ -553,6 +688,34 @@ function uploadBoxSnapshot(backupConfig, callback) {
});
}
function backupDone(apiConfig, backupId, appBackupIds, callback) {
assert.strictEqual(typeof apiConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
assert(Array.isArray(appBackupIds));
assert.strictEqual(typeof callback, 'function');
if (apiConfig.provider !== 'caas') return callback();
debug('[%s] backupDone: %s apps %j', backupId, backupId, appBackupIds);
var url = config.apiServerOrigin() + '/api/v1/boxes/' + apiConfig.fqdn + '/backupDone';
var data = {
boxVersion: config.version(),
backupId: backupId,
appId: null, // now unused
appVersion: null, // now unused
appBackupIds: appBackupIds
};
superagent.post(url).send(data).query({ token: apiConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
if (result.statusCode !== 200) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, result.text));
return callback(null);
});
}
function rotateBoxBackup(backupConfig, timestamp, appBackupIds, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof timestamp, 'string');
@@ -582,8 +745,7 @@ function rotateBoxBackup(backupConfig, timestamp, appBackupIds, callback) {
log(`Rotated box backup successfully as id ${backupId}`);
// FIXME this is only needed for caas, hopefully we can remove that in the future
api(backupConfig.provider).backupDone(backupConfig, backupId, appBackupIds, function (error) {
backupDone(backupConfig, backupId, appBackupIds, function (error) {
if (error) return callback(error);
callback(null, backupId);
@@ -720,7 +882,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 +909,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 +924,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
});

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 : '');

View File

@@ -1,11 +1,16 @@
'use strict';
exports = module.exports = {
verifySetupToken: verifySetupToken,
setupDone: setupDone,
changePlan: changePlan,
upgrade: upgrade,
sendHeartbeat: sendHeartbeat,
getBoxAndUserDetails: getBoxAndUserDetails,
setPtrRecord: setPtrRecord
setPtrRecord: setPtrRecord,
CaasError: CaasError
};
var assert = require('assert'),
@@ -15,13 +20,13 @@ var assert = require('assert'),
locker = require('./locker.js'),
path = require('path'),
progress = require('./progress.js'),
settings = require('./settings.js'),
shell = require('./shell.js'),
superagent = require('superagent'),
util = require('util'),
_ = require('underscore');
const RETIRE_CMD = path.join(__dirname, 'scripts/retire.sh');
var gBoxAndUserDetails = null; // cached cloudron details like region,size...
function CaasError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
@@ -43,9 +48,10 @@ function CaasError(reason, errorOrMessage) {
}
util.inherits(CaasError, Error);
CaasError.BAD_FIELD = 'Field error';
CaasError.BAD_STATE = 'Bad state';
CaasError.INVALID_TOKEN = 'Invalid Token';
CaasError.INTERNAL_ERROR = 'Internal Error';
CaasError.EXTERNAL_ERROR = 'External Error';
CaasError.BAD_STATE = 'Bad state';
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
@@ -56,14 +62,64 @@ function retire(reason, info, callback) {
var data = {
apiServerOrigin: config.apiServerOrigin(),
adminFqdn: config.adminFqdn(),
fqdn: config.fqdn()
adminFqdn: config.adminFqdn()
};
shell.sudo('retire', [ RETIRE_CMD, reason, JSON.stringify(info), JSON.stringify(data) ], callback);
}
function doMigrate(options, callback) {
function getCaasConfig(callback) {
assert.strictEqual(typeof callback, 'function');
settings.getCaasConfig(function (error, result) {
if (error) return callback(new CaasError(CaasError.INTERNAL_ERROR, error));
callback(null, result);
});
}
function verifySetupToken(setupToken, callback) {
assert.strictEqual(typeof setupToken, 'string');
assert.strictEqual(typeof callback, 'function');
settings.getCaasConfig(function (error, caasConfig) {
if (error) return callback(new CaasError(CaasError.INTERNAL_ERROR, error));
superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId + '/setup/verify').query({ setupToken: setupToken })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
if (result.statusCode === 403) return callback(new CaasError(CaasError.INVALID_TOKEN));
if (result.statusCode === 409) return callback(new CaasError(CaasError.BAD_STATE, 'Already setup'));
if (result.statusCode !== 200) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
callback(null);
});
});
}
function setupDone(setupToken, callback) {
assert.strictEqual(typeof setupToken, 'string');
assert.strictEqual(typeof callback, 'function');
settings.getCaasConfig(function (error, caasConfig) {
if (error) return callback(new CaasError(CaasError.INTERNAL_ERROR, error));
// Now let the api server know we got activated
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId + '/setup/done').query({ setupToken: setupToken })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
if (result.statusCode === 403) return callback(new CaasError(CaasError.INVALID_TOKEN));
if (result.statusCode === 409) return callback(new CaasError(CaasError.BAD_STATE, 'Already setup'));
if (result.statusCode !== 201) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
callback(null);
});
});
}
function doMigrate(options, caasConfig, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof caasConfig, 'object');
assert.strictEqual(typeof callback, 'function');
var error = locker.lock(locker.OP_MIGRATE);
@@ -84,8 +140,8 @@ function doMigrate(options, callback) {
debug('migrate: domain: %s size %s region %s', options.domain, options.size, options.region);
superagent
.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/migrate')
.query({ token: config.token() })
.post(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId + '/migrate')
.query({ token: caasConfig.token })
.send(options)
.timeout(30 * 1000)
.end(function (error, result) {
@@ -109,7 +165,11 @@ function changePlan(options, callback) {
if (config.isDemo()) return callback(new CaasError(CaasError.BAD_FIELD, 'Not allowed in demo mode'));
doMigrate(options, callback);
getCaasConfig(function (error, result) {
if (error) return callback(error);
doMigrate(options, result, callback);
});
}
// this function expects a lock
@@ -126,69 +186,81 @@ function upgrade(boxUpdateInfo, callback) {
backups.backupBoxAndApps({ userId: null, username: 'upgrader' }, function (error) {
if (error) return upgradeError(error);
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/upgrade')
.query({ token: config.token() })
.send({ version: boxUpdateInfo.version })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return upgradeError(new Error('Network error making upgrade request: ' + error));
if (result.statusCode !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
getCaasConfig(function (error, result) {
if (error) return upgradeError(error);
progress.set(progress.UPDATE, 10, 'Updating base system');
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + result.boxId + '/upgrade')
.query({ token: result.token })
.send({ version: boxUpdateInfo.version })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return upgradeError(new Error('Network error making upgrade request: ' + error));
if (result.statusCode !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
// no need to unlock since this is the last thing we ever do on this box
callback();
progress.set(progress.UPDATE, 10, 'Updating base system');
retire('upgrade');
});
// no need to unlock since this is the last thing we ever do on this box
callback();
retire('upgrade');
});
});
});
}
function sendHeartbeat() {
assert(config.provider() === 'caas', 'Heartbeat is only sent for managed cloudrons');
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat';
superagent.post(url).query({ token: config.token(), version: config.version() }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) debug('Network error sending heartbeat.', error);
else if (result.statusCode !== 200) debug('Server responded to heartbeat with %s %s', result.statusCode, result.text);
else debug('Heartbeat sent to %s', url);
getCaasConfig(function (error, result) {
if (error) return debug('Caas config missing', error);
var url = config.apiServerOrigin() + '/api/v1/boxes/' + result.boxId + '/heartbeat';
superagent.post(url).query({ token: result.token, version: config.version() }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) debug('Network error sending heartbeat.', error);
else if (result.statusCode !== 200) debug('Server responded to heartbeat with %s %s', result.statusCode, result.text);
else debug('Heartbeat sent to %s', url);
});
});
}
function getBoxAndUserDetails(callback) {
assert.strictEqual(typeof callback, 'function');
if (gBoxAndUserDetails) return callback(null, gBoxAndUserDetails);
if (config.provider() !== 'caas') return callback(null, {});
superagent
.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn())
.query({ token: config.token() })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, 'Cannot reach appstore'));
if (result.statusCode !== 200) return callback(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
getCaasConfig(function (error, caasConfig) {
if (error) return callback(error);
gBoxAndUserDetails = result.body;
superagent
.get(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId)
.query({ token: caasConfig.token })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, 'Cannot reach appstore'));
if (result.statusCode !== 200) return callback(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
return callback(null, gBoxAndUserDetails);
});
return callback(null, result.body);
});
});
}
function setPtrRecord(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
superagent
.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/ptr')
.query({ token: config.token() })
.send({ domain: domain })
.timeout(5 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, 'Cannot reach appstore'));
if (result.statusCode !== 202) return callback(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
getCaasConfig(function (error, result) {
if (error) return callback(error);
return callback(null);
});
superagent
.post(config.apiServerOrigin() + '/api/v1/boxes/' + result.boxId + '/ptr')
.query({ token: result.token })
.send({ domain: domain })
.timeout(5 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, 'Cannot reach appstore'));
if (result.statusCode !== 202) return callback(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
return callback(null);
});
});
}

View File

@@ -10,12 +10,12 @@ exports = module.exports = {
var assert = require('assert'),
debug = require('debug')('box:cert/caas.js');
function getCertificate(domain, options, callback) {
assert.strictEqual(typeof domain, 'string');
function getCertificate(vhost, options, callback) {
assert.strictEqual(typeof vhost, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debug('getCertificate: using fallback certificate', domain);
debug('getCertificate: using fallback certificate', vhost);
return callback(null, 'cert/host.cert', 'cert/host.key');
return callback(null, '', '');
}

View File

@@ -10,12 +10,12 @@ exports = module.exports = {
var assert = require('assert'),
debug = require('debug')('box:cert/fallback.js');
function getCertificate(domain, options, callback) {
assert.strictEqual(typeof domain, 'string');
function getCertificate(vhost, options, callback) {
assert.strictEqual(typeof vhost, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debug('getCertificate: using fallback certificate', domain);
debug('getCertificate: using fallback certificate', vhost);
return callback(null, '', '');
}

View File

@@ -1,447 +0,0 @@
'use strict';
exports = module.exports = {
CertificatesError: CertificatesError,
ensureFallbackCertificate: ensureFallbackCertificate,
setFallbackCertificate: setFallbackCertificate,
getFallbackCertificate: getFallbackCertificate,
validateCertificate: validateCertificate,
ensureCertificate: ensureCertificate,
setAdminCertificate: setAdminCertificate,
getAdminCertificate: getAdminCertificate,
renewAll: renewAll,
initialize: initialize,
uninitialize: uninitialize,
events: null,
EVENT_CERT_CHANGED: 'cert_changed',
// exported for testing
_getApi: getApi
};
var acme = require('./cert/acme.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
caas = require('./cert/caas.js'),
config = require('./config.js'),
constants = require('./constants.js'),
debug = require('debug')('box:certificates'),
eventlog = require('./eventlog.js'),
fallback = require('./cert/fallback.js'),
fs = require('fs'),
mailer = require('./mailer.js'),
nginx = require('./nginx.js'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
user = require('./user.js'),
util = require('util');
function CertificatesError(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(CertificatesError, Error);
CertificatesError.INTERNAL_ERROR = 'Internal Error';
CertificatesError.INVALID_CERT = 'Invalid certificate';
CertificatesError.NOT_FOUND = 'Not Found';
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
exports.events = new (require('events').EventEmitter)();
callback();
}
function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
exports.events = null;
callback();
}
function getApi(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
settings.getTlsConfig(function (error, tlsConfig) {
if (error) return callback(error);
if (tlsConfig.provider === 'fallback') return callback(null, fallback, {});
// use acme if we have altDomain or the tlsConfig is not caas
var api = (app.altDomain || tlsConfig.provider !== 'caas') ? acme : caas;
var options = { };
if (tlsConfig.provider === 'caas') {
options.prod = true; // with altDomain, we will choose acme setting based on this
} else { // acme
options.prod = tlsConfig.provider.match(/.*-prod/) !== null;
}
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
// we cannot use admin@fqdn because the user might not have set it up.
// we 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) {
options.email = error ? 'support@cloudron.io' : (owner.alternateEmail || owner.email); // can error if not activated yet
callback(null, api, options);
});
});
}
function ensureFallbackCertificate(callback) {
// ensure a fallback certificate that much of our code requires
var certFilePath = path.join(paths.APP_CERTS_DIR, 'host.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, 'host.key');
var fallbackCertPath = path.join(paths.NGINX_CERT_DIR, 'host.cert');
var fallbackKeyPath = path.join(paths.NGINX_CERT_DIR, 'host.key');
if (fs.existsSync(fallbackCertPath) && fs.existsSync(fallbackKeyPath)) {
debug('ensureFallbackCertificate: pre-existing fallback certs');
return callback();
}
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) { // existing custom fallback certs (when restarting, restoring, updating)
debug('ensureFallbackCertificate: using fallback certs provided by user');
if (!safe.child_process.execSync('cp ' + certFilePath + ' ' + fallbackCertPath)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
if (!safe.child_process.execSync('cp ' + keyFilePath + ' ' + fallbackKeyPath)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
return callback();
}
// generate a self-signed cert. it's in backup dir so that we don't create a new cert across restarts
// FIXME: this cert does not cover the naked domain. needs SAN
if (config.fqdn()) {
debug('ensureFallbackCertificate: generating self-signed certificate');
var certCommand = util.format('openssl req -x509 -newkey rsa:2048 -keyout %s -out %s -days 3650 -subj /CN=*.%s -nodes', keyFilePath, certFilePath, config.fqdn());
safe.child_process.execSync(certCommand);
if (!safe.child_process.execSync('cp ' + certFilePath + ' ' + fallbackCertPath)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
if (!safe.child_process.execSync('cp ' + keyFilePath + ' ' + fallbackKeyPath)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
return callback();
} else {
debug('ensureFallbackCertificate: cannot generate fallback certificate without domain');
return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, 'No domain set'));
}
}
function isExpiringSync(certFilePath, hours) {
assert.strictEqual(typeof certFilePath, 'string');
assert.strictEqual(typeof hours, 'number');
if (!fs.existsSync(certFilePath)) return 2; // not found
var result = safe.child_process.spawnSync('/usr/bin/openssl', [ 'x509', '-checkend', String(60 * 60 * hours), '-in', certFilePath ]);
debug('isExpiringSync: %s %s %s', certFilePath, result.stdout.toString('utf8').trim(), result.status);
return result.status === 1; // 1 - expired 0 - not expired
}
function renewAll(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('renewAll: Checking certificates for renewal');
apps.getAll(function (error, allApps) {
if (error) return callback(error);
allApps.push({ intrinsicFqdn: config.adminFqdn() }); // inject fake webadmin app
var expiringApps = [ ];
for (var i = 0; i < allApps.length; i++) {
var appDomain = allApps[i].altDomain || allApps[i].instrincFqdn;
var certFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.user.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.user.key');
if (safe.fs.existsSync(certFilePath) && safe.fs.existsSync(keyFilePath)) {
debug('renewAll: existing user key file for %s. skipping', appDomain);
continue;
}
// check if we have an auto cert to be renewed
certFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.cert');
keyFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.key');
if (!safe.fs.existsSync(keyFilePath)) {
debug('renewAll: no existing key file for %s. skipping', appDomain);
continue;
}
if (isExpiringSync(certFilePath, 24 * 30)) { // expired or not found
expiringApps.push(allApps[i]);
}
}
debug('renewAll: %j needs to be renewed', expiringApps.map(function (app) { return app.altDomain || app.intrinsicFqdn; }));
async.eachSeries(expiringApps, function iterator(app, iteratorCallback) {
var domain = app.altDomain || app.intrinsicFqdn;
getApi(app, function (error, api, apiOptions) {
if (error) return callback(error);
debug('renewAll: renewing cert for %s with options %j', domain, apiOptions);
api.getCertificate(domain, apiOptions, function (error) {
var certFilePath = path.join(paths.APP_CERTS_DIR, domain + '.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.key');
var errorMessage = error ? error.message : '';
eventlog.add(eventlog.ACTION_CERTIFICATE_RENEWAL, auditSource, { domain: domain, errorMessage: errorMessage });
if (error) {
debug('renewAll: could not renew cert for %s because %s', domain, error);
mailer.certificateRenewalError(domain, errorMessage);
// check if we should fallback if we expire in the coming day
if (!isExpiringSync(certFilePath, 24 * 1)) return iteratorCallback();
debug('renewAll: using fallback certs for %s since it expires soon', domain, error);
// if no cert was returned use fallback, the fallback provider will not provide any for example
var fallbackCertFilePath = path.join(paths.NGINX_CERT_DIR, domain + '.cert');
var fallbackKeyFilePath = path.join(paths.NGINX_CERT_DIR, domain + '.key');
certFilePath = fs.existsSync(fallbackCertFilePath) ? fallbackCertFilePath : 'cert/host.cert';
keyFilePath = fs.existsSync(fallbackKeyFilePath) ? fallbackKeyFilePath : 'cert/host.key';
} else {
debug('renewAll: certificate for %s renewed', domain);
}
// reconfigure and reload nginx. this is required for the case where we got a renewed cert after fallback
var configureFunc = app.intrinsicFqdn === config.adminFqdn() ?
nginx.configureAdmin.bind(null, certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn())
: nginx.configureApp.bind(null, app, certFilePath, keyFilePath);
configureFunc(function (ignoredError) {
if (ignoredError) debug('fallbackExpiredCertificates: error reconfiguring app', ignoredError);
exports.events.emit(exports.EVENT_CERT_CHANGED, domain);
iteratorCallback(); // move to next app
});
});
});
});
});
}
// note: https://tools.ietf.org/html/rfc4346#section-7.4.2 (certificate_list) requires that the
// servers certificate appears first (and not the intermediate cert)
function validateCertificate(cert, key, fqdn) {
assert(cert === null || typeof cert === 'string');
assert(key === null || typeof key === 'string');
assert.strictEqual(typeof fqdn, 'string');
function matchesDomain(domain) {
if (typeof domain !== 'string') return false;
if (domain === fqdn) return true;
if (domain.indexOf('*') === 0 && domain.slice(2) === fqdn.slice(fqdn.indexOf('.') + 1)) return true;
return false;
}
if (cert === null && key === null) return null;
if (!cert && key) return new Error('missing cert');
if (cert && !key) return new Error('missing key');
var result = safe.child_process.execSync('openssl x509 -noout -checkhost "' + fqdn + '"', { encoding: 'utf8', input: cert });
if (!result) return new Error('Invalid certificate. 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 Error(util.format('Certificate is not valid for this domain. Expecting %s in %j', fqdn, altNames));
}
// 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 });
var keyModulus = safe.child_process.execSync('openssl rsa -noout -modulus', { encoding: 'utf8', input: key });
if (certModulus !== keyModulus) return new Error('Key does not match the certificate.');
// check expiration
result = safe.child_process.execSync('openssl x509 -checkend 0', { encoding: 'utf8', input: cert });
if (!result) return new Error('Certificate is expired.');
return null;
}
function setFallbackCertificate(cert, key, fqdn, callback) {
assert.strictEqual(typeof cert, 'string');
assert.strictEqual(typeof key, 'string');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof callback, 'function');
var error = validateCertificate(cert, key, '*.' + fqdn);
if (error) return callback(new CertificatesError(CertificatesError.INVALID_CERT, error.message));
// backup the cert
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.cert'), cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.key'), key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
// copy over fallback cert
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, fqdn + '.cert'), cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, fqdn + '.key'), key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
exports.events.emit(exports.EVENT_CERT_CHANGED, '*.' + fqdn);
nginx.reload(function (error) {
if (error) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, error));
return callback(null);
});
}
function getFallbackCertificate(fqdn, callback) {
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof callback, 'function');
var cert = safe.fs.readFileSync(path.join(paths.NGINX_CERT_DIR, fqdn + '.cert'), 'utf-8');
var key = safe.fs.readFileSync(path.join(paths.NGINX_CERT_DIR, fqdn + '.key'), 'utf-8');
if (!cert || !key) return callback(new CertificatesError(CertificatesError.NOT_FOUND));
callback(null, { cert: cert, key: key });
}
function setAdminCertificate(cert, key, callback) {
assert.strictEqual(typeof cert, 'string');
assert.strictEqual(typeof key, 'string');
assert.strictEqual(typeof callback, 'function');
var vhost = config.adminFqdn();
var certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.user.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.user.key');
var error = validateCertificate(cert, key, vhost);
if (error) return callback(new CertificatesError(CertificatesError.INVALID_CERT, error.message));
// backup the cert
if (!safe.fs.writeFileSync(certFilePath, cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(keyFilePath, key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
exports.events.emit(exports.EVENT_CERT_CHANGED, vhost);
nginx.configureAdmin(certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), callback);
}
function getAdminCertificatePath(callback) {
assert.strictEqual(typeof callback, 'function');
var vhost = config.adminFqdn();
var certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.user.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.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');
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, certFilePath, keyFilePath);
// any user fallback cert is always copied over to nginx cert dir
callback(null, path.join(paths.NGINX_CERT_DIR, 'host.cert'), path.join(paths.NGINX_CERT_DIR, 'host.key'));
}
function getAdminCertificate(callback) {
assert.strictEqual(typeof callback, 'function');
getAdminCertificatePath(function (error, certFilePath, keyFilePath) {
if (error) return callback(error);
var cert = safe.fs.readFileSync(certFilePath);
if (!cert) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error));
var key = safe.fs.readFileSync(keyFilePath);
if (!cert) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error));
return callback(null, cert, key);
});
}
function ensureCertificate(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var domain = app.altDomain || app.intrinsicFqdn;
var certFilePath = path.join(paths.APP_CERTS_DIR, domain + '.user.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.user.key');
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) {
debug('ensureCertificate: %s. user certificate already exists at %s', domain, keyFilePath);
return callback(null, certFilePath, keyFilePath);
}
certFilePath = path.join(paths.APP_CERTS_DIR, domain + '.cert');
keyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.key');
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) {
debug('ensureCertificate: %s. certificate already exists at %s', domain, keyFilePath);
if (!isExpiringSync(certFilePath, 24 * 1)) return callback(null, certFilePath, keyFilePath);
debug('ensureCertificate: %s cert require renewal', domain);
} else {
debug('ensureCertificate: %s cert does not exist', domain);
}
getApi(app, function (error, api, apiOptions) {
if (error) return callback(error);
debug('ensureCertificate: getting certificate for %s with options %j', domain, apiOptions);
api.getCertificate(domain, apiOptions, function (error, certFilePath, keyFilePath) {
if (error) debug('ensureCertificate: could not get certificate. using fallback certs', error);
// if no cert was returned use fallback, the fallback provider will not provide any for example
if (!certFilePath || !keyFilePath) {
var fallbackCertFilePath = path.join(paths.NGINX_CERT_DIR, app.domain + '.cert');
var fallbackKeyFilePath = path.join(paths.NGINX_CERT_DIR, app.domain + '.key');
certFilePath = fs.existsSync(fallbackCertFilePath) ? fallbackCertFilePath : 'cert/host.cert';
keyFilePath = fs.existsSync(fallbackKeyFilePath) ? fallbackKeyFilePath : 'cert/host.key';
}
callback(null, certFilePath, keyFilePath);
});
});
}

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);
}

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,11 +29,15 @@ var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
clientdb = require('./clientdb.js'),
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:clients'),
hat = require('hat'),
eventlog = require('./eventlog.js'),
hat = require('./hat.js'),
accesscontrol = require('./accesscontrol.js'),
tokendb = require('./tokendb.js'),
users = require('./users.js'),
UsersError = users.UsersError,
util = require('util'),
uuid = require('uuid');
@@ -85,28 +79,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');
@@ -114,13 +86,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.validateScopeString(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);
@@ -192,7 +160,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);
@@ -217,7 +185,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');
@@ -236,7 +204,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');
@@ -276,7 +244,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,22 +253,51 @@ function addClientTokenByUserId(clientId, userId, expiresAt, callback) {
get(clientId, function (error, result) {
if (error) return callback(error);
var token = tokendb.generateToken();
tokendb.add(token, userId, result.id, expiresAt, result.scope, function (error) {
users.get(userId, function (error, user) {
if (error && error.reason === UsersError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such user'));
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
callback(null, {
accessToken: token,
identifier: userId,
clientId: result.id,
scope: result.id,
expires: expiresAt
accesscontrol.scopesForUser(user, function (error, userScopes) {
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
var scope = accesscontrol.canonicalScopeString(result.scope);
var authorizedScopes = accesscontrol.intersectScopes(userScopes, scope.split(','));
var token = tokendb.generateToken();
tokendb.add(token, userId, result.id, expiresAt, authorizedScopes.join(','), function (error) {
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
callback(null, {
accessToken: token,
tokenScopes: authorizedScopes,
identifier: userId,
clientId: result.id,
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');
@@ -318,19 +315,17 @@ function delToken(clientId, tokenId, callback) {
});
}
function addDefaultClients(callback) {
function addDefaultClients(origin, callback) {
assert.strictEqual(typeof origin, 'string');
assert.strictEqual(typeof callback, 'function');
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', config.adminOrigin(), ADMIN_SCOPES),
clientdb.upsert.bind(null, 'cid-sdk', 'SDK', 'built-in', 'secret-sdk', config.adminOrigin(), '*,roleSdk'),
clientdb.upsert.bind(null, 'cid-cli', 'Cloudron Tool', 'built-in', 'secret-cli', config.adminOrigin(), '*, 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);
}

View File

@@ -5,77 +5,42 @@ exports = module.exports = {
initialize: initialize,
uninitialize: uninitialize,
activate: activate,
getConfig: getConfig,
getStatus: getStatus,
getDisks: getDisks,
dnsSetup: dnsSetup,
getLogs: getLogs,
updateToLatest: updateToLatest,
restore: restore,
reboot: reboot,
checkDiskSpace: checkDiskSpace,
onActivated: onActivated,
readDkimPublicKeySync: readDkimPublicKeySync,
refreshDNS: refreshDNS,
configureWebadmin: configureWebadmin
checkDiskSpace: checkDiskSpace
};
var appdb = require('./appdb.js'),
apps = require('./apps.js'),
assert = require('assert'),
var assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
BackupsError = require('./backups.js').BackupsError,
caas = require('./caas.js'),
certificates = require('./certificates.js'),
child_process = require('child_process'),
clients = require('./clients.js'),
config = require('./config.js'),
constants = require('./constants.js'),
cron = require('./cron.js'),
debug = require('debug')('box:cloudron'),
df = require('@sindresorhus/df'),
domains = require('./domains.js'),
DomainError = domains.DomainError,
eventlog = require('./eventlog.js'),
fs = require('fs'),
locker = require('./locker.js'),
mailer = require('./mailer.js'),
nginx = require('./nginx.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
platform = require('./platform.js'),
progress = require('./progress.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
settingsdb = require('./settingsdb.js'),
SettingsError = settings.SettingsError,
shell = require('./shell.js'),
spawn = require('child_process').spawn,
split = require('split'),
superagent = require('superagent'),
sysinfo = require('./sysinfo.js'),
tld = require('tldjs'),
tokendb = require('./tokendb.js'),
updateChecker = require('./updatechecker.js'),
user = require('./user.js'),
UserError = user.UserError,
util = require('util'),
_ = require('underscore');
users = require('./users.js'),
util = require('util');
var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'),
UPDATE_CMD = path.join(__dirname, 'scripts/update.sh'),
RESTART_CMD = path.join(__dirname, 'scripts/restart.sh');
var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
var gWebadminStatus = { dns: false, tls: false, configuring: false, restoring: false };
function CloudronError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
@@ -98,8 +63,6 @@ util.inherits(CloudronError, Error);
CloudronError.BAD_FIELD = 'Field error';
CloudronError.INTERNAL_ERROR = 'Internal Error';
CloudronError.EXTERNAL_ERROR = 'External Error';
CloudronError.ALREADY_PROVISIONED = 'Already Provisioned';
CloudronError.ALREADY_SETUP = 'Already Setup';
CloudronError.BAD_STATE = 'Bad state';
CloudronError.ALREADY_UPTODATE = 'No Update Available';
CloudronError.NOT_FOUND = 'Not found';
@@ -108,21 +71,12 @@ CloudronError.SELF_UPGRADE_NOT_SUPPORTED = 'Self upgrade not supported';
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
gWebadminStatus = { dns: false, tls: false, configuring: false, restoring: false };
async.series([
certificates.initialize,
settings.initialize,
configureDefaultServer,
onDomainConfigured,
reverseProxy.configureDefaultServer,
cron.initialize, // required for caas heartbeat before activation
onActivated
], function (error) {
if (error) return callback(error);
configureWebadmin(NOOP_CALLBACK); // for restore() and caas initial setup. do not block
callback();
});
], callback);
}
function uninitialize(callback) {
@@ -131,31 +85,17 @@ function uninitialize(callback) {
async.series([
cron.uninitialize,
platform.stop,
certificates.uninitialize,
settings.uninitialize
], callback);
}
function onDomainConfigured(callback) {
callback = callback || NOOP_CALLBACK;
if (!config.fqdn()) return callback();
async.series([
clients.addDefaultClients,
certificates.ensureFallbackCertificate,
ensureDkimKey,
cron.initialize // required for caas heartbeat before activation
], callback);
}
function onActivated(callback) {
callback = callback || NOOP_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
@@ -163,238 +103,6 @@ function onActivated(callback) {
});
}
function autoprovision(callback) {
assert.strictEqual(typeof callback, 'function');
const confJson = safe.fs.readFileSync(paths.AUTO_PROVISION_FILE, 'utf8');
if (!confJson) return callback();
const conf = safe.JSON.parse(confJson);
if (!conf) return callback();
async.eachSeries(Object.keys(conf), function (key, iteratorDone) {
var name;
switch (key) {
case 'dnsConfig': name = 'dns_config'; break;
case 'tlsConfig': name = 'tls_config'; break;
case 'backupConfig': name = 'backup_config'; break;
case 'tlsCert':
debug(`autoprovision: ${key}`);
return fs.writeFile(path.join(paths.NGINX_CERT_DIR, 'host.cert'), conf[key], iteratorDone);
case 'tlsKey':
debug(`autoprovision: ${key}`);
return fs.writeFile(path.join(paths.NGINX_CERT_DIR, 'host.key'), conf[key], iteratorDone);
default:
debug(`autoprovision: ${key} ignored`);
return iteratorDone();
}
debug(`autoprovision: ${name}`);
settingsdb.set(name, JSON.stringify(conf[key]), iteratorDone);
}, callback);
}
function dnsSetup(adminFqdn, domain, zoneName, provider, dnsConfig, callback) {
assert.strictEqual(typeof adminFqdn, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
if (config.fqdn()) return callback(new CloudronError(CloudronError.ALREADY_SETUP));
if (!zoneName) zoneName = tld.getDomain(domain) || domain;
debug('dnsSetup: Setting up Cloudron with domain %s and zone %s', domain, zoneName);
function done(error) {
if (error && error.reason === DomainError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
autoprovision(function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
config.setFqdn(domain); // set fqdn only after dns config is valid, otherwise cannot re-setup if we failed
config.setAdminFqdn(adminFqdn);
config.setAdminLocation('my');
config.setZoneName(zoneName);
callback();
async.series([ // do not block
onDomainConfigured,
configureWebadmin
], NOOP_CALLBACK);
});
}
domains.get(domain, function (error, result) {
if (error && error.reason !== DomainError.NOT_FOUND) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (!result) domains.add(domain, zoneName, provider, dnsConfig, null /* cert */, done);
else domains.update(domain, provider, dnsConfig, null /* cert */, done);
});
}
function configureDefaultServer(callback) {
callback = callback || NOOP_CALLBACK;
debug('configureDefaultServer: domain %s', config.fqdn());
if (process.env.BOX_ENV === 'test') return callback();
var certFilePath = path.join(paths.NGINX_CERT_DIR, 'default.cert');
var keyFilePath = path.join(paths.NGINX_CERT_DIR, 'default.key');
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
debug('configureDefaultServer: create new cert');
var cn = 'cloudron-' + (new Date()).toISOString(); // randomize date a bit to keep firefox happy
var certCommand = util.format('openssl req -x509 -newkey rsa:2048 -keyout %s -out %s -days 3650 -subj /CN=%s -nodes', keyFilePath, certFilePath, cn);
safe.child_process.execSync(certCommand);
}
nginx.configureAdmin(certFilePath, keyFilePath, 'default.conf', '', function (error) {
if (error) return callback(error);
debug('configureDefaultServer: done');
callback(null);
});
}
function configureWebadmin(callback) {
callback = callback || NOOP_CALLBACK;
debug('configureWebadmin: fqdn:%s status:%j', config.fqdn(), gWebadminStatus);
if (process.env.BOX_ENV === 'test' || !config.fqdn() || gWebadminStatus.configuring) return callback();
gWebadminStatus.configuring = true; // re-entracy guard
function done(error) {
gWebadminStatus.configuring = false;
debug('configureWebadmin: done error: %j', error || {});
callback(error);
}
function configureNginx(error) {
debug('configureNginx: dns update: %j', error || {});
certificates.ensureCertificate({ domain: config.fqdn(), location: config.adminLocation(), intrinsicFqdn: config.adminFqdn() }, function (error, certFilePath, keyFilePath) {
if (error) return done(error);
gWebadminStatus.tls = true;
nginx.configureAdmin(certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), done);
});
}
// update the DNS. configure nginx regardless of whether it succeeded so that
// box is accessible even if dns creds are invalid
sysinfo.getPublicIp(function (error, ip) {
if (error) return configureNginx(error);
addDnsRecords(ip, function (error) {
if (error) return configureNginx(error);
domains.waitForDNSRecord(config.adminFqdn(), config.fqdn(), ip, 'A', { interval: 30000, times: 50000 }, function (error) {
if (error) return configureNginx(error);
gWebadminStatus.dns = true;
configureNginx();
});
});
});
}
function setTimeZone(ip, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
debug('setTimeZone ip:%s', ip);
superagent.get('https://geolocation.cloudron.io/json').query({ ip: ip }).timeout(10 * 1000).end(function (error, result) {
if ((error && !error.response) || result.statusCode !== 200) {
debug('Failed to get geo location: %s', error.message);
return callback(null);
}
var timezone = safe.query(result.body, 'location.time_zone');
if (!timezone || typeof timezone !== 'string') {
debug('No timezone in geoip response : %j', result.body);
return callback(null);
}
debug('Setting timezone to ', timezone);
settings.setTimeZone(timezone, callback);
});
}
function activate(username, password, email, displayName, ip, auditSource, callback) {
assert.strictEqual(typeof username, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof displayName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('activating user:%s email:%s', username, email);
setTimeZone(ip, function () { }); // TODO: get this from user. note that timezone is detected based on the browser location and not the cloudron region
user.createOwner(username, password, email, displayName, auditSource, function (error, userObject) {
if (error && error.reason === UserError.ALREADY_EXISTS) return callback(new CloudronError(CloudronError.ALREADY_PROVISIONED));
if (error && error.reason === UserError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
clients.get('cid-webadmin', function (error, result) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
// Also generate a token so the admin creation can also act as a login
var token = tokendb.generateToken();
var expires = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
tokendb.add(token, userObject.id, result.id, expires, '*', function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { });
onActivated();
callback(null, { token: token, expires: expires });
});
});
});
}
function getStatus(callback) {
assert.strictEqual(typeof callback, 'function');
user.count(function (error, count) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
settings.getCloudronName(function (error, cloudronName) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
callback(null, {
activated: count !== 0,
version: config.version(),
apiServerOrigin: config.apiServerOrigin(), // used by CaaS tool
provider: config.provider(),
cloudronName: cloudronName,
adminFqdn: config.fqdn() ? config.adminFqdn() : null,
webadminStatus: gWebadminStatus
});
});
});
}
function getDisks(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -424,208 +132,23 @@ function getDisks(callback) {
function getConfig(callback) {
assert.strictEqual(typeof callback, 'function');
// result to not depend on the appstore
const BOX_AND_USER_TEMPLATE = {
box: {
region: null,
size: null,
plan: 'Custom Plan'
},
user: {
billing: false,
currency: ''
}
};
caas.getBoxAndUserDetails(function (error, result) {
if (error) debug('Failed to fetch cloudron details.', error.reason, error.message);
result = _.extend(BOX_AND_USER_TEMPLATE, result || {});
settings.getCloudronName(function (error, cloudronName) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
callback(null, {
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
fqdn: config.fqdn(),
adminLocation: config.adminLocation(),
adminFqdn: config.adminFqdn(),
mailFqdn: config.mailFqdn(),
version: config.version(),
update: updateChecker.getUpdateInfo(),
progress: progress.getAll(),
isDemo: config.isDemo(),
region: result.box.region,
size: result.box.size,
billing: !!result.user.billing,
plan: result.box.plan,
currency: result.user.currency,
memory: os.totalmem(),
provider: config.provider(),
cloudronName: cloudronName
});
});
});
}
function ensureDkimKey(callback) {
assert(config.fqdn(), 'fqdn is not set');
var dkimPath = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn());
var dkimPrivateKeyFile = path.join(dkimPath, 'private');
var dkimPublicKeyFile = path.join(dkimPath, 'public');
if (!fs.existsSync(dkimPrivateKeyFile) || !fs.existsSync(dkimPublicKeyFile)) {
debug('Generating new DKIM keys');
if (!safe.fs.mkdirSync(dkimPath) && safe.error.code !== 'EEXIST') {
debug('Error creating dkim.', safe.error);
return null;
}
child_process.execSync('openssl genrsa -out ' + dkimPrivateKeyFile + ' 1024');
child_process.execSync('openssl rsa -in ' + dkimPrivateKeyFile + ' -out ' + dkimPublicKeyFile + ' -pubout -outform PEM');
} else {
debug('DKIM keys already present');
}
callback();
}
function readDkimPublicKeySync() {
if (!config.fqdn()) {
debug('Cannot read dkim public key without a domain.', safe.error);
return null;
}
var dkimPath = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn());
var dkimPublicKeyFile = path.join(dkimPath, 'public');
var publicKey = safe.fs.readFileSync(dkimPublicKeyFile, 'utf8');
if (publicKey === null) {
debug('Error reading dkim public key.', safe.error);
return null;
}
// remove header, footer and new lines
publicKey = publicKey.split('\n').slice(1, -2).join('');
return publicKey;
}
// NOTE: if you change the SPF record here, be sure the wait check in mailer.js
// https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
function txtRecordsWithSpf(callback) {
assert.strictEqual(typeof callback, 'function');
domains.getDNSRecords('', config.fqdn(), 'TXT', function (error, txtRecords) {
if (error) return callback(error);
debug('txtRecordsWithSpf: current txt records - %j', txtRecords);
var i, matches, validSpf;
for (i = 0; i < txtRecords.length; i++) {
matches = txtRecords[i].match(/^("?v=spf1) /); // DO backend may return without quotes
if (matches === null) continue;
// this won't work if the entry is arbitrarily "split" across quoted strings
validSpf = txtRecords[i].indexOf('a:' + config.adminFqdn()) !== -1;
break; // there can only be one SPF record
}
if (validSpf) return callback(null, null);
if (!matches) { // no spf record was found, create one
txtRecords.push('"v=spf1 a:' + config.adminFqdn() + ' ~all"');
debug('txtRecordsWithSpf: adding txt record');
} else { // just add ourself
txtRecords[i] = matches[1] + ' a:' + config.adminFqdn() + txtRecords[i].slice(matches[1].length);
debug('txtRecordsWithSpf: inserting txt record');
}
return callback(null, txtRecords);
});
}
function addDnsRecords(ip, callback) {
assert.strictEqual(typeof ip, 'string');
callback = callback || NOOP_CALLBACK;
if (process.env.BOX_ENV === 'test') return callback();
var dkimKey = readDkimPublicKeySync();
if (!dkimKey) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, new Error('Failed to read dkim public key')));
var webadminRecord = { subdomain: config.adminLocation(), domain: config.fqdn(), type: 'A', values: [ ip ] };
// t=s limits the domainkey to this domain and not it's subdomains
var dkimRecord = { subdomain: config.dkimSelector() + '._domainkey', domain: config.fqdn(), type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
var records = [ ];
records.push(webadminRecord);
records.push(dkimRecord);
debug('addDnsRecords: %j', records);
async.retry({ times: 10, interval: 20000 }, function (retryCallback) {
txtRecordsWithSpf(function (error, txtRecords) {
if (error) return retryCallback(error);
if (txtRecords) records.push({ subdomain: '', domain: config.fqdn(), type: 'TXT', values: txtRecords });
debug('addDnsRecords: will update %j', records);
async.mapSeries(records, function (record, iteratorCallback) {
domains.upsertDNSRecords(record.subdomain, record.domain, record.type, record.values, iteratorCallback);
}, function (error, changeIds) {
if (error) debug('addDnsRecords: failed to update : %s. will retry', error);
else debug('addDnsRecords: records %j added with changeIds %j', records, changeIds);
retryCallback(error);
});
});
}, function (error) {
if (error) debug('addDnsRecords: done updating records with error:', error);
else debug('addDnsRecords: done');
callback(error);
});
}
function restore(backupConfig, backupId, version, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof version, 'string');
assert.strictEqual(typeof callback, 'function');
if (!semver.valid(version)) return callback(new CloudronError(CloudronError.BAD_STATE, 'version is not a valid semver'));
if (semver.major(config.version()) !== semver.major(version) || semver.minor(config.version()) !== semver.minor(version)) return callback(new CloudronError(CloudronError.BAD_STATE, `Run cloudron-setup with --version ${version} to restore from this backup`));
user.count(function (error, count) {
settings.getAll(function (error, allSettings) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
if (count) return callback(new CloudronError(CloudronError.ALREADY_PROVISIONED, 'Already activated'));
backups.testConfig(backupConfig, function (error) {
if (error && error.reason === BackupsError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, error.message));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
debug(`restore: restoring from ${backupId} from provider ${backupConfig.provider}`);
gWebadminStatus.restoring = true;
callback(null); // do no block
async.series([
backups.restore.bind(null, backupConfig, backupId),
autoprovision,
shell.sudo.bind(null, 'restart', [ RESTART_CMD ])
], function (error) {
debug('restore:', error);
gWebadminStatus.restoring = false;
});
// be picky about what we send out here since this is sent for 'normal' users as well
callback(null, {
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
adminDomain: config.adminDomain(),
adminFqdn: config.adminFqdn(),
mailFqdn: config.mailFqdn(),
version: config.version(),
progress: progress.getAll(),
isDemo: config.isDemo(),
memory: os.totalmem(),
provider: config.provider(),
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
spaces: allSettings[settings.SPACES_CONFIG_KEY] // here because settings route cannot be accessed by spaces users
});
});
}
@@ -634,125 +157,6 @@ function reboot(callback) {
shell.sudo('reboot', [ REBOOT_CMD ], callback);
}
function update(boxUpdateInfo, auditSource, callback) {
assert.strictEqual(typeof boxUpdateInfo, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
if (!boxUpdateInfo) return callback(null);
var error = locker.lock(locker.OP_BOX_UPDATE);
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
eventlog.add(eventlog.ACTION_UPDATE, auditSource, { boxUpdateInfo: boxUpdateInfo });
// ensure tools can 'wait' on progress
progress.set(progress.UPDATE, 0, 'Starting');
// initiate the update/upgrade but do not wait for it
if (boxUpdateInfo.upgrade) {
debug('Starting upgrade');
caas.upgrade(boxUpdateInfo, function (error) {
if (error) {
debug('Upgrade failed with error:', error);
locker.unlock(locker.OP_BOX_UPDATE);
}
});
} else {
debug('Starting update');
doUpdate(boxUpdateInfo, function (error) {
if (error) {
debug('Update failed with error:', error);
locker.unlock(locker.OP_BOX_UPDATE);
}
});
}
callback(null);
}
function updateToLatest(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var boxUpdateInfo = updateChecker.getUpdateInfo().box;
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');
function updateError(e) {
progress.set(progress.UPDATE, -1, e.message);
callback(e);
}
progress.set(progress.UPDATE, 5, 'Backing up for update');
backups.backupBoxAndApps({ userId: null, username: 'updater' }, function (error) {
if (error) return updateError(error);
// NOTE: this data is opaque and will be passed through the installer.sh
var data= {
provider: config.provider(),
token: config.token(),
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
fqdn: config.fqdn(),
adminFqdn: config.adminFqdn(),
adminLocation: config.adminLocation(),
isDemo: config.isDemo(),
zoneName: config.zoneName(),
appstore: {
token: config.token(),
apiServerOrigin: config.apiServerOrigin()
},
caas: {
token: config.token(),
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin()
},
version: boxUpdateInfo.version
};
debug('updating box %s %j', boxUpdateInfo.sourceTarballUrl, _.omit(data, 'tlsCert', 'tlsKey', 'token', 'appstore', 'caas'));
progress.set(progress.UPDATE, 5, 'Downloading and extracting new version');
shell.sudo('update', [ UPDATE_CMD, boxUpdateInfo.sourceTarballUrl, JSON.stringify(data) ], function (error) {
if (error) return updateError(error);
// Do not add any code here. The installer script will stop the box code any instant
});
});
}
function checkDiskSpace(callback) {
callback = callback || NOOP_CALLBACK;
@@ -795,78 +199,66 @@ function checkDiskSpace(callback) {
});
}
// called for dynamic dns setups where we have to update the IP
function refreshDNS(callback) {
callback = callback || NOOP_CALLBACK;
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
debug('refreshDNS: current ip %s', ip);
addDnsRecords(ip, function (error) {
if (error) return callback(error);
debug('refreshDNS: done for system records');
apps.getAll(function (error, result) {
if (error) return callback(error);
async.each(result, function (app, callback) {
// do not change state of installing apps since apptask will error if dns record already exists
if (app.installationState !== appdb.ISTATE_INSTALLED) return callback();
domains.upsertDNSRecords(app.location, app.domain, 'A', [ ip ], callback);
}, function (error) {
if (error) return callback(error);
debug('refreshDNS: done for apps');
callback();
});
});
});
});
}
function getLogs(options, callback) {
function getLogs(unit, options, callback) {
assert.strictEqual(typeof unit, 'string');
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
var units = options.units || [],
lines = options.lines || 100,
var lines = options.lines || 100,
format = options.format || 'json',
follow = !!options.follow;
assert(Array.isArray(units));
assert.strictEqual(typeof lines, 'number');
assert.strictEqual(typeof format, 'string');
debug('Getting logs for %j', units);
assert.strictEqual(typeof lines, 'number');
assert.strictEqual(typeof format, 'string');
var args = [ '--no-pager', '--lines=' + lines ];
units.forEach(function (u) {
if (u === 'box') args.push('--unit=box');
else if (u === 'mail') args.push('CONTAINER_NAME=mail');
});
if (format === 'short') args.push('--output=short', '-a'); else args.push('--output=json');
if (follow) args.push('--follow');
debug('Getting logs for %s as %s', unit, format);
var cp = spawn('/bin/journalctl', args);
var cp, transformStream;
if (unit === 'box') {
let args = [ '--no-pager', `--lines=${lines}` ];
if (format === 'short') args.push('--output=short', '-a'); else args.push('--output=json');
if (follow) args.push('--follow');
args.push('--unit=box');
args.push('--unit=cloudron-updater');
cp = spawn('/bin/journalctl', args);
var transformStream = split(function mapper(line) {
if (format !== 'json') return line + '\n';
transformStream = split(function mapper(line) {
if (format !== 'json') return line + '\n';
var obj = safe.JSON.parse(line);
if (!obj) return undefined;
var obj = safe.JSON.parse(line);
if (!obj) return undefined;
return JSON.stringify({
realtimeTimestamp: obj.__REALTIME_TIMESTAMP,
monotonicTimestamp: obj.__MONOTONIC_TIMESTAMP,
message: obj.MESSAGE,
source: obj.SYSLOG_IDENTIFIER || ''
}) + '\n';
});
return JSON.stringify({
realtimeTimestamp: obj.__REALTIME_TIMESTAMP,
monotonicTimestamp: obj.__MONOTONIC_TIMESTAMP,
message: obj.MESSAGE,
source: obj.SYSLOG_IDENTIFIER || ''
}) + '\n';
});
} else { // mail, mongodb, mysql, postgresql, backup
let args = [ '--lines=' + lines ];
if (follow) args.push('--follow');
args.push(path.join(paths.LOG_DIR, unit, 'app.log'));
cp = spawn('/usr/bin/tail', args);
transformStream = split(function mapper(line) {
if (format !== 'json') return line + '\n';
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
var timestamp = (new Date(data[0])).getTime();
if (isNaN(timestamp)) timestamp = 0;
return JSON.stringify({
realtimeTimestamp: timestamp * 1000,
message: line.slice(data[0].length+1),
source: unit
}) + '\n';
});
}
transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process

View File

@@ -16,12 +16,11 @@ exports = module.exports = {
provider: provider,
apiServerOrigin: apiServerOrigin,
webServerOrigin: webServerOrigin,
fqdn: fqdn,
zoneName: zoneName,
setFqdn: setFqdn,
adminDomain: adminDomain,
setFqdn: setAdminDomain,
setAdminDomain: setAdminDomain,
setAdminFqdn: setAdminFqdn,
setAdminLocation: setAdminLocation,
token: token,
version: version,
setVersion: setVersion,
database: database,
@@ -34,7 +33,6 @@ exports = module.exports = {
adminFqdn: adminFqdn,
mailLocation: mailLocation,
mailFqdn: mailFqdn,
setZoneName: setZoneName,
hasIPv6: hasIPv6,
dkimSelector: dkimSelector,
@@ -48,7 +46,6 @@ var assert = require('assert'),
fs = require('fs'),
path = require('path'),
safe = require('safetydance'),
tld = require('tldjs'),
_ = require('underscore');
@@ -73,12 +70,10 @@ function saveSync() {
// only save values we want to have in the cloudron.conf, see start.sh
var conf = {
version: data.version,
token: data.token,
apiServerOrigin: data.apiServerOrigin,
webServerOrigin: data.webServerOrigin,
fqdn: data.fqdn,
adminDomain: data.adminDomain,
adminFqdn: data.adminFqdn,
zoneName: data.zoneName,
adminLocation: data.adminLocation,
provider: data.provider,
isDemo: data.isDemo
@@ -97,16 +92,14 @@ function _reset(callback) {
function initConfig() {
// setup defaults
data.fqdn = '';
data.adminFqdn = '';
data.zoneName = '';
data.adminDomain = '';
data.adminLocation = 'my';
data.port = 3000;
data.token = null;
data.version = null;
data.apiServerOrigin = null;
data.webServerOrigin = null;
data.provider = 'caas';
data.provider = 'generic';
data.smtpPort = 2525; // this value comes from mail container
data.sysadminPort = 3001;
data.ldapPort = 3002;
@@ -124,7 +117,6 @@ function initConfig() {
if (exports.TEST) {
data.version = '1.1.1-test';
data.port = 5454;
data.token = 'APPSTORE_TOKEN';
data.apiServerOrigin = 'http://localhost:6060'; // hock doesn't support https
data.database.password = '';
data.database.name = 'boxtest';
@@ -148,6 +140,7 @@ function set(key, value) {
} else {
data = safe.set(data, key, value);
}
saveSync();
}
@@ -165,24 +158,12 @@ function webServerOrigin() {
return get('webServerOrigin');
}
function setFqdn(fqdn) {
set('fqdn', fqdn);
function setAdminDomain(domain) {
set('adminDomain', domain);
}
function fqdn() {
return get('fqdn');
}
function setZoneName(zone) {
set('zoneName', zone);
}
function zoneName() {
var zone = get('zoneName');
if (zone) return zone;
// TODO: move this to migration code path instead
return tld.getDomain(fqdn()) || '';
function adminDomain() {
return get('adminDomain');
}
function mailLocation() {
@@ -222,10 +203,6 @@ function sysadminOrigin() {
return 'http://127.0.0.1:' + get('sysadminPort');
}
function token() {
return get('token');
}
function version() {
return get('version');
}
@@ -251,6 +228,8 @@ function hasIPv6() {
return fs.existsSync(IPV6_PROC_FILE);
}
// it has to change with the adminLocation so that multiple cloudrons
// can send out emails at the same time.
function dkimSelector() {
var loc = adminLocation();
return loc === 'my' ? 'cloudron' : `cloudron-${loc.replace(/\./g, '')}`;

View File

@@ -4,13 +4,14 @@ exports = module.exports = {
API_LOCATION: 'api', // this is unused but reserved for future use (#403)
SMTP_LOCATION: 'smtp',
IMAP_LOCATION: 'imap',
POSTMAN_LOCATION: 'postman', // used in dovecot bounces
// These are combined into one array because users and groups become mailboxes
RESERVED_NAMES: [
// Reserved usernames
// https://github.com/gogits/gogs/blob/52c8f691630548fe091d30bcfe8164545a05d3d5/models/repo.go#L393
'admin', 'no-reply', 'postmaster', 'mailer-daemon', // apps like wordpress, gogs don't like these
// apps like wordpress, gogs don't like these
// postmaster is used in dovecot and haraka
'admin', 'no-reply', 'postmaster', 'mailer-daemon',
// Reserved groups
'admins', 'users' // ldap code uses 'users' pseudo group
@@ -20,9 +21,6 @@ exports = module.exports = {
ADMIN_CLIENT_ID: 'webadmin', // oauth client id
ADMIN_GROUP_NAME: 'admin',
ADMIN_GROUP_ID: 'admin',
NGINX_ADMIN_CONFIG_FILE_NAME: 'admin.conf',
GHOST_USER_FILE: '/tmp/cloudron_ghost.json',

View File

@@ -10,23 +10,25 @@ var apps = require('./apps.js'),
assert = require('assert'),
backups = require('./backups.js'),
caas = require('./caas.js'),
certificates = require('./certificates.js'),
cloudron = require('./cloudron.js'),
config = require('./config.js'),
constants = require('./constants.js'),
CronJob = require('cron').CronJob,
debug = require('debug')('box:cron'),
digest = require('./digest.js'),
dyndns = require('./dyndns.js'),
eventlog = require('./eventlog.js'),
janitor = require('./janitor.js'),
reverseProxy = require('./reverseproxy.js'),
scheduler = require('./scheduler.js'),
settings = require('./settings.js'),
semver = require('semver'),
updater = require('./updater.js'),
updateChecker = require('./updatechecker.js');
var gJobs = {
alive: null, // send periodic stats
autoUpdater: null,
appAutoUpdater: null,
boxAutoUpdater: null,
appUpdateChecker: null,
backup: null,
boxUpdateChecker: null,
@@ -77,15 +79,17 @@ function initialize(callback) {
});
settings.events.on(settings.TIME_ZONE_KEY, recreateJobs);
settings.events.on(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
settings.events.on(settings.DYNAMIC_DNS_KEY, dynamicDNSChanged);
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]);
dynamicDNSChanged(allSettings[settings.DYNAMIC_DNS_KEY]);
appAutoupdatePatternChanged(allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY]);
boxAutoupdatePatternChanged(allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY]);
dynamicDnsChanged(allSettings[settings.DYNAMIC_DNS_KEY]);
callback();
});
@@ -174,7 +178,7 @@ function recreateJobs(tz) {
if (gJobs.certificateRenew) gJobs.certificateRenew.stop();
gJobs.certificateRenew = new CronJob({
cronTime: '00 00 */12 * * *', // every 12 hours
onTick: certificates.renewAll.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
onTick: reverseProxy.renewAll.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
start: true,
timeZone: tz
});
@@ -188,32 +192,25 @@ 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 app update to %j', updateInfo.apps);
apps.autoupdateApps(updateInfo.apps, AUDIT_SOURCE, NOOP_CALLBACK);
debug('Starting autoupdate to %j', updateInfo.box);
updater.updateToLatest(AUDIT_SOURCE, NOOP_CALLBACK);
} else {
debug('No auto updates available');
debug('No box auto updates available');
}
},
start: true,
@@ -221,7 +218,33 @@ function autoupdatePatternChanged(pattern) {
});
}
function dynamicDNSChanged(enabled) {
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 app auto updates available');
}
},
start: true,
timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack
});
}
function dynamicDnsChanged(enabled) {
assert.strictEqual(typeof enabled, 'boolean');
assert(gJobs.boxUpdateCheckerJob);
@@ -230,7 +253,7 @@ function dynamicDNSChanged(enabled) {
if (enabled) {
gJobs.dynamicDNS = new CronJob({
cronTime: '00 */10 * * * *',
onTick: cloudron.refreshDNS,
onTick: dyndns.sync,
start: true,
timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack
});
@@ -244,8 +267,9 @@ 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.DYNAMIC_DNS_KEY, dynamicDNSChanged);
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) {
if (!gJobs[job]) continue;

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,
@@ -103,9 +91,7 @@ function clear(callback) {
async.series([
child_process.exec.bind(null, cmd),
require('./clientdb.js')._addDefaultClients,
require('./domaindb.js')._addDefaultDomain,
require('./groupdb.js')._addDefaultGroups
require('./clientdb.js')._addDefaultClients
], callback);
}

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() });
});
}

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);
});
}

View File

@@ -28,44 +28,36 @@ function maybeSend(callback) {
var pendingAppUpdates = updateInfo.apps || {};
pendingAppUpdates = Object.keys(pendingAppUpdates).map(function (key) { return pendingAppUpdates[key]; });
appstore.getSubscription(function (error, result) {
if (error) debug('Error getting subscription:', error);
eventlog.getByCreationTime(new Date(new Date() - 7*86400000), function (error, events) {
if (error) return callback(error);
var hasSubscription = result && result.plan.id !== 'free' && result.plan.id !== 'undecided';
var appUpdates = events.filter(function (e) { return e.action === eventlog.ACTION_APP_UPDATE; }).map(function (e) { return e.data; });
var boxUpdates = events.filter(function (e) { return e.action === eventlog.ACTION_UPDATE; }).map(function (e) { return e.data; });
var certRenewals = events.filter(function (e) { return e.action === eventlog.ACTION_CERTIFICATE_RENEWAL; }).map(function (e) { return e.data; });
var usersAdded = events.filter(function (e) { return e.action === eventlog.ACTION_USER_ADD; }).map(function (e) { return e.data; });
var usersRemoved = events.filter(function (e) { return e.action === eventlog.ACTION_USER_REMOVE; }).map(function (e) { return e.data; });
var finishedBackups = events.filter(function (e) { return e.action === eventlog.ACTION_BACKUP_FINISH && !e.errorMessage; }).map(function (e) { return e.data; });
eventlog.getByCreationTime(new Date(new Date() - 7*86400000), function (error, events) {
if (error) return callback(error);
if (error) return callback(error);
var appUpdates = events.filter(function (e) { return e.action === eventlog.ACTION_APP_UPDATE; }).map(function (e) { return e.data; });
var boxUpdates = events.filter(function (e) { return e.action === eventlog.ACTION_UPDATE; }).map(function (e) { return e.data; });
var certRenewals = events.filter(function (e) { return e.action === eventlog.ACTION_CERTIFICATE_RENEWAL; }).map(function (e) { return e.data; });
var usersAdded = events.filter(function (e) { return e.action === eventlog.ACTION_USER_ADD; }).map(function (e) { return e.data; });
var usersRemoved = events.filter(function (e) { return e.action === eventlog.ACTION_USER_REMOVE; }).map(function (e) { return e.data; });
var finishedBackups = events.filter(function (e) { return e.action === eventlog.ACTION_BACKUP_FINISH && !e.errorMessage; }).map(function (e) { return e.data; });
var info = {
pendingAppUpdates: pendingAppUpdates,
pendingBoxUpdate: updateInfo.box || null,
if (error) return callback(error);
finishedAppUpdates: appUpdates,
finishedBoxUpdates: boxUpdates,
var info = {
hasSubscription: hasSubscription,
certRenewals: certRenewals,
finishedBackups: finishedBackups, // only the successful backups
usersAdded: usersAdded,
usersRemoved: usersRemoved // unused because we don't have username to work with
};
pendingAppUpdates: pendingAppUpdates,
pendingBoxUpdate: updateInfo.box || null,
// always send digest for backup failure notification
debug('maybeSend: sending digest email', info);
mailer.sendDigest(info);
finishedAppUpdates: appUpdates,
finishedBoxUpdates: boxUpdates,
certRenewals: certRenewals,
finishedBackups: finishedBackups, // only the successful backups
usersAdded: usersAdded,
usersRemoved: usersRemoved // unused because we don't have username to work with
};
// always send digest for backup failure notification
debug('maybeSend: sending digest email', info);
mailer.sendDigest(info);
callback();
});
callback();
});
});
}

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,12 +45,12 @@ 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);
return callback(null);
});
}
@@ -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);
});
@@ -126,10 +126,26 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
var credentials = {
token: dnsConfig.token,
fqdn: domain
};
return callback(null, credentials);
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(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
callback(null, credentials);
});
});
}

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) {
@@ -46,19 +46,19 @@ function getZoneByName(dnsConfig, zoneName, callback) {
assert.strictEqual(typeof callback, 'function');
superagent.get(CLOUDFLARE_ENDPOINT + '/zones?name=' + zoneName + '&status=active')
.set('X-Auth-Key', dnsConfig.token)
.set('X-Auth-Email', dnsConfig.email)
.timeout(30 * 1000)
.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)));
.set('X-Auth-Key', dnsConfig.token)
.set('X-Auth-Email', dnsConfig.email)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
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]);
});
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');
@@ -69,18 +69,18 @@ function getDNSRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, cal
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName;
superagent.get(CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records')
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.query({ type: type, name: fqdn })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.query({ type: type, name: fqdn })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
var tmp = result.body.result;
var tmp = result.body.result;
return callback(null, tmp);
});
return callback(null, tmp);
});
}
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
@@ -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;
@@ -126,37 +126,33 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
if (i >= dnsRecords.length) {
superagent.post(CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records')
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
callback(null);
});
callback(null);
});
} else {
superagent.put(CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records/' + dnsRecords[i].id)
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
// increment, as we have consumed the record
++i;
++i;
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
callback(null);
});
callback(null);
});
}
}, function (error) {
if (error) return callback(error);
callback(null, 'unused');
});
}, callback);
});
});
}
@@ -171,7 +167,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 +189,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);
@@ -206,17 +202,17 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
async.eachSeries(tmp, function (record, callback) {
superagent.del(CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records/' + record.id)
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 204 || result.body.success !== true) return translateRequestError(result, callback);
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
debug('del: done');
debug('del: done');
callback(null);
});
callback(null);
});
}, function (error) {
if (error) return callback(error);
@@ -233,8 +229,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 +239,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';

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,21 +119,17 @@ 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'));
return iteratorCallback(null);
});
}
}, function (error) {
if (error) return callback(error);
callback(null, '' + recordIds[0]); // DO ids are integers
});
}, callback);
});
}
@@ -185,10 +181,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');
@@ -204,19 +200,21 @@ 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 DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
var credentials = {
token: dnsConfig.token
};
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';

148
src/dns/gandi.js Normal file
View File

@@ -0,0 +1,148 @@
'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);
});
}
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');
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
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);
});
});
});
}

View File

@@ -9,10 +9,9 @@ exports = module.exports = {
};
var assert = require('assert'),
config = require('../config.js'),
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');
@@ -20,19 +19,13 @@ var assert = require('assert'),
function getDnsCredentials(dnsConfig) {
assert.strictEqual(typeof dnsConfig, 'object');
var config = {
return {
projectId: dnsConfig.projectId,
keyFilename: dnsConfig.keyFilename,
email: dnsConfig.email
};
if (dnsConfig.credentials) {
config.credentials = {
credentials: {
client_email: dnsConfig.credentials.client_email,
private_key: dnsConfig.credentials.private_key
};
}
return config;
}
};
}
function getZoneByName(dnsConfig, zoneName, callback) {
@@ -43,20 +36,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:[]}
});
@@ -78,10 +71,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, {
@@ -91,14 +84,14 @@ 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);
callback(null);
});
});
});
@@ -120,8 +113,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);
@@ -143,18 +136,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);
@@ -170,20 +163,25 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
if (typeof dnsConfig.projectId !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'projectId must be a string'));
if (!dnsConfig.credentials || typeof dnsConfig.credentials !== 'object') return callback(new DomainsError(DomainsError.BAD_FIELD, 'credentials must be an object'));
if (typeof dnsConfig.credentials.client_email !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'credentials.client_email must be a string'));
if (typeof dnsConfig.credentials.private_key !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'credentials.private_key must be a string'));
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';

184
src/dns/godaddy.js Normal file
View File

@@ -0,0 +1,184 @@
'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);
});
}
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');
if (!dnsConfig.apiKey || typeof dnsConfig.apiKey !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'apiKey must be a non-empty string'));
if (!dnsConfig.apiSecret || typeof dnsConfig.apiSecret !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'apiSecret must be a non-empty string'));
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);
});
});
});
}

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) {
@@ -26,7 +26,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
// Result: backend specific change id, to be passed into getChangeStatus()
// Result: none
callback(new Error('not implemented'));
}

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) {
@@ -24,7 +24,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
return callback(null, 'noop-record-id');
return callback(null);
}
function get(dnsConfig, zoneName, subdomain, type, 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
src/dns/namecom.js Normal file
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);
});
}
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);
});
});
});
}

View File

@@ -22,7 +22,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
return callback(null, 'noop-record-id');
return callback(null);
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
@@ -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');

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,13 +115,13 @@ 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);
callback(null);
});
});
}
@@ -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);
@@ -225,25 +233,29 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
if (!dnsConfig.accessKeyId || typeof dnsConfig.accessKeyId !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'accessKeyId must be a non-empty string'));
if (!dnsConfig.secretAccessKey || typeof dnsConfig.secretAccessKey !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'secretAccessKey must be a non-empty string'));
var credentials = {
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 +265,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');

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);
});

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,12 +173,20 @@ function createSubcontainer(app, name, cmd, options, callback) {
'/run': {}
},
Labels: {
'fqdn': app.intrinsicFqdn,
'fqdn': app.fqdn,
'appId': app.id,
'isSubcontainer': String(!isAppContainer)
},
HostConfig: {
Binds: addons.getBindsSync(app, app.manifest.addons),
LogConfig: {
Type: 'syslog',
Config: {
'tag': app.id,
'syslog-address': 'udp://127.0.0.1:2514', // see apps.js:validatePortBindings()
'syslog-format': 'rfc5424'
}
},
Memory: memoryLimit / 2,
MemorySwap: memoryLimit, // Memory + Swap
PortBindings: isAppContainer ? dockerPortBindings : { },

View File

@@ -7,22 +7,23 @@ exports = module.exports = {
get: get,
getAll: getAll,
update: update,
upsert: upsert,
del: del,
_clear: clear,
_addDefaultDomain: addDefaultDomain
_clear: clear
};
var assert = require('assert'),
database = require('./database.js'),
DatabaseError = require('./databaseerror'),
config = require('./config.js'),
safe = require('safetydance');
var DOMAINS_FIELDS = [ 'domain', 'zoneName', 'provider', 'configJson', 'tlsConfigJson' ].join(',');
function postProcess(data) {
data.config = safe.JSON.parse(data.configJson);
data.tlsConfig = safe.JSON.parse(data.tlsConfigJson);
delete data.configJson;
delete data.tlsConfigJson;
return data;
}
@@ -31,7 +32,7 @@ function get(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT * FROM domains WHERE domain=?', [ domain ], function (error, result) {
database.query(`SELECT ${DOMAINS_FIELDS} FROM domains WHERE domain=?`, [ domain ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
@@ -42,7 +43,7 @@ function get(domain, callback) {
}
function getAll(callback) {
database.query('SELECT * FROM domains ORDER BY domain', function (error, results) {
database.query(`SELECT ${DOMAINS_FIELDS} FROM domains ORDER BY domain`, function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(postProcess);
@@ -51,14 +52,16 @@ function getAll(callback) {
});
}
function add(domain, zoneName, provider, config, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof config, 'object');
function add(name, domain, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'object');
assert.strictEqual(typeof domain.zoneName, 'string');
assert.strictEqual(typeof domain.provider, 'string');
assert.strictEqual(typeof domain.config, 'object');
assert.strictEqual(typeof domain.tlsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO domains (domain, zoneName, provider, configJson) VALUES (?, ?, ?, ?)', [ domain, zoneName, provider, JSON.stringify(config) ], function (error) {
database.query('INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson) VALUES (?, ?, ?, ?, ?)', [ name, domain.zoneName, domain.provider, JSON.stringify(domain.config), JSON.stringify(domain.tlsConfig) ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -66,27 +69,27 @@ function add(domain, zoneName, provider, config, callback) {
});
}
function upsert(domain, zoneName, provider, config, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof config, 'object');
function update(name, domain, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'object');
assert.strictEqual(typeof callback, 'function');
database.query('REPLACE INTO domains (domain, zoneName, provider, configJson) VALUES (?, ?, ?, ?)', [ domain, zoneName, provider, JSON.stringify(config) ], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
var args = [ ], fields = [ ];
for (var k in domain) {
if (k === 'config') {
fields.push('configJson = ?');
args.push(JSON.stringify(domain[k]));
} else if (k === 'tlsConfig') {
fields.push('tlsConfigJson = ?');
args.push(JSON.stringify(domain[k]));
} else {
fields.push(k + ' = ?');
args.push(domain[k]);
}
}
args.push(name);
callback(null);
});
}
function update(domain, provider, config, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof callback, 'function');
database.query('UPDATE domains SET provider=?, configJson=? WHERE domain=?', [ provider, JSON.stringify(config), domain ], function (error) {
database.query('UPDATE domains SET ' + fields.join(', ') + ' WHERE domain=?', args, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -101,7 +104,7 @@ function del(domain, callback) {
database.query('DELETE FROM domains WHERE domain=?', [ domain ], function (error, result) {
if (error && error.code === 'ER_ROW_IS_REFERENCED_2') return callback(new DatabaseError(DatabaseError.IN_USE));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null);
});
@@ -114,12 +117,3 @@ function clear(callback) {
callback(error);
});
}
function addDefaultDomain(callback) {
assert(config.fqdn(), 'no fqdn set in config, cannot continue');
add(config.fqdn(), config.zoneName(), 'manual', { }, function (error) {
if (error && error.reason !== DatabaseError.ALREADY_EXISTS) return callback(error);
callback();
});
}

View File

@@ -10,33 +10,38 @@ 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,
removeRestrictedFields: removeRestrictedFields,
DomainsError: DomainsError
};
var assert = require('assert'),
caas = require('./caas.js'),
config = require('./config.js'),
certificates = require('./certificates.js'),
CertificatesError = certificates.CertificatesError,
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:domains'),
domaindb = require('./domaindb.js'),
path = require('path'),
reverseProxy = require('./reverseproxy.js'),
ReverseProxyError = reverseProxy.ReverseProxyError,
safe = require('safetydance'),
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');
@@ -54,17 +59,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) {
@@ -76,13 +81,15 @@ 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;
}
}
// TODO make it return a DomainError instead of DomainError
function verifyDnsConfig(config, domain, zoneName, provider, ip, callback) {
assert(config && typeof config === 'object'); // the dns config to test with
assert.strictEqual(typeof domain, 'string');
@@ -92,48 +99,58 @@ 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);
}
function add(domain, zoneName, provider, config, fallbackCertificate, callback) {
function add(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');
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(zoneName)) return callback(new DomainError(DomainError.BAD_FIELD, 'Invalid zoneName'));
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 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 = certificates.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 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, provider, result, 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));
domaindb.add(domain, { zoneName: zoneName, provider: provider, config: result, tlsConfig: tlsConfig }, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new DomainsError(DomainsError.ALREADY_EXISTS));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
if (!fallbackCertificate) return callback();
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
// cert validation already happened above no need to check all errors again
certificates.setFallbackCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain, function (error) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
callback();
});
});
@@ -147,13 +164,18 @@ 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));
certificates.getFallbackCertificate(domain, function (error, fallbackCertificate) {
if (error && error.reason !== CertificatesError.NOT_FOUND) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
reverseProxy.getFallbackCertificate(domain, function (error, bundle) {
if (error && error.reason !== ReverseProxyError.NOT_FOUND) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
if (fallbackCertificate) result.fallbackCertificate = fallbackCertificate;
var cert = safe.fs.readFileSync(bundle.certFilePath, 'utf-8');
var key = safe.fs.readFileSync(bundle.keyFilePath, 'utf-8');
if (!cert || !key) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, 'unable to read certificates from disk'));
result.fallbackCertificate = { cert: cert, key: key };
return callback(null, result);
});
@@ -164,48 +186,60 @@ 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, 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');
assert.strictEqual(typeof tlsConfig, 'object');
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 = certificates.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 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, result, 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();
// cert validation already happened above no need to check all errors again
certificates.setFallbackCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain, function (error) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
callback();
});
});
@@ -219,24 +253,35 @@ 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);
});
}
function getDNSRecords(subdomain, domain, type, callback) {
function getName(domain, subdomain) {
// support special caas domains
if (domain.provider === 'caas') return subdomain;
if (domain.domain === domain.zoneName) return subdomain;
var part = domain.domain.slice(0, -domain.zoneName.length - 1);
return subdomain === '' ? part : subdomain + '.' + part;
}
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, subdomain, type, function (error, values) {
api(result.provider).get(result.config, result.zoneName, getName(result, subdomain), type, function (error, values) {
if (error) return callback(error);
callback(null, values);
@@ -244,7 +289,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');
@@ -254,17 +299,17 @@ 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, subdomain, type, values, function (error, changeId) {
api(result.provider).upsert(result.config, result.zoneName, getName(result, subdomain), type, values, function (error) {
if (error) return callback(error);
callback(null, changeId);
callback(null);
});
});
}
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');
@@ -276,30 +321,26 @@ function removeDNSRecords(subdomain, domain, type, values, callback) {
get(domain, function (error, result) {
if (error) return callback(error);
api(result.provider).del(result.config, result.zoneName, subdomain, type, values, function (error) {
if (error && error.reason !== DomainError.NOT_FOUND) return callback(error);
api(result.provider).del(result.config, result.zoneName, getName(result, subdomain), type, values, function (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);
});
}
@@ -315,12 +356,11 @@ 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.setFqdn(result.domain);
config.setAdminDomain(result.domain);
config.setAdminLocation('my');
config.setAdminFqdn('my' + (result.provider === 'caas' ? '-' : '.') + result.domain);
config.setZoneName(result.zoneName);
callback();
@@ -333,3 +373,15 @@ function fqdn(location, domain, provider) {
return location + (location ? (provider !== 'caas' ? '.' : '-') : '') + domain;
}
// removes all fields that are strictly private and should never be returned by API calls
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;
}
// removes all fields that are not accessible by a normal user
function removeRestrictedFields(domain) {
var result = _.pick(domain, 'domain', 'zoneName', 'provider');
return result;
}

49
src/dyndns.js Normal file
View File

@@ -0,0 +1,49 @@
'use strict';
exports = module.exports = {
sync: sync
};
var appdb = require('./appdb.js'),
apps = require('./apps.js'),
async = require('async'),
config = require('./config.js'),
debug = require('debug')('box:dyndns'),
domains = require('./domains.js'),
sysinfo = require('./sysinfo.js');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
// called for dynamic dns setups where we have to update the IP
function sync(callback) {
callback = callback || NOOP_CALLBACK;
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
debug('refreshDNS: current ip %s', ip);
domains.upsertDnsRecords(config.adminLocation(), config.adminDomain(), 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('refreshDNS: done for admin location');
apps.getAll(function (error, result) {
if (error) return callback(error);
async.each(result, function (app, callback) {
// do not change state of installing apps since apptask will error if dns record already exists
if (app.installationState !== appdb.ISTATE_INSTALLED) return callback();
domains.upsertDnsRecords(app.location, app.domain, 'A', [ ip ], callback);
}, function (error) {
if (error) return callback(error);
debug('refreshDNS: done for apps');
callback();
});
});
});
});
}

View File

@@ -1,398 +0,0 @@
'use strict';
exports = module.exports = {
verifyRelay: verifyRelay,
getStatus: getStatus,
checkRblStatus: checkRblStatus,
EmailError: EmailError
};
var assert = require('assert'),
async = require('async'),
cloudron = require('./cloudron.js'),
config = require('./config.js'),
debug = require('debug')('box:email'),
dig = require('./dig.js'),
net = require('net'),
nodemailer = require('nodemailer'),
safe = require('safetydance'),
settings = require('./settings.js'),
smtpTransport = require('nodemailer-smtp-transport'),
sysinfo = require('./sysinfo.js'),
util = require('util'),
_ = require('underscore');
const digOptions = { server: '127.0.0.1', port: 53, timeout: 5000 };
function EmailError(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(EmailError, Error);
EmailError.INTERNAL_ERROR = 'Internal Error';
EmailError.BAD_FIELD = 'Bad Field';
function checkOutboundPort25(callback) {
assert.strictEqual(typeof callback, 'function');
var smtpServer = _.sample([
'smtp.gmail.com',
'smtp.live.com',
'smtp.mail.yahoo.com',
'smtp.o2.ie',
'smtp.comcast.net',
'outgoing.verizon.net'
]);
var relay = {
value: 'OK',
status: false
};
var client = new net.Socket();
client.setTimeout(5000);
client.connect(25, smtpServer);
client.on('connect', function () {
relay.status = true;
relay.value = 'OK';
client.destroy(); // do not use end() because it still triggers timeout
callback(null, relay);
});
client.on('timeout', function () {
relay.status = false;
relay.value = 'Connect to ' + smtpServer + ' timed out';
client.destroy();
callback(new Error('Timeout'), relay);
});
client.on('error', function (error) {
relay.status = false;
relay.value = 'Connect to ' + smtpServer + ' failed: ' + error.message;
client.destroy();
callback(error, relay);
});
}
function checkSmtpRelay(relay, callback) {
var result = {
value: 'OK',
status: false
};
var transporter = nodemailer.createTransport(smtpTransport({
host: relay.host,
port: relay.port,
auth: {
user: relay.username,
pass: relay.password
}
}));
transporter.verify(function(error) {
result.status = !error;
if (error) {
result.value = error.message;
return callback(error, result);
}
callback(null, result);
});
}
function verifyRelay(relay, callback) {
assert.strictEqual(typeof relay, 'object');
assert.strictEqual(typeof callback, 'function');
var verifier = relay.provider === 'cloudron-smtp' ? checkOutboundPort25 : checkSmtpRelay.bind(null, relay);
verifier(function (error) {
if (error) return callback(new EmailError(EmailError.BAD_FIELD, error.message));
callback();
});
}
function checkDkim(callback) {
var dkim = {
domain: config.dkimSelector() + '._domainkey.' + config.fqdn(),
type: 'TXT',
expected: null,
value: null,
status: false
};
var dkimKey = cloudron.readDkimPublicKeySync();
if (!dkimKey) return callback(new Error('Failed to read dkim public key'), dkim);
dkim.expected = '"v=DKIM1; t=s; p=' + dkimKey + '"';
dig.resolve(dkim.domain, dkim.type, digOptions, function (error, txtRecords) {
if (error && error.code === 'ENOTFOUND') return callback(null, dkim); // not setup
if (error) return callback(error, dkim);
if (Array.isArray(txtRecords) && txtRecords.length !== 0) {
dkim.value = txtRecords[0];
dkim.status = (dkim.value === dkim.expected);
}
callback(null, dkim);
});
}
function checkSpf(callback) {
var spf = {
domain: config.fqdn(),
type: 'TXT',
value: null,
expected: '"v=spf1 a:' + config.adminFqdn() + ' ~all"',
status: false
};
// https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
dig.resolve(spf.domain, spf.type, digOptions, function (error, txtRecords) {
if (error && error.code === 'ENOTFOUND') return callback(null, spf); // not setup
if (error) return callback(error, spf);
if (!Array.isArray(txtRecords)) return callback(null, spf);
var i;
for (i = 0; i < txtRecords.length; i++) {
if (txtRecords[i].indexOf('"v=spf1 ') !== 0) continue; // not SPF
spf.value = txtRecords[i];
spf.status = spf.value.indexOf(' a:' + config.adminFqdn()) !== -1;
break;
}
if (spf.status) {
spf.expected = spf.value;
} else if (i !== txtRecords.length) {
spf.expected = '"v=spf1 a:' + config.adminFqdn() + ' ' + spf.value.slice('"v=spf1 '.length);
}
callback(null, spf);
});
}
function checkMx(callback) {
var mx = {
domain: config.fqdn(),
type: 'MX',
value: null,
expected: '10 ' + config.mailFqdn() + '.',
status: false
};
dig.resolve(mx.domain, mx.type, digOptions, function (error, mxRecords) {
if (error && error.code === 'ENOTFOUND') return callback(null, mx); // not setup
if (error) return callback(error, mx);
if (Array.isArray(mxRecords) && mxRecords.length !== 0) {
mx.status = mxRecords.length == 1 && mxRecords[0].exchange === (config.mailFqdn() + '.');
mx.value = mxRecords.map(function (r) { return r.priority + ' ' + r.exchange; }).join(' ');
}
callback(null, mx);
});
}
function checkDmarc(callback) {
var dmarc = {
domain: '_dmarc.' + config.fqdn(),
type: 'TXT',
value: null,
expected: '"v=DMARC1; p=reject; pct=100"',
status: false
};
dig.resolve(dmarc.domain, dmarc.type, digOptions, function (error, txtRecords) {
if (error && error.code === 'ENOTFOUND') return callback(null, dmarc); // not setup
if (error) return callback(error, dmarc);
if (Array.isArray(txtRecords) && txtRecords.length !== 0) {
dmarc.value = txtRecords[0];
dmarc.status = (dmarc.value === dmarc.expected);
}
callback(null, dmarc);
});
}
function checkPtr(callback) {
var ptr = {
domain: null,
type: 'PTR',
value: null,
expected: config.mailFqdn() + '.',
status: false
};
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error, ptr);
ptr.domain = ip.split('.').reverse().join('.') + '.in-addr.arpa';
dig.resolve(ip, 'PTR', digOptions, function (error, ptrRecords) {
if (error && error.code === 'ENOTFOUND') return callback(null, ptr); // not setup
if (error) return callback(error, ptr);
if (Array.isArray(ptrRecords) && ptrRecords.length !== 0) {
ptr.value = ptrRecords.join(' ');
ptr.status = ptrRecords.some(function (v) { return v === ptr.expected; });
}
return callback(null, ptr);
});
});
}
// https://raw.githubusercontent.com/jawsome/node-dnsbl/master/list.json
const RBL_LIST = [
{
'name': 'Barracuda',
'dns': 'b.barracudacentral.org',
'site': 'http://www.barracudacentral.org/rbl/removal-request'
},
{
'name': 'SpamCop',
'dns': 'bl.spamcop.net',
'site': 'http://spamcop.net'
},
{
'name': 'Sorbs Aggregate Zone',
'dns': 'dnsbl.sorbs.net',
'site': 'http://dnsbl.sorbs.net/'
},
{
'name': 'Sorbs spam.dnsbl Zone',
'dns': 'spam.dnsbl.sorbs.net',
'site': 'http://sorbs.net'
},
{
'name': 'Composite Blocking List',
'dns': 'cbl.abuseat.org',
'site': 'http://www.abuseat.org'
},
{
'name': 'SpamHaus Zen',
'dns': 'zen.spamhaus.org',
'site': 'http://spamhaus.org'
},
{
'name': 'Multi SURBL',
'dns': 'multi.surbl.org',
'site': 'http://www.surbl.org'
},
{
'name': 'Spam Cannibal',
'dns': 'bl.spamcannibal.org',
'site': 'http://www.spamcannibal.org/cannibal.cgi'
},
{
'name': 'dnsbl.abuse.ch',
'dns': 'spam.abuse.ch',
'site': 'http://dnsbl.abuse.ch/'
},
{
'name': 'The Unsubscribe Blacklist(UBL)',
'dns': 'ubl.unsubscore.com ',
'site': 'http://www.lashback.com/blacklist/'
},
{
'name': 'UCEPROTECT Network',
'dns': 'dnsbl-1.uceprotect.net',
'site': 'http://www.uceprotect.net/en'
}
];
function checkRblStatus(callback) {
assert.strictEqual(typeof callback, 'function');
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error, ip);
var flippedIp = ip.split('.').reverse().join('.');
// https://tools.ietf.org/html/rfc5782
async.map(RBL_LIST, function (rblServer, iteratorDone) {
dig.resolve(flippedIp + '.' + rblServer.dns, 'A', digOptions, function (error, records) {
if (error || !records) return iteratorDone(null, null); // not listed
debug('checkRblStatus: %s (ip: %s) is in the blacklist of %j', config.fqdn(), flippedIp, rblServer);
var result = _.extend({ }, rblServer);
dig.resolve(flippedIp + '.' + rblServer.dns, 'TXT', digOptions, function (error, txtRecords) {
result.txtRecords = error || !txtRecords ? 'No txt record' : txtRecords;
debug('checkRblStatus: %s (error: %s) (txtRecords: %j)', config.fqdn(), error, txtRecords);
return iteratorDone(null, result);
});
});
}, function (ignoredError, blacklistedServers) {
blacklistedServers = blacklistedServers.filter(function(b) { return b !== null; });
debug('checkRblStatus: %s (ip: %s) servers: %j', config.fqdn(), ip, blacklistedServers);
return callback(null, { status: blacklistedServers.length === 0, ip: ip, servers: blacklistedServers });
});
});
}
function getStatus(callback) {
assert.strictEqual(typeof callback, 'function');
var results = {};
function recordResult(what, func) {
return function (callback) {
func(function (error, result) {
if (error) debug('Ignored error - ' + what + ':', error);
safe.set(results, what, result);
callback();
});
};
}
settings.getMailRelay(function (error, relay) {
if (error) return callback(error);
var checks = [
recordResult('dns.mx', checkMx),
recordResult('dns.dmarc', checkDmarc)
];
if (relay.provider === 'cloudron-smtp') {
// these tests currently only make sense when using Cloudron's SMTP server at this point
checks.push(
recordResult('dns.spf', checkSpf),
recordResult('dns.dkim', checkDkim),
recordResult('dns.ptr', checkPtr),
recordResult('relay', checkOutboundPort25),
recordResult('rbl', checkRblStatus)
);
} else {
checks.push(recordResult('relay', checkSmtpRelay.bind(null, relay)));
}
async.parallel(checks, function () {
callback(null, results);
});
});
}

View File

@@ -22,13 +22,13 @@ 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',
ACTION_USER_LOGIN: 'user.login',
ACTION_USER_REMOVE: 'user.remove',
ACTION_USER_UPDATE: 'user.update'
ACTION_USER_UPDATE: 'user.update',
ACTION_USER_TRANSFER: 'user.transfer',
};
var assert = require('assert'),
@@ -91,14 +91,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 +122,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);

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);

View File

@@ -6,6 +6,7 @@ exports = module.exports = {
getAll: getAll,
getAllWithMembers: getAllWithMembers,
add: add,
update: update,
del: del,
count: count,
@@ -15,19 +16,17 @@ exports = module.exports = {
setMembers: setMembers,
isMember: isMember,
getGroups: getGroups,
setGroups: setGroups,
getMembership: getMembership,
setMembership: setMembership,
_clear: clear,
_addDefaultGroups: addDefaultGroups
getGroups: getGroups,
_clear: clear
};
var assert = require('assert'),
constants = require('./constants.js'),
config = require('./config.js'),
database = require('./database.js'),
DatabaseError = require('./databaseerror'),
mailboxdb = require('./mailboxdb.js');
DatabaseError = require('./databaseerror');
var GROUPS_FIELDS = [ 'id', 'name' ].join(',');
@@ -64,10 +63,10 @@ function getWithMembers(groupId, callback) {
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ' FROM groups', function (error, result) {
database.query('SELECT ' + GROUPS_FIELDS + ' FROM groups', function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, result);
callback(null, results);
});
}
@@ -89,18 +88,39 @@ function add(id, name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
var queries = [];
queries.push({ query: 'INSERT INTO mailboxes (name, domain, ownerId, ownerType) VALUES (?, ?, ?, ?)', args: [ name, config.fqdn(), id, mailboxdb.TYPE_GROUP ] });
queries.push({ query: 'INSERT INTO groups (id, name) VALUES (?, ?)', args: [ id, name ] });
database.transaction(queries, function (error, result) {
database.query('INSERT INTO groups (id, name) VALUES (?, ?)', [ id, name ], function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
if (error || result[1].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
function update(id, data, callback) {
assert.strictEqual(typeof id, 'string');
assert(data && typeof data === 'object');
assert.strictEqual(typeof callback, 'function');
var args = [ ];
var fields = [ ];
for (var k in data) {
if (k === 'name') {
assert.strictEqual(typeof data.name, 'string');
fields.push(k + ' = ?');
args.push(data.name);
}
}
args.push(id);
database.query('UPDATE groups SET ' + fields.join(', ') + ' WHERE id = ?', args, function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('groups_name') !== -1) return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'name already exists'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
return callback(null);
});
}
function del(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -109,7 +129,6 @@ function del(id, callback) {
var queries = [];
queries.push({ query: 'DELETE FROM groupMembers WHERE groupId = ?', args: [ id ] });
queries.push({ query: 'DELETE FROM groups WHERE id = ?', args: [ id ] });
queries.push({ query: 'DELETE FROM mailboxes WHERE ownerId=?', args: [ id ] });
database.transaction(queries, function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -133,7 +152,7 @@ function clear(callback) {
database.query('DELETE FROM groupMembers', function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
database.query('DELETE FROM groups WHERE id != ?', [ 'admin' ], function (error) {
database.query('DELETE FROM groups', function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(error);
@@ -172,7 +191,7 @@ function setMembers(groupId, userIds, callback) {
});
}
function getGroups(userId, callback) {
function getMembership(userId, callback) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -184,7 +203,7 @@ function getGroups(userId, callback) {
});
}
function setGroups(userId, groupIds, callback) {
function setMembership(userId, groupIds, callback) {
assert.strictEqual(typeof userId, 'string');
assert(Array.isArray(groupIds));
assert.strictEqual(typeof callback, 'function');
@@ -242,6 +261,14 @@ function isMember(groupId, userId, callback) {
});
}
function addDefaultGroups(callback) {
add(constants.ADMIN_GROUP_ID, 'admin', callback);
function getGroups(userId, callback) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ' ' +
' FROM groups INNER JOIN groupMembers ON groups.id = groupMembers.groupId AND groupMembers.userId = ?', [ userId ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, results);
});
}

View File

@@ -1,11 +1,12 @@
'use strict';
exports = module.exports = {
GroupError: GroupError,
GroupsError: GroupsError,
create: create,
remove: remove,
get: get,
update: update,
getWithMembers: getWithMembers,
getAll: getAll,
getAllWithMembers: getAllWithMembers,
@@ -17,7 +18,9 @@ exports = module.exports = {
isMember: isMember,
getGroups: getGroups,
setGroups: setGroups
setMembership: setMembership,
getMembership: getMembership
};
var assert = require('assert'),
@@ -25,11 +28,12 @@ var assert = require('assert'),
DatabaseError = require('./databaseerror.js'),
groupdb = require('./groupdb.js'),
util = require('util'),
uuid = require('uuid');
uuid = require('uuid'),
_ = require('underscore');
// 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');
@@ -47,28 +51,25 @@ 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');
// 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');
// need to consider valid LDAP characters here (e.g '+' is reserved)
if (/[^a-zA-Z0-9.-]/.test(name)) return new GroupsError(GroupsError.BAD_FIELD, 'name can only contain alphanumerals, hyphen and dot');
return null;
}
@@ -85,8 +86,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 });
});
@@ -96,12 +97,9 @@ function remove(id, callback) {
assert.strictEqual(typeof id, 'string');
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));
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));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupsError(GroupsError.NOT_FOUND));
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
callback(null);
});
@@ -112,8 +110,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);
});
@@ -124,8 +122,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);
});
@@ -135,7 +133,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);
});
@@ -145,7 +143,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);
});
@@ -156,33 +154,33 @@ 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);
});
}
function getGroups(userId, callback) {
function getMembership(userId, callback) {
assert.strictEqual(typeof userId, 'string');
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));
groupdb.getMembership(userId, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupsError(GroupsError.NOT_FOUND));
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
return callback(null, result);
});
}
function setGroups(userId, groupIds, callback) {
function setMembership(userId, groupIds, callback) {
assert.strictEqual(typeof userId, 'string');
assert(Array.isArray(groupIds));
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));
groupdb.setMembership(userId, groupIds, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupsError(GroupsError.NOT_FOUND));
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
return callback(null);
});
@@ -194,8 +192,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);
});
@@ -207,8 +205,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);
});
@@ -220,8 +218,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);
});
@@ -233,9 +231,40 @@ 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);
});
}
function update(groupId, data, callback) {
assert.strictEqual(typeof groupId, 'string');
assert(data && typeof data === 'object');
assert.strictEqual(typeof callback, 'function');
let error;
if ('name' in data) {
assert.strictEqual(typeof data.name, 'string');
error = validateGroupname(data.name);
if (error) return callback(error);
}
groupdb.update(groupId, _.pick(data, 'name'), function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupsError(GroupsError.NOT_FOUND));
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
callback(null);
});
}
function getGroups(userId, callback) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
groupdb.getGroups(userId, function (error, results) {
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
callback(null, results);
});
}

9
src/hat.js Normal file
View File

@@ -0,0 +1,9 @@
'use strict';
exports = module.exports = hat;
var crypto = require('crypto');
function hat (bits) {
return crypto.randomBytes(bits / 8).toString('hex');
}

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.8.0',
'version': '48.11.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:0.18.0' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:0.17.1' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:0.13.0' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:0.11.0' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.40.0' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:0.12.0' }
'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.4.0' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:1.0.0' }
}
};

View File

@@ -10,12 +10,15 @@ var assert = require('assert'),
apps = require('./apps.js'),
async = require('async'),
config = require('./config.js'),
constants = require('./constants.js'),
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,
mailboxdb = require('./mailboxdb.js'),
safe = require('safetydance');
@@ -49,13 +52,13 @@ 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) {
async.filter(result, apps.hasAccessTo.bind(null, app), function (error, allowedUsers) {
if (error) return callback(new ldap.OperationsError(error.toString()));
callback(null, result);
callback(null, allowedUsers);
});
});
});
@@ -152,7 +155,7 @@ function userSearch(req, res, next) {
cn: entry.id,
uid: entry.id,
mail: entry.email,
mailAlternateAddress: entry.alternateEmail,
mailAlternateAddress: entry.fallbackEmail,
displayname: displayName,
givenName: firstName,
username: entry.username,
@@ -256,41 +259,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 name = req.dn.rdns[0].attrs.cn.value.toLowerCase();
// allow login via email
var parts = name.split('@');
if (parts[1] === config.fqdn()) {
name = parts[0];
}
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(name, config.fqdn(), 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,
uid: mailbox.name,
mail: mailbox.name + '@' + config.fqdn(),
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) {
@@ -298,19 +343,24 @@ function mailAliasSearch(req, res, next) {
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
mailboxdb.getAlias(req.dn.rdns[0].attrs.cn.value.toLowerCase(), config.fqdn(), function (error, alias) {
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.getAlias(parts[0], parts[1], function (error, alias) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.toString()));
// https://wiki.debian.org/LDAP/MigrationTools/Examples
// https://docs.oracle.com/cd/E19455-01/806-5580/6jej518pp/index.html
// member is fully qualified - https://docs.oracle.com/cd/E19957-01/816-6082-10/chap4.doc.html#43314
var obj = {
dn: req.dn.toString(),
attributes: {
objectclass: ['nisMailAlias'],
objectcategory: 'nisMailAlias',
cn: alias.name,
rfc822MailMember: alias.aliasTarget
cn: `${alias.name}@${alias.domain}`,
rfc822MailMember: `${alias.aliasTarget}@${alias.domain}`
}
};
@@ -331,19 +381,24 @@ function mailingListSearch(req, res, next) {
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
mailboxdb.getGroup(req.dn.rdns[0].attrs.cn.value.toLowerCase(), config.fqdn(), function (error, group) {
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.getGroup(parts[0], parts[1], function (error, group) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.toString()));
// http://ldapwiki.willeke.com/wiki/Original%20Mailgroup%20Schema%20From%20Netscape
// members are fully qualified (https://docs.oracle.com/cd/E19444-01/816-6018-10/groups.htm#13356)
var obj = {
dn: req.dn.toString(),
attributes: {
objectclass: ['mailGroup'],
objectcategory: 'mailGroup',
cn: group.name,
mail: group.name + '@' + config.fqdn(),
mgrpRFC822MailMember: group.members
cn: `${group.name}@${group.domain}`, // fully qualified
mail: `${group.name}@${group.domain}`,
mgrpRFC822MailMember: group.members.map(function (m) { return `${m}@${group.domain}`; })
}
};
@@ -369,18 +424,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;
@@ -401,7 +456,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();
});
@@ -409,43 +464,51 @@ 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 name = req.dn.rdns[0].attrs.cn.value.toLowerCase();
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()));
// allow login via email
var parts = name.split('@');
if (parts[1] === config.fqdn()) {
name = parts[0];
}
mailboxdb.getMailbox(name, config.fqdn(), function (error, mailbox) {
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.message));
if (mailbox.ownerType === mailboxdb.TYPE_APP) {
var addonId = req.dn.rdns[1].attrs.ou.value.toLowerCase(); // 'sendmail' or 'recvmail'
var name;
if (addonId === 'sendmail') name = 'MAIL_SMTP_PASSWORD';
else if (addonId === 'recvmail') name = 'MAIL_IMAP_PASSWORD';
else return next(new ldap.OperationsError('Invalid DN'));
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));
appdb.getAddonConfigByName(mailbox.ownerId, addonId, name, function (error, value) {
if (error) return next(new ldap.OperationsError(error.message));
if (req.credentials !== value) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
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';
else if (addonId === 'recvmail') name = 'MAIL_IMAP_PASSWORD';
else return next(new ldap.OperationsError('Invalid DN'));
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) {
authenticateUser(req, res, function (error) {
if (error) return next(error);
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: name }, { userId: req.user.username });
res.end();
});
} else {
return next(new ldap.OperationsError('Unknown ownerType for mailbox'));
}
appdb.getAddonConfigByName(mailbox.ownerId, addonId, name, function (error, value) {
if (error) return next(new ldap.OperationsError(error.message));
if (req.credentials !== value) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: name }, { appId: mailbox.ownerId, addonId: addonId });
return res.end();
});
} else if (mailbox.ownerType === mailboxdb.OWNER_TYPE_USER) {
if (!domain.enabled) return next(new ldap.NoSuchObjectError(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: result.id, user: users.removePrivateFields(result) });
res.end();
});
} else {
return next(new ldap.OperationsError('Unknown ownerType for mailbox'));
}
});
});
}
@@ -472,6 +535,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);

1082
src/mail.js Normal file

File diff suppressed because it is too large Load Diff

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

View File

@@ -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="" />
<% } %>

View File

@@ -1,8 +1,8 @@
<%if (format === 'text') { %>
Dear Cloudron Admin,
Dear <%= cloudronName %> Admin,
creating a backup of <%= fqdn %> has failed.
creating a backup has failed.
-------------------------------------

View File

@@ -2,7 +2,7 @@
Dear <%= cloudronName %> Admin,
Version <%= newBoxVersion %> for Cloudron <%= fqdn %> is now available!
Version <%= newBoxVersion %> is now available!
Changelog:
<% for (var i = 0; i < changelog.length; i++) { %>
@@ -27,7 +27,7 @@ Sent at: <%= new Date().toUTCString() %>
<div style="width: 650px; text-align: left;">
<p>
Version <b><%= newBoxVersion %></b> for Cloudron <%= fqdn %> is now available!
Version <b><%= newBoxVersion %></b> is now available!
</p>
<h5>Changelog:</h5>
@@ -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="" />
<% } %>

View File

@@ -2,7 +2,7 @@
Dear <%= cloudronName %> Admin,
This is a summary of the activities on your Cloudron <%= fqdn %>.
This is a summary of the activities on your Cloudron.
<% if (info.usersAdded.length) { -%>
The following users were added:
@@ -53,11 +53,6 @@ Last successful backup: <%- info.finishedBackups[0].backupId || info.finishedBac
This Cloudron did **not** backup successfully in the last week!
<% } -%>
<% if (!info.hasSubscription) { -%>
*Keep your Cloudron automatically up-to-date and secure by upgrading to a paid plan at* <%= webadminUrl %>/#/settings
<% } -%>
Powered by https://cloudron.io
Sent at: <%= new Date().toUTCString() %>
@@ -156,12 +151,6 @@ Sent at: <%= new Date().toUTCString() %>
<% } %>
<br/>
<% if (!info.hasSubscription) { %>
Keep your Cloudron automatically up-to-date and secure by upgrading to a <a href="<%= webadminUrl %>/#/settings">paid plan</a>.
<% } %>
<br/>
<br/>
<br/>
@@ -174,5 +163,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="" />
<% } %>

View File

@@ -1,8 +1,8 @@
<%if (format === 'text') { %>
Dear Cloudron Admin,
Dear <%= cloudronName %> Admin,
<%= program %> on <%= fqdn %> exited unexpectedly using too much memory!
<%= program %> exited unexpectedly using too much memory!
The app has been restarted now. Should this message appear repeatedly or
undefined behavior is observed, give the app more memory.

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