Compare commits

...

899 Commits

Author SHA1 Message Date
Girish Ramakrishnan
9e8179a235 up link is relative 2016-03-29 14:02:53 -07:00
Girish Ramakrishnan
3fbeb2a1c1 more 0.10.2 changes 2016-03-29 13:24:26 -07:00
Girish Ramakrishnan
2c4cf0a505 Download intermediate cert following the 'up' Link 2016-03-29 12:51:05 -07:00
Girish Ramakrishnan
adab544e99 Version 0.10.2 changes 2016-03-28 10:55:20 -07:00
Girish Ramakrishnan
ae8a371597 add adminFqdn in the spf record
For custom domains, we do not set the A record for the naked domain
(because the user might be using it for his own). This means that
a:domain.com will not work.

The solution is to simply use the admin domain.
2016-03-27 23:05:29 -07:00
Girish Ramakrishnan
ead076bd9f add MAIL_SMTP_PASSWORD 2016-03-25 23:14:09 -07:00
Girish Ramakrishnan
f8c683f451 Disallow updating an app with mismatching manifest id
Story so far:
1. App installed from store. appStoreId is set to manifest.id.
2. User installed a custom built app with a custom manifest.id using cloudron install --app <id>. The appStoreId is still set.
3. When we make a new release, it overrides the users install.

The fix (for now) is:
1. Do not allow mismatching ids to start with.
2. When forced, it is allowed but appStoreId is cleared so as to not get any auto updates.

This leaves the user vulnerable to 'cloudron uninstall' simply autoselecting this new app.
For this, they have to simply disable CLI mode for now.

There is also a corner case where:
1. Dev installs from app store
2. Dev compiles from source and updates on top of app store install with --app <id>
3. Dev find out that his installation has auto-updated the next day.
2016-03-25 11:46:25 -07:00
Johannes Zellner
b56bc08e9a Allow to use email and username for ldap bind 2016-03-24 21:03:04 +01:00
Girish Ramakrishnan
daadbfa23f fix wording 2016-03-23 12:00:30 -07:00
Girish Ramakrishnan
a215443c56 do not renew apps without any cert
autoRenew was mistakenly reconfiguring app without a cert (this
is the common case for apps in non-custom domain)
2016-03-23 08:49:08 -07:00
girish@cloudron.io
4e22c6d5ac minor nakedomain fixes 2016-03-21 15:07:10 -07:00
girish@cloudron.io
d43810fea9 add comment on why we add naked domain for custom domains 2016-03-21 13:50:26 -07:00
girish@cloudron.io
f5ab63e8ec naked domain page styling 2016-03-21 13:49:11 -07:00
girish@cloudron.io
b1f172ed17 trim the output string 2016-03-21 08:25:10 -07:00
Girish Ramakrishnan
413f9231b3 fix formatting 2016-03-20 12:12:22 -07:00
Girish Ramakrishnan
11513f9428 send a message for cert renewal status 2016-03-19 20:40:03 -07:00
Girish Ramakrishnan
5042741435 renew cert every 12 hours 2016-03-19 20:30:01 -07:00
Girish Ramakrishnan
75ed9c4a63 Check for key file instead of csr file
1) csr file in older backups got corrupt
2) new key results in a new cert request in LE (for rate limits)
2016-03-19 18:49:55 -07:00
Girish Ramakrishnan
8c36f3aab4 add debug for fallback case 2016-03-19 18:37:05 -07:00
Girish Ramakrishnan
7aa5e8720a 0.10.1 changes 2016-03-19 14:17:28 -07:00
Girish Ramakrishnan
14ef71002f write the DER cert properly into the csr file 2016-03-19 14:07:58 -07:00
Girish Ramakrishnan
ea87841e77 merge fallback cert job into renewal
this is becase we need to reconfigure for the case where we got a
renewed cert (but the app was switched to fallback cert at some point)
2016-03-19 13:54:52 -07:00
Girish Ramakrishnan
091e424c0e Fix description 2016-03-19 13:37:58 -07:00
Girish Ramakrishnan
20629ea078 fix linter errors 2016-03-19 13:22:38 -07:00
Girish Ramakrishnan
b1b6a9ae65 reconfigure admin using configureAdmin 2016-03-19 12:54:11 -07:00
Girish Ramakrishnan
7ddbf7b652 refactor expiry check 2016-03-19 12:50:31 -07:00
Girish Ramakrishnan
3d088aa9c4 fix debug message 2016-03-19 12:31:48 -07:00
Girish Ramakrishnan
f329e0da92 fix typo 2016-03-19 12:14:23 -07:00
Girish Ramakrishnan
a18737882b run more aggressively in test mode 2016-03-19 12:12:39 -07:00
Girish Ramakrishnan
a58a458950 do not abbrev 2016-03-19 12:11:28 -07:00
Girish Ramakrishnan
44c5f84c56 Fix usage of isExpiringSync 2016-03-19 12:06:13 -07:00
Girish Ramakrishnan
d6b92ee301 remove Job suffix 2016-03-19 10:25:19 -07:00
Girish Ramakrishnan
c769a12c45 set the box version for test for pass 2016-03-19 10:23:12 -07:00
Girish Ramakrishnan
017c32c3dd fix certificate renewal
Do the whole acme flow for certificate renewal. the idea here is
simply reuse the key and the csr. In this case, it does not count
as a new certificate issuance.

https://github.com/diafygi/letsencrypt-nosudo/issues/55
2016-03-19 02:44:05 -07:00
Girish Ramakrishnan
5d54c9e668 check my domain for expiry and falling back 2016-03-18 23:43:56 -07:00
Girish Ramakrishnan
adaaca5ceb switch expired certs of domains to use fallback cert
1) nginx won't reload when using expired certs
2) this is the only way the user can use the app now
2016-03-18 23:26:57 -07:00
Girish Ramakrishnan
4a73e1490e Refactor code to take hours 2016-03-18 23:00:02 -07:00
Girish Ramakrishnan
f31a7a5061 use fallback certs if renewal fails 2016-03-17 12:20:02 -07:00
Girish Ramakrishnan
3499a4cc6c move requiresOAuthProxy to nginx
we have 3 levels
    * routes, cron, apptask
    * everything else where everyone calls everyone :-)
    * the db layer
2016-03-17 11:38:29 -07:00
girish@cloudron.io
42796b12dc update safetydance to 0.1.1 2016-03-14 22:50:48 -07:00
girish@cloudron.io
20ac040dde cert: check expiry correctly 2016-03-14 22:50:06 -07:00
girish@cloudron.io
7f2b3eb835 acme: disable renewal via url fetch for now
this does not seem to work.

From cf85854177:

// RenewCertificate attempts to renew an existing certificate.
// Let's Encrypt may return the same certificate. You should load your
// current x509.Certificate and use the Equal method to compare to the "new"
// certificate. If it's identical, you'll need to run NewCertificate and/or
// start a new certificate flow.
2016-03-14 22:22:57 -07:00
girish@cloudron.io
2b562f76ea le: handle renewal upto 30 days in advance 2016-03-14 22:18:43 -07:00
Girish Ramakrishnan
b942033512 acme: debug output the domain 2016-03-14 16:21:03 -07:00
Girish Ramakrishnan
fa4a8c2036 add debug for successful renewal 2016-03-14 15:55:51 -07:00
Johannes Zellner
27febbf1e9 The blue cloud is gone 2016-03-11 16:48:56 -08:00
girish@cloudron.io
8da2eb36cc fix email wording 2016-03-09 18:37:02 -08:00
girish@cloudron.io
cbb34005c6 restoreKey -> filename 2016-03-09 14:23:42 -08:00
girish@cloudron.io
efc1627648 more changes 2016-03-09 09:34:57 -08:00
girish@cloudron.io
f513dcdf3b Add 0.10.0 changelog 2016-03-09 09:29:17 -08:00
girish@cloudron.io
61a52d8888 dist-upgrade to update more aggressively 2016-03-09 09:29:07 -08:00
Johannes Zellner
4cfc187063 Add sender name to admin email 2016-03-09 07:41:50 +01:00
Johannes Zellner
065af03e5f Stop docker proxy in ldap tests 2016-03-09 07:34:44 +01:00
Johannes Zellner
c4eeebdfbe Enable admin change test 2016-03-09 06:18:39 +01:00
Johannes Zellner
b1004de358 Notify admins about newly added admin 2016-03-09 06:16:21 +01:00
Girish Ramakrishnan
fbca0fef38 fix missing assert 2016-03-08 18:51:40 -08:00
Girish Ramakrishnan
d658530e66 fix failing tests 2016-03-08 18:44:51 -08:00
Girish Ramakrishnan
21d4cc9cb2 getAllPaged -> getPaged 2016-03-08 18:10:39 -08:00
Girish Ramakrishnan
e2b7ec3ffd store filename with tar.gz extension 2016-03-08 16:47:53 -08:00
Girish Ramakrishnan
8014e2eaf8 add route to download backup 2016-03-08 16:28:42 -08:00
girish@cloudron.io
a10ed73af2 get zoneName using tldjs 2016-03-08 09:52:13 -08:00
girish@cloudron.io
8b2903015d list app backups from db 2016-03-08 08:57:28 -08:00
girish@cloudron.io
d157bf30f3 remove box backups from the database 2016-03-08 08:52:20 -08:00
girish@cloudron.io
7996b32022 add backups to the database
ideally, these should be done _after_ the backup is successful and not when
the backup url is generated.

we had a discussion on why need backupdb to start with. Some rationale includes:
1. we can use it as a FK constraint (like make sure you delete backups when you delete app)
2. have labels for backups
3. dependancy relation from box backup to apps
4. s3 reverse sort is little painful and requires us to get all items in bulk and sort in JS
   (also requires us to change our backup filename format)
5. any metadata storage requires database

The cons include:
1. s3 and this db go out of sync
2. db entry is useless if s3 file is missing
2016-03-08 08:42:00 -08:00
girish@cloudron.io
4b77703902 export getByAppIdPaged 2016-03-07 17:52:13 -08:00
Girish Ramakrishnan
4dd82d10ad backup: ensure same timestamp for app data and config 2016-03-07 12:13:54 -08:00
Girish Ramakrishnan
83d05c99d3 mount manually instead of fstab because of race
I cannot figure how to make the box-setup.service run before the mounting
of a specific mount point. adding a dep on mount.target locks up the system.
2016-03-07 10:48:09 -08:00
Girish Ramakrishnan
b0acdfb908 use truncate instead of fallocate 2016-03-07 10:44:35 -08:00
Girish Ramakrishnan
b062dab65c mysql also uses the data partition 2016-03-07 10:38:59 -08:00
Girish Ramakrishnan
eadcdeee1c not being mounted is the normal case 2016-03-07 10:37:26 -08:00
Girish Ramakrishnan
9de6f9c1c2 add backupdb
mostly same code as the appstore side
2016-03-07 09:30:44 -08:00
Girish Ramakrishnan
89f54245f7 Add backups table 2016-03-07 09:27:10 -08:00
Girish Ramakrishnan
5fbd1dae30 bump the mysql memory limit
we hit this memory limit often in phabricator backup. this is all
very crude but should suffice for now.
2016-03-05 18:35:28 -08:00
girish@cloudron.io
486ced0946 fix LDAP debug 2016-03-04 17:52:27 -08:00
girish@cloudron.io
d1c1fb8786 fix ldap debug ("ldap" already appears as part of debug) 2016-03-04 17:51:18 -08:00
Johannes Zellner
57ff8b6770 fix feedback test 2016-03-04 22:27:18 +01:00
Johannes Zellner
d12d8f5c0b Properly extract referrers, which contain queries on their own and are not properly encoded 2016-03-03 15:06:14 +01:00
Johannes Zellner
17deac756b Also log app manifest id for alive apps 2016-03-03 09:30:46 +01:00
Johannes Zellner
f7bb3bac98 Log app manifest id in healthmonitor 2016-03-03 09:30:46 +01:00
girish@cloudron.io
744c721000 use docker 1.10.2 (untested) 2016-03-01 10:13:44 -08:00
girish@cloudron.io
0500bae221 install aufs tools
https://github.com/docker/docker/issues/915
2016-03-01 10:13:04 -08:00
girish@cloudron.io
a7b5b49d96 fix language 2016-02-26 10:14:37 -08:00
Johannes Zellner
93ef1919c2 Hide superuser checkbox for the user himself 2016-02-26 18:08:56 +01:00
girish@cloudron.io
254d6ac92e handle singular case as well 2016-02-26 08:43:59 -08:00
girish@cloudron.io
3a12265f42 Do not preallocate data volume
This is not tested but will get tested in the next upgrade
2016-02-26 08:27:13 -08:00
Johannes Zellner
71eeb47f0f Hide groups in user listing if the screen is too tiny 2016-02-26 13:10:29 +01:00
Johannes Zellner
5ea5023d97 Better encapsulate the form related functions in install view 2016-02-26 12:50:05 +01:00
Johannes Zellner
1148e21cd4 Add more changes 2016-02-26 12:29:52 +01:00
Johannes Zellner
e9a2b2a7cf Disable group access control and show info if there are no groups 2016-02-26 11:59:18 +01:00
Johannes Zellner
7a34f40611 Allow to specify accessRestrictions on app install 2016-02-26 11:47:20 +01:00
Johannes Zellner
c630de1003 Fetch groups in appstore.js 2016-02-26 11:24:01 +01:00
Johannes Zellner
74da8f5af8 Add 0.9.3 changes 2016-02-26 11:19:45 +01:00
girish@cloudron.io
b758be5ae2 0.9.2 changes 2016-02-26 11:17:36 +01:00
Johannes Zellner
c585be4eec Adjust group length error message 2016-02-26 11:07:31 +01:00
Johannes Zellner
3ebc569438 Avoid using superuser in the ui, but describe what it is 2016-02-26 11:03:39 +01:00
Johannes Zellner
5a2cf3cbfe Move superuser checkbox at the bottom of the form 2016-02-26 11:02:43 +01:00
Johannes Zellner
715c5f9f61 Do not set admin group multiple times 2016-02-26 10:56:33 +01:00
Johannes Zellner
6843fda601 Show group names and make sure we don't break layout right away 2016-02-26 10:53:41 +01:00
Johannes Zellner
a78f3b1db3 Give more space to some views 2016-02-26 10:52:47 +01:00
girish@cloudron.io
1419108a86 umount is for unmounting 2016-02-25 20:13:16 -08:00
girish@cloudron.io
7a8b457ce9 truncate to shrink the file if required 2016-02-25 19:26:46 -08:00
girish@cloudron.io
10967ff8ce allow 1.2 times RAM
This is basically to allow 2 phabricators and another small app with
no warning on a 4gb droplet :-)
2016-02-25 18:34:28 -08:00
girish@cloudron.io
1fdfd3681c Revert "Display group ids"
This reverts commit d80ce25363061f95cb14e223efd4ab9828739eea.

Didn't mean to commit this
2016-02-25 18:11:25 -08:00
girish@cloudron.io
187d4f9ca2 round memory to nearest GB
os.totalmem returns some close-to-GB number. Because of this two
apps with 2GB don't install on 4GB.
2016-02-25 17:19:39 -08:00
girish@cloudron.io
6b67e64bf1 Display group ids 2016-02-25 16:33:58 -08:00
girish@cloudron.io
7ae6061d72 Edit -> Save 2016-02-25 15:24:50 -08:00
Johannes Zellner
e96b9c3e3f Give the user the perception he gets what he pays for 2016-02-26 00:18:47 +01:00
Johannes Zellner
c9ca05a703 Do not offer admin group for access restriction
The same can be achieved with a new group and it just
keeps the superuser/admin out of the way here. In any case
admins can always access all apps.
2016-02-26 00:13:09 +01:00
Johannes Zellner
23e5bed247 Fix the group numbering to ignore admin group 2016-02-26 00:02:38 +01:00
Johannes Zellner
bae0d728b3 Only show group count to not break layout and allow quickedit 2016-02-26 00:01:25 +01:00
Johannes Zellner
5cd1c7d714 add hand selector 2016-02-26 00:01:01 +01:00
Johannes Zellner
d430e902bf Separate superuser checkbox from the other groups in user edit 2016-02-25 23:20:55 +01:00
Johannes Zellner
4fb89de34f Remove 'this is you' 2016-02-25 22:29:06 +01:00
Johannes Zellner
7cd3bb31e1 Add new support type for failing erroring apps 2016-02-25 21:38:37 +01:00
Girish Ramakrishnan
2857158543 do not set memoryLimit 2016-02-25 11:05:19 -08:00
Johannes Zellner
82a347ea4b Add tooltips for superusers 2016-02-25 16:07:31 +01:00
Johannes Zellner
b5c7f978a2 Do not show admin group in group listing 2016-02-25 15:54:30 +01:00
Johannes Zellner
625da29fce Show admins with an icon instead of a group tag 2016-02-25 15:53:36 +01:00
Johannes Zellner
b82b183df6 Show alternative text for non admins, when no apps are available for this user 2016-02-25 15:38:46 +01:00
Johannes Zellner
ce36fadf2b Fix bug for non admins to view the appstore 2016-02-25 15:34:44 +01:00
Johannes Zellner
2429599733 Change text from installed to your applications 2016-02-25 15:34:33 +01:00
Johannes Zellner
261a0a1728 Add account ui to change displayName 2016-02-25 15:09:52 +01:00
Johannes Zellner
d8def61f67 Encapsulate the business logic in the account controller 2016-02-25 14:58:26 +01:00
Johannes Zellner
2732af24c1 Some code cleanups and bugfixes for the accounts view 2016-02-25 14:46:53 +01:00
Johannes Zellner
3d48da0e8d Remove unused function in client to change email 2016-02-25 14:34:35 +01:00
Johannes Zellner
d3b8bd1314 Remove password field for user email change 2016-02-25 14:34:16 +01:00
Johannes Zellner
f600ebcf19 Remove password entry from user edit form 2016-02-25 14:15:48 +01:00
Johannes Zellner
160467e199 Do not require password for user profile changes 2016-02-25 14:03:42 +01:00
Johannes Zellner
384c410e7c Do not require a password for user profile changes 2016-02-25 13:54:44 +01:00
Johannes Zellner
84c4187fa9 Test normal users accessing the user api 2016-02-25 13:53:18 +01:00
Johannes Zellner
4f7fd9177c Allow user details only for the same user or admins 2016-02-25 13:44:53 +01:00
Johannes Zellner
b5b0ab7475 Require admin rights for user listing 2016-02-25 13:43:15 +01:00
Johannes Zellner
a0d7406b3c Do not allow normal users to get group listings or details 2016-02-25 13:34:01 +01:00
Johannes Zellner
7165be0513 Warn the user about installing too many apps, but give an override button 2016-02-25 12:41:15 +01:00
Johannes Zellner
9c995277f7 Fixup the apps unit tests 2016-02-25 12:20:18 +01:00
Johannes Zellner
aa693e529b Only list apps where a user has access to 2016-02-25 12:20:11 +01:00
Johannes Zellner
63013c7297 Just check for .admin flag in the user object 2016-02-25 11:42:25 +01:00
Johannes Zellner
c8db6419d8 Admins are not special cased in apps.js app listing
This is done in the route
2016-02-25 11:41:14 +01:00
Johannes Zellner
93c1ddd982 Always amend the admin flag for further use 2016-02-25 11:40:48 +01:00
Johannes Zellner
df102ec374 Add basic getAllByUser() app tests 2016-02-25 11:28:45 +01:00
Johannes Zellner
9688e4c124 Add apps.getAllByUser() 2016-02-25 11:28:29 +01:00
Johannes Zellner
00d277b1c3 Make the error dialog generic not only for app install errors 2016-02-24 18:36:40 +01:00
Johannes Zellner
0fb44bfbc1 Forward the error.message instead of making a new Error object
That leads to only Internal Error
2016-02-24 18:12:31 +01:00
Johannes Zellner
c167bd8996 Set error in installationProgress also on uninstallation errors 2016-02-24 17:53:21 +01:00
Johannes Zellner
a3737c3797 Report access denied errors in route53 backend 2016-02-23 17:29:28 +01:00
Johannes Zellner
8fcb0b46a5 add 0.9.1 changes 2016-02-21 14:51:51 +01:00
Johannes Zellner
f5189e0a56 Force the user to set at least one access restriction group 2016-02-19 18:02:51 +01:00
Johannes Zellner
86f14b0149 Thanks JS for being so value focused... 2016-02-19 17:23:09 +01:00
Johannes Zellner
30913006e3 Remove all occurances of oauthProxy in the webadmin 2016-02-19 16:50:25 +01:00
Johannes Zellner
81bd4f2ea5 Remove console.log() 2016-02-19 16:29:40 +01:00
Johannes Zellner
351ddcb218 Fixup the unit tests 2016-02-19 16:29:29 +01:00
Johannes Zellner
dd18f9741a Dynamically detect oauth proxy needs in apptask 2016-02-19 16:18:47 +01:00
Johannes Zellner
cdce6e605d Adjust to new apps api 2016-02-19 16:14:02 +01:00
Johannes Zellner
d4480ec407 Remove oauthProxy usage in the database wrapper 2016-02-19 16:12:58 +01:00
Johannes Zellner
85c92ab0b4 Remove oauthProxy from the apps.js api 2016-02-19 16:07:49 +01:00
Johannes Zellner
230c24d6c6 Adjust the route unit tests, to remove oauthProxy 2016-02-19 16:01:08 +01:00
Johannes Zellner
07c935dfec Remove oauthProxy from client side api wrapper 2016-02-19 16:00:48 +01:00
Johannes Zellner
eab3bda8e1 Remove oauthProxy from the apps rest routes 2016-02-19 15:54:01 +01:00
Johannes Zellner
f731c1ed0b Dynamically detect if an oauth proxy should be used for an app 2016-02-19 15:44:15 +01:00
Johannes Zellner
edec3601f4 Do not rely on oauthProxy property of app object in nginx configuration code 2016-02-19 15:43:39 +01:00
Johannes Zellner
9e87fd0440 Add apps.requiresOAuthProxy() 2016-02-19 15:03:36 +01:00
Johannes Zellner
8cb304e1c9 Ensure app ordering by location 2016-02-19 13:36:39 +01:00
Johannes Zellner
a24335d68b Remove oauth proxy setting ui 2016-02-19 12:04:05 +01:00
Johannes Zellner
78d1ed7aa5 Special case the admin button in apps access control UI 2016-02-18 18:26:24 +01:00
Johannes Zellner
deb30e440a Disable ejs debug flag 2016-02-18 18:00:23 +01:00
Johannes Zellner
86ef9074b1 Add access restriction tests for ldap auth 2016-02-18 17:40:53 +01:00
Johannes Zellner
1a13128ae1 Ensure accessRestriction is either an object or an empty string 2016-02-18 17:28:00 +01:00
Johannes Zellner
b41642552d The ldap property is part of req.connection 2016-02-18 16:40:30 +01:00
Johannes Zellner
f5570c2e63 Enable the apps group access control ui 2016-02-18 16:14:45 +01:00
Johannes Zellner
b0d11ddcab Adhere to access control on ldap user bind 2016-02-18 16:04:53 +01:00
Johannes Zellner
804464c304 Add apps.getByIpAddress() 2016-02-18 15:43:46 +01:00
Johannes Zellner
ecf7f442ba Add docker.getContainerIdByIp() 2016-02-18 15:39:27 +01:00
Johannes Zellner
9ddd3aeb07 Show app id and fix naked domain in debugApp() 2016-02-18 12:51:25 +01:00
Johannes Zellner
864e3ff217 Give form feedback if group name is invalid
Fixes #583
2016-02-15 13:09:58 +01:00
Johannes Zellner
9bf1fe3b7d Show naked_domain for healthtask summary 2016-02-14 17:42:52 +01:00
Johannes Zellner
b32a48c212 Add changelog for 0.9.0 2016-02-14 13:19:12 +01:00
Johannes Zellner
22a3dd7653 Disable advanced accessControl ui
This is working, from a configuration standpoint.
However not all auth methods support this yet, so
we hide it until that is done, otherwise it is just
confusing
2016-02-14 13:15:09 +01:00
Johannes Zellner
132b463e0a Hide memoryLimit ui 2016-02-14 13:14:03 +01:00
Johannes Zellner
7aefe5226a Properly layout the group labels 2016-02-14 13:12:04 +01:00
Johannes Zellner
656c1bfd3a Make Admin group members visible 2016-02-14 13:11:49 +01:00
Johannes Zellner
e237b609f5 Make admin group buttons red 2016-02-14 13:08:50 +01:00
Johannes Zellner
057b9e954e Support btn-admin 2016-02-14 13:08:40 +01:00
Johannes Zellner
f79c00d9be Special case admin group button in user profile 2016-02-14 12:51:58 +01:00
Johannes Zellner
5f96d862ab Move default memory limit to constants.js 2016-02-14 12:13:49 +01:00
Johannes Zellner
79199bf023 Ensure we stash and restore the memoryLimit 2016-02-14 12:10:22 +01:00
girish@cloudron.io
beec4dddca my -> our 2016-02-13 11:22:47 -08:00
Johannes Zellner
7c243cb219 Warn the user on group deletion if it still has members 2016-02-13 12:42:41 +01:00
Johannes Zellner
754e33af2a do not allow removing the admin group 2016-02-13 12:42:41 +01:00
Johannes Zellner
63cab7d751 Allow non-empty groups to be deleted 2016-02-13 12:42:41 +01:00
Johannes Zellner
503714a10b Special case admin group in group listing 2016-02-13 12:42:41 +01:00
girish@cloudron.io
ada5be6ae0 Add 0.9.0 changes 2016-02-13 03:27:21 -08:00
girish@cloudron.io
2112494b43 bump mysql image version 2016-02-13 03:26:29 -08:00
girish@cloudron.io
c0b45ad71e update manifestformat to 2.3.0 2016-02-12 17:40:00 -08:00
girish@cloudron.io
5669d387af check app state before exec 2016-02-12 12:32:58 -08:00
Johannes Zellner
957f20a9a8 Always store the memory limit in the app db record and adjust on update if needed 2016-02-11 18:14:16 +01:00
Johannes Zellner
71bfc1cbda Ensure we never go below minimum memoryLimit 2016-02-11 18:13:42 +01:00
Johannes Zellner
489ea3a980 Add memoryLimit validation 2016-02-11 17:39:15 +01:00
Johannes Zellner
8c6f655628 Ensure we deal with byte values for memoryLimit 2016-02-11 17:29:00 +01:00
Johannes Zellner
75d22d7988 Introduce memoryLimit to apps routes 2016-02-11 17:00:21 +01:00
Johannes Zellner
a7bf043a9e Improve memory limit ui 2016-02-11 16:31:11 +01:00
Johannes Zellner
402385faca Remove unused linter options 2016-02-11 14:35:51 +01:00
Johannes Zellner
cdd82fa456 Add access restriction by group ui to app configure dialog 2016-02-11 14:14:51 +01:00
Johannes Zellner
2f7d99f3f6 Do not allow to remove the user from the admin group 2016-02-11 13:43:02 +01:00
Johannes Zellner
e4799991ec Remove reference to non-existing css selector form-signin 2016-02-11 12:53:35 +01:00
Johannes Zellner
66167e74dc Cleanup the user forms 2016-02-11 12:50:02 +01:00
Johannes Zellner
5643d49bef Check if user deletion actually affected a row 2016-02-11 12:07:43 +01:00
Johannes Zellner
81ec26e45c Ensure we can delete users which belong to a group 2016-02-11 12:02:35 +01:00
Johannes Zellner
72c5ebcc06 Add user api tests for adding/removing from admin group 2016-02-11 11:39:19 +01:00
Johannes Zellner
ecf7575dd3 UserError.NOT_ALLOWED is not unused 2016-02-11 11:32:48 +01:00
Johannes Zellner
98a7f44dc1 Check for last admin not required anymore
This is now prevented by the fact that an admin
cannot remove itself from the admin group. There
remains a race, just like before, where two admins could
trigger an admin group removal of the other admin in parallel
and the calls are in a state after admin flag check of
the used tokens. This can only be prevented with a db constraint
in the end.
2016-02-11 11:30:21 +01:00
Johannes Zellner
5fce9c8d1f Do not allow an admin remove itself from admins group 2016-02-11 11:29:04 +01:00
Johannes Zellner
0ea89fccb8 Remove admin api route tests 2016-02-11 11:26:35 +01:00
Johannes Zellner
2c2922d725 Make tests succeed for now
We still have to bring back the sending of email when admins are changed
2016-02-11 11:26:35 +01:00
Johannes Zellner
fbeefeca7d setGroups() has no result 2016-02-11 11:26:35 +01:00
Johannes Zellner
163ceef527 Remove the admin toggle route 2016-02-11 11:26:35 +01:00
Girish Ramakrishnan
db5cc1f694 dropping groupMembers table makes not much sense 2016-02-10 08:57:06 -08:00
Johannes Zellner
a3b9a7365c No need to check for admin, the whole view is admin only 2016-02-10 17:35:40 +01:00
Johannes Zellner
213b2a2802 show groups of each user 2016-02-10 17:34:29 +01:00
Johannes Zellner
229d09bb9e Give the group buttons some space 2016-02-10 17:26:51 +01:00
Johannes Zellner
f127680c8c Fixup the group delete dialog title 2016-02-10 17:23:42 +01:00
Johannes Zellner
f767f7f1b9 We do not actually allow group edit 2016-02-10 17:22:04 +01:00
Johannes Zellner
acb1afa955 Make delete action the last one 2016-02-10 17:20:24 +01:00
Johannes Zellner
d132109925 Fixup the modal dialog css selector 2016-02-10 17:16:50 +01:00
Johannes Zellner
820e417026 angular requires special treatment for DELETE 2016-02-10 17:12:58 +01:00
Johannes Zellner
94bd0c606b Add group removal ui 2016-02-10 16:59:24 +01:00
Johannes Zellner
9a8328e6db We should really use camelCase 2016-02-10 16:43:23 +01:00
Johannes Zellner
5c75d64a07 Do not forget to reset the busy state 2016-02-10 16:41:32 +01:00
Johannes Zellner
a8001995c8 Add business logic for group adding 2016-02-10 16:37:58 +01:00
Johannes Zellner
9ba4d52fb7 Add Client.createGroup() 2016-02-10 16:37:46 +01:00
Johannes Zellner
0e613a1cab Ensure focus 2016-02-10 16:25:38 +01:00
Johannes Zellner
cf3d503a74 Add group add form 2016-02-10 16:24:25 +01:00
Johannes Zellner
1ab46a96f9 Reset the user edit form password error 2016-02-10 15:18:36 +01:00
Johannes Zellner
1a3164ef32 Add ui components for group management 2016-02-10 15:15:09 +01:00
Johannes Zellner
bd62efcff5 That api is of course a PUT api 2016-02-10 15:01:51 +01:00
Johannes Zellner
7fc37b7c70 Allow admins to edit other users 2016-02-10 14:48:54 +01:00
Johannes Zellner
8ddccae15a Save the groups on user edit 2016-02-10 14:47:49 +01:00
Johannes Zellner
675d7c8730 Add Client.setGroups() 2016-02-10 14:47:35 +01:00
Johannes Zellner
ba35d4a313 remove unused class 2016-02-10 14:40:29 +01:00
Johannes Zellner
c1280ddcc2 Make group buttons toggle able 2016-02-10 14:39:49 +01:00
Johannes Zellner
36ded4c06a Group -> Groups 2016-02-10 14:26:17 +01:00
Johannes Zellner
9fb276019e Add ui elements for group selection 2016-02-10 14:26:04 +01:00
Johannes Zellner
19982b1815 Add Client.getGroups() 2016-02-10 14:25:08 +01:00
Johannes Zellner
459d5b8f60 Adjust user action buttons 2016-02-10 14:07:37 +01:00
Johannes Zellner
8ba5dc2352 Fix indentation 2016-02-10 13:55:49 +01:00
Johannes Zellner
8c73a7c7c2 Send admin flag with user profile 2016-02-10 13:35:16 +01:00
Johannes Zellner
e78dd41e88 Replace deprecated gulp-minify-css with gulp-cssnano 2016-02-10 13:13:08 +01:00
Johannes Zellner
59ecb056d0 Fixup the oauth tests to set memoryLimit 2016-02-10 12:49:02 +01:00
Johannes Zellner
11b17fec3a Ensure groupMembers table is created first 2016-02-10 12:38:00 +01:00
Johannes Zellner
5ea81d0fd3 Ensure default minimum memory limit 2016-02-10 12:30:19 +01:00
Johannes Zellner
19cbd1f394 Ensure we never go below 256mb memoryLimit 2016-02-10 12:30:19 +01:00
Johannes Zellner
1b7265f866 Fixup the merge 2016-02-10 12:28:57 +01:00
Johannes Zellner
1cdb64e78d Use memoryLimit from app object instead of manifest in docker.js 2016-02-10 12:25:26 +01:00
Johannes Zellner
eec8708249 Set memory limit on app installation to the default or the one specified in the manifest 2016-02-10 12:25:26 +01:00
Johannes Zellner
ab003bf81f adjust appdb.js and unit tests to support memoryLimit 2016-02-10 12:25:26 +01:00
Johannes Zellner
2d60901b6e memoryLimit has to be BIGINT 2016-02-10 12:25:26 +01:00
Johannes Zellner
3fc9bde4f4 Make memoryLimit step in 8s 2016-02-10 12:25:26 +01:00
Johannes Zellner
4fc0df31fe Add apps.memoryLimit 2016-02-10 12:25:26 +01:00
Girish Ramakrishnan
3ac326e766 try upto 5 minutes to download the tarball
DO/S3 can be really slow at times
2016-02-09 21:07:03 -08:00
Girish Ramakrishnan
4770f9ddf6 add hasAccessTo tests 2016-02-09 21:07:03 -08:00
girish@cloudron.io
7e60fd554a add some failing groups for good measure 2016-02-09 18:55:42 -08:00
girish@cloudron.io
c1cd7ac129 fix typo 2016-02-09 18:53:14 -08:00
girish@cloudron.io
aab62263a7 add accessRestriction group test in oauth2 2016-02-09 18:52:27 -08:00
girish@cloudron.io
79889a0aac test simple auth accessRestriction 2016-02-09 18:40:20 -08:00
Girish Ramakrishnan
f413bfb3a0 Add route to set the users groups 2016-02-09 16:43:32 -08:00
Girish Ramakrishnan
2b0791f4a3 rename vars 2016-02-09 16:19:00 -08:00
Girish Ramakrishnan
d95339534f rename test file 2016-02-09 16:17:01 -08:00
Girish Ramakrishnan
82cf667f3b Add groups route tests 2016-02-09 15:26:46 -08:00
girish@cloudron.io
e20b3f75e4 Handle NOT_EMPTY group deletion 2016-02-09 13:45:28 -08:00
girish@cloudron.io
6cca7b3e0e initial rest API for groups 2016-02-09 13:34:36 -08:00
girish@cloudron.io
0b814af206 Add api to get groups 2016-02-09 13:33:30 -08:00
girish@cloudron.io
bfdabf9272 check groups property in accessRestriction 2016-02-09 13:03:52 -08:00
girish@cloudron.io
60988ff7f3 make hasAccessTo take a callback 2016-02-09 12:48:21 -08:00
girish@cloudron.io
3649fd0c31 drop the gid: prefix for group id
like the username, id and name is same in groups.
2016-02-09 12:28:50 -08:00
girish@cloudron.io
00c5aa041f clear database in backups test 2016-02-09 12:21:36 -08:00
girish@cloudron.io
4569b67007 make users and admins groups as reserved 2016-02-09 12:16:30 -08:00
girish@cloudron.io
1fb26bc441 make startAppTask take a callback 2016-02-09 12:14:04 -08:00
girish@cloudron.io
e6d23a9701 stop previous task explicitly
there is a race:
1. task is running
2. new task is created overwriting the installationState
3. new task kills the old task of step 1. this results in installationState getting overwritten by 'error' because of the sigkill
4. new task that is launched loses the installationState that was step in 2.
2016-02-09 12:09:20 -08:00
girish@cloudron.io
0785266741 kill immediately. by default, it sends SIGTERM 2016-02-09 12:03:21 -08:00
girish@cloudron.io
e752949752 make all tests work after group changes 2016-02-09 11:29:32 -08:00
girish@cloudron.io
199eb2b3e1 set the admin flag in user object 2016-02-09 09:25:17 -08:00
Girish Ramakrishnan
49cbea93fb fix ldap test 2016-02-09 08:52:16 -08:00
girish@cloudron.io
451c410547 make user test pass 2016-02-08 21:17:21 -08:00
girish@cloudron.io
f6541720c4 pass owner flag in createUser 2016-02-08 21:05:02 -08:00
girish@cloudron.io
5e5435e869 send email for userAded 2016-02-08 20:51:20 -08:00
girish@cloudron.io
0d4f113d7d add groupIds to user object 2016-02-08 20:38:50 -08:00
girish@cloudron.io
14fab0992f make user test mostly work 2016-02-08 16:53:20 -08:00
girish@cloudron.io
d7eb004bc1 remove admin arg from user.create 2016-02-08 16:36:45 -08:00
girish@cloudron.io
c34f3ee653 null invitor is ok 2016-02-08 16:36:26 -08:00
girish@cloudron.io
96d595de39 fix database test 2016-02-08 16:25:29 -08:00
girish@cloudron.io
b1f4508313 remove admin references from userdb 2016-02-08 16:18:51 -08:00
girish@cloudron.io
52ce59faaf createUser does not take admin anymore 2016-02-08 16:14:43 -08:00
girish@cloudron.io
85085ae0b2 implement getAllAdmins based on groups 2016-02-08 16:10:44 -08:00
girish@cloudron.io
c14cf9c260 migrate admin flag to group membership 2016-02-08 16:07:44 -08:00
girish@cloudron.io
a47c6f0774 make requires alphabetical 2016-02-08 15:17:54 -08:00
girish@cloudron.io
888955bd9b add groups.isMember 2016-02-08 10:53:01 -08:00
girish@cloudron.io
6abf5e2c44 group members: add/remove/get 2016-02-08 10:48:21 -08:00
girish@cloudron.io
b1935c3550 restrict group names 2016-02-08 09:41:25 -08:00
girish@cloudron.io
e39d7750c5 add group membership table 2016-02-08 08:55:37 -08:00
girish@cloudron.io
1d83a48a1a make group id distinct from name 2016-02-08 08:44:18 -08:00
Girish Ramakrishnan
802ee6c456 more group tests 2016-02-07 20:49:55 -08:00
Girish Ramakrishnan
278085ba22 initial tests for adding group 2016-02-07 20:34:05 -08:00
Girish Ramakrishnan
b945a8a04c add groups model and db code 2016-02-07 20:25:08 -08:00
Girish Ramakrishnan
7ef92071c5 add groups table 2016-02-07 20:11:37 -08:00
girish@cloudron.io
c16ab95193 reword CLI message 2016-02-04 23:13:29 -08:00
Girish Ramakrishnan
c5e2d9a9cc download new app image as the first thing in update
this will reduce downtime.
2016-02-04 22:49:22 -08:00
Girish Ramakrishnan
07df76b25e Change developer mode text to cli 2016-02-04 22:49:22 -08:00
girish@cloudron.io
5b264565db set the target for cloudron links 2016-02-04 18:22:53 -08:00
girish@cloudron.io
a3561bd040 Make Cloudron a link and fix copyright 2016-02-04 17:47:31 -08:00
girish@cloudron.io
6e4f47e807 0.8.1 changes 2016-02-04 15:20:40 -08:00
Johannes Zellner
471965dc66 Add initial still disabled slider to app configure 2016-02-04 16:45:00 +01:00
Johannes Zellner
3b109ea2e7 Include bootstrap-slider in our angular app 2016-02-04 16:44:40 +01:00
Johannes Zellner
6011526d5e Add bootstrap-slider assets 2016-02-04 16:44:03 +01:00
Johannes Zellner
1395d2971b Send app update changelog with email
Fixes #579
2016-02-04 15:31:40 +01:00
Johannes Zellner
e9d6badae7 Changelog is just a flat text, no array 2016-02-04 15:17:18 +01:00
Johannes Zellner
65ddc7f24c Show changelog in app update ui 2016-02-04 15:17:18 +01:00
girish@cloudron.io
fa871c7ada some apache configs require Host header to be set 2016-02-03 20:18:59 -08:00
girish@cloudron.io
8652d6c136 Add 0.8.0 changes 2016-02-02 08:51:13 -08:00
girish@cloudron.io
16d976a145 use multidb version of mysql addon 2016-02-02 08:46:09 -08:00
girish@cloudron.io
fa1f5cc454 call the multi methods if multipleDatabases is set 2016-02-02 08:41:41 -08:00
Johannes Zellner
84c3b367d5 Actually wait for apps to be stopped 2016-01-29 17:24:18 +01:00
Johannes Zellner
793aa6512d Rework parts of the apps tests to be more reliable
No need to create and tear down the addons everytime.
Docker proxy can also be just injected once.
Wait explicitly for apptasks to be terminated.
2016-01-29 16:27:37 +01:00
Johannes Zellner
98ab99ab34 Add callback to config._reset() for convenience 2016-01-29 16:27:04 +01:00
Johannes Zellner
24a826bdd1 Reduce from 20 to 10 sec wait for addons
Since that is reliable already with 10 on my laptop
I assume this is fine for any other more modern machine
2016-01-29 16:26:15 +01:00
Johannes Zellner
05245f5fc7 Add a way to wait and stop pending tasks 2016-01-29 16:25:31 +01:00
Johannes Zellner
b718c8d044 Cleanup config before and after apps tests 2016-01-29 14:30:40 +01:00
Johannes Zellner
2888a85081 Remove cloudron side migrate api
This is some old api when we had a migrate view in the webadmin
This is entirely handled on the appstore for now.
2016-01-29 14:17:33 +01:00
Johannes Zellner
307262244a Also cleanup the config file on config._reset() 2016-01-29 14:17:10 +01:00
Johannes Zellner
9a875634f8 Improve error message in mailer 2016-01-29 12:40:34 +01:00
Johannes Zellner
4af33486ae Add missing fi in shell script 2016-01-29 12:31:59 +01:00
Johannes Zellner
befa898f18 Do not show app version, but make it available as a tooltip on the title for us 2016-01-29 12:29:35 +01:00
Johannes Zellner
18525e1236 Show app website in appstore install dialog
Fixes #580
2016-01-29 12:26:42 +01:00
Johannes Zellner
28ffd01cf4 Invoke BACKUP_APP_CMD with the additional backupConfig.url 2016-01-29 11:55:52 +01:00
Johannes Zellner
09c7aa4440 Remove whitespace 2016-01-29 11:54:58 +01:00
Johannes Zellner
ea4862d351 Strictly assert on app 2016-01-29 11:54:40 +01:00
Johannes Zellner
3e4d62329e Also copy the backup config json 2016-01-29 11:54:15 +01:00
Johannes Zellner
d12366576b Add backup config url to backupapp.sh 2016-01-29 11:44:24 +01:00
Johannes Zellner
7b1d906494 Add backups.getAppBackupConfigUrl() 2016-01-29 11:44:24 +01:00
Johannes Zellner
0972c88b8b Set the correct environment for the installer.sh 2016-01-28 16:36:34 +01:00
girish@cloudron.io
9464a26a7e put version in the mail 2016-01-27 10:35:48 -08:00
Johannes Zellner
10f1ad5cfe Until we can distinguish by the return data, mark both fields red 2016-01-26 19:44:33 +01:00
Johannes Zellner
fd11eb8da0 Ensure the user add form is reset on show 2016-01-26 19:40:57 +01:00
girish@cloudron.io
62d5e99802 Version 0.7.2 changes 2016-01-26 10:34:09 -08:00
girish@cloudron.io
48305f0e95 call retire after sending off the http response 2016-01-26 10:29:32 -08:00
girish@cloudron.io
8170b490f2 add retire.sh
this is a sudo script that retires the box
2016-01-26 09:37:25 -08:00
girish@cloudron.io
072962bbc3 add internal route to retire 2016-01-26 08:46:48 -08:00
girish@cloudron.io
33bc1cf7d9 Add cloudron.retire 2016-01-26 08:39:42 -08:00
Johannes Zellner
85df9d1472 We use 'wrong password' now everywhere 2016-01-26 16:09:14 +01:00
Johannes Zellner
109ba3bf56 Do not perform any client side password validation in user delete form 2016-01-26 16:06:42 +01:00
Johannes Zellner
8083362e71 Ensure we enforce the password pattern for setup view 2016-01-26 15:55:02 +01:00
Johannes Zellner
9b4c385a64 Ensure we send proper password requirements on password reset 2016-01-26 15:21:03 +01:00
girish@cloudron.io
ee9c8ba4eb fix dead comment 2016-01-25 20:15:54 -08:00
girish@cloudron.io
000a64d54a remove installer.retire 2016-01-25 17:52:35 -08:00
girish@cloudron.io
eba74d77a6 remove installer certs
the installer is no more a public server
2016-01-25 17:47:57 -08:00
girish@cloudron.io
714a1bcb1d installer: remove provisioning server
the box will retire itself
2016-01-25 17:46:24 -08:00
girish@cloudron.io
02d17dc2e4 fix typo 2016-01-25 16:51:14 -08:00
girish@cloudron.io
4b54e776cc reset update info after short-circuit 2016-01-25 16:46:54 -08:00
girish@cloudron.io
ba6f05b119 Clear the progress for web ui to work 2016-01-25 16:35:03 -08:00
girish@cloudron.io
1d9ae120dc strip prerelease from box version and not the released version 2016-01-25 16:27:22 -08:00
girish@cloudron.io
3ce841e050 add test for checkDiskSpace 2016-01-25 16:03:12 -08:00
girish@cloudron.io
436fc2ba13 checking capacity does not work well for /
Just check if we have around 1.25GB left.

/root (data image file) is 26G
/home (our code) is 1G
/apps.swap is 2G
/backup.swap is 1G
/var is 6.2G (docker)

Rest of the system is 1.5G

Fixes #577
2016-01-25 15:56:46 -08:00
girish@cloudron.io
77d652fc2b add test for config.setVersion 2016-01-25 15:12:22 -08:00
girish@cloudron.io
ac3681296e short-circuit updates from prerelease to release version
Fixes #575
2016-01-25 14:48:12 -08:00
girish@cloudron.io
5254d3325f add comment on fields on box update info object 2016-01-25 13:57:59 -08:00
girish@cloudron.io
ce0a24a95d comment out public graphite paths 2016-01-25 12:51:37 -08:00
Johannes Zellner
1bb596bf58 Add changes for 0.7.1 2016-01-25 16:40:57 +01:00
Johannes Zellner
c384ac6080 Use local cache when looking up apps 2016-01-25 16:25:25 +01:00
Johannes Zellner
61c2ce0f47 Keep appstore url bar up-to-date with the selected app
This allows sharing links directly

Fixes #526
2016-01-25 16:21:44 +01:00
Johannes Zellner
7a71315d33 Adjust angular's to allow non reload path changes 2016-01-25 16:21:20 +01:00
Johannes Zellner
0a658e5862 Ensure the focus is setup correctly 2016-01-25 15:30:29 +01:00
Johannes Zellner
5f8c99aa0e There is no body here 2016-01-25 15:29:52 +01:00
Johannes Zellner
4c6f1e4b4a Allow admins or users to operate on themselves 2016-01-25 15:29:52 +01:00
Johannes Zellner
226ae627f9 Move updateUser() to where it belongs 2016-01-25 15:29:52 +01:00
Johannes Zellner
27a02aa918 Make user edit form submit with enter 2016-01-25 15:29:52 +01:00
Johannes Zellner
3c43503df8 Add businesslogic for user edit form 2016-01-25 14:58:12 +01:00
Johannes Zellner
35c926d504 Ensure we actually update the correct user, not the user holding the token 2016-01-25 14:58:02 +01:00
Johannes Zellner
ea18ca5c60 Fix copy and paste error in the user add form 2016-01-25 14:46:51 +01:00
Johannes Zellner
55a56355d5 Add email change fields to user edit form 2016-01-25 14:28:47 +01:00
Johannes Zellner
dc83ba2686 Require displayName in updateUser() 2016-01-25 14:26:42 +01:00
Johannes Zellner
62615dfd0f Make email in user change optional 2016-01-25 14:12:09 +01:00
Johannes Zellner
a6998550a7 Add displayName change unit tests 2016-01-25 14:08:35 +01:00
Johannes Zellner
3b199170be Support changing the displayName 2016-01-25 14:08:11 +01:00
Johannes Zellner
1f93787a63 Also send displayName for users 2016-01-25 13:36:51 +01:00
Johannes Zellner
199c5b926a Show tooltip for user action buttons 2016-01-25 13:31:49 +01:00
Johannes Zellner
d9ad7085c3 Shorten the user action buttons 2016-01-25 13:28:48 +01:00
Johannes Zellner
df12f31800 Add ui components for user edit 2016-01-25 13:28:30 +01:00
Johannes Zellner
ad205da3db Match any loop back device
This is just a quick fix and will potentially match all
loop* devices.
2016-01-25 13:06:38 +01:00
Johannes Zellner
34aab65db3 Use the first part of the dn to get the common name in ldap
It is no must to have the first part named 'cn' but the first
part is always the id we want to verify
2016-01-25 11:31:57 +01:00
Johannes Zellner
63c06a508e Make /api available on just the IP
We might want to also show something else than
the naked domain placeholder page when just
accessing the ip
2016-01-24 12:08:10 +01:00
Girish Ramakrishnan
a2899c9c65 add appupdate tests 2016-01-24 00:44:46 -08:00
Girish Ramakrishnan
ff6d5e9efc complete the box update tests 2016-01-24 00:06:32 -08:00
Girish Ramakrishnan
f48fe0a7c0 Get started with updatechecker tests 2016-01-23 22:38:46 -08:00
Girish Ramakrishnan
5f6c8ca520 fix crash 2016-01-23 13:37:21 -08:00
Girish Ramakrishnan
0eaa3a8d94 clear update info so that we use the latest settings 2016-01-23 11:08:01 -08:00
Girish Ramakrishnan
8ad190fa83 make updateConfig a provision argument 2016-01-23 10:15:09 -08:00
girish@cloudron.io
70f096c820 check settingsdb whether to notify for app prerelease
fixes #573
2016-01-23 05:56:08 -08:00
girish@cloudron.io
2840251862 check if updateInfo is null earlier 2016-01-23 05:37:22 -08:00
girish@cloudron.io
b43966df22 code without callback is hard to read 2016-01-23 05:35:57 -08:00
girish@cloudron.io
cc22285beb Check settingsdb whether to notify for prereleases
part of #573
2016-01-23 05:33:16 -08:00
girish@cloudron.io
b72d48b49f set default update config 2016-01-23 05:07:12 -08:00
girish@cloudron.io
3a6b9c23c6 settings: add update config 2016-01-23 05:06:09 -08:00
girish@cloudron.io
b2da364345 fix typo in comment 2016-01-22 17:58:38 -08:00
girish@cloudron.io
de7a6abc50 Check for out of disk space
Fixes #567
2016-01-22 17:46:23 -08:00
girish@cloudron.io
10f74349ca collectd: disable vmem plugin 2016-01-22 15:44:46 -08:00
girish@cloudron.io
05a771c365 collectd: disable process plugin 2016-01-22 15:43:47 -08:00
girish@cloudron.io
cfa2089d7b collectd: Remove ping metric 2016-01-22 15:36:13 -08:00
girish@cloudron.io
d56abd94a9 collectd uses the data lo partition that is resized by box-setup.sh 2016-01-22 15:06:43 -08:00
girish@cloudron.io
2f20ff8def use loop1 instead of loop0
box-setup.sh always resizes the loopback partition on reboot. This
means that it always be loop1.
2016-01-22 15:03:23 -08:00
girish@cloudron.io
9706daf330 Just track ext4 and btrfs file systems 2016-01-22 14:33:02 -08:00
girish@cloudron.io
a246b3e90c box-setup needs to run after mounting to prevent race in script 2016-01-22 14:21:36 -08:00
girish@cloudron.io
e28e1b239f fix comment 2016-01-22 14:21:20 -08:00
girish@cloudron.io
4aead483de This hack is not needed in 15.10 anymore
collectd is still same version in 15.10 but it collects info correctly
as df-vda1 now.
2016-01-22 14:00:40 -08:00
girish@cloudron.io
f8cc6e471e 0.7.0 changes 2016-01-22 11:02:04 -08:00
girish@cloudron.io
6b9ed9472d upgrade to 15.10 2016-01-22 10:46:13 -08:00
girish@cloudron.io
a763b08c41 pin packages
fixes #558
2016-01-22 10:46:13 -08:00
Johannes Zellner
178f904143 Put installer.sh location in README 2016-01-22 13:03:31 +01:00
Girish Ramakrishnan
bb88fa3620 Restart node processes if journald crashes
Note that we cannot simply ignore EPIPE in the node programs.
Doing so results in no logs anymore :-( This is supposedly
fixed in systemd 228.

Fixes #550
2016-01-21 22:13:19 -08:00
Girish Ramakrishnan
1e1249d8e0 Give journald more time to sync
Part of #550
2016-01-21 21:43:49 -08:00
girish@cloudron.io
bcb0e61bfc Kill child processes
On Unix, child processes are not killed when parent dies.

Each process is part of a process group (pgid). When pgid == pid,
it is the process group leader.

node creates child processes with the parent as the group leader
(detached = false).

You can send a signal to entire group using kill(-pgid), as in,
negative value in argument. Systemd can be made to do this by
setting the KillMode=control-group.

Unrelated: Process groups reside inside session groups. Each session
group has a controlling terminal. Only one process in the session
group has access to the terminal. Process group is basically like
a bash pipeline. A session group is the entire login session with only
one process having terminal access at a time.

Fixes #543
2016-01-21 17:44:17 -08:00
girish@cloudron.io
022ff89836 Add 0.6.6 changes 2016-01-21 15:57:22 -08:00
girish@cloudron.io
b9d4b8f6e8 Remove docker images by tag
docker pull previously used to pull down all tags.
    docker pull tag1 # might pull down tag2, tag3 if they are all same!
    docker rm tag1 # oops, tag2 and tag3 still hold on to the image

However, the above feature was not possible with registry v2 (some
technical stuff to do with each tag being separately signed). As a result,
this feature was removed from v1 as well - https://github.com/docker/docker/pull/10571

This means we can now do:
    docker pull tag1 # nice
    docker rm tag1 # image goes away if no one else is using it

references:
https://github.com/docker/docker/issues/8689
https://github.com/docker/docker/pull/8193 (added this feature to v1)
https://github.com/docker/docker/issues/8141 (the request)
https://github.com/docker/docker/pull/10571 (removes the v1 feature)

Fixes #563
2016-01-21 15:53:51 -08:00
Johannes Zellner
0f5ce651cc Show errors if passwords do not match for reset and setup 2016-01-21 16:33:51 +01:00
Johannes Zellner
6b8d5f92de Set meaningful page title for oauth rendered pages 2016-01-21 16:19:38 +01:00
Johannes Zellner
55e556c725 Also provide client side password validation for password setup and reset forms 2016-01-21 16:08:51 +01:00
Johannes Zellner
19bb0a6ec2 Move form feedback below in setup screen 2016-01-21 16:03:46 +01:00
Johannes Zellner
290132f432 Add warning in password.js to update the UI parts 2016-01-21 16:00:12 +01:00
Johannes Zellner
4a8be8e62d Add changes for 0.6.5 2016-01-21 15:57:48 +01:00
Johannes Zellner
23b61aef0c Use client side pattern validation for setup password 2016-01-21 15:52:24 +01:00
Johannes Zellner
24cc433a3d Also take care of the developermode toggle form 2016-01-21 15:17:49 +01:00
Johannes Zellner
e014b7de81 It should be called 'Wrong password' 2016-01-21 15:11:42 +01:00
Johannes Zellner
0895a2bdea Same procedure for the app uninstall dialog 2016-01-21 15:09:22 +01:00
Johannes Zellner
03ca4887ba Streamline the app restore password validation 2016-01-21 15:06:22 +01:00
Johannes Zellner
9eeb17c397 Fixup app configure password validation 2016-01-21 15:03:51 +01:00
Johannes Zellner
6a5da2745a Simplify password validation in email edit 2016-01-21 14:59:39 +01:00
Johannes Zellner
e1111ba2bb Simplify password validation for cloudron update 2016-01-21 14:57:21 +01:00
Johannes Zellner
d186084835 Use password regexp instead of min-max to do client side validation also 2016-01-21 14:47:21 +01:00
Johannes Zellner
06c2ba9fa9 Fixup email edit form with password changes 2016-01-21 14:34:36 +01:00
Johannes Zellner
b82e5fd8c6 Remove console.log() 2016-01-21 14:29:04 +01:00
Johannes Zellner
6e1f96a832 Set min and max length for all password fields 2016-01-21 14:26:24 +01:00
Johannes Zellner
f68135c7aa Fixup password requirement feedback in account settings 2016-01-21 14:03:24 +01:00
Johannes Zellner
f48cbb457b Call reset avatar to trigger favicon change 2016-01-20 17:15:13 +01:00
Johannes Zellner
8d192dc992 Add Client.resetAvatar() 2016-01-20 17:14:50 +01:00
Johannes Zellner
b70324aa24 Give favicon an id 2016-01-20 17:14:36 +01:00
Johannes Zellner
390afaf614 Remove dead code 2016-01-20 16:56:14 +01:00
Johannes Zellner
5112322e7d Ensure the avatar is always updated in all places
Fixes #549
2016-01-20 16:55:44 +01:00
Johannes Zellner
2cb498d500 Improve singleUser configure dialog
Fixes #565
2016-01-20 16:27:43 +01:00
Johannes Zellner
2bd6e02cdc Do not show oauth proxy settings for singleUser apps 2016-01-20 16:23:31 +01:00
Johannes Zellner
85423cbc20 Actually send displayName instead of name in cloudron activation tests 2016-01-20 16:14:44 +01:00
Johannes Zellner
1c0d027bd3 Fix error message if displayName has wrong type 2016-01-20 16:14:21 +01:00
Johannes Zellner
5a8a023039 Fixup all the route tests with new password requirement 2016-01-20 16:06:51 +01:00
Johannes Zellner
196b059cfb Immediately set icon fallback to avoid flickering 2016-01-20 16:01:39 +01:00
Johannes Zellner
2d930b9c3d Explicitly set icon urls to null if we dont have them 2016-01-20 16:01:21 +01:00
Johannes Zellner
a5ba3faa49 Correctly report password errors 2016-01-20 15:41:29 +01:00
Johannes Zellner
02ba91f1bb Move password generation into separate file and ensure we generate strong passwords 2016-01-20 15:33:11 +01:00
Johannes Zellner
bfa917e057 Add password strength unit tests 2016-01-20 14:50:06 +01:00
Johannes Zellner
909dd0725a Fix copy and paste error 2016-01-20 14:49:45 +01:00
Johannes Zellner
74860f2d16 Fix tests for password strength change 2016-01-20 14:39:08 +01:00
Johannes Zellner
132ebb4e74 Require strong passwords
Fixes #568
2016-01-20 14:38:41 +01:00
Johannes Zellner
698158cd93 Change some of the mail added email text 2016-01-20 13:14:19 +01:00
Johannes Zellner
5bfc684f1b Fix crash due to missing event 2016-01-20 13:10:16 +01:00
Johannes Zellner
c944c9b65b Add changelog for 0.6.4 2016-01-20 12:43:40 +01:00
Johannes Zellner
d61698b894 Send user_added email instead of generic user event to admins
Fixes #569
2016-01-20 12:40:56 +01:00
Johannes Zellner
a4d32009ad Make it clear why this if condition is there 2016-01-20 12:39:28 +01:00
Johannes Zellner
3007875e35 Add user_added.ejs email template
This allows us to also send an invitation link to admins
in case the user was not invited already.
2016-01-20 12:38:07 +01:00
Johannes Zellner
b4aad138fc Fixup the update badge for mobile 2016-01-20 12:00:19 +01:00
Johannes Zellner
8df7eb2acb Check if the versions for app updates match
Fixes #566
2016-01-20 11:56:46 +01:00
girish@cloudron.io
18cab6f861 initialize displayName from activation link 2016-01-20 00:16:48 -08:00
girish@cloudron.io
b2071c65d8 Fix typo 2016-01-20 00:05:06 -08:00
girish@cloudron.io
402dba096e webadmin: display name for users 2016-01-19 23:58:52 -08:00
girish@cloudron.io
abf0c81de4 provide displayName in createAdmin route 2016-01-19 23:58:08 -08:00
girish@cloudron.io
613985a17c Set default displayName as empty 2016-01-19 23:47:29 -08:00
girish@cloudron.io
bfc9801699 provide displayName in ldap response when available 2016-01-19 23:47:24 -08:00
girish@cloudron.io
ee705eb979 Add displayName to create user and activate routes 2016-01-19 23:34:49 -08:00
girish@cloudron.io
67b94c7fde give message for development mode 2016-01-19 10:20:24 -08:00
Johannes Zellner
77e5d3f4bb Retry checking for app start state in test 2016-01-19 16:45:58 +01:00
Johannes Zellner
30618b8644 add missing argument 2016-01-19 14:03:01 +01:00
Johannes Zellner
57a2613286 Remove leftover js-error target reference in gulpfile 2016-01-19 13:36:32 +01:00
Johannes Zellner
e15bd89ba2 Add route to list application backups 2016-01-19 13:35:28 +01:00
Johannes Zellner
d2ed816f44 Add apps.listBackups() 2016-01-19 13:35:18 +01:00
Johannes Zellner
e51234928b Add FIXME for selfhost backup listing 2016-01-19 13:32:11 +01:00
Johannes Zellner
3aa668aea3 Fixup tests 2016-01-19 12:42:19 +01:00
Johannes Zellner
870edab78a Set empty displayName for users 2016-01-19 12:40:50 +01:00
Johannes Zellner
ebc9d9185d Use displayName in userdb 2016-01-19 12:39:54 +01:00
Johannes Zellner
093150d4e3 Add displayName to users table 2016-01-19 12:37:22 +01:00
Johannes Zellner
de80a6692d Remove unused error.js the code is inline 2016-01-19 11:22:06 +01:00
Johannes Zellner
c28f564a47 Remove unused gulp target for error.js 2016-01-19 11:21:43 +01:00
Johannes Zellner
eb6a09c2bd Try to fetch the cloudron status to offer a way to reload 2016-01-19 11:20:32 +01:00
Johannes Zellner
19f404e092 Changes for 0.6.3 2016-01-19 11:00:29 +01:00
girish@cloudron.io
55799ebb2d cli tool demuxes stream now 2016-01-18 21:36:05 -08:00
girish@cloudron.io
fdf4d8fdcf maybe stream is duplex 2016-01-18 13:39:18 -08:00
girish@cloudron.io
6dc11edafe make exec route more debugging friedly
allow upto 30 minutes of idle connection
2016-01-18 12:49:06 -08:00
girish@cloudron.io
c82ca1c69d disable http server timeout 2016-01-18 12:28:53 -08:00
girish@cloudron.io
7ef3d55cbf add tty option to exec 2016-01-18 11:39:09 -08:00
Johannes Zellner
44e4f53827 Change user creation api to require the invite flag 2016-01-18 16:53:51 +01:00
Johannes Zellner
643e490cbb Allow to specify if a new user gets invited immediately 2016-01-18 16:44:11 +01:00
Johannes Zellner
e61498c3b6 Ensure the avatar is also based on the apiOrigin 2016-01-18 16:35:25 +01:00
Johannes Zellner
bb6b61d810 Remove unused Client.prototype.getAppLogUrl() 2016-01-18 16:29:33 +01:00
Johannes Zellner
cff173c2e6 Ensure all api calls are absolute 2016-01-18 16:29:13 +01:00
Johannes Zellner
226501d103 Clear user remove form 2016-01-18 16:25:42 +01:00
Johannes Zellner
c5b8b0e3db Split up userAdd and sendInvite mailer calls 2016-01-18 16:11:00 +01:00
Johannes Zellner
46878e4363 Add button to trigger the invite email 2016-01-18 15:45:54 +01:00
Johannes Zellner
f77682365e Add client.sendInvite() 2016-01-18 15:45:44 +01:00
Johannes Zellner
d9850fa660 Add send invite route 2016-01-18 15:37:03 +01:00
Johannes Zellner
9258585746 add user.sendInvite() with tests 2016-01-18 15:16:18 +01:00
Johannes Zellner
e635aaaa58 Fix oauth2 tests 2016-01-18 14:31:25 +01:00
Johannes Zellner
d0d6725df5 Remove obsolete comments 2016-01-18 14:19:38 +01:00
Johannes Zellner
61f4fea9c3 Check for emails sent in users tests 2016-01-18 14:19:20 +01:00
Johannes Zellner
66d59c1d6c Do not require the invitor in the invite mail 2016-01-18 14:18:57 +01:00
Johannes Zellner
f9725965e2 Add test hooks to check if mails are queued 2016-01-18 14:00:35 +01:00
Johannes Zellner
4629739a14 Fix ldap tests 2016-01-18 13:56:59 +01:00
Johannes Zellner
e9b3a1e99c Fixup user tests 2016-01-18 13:50:54 +01:00
Johannes Zellner
8ac27b9dc7 Adjust api to set a flag if invitiation should be sent on user creation 2016-01-18 13:48:10 +01:00
Johannes Zellner
2edd434474 Support more notification types 2016-01-18 13:39:27 +01:00
Johannes Zellner
bebf480321 Use appdb.exists() instead of a apps.get() 2016-01-17 16:05:47 +01:00
Johannes Zellner
10c09d9def Fix wrong conditional in appdb 2016-01-17 16:01:17 +01:00
Johannes Zellner
6ce6b96e5c Fix linter issues 2016-01-17 15:59:11 +01:00
Johannes Zellner
16a9cae80e Allow to specify the restore id 2016-01-17 15:50:20 +01:00
Johannes Zellner
e865e2ae6d Employ hack to ensure chrome and firefox do not autocomplete
http://stackoverflow.com/questions/12374442/chrome-browser-ignoring-autocomplete-off
2016-01-16 17:38:18 +01:00
Johannes Zellner
06363a43f9 Offset the status bar 2016-01-16 16:48:47 +01:00
girish@cloudron.io
09a88e6a1c Version 0.6.2 changes 2016-01-15 18:12:20 -08:00
girish@cloudron.io
28baef8929 Go back to using docker exec in cloudron exec
The main issue is that multiple cloudron exec sessions do not
share the same rootfs. Which makes it annoying to debug.

We also have some nginx timeout which drops you out of exec
now and then resulting in loss of all state.
2016-01-15 15:24:46 -08:00
girish@cloudron.io
9b061a4c7c make the command work 2016-01-15 14:50:13 -08:00
girish@cloudron.io
0b542dfbdf Pause app container in developmentMode
This allows us to share the network namespace with the app container
2016-01-15 14:34:15 -08:00
girish@cloudron.io
d3b039ebd8 support developmentMode flag
- disables readonly rootfs
- disables memory limit
2016-01-15 11:28:43 -08:00
Johannes Zellner
c22924eed7 Use user.js instead of userdb.js in mailer 2016-01-15 16:33:13 +01:00
Johannes Zellner
033ccb121f Add tests for user.getAllAdmins() 2016-01-15 16:33:13 +01:00
Johannes Zellner
ecd91e8f2a Add user.getAllAdmins() 2016-01-15 16:33:13 +01:00
girish@cloudron.io
1cdb954967 0.6.1 changes 2016-01-14 13:30:08 -08:00
girish@cloudron.io
f309f87f55 use no-reply for naked domain apps 2016-01-14 12:56:35 -08:00
girish@cloudron.io
989ab3094d Set initial progress so that tools can wait on it 2016-01-14 11:34:49 -08:00
girish@cloudron.io
70ac18d139 add internal route to update the cloudron
need to way to trigger updates of cloudron using the caas tool
2016-01-14 11:13:02 -08:00
girish@cloudron.io
8f43236e2e adjust update mail text 2016-01-14 11:02:05 -08:00
girish@cloudron.io
38416d46a6 insist on node 4.1.1
this is probably FUD but I think we get npm rebuild issues when
trying to rebuild packages of a newer node to an older node.
2016-01-14 10:55:53 -08:00
girish@cloudron.io
fb96b00922 just keep rebuilding
Jan 14 07:03:27 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:03:27 GMT installer:installer provision (stdout):
Jan 14 07:03:27 rudolf.cloudron.me server.js[541]: > bson@0.2.22 install
/tmp/box-src-a4tklr/node_modules/db-migrate/node_modules/mongodb/node_modules/bson

Jan 14 07:03:27 rudolf.cloudron.me server.js[541]: > (node-gyp rebuild
2> builderror.log) || (exit 0)
Jan 14 07:03:31 rudolf.cloudron.me ntpdate[1344]: step time server
91.189.89.199 offset 0.000661 sec
Jan 14 07:03:31 rudolf.cloudron.me systemd[1]: Time has been changed
Jan 14 07:03:44 rudolf.cloudron.me kernel: IPTables Packet Dropped:
IN=eth0 OUT= MAC=04:01:9a:dd:a9:01:84:b5:9c:fa:08:30:08:00
SRC=79.174.70.237 DST=178.62.202.80 LEN=40 TOS=0x00 PREC=0x00 TTL=248
ID=54321 PROTO=TCP SPT=49152 DPT=22 WINDOW=65535 RES=0x00 SYN URGP=0
Jan 14 07:04:02 rudolf.cloudron.me kernel: IPTables Packet Dropped:
IN=eth0 OUT= MAC=04:01:9a:dd:a9:01:84:b5:9c:fa:08:30:08:00
SRC=124.6.36.197 DST=178.62.202.80 LEN=638 TOS=0x00 PREC=0x00 TTL=59
ID=61522 DF PROTO=TCP SPT=443 DPT=58535 WINDOW=95 RES=0x00 ACK PSH URGP=0
Jan 14 07:04:08 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:04:08 GMT installer:installer provision (stdout):
Jan 14 07:04:08 rudolf.cloudron.me server.js[541]: > kerberos@0.0.11
install
/tmp/box-src-a4tklr/node_modules/db-migrate/node_modules/mongodb/node_modules/kerberos

Jan 14 07:04:08 rudolf.cloudron.me server.js[541]: > (node-gyp rebuild
2> builderror.log) || (exit 0)
Jan 14 07:04:47 rudolf.cloudron.me kernel: IPTables Packet Dropped:
IN=eth0 OUT= MAC=04:01:9a:dd:a9:01:84:b5:9c:fa:08:30:08:00
SRC=58.218.205.83 DST=178.62.202.80 LEN=40 TOS=0x00 PREC=0x00 TTL=112
ID=256 PROTO=TCP SPT=49127 DPT=5555 WINDOW=512 RES=0x00 SYN URGP=0
Jan 14 07:05:18 rudolf.cloudron.me systemd-timesyncd[448]: Timed out
waiting for reply from 91.207.136.55:123 (2.ubuntu.pool.ntp.org).
Jan 14 07:05:28 rudolf.cloudron.me systemd-timesyncd[448]: Timed out
waiting for reply from 194.190.168.1:123 (2.ubuntu.pool.ntp.org).
Jan 14 07:05:49 rudolf.cloudron.me kernel: IPTables Packet Dropped:
IN=eth0 OUT= MAC=04:01:9a:dd:a9:01:84:b5:9c:fa:08:30:08:00
SRC=218.77.79.38 DST=178.62.202.80 LEN=40 TOS=0x00 PREC=0x00 TTL=240
ID=54321 PROTO=TCP SPT=44094 DPT=3306 WINDOW=65535 RES=0x00 SYN URGP=0
Jan 14 07:06:02 rudolf.cloudron.me kernel: IPTables Packet Dropped:
IN=eth0 OUT= MAC=04:01:9a:dd:a9:01:84:b5:9c:fa:08:30:08:00
SRC=124.6.36.197 DST=178.62.202.80 LEN=638 TOS=0x00 PREC=0x00 TTL=59
ID=61523 DF PROTO=TCP SPT=443 DPT=58535 WINDOW=95 RES=0x00 ACK PSH URGP=0
Jan 14 07:06:21 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:06:21 GMT installer:installer provision (stdout):
Jan 14 07:06:21 rudolf.cloudron.me server.js[541]: > sqlite3@3.1.1
install /tmp/box-src-a4tklr/node_modules/db-migrate/node_modules/sqlite3
Jan 14 07:06:21 rudolf.cloudron.me server.js[541]: > node-pre-gyp
install --fallback-to-build
Jan 14 07:06:21 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:06:21 GMT installer:installer provision (stdout): [sqlite3] Success:
"/tmp/box-src-a4tklr/node_modules/db-migrate/node_modules/sqlite3/lib/binding/node-v46-linux-x64/node_sqlite3.node"
already installed
Jan 14 07:06:21 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:06:21 GMT installer:installer provision (stdout): Pass
--update-binary to reinstall or --build-from-source to recompile
Jan 14 07:06:23 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:06:23 GMT installer:installer provision (stdout):
Jan 14 07:06:23 rudolf.cloudron.me server.js[541]: >
dtrace-provider@0.2.8 install
/tmp/box-src-a4tklr/node_modules/ldapjs/node_modules/dtrace-provider
Jan 14 07:06:23 rudolf.cloudron.me server.js[541]: > node-gyp rebuild
Jan 14 07:07:47 rudolf.cloudron.me systemd-timesyncd[448]: Timed out
waiting for reply from 91.206.16.3:123 (2.ubuntu.pool.ntp.org).
Jan 14 07:07:57 rudolf.cloudron.me systemd-timesyncd[448]: Timed out
waiting for reply from 46.254.216.12:123 (2.ubuntu.pool.ntp.org).
Jan 14 07:08:02 rudolf.cloudron.me kernel: IPTables Packet Dropped:
IN=eth0 OUT= MAC=04:01:9a:dd:a9:01:84:b5:9c:fa:08:30:08:00
SRC=124.6.36.197 DST=178.62.202.80 LEN=638 TOS=0x00 PREC=0x00 TTL=59
ID=61524 DF PROTO=TCP SPT=443 DPT=58535 WINDOW=95 RES=0x00 ACK PSH URGP=0
Jan 14 07:08:08 rudolf.cloudron.me systemd-timesyncd[448]: Timed out
waiting for reply from 85.255.214.66:123 (3.ubuntu.pool.ntp.org).
Jan 14 07:08:18 rudolf.cloudron.me systemd-timesyncd[448]: Timed out
waiting for reply from 83.98.201.134:123 (3.ubuntu.pool.ntp.org).
Jan 14 07:08:28 rudolf.cloudron.me systemd-timesyncd[448]: Timed out
waiting for reply from 194.171.167.130:123 (3.ubuntu.pool.ntp.org).
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr): gyp
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr):
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr): WARN
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr):
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr): install
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr):  got an error,
rolling back install

<...>

07:08:31 GMT installer:installer provision (stderr):  Error: connect
ETIMEDOUT 104.20.23.46:443
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr): gyp
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr):
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr): ERR!
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr):
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr): stack
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr):      at
Object.exports._errnoException (util.js:837:11)
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr): gyp
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr):
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr): ERR!
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr):
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr): stack
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr):      at
exports._exceptionWithHostPort (util.js:860:20)
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr): gyp
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr):
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr): ERR!
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr):
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr): stack
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr):      at
TCPConnectWrap.afterConnect [as oncomplete] (net.js:1060:14)
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr): gyp
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr):
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr): ERR!
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr):
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr): System Linux
3.19.0-31-generic
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: gyp ERR! command
"/usr/local/node-4.1.1/bin/node"
"/usr/local/node-4.1.1/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js"
"rebuild"
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: gyp ERR! cwd
/tmp/box-src-a4tklr/node_modules/ldapjs/node_modules/dtrace-provider
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: gyp ERR!
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr):
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr): node -v
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr):  v4.1.1
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr): gyp
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr):
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr): ERR!
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr):
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr): node-gyp -v
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr):  v3.0.3
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: gyp ERR! not ok
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr):
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr): npm
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr):
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr): ERR!
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr):  Linux
3.19.0-31-generic
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr): npm
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr):
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr): ERR!
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr):
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr): argv
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr):
"/usr/local/node-4.1.1/bin/node" "/usr/bin/npm" "rebuild"
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr): npm ERR! node v4.1.1
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: npm ERR! npm  v2.14.4
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: npm ERR! code ELIFECYCLE
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: npm ERR!
dtrace-provider@0.2.8 install: `node-gyp rebuild`
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: npm ERR! Exit status 1
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: npm ERR!
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: npm ERR! Failed at
the dtrace-provider@0.2.8 install script 'node-gyp rebuild'.
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: npm ERR!
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr):  This is most
likely a problem with the dtrace-provider package,
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: npm ERR! not with npm
itself.
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: npm ERR! Tell the
author that this fails on your system:
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: npm ERR!     node-gyp
rebuild
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: npm ERR! You can get
their info via:
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: npm ERR!     npm
owner ls dtrace-provider
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: npm ERR! There is
likely additional logging output above.
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr):
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr): npm
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr):
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr): ERR!
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision (stderr):  Please include the
following file with any support request:
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: npm ERR!
/tmp/box-src-a4tklr/npm-debug.log
Jan 14 07:08:31 rudolf.cloudron.me sudo[1284]: pam_unix(sudo:session):
session closed for user root
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: Thu, 14 Jan 2016
07:08:31 GMT installer:installer provision : child process exited. code:
1 signal: 0
Jan 14 07:08:31 rudolf.cloudron.me server.js[541]: [Error: Exited with
code 1]
2016-01-14 10:51:21 -08:00
Johannes Zellner
0601ea2f39 Show actionable notification in case we miss aws credentials
Fixes #553
2016-01-14 16:01:10 +01:00
Johannes Zellner
a92d4f2af7 add some comments on how to use notifications 2016-01-14 15:56:23 +01:00
Johannes Zellner
658305c969 Tweak the notification appearance 2016-01-14 15:54:32 +01:00
Johannes Zellner
8aed2be19b Support action scopes 2016-01-14 15:54:14 +01:00
Johannes Zellner
263f6e49d8 Use custom notification template 2016-01-14 15:53:47 +01:00
Johannes Zellner
d822e38016 Ensure templates are available 2016-01-14 15:53:32 +01:00
Johannes Zellner
6cf78f19bb Add custom notification template to make actionable 2016-01-14 15:53:16 +01:00
Johannes Zellner
4a3319406c Add Client api to show notifications 2016-01-14 15:08:27 +01:00
Johannes Zellner
cb4418b973 Set default values for the notifications 2016-01-14 15:07:41 +01:00
Johannes Zellner
5a797124b3 Update the angular notification library 2016-01-14 15:07:10 +01:00
Johannes Zellner
63430fbce6 Adjust update email text to let the user know about auto updates
So far the emails make no sense in case the user only clicks the update
link after the Cloudron has already auto updated. We might want to
adjust the text, once the user can disable auto updates, or set the
preferred update time.
2016-01-14 13:55:02 +01:00
girish@cloudron.io
bc2085139e ensure no trailing slash in redis password
Fixes #539
2016-01-13 19:42:28 -08:00
girish@cloudron.io
f98c710f5b use password generator module 2016-01-13 19:02:15 -08:00
girish@cloudron.io
eb7101deff setupToken is property of wizard
fixes #559
2016-01-13 18:41:08 -08:00
girish@cloudron.io
826f50da7e expose wizard in SetupController 2016-01-13 18:04:07 -08:00
girish@cloudron.io
4e94c8ea56 updateContact gets 202 and not 200 2016-01-13 16:46:01 -08:00
girish@cloudron.io
3120eca721 status api should set provider 2016-01-13 16:09:36 -08:00
girish@cloudron.io
26c9bcbc28 fix this and that 2016-01-13 15:00:33 -08:00
girish@cloudron.io
7a2e73a5d6 acme: update account with owner email
fixes #544
2016-01-13 14:21:59 -08:00
girish@cloudron.io
cd35ab5932 acme: update contact information before getting a cert
part of #544

there were two approaches considered:
1. pipe through owner email from appstore. this requires to save this
   value in settingsdb and we need to remember this in case user changes
   the email. another issue is that selfhost installer tooling needs to
   require this new value.

2. simply update owner email each time. this is the chosen approach.
2016-01-13 14:06:31 -08:00
girish@cloudron.io
efaacdb534 Add getOwner 2016-01-13 12:37:56 -08:00
girish@cloudron.io
5eb3c208f1 allow email to be configured 2016-01-13 12:15:27 -08:00
Johannes Zellner
8347b62c1b Mention lets encrypt in the ssl settings 2016-01-13 16:24:15 +01:00
Johannes Zellner
48c3c7b4dc Also hide the webadmin domain cert ui 2016-01-13 16:17:46 +01:00
Johannes Zellner
faefe078af Make links in app description go to a new page 2016-01-13 16:16:40 +01:00
Johannes Zellner
66eb0481b5 Hide app per app certificate upload fields
Fixes #555
2016-01-13 16:00:55 +01:00
Johannes Zellner
0a0fc130d4 Fix linter errors 2016-01-13 16:00:55 +01:00
Johannes Zellner
44afd7b657 No need to check for version equality for the update badge 2016-01-13 14:50:05 +01:00
Johannes Zellner
165b572a5f Fixup the app grid icon action layout 2016-01-13 14:49:00 +01:00
Johannes Zellner
1a30e622cc Only register app updates for apps where the available version is actually bigger
Fixes #533
2016-01-13 14:48:52 +01:00
Johannes Zellner
aec3238e42 Add changlog for 0.5.0 2016-01-13 11:51:21 +01:00
Johannes Zellner
249868dba7 Add CHANGES file 2016-01-13 11:51:13 +01:00
Johannes Zellner
f82e714b3c Remove executable flag for scripts not intended to be called directly 2016-01-13 10:58:20 +01:00
Johannes Zellner
baa7eae77c Move images script to appstore since it is specific to caas 2016-01-13 10:45:23 +01:00
Johannes Zellner
c69542a34d cleanup outdated README.md 2016-01-13 10:42:45 +01:00
Johannes Zellner
9d45892603 Move baseimage relevant scripts to baseimage/ 2016-01-13 10:30:01 +01:00
Johannes Zellner
e65f247b99 Fix path to initializeBaseUbuntuImage.sh in installer.sh 2016-01-12 16:53:36 +01:00
Johannes Zellner
600f061e47 The box tarball is now public, no need for a signed url 2016-01-12 16:50:34 +01:00
Johannes Zellner
8ac0e9e751 Provide sourceTarballUrl with update info 2016-01-12 16:50:14 +01:00
Johannes Zellner
772787fc22 Do not ignore scripts/ for export 2016-01-12 16:15:21 +01:00
Johannes Zellner
985b33b65b Add missing dev modules for images tool 2016-01-12 12:55:10 +01:00
Johannes Zellner
ef7c5c2d2b Remove vultr caas support 2016-01-12 12:55:10 +01:00
Johannes Zellner
0d49aafb54 Move installer/images scripts to scripts/ 2016-01-12 12:55:10 +01:00
Johannes Zellner
98aae5ddc6 Correctly copy installer files 2016-01-11 15:37:40 +01:00
Johannes Zellner
070e8606fa Use json from box/ 2016-01-11 15:20:29 +01:00
Johannes Zellner
511b2848c3 Fixup the paths in the createImage script 2016-01-11 15:14:07 +01:00
Johannes Zellner
7dd96d9a75 Fixup create box tarball script paths 2016-01-11 14:53:18 +01:00
Johannes Zellner
7d80f69ee8 Use the unified tarball instead of separate installer 2016-01-11 14:49:49 +01:00
Johannes Zellner
85491cb7b5 Merge branch 'selfhost' of ../installer into selfhost 2016-01-11 14:45:33 +01:00
Johannes Zellner
3c0b88a1ee Move to subfolder installer/ 2016-01-11 14:42:20 +01:00
Johannes Zellner
33c072f544 Support box and installer in one tarball 2016-01-11 14:25:04 +01:00
Johannes Zellner
a8d08bca3f Merge branch 'master' of ssh://gitlab.smartserver.io:6000/yellowtent/cloudron-installer 2016-01-11 12:53:52 +01:00
Johannes Zellner
d7ddc56ab3 Not applicable for ubuntu 15.10 2016-01-11 11:15:04 +01:00
Johannes Zellner
b562cd5c73 Fix typo when specifying the provisionEnv environment var 2016-01-08 13:15:07 +01:00
Johannes Zellner
2549a41eb3 Add a valid hostname entry in /etc/hosts if not found
Using 127.0.1.1 as per http://www.debian.org/doc/manuals/debian-reference/ch05.en.html#_the_hostname_resolution
2016-01-06 16:52:02 +01:00
Johannes Zellner
464f0fc231 add some unit tests for ensureVersion 2016-01-06 16:02:42 +01:00
Johannes Zellner
5914fd9fb7 Return the whole object, not just the string 2016-01-06 14:52:21 +01:00
Johannes Zellner
2e03217551 Make sysinfo provider selection explicit 2016-01-06 14:34:23 +01:00
Johannes Zellner
e0dc974f87 Add provider argument 2016-01-06 10:52:35 +01:00
Girish Ramakrishnan
fdf1ed829d Revert "use 15.10 as base image"
This reverts commit 50807f3046fdf715cb3bf2afc08436f64995f36a.

15.10 requires more work
2016-01-05 20:32:09 -08:00
Girish Ramakrishnan
ba90490ad9 Simply remove the old sudoers file that we installed
This is alternate fix to 743b8e757b
2016-01-05 20:24:05 -08:00
Girish Ramakrishnan
910be97f54 return calling callback 2016-01-05 16:16:25 -08:00
Girish Ramakrishnan
e162582045 Add collectd hack 2016-01-05 15:38:56 -08:00
Girish Ramakrishnan
cfd197d26c region is not required 2016-01-05 14:06:16 -08:00
Girish Ramakrishnan
aa0486bc2b use 15.10 as base image 2016-01-05 13:27:18 -08:00
Johannes Zellner
97a1fc62ae This rule is obsolete
It should protect the metadata from apps, but that is already
covered with the FORWARD dropping rule below
2016-01-05 21:13:45 +01:00
Johannes Zellner
fd9dcd065a Bring back the sleep 10 to wait for docker's iptable rules
See comment in code for further details
2016-01-05 21:11:43 +01:00
Johannes Zellner
59997560eb Do not remove all files from /etc/sudoers.d/
On DO with caas, there are no other files initially, but
on the ec2 ubuntu images, the files have set the rules for the
ubuntu user to be able to sudo without password, which we want to
keep
2016-01-05 17:06:01 +01:00
Johannes Zellner
026e71cc6e Default provider is caas 2016-01-05 15:58:16 +01:00
Johannes Zellner
98ecc24425 Allow metadata access for selfhosters for now 2016-01-05 15:57:50 +01:00
Johannes Zellner
3886006343 Implement ec2 sysinfo backend 2016-01-05 14:33:33 +01:00
Johannes Zellner
6d539c9203 Fix sysinfo api usage 2016-01-05 13:18:56 +01:00
Johannes Zellner
5f778e61dd Use new getIP() api in certificates.js 2016-01-05 12:23:07 +01:00
Johannes Zellner
9860489f05 Use new getIP() api in cloudron.js 2016-01-05 12:16:48 +01:00
Johannes Zellner
21ca8ac883 Use new getIP() api in apptask 2016-01-05 12:16:39 +01:00
Johannes Zellner
ec93becb17 Add missing asserts 2016-01-05 12:14:39 +01:00
Johannes Zellner
0319445888 Make sysinfo based on provider 2016-01-05 12:10:25 +01:00
Johannes Zellner
7cba9f50c8 Docker startup is fixed with new service file, no need to wait 2016-01-05 10:07:58 +01:00
Johannes Zellner
9483c3afbc Also support /dev/xvda1 with collectd, needed for ec2 2016-01-04 17:26:02 +01:00
Johannes Zellner
e518976534 Also support /dev/xvda for box-setup.sh which is used in ec2 2016-01-04 15:57:52 +01:00
Johannes Zellner
3626cc2394 Skip some code during tests for now 2016-01-02 16:58:27 +01:00
Johannes Zellner
3a61fc7181 We want all strings in the array as separate strings 2016-01-02 16:52:25 +01:00
Johannes Zellner
4a95fa5e87 Cleanup the installer script 2016-01-02 16:37:28 +01:00
Johannes Zellner
8e3d1422f3 Fix typo, linter could do some work ;-) 2016-01-02 15:39:48 +01:00
Johannes Zellner
640a0b2627 Try to get the latest box release if no sourceTarballUrl is specified in the provisioning data 2016-01-02 15:30:01 +01:00
Johannes Zellner
30e0cb6515 Upload box tarballs with public acl 2016-01-02 15:29:20 +01:00
Johannes Zellner
e3253aacdb Add semver 2016-01-02 15:28:49 +01:00
Johannes Zellner
32f49d2122 Use 10GB for system, since it now includes docker images 2016-01-01 16:29:16 +01:00
Johannes Zellner
b4bef44135 Do not put docker images into te btrfs volume 2016-01-01 16:10:16 +01:00
Johannes Zellner
ce38742caf Fix cloudron tests 2015-12-31 11:55:01 +01:00
Johannes Zellner
a7d39cc8d4 Merge branch 'selfhost' 2015-12-31 11:41:46 +01:00
Johannes Zellner
cb7eb660b9 Do not fail to list apps if we are in developer mode and do not have an appstore token 2015-12-31 10:42:02 +01:00
Johannes Zellner
bad28c60ae Do not rely on the VPS name but just get the memory from the system 2015-12-31 10:30:42 +01:00
Johannes Zellner
ab4c04085c Use image init script from within the installer tar ball 2015-12-31 10:01:08 +01:00
Johannes Zellner
761002f39d Include the image scripts in the installer tar 2015-12-31 09:37:55 +01:00
Johannes Zellner
996f9c7f5d Set letsencrypt as tls config 2015-12-31 09:31:50 +01:00
Johannes Zellner
a6eca44a0d Do not attempt to purchase an app if we dont have an appstore token 2015-12-31 09:15:27 +01:00
Girish Ramakrishnan
1820751801 show all fields 2015-12-30 19:48:10 -08:00
Johannes Zellner
030faaa5d1 Remove unused information within backup listing 2015-12-30 20:31:00 +01:00
Johannes Zellner
95e1947352 Merge branch 'selfhost' 2015-12-30 18:54:33 +01:00
Johannes Zellner
7deb11a0a6 Support s3 and route53 configs 2015-12-30 18:45:19 +01:00
Johannes Zellner
41c597801f Sort backups by creationTime 2015-12-30 18:31:44 +01:00
Johannes Zellner
128a138e74 Remove the backup prefix from the key 2015-12-30 18:30:41 +01:00
Johannes Zellner
ca74b5740a Partly implement backup listing for s3 backend 2015-12-30 18:21:38 +01:00
Johannes Zellner
9afcbe1565 Fix the email require detection in setup 2015-12-30 14:10:20 +01:00
Johannes Zellner
9e531a05e1 Do not ask for aws credentials for selfhosting 2015-12-30 13:28:14 +01:00
Johannes Zellner
7c3562cea2 Ensure focus and form validation is correct in the setup wizard 2015-12-29 22:10:10 +01:00
Johannes Zellner
1edddf79d2 token is not yet required for provisioning anymore 2015-12-29 19:47:13 +01:00
Johannes Zellner
114f03e434 'null' does not survive the shell script hopping well 2015-12-29 18:51:49 +01:00
Johannes Zellner
3ee1487985 We might support more than just caas and selfhosted 2015-12-29 17:57:11 +01:00
Johannes Zellner
a9eda2176e Only send heartbeats and fetch cloudron details if we have a token 2015-12-29 17:43:54 +01:00
Johannes Zellner
bb90bafb62 Fix crash when the appstore server does not respond correctly on setup 2015-12-29 16:07:04 +01:00
Johannes Zellner
cea8783fec Skip calling back home on activation in non caas case 2015-12-29 15:56:37 +01:00
Johannes Zellner
789d1fef84 Remove commented dom elements 2015-12-29 15:46:13 +01:00
Johannes Zellner
d83939b165 Fix setup wizard dependency on setupToken 2015-12-29 15:27:46 +01:00
Johannes Zellner
3a4ec5c86a The var is named 2015-12-29 12:25:22 +01:00
Johannes Zellner
013c14530b Provide provider in cloudron.conf 2015-12-29 11:30:03 +01:00
Johannes Zellner
ebf1cfc113 Read provider field from cloudron.conf 2015-12-29 11:29:08 +01:00
Johannes Zellner
ec4d04c338 Skip setupToken auth for non caas cloudrons 2015-12-29 11:24:45 +01:00
Johannes Zellner
584b7790e4 Get the cloudron vps provider from config 2015-12-29 11:24:34 +01:00
Johannes Zellner
ef25f66107 Ask for the user's email if not provided 2015-12-29 11:17:08 +01:00
Johannes Zellner
3060b34bdd customDomain during setup is a special caas case 2015-12-29 11:04:42 +01:00
Johannes Zellner
3fb5c682f8 Remove Client.isServerFirstTime() 2015-12-29 11:00:32 +01:00
Johannes Zellner
bb5dfa13ee Add Client.getStatus() 2015-12-29 10:58:26 +01:00
Johannes Zellner
9e391941c5 Redirect the browser to /setup.html in non caas mode 2015-12-29 10:54:00 +01:00
Johannes Zellner
8b1d3e5fba Send provider field with cloudron config 2015-12-29 10:53:22 +01:00
Girish Ramakrishnan
07e322df96 default targetBoxVersion to the maximum possible version ever
Apps that do not provide a targetBoxVersion are assumed to be
capable of running everywhere.
2015-12-27 16:43:50 -08:00
Girish Ramakrishnan
a8959cbf26 fix path to cpu metrics 2015-12-24 12:46:42 -08:00
Johannes Zellner
d793e5bae5 Remove obsolete generate_certificate.sh 2015-12-24 10:01:00 +01:00
Johannes Zellner
29954fa9e8 Generate certs inline 2015-12-24 10:00:45 +01:00
Girish Ramakrishnan
0384fa9a51 fix debugs 2015-12-23 15:22:36 -08:00
Girish Ramakrishnan
75b19d3883 scheduler: remove scheduler.json
don't bother saving state across restarts. needlessly complicated.
2015-12-23 14:27:26 -08:00
Girish Ramakrishnan
c15f84da08 scheduler: do not bother tracking containerIds 2015-12-23 13:29:00 -08:00
Girish Ramakrishnan
8539d4caf1 scheduler: delete containers by name
scheduler.json gets nuked during updates. When the box code restarts,
the scheduler is unable to remove old container because the state file
scheduler.json is now gone. It proceeds to create new container but that
does not work because of name conflict.

Fixes #531
2015-12-23 13:23:49 -08:00
Johannes Zellner
3eb1fe5e4b Ensure we reload the systemd daemon to pickup the new service files 2015-12-23 13:58:35 +01:00
Johannes Zellner
16b88b697e Ensure docker is running correctly 2015-12-23 13:54:49 +01:00
Johannes Zellner
08ba6ac831 Docker does not have a -d option anymore
This was depricated in 1.8 and is now gone
https://github.com/docker/docker/blob/master/CHANGELOG.md#cli
2015-12-23 13:32:37 +01:00
Johannes Zellner
49710618ff Docker does not have a -d option anymore
This was depricated in 1.8 and is now gone
https://github.com/docker/docker/blob/master/CHANGELOG.md#cli
2015-12-23 13:28:15 +01:00
Johannes Zellner
b4ba001617 Use multipart upload for s3 by reducing the chunk size
This avoids file upload issues for larger files
2015-12-23 13:27:54 +01:00
Johannes Zellner
87e0876cce Only pull infra images if we have an INFRA_VERSION file 2015-12-23 13:27:33 +01:00
Girish Ramakrishnan
87f5e3f102 workaround journalctl logging bug 2015-12-22 13:05:00 -08:00
Johannes Zellner
e921d0db6e do not rely on INFRA_VERSION to be present 2015-12-22 06:25:13 +01:00
Girish Ramakrishnan
b7a85580fa why is the linter not finding this again? 2015-12-21 16:14:30 -08:00
Johannes Zellner
2e93bc2e1d Generate self signed certs for now 2015-12-21 20:44:37 +01:00
Johannes Zellner
5ac15c8c49 Remove trailing slash 2015-12-21 15:06:44 +01:00
Johannes Zellner
98c05f3614 Remove comma 2015-12-21 15:06:44 +01:00
Johannes Zellner
05af56defc Fix typo 2015-12-20 14:58:05 +01:00
Johannes Zellner
ce48a2fc12 Some small fixes for selfhost 2015-12-20 12:01:18 +01:00
Johannes Zellner
01e910af79 Prepare provisioning data for installer 2015-12-20 11:12:59 +01:00
Johannes Zellner
3e2ce9e94c make cloudron.conf file path a 'const' 2015-12-20 10:25:12 +01:00
Johannes Zellner
9db602b274 Decide if we run in caas or selfhost mode and fetch user data accordingly 2015-12-20 10:23:25 +01:00
Johannes Zellner
a2d0ac7ee3 Run installer with selfhost flag 2015-12-20 10:22:55 +01:00
Girish Ramakrishnan
24cbd1a345 if i wrote a linter, these are the bugs it would catch 2015-12-19 13:48:14 -08:00
Girish Ramakrishnan
8b3e6742d5 better debugs 2015-12-19 13:47:48 -08:00
Johannes Zellner
62bd3f6e83 Add installer.sh 2015-12-19 21:56:33 +01:00
Johannes Zellner
20ac2ff6e7 Do not move ssh port in selfhosting case 2015-12-19 21:56:00 +01:00
Johannes Zellner
aa7c9e06a4 Initial commit 2015-12-19 18:47:24 +01:00
Johannes Zellner
0c2fb7c0d9 Use multipart upload for s3 by reducing the chunk size
This avoids file upload issues for larger files
2015-12-19 17:50:54 +01:00
Girish Ramakrishnan
7ec2b1da8c fix function name in debug 2015-12-17 20:30:30 -08:00
Girish Ramakrishnan
190c2b2756 firefox is unhappy with incorrect chain 2015-12-17 19:42:49 -08:00
Girish Ramakrishnan
7c975384cd better error messages 2015-12-17 19:35:52 -08:00
Girish Ramakrishnan
fe042891a3 Add acme.getCertificate 2015-12-17 13:31:28 -08:00
Girish Ramakrishnan
a9b594373d do not pass accountKeyPem everywhere 2015-12-17 13:27:10 -08:00
Girish Ramakrishnan
5edc3cde2a set prod option based on provider 2015-12-17 13:17:46 -08:00
Girish Ramakrishnan
a636731764 allow configuring prod/staging of LE url 2015-12-17 13:12:54 -08:00
Girish Ramakrishnan
b4433af9b5 remove unused require 2015-12-17 12:55:47 -08:00
Girish Ramakrishnan
72cc318607 install docker 1.9.1
We hit this error:
https://github.com/docker/docker/issues/18283
https://github.com/docker/docker/issues/17083
2015-12-15 17:17:28 -08:00
Girish Ramakrishnan
b533d325a4 Creating containers fails sporadically
HTTP code is 500 which indicates error: server error - Cannot start container redis-9d0ae0eb-a08f-4d0d-a980-ac6fa15d1a3d: [8] System error: write /sys/fs/cgroup/memory/system.slice/docker-fa6d6f3fce88f15844710e6ce4a8ac4d3a42e329437501416991b4c55ea3d078.scope/memory.memsw.limit_in_bytes: invalid argument

https://github.com/docker/docker/issues/16256
https://github.com/docker/docker/pull/17704
https://github.com/docker/docker/issues/17653
2015-12-15 15:02:45 -08:00
Girish Ramakrishnan
9dad7ff563 Fix sed 2015-12-15 14:43:01 -08:00
Girish Ramakrishnan
b389d30728 max-time is per retry. it cannot take more than 3 mins to download the tarball 2015-12-12 17:36:34 -08:00
Girish Ramakrishnan
606885b23c fix typo 2015-11-23 13:51:14 -08:00
Girish Ramakrishnan
bc7b8aadc4 vultr: fix waitForSnapshot call 2015-11-23 13:39:02 -08:00
Girish Ramakrishnan
d136b2065f ignore vultr transfer image call 2015-11-23 13:35:36 -08:00
Girish Ramakrishnan
3b2683463d localize transfer logic for DO 2015-11-23 13:35:05 -08:00
Girish Ramakrishnan
989730d402 wait for snapshot 2015-11-23 13:19:23 -08:00
Girish Ramakrishnan
50f7209ba2 print the provider 2015-11-23 12:46:08 -08:00
Girish Ramakrishnan
44b728c660 remove get_image_id api 2015-11-23 12:45:09 -08:00
Girish Ramakrishnan
9abc5bbf96 better error handling 2015-11-23 12:37:30 -08:00
Girish Ramakrishnan
56dd936e9c create systemd log dir if needed 2015-11-23 12:33:45 -08:00
Girish Ramakrishnan
e982281cd4 install acl 2015-11-23 11:32:05 -08:00
Girish Ramakrishnan
a6b7b5fa94 complete vultr backend 2015-11-23 11:30:24 -08:00
Girish Ramakrishnan
ef00114aab rename arg box to name 2015-11-23 11:20:21 -08:00
Girish Ramakrishnan
ba4edc5c0e implement some vultr api 2015-11-23 11:01:52 -08:00
Girish Ramakrishnan
dae2d81764 remove image_region as well 2015-11-23 10:49:09 -08:00
Girish Ramakrishnan
cee9cd14c0 hardcode the box size to smallest 2015-11-23 10:46:16 -08:00
Girish Ramakrishnan
f1ec110673 vultr: getSshKeyId 2015-11-23 10:27:27 -08:00
Girish Ramakrishnan
7104a3b738 use debug to put messages in stderr 2015-11-23 10:14:40 -08:00
Girish Ramakrishnan
114951b18c add get_image_id command 2015-11-23 09:31:42 -08:00
Girish Ramakrishnan
3c85a602a4 add vultr backend 2015-11-23 09:22:43 -08:00
Girish Ramakrishnan
a6415b8689 remove droplet from command names 2015-11-23 09:13:30 -08:00
Girish Ramakrishnan
b37670de84 make DO backend a binary 2015-11-23 08:59:45 -08:00
Girish Ramakrishnan
c9053bb0bc rename image creation schript 2015-11-23 08:39:21 -08:00
Girish Ramakrishnan
5362102be6 add --provider 2015-11-23 08:38:56 -08:00
Girish Ramakrishnan
bf4601470b remove functions not part of vps api 2015-11-23 08:33:57 -08:00
Girish Ramakrishnan
bd6274282b s/droplet/server 2015-11-23 08:32:54 -08:00
Girish Ramakrishnan
331b4d8524 use docker 1.9.0 2015-11-18 18:28:28 -08:00
Girish Ramakrishnan
1e19f68cb5 Install docker binaries instead of apt
The apt binaries lxc-* are obsolete and replaced with 'docker-engine'
packages. The new repos however do not allow pinning to a specific version.

so brain dead.

https://docs.docker.com/engine/installation/binaries/#get-the-linux-binary
2015-11-18 13:02:51 -08:00
Girish Ramakrishnan
c286b491d6 Fix debug output 2015-11-16 16:43:53 -08:00
Girish Ramakrishnan
c7acdbf20d add empty certs dir 2015-11-16 15:21:42 -08:00
Girish Ramakrishnan
3cd0cc01c4 Add test certs
This is simply a self-signed cert
2015-11-16 15:20:08 -08:00
Girish Ramakrishnan
87d109727a fix path to secrets 2015-11-16 15:08:26 -08:00
Girish Ramakrishnan
47b6819ec8 scp does not require this option 2015-11-16 14:48:05 -08:00
Girish Ramakrishnan
00ee89a693 fix paths 2015-11-16 14:31:39 -08:00
Girish Ramakrishnan
ac14b08af4 we have to use 4.1.1 2015-11-16 14:31:39 -08:00
Girish Ramakrishnan
db97d7e836 Fix options usage 2015-11-16 13:15:12 -08:00
Girish Ramakrishnan
5a0c80611e better error message 2015-11-16 12:15:44 -08:00
Girish Ramakrishnan
4e872865a3 use different keys for different env 2015-11-16 12:15:15 -08:00
Girish Ramakrishnan
aea39a83b6 change yellowtent key to caas 2015-11-16 12:12:07 -08:00
Girish Ramakrishnan
b9a3c508c9 Fix target path 2015-11-12 06:58:01 -08:00
Girish Ramakrishnan
9ae49e7169 link npm 2015-11-11 22:04:58 -08:00
Girish Ramakrishnan
7a1cdd62a4 install node 4.2.2 2015-11-11 16:02:42 -08:00
Girish Ramakrishnan
a242881101 change engine requirements 2015-11-11 15:56:14 -08:00
Girish Ramakrishnan
44ca59ac70 update shrinkwrap 2015-11-11 15:49:38 -08:00
Girish Ramakrishnan
398dfce698 update packages 2015-11-11 15:48:32 -08:00
Girish Ramakrishnan
0ebe6bde3d remove async and superagent from dev deps 2015-11-11 15:46:15 -08:00
Girish Ramakrishnan
15f686fc69 reboot automatically on panic after 5 seconds 2015-11-10 01:53:09 -08:00
Girish Ramakrishnan
6a0b8e0722 remove unused backup route 2015-11-03 16:52:38 -08:00
Girish Ramakrishnan
bb040eb5a8 give yellowtent user access to cloudron logs 2015-11-02 13:20:43 -08:00
Girish Ramakrishnan
51a0ad70aa log to journald instead 2015-11-02 08:54:10 -08:00
Girish Ramakrishnan
71faa5f89e Move ssh to port 202 2015-11-01 13:16:32 -08:00
Girish Ramakrishnan
6cf0554e23 do not resolve to a tag 2015-10-30 17:40:17 -07:00
Girish Ramakrishnan
fe0c1745e1 make it explicit that logrotate is run via cron in base system 2015-10-14 15:08:38 -07:00
Girish Ramakrishnan
2cdf73adab Use 20m of logs per app 2015-10-13 11:59:28 -07:00
Girish Ramakrishnan
31125dedc8 add deps of image tooling 2015-10-12 11:36:46 -07:00
Girish Ramakrishnan
a2e2d70660 remove deps of the release tool 2015-10-12 11:34:25 -07:00
Girish Ramakrishnan
7fe1b02ccd moved to release/ repo 2015-10-12 11:30:04 -07:00
Girish Ramakrishnan
04e27496bd remove tmpreaper (will use systemd for this) 2015-10-12 10:52:55 -07:00
Johannes Zellner
51e2e5ec9c Use a temporary file for the release tarball copy
The pipe failed for me a couple of times in a row
2015-10-12 19:37:33 +02:00
Johannes Zellner
cd9602e641 changes 0.0.69 2015-10-12 16:47:25 +02:00
Johannes Zellner
8303991217 Revert "install specific version of python"
The specific version does not create /usr/bin/python but only
the exact version /usr/bin/python2.7

The meta package python is the only one creating that link and
according to https://wiki.ubuntu.com/Python/3 /usr/bin/python
will not point to version 3 anytime soon at all.

This reverts commit be128bbecfd2afe9ef2bdca603a3b26e8ccced7b.
2015-10-12 15:02:05 +02:00
Girish Ramakrishnan
12eae2c002 remove 0.0.69 2015-10-11 16:03:08 -07:00
Girish Ramakrishnan
99489e5e77 install specific version of python 2015-10-11 10:02:44 -07:00
Johannes Zellner
5c9ff468cc Add python to the base image 2015-10-11 18:48:24 +02:00
Johannes Zellner
bf18307168 0.0.69 changes 2015-10-11 18:19:51 +02:00
Girish Ramakrishnan
b3fa76f8c5 0.0.68 changes 2015-10-10 14:39:53 -07:00
Girish Ramakrishnan
8a77242072 clear version field in rerelease 2015-10-09 13:11:53 -07:00
Girish Ramakrishnan
c453df55d6 More 0.0.67 changes 2015-10-09 12:05:03 -07:00
Girish Ramakrishnan
75d69050d5 0.0.67 changes 2015-10-09 10:06:11 -07:00
Girish Ramakrishnan
390285d9e5 version 0.0.66 changes 2015-10-08 13:16:23 -07:00
Girish Ramakrishnan
8ef15df7c0 0.0.65 changes 2015-10-07 18:49:53 -07:00
Girish Ramakrishnan
109f9567ea 0.0.64 changes 2015-09-29 13:21:33 -07:00
Girish Ramakrishnan
748eadd225 stop apps and installer when retiring cloudron
we cannot put this in stop.sh because that is called during update.
2015-09-29 11:58:14 -07:00
Girish Ramakrishnan
b8e115ddf6 move images script 2015-09-28 23:58:00 -07:00
Girish Ramakrishnan
11d4df4f7d fix loop (by actually using nextPage link) 2015-09-28 23:55:31 -07:00
Girish Ramakrishnan
0c285f21c1 rework images script 2015-09-28 23:47:13 -07:00
Girish Ramakrishnan
c9bf017637 0.0.63 changes 2015-09-28 17:00:50 -07:00
Johannes Zellner
0d78150f10 Log forwarding is no more 2015-09-28 14:33:06 +02:00
Johannes Zellner
f36946a8aa Forward the backup trigger status code and error message 2015-09-28 14:20:10 +02:00
Girish Ramakrishnan
5c51619798 Version 0.0.62 changes 2015-09-26 00:04:52 -07:00
Girish Ramakrishnan
a022bdb30d set default version to null to override commander built-in 2015-09-22 22:58:27 -07:00
Girish Ramakrishnan
2cfb91d0ce allow version to be specified in various commands 2015-09-22 22:55:55 -07:00
Girish Ramakrishnan
5885d76b89 Version 0.0.61 changes 2015-09-22 16:15:43 -07:00
Girish Ramakrishnan
5cb1a2d120 0.0.60 changes 2015-09-22 13:04:45 -07:00
Girish Ramakrishnan
53fa339363 0.0.59 changes 2015-09-21 21:56:16 -07:00
Girish Ramakrishnan
5f0bb0c6ce 0.0.58 changes 2015-09-21 16:26:03 -07:00
Girish Ramakrishnan
3dec6ac9f1 0.0.57 changes 2015-09-21 11:12:09 -07:00
Girish Ramakrishnan
4c9ec582dc 0.0.56 changes 2015-09-18 14:47:57 -07:00
Girish Ramakrishnan
28b000c820 admin tool is now merged into caas tool 2015-09-17 21:25:07 -07:00
Girish Ramakrishnan
30320e0ac6 Wait for backup to complete
Fixes #351
2015-09-17 16:44:44 -07:00
Girish Ramakrishnan
88b682a317 take ip as first argument instead of --ip 2015-09-17 16:40:06 -07:00
Girish Ramakrishnan
6c5a5c0882 remove tail-stream 2015-09-17 16:21:18 -07:00
Girish Ramakrishnan
1d27fffe44 remove ununsed requires 2015-09-17 16:21:03 -07:00
Girish Ramakrishnan
a3383b1f98 remove logs route
most of this stuff doesn't work anyways since we moved to systemd
2015-09-17 15:44:05 -07:00
Girish Ramakrishnan
f9c2b0acd1 remove cloudronLogin
alternative: admin/admin ssh <ip>
2015-09-17 15:34:17 -07:00
Girish Ramakrishnan
a9444ed879 rename login to ssh 2015-09-17 15:34:04 -07:00
Girish Ramakrishnan
8d5a3ecd69 admin: remove the cloudron chooser
This needlessly ties down this tool to digitalocean
2015-09-17 15:29:44 -07:00
Girish Ramakrishnan
44ff676eef store dates as iso strings 2015-09-17 13:48:20 -07:00
Girish Ramakrishnan
4bb017b740 verify next version exists 2015-09-17 12:11:14 -07:00
Girish Ramakrishnan
0f2435c308 Version 0.0.55 changes 2015-09-16 17:02:22 -07:00
Girish Ramakrishnan
0cd56f4d4c configure cloudron to use UTC
local timezone should be tracked by the webadmin/box code
2015-09-16 13:17:32 -07:00
Girish Ramakrishnan
e328ec2382 0.0.54 changes 2015-09-16 10:35:59 -07:00
Girish Ramakrishnan
4b5ac67993 Revert "Disable rsyslog"
This reverts commit 3c5db59de2b6c2ef8891ecba54496335b5e2d55f.

Don't revert this. Maybe some system services use this.
2015-09-16 09:48:47 -07:00
Girish Ramakrishnan
cb73218dfe Disable rsyslog 2015-09-15 14:32:47 -07:00
Girish Ramakrishnan
5523c2d34a Disable forwarding to syslog 2015-09-15 14:29:16 -07:00
Girish Ramakrishnan
01889c45a2 Fix typo 2015-09-15 14:04:30 -07:00
Girish Ramakrishnan
e89b4a151e 0.0.52 is folded into 0.0.53 2015-09-15 11:57:50 -07:00
Girish Ramakrishnan
ec235eafe8 fix staging with stripped releases 2015-09-15 11:25:03 -07:00
Girish Ramakrishnan
d99720258a gray out unreachable releases 2015-09-15 11:17:59 -07:00
Girish Ramakrishnan
3f064322e4 strip unreachable releases when processing 2015-09-15 11:11:56 -07:00
Girish Ramakrishnan
11592279e2 fix regexp for non-dev 2015-09-15 10:41:36 -07:00
Girish Ramakrishnan
31b4923eb2 better output 2015-09-15 10:33:36 -07:00
Girish Ramakrishnan
cfdfb9a907 release: add edit command 2015-09-15 09:48:05 -07:00
Girish Ramakrishnan
e7a21c821e 0.0.53 changes 2015-09-14 22:20:13 -07:00
Girish Ramakrishnan
255422d5be 0.0.52 changes 2015-09-14 17:28:31 -07:00
Girish Ramakrishnan
02b4990cb1 Version 0.0.51 changes 2015-09-14 12:24:22 -07:00
Johannes Zellner
11bf39374c 0.0.50 changes 2015-09-14 12:37:39 +02:00
Girish Ramakrishnan
04cf382de5 systemctl stop 2015-09-10 20:45:53 -07:00
Girish Ramakrishnan
38884bc0e6 0.0.49 changes 2015-09-10 11:40:58 -07:00
Girish Ramakrishnan
a4d0394d1a Revert "Move ssh to port 919"
This reverts commit 4e4890810f3a22e7ec990cc44381a2c243044d99.

This change is not done yet
2015-09-10 11:40:27 -07:00
Girish Ramakrishnan
8292e78ef2 Move ssh to port 919 2015-09-10 10:32:59 -07:00
Johannes Zellner
6243404d1d 0.0.48 changes 2015-09-10 14:39:30 +02:00
Girish Ramakrishnan
6fe67c93fe 0.0.47 changes 2015-09-09 17:03:13 -07:00
Girish Ramakrishnan
422b65d934 0.0.46 changes 2015-09-09 01:00:12 -07:00
Girish Ramakrishnan
2fa3a3c47e 0.0.45 changes 2015-09-08 12:58:06 -07:00
Girish Ramakrishnan
27e4810239 0.0.44 changes 2015-09-08 10:31:02 -07:00
Girish Ramakrishnan
cbdae3547b 0.0.43 changes 2015-09-07 23:13:14 -07:00
Girish Ramakrishnan
a5d122c0b3 Leave a note on singleshot After= behavior
"Actually oneshot is also a bit special and that is where RemainAfterExit comes in. For oneshot, systemd waits for the process to exit before it starts any follow-up units (and with multiple ExecStarts I assume it waits for all of them). So that automatically leads to the scheme in berbae's last post. However, with RemainAfterExit, the unit remains active even though the process has exited, so this makes it look more like "normal" service with "
2015-09-07 22:37:23 -07:00
Girish Ramakrishnan
47b662be09 Remove unnecessary alias 2015-09-07 20:53:26 -07:00
Girish Ramakrishnan
1ee09825a0 stop systemd target instead of supervisor 2015-09-07 20:11:19 -07:00
Girish Ramakrishnan
0a679da968 Type belongs to service 2015-09-07 14:10:34 -07:00
Girish Ramakrishnan
59d174004e box code has moved to systemd 2015-09-07 11:19:16 -07:00
Girish Ramakrishnan
d0d0d95475 0.0.42 changes 2015-09-05 09:21:59 -07:00
Johannes Zellner
b08a6840f5 changes for 0.0.41 2015-08-31 21:49:37 -07:00
Girish Ramakrishnan
77ada9c151 Copy upgrade flag 2015-08-31 19:23:43 -07:00
Girish Ramakrishnan
222e6b6611 0.0.40 changes 2015-08-31 09:32:12 -07:00
Girish Ramakrishnan
70c93c7be7 Provision only once 2015-08-30 21:10:57 -07:00
Girish Ramakrishnan
b73fc70ecf limit systemd journal size 2015-08-30 21:04:39 -07:00
Johannes Zellner
eab33150ad 0.0.39 changes 2015-08-30 17:23:47 -07:00
Girish Ramakrishnan
21c16d2009 0.0.38 changes 2015-08-28 11:21:43 -07:00
Girish Ramakrishnan
56413ecce6 Account for ext4 reserved space
2G - ram sawp
1G - backup swap
2G - reserved

root@yellowtent:/# du -hcs bin
13M bin
13M total
root@yellowtent:/# du -hcs boot
70M boot
70M total
root@yellowtent:/# du -hcs etc
6.3M    etc
6.3M    total
root@yellowtent:/# du -hcs lib
530M    lib
530M    total
root@yellowtent:/# du -hcs var
634M    var
634M    total
root@yellowtent:/# du -hcs root
33G root
33G total

Filesystem      Size  Used Avail Use% Mounted on
udev            990M     0  990M   0% /dev
tmpfs           201M  960K  200M   1% /run
/dev/vda1        40G   38G     0 100% /
tmpfs          1001M     0 1001M   0% /dev/shm
tmpfs           5.0M     0  5.0M   0% /run/lock
tmpfs          1001M     0 1001M   0% /sys/fs/cgroup
/dev/loop0       33G  7.9G   24G  25% /home/yellowtent/data
tmpfs           201M     0  201M   0% /run/user/0
2015-08-28 10:46:24 -07:00
Girish Ramakrishnan
e94f2a95de Remove ununsed checkData 2015-08-27 18:51:02 -07:00
Girish Ramakrishnan
c2a43b69a9 Just pass the req.body through
The metadata has things like restoreUrl and restoreKey which should not be
passed through anyways
2015-08-27 18:49:54 -07:00
Girish Ramakrishnan
c90e0fd21e do-resize resizes the disk it seems 2015-08-26 23:38:01 -07:00
Girish Ramakrishnan
6744621415 Print the detected ram and disk size 2015-08-26 22:54:23 -07:00
Girish Ramakrishnan
a6680a775f Start with 8gb instead 2015-08-26 16:02:51 -07:00
Girish Ramakrishnan
5a67be2292 Change ownership only of installer/ 2015-08-26 16:01:45 -07:00
Girish Ramakrishnan
c8b2b34138 Always resize the data volume 2015-08-26 15:40:48 -07:00
Girish Ramakrishnan
af8f4676ba systemd does not use /etc/default/docker 2015-08-26 15:32:40 -07:00
Girish Ramakrishnan
b51cb9d84a Remove redundant variable 2015-08-26 15:22:55 -07:00
Girish Ramakrishnan
ec7a61021f create single btrfs partition
apps and data consume space from the same btrfs partition now
2015-08-26 15:19:57 -07:00
Girish Ramakrishnan
8d0d19132e Add missing variable 2015-08-26 13:51:32 -07:00
Girish Ramakrishnan
d2bde5f0b1 Leave 5G for the system 2015-08-26 13:17:42 -07:00
Girish Ramakrishnan
3f9ae5d6bf refactor size calculation 2015-08-26 12:22:32 -07:00
Girish Ramakrishnan
9b97e26b58 Use fallocate everywhere 2015-08-26 12:15:47 -07:00
Girish Ramakrishnan
219032bbbb Dynamically size the home and docker partitions 2015-08-26 11:31:58 -07:00
Johannes Zellner
f0fd4ea45c Fixup tests after removing provisioning routes 2015-08-26 11:04:02 -07:00
Johannes Zellner
23a5a275f8 Reread box config from args.data 2015-08-26 11:04:02 -07:00
Girish Ramakrishnan
3a1bfa91d1 Create app and data partitions dynamically 2015-08-26 09:59:45 -07:00
Girish Ramakrishnan
a4f77dfcd0 Create systemd service to allocate swap
This can be used to make the swap creation dynamic based on the
ram in the cloudron
2015-08-26 09:11:05 -07:00
Girish Ramakrishnan
795ba3e365 Not sure why this is here 2015-08-26 00:07:15 -07:00
Girish Ramakrishnan
83ef4234bc Fix typo 2015-08-25 23:43:42 -07:00
Girish Ramakrishnan
b12b464462 Set RemainAfterExit
https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=750683
2015-08-25 23:43:04 -07:00
Girish Ramakrishnan
04726ba697 Restore iptables before docker 2015-08-25 23:27:44 -07:00
Girish Ramakrishnan
4d607ada9d Make installer a systemd service 2015-08-25 23:25:42 -07:00
Johannes Zellner
2c7cf9faa1 0.0.37 changes 2015-08-25 20:50:57 -07:00
Girish Ramakrishnan
7efa2fd072 Make update route work 2015-08-25 15:54:15 -07:00
Girish Ramakrishnan
9adf5167c9 Remove apiServerOrigin 2015-08-25 15:25:03 -07:00
Girish Ramakrishnan
4978984d75 Remove provision, restore, update routes 2015-08-25 15:09:35 -07:00
Girish Ramakrishnan
3b7ef4615a installer does not announce anymore 2015-08-25 15:06:39 -07:00
Girish Ramakrishnan
af8f4b64c0 Use userData from metadata API 2015-08-25 15:05:16 -07:00
Girish Ramakrishnan
93042d862d Fix CHANGES 2015-08-25 11:27:49 -07:00
Girish Ramakrishnan
ba5424c250 0.0.37 changes 2015-08-25 11:09:09 -07:00
Girish Ramakrishnan
afdde9b032 Disable forwarding from containers to metadata IP 2015-08-25 10:46:23 -07:00
Girish Ramakrishnan
a033480500 More 0.0.36 changes 2015-08-24 23:31:28 -07:00
Girish Ramakrishnan
14333e2910 Enable memory accounting 2015-08-24 22:34:51 -07:00
Girish Ramakrishnan
20df96b6ba Add v0.0.36 changes 2015-08-24 09:37:20 -07:00
Girish Ramakrishnan
a7729e1597 Add v0.0.35 changelog 2015-08-18 23:46:25 -07:00
Girish Ramakrishnan
29c3233375 0.0.34 changes 2015-08-17 10:12:09 -07:00
Girish Ramakrishnan
d0eab70974 0.0.33 changes 2015-08-13 16:06:52 -07:00
Girish Ramakrishnan
f3cbb91527 fetch the latest codes to generate the change log 2015-08-12 21:54:47 -07:00
Girish Ramakrishnan
9772cfe1f2 0.0.32 changes 2015-08-12 20:33:36 -07:00
Girish Ramakrishnan
3557e8a125 Use constants from the box repo 2015-08-12 19:57:01 -07:00
Girish Ramakrishnan
9e0bb6ca34 Use latest images 2015-08-12 19:04:37 -07:00
Johannes Zellner
bb74eef601 more changes for 0.0.31 2015-08-12 17:45:13 +02:00
Girish Ramakrishnan
d1db38ba8e Add 0.0.31 changes 2015-08-11 17:01:11 -07:00
Girish Ramakrishnan
e9af9fb16b 0.0.30 changes 2015-08-10 21:38:22 -07:00
Girish Ramakrishnan
fd74be8848 Update docker to 1.7.0 2015-08-10 19:17:21 -07:00
Girish Ramakrishnan
926fafd7f6 Add 0.0.29 changelog 2015-08-10 18:24:36 -07:00
Girish Ramakrishnan
c51c715cee Use systemctl 2015-08-10 17:57:24 -07:00
Girish Ramakrishnan
aa80210075 Update base image to 15.04 2015-08-10 17:57:24 -07:00
Johannes Zellner
768654ae63 Version 0.0.28 changelog 2015-08-10 15:33:47 +02:00
Girish Ramakrishnan
6efb291449 0.0.27 changes 2015-08-08 19:13:13 -07:00
Girish Ramakrishnan
7a431b9b83 0.0.26 changes 2015-08-06 14:07:07 -07:00
Johannes Zellner
c78d09df66 Do not install optional dependencies for production
This was needed due to the dtrace-provider failures as optional
deps for ldapjs and bunyan
2015-08-05 17:37:09 +02:00
Johannes Zellner
7d30d9e867 Use shrinkwrap instead of package.json for node module cache 2015-08-05 17:36:51 +02:00
Girish Ramakrishnan
d3fb244cef list ldap as 0.0.25 change 2015-08-04 16:29:49 -07:00
159 changed files with 12694 additions and 3755 deletions

7
.gitattributes vendored
View File

@@ -1,6 +1,7 @@
# Skip files when using git archive
# following files are skipped when exporting using git archive
/release export-ignore
/admin export-ignore
test export-ignore
.gitattributes export-ignore
.gitignore export-ignore
/scripts export-ignore
test export-ignore

2
.gitignore vendored
View File

@@ -3,7 +3,9 @@ coverage/
docs/
webadmin/dist/
setup/splash/website/
installer/src/certs/server.key
# vim swap files
*.swp

441
CHANGES Normal file
View File

@@ -0,0 +1,441 @@
[0.0.1]
- Hot Chocolate
[0.0.2]
- Hotfix appstore ui in webadim
[0.0.3]
- Tall Pike
[0.0.4]
- This will be 0.0.4 changes
[0.0.5]
- App install/configure route fixes
[0.0.6]
- Not sure what happenned here
[0.0.7]
- resetToken is now sent as part of create user
- Same as 0.0.7 which got released by mistake
[0.0.8]
- Manifest changes
[0.0.9]
- Fix app restore
- Fix backup issues
[0.0.10]
- Unknown orchestra
[0.0.11]
- Add ldap addon
[0.0.12]
- Support OAuth2 state
[0.0.13]
- Use docker image from cloudron repository
[0.0.14]
- Improve setup flow
[0.0.15]
- Improved Appstore view
[0.0.16]
- Improved Backup approach
[0.0.17]
- Upgrade testing
- App auto updates
- Usage graphs
[0.0.18]
- Rework backups and updates
[0.0.19]
- Graphite fixes
- Avatar and Cloudron name support
[0.0.20]
- Apptask fixes
- Chrome related fixes
[0.0.21]
- Increase nginx hostname size to 64
[0.0.22]
- Testing the e2e tests
[0.0.23]
- Better error status page
- Fix updater and backup progress reporting
- New avatar set
- Improved setup wizard
[0.0.24]
- Hotfix the ldap support
[0.0.25]
- Add support page
- Really fix ldap issues
[0.0.26]
- Add configurePath support
[0.0.27]
- Improved log collector
[0.0.28]
- Improve app feedback
- Restyle login page
[0.0.29]
- Update to ubuntu 15.04
[0.0.30]
- Move to docker 1.7
[0.0.31]
- WARNING: This update restarts your containers
- System processes are prioritized over apps
- Add ldap group support
[0.0.32]
- MySQL addon update
[0.0.33]
- Fix graphs
- Fix MySQL 5.6 memory usage
[0.0.34]
- Correctly mark apps pending for approval
[0.0.35]
- Fix ldap admin group username
[0.0.36]
- Fix restore without backup
- Optimize image deletion during updates
- Add memory accounting
- Restrict access to metadata from containers
[0.0.37]
- Prepare for Selfhosting 1. part
- Use userData instead of provisioning calls
[0.0.38]
- Account for Ext4 reserved block when partitioning disk
[0.0.39]
- Move subdomain management to the cloudron
[0.0.40]
- Add journal limit
- Fix reprovisioning on reboot
- Fix subdomain management during startup
[0.0.41]
- Finally bring things to a sane state
[0.0.42]
- Parallel apptask
[0.0.43]
- Move to systemd
[0.0.44]
- Fix apptask concurrency bug
[0.0.45]
- Retry subdomain registration
[0.0.46]
- Fix app update email notification
[0.0.47]
- Ensure box code quits within 5 seconds
[0.0.48]
- Styling fixes
- Improved session handling
[0.0.49]
- Fix app autoupdate logic
[0.0.50]
- Use domainmanagement via CaaS
[0.0.51]
- Fix memory management
[0.0.52]
- Restrict addons memory
- Get nofication about container OOMs
[0.0.53]
- Restrict addons memory
- Get notification about container OOMs
- Add retry to subdomain logic
[0.0.54]
- OAuth Proxy now uses internal port forwarding
[0.0.55]
- Setup cloudron timezone based on droplet region
[0.0.56]
- Use correct timezone in updater
[0.0.57]
- Fix systemd logging issues
[0.0.58]
- Ensure backups of failed apps are retained across archival cycles
[0.0.59]
- Installer API fixes
[0.0.60]
- Do full box backup on updates
[0.0.61]
- Track update notifications to inform admin only once
[0.0.62]
- Export bind dn and password from LDAP addon
[0.0.63]
- Fix creation of TXT records
[0.0.64]
- Stop apps in a retired cloudron
- Retry downloading application on failure
[0.0.65]
- Do not send crash mails for apps in development
[0.0.66]
- Readonly application and addon containers
[0.0.67]
- Fix email notifications
- Fix bug when restoring from certain backups
[0.0.68]
- Update graphite image
- Add simpleauth addon support
[0.0.69]
- Support newer manifest format
- Fix app listing rendering in chrome
- Fix redis backup across upgrades
[0.0.70]
- Retry app download on error
[0.0.71]
- Fix oauth and simple auth login
[0.0.72]
- Cleanup application volumes periodically
- New application logging design
[0.0.73]
- Update SSL certificate
[0.0.74]
- Support singleUser apps
[0.0.75]
- scheduler addon
[0.0.76]
- DNS Sync fixes
- Show warning to user when memory limit reached
[0.0.77]
- Do not set hostname in app containers
[0.0.78]
- Support custom domains
[0.0.79]
- Move SSH Port
[0.0.80]
- Use journalctl for container logs
[0.1.0]
- Wait for configuration changes before starting Cloudron
[0.1.1]
- Ensure dns config for all cloudrons
[0.1.2]
- Make email work again
- Add DKIM keys for custom domains
[0.1.3]
- Storage backend
[0.1.4]
- CaaS Backup configuration fix
[0.1.5]
- Use correct tokens for DNS backend
[0.1.6]
- Add hook to determine the api server of the box
- Fix crash notification
[0.2.0]
- New cloudron exec implementation
[0.2.1]
- Update to node 4.1.1
- Fix certification installation with custom domains
[0.2.2]
- Better debug output
- Retry more times if docker registry goes down
[0.3.0]
- Update SSH keys
- Allow bigger manifest files
[0.4.0]
- Update to docker 1.9.0
[0.4.1]
- Fix scheduler crash
- Crucial OAuth fixes
[0.4.2]
- Fix crash when reporting backup error
- Allow larger manifests
[0.4.3]
- Fix cloudron exec
[0.4.4]
- Initial Lets Encrypt integration
[0.4.5]
- Fixup nginx configuration to allow dynamic certificates
[0.4.6]
- LetsEncrypt integration for custom domains
- Rate limit crash emails
[0.5.0]
- Enable staging Lets Encrypt Integration
[0.5.1]
- Display error dialog for app installation errors
- Enable prod Lets Encrypt Integration
- Handle apptask crashes correctly
[0.5.2]
- Fix apphealthtask crash
- Use cgroup fs driver instead of systemd cgroup driver in docker
[0.5.3]
- Changes for e2e testing
[0.5.4]
- Fix bug in LE server selection
[0.5.5]
- Scheduler redesign
- Fix journalctl logging
[0.5.6]
- Prepare for selfhosting option
[0.5.7]
- Move app images off the btrfs subvolume
[0.6.0]
- Consolidate code repositories
[0.6.1]
- Use no-reply as email from address for apps in naked domains
- Update Lets Encrypt account with owner email when available
- Fix email templates to indicate auto update
- Add notification UI
[0.6.2]
- Fix `cloudron exec` container to have same namespaces as app
- Add developmentMode to manifest
[0.6.3]
- Make sending invite for new users optional
[0.6.4]
- Add support for display names
- Send invite links to admins for user setup
- Enforce stronger passwords
[0.6.5]
- Finalize stronger password requirement
[0.7.0]
- Upgrade to 15.10
- Do not remove docker images when in use by another container
- Fix sporadic error when reconfiguring apps
- Handle journald crashes gracefully
[0.7.1]
- Allow admins to edit users
- Fix graphs
- Support more LDAP cases
- Allow appstore deep linking
[0.7.2]
- Fix 5xx errors when password does not meet requirements
- Improved box update management using prereleases
- Less aggressive disk space checks
[0.8.0]
- MySQL addon : multiple database support
[0.8.1]
- Set Host HTTP header when querying healthCheckPath
- Show application Changelog in app update emails
[0.9.0]
- Fix bug in multdb mysql addon backup
- Add initial user group support
- Improved app memory limit handling
[0.9.1]
- Introduce per app group access control
[0.9.2]
- Fix bug where reconfiguring apps would trigger memory limit warning
- Allow more apps to be installed in bigger sized cloudrons
- Allow user to override memory limit warning and install anyway
[0.9.3]
- Admin flag is handled outside of groups
- User interface fixes for groups
- Allow to set access restrictions on app installation
[0.10.0]
- Upgrade to docker 1.10.2
- Fix MySQL addon to handle heavier loads
- Allow listing and download of backups (using the CLI tool)
- Ubuntu security updates till 8th March 2016 (http://www.ubuntu.com/usn)
[0.10.1]
- Fix Let's Encrypt certificate renewal
[0.10.2]
- Apps can now bind with username or email with LDAP
- Disallow updating an app with mismatching manifest id
- Use admin domain instead of naked domain in the SPF record
- Download Lets Encrypt intermediate cert

View File

@@ -1,10 +1,17 @@
The Box
=======
Cloudron a Smart Server
=======================
Development setup
-----------------
* sudo useradd -m yellowtent
** Add admin-localhost as 127.0.0.1 in /etc/hosts
** All apps will be installed as hypened-subdomains of localhost. You should add
hyphened-subdomains of your apps into /etc/hosts
Selfhost Instructions
---------------------
The smart server currently relies on an AWS account with access to Route53 and S3 and is tested on DigitalOcean and EC2.
First create a virtual private server with Ubuntu 15.04 and run the following commands in an ssh session to initialize the base image:
```
curl https://s3.amazonaws.com/prod-cloudron-releases/installer.sh -o installer.sh
chmod +x installer.sh
./installer.sh <domain> <aws access key> <aws acccess secret> <backup bucket> <provider> <release sha1>
```

186
baseimage/createImage Executable file
View File

@@ -0,0 +1,186 @@
#!/bin/bash
set -eu -o pipefail
assertNotEmpty() {
: "${!1:? "$1 is not set."}"
}
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)"
export JSON="${SOURCE_DIR}/node_modules/.bin/json"
provider="digitalocean"
installer_revision=$(git rev-parse HEAD)
box_name=""
server_id=""
server_ip=""
destroy_server="yes"
deploy_env="dev"
# Only GNU getopt supports long options. OS X comes bundled with the BSD getopt
# brew install gnu-getopt to get the GNU getopt on OS X
[[ $(uname -s) == "Darwin" ]] && GNU_GETOPT="/usr/local/opt/gnu-getopt/bin/getopt" || GNU_GETOPT="getopt"
readonly GNU_GETOPT
args=$(${GNU_GETOPT} -o "" -l "provider:,revision:,regions:,size:,name:,no-destroy,env:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--env) deploy_env="$2"; shift 2;;
--revision) installer_revision="$2"; shift 2;;
--provider) provider="$2"; shift 2;;
--name) box_name="$2"; destroy_server="no"; shift 2;;
--no-destroy) destroy_server="no"; shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
echo "Creating image using ${provider}"
if [[ "${provider}" == "digitalocean" ]]; then
if [[ "${deploy_env}" == "staging" ]]; then
assertNotEmpty DIGITAL_OCEAN_TOKEN_STAGING
export DIGITAL_OCEAN_TOKEN="${DIGITAL_OCEAN_TOKEN_STAGING}"
elif [[ "${deploy_env}" == "dev" ]]; then
assertNotEmpty DIGITAL_OCEAN_TOKEN_DEV
export DIGITAL_OCEAN_TOKEN="${DIGITAL_OCEAN_TOKEN_DEV}"
elif [[ "${deploy_env}" == "prod" ]]; then
assertNotEmpty DIGITAL_OCEAN_TOKEN_PROD
export DIGITAL_OCEAN_TOKEN="${DIGITAL_OCEAN_TOKEN_PROD}"
else
echo "No such env ${deploy_env}."
exit 1
fi
vps="/bin/bash ${SCRIPT_DIR}/digitalocean.sh"
else
echo "Unknown provider : ${provider}"
exit 1
fi
readonly ssh_keys="${HOME}/.ssh/id_rsa_caas_${deploy_env}"
readonly scp202="scp -P 202 -o ConnectTimeout=10 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
readonly scp22="scp -o ConnectTimeout=10 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
readonly ssh202="ssh -p 202 -o IdentitiesOnly=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
readonly ssh22="ssh -o IdentitiesOnly=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
if [[ ! -f "${ssh_keys}" ]]; then
echo "caas ssh key is missing at ${ssh_keys} (pick it up from secrets repo)"
exit 1
fi
function get_pretty_revision() {
local git_rev="$1"
local sha1=$(git rev-parse --short "${git_rev}" 2>/dev/null)
echo "${sha1}"
}
now=$(date "+%Y-%m-%d-%H%M%S")
pretty_revision=$(get_pretty_revision "${installer_revision}")
if [[ -z "${box_name}" ]]; then
# if you change this, change the regexp is appstore/janitor.js
box_name="box-${deploy_env}-${pretty_revision}-${now}" # remove slashes
# create a new server if no name given
if ! caas_ssh_key_id=$($vps get_ssh_key_id "caas"); then
echo "Could not query caas ssh key"
exit 1
fi
echo "Detected caas ssh key id: ${caas_ssh_key_id}"
echo "Creating Server with name [${box_name}]"
if ! server_id=$($vps create ${caas_ssh_key_id} ${box_name}); then
echo "Failed to create server"
exit 1
fi
echo "Created server with id: ${server_id}"
# If we run scripts overenthusiastically without the wait, setup script randomly fails
echo -n "Waiting 120 seconds for server creation"
for i in $(seq 1 24); do
echo -n "."
sleep 5
done
echo ""
else
if ! server_id=$($vps get_id "${box_name}"); then
echo "Could not determine id from name"
exit 1
fi
echo "Reusing server with id: ${server_id}"
$vps power_on "${server_id}"
fi
# Query until we get an IP
while true; do
echo "Trying to get the server IP"
if server_ip=$($vps get_ip "${server_id}"); then
echo "Server IP : [${server_ip}]"
break
fi
echo "Timedout, trying again in 10 seconds"
sleep 10
done
while true; do
echo "Trying to copy init script to server"
if $scp22 "${SCRIPT_DIR}/initializeBaseUbuntuImage.sh" root@${server_ip}:.; then
break
fi
echo "Timedout, trying again in 30 seconds"
sleep 30
done
echo "Copying INFRA_VERSION"
$scp22 "${SCRIPT_DIR}/../setup/INFRA_VERSION" root@${server_ip}:.
echo "Copying box source"
cd "${SOURCE_DIR}"
git archive --format=tar HEAD | $ssh22 "root@${server_ip}" "cat - > /tmp/box.tar.gz"
echo "Executing init script"
if ! $ssh22 "root@${server_ip}" "/bin/bash /root/initializeBaseUbuntuImage.sh ${installer_revision}"; then
echo "Init script failed"
exit 1
fi
echo "Shutting down server with id : ${server_id}"
$ssh202 "root@${server_ip}" "shutdown -f now" || true # shutdown sometimes terminates ssh connection immediately making this command fail
# wait 10 secs for actual shutdown
echo "Waiting for 10 seconds for server to shutdown"
sleep 30
echo "Powering off server"
if ! $vps power_off "${server_id}"; then
echo "Could not power off server"
exit 1
fi
snapshot_name="box-${deploy_env}-${pretty_revision}-${now}"
echo "Snapshotting as ${snapshot_name}"
if ! image_id=$($vps snapshot "${server_id}" "${snapshot_name}"); then
echo "Could not snapshot and get image id"
exit 1
fi
if [[ "${destroy_server}" == "yes" ]]; then
echo "Destroying server"
if ! $vps destroy "${server_id}"; then
echo "Could not destroy server"
exit 1
fi
else
echo "Skipping server destroy"
fi
echo "Transferring image ${image_id} to other regions"
$vps transfer_image_to_all_regions "${image_id}"
echo "Done."

240
baseimage/digitalocean.sh Normal file
View File

@@ -0,0 +1,240 @@
#!/bin/bash
if [[ -z "${DIGITAL_OCEAN_TOKEN}" ]]; then
echo "Script requires DIGITAL_OCEAN_TOKEN env to be set"
exit 1
fi
if [[ -z "${JSON}" ]]; then
echo "Script requires JSON env to be set to path of JSON binary"
exit 1
fi
readonly CURL="curl -s -u ${DIGITAL_OCEAN_TOKEN}:"
function debug() {
echo "$@" >&2
}
function get_ssh_key_id() {
id=$($CURL "https://api.digitalocean.com/v2/account/keys" \
| $JSON ssh_keys \
| $JSON -c "this.name === \"$1\"" \
| $JSON 0.id)
[[ -z "$id" ]] && exit 1
echo "$id"
}
function create_droplet() {
local ssh_key_id="$1"
local box_name="$2"
local image_region="sfo1"
local ubuntu_image_slug="ubuntu-15-10-x64"
local box_size="512mb"
local data="{\"name\":\"${box_name}\",\"size\":\"${box_size}\",\"region\":\"${image_region}\",\"image\":\"${ubuntu_image_slug}\",\"ssh_keys\":[ \"${ssh_key_id}\" ],\"backups\":false}"
id=$($CURL -X POST -H 'Content-Type: application/json' -d "${data}" "https://api.digitalocean.com/v2/droplets" | $JSON droplet.id)
[[ -z "$id" ]] && exit 1
echo "$id"
}
function get_droplet_ip() {
local droplet_id="$1"
ip=$($CURL "https://api.digitalocean.com/v2/droplets/${droplet_id}" | $JSON "droplet.networks.v4[0].ip_address")
[[ -z "$ip" ]] && exit 1
echo "$ip"
}
function get_droplet_id() {
local droplet_name="$1"
id=$($CURL "https://api.digitalocean.com/v2/droplets?per_page=100" | $JSON "droplets" | $JSON -c "this.name === '${droplet_name}'" | $JSON "[0].id")
[[ -z "$id" ]] && exit 1
echo "$id"
}
function power_off_droplet() {
local droplet_id="$1"
local data='{"type":"power_off"}'
local response=$($CURL -X POST -H 'Content-Type: application/json' -d "${data}" "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions")
local event_id=`echo "${response}" | $JSON action.id`
if [[ -z "${event_id}" ]]; then
debug "Got no event id, assuming already powered off."
debug "Response: ${response}"
return
fi
debug "Powered off droplet. Event id: ${event_id}"
debug -n "Waiting for droplet to power off"
while true; do
local event_status=`$CURL "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions/${event_id}" | $JSON action.status`
if [[ "${event_status}" == "completed" ]]; then
break
fi
debug -n "."
sleep 10
done
debug ""
}
function power_on_droplet() {
local droplet_id="$1"
local data='{"type":"power_on"}'
local event_id=`$CURL -X POST -H 'Content-Type: application/json' -d "${data}" "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions" | $JSON action.id`
debug "Powered on droplet. Event id: ${event_id}"
if [[ -z "${event_id}" ]]; then
debug "Got no event id, assuming already powered on"
return
fi
debug -n "Waiting for droplet to power on"
while true; do
local event_status=`$CURL "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions/${event_id}" | $JSON action.status`
if [[ "${event_status}" == "completed" ]]; then
break
fi
debug -n "."
sleep 10
done
debug ""
}
function get_image_id() {
local snapshot_name="$1"
local image_id=""
image_id=$($CURL "https://api.digitalocean.com/v2/images?per_page=100" \
| $JSON images \
| $JSON -c "this.name === \"${snapshot_name}\"" 0.id)
if [[ -n "${image_id}" ]]; then
echo "${image_id}"
fi
}
function snapshot_droplet() {
local droplet_id="$1"
local snapshot_name="$2"
local data="{\"type\":\"snapshot\",\"name\":\"${snapshot_name}\"}"
local event_id=`$CURL -X POST -H 'Content-Type: application/json' -d "${data}" "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions" | $JSON action.id`
debug "Droplet snapshotted as ${snapshot_name}. Event id: ${event_id}"
debug -n "Waiting for snapshot to complete"
while true; do
local event_status=`$CURL "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions/${event_id}" | $JSON action.status`
if [[ "${event_status}" == "completed" ]]; then
break
fi
debug -n "."
sleep 10
done
debug ""
get_image_id "${snapshot_name}"
}
function destroy_droplet() {
local droplet_id="$1"
# TODO: check for 204 status
$CURL -X DELETE "https://api.digitalocean.com/v2/droplets/${droplet_id}"
debug "Droplet destroyed"
debug ""
}
function transfer_image() {
local image_id="$1"
local region_slug="$2"
local data="{\"type\":\"transfer\",\"region\":\"${region_slug}\"}"
local event_id=`$CURL -X POST -H 'Content-Type: application/json' -d "${data}" "https://api.digitalocean.com/v2/images/${image_id}/actions" | $JSON action.id`
echo "${event_id}"
}
function wait_for_image_event() {
local image_id="$1"
local event_id="$2"
debug -n "Waiting for ${event_id}"
while true; do
local event_status=`$CURL "https://api.digitalocean.com/v2/images/${image_id}/actions/${event_id}" | $JSON action.status`
if [[ "${event_status}" == "completed" ]]; then
break
fi
debug -n "."
sleep 10
done
debug ""
}
function transfer_image_to_all_regions() {
local image_id="$1"
xfer_events=()
image_regions=(ams3) ## sfo1 is where the image is created
for image_region in ${image_regions[@]}; do
xfer_event=$(transfer_image ${image_id} ${image_region})
echo "Image transfer to ${image_region} initiated. Event id: ${xfer_event}"
xfer_events+=("${xfer_event}")
sleep 1
done
echo "Image transfer initiated, but they will take some time to get transferred."
for xfer_event in ${xfer_events[@]}; do
$vps wait_for_image_event "${image_id}" "${xfer_event}"
done
}
if [[ $# -lt 1 ]]; then
debug "<command> <params...>"
exit 1
fi
case $1 in
get_ssh_key_id)
get_ssh_key_id "${@:2}"
;;
create)
create_droplet "${@:2}"
;;
get_id)
get_droplet_id "${@:2}"
;;
get_ip)
get_droplet_ip "${@:2}"
;;
power_on)
power_on_droplet "${@:2}"
;;
power_off)
power_off_droplet "${@:2}"
;;
snapshot)
snapshot_droplet "${@:2}"
;;
destroy)
destroy_droplet "${@:2}"
;;
transfer_image_to_all_regions)
transfer_image_to_all_regions "${@:2}"
;;
*)
echo "Unknown command $1"
exit 1
esac

View File

@@ -0,0 +1,326 @@
#!/bin/bash
set -euv -o pipefail
readonly USER=yellowtent
readonly USER_HOME="/home/${USER}"
readonly INSTALLER_SOURCE_DIR="${USER_HOME}/installer"
readonly INSTALLER_REVISION="$1"
readonly SELFHOSTED=$(( $# > 1 ? 1 : 0 ))
readonly USER_DATA_FILE="/root/user_data.img"
readonly USER_DATA_DIR="/home/yellowtent/data"
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
function die {
echo $1
exit 1
}
[[ "$(systemd --version 2>&1)" == *"systemd 225"* ]] || die "Expecting systemd to be 225"
if [ -f "${SOURCE_DIR}/INFRA_VERSION" ]; then
source "${SOURCE_DIR}/INFRA_VERSION"
else
echo "No INFRA_VERSION found, skip pulling docker images"
fi
if [ ${SELFHOSTED} == 0 ]; then
echo "!! Initializing Ubuntu image for CaaS"
else
echo "!! Initializing Ubuntu image for Selfhosting"
fi
echo "==== Create User ${USER} ===="
if ! id "${USER}"; then
useradd "${USER}" -m
fi
echo "=== Yellowtent base image preparation (installer revision - ${INSTALLER_REVISION}) ==="
echo "=== Prepare installer source ==="
rm -rf "${INSTALLER_SOURCE_DIR}" && mkdir -p "${INSTALLER_SOURCE_DIR}"
rm -rf /tmp/box && mkdir -p /tmp/box
tar xvf /tmp/box.tar.gz -C /tmp/box && rm /tmp/box.tar.gz
cp -rf /tmp/box/installer/* "${INSTALLER_SOURCE_DIR}"
echo "${INSTALLER_REVISION}" > "${INSTALLER_SOURCE_DIR}/REVISION"
export DEBIAN_FRONTEND=noninteractive
echo "=== Upgrade ==="
apt-get update
apt-get dist-upgrade -y
apt-get install -y curl
# Setup firewall before everything. docker creates it's own chain and the -X below will remove it
# Do NOT use iptables-persistent because it's startup ordering conflicts with docker
echo "=== Setting up firewall ==="
# clear tables and set default policy
iptables -F # flush all chains
iptables -X # delete all chains
# default policy for filter table
iptables -P INPUT DROP
iptables -P FORWARD ACCEPT # TODO: disable icc and make this as reject
iptables -P OUTPUT ACCEPT
# NOTE: keep these in sync with src/apps.js validatePortBindings
# allow ssh, http, https, ping, dns
iptables -I INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
if [ ${SELFHOSTED} == 0 ]; then
iptables -A INPUT -p tcp -m tcp -m multiport --dports 80,202,443,886 -j ACCEPT
else
iptables -A INPUT -p tcp -m tcp -m multiport --dports 80,22,443,886 -j ACCEPT
fi
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
iptables -A INPUT -p icmp --icmp-type echo-reply -j ACCEPT
iptables -A INPUT -p udp --sport 53 -j ACCEPT
iptables -A INPUT -s 172.17.0.0/16 -j ACCEPT # required to accept any connections from apps to our IP:<public port>
# loopback
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT
# prevent DoS
# iptables -A INPUT -p tcp --dport 80 -m limit --limit 25/minute --limit-burst 100 -j ACCEPT
# log dropped incoming. keep this at the end of all the rules
iptables -N LOGGING # new chain
iptables -A INPUT -j LOGGING # last rule in INPUT chain
iptables -A LOGGING -m limit --limit 2/min -j LOG --log-prefix "IPTables Packet Dropped: " --log-level 7
iptables -A LOGGING -j DROP
echo "==== Install btrfs tools ==="
apt-get -y install btrfs-tools
echo "==== Install docker ===="
# install docker from binary to pin it to a specific version. the current debian repo does not allow pinning
curl https://get.docker.com/builds/Linux/x86_64/docker-1.10.2 > /usr/bin/docker
apt-get -y install aufs-tools
chmod +x /usr/bin/docker
groupadd docker
cat > /etc/systemd/system/docker.socket <<EOF
[Unit]
Description=Docker Socket for the API
PartOf=docker.service
[Socket]
ListenStream=/var/run/docker.sock
SocketMode=0660
SocketUser=root
SocketGroup=docker
[Install]
WantedBy=sockets.target
EOF
cat > /etc/systemd/system/docker.service <<EOF
[Unit]
Description=Docker Application Container Engine
After=network.target docker.socket
Requires=docker.socket
[Service]
ExecStart=/usr/bin/docker daemon -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs
MountFlags=slave
LimitNOFILE=1048576
LimitNPROC=1048576
LimitCORE=infinity
[Install]
WantedBy=multi-user.target
EOF
echo "=== Setup btrfs data ==="
truncate -s "8192m" "${USER_DATA_FILE}" # 8gb start (this will get resized dynamically by box-setup.service)
mkfs.btrfs -L UserHome "${USER_DATA_FILE}"
mkdir -p "${USER_DATA_DIR}"
mount -t btrfs -o loop,nosuid "${USER_DATA_FILE}" ${USER_DATA_DIR}
systemctl daemon-reload
systemctl enable docker
systemctl start docker
# give docker sometime to start up and create iptables rules
# those rules come in after docker has started, and we want to wait for them to be sure iptables-save has all of them
sleep 10
# Disable forwarding to metadata route from containers
iptables -I FORWARD -d 169.254.169.254 -j DROP
# ubuntu will restore iptables from this file automatically. this is here so that docker's chain is saved to this file
mkdir /etc/iptables && iptables-save > /etc/iptables/rules.v4
echo "=== Enable memory accounting =="
sed -e 's/GRUB_CMDLINE_LINUX=.*/GRUB_CMDLINE_LINUX="cgroup_enable=memory swapaccount=1 panic_on_oops=1 panic=5"/' -i /etc/default/grub
update-grub
# now add the user to the docker group
usermod "${USER}" -a -G docker
if [ -z $(echo "${INFRA_VERSION}") ]; then
echo "Skip pulling base docker images"
else
echo "=== Pulling base docker images ==="
docker pull "${BASE_IMAGE}"
echo "=== Pulling mysql addon image ==="
docker pull "${MYSQL_IMAGE}"
echo "=== Pulling postgresql addon image ==="
docker pull "${POSTGRESQL_IMAGE}"
echo "=== Pulling redis addon image ==="
docker pull "${REDIS_IMAGE}"
echo "=== Pulling mongodb addon image ==="
docker pull "${MONGODB_IMAGE}"
echo "=== Pulling graphite docker images ==="
docker pull "${GRAPHITE_IMAGE}"
echo "=== Pulling mail relay ==="
docker pull "${MAIL_IMAGE}"
fi
echo "==== Install nginx ===="
apt-get -y install nginx-full
[[ "$(nginx -v 2>&1)" == *"nginx/1.9."* ]] || die "Expecting nginx version to be 1.9.x"
echo "==== Install build-essential ===="
apt-get -y install build-essential rcconf
echo "==== Install mysql ===="
debconf-set-selections <<< 'mysql-server mysql-server/root_password password password'
debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password password'
apt-get -y install mysql-server
[[ "$(mysqld --version 2>&1)" == *"5.6."* ]] || die "Expecting nginx version to be 5.6.x"
echo "==== Install pwgen ===="
apt-get -y install pwgen
echo "==== Install collectd ==="
if ! apt-get install -y collectd collectd-utils; then
# FQDNLookup is true in default debian config. The box code has a custom collectd.conf that fixes this
echo "Failed to install collectd. Presumably because of http://mailman.verplant.org/pipermail/collectd/2015-March/006491.html"
sed -e 's/^FQDNLookup true/FQDNLookup false/' -i /etc/collectd/collectd.conf
fi
update-rc.d -f collectd remove
# this simply makes it explicit that we run logrotate via cron. it's already part of base ubuntu
echo "==== Install logrotate ==="
apt-get install -y cron logrotate
systemctl enable cron
echo "==== Install nodejs ===="
# Cannot use anything above 4.1.1 - https://github.com/nodejs/node/issues/3803
mkdir -p /usr/local/node-4.1.1
curl -sL https://nodejs.org/dist/v4.1.1/node-v4.1.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-4.1.1
ln -s /usr/local/node-4.1.1/bin/node /usr/bin/node
ln -s /usr/local/node-4.1.1/bin/npm /usr/bin/npm
apt-get install -y python # Install python which is required for npm rebuild
[[ "$(python --version 2>&1)" == "Python 2.7."* ]] || die "Expecting python version to be 2.7.x"
echo "=== Rebuilding npm packages ==="
cd "${INSTALLER_SOURCE_DIR}" && npm install --production
chown "${USER}:${USER}" -R "${INSTALLER_SOURCE_DIR}"
echo "==== Install installer systemd script ===="
provisionEnv="PROVISION=digitalocean"
if [ ${SELFHOSTED} == 1 ]; then
provisionEnv="PROVISION=local"
fi
cat > /etc/systemd/system/cloudron-installer.service <<EOF
[Unit]
Description=Cloudron Installer
; journald crashes result in a EPIPE in node. Cannot ignore it as it results in loss of logs.
BindsTo=systemd-journald.service
[Service]
Type=idle
ExecStart="${INSTALLER_SOURCE_DIR}/src/server.js"
Environment="DEBUG=installer*,connect-lastmile" ${provisionEnv}
; kill any child (installer.sh) as well
KillMode=control-group
Restart=on-failure
[Install]
WantedBy=multi-user.target
EOF
# Restore iptables before docker
echo "==== Install iptables-restore systemd script ===="
cat > /etc/systemd/system/iptables-restore.service <<EOF
[Unit]
Description=IPTables Restore
Before=docker.service
[Service]
Type=oneshot
ExecStart=/sbin/iptables-restore /etc/iptables/rules.v4
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF
# Allocate swap files
# https://bbs.archlinux.org/viewtopic.php?id=194792 ensures this runs after do-resize.service
echo "==== Install box-setup systemd script ===="
cat > /etc/systemd/system/box-setup.service <<EOF
[Unit]
Description=Box Setup
Before=docker.service collectd.service mysql.service
After=do-resize.service
[Service]
Type=oneshot
ExecStart="${INSTALLER_SOURCE_DIR}/systemd/box-setup.sh"
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable cloudron-installer
systemctl enable iptables-restore
systemctl enable box-setup
# Configure systemd
sed -e "s/^#SystemMaxUse=.*$/SystemMaxUse=100M/" \
-e "s/^#ForwardToSyslog=.*$/ForwardToSyslog=no/" \
-i /etc/systemd/journald.conf
# When rotating logs, systemd kills journald too soon sometimes
# See https://github.com/systemd/systemd/issues/1353 (this is upstream default)
sed -e "s/^WatchdogSec=.*$/WatchdogSec=3min/" \
-i /lib/systemd/system/systemd-journald.service
sync
# Configure time
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
# Give user access to system logs
apt-get -y install acl
usermod -a -G systemd-journal ${USER}
mkdir -p /var/log/journal # in some images, this directory is not created making system log to /run/systemd instead
chown root:systemd-journal /var/log/journal
systemctl restart systemd-journald
setfacl -n -m u:${USER}:r /var/log/journal/*/system.journal
if [ ${SELFHOSTED} == 0 ]; then
echo "==== Install ssh ==="
apt-get -y install openssh-server
# https://stackoverflow.com/questions/4348166/using-with-sed on why ? must be escaped
sed -e 's/^#\?Port .*/Port 202/g' \
-e 's/^#\?PermitRootLogin .*/PermitRootLogin without-password/g' \
-e 's/^#\?PermitEmptyPasswords .*/PermitEmptyPasswords no/g' \
-e 's/^#\?PasswordAuthentication .*/PasswordAuthentication no/g' \
-i /etc/ssh/sshd_config
# required so we can connect to this machine since port 22 is blocked by iptables by now
systemctl reload sshd
fi

View File

@@ -10,7 +10,7 @@ var ejs = require('gulp-ejs'),
serve = require('gulp-serve'),
sass = require('gulp-sass'),
sourcemaps = require('gulp-sourcemaps'),
minifyCSS = require('gulp-minify-css'),
cssnano = require('gulp-cssnano'),
autoprefixer = require('gulp-autoprefixer'),
argv = require('yargs').argv;
@@ -39,7 +39,7 @@ gulp.task('3rdparty', function () {
// JavaScript
// --------------
gulp.task('js', ['js-index', 'js-setup', 'js-update', 'js-error'], function () {});
gulp.task('js', ['js-index', 'js-setup', 'js-update'], function () {});
var oauth = {
clientId: argv.clientId || 'cid-webadmin',
@@ -80,14 +80,6 @@ gulp.task('js-setup', function () {
.pipe(gulp.dest('webadmin/dist/js'));
});
gulp.task('js-error', function () {
gulp.src(['webadmin/src/js/error.js'])
.pipe(sourcemaps.init())
.pipe(uglify())
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist/js'));
});
gulp.task('js-update', function () {
gulp.src(['webadmin/src/js/update.js'])
.pipe(sourcemaps.init())
@@ -102,7 +94,7 @@ gulp.task('js-update', function () {
// HTML
// --------------
gulp.task('html', ['html-views', 'html-update'], function () {
gulp.task('html', ['html-views', 'html-update', 'html-templates'], function () {
return gulp.src('webadmin/src/*.html').pipe(gulp.dest('webadmin/dist'));
});
@@ -114,6 +106,10 @@ 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
// --------------
@@ -123,7 +119,7 @@ gulp.task('css', function () {
.pipe(sourcemaps.init())
.pipe(sass({ includePaths: ['node_modules/bootstrap-sass/assets/stylesheets/'] }).on('error', sass.logError))
.pipe(autoprefixer())
.pipe(minifyCSS())
.pipe(cssnano())
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist'))
.pipe(gulp.dest('setup/splash/website'));
@@ -143,8 +139,8 @@ gulp.task('watch', ['default'], function () {
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/error.js'], ['js-error']);
gulp.watch(['webadmin/src/js/setup.js', 'webadmin/src/js/client.js'], ['js-setup']);
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']);

164
installer.sh.ejs Executable file
View File

@@ -0,0 +1,164 @@
#!/bin/bash
set -eu -o pipefail
echo ""
echo "======== Cloudron Installer ========"
echo ""
if [ $# -lt 4 ]; then
echo "Usage: ./installer.sh <fqdn> <aws key id> <aws key secret> <bucket> <provider> <revision>"
exit 1
fi
# commandline arguments
readonly fqdn="${1}"
readonly aws_access_key_id="${2}"
readonly aws_access_key_secret="${3}"
readonly aws_backup_bucket="${4}"
readonly provider="${5}"
readonly revision="${6}"
# environment specific urls
<% if (env === 'prod') { %>
readonly api_server_origin="https://api.cloudron.io"
readonly web_server_origin="https://cloudron.io"
<% } else { %>
readonly api_server_origin="https://api.<%= env %>.cloudron.io"
readonly web_server_origin="https://<%= env %>.cloudron.io"
<% } %>
readonly release_bucket_url="https://s3.amazonaws.com/<%= env %>-cloudron-releases"
readonly versions_url="https://s3.amazonaws.com/<%= env %>-cloudron-releases/versions.json"
readonly installer_code_url="${release_bucket_url}/box-${revision}.tar.gz"
# runtime consts
readonly installer_code_file="/tmp/box.tar.gz"
readonly installer_tmp_dir="/tmp/box"
readonly cert_folder="/tmp/certificates"
# check for fqdn in /ets/hosts
echo "[INFO] checking for hostname entry"
readonly hostentry_found=$(grep "${fqdn}" /etc/hosts || true)
if [[ -z $hostentry_found ]]; then
echo "[WARNING] No entry for ${fqdn} found in /etc/hosts"
echo "Adding an entry ..."
cat >> /etc/hosts <<EOF
# The following line was added by the Cloudron installer script
127.0.1.1 ${fqdn} ${fqdn}
EOF
else
echo "Valid hostname entry found in /etc/hosts"
fi
echo ""
echo "[INFO] ensure minimal dependencies ..."
apt-get update
apt-get install -y curl
echo ""
echo "[INFO] Generating certificates ..."
rm -rf "${cert_folder}"
mkdir -p "${cert_folder}"
cat > "${cert_folder}/CONFIG" <<EOF
[ req ]
default_bits = 1024
default_keyfile = keyfile.pem
distinguished_name = req_distinguished_name
prompt = no
req_extensions = v3_req
[ req_distinguished_name ]
C = DE
ST = Berlin
L = Berlin
O = Cloudron UG
OU = Cloudron
CN = ${fqdn}
emailAddress = cert@cloudron.io
[ v3_req ]
# Extensions to add to a certificate request
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = ${fqdn}
DNS.2 = *.${fqdn}
EOF
# generate cert files
openssl genrsa 2048 > "${cert_folder}/host.key"
openssl req -new -out "${cert_folder}/host.csr" -key "${cert_folder}/host.key" -config "${cert_folder}/CONFIG"
openssl x509 -req -days 3650 -in "${cert_folder}/host.csr" -signkey "${cert_folder}/host.key" -out "${cert_folder}/host.cert" -extensions v3_req -extfile "${cert_folder}/CONFIG"
# make them json compatible, by collapsing to one line
tls_cert=$(sed ':a;N;$!ba;s/\n/\\n/g' "${cert_folder}/host.cert")
tls_key=$(sed ':a;N;$!ba;s/\n/\\n/g' "${cert_folder}/host.key")
echo ""
echo "[INFO] Fetching installer code ..."
curl "${installer_code_url}" -o "${installer_code_file}"
echo ""
echo "[INFO] Extracting installer code to ${installer_tmp_dir} ..."
rm -rf "${installer_tmp_dir}" && mkdir -p "${installer_tmp_dir}"
tar xvf "${installer_code_file}" -C "${installer_tmp_dir}"
echo ""
echo "Creating initial provisioning config ..."
cat > /root/provision.json <<EOF
{
"sourceTarballUrl": "",
"data": {
"apiServerOrigin": "${api_server_origin}",
"webServerOrigin": "${web_server_origin}",
"fqdn": "${fqdn}",
"token": "",
"isCustomDomain": true,
"boxVersionsUrl": "${versions_url}",
"version": "",
"tlsCert": "${tls_cert}",
"tlsKey": "${tls_key}",
"provider": "${provider}",
"backupConfig": {
"provider": "s3",
"accessKeyId": "${aws_access_key_id}",
"secretAccessKey": "${aws_access_key_secret}",
"bucket": "${aws_backup_bucket}",
"prefix": "backups"
},
"dnsConfig": {
"provider": "route53",
"accessKeyId": "${aws_access_key_id}",
"secretAccessKey": "${aws_access_key_secret}"
},
"tlsConfig": {
"provider": "letsencrypt-<%= env %>"
}
}
}
EOF
echo "[INFO] Running Ubuntu initializing script ..."
/bin/bash "${installer_tmp_dir}/baseimage/initializeBaseUbuntuImage.sh" "${revision}" selfhosting
echo ""
echo "[INFO] Reloading systemd daemon ..."
systemctl daemon-reload
echo ""
echo "[INFO] Restart docker ..."
systemctl restart docker
echo ""
echo "[FINISHED] Now starting Cloudron init jobs ..."
systemctl start box-setup
# TODO this is only for convenience we should probably just let the user do a restart
sleep 5 && sync
systemctl start cloudron-installer
journalctl -u cloudron-installer.service -f

516
installer/npm-shrinkwrap.json generated Normal file
View File

@@ -0,0 +1,516 @@
{
"name": "installer",
"version": "0.0.1",
"dependencies": {
"async": {
"version": "1.5.0",
"from": "async@>=1.5.0 <2.0.0",
"resolved": "https://registry.npmjs.org/async/-/async-1.5.0.tgz"
},
"body-parser": {
"version": "1.14.1",
"from": "body-parser@>=1.12.0 <2.0.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.14.1.tgz",
"dependencies": {
"bytes": {
"version": "2.1.0",
"from": "bytes@2.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-2.1.0.tgz"
},
"content-type": {
"version": "1.0.1",
"from": "content-type@>=1.0.1 <1.1.0",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.1.tgz"
},
"depd": {
"version": "1.1.0",
"from": "depd@>=1.1.0 <1.2.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz"
},
"http-errors": {
"version": "1.3.1",
"from": "http-errors@>=1.3.1 <1.4.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz",
"dependencies": {
"inherits": {
"version": "2.0.1",
"from": "inherits@>=2.0.1 <2.1.0",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
},
"statuses": {
"version": "1.2.1",
"from": "statuses@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz"
}
}
},
"iconv-lite": {
"version": "0.4.12",
"from": "iconv-lite@0.4.12",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.12.tgz"
},
"on-finished": {
"version": "2.3.0",
"from": "on-finished@>=2.3.0 <2.4.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"dependencies": {
"ee-first": {
"version": "1.1.1",
"from": "ee-first@1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz"
}
}
},
"qs": {
"version": "5.1.0",
"from": "qs@5.1.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-5.1.0.tgz"
},
"raw-body": {
"version": "2.1.4",
"from": "raw-body@>=2.1.4 <2.2.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.1.4.tgz",
"dependencies": {
"unpipe": {
"version": "1.0.0",
"from": "unpipe@1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz"
}
}
},
"type-is": {
"version": "1.6.9",
"from": "type-is@>=1.6.9 <1.7.0",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.9.tgz",
"dependencies": {
"media-typer": {
"version": "0.3.0",
"from": "media-typer@0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz"
},
"mime-types": {
"version": "2.1.7",
"from": "mime-types@>=2.1.7 <2.2.0",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.7.tgz",
"dependencies": {
"mime-db": {
"version": "1.19.0",
"from": "mime-db@>=1.19.0 <1.20.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.19.0.tgz"
}
}
}
}
}
}
},
"connect-lastmile": {
"version": "0.0.13",
"from": "connect-lastmile@0.0.13",
"resolved": "https://registry.npmjs.org/connect-lastmile/-/connect-lastmile-0.0.13.tgz",
"dependencies": {
"debug": {
"version": "2.1.3",
"from": "debug@>=2.1.0 <2.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz",
"dependencies": {
"ms": {
"version": "0.7.0",
"from": "ms@0.7.0",
"resolved": "http://registry.npmjs.org/ms/-/ms-0.7.0.tgz"
}
}
}
}
},
"debug": {
"version": "2.2.0",
"from": "debug@>=2.1.1 <3.0.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
"dependencies": {
"ms": {
"version": "0.7.1",
"from": "ms@0.7.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
}
}
},
"express": {
"version": "4.13.3",
"from": "express@>=4.11.2 <5.0.0",
"resolved": "https://registry.npmjs.org/express/-/express-4.13.3.tgz",
"dependencies": {
"accepts": {
"version": "1.2.13",
"from": "accepts@>=1.2.12 <1.3.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.2.13.tgz",
"dependencies": {
"mime-types": {
"version": "2.1.7",
"from": "mime-types@>=2.1.6 <2.2.0",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.7.tgz",
"dependencies": {
"mime-db": {
"version": "1.19.0",
"from": "mime-db@>=1.19.0 <1.20.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.19.0.tgz"
}
}
},
"negotiator": {
"version": "0.5.3",
"from": "negotiator@0.5.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.5.3.tgz"
}
}
},
"array-flatten": {
"version": "1.1.1",
"from": "array-flatten@1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz"
},
"content-disposition": {
"version": "0.5.0",
"from": "content-disposition@0.5.0",
"resolved": "http://registry.npmjs.org/content-disposition/-/content-disposition-0.5.0.tgz"
},
"content-type": {
"version": "1.0.1",
"from": "content-type@>=1.0.1 <1.1.0",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.1.tgz"
},
"cookie": {
"version": "0.1.3",
"from": "cookie@0.1.3",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.3.tgz"
},
"cookie-signature": {
"version": "1.0.6",
"from": "cookie-signature@1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz"
},
"depd": {
"version": "1.0.1",
"from": "depd@>=1.0.1 <1.1.0",
"resolved": "http://registry.npmjs.org/depd/-/depd-1.0.1.tgz"
},
"escape-html": {
"version": "1.0.2",
"from": "escape-html@1.0.2",
"resolved": "http://registry.npmjs.org/escape-html/-/escape-html-1.0.2.tgz"
},
"etag": {
"version": "1.7.0",
"from": "etag@>=1.7.0 <1.8.0",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.7.0.tgz"
},
"finalhandler": {
"version": "0.4.0",
"from": "finalhandler@0.4.0",
"resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-0.4.0.tgz",
"dependencies": {
"unpipe": {
"version": "1.0.0",
"from": "unpipe@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz"
}
}
},
"fresh": {
"version": "0.3.0",
"from": "fresh@0.3.0",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.3.0.tgz"
},
"merge-descriptors": {
"version": "1.0.0",
"from": "merge-descriptors@1.0.0",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.0.tgz"
},
"methods": {
"version": "1.1.1",
"from": "methods@>=1.1.1 <1.2.0",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.1.tgz"
},
"on-finished": {
"version": "2.3.0",
"from": "on-finished@>=2.3.0 <2.4.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"dependencies": {
"ee-first": {
"version": "1.1.1",
"from": "ee-first@1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz"
}
}
},
"parseurl": {
"version": "1.3.0",
"from": "parseurl@>=1.3.0 <1.4.0",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.0.tgz"
},
"path-to-regexp": {
"version": "0.1.7",
"from": "path-to-regexp@0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz"
},
"proxy-addr": {
"version": "1.0.8",
"from": "proxy-addr@>=1.0.8 <1.1.0",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.0.8.tgz",
"dependencies": {
"forwarded": {
"version": "0.1.0",
"from": "forwarded@>=0.1.0 <0.2.0",
"resolved": "http://registry.npmjs.org/forwarded/-/forwarded-0.1.0.tgz"
},
"ipaddr.js": {
"version": "1.0.1",
"from": "ipaddr.js@1.0.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.0.1.tgz"
}
}
},
"qs": {
"version": "4.0.0",
"from": "qs@4.0.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-4.0.0.tgz"
},
"range-parser": {
"version": "1.0.3",
"from": "range-parser@>=1.0.2 <1.1.0",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.0.3.tgz"
},
"send": {
"version": "0.13.0",
"from": "send@0.13.0",
"resolved": "http://registry.npmjs.org/send/-/send-0.13.0.tgz",
"dependencies": {
"destroy": {
"version": "1.0.3",
"from": "destroy@1.0.3",
"resolved": "http://registry.npmjs.org/destroy/-/destroy-1.0.3.tgz"
},
"http-errors": {
"version": "1.3.1",
"from": "http-errors@>=1.3.1 <1.4.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz",
"dependencies": {
"inherits": {
"version": "2.0.1",
"from": "inherits@>=2.0.1 <2.1.0",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
}
}
},
"mime": {
"version": "1.3.4",
"from": "mime@1.3.4",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz"
},
"ms": {
"version": "0.7.1",
"from": "ms@0.7.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
},
"statuses": {
"version": "1.2.1",
"from": "statuses@>=1.2.1 <1.3.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz"
}
}
},
"serve-static": {
"version": "1.10.0",
"from": "serve-static@>=1.10.0 <1.11.0",
"resolved": "http://registry.npmjs.org/serve-static/-/serve-static-1.10.0.tgz"
},
"type-is": {
"version": "1.6.9",
"from": "type-is@>=1.6.9 <1.7.0",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.9.tgz",
"dependencies": {
"media-typer": {
"version": "0.3.0",
"from": "media-typer@0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz"
},
"mime-types": {
"version": "2.1.7",
"from": "mime-types@>=2.1.6 <2.2.0",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.7.tgz",
"dependencies": {
"mime-db": {
"version": "1.19.0",
"from": "mime-db@>=1.19.0 <1.20.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.19.0.tgz"
}
}
}
}
},
"utils-merge": {
"version": "1.0.0",
"from": "utils-merge@1.0.0",
"resolved": "http://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz"
},
"vary": {
"version": "1.0.1",
"from": "vary@>=1.0.1 <1.1.0",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.0.1.tgz"
}
}
},
"json": {
"version": "9.0.3",
"from": "json@>=9.0.3 <10.0.0",
"resolved": "https://registry.npmjs.org/json/-/json-9.0.3.tgz"
},
"morgan": {
"version": "1.6.1",
"from": "morgan@>=1.5.1 <2.0.0",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.6.1.tgz",
"dependencies": {
"basic-auth": {
"version": "1.0.3",
"from": "basic-auth@>=1.0.3 <1.1.0",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-1.0.3.tgz"
},
"depd": {
"version": "1.0.1",
"from": "depd@>=1.0.1 <1.1.0",
"resolved": "http://registry.npmjs.org/depd/-/depd-1.0.1.tgz"
},
"on-finished": {
"version": "2.3.0",
"from": "on-finished@>=2.3.0 <2.4.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"dependencies": {
"ee-first": {
"version": "1.1.1",
"from": "ee-first@1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz"
}
}
},
"on-headers": {
"version": "1.0.1",
"from": "on-headers@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz"
}
}
},
"proxy-middleware": {
"version": "0.15.0",
"from": "proxy-middleware@>=0.15.0 <0.16.0",
"resolved": "https://registry.npmjs.org/proxy-middleware/-/proxy-middleware-0.15.0.tgz"
},
"safetydance": {
"version": "0.0.19",
"from": "safetydance@0.0.19",
"resolved": "https://registry.npmjs.org/safetydance/-/safetydance-0.0.19.tgz"
},
"semver": {
"version": "5.1.0",
"from": "semver@>=5.1.0 <6.0.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.1.0.tgz"
},
"superagent": {
"version": "0.21.0",
"from": "superagent@>=0.21.0 <0.22.0",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-0.21.0.tgz",
"dependencies": {
"component-emitter": {
"version": "1.1.2",
"from": "component-emitter@1.1.2",
"resolved": "http://registry.npmjs.org/component-emitter/-/component-emitter-1.1.2.tgz"
},
"cookiejar": {
"version": "2.0.1",
"from": "cookiejar@2.0.1",
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.0.1.tgz"
},
"extend": {
"version": "1.2.1",
"from": "extend@>=1.2.1 <1.3.0",
"resolved": "https://registry.npmjs.org/extend/-/extend-1.2.1.tgz"
},
"form-data": {
"version": "0.1.3",
"from": "form-data@0.1.3",
"resolved": "http://registry.npmjs.org/form-data/-/form-data-0.1.3.tgz",
"dependencies": {
"async": {
"version": "0.9.2",
"from": "async@>=0.9.0 <0.10.0",
"resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz"
},
"combined-stream": {
"version": "0.0.7",
"from": "combined-stream@>=0.0.4 <0.1.0",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-0.0.7.tgz",
"dependencies": {
"delayed-stream": {
"version": "0.0.5",
"from": "delayed-stream@0.0.5",
"resolved": "http://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.5.tgz"
}
}
}
}
},
"formidable": {
"version": "1.0.14",
"from": "formidable@1.0.14",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-1.0.14.tgz"
},
"methods": {
"version": "1.0.1",
"from": "methods@1.0.1",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.0.1.tgz"
},
"mime": {
"version": "1.2.11",
"from": "mime@1.2.11",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz"
},
"qs": {
"version": "1.2.0",
"from": "qs@1.2.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-1.2.0.tgz"
},
"readable-stream": {
"version": "1.0.27-1",
"from": "readable-stream@1.0.27-1",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.27-1.tgz",
"dependencies": {
"core-util-is": {
"version": "1.0.1",
"from": "core-util-is@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.1.tgz"
},
"inherits": {
"version": "2.0.1",
"from": "inherits@>=2.0.1 <2.1.0",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
},
"isarray": {
"version": "0.0.1",
"from": "isarray@0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
},
"string_decoder": {
"version": "0.10.31",
"from": "string_decoder@>=0.10.0 <0.11.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
}
}
},
"reduce-component": {
"version": "1.0.1",
"from": "reduce-component@1.0.1",
"resolved": "http://registry.npmjs.org/reduce-component/-/reduce-component-1.0.1.tgz"
}
}
}
}
}

47
installer/package.json Normal file
View File

@@ -0,0 +1,47 @@
{
"name": "installer",
"description": "Cloudron Installer",
"version": "0.0.1",
"private": "true",
"author": {
"name": "Cloudron authors"
},
"repository": {
"type": "git"
},
"engines": [
"node >=4.0.0 <=4.1.1"
],
"dependencies": {
"async": "^1.5.0",
"body-parser": "^1.12.0",
"connect-lastmile": "0.0.13",
"debug": "^2.1.1",
"express": "^4.11.2",
"json": "^9.0.3",
"morgan": "^1.5.1",
"proxy-middleware": "^0.15.0",
"safetydance": "0.0.19",
"semver": "^5.1.0",
"superagent": "^0.21.0"
},
"devDependencies": {
"colors": "^1.1.2",
"commander": "^2.8.1",
"expect.js": "^0.3.1",
"istanbul": "^0.3.5",
"lodash": "^3.2.0",
"mocha": "^2.1.0",
"nock": "^0.59.1",
"sleep": "^3.0.0",
"superagent-sync": "^0.2.0",
"supererror": "^0.7.0",
"yesno": "0.0.1"
},
"scripts": {
"test": "NODE_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- -R spec ./src/test",
"precommit": "/bin/true",
"prepush": "npm test",
"postmerge": "/bin/true"
}
}

0
installer/src/certs/.gitignore vendored Normal file
View File

112
installer/src/installer.js Normal file
View File

@@ -0,0 +1,112 @@
/* jslint node: true */
'use strict';
var assert = require('assert'),
child_process = require('child_process'),
debug = require('debug')('installer:installer'),
path = require('path'),
safe = require('safetydance'),
semver = require('semver'),
superagent = require('superagent'),
util = require('util');
exports = module.exports = {
InstallerError: InstallerError,
provision: provision,
_ensureVersion: ensureVersion
};
var INSTALLER_CMD = path.join(__dirname, 'scripts/installer.sh'),
SUDO = '/usr/bin/sudo';
function InstallerError(reason, info) {
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
this.message = !info ? reason : (typeof info === 'object' ? JSON.stringify(info) : info);
}
util.inherits(InstallerError, Error);
InstallerError.INTERNAL_ERROR = 1;
InstallerError.ALREADY_PROVISIONED = 2;
// system until file has KillMode=control-group to bring down child processes
function spawn(tag, cmd, args, callback) {
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof cmd, 'string');
assert(util.isArray(args));
assert.strictEqual(typeof callback, 'function');
var cp = child_process.spawn(cmd, args, { timeout: 0 });
cp.stdout.setEncoding('utf8');
cp.stdout.on('data', function (data) { debug('%s (stdout): %s', tag, data); });
cp.stderr.setEncoding('utf8');
cp.stderr.on('data', function (data) { debug('%s (stderr): %s', tag, data); });
cp.on('error', function (error) {
debug('%s : child process errored %s', tag, error.message);
callback(error);
});
cp.on('exit', function (code, signal) {
debug('%s : child process exited. code: %d signal: %d', tag, code, signal);
if (signal) return callback(new Error('Exited with signal ' + signal));
if (code !== 0) return callback(new Error('Exited with code ' + code));
callback(null);
});
}
function ensureVersion(args, callback) {
assert.strictEqual(typeof args, 'object');
assert.strictEqual(typeof callback, 'function');
if (!args.data || !args.data.boxVersionsUrl) return callback(new Error('No boxVersionsUrl specified'));
if (args.sourceTarballUrl) return callback(null, args);
superagent.get(args.data.boxVersionsUrl).end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200) return callback(new Error(util.format('Bad status: %s %s', result.statusCode, result.text)));
var versions = safe.JSON.parse(result.text);
if (!versions || typeof versions !== 'object') return callback(new Error('versions is not in valid format:' + safe.error));
var latestVersion = Object.keys(versions).sort(semver.compare).pop();
debug('ensureVersion: Latest version is %s etag:%s', latestVersion, result.header['etag']);
if (!versions[latestVersion]) return callback(new Error('No version available'));
if (!versions[latestVersion].sourceTarballUrl) return callback(new Error('No sourceTarballUrl specified'));
args.sourceTarballUrl = versions[latestVersion].sourceTarballUrl;
args.data.version = latestVersion;
callback(null, args);
});
}
function provision(args, callback) {
assert.strictEqual(typeof args, 'object');
assert.strictEqual(typeof callback, 'function');
if (process.env.NODE_ENV === 'test') return callback(null);
ensureVersion(args, function (error, result) {
if (error) return callback(error);
var pargs = [ INSTALLER_CMD ];
pargs.push('--sourcetarballurl', result.sourceTarballUrl);
pargs.push('--data', JSON.stringify(result.data));
debug('provision: calling with args %j', pargs);
// sudo is required for update()
spawn('provision', SUDO, pargs, callback);
});
}

View File

@@ -0,0 +1,67 @@
#!/bin/bash
set -eu -o pipefail
readonly BOX_SRC_DIR=/home/yellowtent/box
readonly DATA_DIR=/home/yellowtent/data
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly json="${script_dir}/../../node_modules/.bin/json"
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 300"
readonly is_update=$([[ -d "${BOX_SRC_DIR}" ]] && echo "yes" || echo "no")
# create a provision file for testing. %q escapes args. %q is reused as much as necessary to satisfy $@
(echo -e "#!/bin/bash\n"; printf "%q " "${script_dir}/installer.sh" "$@") > /home/yellowtent/provision.sh
chmod +x /home/yellowtent/provision.sh
arg_source_tarball_url=""
arg_data=""
args=$(getopt -o "" -l "sourcetarballurl:,data:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--sourcetarballurl) arg_source_tarball_url="$2";;
--data) arg_data="$2";;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
shift 2
done
box_src_tmp_dir=$(mktemp -dt box-src-XXXXXX)
echo "Downloading box code from ${arg_source_tarball_url} to ${box_src_tmp_dir}"
while true; do
if $curl -L "${arg_source_tarball_url}" | tar -zxf - -C "${box_src_tmp_dir}"; then break; fi
echo "Failed to download source tarball, trying again"
sleep 5
done
while true; do
# for reasons unknown, the dtrace package will fail. but rebuilding second time will work
if cd "${box_src_tmp_dir}" && npm rebuild; then break; fi
echo "Failed to rebuild, trying again"
sleep 5
done
if [[ "${is_update}" == "yes" ]]; then
echo "Setting up update splash screen"
"${box_src_tmp_dir}/setup/splashpage.sh" --data "${arg_data}" # show splash from new code
${BOX_SRC_DIR}/setup/stop.sh # stop the old code
fi
# switch the codes
rm -rf "${BOX_SRC_DIR}"
mv "${box_src_tmp_dir}" "${BOX_SRC_DIR}"
chown -R yellowtent.yellowtent "${BOX_SRC_DIR}"
# create a start file for testing. %q escapes args
(echo -e "#!/bin/bash\n"; printf "%q " "${BOX_SRC_DIR}/setup/start.sh" --data "${arg_data}") > /home/yellowtent/setup_start.sh
chmod +x /home/yellowtent/setup_start.sh
echo "Calling box setup script"
"${BOX_SRC_DIR}/setup/start.sh" --data "${arg_data}"

144
installer/src/server.js Executable file
View File

@@ -0,0 +1,144 @@
#!/usr/bin/env node
/* jslint node: true */
'use strict';
var assert = require('assert'),
async = require('async'),
debug = require('debug')('installer:server'),
express = require('express'),
fs = require('fs'),
http = require('http'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
installer = require('./installer.js'),
json = require('body-parser').json,
lastMile = require('connect-lastmile'),
morgan = require('morgan'),
superagent = require('superagent');
exports = module.exports = {
start: start,
stop: stop
};
var PROVISION_CONFIG_FILE = '/root/provision.json';
var CLOUDRON_CONFIG_FILE = '/home/yellowtent/configs/cloudron.conf';
var gHttpServer = null; // update server; used for updates
function provisionDigitalOcean(callback) {
if (fs.existsSync(CLOUDRON_CONFIG_FILE)) return callback(null); // already provisioned
superagent.get('http://169.254.169.254/metadata/v1.json').end(function (error, result) {
if (error || result.statusCode !== 200) {
console.error('Error getting metadata', error);
return callback(new Error('Error getting metadata'));
}
var userData = JSON.parse(result.body.user_data);
installer.provision(userData, callback);
});
}
function provisionLocal(callback) {
if (fs.existsSync(CLOUDRON_CONFIG_FILE)) return callback(null); // already provisioned
if (!fs.existsSync(PROVISION_CONFIG_FILE)) {
console.error('No provisioning data found at %s', PROVISION_CONFIG_FILE);
return callback(new Error('No provisioning data found'));
}
var userData = require(PROVISION_CONFIG_FILE);
installer.provision(userData, callback);
}
function update(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.sourceTarballUrl || typeof req.body.sourceTarballUrl !== 'string') return next(new HttpError(400, 'No sourceTarballUrl provided'));
if (!req.body.data || typeof req.body.data !== 'object') return next(new HttpError(400, 'No data provided'));
debug('provision: received from box %j', req.body);
installer.provision(req.body, function (error) {
if (error) console.error(error);
});
next(new HttpSuccess(202, { }));
}
function startUpdateServer(callback) {
assert.strictEqual(typeof callback, 'function');
debug('Starting update server');
var app = express();
var router = new express.Router();
if (process.env.NODE_ENV !== 'test') app.use(morgan('dev', { immediate: false }));
app.use(json({ strict: true }))
.use(router)
.use(lastMile());
router.post('/api/v1/installer/update', update);
gHttpServer = http.createServer(app);
gHttpServer.on('error', console.error);
gHttpServer.listen(2020, '127.0.0.1', callback);
}
function stopUpdateServer(callback) {
assert.strictEqual(typeof callback, 'function');
debug('Stopping update server');
if (!gHttpServer) return callback(null);
gHttpServer.close(callback);
gHttpServer = null;
}
function start(callback) {
assert.strictEqual(typeof callback, 'function');
var actions;
if (process.env.PROVISION === 'local') {
debug('Starting Installer in selfhost mode');
actions = [
startUpdateServer,
provisionLocal
];
} else { // current fallback, should be 'digitalocean' eventually, see initializeBaseUbuntuImage.sh
debug('Starting Installer in managed mode');
actions = [
startUpdateServer,
provisionDigitalOcean
];
}
async.series(actions, callback);
}
function stop(callback) {
assert.strictEqual(typeof callback, 'function');
async.series([
stopUpdateServer
], callback);
}
if (require.main === module) {
start(function (error) {
if (error) console.error(error);
});
}

View File

@@ -0,0 +1,179 @@
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
var expect = require('expect.js'),
fs = require('fs'),
path = require('path'),
nock = require('nock'),
os = require('os'),
request = require('superagent'),
server = require('../server.js'),
installer = require('../installer.js'),
_ = require('lodash');
var EXTERNAL_SERVER_URL = 'https://localhost:4443';
var INTERNAL_SERVER_URL = 'http://localhost:2020';
var APPSERVER_ORIGIN = 'http://appserver';
var FQDN = os.hostname();
describe('Server', function () {
this.timeout(5000);
before(function (done) {
var user_data = JSON.stringify({ apiServerOrigin: APPSERVER_ORIGIN }); // user_data is a string
var scope = nock('http://169.254.169.254')
.persist()
.get('/metadata/v1.json')
.reply(200, JSON.stringify({ user_data: user_data }), { 'Content-Type': 'application/json' });
done();
});
after(function (done) {
nock.cleanAll();
done();
});
describe('starts and stop', function () {
it('starts', function (done) {
server.start(done);
});
it('stops', function (done) {
server.stop(done);
});
});
describe('update (internal server)', function () {
before(function (done) {
server.start(done);
});
after(function (done) {
server.stop(done);
});
it('does not respond to provision', function (done) {
request.post(INTERNAL_SERVER_URL + '/api/v1/installer/provision').send({ }).end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(404);
done();
});
});
it('does not respond to restore', function (done) {
request.post(INTERNAL_SERVER_URL + '/api/v1/installer/restore').send({ }).end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(404);
done();
});
});
var data = {
sourceTarballUrl: "https://foo.tar.gz",
data: {
token: 'sometoken',
apiServerOrigin: APPSERVER_ORIGIN,
webServerOrigin: 'https://somethingelse.com',
fqdn: 'www.something.com',
tlsKey: 'key',
tlsCert: 'cert',
boxVersionsUrl: 'https://versions.json',
version: '0.1'
}
};
Object.keys(data).forEach(function (key) {
it('fails due to missing ' + key, function (done) {
var dataCopy = _.merge({ }, data);
delete dataCopy[key];
request.post(INTERNAL_SERVER_URL + '/api/v1/installer/update').send(dataCopy).end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
});
it('succeeds', function (done) {
request.post(INTERNAL_SERVER_URL + '/api/v1/installer/update').send(data).end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(202);
done();
});
});
});
describe('ensureVersion', function () {
before(function () {
process.env.NODE_ENV = undefined;
});
after(function () {
process.env.NODE_ENV = 'test';
});
it ('fails without data', function (done) {
installer._ensureVersion({}, function (error) {
expect(error).to.be.an(Error);
done();
});
});
it ('fails without boxVersionsUrl', function (done) {
installer._ensureVersion({ data: {}}, function (error) {
expect(error).to.be.an(Error);
done();
});
});
it ('succeeds with sourceTarballUrl', function (done) {
var data = {
sourceTarballUrl: 'sometarballurl',
data: {
boxVersionsUrl: 'http://foobar/versions.json'
}
};
installer._ensureVersion(data, function (error, result) {
expect(error).to.equal(null);
expect(result).to.eql(data);
done();
});
});
it ('succeeds without sourceTarballUrl', function (done) {
var versions = {
'0.1.0': {
sourceTarballUrl: 'sometarballurl1'
},
'0.2.0': {
sourceTarballUrl: 'sometarballurl2'
}
};
var scope = nock('http://foobar')
.get('/versions.json')
.reply(200, JSON.stringify(versions), { 'Content-Type': 'application/json' });
var data = {
data: {
boxVersionsUrl: 'http://foobar/versions.json'
}
};
installer._ensureVersion(data, function (error, result) {
expect(error).to.equal(null);
expect(result.sourceTarballUrl).to.equal(versions['0.2.0'].sourceTarballUrl);
expect(result.data.boxVersionsUrl).to.equal(data.data.boxVersionsUrl);
done();
});
});
});
});

66
installer/systemd/box-setup.sh Executable file
View File

@@ -0,0 +1,66 @@
#!/bin/bash
set -eu -o pipefail
readonly USER_HOME="/home/yellowtent"
readonly APPS_SWAP_FILE="/apps.swap"
readonly BACKUP_SWAP_FILE="/backup.swap" # used when doing app backups
readonly USER_DATA_FILE="/root/user_data.img"
readonly USER_DATA_DIR="/home/yellowtent/data"
# detect device
if [[ -b "/dev/vda1" ]]; then
disk_device="/dev/vda1"
fi
if [[ -b "/dev/xvda1" ]]; then
disk_device="/dev/xvda1"
fi
# all sizes are in mb
readonly physical_memory=$(free -m | awk '/Mem:/ { print $2 }')
readonly swap_size="${physical_memory}" # if you change this, fix enoughResourcesAvailable() in client.js
readonly app_count=$((${physical_memory} / 200)) # estimated app count
readonly disk_size_gb=$(fdisk -l ${disk_device} | grep "Disk ${disk_device}" | awk '{ print $3 }')
readonly disk_size=$((disk_size_gb * 1024))
readonly backup_swap_size=1024
readonly system_size=10240 # 10 gigs for system libs, apps images, installer, box code and tmp
readonly ext4_reserved=$((disk_size * 5 / 100)) # this can be changes using tune2fs -m percent /dev/vda1
echo "Disk device: ${disk_device}"
echo "Physical memory: ${physical_memory}"
echo "Estimated app count: ${app_count}"
echo "Disk size: ${disk_size}"
# Allocate two sets of swap files - one for general app usage and another for backup
# The backup swap is setup for swap on the fly by the backup scripts
if [[ ! -f "${APPS_SWAP_FILE}" ]]; then
echo "Creating Apps swap file of size ${swap_size}M"
fallocate -l "${swap_size}m" "${APPS_SWAP_FILE}"
chmod 600 "${APPS_SWAP_FILE}"
mkswap "${APPS_SWAP_FILE}"
swapon "${APPS_SWAP_FILE}"
echo "${APPS_SWAP_FILE} none swap sw 0 0" >> /etc/fstab
else
echo "Apps Swap file already exists"
fi
if [[ ! -f "${BACKUP_SWAP_FILE}" ]]; then
echo "Creating Backup swap file of size ${backup_swap_size}M"
fallocate -l "${backup_swap_size}m" "${BACKUP_SWAP_FILE}"
chmod 600 "${BACKUP_SWAP_FILE}"
mkswap "${BACKUP_SWAP_FILE}"
else
echo "Backups Swap file already exists"
fi
echo "Resizing data volume"
home_data_size=$((disk_size - system_size - swap_size - backup_swap_size - ext4_reserved))
echo "Resizing up btrfs user data to size ${home_data_size}M"
umount "${USER_DATA_DIR}" || true
# Do not preallocate (non-sparse). Doing so overallocates for data too much in advance and causes problems when using many apps with smaller data
# fallocate -l "${home_data_size}m" "${USER_DATA_FILE}" # does not overwrite existing data
truncate -s "${home_data_size}m" "${USER_DATA_FILE}" # this will shrink it if the file had existed. this is useful when running this script on a live system
mount -t btrfs -o loop,nosuid "${USER_DATA_FILE}" ${USER_DATA_DIR}
btrfs filesystem resize max "${USER_DATA_DIR}"

View File

@@ -0,0 +1,15 @@
dbm = dbm || require('db-migrate');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN displayName VARCHAR(512) DEFAULT ""', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN displayName', function (error) {
if (error) console.error(error);
callback(error);
});
};

View File

@@ -0,0 +1,15 @@
dbm = dbm || require('db-migrate');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN memoryLimit BIGINT DEFAULT 0', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN memoryLimit', function (error) {
if (error) console.error(error);
callback(error);
});
};

View File

@@ -0,0 +1,21 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
var cmd = "CREATE TABLE groups(" +
"id VARCHAR(128) NOT NULL UNIQUE," +
"name VARCHAR(128) NOT NULL UNIQUE," +
"PRIMARY KEY(id))";
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE groups', function (error) {
if (error) console.error(error);
callback(error);
});
};

View File

@@ -0,0 +1,22 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
var cmd = "CREATE TABLE IF NOT EXISTS groupMembers(" +
"groupId VARCHAR(128) NOT NULL," +
"userId VARCHAR(128) NOT NULL," +
"FOREIGN KEY(groupId) REFERENCES groups(id)," +
"FOREIGN KEY(userId) REFERENCES users(id));";
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE groupMembers', function (error) {
if (error) console.error(error);
callback(error);
});
};

View File

@@ -0,0 +1,30 @@
'use strict';
var dbm = global.dbm || require('db-migrate');
var async = require('async');
var ADMIN_GROUP_ID = 'admin'; // see groups.js
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'INSERT INTO groups (id, name) VALUES (?, ?)', [ ADMIN_GROUP_ID, 'admin' ]),
function migrateAdminFlag(done) {
db.all('SELECT * FROM users WHERE admin=1', function (error, results) {
if (error) return done(error);
console.dir(results);
async.eachSeries(results, function (r, next) {
db.runSql('INSERT INTO groupMembers (groupId, userId) VALUES (?, ?)', [ ADMIN_GROUP_ID, r.id ], next);
}, done);
});
},
db.runSql.bind(db, 'ALTER TABLE users DROP COLUMN admin'),
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
callback();
};

View File

@@ -0,0 +1,25 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
var cmd = "CREATE TABLE backups(" +
"filename VARCHAR(128) NOT NULL," +
"creationTime TIMESTAMP," +
"version VARCHAR(128) NOT NULL," +
"type VARCHAR(16) NOT NULL," +
"dependsOn VARCHAR(4096)," +
"state VARCHAR(16) NOT NULL," +
"PRIMARY KEY (filename))";
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE backups', function (error) {
if (error) console.error(error);
callback(error);
});
};

View File

@@ -18,8 +18,20 @@ CREATE TABLE IF NOT EXISTS users(
createdAt VARCHAR(512) NOT NULL,
modifiedAt VARCHAR(512) NOT NULL,
admin INTEGER NOT NULL,
displayName VARCHAR(512) DEFAULT '',
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS groups(
id VARCHAR(128) NOT NULL UNIQUE,
username VARCHAR(254) NOT NULL UNIQUE,
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS groupMembers(
groupId VARCHAR(128) NOT NULL,
userId VARCHAR(128) NOT NULL,
FOREIGN KEY(groupId) REFERENCES groups(id),
FOREIGN KEY(userId) REFERENCES users(id));
CREATE TABLE IF NOT EXISTS tokens(
accessToken VARCHAR(128) NOT NULL UNIQUE,
identifier VARCHAR(128) NOT NULL,
@@ -52,6 +64,7 @@ CREATE TABLE IF NOT EXISTS apps(
accessRestrictionJson TEXT,
oauthProxy BOOLEAN DEFAULT 0,
createdAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
memoryLimit BIGINT DEFAULT 0,
lastBackupId VARCHAR(128),
lastBackupConfigJson TEXT, // used for appstore and non-appstore installs. it's here so it's easy to do REST validation
@@ -85,3 +98,12 @@ CREATE TABLE IF NOT EXISTS appAddonConfigs(
value VARCHAR(512) NOT NULL,
FOREIGN KEY(appId) REFERENCES apps(id));
CREATE TABLE IF NOT EXISTS backups(
filename VARCHAR(128) NOT NULL, /* s3 url */
creationTime TIMESTAMP,
version VARCHAR(128) NOT NULL, /* app version or box version */
type VARCHAR(16) NOT NULL, /* 'box' or 'app' */
dependsOn VARCHAR(4096), /* comma separate list of objects this backup depends on */
state VARCHAR(16) NOT NULL,
PRIMARY KEY (filename));

929
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,7 @@
"aws-sdk": "^2.1.46",
"body-parser": "^1.13.1",
"bytes": "^2.1.0",
"cloudron-manifestformat": "^2.0.0",
"cloudron-manifestformat": "^2.3.0",
"connect-ensure-login": "^0.1.1",
"connect-lastmile": "0.0.13",
"connect-timeout": "^1.5.0",
@@ -42,11 +42,13 @@
"multiparty": "^4.1.2",
"mysql": "^2.7.0",
"native-dns": "^0.7.0",
"node-df": "^0.1.1",
"node-uuid": "^1.4.3",
"nodemailer": "^1.3.0",
"nodemailer-smtp-transport": "^1.0.3",
"oauth2orize": "^1.0.1",
"once": "^1.3.2",
"parse-links": "^0.1.0",
"passport": "^0.2.2",
"passport-http": "^0.2.2",
"passport-http-bearer": "^1.0.1",
@@ -54,13 +56,14 @@
"passport-oauth2-client-password": "^0.1.2",
"password-generator": "^2.0.2",
"proxy-middleware": "^0.13.0",
"safetydance": "^0.1.0",
"safetydance": "^0.1.1",
"semver": "^4.3.6",
"serve-favicon": "^2.2.0",
"split": "^1.0.0",
"superagent": "^1.5.0",
"supererror": "^0.7.1",
"tail-stream": "https://registry.npmjs.org/tail-stream/-/tail-stream-0.2.1.tgz",
"tldjs": "^1.6.2",
"underscore": "^1.7.0",
"ursa": "^0.9.1",
"valid-url": "^1.0.9",
@@ -70,13 +73,14 @@
"devDependencies": {
"apidoc": "*",
"bootstrap-sass": "^3.3.3",
"deep-extend": "^0.4.1",
"del": "^1.1.1",
"expect.js": "*",
"gulp": "^3.8.11",
"gulp-autoprefixer": "^2.3.0",
"gulp-concat": "^2.4.3",
"gulp-cssnano": "^2.1.0",
"gulp-ejs": "^1.0.0",
"gulp-minify-css": "^1.1.3",
"gulp-sass": "^2.0.1",
"gulp-serve": "^1.0.0",
"gulp-sourcemaps": "^1.5.2",

123
scripts/createReleaseTarball Executable file
View File

@@ -0,0 +1,123 @@
#!/bin/bash
set -eu
assertNotEmpty() {
: "${!1:? "$1 is not set."}"
}
# Only GNU getopt supports long options. OS X comes bundled with the BSD getopt
# brew install gnu-getopt to get the GNU getopt on OS X
[[ $(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:,publish,no-upload" -n "$0" -- "$@")
eval set -- "${args}"
readonly RELEASE_TOOL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../release" && pwd)"
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
delete_bundle="yes"
commitish="HEAD"
publish="no"
upload="yes"
bundle_file=""
while true; do
case "$1" in
--revision) commitish="$2"; shift 2;;
--output) bundle_file="$2"; delete_bundle="no"; shift 2;;
--no-upload) upload="no"; shift;;
--publish) publish="yes"; shift;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
if [[ "${upload}" == "no" && "${publish}" == "yes" ]]; then
echo "Cannot publish without uploading"
exit 1
fi
readonly TMPDIR=${TMPDIR:-/tmp} # why is this not set on mint?
assertNotEmpty AWS_DEV_ACCESS_KEY
assertNotEmpty AWS_DEV_SECRET_KEY
if ! $(cd "${SOURCE_DIR}" && git diff --exit-code >/dev/null); then
echo "You have local changes, stash or commit them to proceed"
exit 1
fi
if [[ "$(node --version)" != "v4.1.1" ]]; then
echo "This script requires node 4.1.1"
exit 1
fi
version=$(cd "${SOURCE_DIR}" && git rev-parse "${commitish}")
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"
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 -))
if diff "${TMPDIR}/boxtarball.cache/npm-shrinkwrap.json.all" "${bundle_dir}/npm-shrinkwrap.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 "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}/npm-shrinkwrap.json" "${TMPDIR}/boxtarball.cache/npm-shrinkwrap.json.all"
fi
echo "Building webadmin assets"
(cd "${bundle_dir}" && gulp)
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"
if diff "${TMPDIR}/boxtarball.cache/npm-shrinkwrap.json.prod" "${bundle_dir}/npm-shrinkwrap.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 "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}/npm-shrinkwrap.json" "${TMPDIR}/boxtarball.cache/npm-shrinkwrap.json.prod"
fi
echo "Create final tarball"
(cd "${bundle_dir}" && tar czf "${bundle_file}" .)
echo "Cleaning up ${bundle_dir}"
rm -rf "${bundle_dir}"
if [[ "${upload}" == "yes" ]]; then
echo "Uploading bundle to S3"
# That special header is needed to allow access with singed urls created with different aws credentials than the ones the file got uploaded
s3cmd --multipart-chunk-size-mb=5 --ssl --acl-public --access_key="${AWS_DEV_ACCESS_KEY}" --secret_key="${AWS_DEV_SECRET_KEY}" --no-mime-magic put "${bundle_file}" "s3://dev-cloudron-releases/box-${version}.tar.gz"
versions_file_url="https://dev-cloudron-releases.s3.amazonaws.com/box-${version}.tar.gz"
echo "The URL for the versions file is: ${versions_file_url}"
if [[ "${publish}" == "yes" ]]; then
echo "Publishing to dev"
${RELEASE_TOOL_DIR}/release create --env dev --code "${versions_file_url}"
fi
fi
if [[ "${delete_bundle}" == "no" ]]; then
echo "Tarball preserved at ${bundle_file}"
else
rm "${bundle_file}"
fi

View File

@@ -3,12 +3,12 @@
# If you change the infra version, be sure to put a warning
# in the change log
INFRA_VERSION=21
INFRA_VERSION=23
# WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
# These constants are used in the installer script as well
BASE_IMAGE=cloudron/base:0.8.0
MYSQL_IMAGE=cloudron/mysql:0.8.0
MYSQL_IMAGE=cloudron/mysql:0.10.0
POSTGRESQL_IMAGE=cloudron/postgresql:0.8.0
MONGODB_IMAGE=cloudron/mongodb:0.8.0
REDIS_IMAGE=cloudron/redis:0.8.0 # if you change this, fix src/addons.js as well

View File

@@ -19,6 +19,8 @@ arg_version=""
arg_web_server_origin=""
arg_backup_config=""
arg_dns_config=""
arg_update_config=""
arg_provider=""
args=$(getopt -o "" -l "data:,retire" -n "$0" -- "$@")
eval set -- "${args}"
@@ -31,12 +33,14 @@ while true; do
;;
--data)
# only read mandatory non-empty parameters here
read -r arg_api_server_origin arg_web_server_origin arg_fqdn arg_token arg_is_custom_domain arg_box_versions_url arg_version <<EOF
$(echo "$2" | $json apiServerOrigin webServerOrigin fqdn token isCustomDomain boxVersionsUrl version | tr '\n' ' ')
read -r arg_api_server_origin arg_web_server_origin arg_fqdn arg_is_custom_domain arg_box_versions_url arg_version <<EOF
$(echo "$2" | $json apiServerOrigin webServerOrigin fqdn isCustomDomain boxVersionsUrl version | tr '\n' ' ')
EOF
# read possibly empty parameters here
arg_tls_cert=$(echo "$2" | $json tlsCert)
arg_tls_key=$(echo "$2" | $json tlsKey)
arg_token=$(echo "$2" | $json token)
arg_provider=$(echo "$2" | $json provider)
arg_tls_config=$(echo "$2" | $json tlsConfig)
[[ "${arg_tls_config}" == "null" ]] && arg_tls_config=""
@@ -53,6 +57,9 @@ EOF
arg_dns_config=$(echo "$2" | $json dnsConfig)
[[ "${arg_dns_config}" == "null" ]] && arg_dns_config=""
arg_update_config=$(echo "$2" | $json updateConfig)
[[ "${arg_update_config}" == "null" ]] && arg_update_config=""
shift 2
;;
--) break;;
@@ -73,3 +80,4 @@ echo "token: ${arg_token}"
echo "tlsConfig: ${arg_tls_config}"
echo "version: ${arg_version}"
echo "web server: ${arg_web_server_origin}"
echo "provider: ${arg_provider}"

View File

@@ -20,7 +20,7 @@ systemctl daemon-reload
systemctl enable cloudron.target
########## sudoers
rm -f /etc/sudoers.d/*
rm -f /etc/sudoers.d/yellowtent
cp "${container_files}/sudoers" /etc/sudoers.d/yellowtent
########## collectd

View File

@@ -1,3 +1,6 @@
# sudo logging breaks journalctl output with very long urls (systemd bug)
Defaults !syslog
Defaults!/home/yellowtent/box/src/scripts/createappdir.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/createappdir.sh
@@ -27,3 +30,7 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/backupswap.sh
Defaults!/home/yellowtent/box/src/scripts/collectlogs.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/collectlogs.sh
Defaults!/home/yellowtent/box/src/scripts/retire.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/retire.sh

View File

@@ -2,6 +2,8 @@
Description=Cloudron Admin
OnFailure=crashnotifier@%n.service
StopWhenUnneeded=true
; journald crashes result in a EPIPE in node. Cannot ignore it as it results in loss of logs.
BindsTo=systemd-journald.service
[Service]
Type=idle
@@ -9,7 +11,8 @@ WorkingDirectory=/home/yellowtent/box
Restart=always
ExecStart=/usr/bin/node --max_old_space_size=150 /home/yellowtent/box/box.js
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
KillMode=process
; kill apptask processes as well
KillMode=control-group
User=yellowtent
Group=yellowtent
MemoryLimit=200M

View File

@@ -1,5 +1,5 @@
[Unit]
Description=Cloudron Smart Cloud
Description=Cloudron Smartserver
Documentation=https://cloudron.io/documentation.html
StopWhenUnneeded=true
Requires=box.service

View File

@@ -92,10 +92,6 @@ EOF
set_progress "28" "Setup collectd"
cp "${script_dir}/start/collectd.conf" "${DATA_DIR}/collectd/collectd.conf"
# collectd 5.4.1 has some bug where we simply cannot get it to create df-vda1
mkdir -p "${DATA_DIR}/graphite/whisper/collectd/localhost/"
vda1_id=$(blkid -s UUID -o value /dev/vda1)
ln -sfF "df-disk_by-uuid_${vda1_id}" "${DATA_DIR}/graphite/whisper/collectd/localhost/df-vda1"
service collectd restart
set_progress "30" "Setup nginx"
@@ -143,6 +139,7 @@ cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
"isCustomDomain": ${arg_is_custom_domain},
"boxVersionsUrl": "${arg_box_versions_url}",
"adminEmail": "admin@${arg_fqdn}",
"provider": "${arg_provider}",
"database": {
"hostname": "localhost",
"username": "root",
@@ -177,6 +174,14 @@ if [[ ! -z "${arg_dns_config}" ]]; then
-e "REPLACE INTO settings (name, value) VALUES (\"dns_config\", '$arg_dns_config')" box
fi
# Add Update Configuration
if [[ ! -z "${arg_update_config}" ]]; then
echo "Add Update Config"
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO settings (name, value) VALUES (\"update_config\", '$arg_update_config')" box
fi
# Add TLS Configuration
if [[ ! -z "${arg_tls_config}" ]]; then
echo "Add TLS Config"

View File

@@ -133,10 +133,10 @@ LoadPlugin nginx
# Globals true
#</LoadPlugin>
#LoadPlugin pinba
LoadPlugin ping
#LoadPlugin ping
#LoadPlugin postgresql
#LoadPlugin powerdns
LoadPlugin processes
#LoadPlugin processes
#LoadPlugin protocols
#<LoadPlugin python>
# Globals true
@@ -161,7 +161,7 @@ LoadPlugin tail
#LoadPlugin users
#LoadPlugin uuid
#LoadPlugin varnish
LoadPlugin vmem
#LoadPlugin vmem
#LoadPlugin vserver
#LoadPlugin wireless
LoadPlugin write_graphite
@@ -193,11 +193,11 @@ LoadPlugin write_graphite
</Plugin>
<Plugin df>
FSType "tmpfs"
MountPoint "/dev"
FSType "ext4"
FSType "btrfs"
ReportByDevice true
IgnoreSelected true
IgnoreSelected false
ValuesAbsolute true
ValuesPercentage true
@@ -212,17 +212,6 @@ LoadPlugin write_graphite
URL "http://127.0.0.1/nginx_status"
</Plugin>
<Plugin ping>
Host "google.com"
Interval 1.0
Timeout 0.9
TTL 255
</Plugin>
<Plugin processes>
ProcessMatch "app" "node box.js"
</Plugin>
<Plugin swap>
ReportByDevice false
ReportBytes true
@@ -255,10 +244,6 @@ LoadPlugin write_graphite
</File>
</Plugin>
<Plugin vmem>
Verbose false
</Plugin>
<Plugin write_graphite>
<Node "graphing">
Host "localhost"

View File

@@ -58,12 +58,17 @@ server {
client_max_body_size 1m;
}
# graphite paths
location ~ ^/(graphite|content|metrics|dashboard|render|browser|composer)/ {
proxy_pass http://127.0.0.1:8000;
client_max_body_size 1m;
location ~ ^/api/v1/apps/.*/exec$ {
proxy_pass http://127.0.0.1:3000;
proxy_read_timeout 30m;
}
# graphite paths
# 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;
index index.html index.htm;

View File

@@ -58,6 +58,14 @@ http {
ssl_certificate cert/host.cert;
ssl_certificate_key cert/host.key;
# increase the proxy buffer sizes to not run into buffer issues (http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffers)
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
# Disable check to allow unlimited body sizes
client_max_body_size 0;
error_page 404 = @fallback;
location @fallback {
internal;
@@ -65,7 +73,16 @@ http {
rewrite ^/$ /nakeddomain.html break;
}
return 404;
location / {
internal;
root /home/yellowtent/box/webadmin/dist;
rewrite ^/$ /nakeddomain.html break;
}
location /api/ {
proxy_pass http://127.0.0.1:3000;
client_max_body_size 1m;
}
}
include applications/*.conf;

View File

@@ -63,8 +63,8 @@ readonly MYSQL_ROOT_PASSWORD='${mysql_addon_root_password}'
readonly MYSQL_ROOT_HOST='${docker0_ip}'
EOF
mysql_container_id=$(docker run --restart=always -d --name="mysql" \
-m 100m \
--memory-swap 200m \
-m 256m \
--memory-swap 512m \
-h "${arg_fqdn}" \
-v "${DATA_DIR}/mysql:/var/lib/mysql" \
-v "${DATA_DIR}/addons/mysql_vars.sh:/etc/mysql/mysql_vars.sh:ro" \

View File

@@ -388,10 +388,13 @@ function setupSendMail(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var username = app.location ? app.location + '-app' : 'no-reply'; // use no-reply for bare domains
var env = [
'MAIL_SMTP_SERVER=mail',
'MAIL_SMTP_PORT=2500', // if you change this, change the mail container
'MAIL_SMTP_USERNAME=' + (app.location || app.id) + '-app', // use app.id for bare domains
'MAIL_SMTP_USERNAME=' + username,
'MAIL_SMTP_PASSWORD=' + hat(256), // this is ignored
'MAIL_DOMAIN=' + config.fqdn()
];
@@ -418,7 +421,7 @@ function setupMySql(app, options, callback) {
debugApp(app, 'Setting up mysql');
var container = docker.getContainer('mysql');
var cmd = [ '/addons/mysql/service.sh', 'add', app.id ];
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'add-prefix' : 'add', app.id ];
container.exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true }, function (error, execContainer) {
if (error) return callback(error);
@@ -451,7 +454,7 @@ function teardownMySql(app, options, callback) {
assert.strictEqual(typeof callback, 'function');
var container = docker.getContainer('mysql');
var cmd = [ '/addons/mysql/service.sh', 'remove', app.id ];
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'remove-prefix' : 'remove', app.id ];
debugApp(app, 'Tearing down mysql');
@@ -479,7 +482,7 @@ function backupMySql(app, options, callback) {
var output = fs.createWriteStream(path.join(paths.DATA_DIR, app.id, 'mysqldump'));
output.on('error', callback);
var cp = spawn('/usr/bin/docker', [ 'exec', 'mysql', '/addons/mysql/service.sh', 'backup', app.id ]);
var cp = spawn('/usr/bin/docker', [ 'exec', 'mysql', '/addons/mysql/service.sh', options.multipleDatabases ? 'backup-prefix' : 'backup', app.id ]);
cp.on('error', callback);
cp.on('exit', function (code, signal) {
debugApp(app, 'backupMySql: done. code:%s signal:%s', code, signal);
@@ -502,7 +505,7 @@ function restoreMySql(app, options, callback) {
input.on('error', callback);
// cannot get this to work through docker.exec
var cp = spawn('/usr/bin/docker', [ 'exec', '-i', 'mysql', '/addons/mysql/service.sh', 'restore', app.id ]);
var cp = spawn('/usr/bin/docker', [ 'exec', '-i', 'mysql', '/addons/mysql/service.sh', options.multipleDatabases ? 'restore-prefix' : 'restore', app.id ]);
cp.on('error', callback);
cp.on('exit', function (code, signal) {
debugApp(app, 'restoreMySql: done %s %s', code, signal);
@@ -764,7 +767,7 @@ function setupRedis(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var redisPassword = generatePassword(64, false /* memorable */);
var redisPassword = generatePassword(64, 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.DATA_DIR, app.id + '/redis');

View File

@@ -59,7 +59,7 @@ var assert = require('assert'),
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.installationProgress', 'apps.runState',
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'apps.location', 'apps.dnsRecordId',
'apps.accessRestrictionJson', 'apps.lastBackupId', 'apps.lastBackupConfigJson', 'apps.oldConfigJson', 'apps.oauthProxy' ].join(',');
'apps.accessRestrictionJson', 'apps.lastBackupId', 'apps.lastBackupConfigJson', 'apps.oldConfigJson', 'apps.memoryLimit' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
@@ -92,8 +92,6 @@ function postProcess(result) {
result.portBindings[environmentVariables[i]] = parseInt(hostPorts[i], 10);
}
result.oauthProxy = !!result.oauthProxy;
assert(result.accessRestrictionJson === null || typeof result.accessRestrictionJson === 'string');
result.accessRestriction = safe.JSON.parse(result.accessRestrictionJson);
if (result.accessRestriction && !result.accessRestriction.users) result.accessRestriction.users = [];
@@ -179,7 +177,7 @@ function getAll(callback) {
});
}
function add(id, appStoreId, manifest, location, portBindings, accessRestriction, oauthProxy, callback) {
function add(id, appStoreId, manifest, location, portBindings, accessRestriction, memoryLimit, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof appStoreId, 'string');
assert(manifest && typeof manifest === 'object');
@@ -187,7 +185,7 @@ function add(id, appStoreId, manifest, location, portBindings, accessRestriction
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(typeof accessRestriction, 'object');
assert.strictEqual(typeof oauthProxy, 'boolean');
assert.strictEqual(typeof memoryLimit, 'number');
assert.strictEqual(typeof callback, 'function');
portBindings = portBindings || { };
@@ -197,8 +195,8 @@ function add(id, appStoreId, manifest, location, portBindings, accessRestriction
var queries = [ ];
queries.push({
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, oauthProxy) VALUES (?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, exports.ISTATE_PENDING_INSTALL, location, accessRestrictionJson, oauthProxy ]
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit) VALUES (?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, exports.ISTATE_PENDING_INSTALL, location, accessRestrictionJson, memoryLimit ]
});
Object.keys(portBindings).forEach(function (env) {
@@ -283,6 +281,7 @@ function updateWithConstraints(id, app, constraints, callback) {
assert.strictEqual(typeof constraints, 'string');
assert.strictEqual(typeof callback, 'function');
assert(!('portBindings' in app) || typeof app.portBindings === 'object');
assert(!('accessRestriction' in app) || typeof app.accessRestriction === 'object' || app.accessRestriction === '');
var queries = [ ];
@@ -368,7 +367,7 @@ function setInstallationCommand(appId, installationState, values, callback) {
updateWithConstraints(appId, values, '', callback);
} else if (installationState === exports.ISTATE_PENDING_RESTORE) {
updateWithConstraints(appId, values, 'AND (installationState = "installed" OR installationState = "error")', callback);
} else if (installationState === exports.ISTATE_PENDING_UPDATE || exports.ISTATE_PENDING_CONFIGURE || installationState == exports.ISTATE_PENDING_BACKUP) {
} else if (installationState === exports.ISTATE_PENDING_UPDATE || installationState === exports.ISTATE_PENDING_CONFIGURE || installationState === exports.ISTATE_PENDING_BACKUP) {
updateWithConstraints(appId, values, 'AND installationState = "installed"', callback);
} else {
callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, 'invalid installationState'));

View File

@@ -3,6 +3,7 @@
var appdb = require('./appdb.js'),
assert = require('assert'),
async = require('async'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:apphealthmonitor'),
docker = require('./docker.js').connection,
@@ -24,8 +25,11 @@ var gDockerEventStream = null;
function debugApp(app) {
assert(!app || typeof app === 'object');
var prefix = app ? app.location : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
var prefix = app ? (app.location || 'naked_domain') : '(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);
}
function setHealth(app, health, callback) {
@@ -89,6 +93,7 @@ function checkAppHealth(app, callback) {
var healthCheckUrl = 'http://127.0.0.1:' + app.httpPort + manifest.healthCheckPath;
superagent
.get(healthCheckUrl)
.set('Host', config.appFqdn(app.location)) // required for some apache configs with rewrite rules
.redirects(0)
.timeout(HEALTHCHECK_INTERVAL)
.end(function (error, res) {
@@ -114,7 +119,7 @@ function processApps(callback) {
var alive = apps
.filter(function (a) { return a.installationState === appdb.ISTATE_INSTALLED && a.runState === appdb.RSTATE_RUNNING && a.health === appdb.HEALTH_HEALTHY; })
.map(function (a) { return a.location; }).join(', ');
.map(function (a) { return (a.location || 'naked_domain') + '|' + a.manifest.id; }).join(', ');
debug('apps alive: [%s]', alive);

View File

@@ -9,7 +9,9 @@ exports = module.exports = {
get: get,
getBySubdomain: getBySubdomain,
getByIpAddress: getByIpAddress,
getAll: getAll,
getAllByUser: getAllByUser,
purchase: purchase,
install: install,
configure: configure,
@@ -22,6 +24,7 @@ exports = module.exports = {
backup: backup,
backupApp: backupApp,
listBackups: listBackups,
getLogs: getLogs,
@@ -55,8 +58,8 @@ var addons = require('./addons.js'),
debug = require('debug')('box:apps'),
docker = require('./docker.js'),
fs = require('fs'),
groups = require('./groups.js'),
manifestFormat = require('cloudron-manifestformat'),
once = require('once'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
@@ -73,8 +76,6 @@ var BACKUP_APP_CMD = path.join(__dirname, 'scripts/backupapp.sh'),
RESTORE_APP_CMD = path.join(__dirname, 'scripts/restoreapp.sh'),
BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function debugApp(app, args) {
assert(!app || typeof app === 'object');
@@ -194,9 +195,38 @@ function validateAccessRestriction(accessRestriction) {
if (accessRestriction === null) return null;
if (!accessRestriction.users || !Array.isArray(accessRestriction.users)) return new Error('users array property required');
if (accessRestriction.users.length === 0) return new Error('users array cannot be empty');
if (!accessRestriction.users.every(function (e) { return typeof e === 'string'; })) return new Error('All users have to be strings');
var noUsers = true, noGroups = true;
if (accessRestriction.users) {
if (!Array.isArray(accessRestriction.users)) return new Error('users array property required');
if (!accessRestriction.users.every(function (e) { return typeof e === 'string'; })) return new Error('All users have to be strings');
noUsers = accessRestriction.users.length === 0;
}
if (accessRestriction.groups) {
if (!Array.isArray(accessRestriction.groups)) return new Error('groups array property required');
if (!accessRestriction.groups.every(function (e) { return typeof e === 'string'; })) return new Error('All groups have to be strings');
noGroups = accessRestriction.groups.length === 0;
}
if (noUsers && noGroups) return new Error('users and groups array cannot both be empty');
return null;
}
function validateMemoryLimit(manifest, memoryLimit) {
assert.strictEqual(typeof manifest, 'object');
assert.strictEqual(typeof memoryLimit, 'number');
var min = manifest.memoryLimit || constants.DEFAULT_MEMORY_LIMIT;
var max = (4096 * 1024 * 1024);
// allow 0, which indicates that it is not set, the one from the manifest will be choosen but we don't commit any user value
// this is needed so an app update can change the value in the manifest, and if not set by the user, the new value should be used
if (memoryLimit === 0) return null;
if (memoryLimit < min) return new Error('memoryLimit too small');
if (memoryLimit > max) return new Error('memoryLimit too large');
return null;
}
@@ -228,12 +258,26 @@ function getIconUrlSync(app) {
return fs.existsSync(iconPath) ? '/api/v1/apps/' + app.id + '/icon' : null;
}
function hasAccessTo(app, user) {
function hasAccessTo(app, user, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof callback, 'function');
if (app.accessRestriction === null) return true;
return app.accessRestriction.users.some(function (e) { return e === user.id; });
if (app.accessRestriction === null) return callback(null, true);
// check user access
if (app.accessRestriction.users.some(function (e) { return e === user.id; })) return callback(null, true);
// check group access
if (!app.accessRestriction.groups) return callback(null, false);
async.some(app.accessRestriction.groups, function (groupId, iteratorDone) {
groups.isMember(groupId, user.id, function (error, member) {
iteratorDone(!error && member); // async.some does not take error argument in callback
});
}, function (result) {
callback(null, result);
});
}
function get(appId, callback) {
@@ -266,6 +310,25 @@ function getBySubdomain(subdomain, callback) {
});
}
function getByIpAddress(ip, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
docker.getContainerIdByIp(ip, function (error, containerId) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
appdb.getByContainerId(containerId, 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));
app.iconUrl = getIconUrlSync(app);
app.fqdn = config.appFqdn(app.location);
callback(null, app);
});
});
}
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -281,6 +344,21 @@ function getAll(callback) {
});
}
function getAllByUser(user, callback) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof callback, 'function');
getAll(function (error, result) {
if (error) return callback(error);
async.filter(result, function (app, callback) {
hasAccessTo(app, user, function (error, hasAccess) {
callback(hasAccess);
});
}, callback.bind(null, null)); // never error
});
}
function purchase(appStoreId, callback) {
assert.strictEqual(typeof appStoreId, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -288,6 +366,9 @@ function purchase(appStoreId, callback) {
// Skip purchase if appStoreId is empty
if (appStoreId === '') return callback(null);
// Skip if we don't have an appstore token
if (config.token() === '') return callback(null);
var url = config.apiServerOrigin() + '/api/v1/apps/' + appStoreId + '/purchase';
superagent.post(url).query({ token: config.token() }).end(function (error, res) {
@@ -300,17 +381,17 @@ function purchase(appStoreId, callback) {
});
}
function install(appId, appStoreId, manifest, location, portBindings, accessRestriction, oauthProxy, icon, cert, key, callback) {
function install(appId, appStoreId, manifest, location, portBindings, accessRestriction, icon, cert, key, memoryLimit, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof appStoreId, 'string');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(typeof accessRestriction, 'object');
assert.strictEqual(typeof oauthProxy, 'boolean');
assert(!icon || typeof icon === 'string');
assert(cert === null || typeof cert === 'string');
assert(key === null || typeof key === 'string');
assert.strictEqual(typeof memoryLimit, 'number');
assert.strictEqual(typeof callback, 'function');
var error = manifestFormat.parse(manifest);
@@ -328,6 +409,12 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
error = validateAccessRestriction(accessRestriction);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
error = validateMemoryLimit(manifest, memoryLimit);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
// memoryLimit might come in as 0 if not specified
memoryLimit = memoryLimit || manifest.memoryLimit || constants.DEFAULT_MEMORY_LIMIT;
// singleUser mode requires accessRestriction to contain exactly one user
if (manifest.singleUser && accessRestriction === null) return callback(new AppsError(AppsError.USER_REQUIRED));
if (manifest.singleUser && accessRestriction.users.length !== 1) return callback(new AppsError(AppsError.USER_REQUIRED));
@@ -348,7 +435,7 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
purchase(appStoreId, function (error) {
if (error) return callback(error);
appdb.add(appId, appStoreId, manifest, location.toLowerCase(), portBindings, accessRestriction, oauthProxy, function (error) {
appdb.add(appId, appStoreId, manifest, location.toLowerCase(), portBindings, accessRestriction, memoryLimit, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location.toLowerCase(), portBindings, error));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -365,14 +452,14 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
});
}
function configure(appId, location, portBindings, accessRestriction, oauthProxy, cert, key, callback) {
function configure(appId, location, portBindings, accessRestriction, cert, key, memoryLimit, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(typeof accessRestriction, 'object');
assert.strictEqual(typeof oauthProxy, 'boolean');
assert(cert === null || typeof cert === 'string');
assert(key === null || typeof key === 'string');
assert.strictEqual(typeof memoryLimit, 'number');
assert.strictEqual(typeof callback, 'function');
var error = validateHostname(location, config.fqdn());
@@ -391,6 +478,12 @@ function configure(appId, location, portBindings, accessRestriction, oauthProxy,
error = validatePortBindings(portBindings, app.manifest.tcpPorts);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
error = validateMemoryLimit(app.manifest, memoryLimit);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
// memoryLimit might come in as 0 if not specified
memoryLimit = memoryLimit || app.memoryLimit || app.manifest.memoryLimit || constants.DEFAULT_MEMORY_LIMIT;
// save cert to data/box/certs
if (cert && key) {
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
@@ -400,14 +493,14 @@ function configure(appId, location, portBindings, accessRestriction, oauthProxy,
var values = {
location: location.toLowerCase(),
accessRestriction: accessRestriction,
oauthProxy: oauthProxy,
portBindings: portBindings,
memoryLimit: memoryLimit,
oldConfig: {
location: app.location,
accessRestriction: app.accessRestriction,
portBindings: app.portBindings,
oauthProxy: app.oauthProxy
memoryLimit: app.memoryLimit
}
};
@@ -456,14 +549,30 @@ function update(appId, force, manifest, portBindings, icon, 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));
var appStoreId = app.appStoreId;
// 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
if (app.manifest.id !== manifest.id) {
if (!force) return callback(new AppsError(AppsError.BAD_FIELD, 'manifest id does not match. force to override'));
// clear appStoreId so that this app does not get updates anymore. this will mark is a dev app
appStoreId = '';
}
// Ensure we update the memory limit in case the new app requires more memory as a minimum
var memoryLimit = manifest.memoryLimit ? (app.memoryLimit < manifest.memoryLimit ? manifest.memoryLimit : app.memoryLimit) : app.memoryLimit;
var values = {
appStoreId: appStoreId,
manifest: manifest,
portBindings: portBindings,
memoryLimit: memoryLimit,
oldConfig: {
manifest: app.manifest,
portBindings: app.portBindings,
accessRestriction: app.accessRestriction,
oauthProxy: app.oauthProxy
memoryLimit: app.memoryLimit
}
};
@@ -549,12 +658,13 @@ function restore(appId, callback) {
values = {
manifest: restoreConfig.manifest,
portBindings: restoreConfig.portBindings,
memoryLimit: restoreConfig.memoryLimit,
oldConfig: {
location: app.location,
accessRestriction: app.accessRestriction,
oauthProxy: app.oauthProxy,
portBindings: app.portBindings,
memoryLimit: app.memoryLimit,
manifest: app.manifest
}
};
@@ -577,13 +687,13 @@ function uninstall(appId, callback) {
debug('Will uninstall app with id:%s', appId);
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_UNINSTALL, function (error) {
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));
taskmanager.stopAppTask(appId, function () {
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_UNINSTALL, function (error) {
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));
taskmanager.restartAppTask(appId); // since uninstall is allowed from any state, kill current task
callback(null);
taskmanager.startAppTask(appId, callback);
});
});
}
@@ -645,42 +755,35 @@ function exec(appId, options, 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));
var createOptions = {
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
return callback(new AppsError(AppsError.BAD_STATE, 'App not installed or running'));
}
var container = docker.connection.getContainer(app.containerId);
var execOptions = {
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
OpenStdin: true,
StdinOnce: false,
Tty: true
Tty: options.tty,
Cmd: cmd
};
docker.createSubcontainer(app, app.id + '-exec-' + Date.now(), cmd, createOptions, function (error, container) {
container.exec(execOptions, function (error, exec) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
container.attach({ stream: true, stdin: true, stdout: true, stderr: true }, function (error, stream) {
var startOptions = {
Detach: false,
Tty: options.tty,
stdin: true // this is a dockerode option that enabled openStdin in the modem
};
exec.start(startOptions, function(error, stream) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
docker.startContainer(container.id, function (error) {
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); });
}
if (options.rows && options.columns) {
container.resize({ h: options.rows, w: options.columns }, NOOP_CALLBACK);
}
var deleteContainer = once(docker.deleteContainer.bind(null, container.id, NOOP_CALLBACK));
container.wait(function (error) {
if (error) debug('Error waiting on container', error);
debug('exec: container finished', container.id);
deleteContainer();
});
stream.close = deleteContainer;
callback(null, stream);
});
return callback(null, stream);
});
});
});
@@ -773,16 +876,16 @@ function createNewBackup(app, addonsToBackup, callback) {
assert(!addonsToBackup || typeof addonsToBackup, 'object');
assert.strictEqual(typeof callback, 'function');
backups.getBackupUrl(app, function (error, result) {
backups.getAppBackupUrl(app, function (error, result) {
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
debugApp(app, 'backupApp: backup url:%s backup id:%s', result.url, result.id);
debugApp(app, 'backupApp: backup url:%s backup config url:%s', result.url, result.configUrl);
async.series([
ignoreError(shell.sudo.bind(null, 'mountSwap', [ BACKUP_SWAP_CMD, '--on' ])),
addons.backupAddons.bind(null, app, addonsToBackup),
shell.sudo.bind(null, 'backupApp', [ BACKUP_APP_CMD, app.id, result.url, result.backupKey, result.sessionToken ]),
shell.sudo.bind(null, 'backupApp', [ BACKUP_APP_CMD, app.id, result.url, result.configUrl, result.backupKey, result.sessionToken ]),
ignoreError(shell.sudo.bind(null, 'unmountSwap', [ BACKUP_SWAP_CMD, '--off' ])),
], function (error) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -813,7 +916,7 @@ function backupApp(app, addonsToBackup, callback) {
location: app.location,
portBindings: app.portBindings,
accessRestriction: app.accessRestriction,
oauthProxy: app.oauthProxy
memoryLimit: app.memoryLimit
};
backupFunction = createNewBackup.bind(null, app, addonsToBackup);
@@ -839,9 +942,9 @@ function backup(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
get(appId, function (error, app) {
if (error && error.reason === AppsError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
appdb.exists(appId, function (error, exists) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!exists) return callback(new AppsError(AppsError.NOT_FOUND));
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_BACKUP, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
@@ -854,13 +957,14 @@ function backup(appId, callback) {
});
}
function restoreApp(app, addonsToRestore, callback) {
function restoreApp(app, addonsToRestore, backupId, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof addonsToRestore, 'object');
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof callback, 'function');
assert(app.lastBackupId);
backups.getRestoreUrl(app.lastBackupId, function (error, result) {
backups.getRestoreUrl(backupId, function (error, result) {
if (error && error.reason == BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -873,3 +977,22 @@ function restoreApp(app, addonsToRestore, callback) {
});
});
}
function listBackups(page, perPage, appId, callback) {
assert(typeof page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0);
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
appdb.exists(appId, function (error, exists) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!exists) return callback(new AppsError(AppsError.NOT_FOUND));
backups.getByAppIdPaged(page, perPage, appId, function (error, results) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
callback(null, results);
});
});
}

View File

@@ -66,17 +66,22 @@ var COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd.config.ejs', {
CREATEAPPDIR_CMD = path.join(__dirname, 'scripts/createappdir.sh');
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
database.initialize(callback);
}
function debugApp(app, args) {
assert(!app || typeof app === 'object');
function debugApp(app) {
assert.strictEqual(typeof app, 'object');
var prefix = app ? (app.location || '(bare)') : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function reserveHttpPort(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var server = net.createServer();
server.listen(0, function () {
var port = server.address().port;
@@ -91,7 +96,10 @@ function reserveHttpPort(app, callback) {
});
}
function configureNginx(app, callback) {
function configureNginx(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var vhost = config.appFqdn(app.location);
certificates.ensureCertificate(vhost, function (error, certFilePath, keyFilePath) {
@@ -102,11 +110,16 @@ function configureNginx(app, callback) {
}
function unconfigureNginx(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
// TODO: maybe revoke the cert
nginx.unconfigureApp(app, callback);
}
function createContainer(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
assert(!app.containerId); // otherwise, it will trigger volumeFrom
debugApp(app, 'creating container');
@@ -119,6 +132,9 @@ function createContainer(app, callback) {
}
function deleteContainers(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'deleting containers');
docker.deleteContainers(app.id, function (error) {
@@ -129,10 +145,16 @@ function deleteContainers(app, callback) {
}
function createVolume(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
shell.sudo('createVolume', [ CREATEAPPDIR_CMD, app.id ], callback);
}
function deleteVolume(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
shell.sudo('deleteVolume', [ RMAPPDIR_CMD, app.id ], callback);
}
@@ -140,7 +162,7 @@ function allocateOAuthProxyCredentials(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (!app.oauthProxy) return callback(null);
if (!nginx.requiresOAuthProxy(app)) return callback(null);
var id = 'cid-' + uuid.v4();
var clientSecret = hat(256);
@@ -165,6 +187,9 @@ function removeOAuthProxyCredentials(app, callback) {
}
function addCollectdProfile(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var collectdConf = ejs.render(COLLECTD_CONFIG_EJS, { appId: app.id, containerId: app.containerId });
fs.writeFile(path.join(paths.COLLECTD_APPCONFIG_DIR, app.id + '.conf'), collectdConf, function (error) {
if (error) return callback(error);
@@ -173,6 +198,9 @@ function addCollectdProfile(app, callback) {
}
function removeCollectdProfile(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
fs.unlink(path.join(paths.COLLECTD_APPCONFIG_DIR, app.id + '.conf'), function (error) {
if (error && error.code !== 'ENOENT') debugApp(app, 'Error removing collectd profile', error);
shell.sudo('removeCollectdProfile', [ RELOAD_COLLECTD_CMD ], callback);
@@ -180,6 +208,9 @@ function removeCollectdProfile(app, callback) {
}
function verifyManifest(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Verifying manifest');
var manifest = app.manifest;
@@ -193,6 +224,9 @@ function verifyManifest(app, callback) {
}
function downloadIcon(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Downloading icon of %s@%s', app.appStoreId, app.manifest.version);
var iconUrl = config.apiServerOrigin() + '/api/v1/apps/' + app.appStoreId + '/versions/' + app.manifest.version + '/icon';
@@ -211,46 +245,64 @@ function downloadIcon(app, callback) {
}
function registerSubdomain(app, callback) {
// even though the bare domain is already registered in the appstore, we still
// need to register it so that we have a dnsRecordId to wait for it to complete
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Registering subdomain location [%s]', app.location);
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
subdomains.add(app.location, 'A', [ sysinfo.getIp() ], function (error, changeId) {
if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
sysinfo.getIp(function (error, ip) {
if (error) return callback(error);
retryCallback(null, error || changeId);
// even though the bare domain is already registered in the appstore, we still
// need to register it so that we have a dnsRecordId to wait for it to complete
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Registering subdomain location [%s]', app.location);
subdomains.add(app.location, 'A', [ ip ], function (error, changeId) {
if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(null, error || changeId);
});
}, function (error, result) {
if (error || result instanceof Error) return callback(error || result);
updateApp(app, { dnsRecordId: result }, callback);
});
}, function (error, result) {
if (error || result instanceof Error) return callback(error || result);
updateApp(app, { dnsRecordId: result }, callback);
});
}
function unregisterSubdomain(app, location, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof callback, 'function');
// do not unregister bare domain because we show a error/cloudron info page there
if (location === '') {
debugApp(app, 'Skip unregister of empty subdomain');
return callback(null);
}
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Unregistering subdomain: %s', location);
sysinfo.getIp(function (error, ip) {
if (error) return callback(error);
subdomains.remove(location, 'A', [ sysinfo.getIp() ], function (error) {
if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Unregistering subdomain: %s', location);
retryCallback(null, error);
subdomains.remove(location, 'A', [ ip ], function (error) {
if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.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);
});
}, function (error, result) {
if (error || result instanceof Error) return callback(error || result);
updateApp(app, { dnsRecordId: null }, callback);
});
}
function removeIcon(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
fs.unlink(path.join(paths.APPICONS_DIR, app.id + '.png'), function (error) {
if (error && error.code !== 'ENOENT') debugApp(app, 'cannot remove icon : %s', error);
callback(null);
@@ -258,6 +310,9 @@ function removeIcon(app, callback) {
}
function waitForDnsPropagation(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (!config.CLOUDRON) {
debugApp(app, 'Skipping dns propagation check for development');
return callback(null);
@@ -281,6 +336,10 @@ function waitForDnsPropagation(app, callback) {
// updates the app object and the database
function updateApp(app, values, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof values, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'updating app with values: %j', values);
appdb.update(app.id, values, function (error) {
@@ -305,6 +364,9 @@ function updateApp(app, values, callback) {
// - setup the container (requires image, volumes, addons)
// - setup collectd (requires container id)
function install(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
async.series([
verifyManifest.bind(null, app),
@@ -369,6 +431,9 @@ function install(app, callback) {
}
function backup(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
async.series([
updateApp.bind(null, app, { installationProgress: '10, Backing up' }),
apps.backupApp.bind(null, app, app.manifest.addons),
@@ -389,6 +454,9 @@ function backup(app, callback) {
// restore is also called for upgrades and infra updates. note that in those cases it is possible there is no backup
function restore(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
// we don't have a backup, same as re-install. this allows us to install from install failures (update failures always
// have a backupId)
if (!app.lastBackupId) {
@@ -396,6 +464,8 @@ function restore(app, callback) {
return install(app, callback);
}
var backupId = app.lastBackupId;
async.series([
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
unconfigureNginx.bind(null, app),
@@ -431,7 +501,7 @@ function restore(app, callback) {
createVolume.bind(null, app),
updateApp.bind(null, app, { installationProgress: '70, Download backup and restore addons' }),
apps.restoreApp.bind(null, app, app.manifest.addons),
apps.restoreApp.bind(null, app, app.manifest.addons, backupId),
updateApp.bind(null, app, { installationProgress: '75, Creating container' }),
createContainer.bind(null, app),
@@ -464,6 +534,9 @@ function restore(app, callback) {
// note that configure is called after an infra update as well
function configure(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
async.series([
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
unconfigureNginx.bind(null, app),
@@ -519,18 +592,27 @@ function configure(app, callback) {
// nginx configuration is skipped because app.httpPort is expected to be available
function update(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Updating to %s', safe.query(app, 'manifest.version'));
// app does not want these addons anymore
// FIXME: this does not handle option changes (like multipleDatabases)
var unusedAddons = _.omit(app.oldConfig.manifest.addons, Object.keys(app.manifest.addons));
async.series([
updateApp.bind(null, app, { installationProgress: '0, Verify manifest' }),
verifyManifest.bind(null, app),
// download new image before app is stopped. this is so we can reduce downtime
// and also not remove the 'common' layers when the old image is deleted
updateApp.bind(null, app, { installationProgress: '15, Downloading image' }),
docker.downloadImage.bind(null, app.manifest),
// note: we cleanup first and then backup. this is done so that the app is not running should backup fail
// we cannot easily 'recover' from backup failures because we have to revert manfest and portBindings
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
updateApp.bind(null, app, { installationProgress: '25, Cleaning up old install' }),
removeCollectdProfile.bind(null, app),
stopApp.bind(null, app),
deleteContainers.bind(null, app),
@@ -546,17 +628,14 @@ function update(app, callback) {
if (app.installationState === appdb.ISTATE_PENDING_FORCE_UPDATE) return next(null);
async.series([
updateApp.bind(null, app, { installationProgress: '20, Backup app' }),
updateApp.bind(null, app, { installationProgress: '30, Backup app' }),
apps.backupApp.bind(null, app, app.oldConfig.manifest.addons)
], next);
},
updateApp.bind(null, app, { installationProgress: '35, Downloading icon' }),
updateApp.bind(null, app, { installationProgress: '45, Downloading icon' }),
downloadIcon.bind(null, app),
updateApp.bind(null, app, { installationProgress: '45, Downloading image' }),
docker.downloadImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '70, Updating addons' }),
addons.setupAddons.bind(null, app, app.manifest.addons),
@@ -583,6 +662,9 @@ function update(app, callback) {
}
function uninstall(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'uninstalling');
async.series([
@@ -618,10 +700,19 @@ function uninstall(app, callback) {
updateApp.bind(null, app, { installationProgress: '95, Remove app from database' }),
appdb.del.bind(null, app.id)
], callback);
], function seriesDone(error) {
if (error) {
debugApp(app, 'error uninstalling app: %s', error);
return updateApp(app, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message }, callback.bind(null, error));
}
callback(null);
});
}
function runApp(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
docker.startContainer(app.containerId, function (error) {
if (error) return callback(error);
@@ -630,6 +721,9 @@ function runApp(app, callback) {
}
function stopApp(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
docker.stopContainers(app.id, function (error) {
if (error) return callback(error);
@@ -638,6 +732,9 @@ function stopApp(app, callback) {
}
function handleRunCommand(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (app.runState === appdb.RSTATE_PENDING_STOP) {
return stopApp(app, callback);
}
@@ -653,6 +750,9 @@ function handleRunCommand(app, callback) {
}
function startTask(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
// determine what to do
appdb.get(appId, function (error, app) {
if (error) return callback(error);
@@ -669,7 +769,7 @@ function startTask(appId, callback) {
case appdb.ISTATE_PENDING_INSTALL: return install(app, callback);
case appdb.ISTATE_PENDING_FORCE_UPDATE: return update(app, callback);
case appdb.ISTATE_ERROR:
debugApp(app, 'Apptask launched with error states.');
debugApp(app, 'Internal error. apptask launched with error status.');
return callback(null);
default:
debugApp(app, 'apptask launched with invalid command');

View File

@@ -16,6 +16,7 @@ var assert = require('assert'),
debug = require('debug')('box:auth'),
LocalStrategy = require('passport-local').Strategy,
crypto = require('crypto'),
groups = require('./groups'),
passport = require('passport'),
tokendb = require('./tokendb'),
user = require('./user'),
@@ -123,7 +124,14 @@ function initialize(callback) {
// amend the tokenType of the token owner
user.tokenType = tokenType;
callback(null, user, info);
// amend the admin flag
groups.isMember(groups.ADMIN_GROUP_ID, user.id, function (error, isAdmin) {
if (error) return callback(error);
user.admin = isAdmin;
callback(null, user, info);
});
});
});
}));

116
src/backupdb.js Normal file
View File

@@ -0,0 +1,116 @@
/* jslint node:true */
'use strict';
var assert = require('assert'),
database = require('./database.js'),
DatabaseError = require('./databaseerror.js'),
util = require('util');
var BACKUPS_FIELDS = [ 'filename', 'creationTime', 'version', 'type', 'dependsOn', 'state' ];
exports = module.exports = {
add: add,
getPaged: getPaged,
get: get,
del: del,
getByAppIdPaged: getByAppIdPaged,
_clear: clear,
BACKUP_TYPE_APP: 'app',
BACKUP_TYPE_BOX: 'box',
BACKUP_STATE_NORMAL: 'normal', // should rename to created to avoid listing in UI?
};
function postProcess(result) {
assert.strictEqual(typeof result, 'object');
result.dependsOn = result.dependsOn ? result.dependsOn.split(',') : [ ];
}
function getPaged(page, perPage, callback) {
assert(typeof page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0);
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? AND state = ? ORDER BY creationTime DESC LIMIT ?,?',
[ exports.BACKUP_TYPE_BOX, exports.BACKUP_STATE_NORMAL, (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
}
function getByAppIdPaged(page, perPage, appId, callback) {
assert(typeof page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0);
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? AND state = ? AND filename LIKE ? ORDER BY creationTime DESC LIMIT ?,?',
[ exports.BACKUP_TYPE_APP, exports.BACKUP_STATE_NORMAL, 'appbackup\\_' + appId + '\\_%', (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
}
function get(filename, callback) {
assert.strictEqual(typeof filename, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE filename = ? AND type = ? AND state = ? ORDER BY creationTime DESC',
[ filename, exports.BACKUP_TYPE_BOX, exports.BACKUP_STATE_NORMAL ], 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]);
callback(null, result[0]);
});
}
function add(backup, callback) {
assert(backup && typeof backup === 'object');
assert.strictEqual(typeof backup.filename, 'string');
assert.strictEqual(typeof backup.version, 'string');
assert(backup.type === exports.BACKUP_TYPE_APP || backup.type === exports.BACKUP_TYPE_BOX);
assert(util.isArray(backup.dependsOn));
assert.strictEqual(typeof callback, 'function');
var creationTime = backup.creationTime || new Date(); // allow tests to set the time
database.query('INSERT INTO backups (filename, version, type, creationTime, state, dependsOn) VALUES (?, ?, ?, ?, ?, ?)',
[ backup.filename, backup.version, backup.type, creationTime, exports.BACKUP_STATE_NORMAL, backup.dependsOn.join(',') ],
function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('TRUNCATE TABLE backups', [], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
function del(filename, callback) {
assert.strictEqual(typeof filename, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM backups WHERE filename=?', [ filename ], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}

View File

@@ -3,15 +3,18 @@
exports = module.exports = {
BackupsError: BackupsError,
getAllPaged: getAllPaged,
getPaged: getPaged,
getByAppIdPaged: getByAppIdPaged,
getBackupUrl: getBackupUrl,
getAppBackupUrl: getAppBackupUrl,
getRestoreUrl: getRestoreUrl,
copyLastBackup: copyLastBackup
};
var assert = require('assert'),
backupdb = require('./backupdb.js'),
caas = require('./storage/caas.js'),
config = require('./config.js'),
debug = require('debug')('box:backups'),
@@ -51,32 +54,38 @@ function api(provider) {
}
}
function getAllPaged(page, perPage, callback) {
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
function getPaged(page, perPage, callback) {
assert(typeof page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0);
assert.strictEqual(typeof callback, 'function');
settings.getBackupConfig(function (error, backupConfig) {
backupdb.getPaged(page, perPage, function (error, results) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
api(backupConfig.provider).getAllPaged(backupConfig, page, perPage, function (error, backups) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
return callback(null, backups); // [ { creationTime, boxVersion, restoreKey, dependsOn: [ ] } ] sorted by time (latest first
});
callback(null, results);
});
}
function getBackupUrl(app, callback) {
assert(!app || typeof app === 'object');
function getByAppIdPaged(page, perPage, appId, callback) {
assert(typeof page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0);
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
var filename = '';
if (app) {
filename = util.format('appbackup_%s_%s-v%s.tar.gz', app.id, (new Date()).toISOString(), app.manifest.version);
} else {
filename = util.format('backup_%s-v%s.tar.gz', (new Date()).toISOString(), config.version());
}
backupdb.getByAppIdPaged(page, perPage, appId, function (error, results) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
callback(null, results);
});
}
function getBackupUrl(appBackupIds, callback) {
assert(util.isArray(appBackupIds));
assert.strictEqual(typeof callback, 'function');
var now = new Date();
var filebase = util.format('backup_%s-v%s', now.toISOString(), config.version());
var filename = filebase + '.tar.gz';
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
@@ -93,7 +102,48 @@ function getBackupUrl(app, callback) {
debug('getBackupUrl: id:%s url:%s sessionToken:%s backupKey:%s', obj.id, obj.url, obj.sessionToken, obj.backupKey);
callback(null, obj);
backupdb.add({ filename: filename, creationTime: now, version: config.version(), type: backupdb.BACKUP_TYPE_BOX, dependsOn: appBackupIds }, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
callback(null, obj);
});
});
});
}
function getAppBackupUrl(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var now = new Date();
var filebase = util.format('appbackup_%s_%s-v%s', app.id, now.toISOString(), app.manifest.version);
var configFilename = filebase + '.json', dataFilename = filebase + '.tar.gz';
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
api(backupConfig.provider).getSignedUploadUrl(backupConfig, configFilename, function (error, configResult) {
if (error) return callback(error);
api(backupConfig.provider).getSignedUploadUrl(backupConfig, dataFilename, function (error, dataResult) {
if (error) return callback(error);
var obj = {
id: dataFilename,
url: dataResult.url,
configUrl: configResult.url,
sessionToken: dataResult.sessionToken, // this token can be used for both config and data upload
backupKey: backupConfig.key // only data is encrypted
};
debug('getAppBackupUrl: %j', obj);
backupdb.add({ filename: dataFilename, creationTime: now, version: app.manifest.version, type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ] }, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
callback(null, obj);
});
});
});
});
}
@@ -124,19 +174,27 @@ function getRestoreUrl(backupId, callback) {
}
function copyLastBackup(app, callback) {
assert(app && typeof app === 'object');
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof app.lastBackupId, 'string');
assert.strictEqual(typeof callback, 'function');
var toFilename = util.format('appbackup_%s_%s-v%s.tar.gz', app.id, (new Date()).toISOString(), app.manifest.version);
var toFilenameArchive = util.format('appbackup_%s_%s-v%s.tar.gz', app.id, (new Date()).toISOString(), app.manifest.version);
var toFilenameConfig = util.format('appbackup_%s_%s-v%s.json', app.id, (new Date()).toISOString(), app.manifest.version);
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
api(backupConfig.provider).copyObject(backupConfig, app.lastBackupId, toFilename, function (error) {
api(backupConfig.provider).copyObject(backupConfig, app.lastBackupId, toFilenameArchive, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
return callback(null, toFilename);
// TODO change that logic by adjusting app.lastBackupId to not contain the file type
var configFileId = app.lastBackupId.slice(0, -'.tar.gz'.length) + '.json';
api(backupConfig.provider).copyObject(backupConfig, configFileId, toFilenameConfig, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
return callback(null, toFilenameArchive);
});
});
});
}

View File

@@ -1,13 +1,11 @@
/* jslint node:true */
'use strict';
var assert = require('assert'),
async = require('async'),
config = require('../config.js'),
crypto = require('crypto'),
debug = require('debug')('box:cert/acme'),
fs = require('fs'),
parseLinks = require('parse-links'),
path = require('path'),
paths = require('../paths.js'),
safe = require('safetydance'),
@@ -18,7 +16,6 @@ var assert = require('assert'),
var CA_PROD = 'https://acme-v01.api.letsencrypt.org',
CA_STAGING = 'https://acme-staging.api.letsencrypt.org',
CA_ORIGIN = CA_PROD,
LE_AGREEMENT = 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf';
exports = module.exports = {
@@ -54,14 +51,22 @@ AcmeError.FORBIDDEN = 'Forbidden';
// https://www.ietf.org/proceedings/92/slides/slides-92-acme-1.pdf
// https://community.letsencrypt.org/t/list-of-client-implementations/2103
function getNonce(callback) {
superagent.get(CA_ORIGIN + '/directory', function (error, response) {
function Acme(options) {
assert.strictEqual(typeof options, 'object');
this.caOrigin = options.prod ? CA_PROD : CA_STAGING;
this.accountKeyPem = null; // Buffer
this.email = options.email;
}
Acme.prototype.getNonce = function (callback) {
superagent.get(this.caOrigin + '/directory', function (error, response) {
if (error) return callback(error);
if (response.statusCode !== 200) return callback(new Error('Invalid response code when fetching nonce : ' + response.statusCode));
return callback(null, response.headers['Replay-Nonce'.toLowerCase()]);
});
}
};
// urlsafe base64 encoding (jose)
function urlBase64Encode(string) {
@@ -73,13 +78,13 @@ function b64(str) {
return urlBase64Encode(buf.toString('base64'));
}
function sendSignedRequest(url, accountKeyPem, payload, callback) {
Acme.prototype.sendSignedRequest = function (url, payload, callback) {
assert.strictEqual(typeof url, 'string');
assert(util.isBuffer(accountKeyPem));
assert.strictEqual(typeof payload, 'string');
assert.strictEqual(typeof callback, 'function');
var privateKey = ursa.createPrivateKey(accountKeyPem);
assert(util.isBuffer(this.accountKeyPem));
var privateKey = ursa.createPrivateKey(this.accountKeyPem);
var header = {
alg: 'RS256',
@@ -92,7 +97,7 @@ function sendSignedRequest(url, accountKeyPem, payload, callback) {
var payload64 = b64(payload);
getNonce(function (error, nonce) {
this.getNonce(function (error, nonce) {
if (error) return callback(error);
debug('sendSignedRequest: using nonce %s for url %s', nonce, url);
@@ -116,34 +121,56 @@ function sendSignedRequest(url, accountKeyPem, payload, callback) {
callback(null, res);
});
});
}
};
function registerUser(accountKeyPem, email, callback) {
assert(util.isBuffer(accountKeyPem));
assert.strictEqual(typeof email, 'string');
Acme.prototype.updateContact = function (registrationUri, callback) {
assert.strictEqual(typeof registrationUri, 'string');
assert.strictEqual(typeof callback, 'function');
debug('updateContact: %s %s', registrationUri, this.email);
// https://github.com/ietf-wg-acme/acme/issues/30
var payload = {
resource: 'reg',
contact: [ 'mailto:' + this.email ],
agreement: LE_AGREEMENT
};
var that = this;
this.sendSignedRequest(registrationUri, JSON.stringify(payload), function (error, result) {
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
if (result.statusCode !== 202) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to update contact. Expecting 202, got %s %s', result.statusCode, result.text)));
debug('updateContact: contact of user updated to %s', that.email);
callback();
});
};
Acme.prototype.registerUser = function (callback) {
assert.strictEqual(typeof callback, 'function');
var payload = {
resource: 'new-reg',
contact: [ 'mailto:' + email ],
contact: [ 'mailto:' + this.email ],
agreement: LE_AGREEMENT
};
debug('registerUser: %s', email);
debug('registerUser: %s', this.email);
sendSignedRequest(CA_ORIGIN + '/acme/new-reg', accountKeyPem, JSON.stringify(payload), function (error, result) {
var that = this;
this.sendSignedRequest(this.caOrigin + '/acme/new-reg', JSON.stringify(payload), function (error, result) {
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
if (result.statusCode === 409) return callback(new AcmeError(AcmeError.ALREADY_EXISTS, result.body.detail));
if (result.statusCode === 409) return that.updateContact(result.headers.location, callback); // already exists
if (result.statusCode !== 201) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
debug('registerUser: registered user %s', email);
debug('registerUser: registered user %s', that.email);
callback();
callback(null);
});
}
};
function registerDomain(accountKeyPem, domain, callback) {
assert(util.isBuffer(accountKeyPem));
Acme.prototype.registerDomain = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -157,7 +184,7 @@ function registerDomain(accountKeyPem, domain, callback) {
debug('registerDomain: %s', domain);
sendSignedRequest(CA_ORIGIN + '/acme/new-authz', accountKeyPem, JSON.stringify(payload), function (error, result) {
this.sendSignedRequest(this.caOrigin + '/acme/new-authz', JSON.stringify(payload), function (error, result) {
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when registering domain: ' + error.message));
if (result.statusCode === 403) return callback(new AcmeError(AcmeError.FORBIDDEN, result.body.detail));
if (result.statusCode !== 201) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
@@ -166,10 +193,9 @@ function registerDomain(accountKeyPem, domain, callback) {
callback(null, result.body);
});
}
};
function prepareHttpChallenge(accountKeyPem, challenge, callback) {
assert(util.isBuffer(accountKeyPem));
Acme.prototype.prepareHttpChallenge = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -177,7 +203,8 @@ function prepareHttpChallenge(accountKeyPem, challenge, callback) {
var token = challenge.token;
var privateKey = ursa.createPrivateKey(accountKeyPem);
assert(util.isBuffer(this.accountKeyPem));
var privateKey = ursa.createPrivateKey(this.accountKeyPem);
var jwk = {
e: b64(privateKey.getExponent()),
@@ -197,10 +224,9 @@ function prepareHttpChallenge(accountKeyPem, challenge, callback) {
callback();
});
}
};
function notifyChallengeReady(accountKeyPem, challenge, callback) {
assert(util.isBuffer(accountKeyPem));
Acme.prototype.notifyChallengeReady = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -213,15 +239,15 @@ function notifyChallengeReady(accountKeyPem, challenge, callback) {
keyAuthorization: keyAuthorization
};
sendSignedRequest(challenge.uri, accountKeyPem, JSON.stringify(payload), function (error, result) {
this.sendSignedRequest(challenge.uri, JSON.stringify(payload), function (error, result) {
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when notifying challenge: ' + error.message));
if (result.statusCode !== 202) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 202, got %s %s', result.statusCode, result.text)));
callback();
});
}
};
function waitForChallenge(challenge, callback) {
Acme.prototype.waitForChallenge = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -250,11 +276,10 @@ function waitForChallenge(challenge, callback) {
// async.retry will pass 'undefined' as second arg making it unusable with async.waterfall()
callback(error);
});
}
};
// https://community.letsencrypt.org/t/public-beta-rate-limits/4772 for rate limits
function signCertificate(accountKeyPem, domain, csrDer, callback) {
assert(util.isBuffer(accountKeyPem));
Acme.prototype.signCertificate = function (domain, csrDer, callback) {
assert.strictEqual(typeof domain, 'string');
assert(util.isBuffer(csrDer));
assert.strictEqual(typeof callback, 'function');
@@ -268,7 +293,7 @@ function signCertificate(accountKeyPem, domain, csrDer, callback) {
debug('signCertificate: sending new-cert request');
sendSignedRequest(CA_ORIGIN + '/acme/new-cert', accountKeyPem, JSON.stringify(payload), function (error, result) {
this.sendSignedRequest(this.caOrigin + '/acme/new-cert', JSON.stringify(payload), function (error, result) {
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when signing certificate: ' + error.message));
// 429 means we reached the cert limit for this domain
if (result.statusCode !== 201) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 201, got %s %s', result.statusCode, result.text)));
@@ -277,42 +302,75 @@ function signCertificate(accountKeyPem, domain, csrDer, callback) {
if (!certUrl) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Missing location in downloadCertificate'));
safe.fs.writeFileSync(path.join(outdir, domain + '.url'), certUrl, 'utf8'); // for renewal
safe.fs.writeFileSync(path.join(outdir, domain + '.url'), certUrl, 'utf8'); // maybe use for renewal
return callback(null, result.headers.location);
});
}
};
function createKeyAndCsr(domain, callback) {
Acme.prototype.createKeyAndCsr = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
var csrFile = path.join(outdir, domain + '.csr');
var privateKeyFile = path.join(outdir, domain + '.key');
var execSync = safe.child_process.execSync;
var privateKeyFile = path.join(outdir, domain + '.key');
var key = execSync('openssl genrsa 4096');
if (!key) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
if (safe.fs.existsSync(privateKeyFile)) {
// in some old releases, csr file was corrupt. so always regenerate it
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
} else {
var key = execSync('openssl genrsa 4096');
if (!key) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
}
var csrDer = execSync(util.format('openssl req -new -key %s -outform DER -subj /CN=%s', privateKeyFile, domain));
if (!csrDer) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
var csrFile = path.join(outdir, domain + '.csr');
if (!safe.fs.writeFileSync(csrFile, csrFile)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error)); // bookkeeping
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFile);
callback(null, csrDer);
}
};
function downloadCertificate(domain, certUrl, callback) {
// TODO: download the chain in a loop following 'up' header
Acme.prototype.downloadChain = function (linkHeader, callback) {
if (!linkHeader) return new AcmeError(AcmeError.EXTERNAL_ERROR, 'Empty link header when downloading certificate chain');
var linkInfo = parseLinks(linkHeader);
if (!linkInfo || !linkInfo.up) return new AcmeError(AcmeError.EXTERNAL_ERROR, 'Failed to parse link header when downloading certificate chain');
debug('downloadChain: downloading from %s', this.caOrigin + linkInfo.up);
superagent.get(this.caOrigin + linkInfo.up).buffer().parse(function (res, done) {
var data = [ ];
res.on('data', function(chunk) { data.push(chunk); });
res.on('end', function () { res.text = Buffer.concat(data); done(); });
}).end(function (error, result) {
if (error && !error.response) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when downloading certificate'));
if (result.statusCode !== 200) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
var chainDer = result.text;
var execSync = safe.child_process.execSync;
var chainPem = execSync('openssl x509 -inform DER -outform PEM', { input: chainDer }); // this is really just base64 encoding with header
if (!chainPem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
callback(null, chainPem);
});
};
Acme.prototype.downloadCertificate = function (domain, certUrl, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof certUrl, 'string');
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
var that = this;
superagent.get(certUrl).buffer().parse(function (res, done) {
var data = [ ];
@@ -327,50 +385,45 @@ function downloadCertificate(domain, certUrl, callback) {
var execSync = safe.child_process.execSync;
safe.fs.writeFileSync(path.join(outdir, domain + '.der'), certificateDer);
debug('downloadCertificate: cert der file saved');
debug('downloadCertificate: cert der file for %s saved', domain);
var certificatePem = execSync('openssl x509 -inform DER -outform PEM', { input: certificateDer }); // this is really just base64 encoding with header
if (!certificatePem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
var chainPem = safe.fs.readFileSync(__dirname + '/lets-encrypt-x1-cross-signed.pem.txt');
if (!chainPem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
that.downloadChain(result.header['link'], function (error, chainPem) {
if (error) return callback(error);
var certificateFile = path.join(outdir, domain + '.cert');
var fullChainPem = Buffer.concat([certificatePem, chainPem]);
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
var certificateFile = path.join(outdir, domain + '.cert');
var fullChainPem = Buffer.concat([certificatePem, chainPem]);
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
debug('downloadCertificate: cert file saved at %s', certificateFile);
debug('downloadCertificate: cert file for %s saved at %s', domain, certificateFile);
callback();
callback();
});
});
}
};
function acmeFlow(domain, callback) {
Acme.prototype.acmeFlow = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
// we cannot use admin@fqdn because the user might not have set it up.
// we cannot use owner email because we don't have it yet (the admin cert is fetched before activation)
// one option is to update the owner email when a second cert is requested (https://github.com/ietf-wg-acme/acme/issues/30)
var email = 'admin@cloudron.io';
var accountKeyPem;
if (!fs.existsSync(paths.ACME_ACCOUNT_KEY_FILE)) {
debug('getCertificate: generating acme account key on first run');
accountKeyPem = safe.child_process.execSync('openssl genrsa 4096');
if (!accountKeyPem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
this.accountKeyPem = safe.child_process.execSync('openssl genrsa 4096');
if (!this.accountKeyPem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
safe.fs.writeFileSync(paths.ACME_ACCOUNT_KEY_FILE, accountKeyPem);
safe.fs.writeFileSync(paths.ACME_ACCOUNT_KEY_FILE, this.accountKeyPem);
} else {
debug('getCertificate: using existing acme account key');
accountKeyPem = fs.readFileSync(paths.ACME_ACCOUNT_KEY_FILE);
this.accountKeyPem = fs.readFileSync(paths.ACME_ACCOUNT_KEY_FILE);
}
registerUser(accountKeyPem, email, function (error) {
if (error && error.reason !== AcmeError.ALREADY_EXISTS) return callback(error);
var that = this;
this.registerUser(function (error) {
if (error) return callback(error);
registerDomain(accountKeyPem, domain, function (error, result) {
that.registerDomain(domain, function (error, result) {
if (error) return callback(error);
debug('acmeFlow: challenges: %j', result);
@@ -380,35 +433,35 @@ function acmeFlow(domain, callback) {
var challenge = httpChallenges[0];
async.waterfall([
prepareHttpChallenge.bind(null, accountKeyPem, challenge),
notifyChallengeReady.bind(null, accountKeyPem, challenge),
waitForChallenge.bind(null, challenge),
createKeyAndCsr.bind(null, domain),
signCertificate.bind(null, accountKeyPem, domain),
downloadCertificate.bind(null, domain)
that.prepareHttpChallenge.bind(that, challenge),
that.notifyChallengeReady.bind(that, challenge),
that.waitForChallenge.bind(that, challenge),
that.createKeyAndCsr.bind(that, domain),
that.signCertificate.bind(that, domain),
that.downloadCertificate.bind(that, domain)
], callback);
});
});
}
};
function getCertificate(domain, callback) {
Acme.prototype.getCertificate = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
var certUrl = safe.fs.readFileSync(path.join(outdir, domain + '.url'), 'utf8');
var certificateGetter;
if (certUrl) {
debug('getCertificate: renewing existing cert for %s from %s', domain, certUrl);
certificateGetter = downloadCertificate.bind(null, domain, certUrl);
} else {
debug('getCertificate: start acme flow for %s', domain);
certificateGetter = acmeFlow.bind(null, domain);
}
certificateGetter(function (error) {
debug('getCertificate: start acme flow for %s from %s', domain, this.caOrigin);
this.acmeFlow(domain, function (error) {
if (error) return callback(error);
var outdir = paths.APP_CERTS_DIR;
callback(null, path.join(outdir, domain + '.cert'), path.join(outdir, domain + '.key'));
});
};
function getCertificate(domain, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var acme = new Acme(options || { });
acme.getCertificate(domain, callback);
}

View File

@@ -7,8 +7,9 @@ exports = module.exports = {
var assert = require('assert'),
debug = require('debug')('box:cert/caas.js');
function getCertificate(domain, callback) {
function getCertificate(domain, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debug('getCertificate: using fallback certificate', domain);

View File

@@ -1,27 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIEqDCCA5CgAwIBAgIRAJgT9HUT5XULQ+dDHpceRL0wDQYJKoZIhvcNAQELBQAw
PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD
Ew5EU1QgUm9vdCBDQSBYMzAeFw0xNTEwMTkyMjMzMzZaFw0yMDEwMTkyMjMzMzZa
MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD
ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMTCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAJzTDPBa5S5Ht3JdN4OzaGMw6tc1Jhkl4b2+NfFwki+3uEtB
BaupnjUIWOyxKsRohwuj43Xk5vOnYnG6eYFgH9eRmp/z0HhncchpDpWRz/7mmelg
PEjMfspNdxIknUcbWuu57B43ABycrHunBerOSuu9QeU2mLnL/W08lmjfIypCkAyG
dGfIf6WauFJhFBM/ZemCh8vb+g5W9oaJ84U/l4avsNwa72sNlRZ9xCugZbKZBDZ1
gGusSvMbkEl4L6KWTyogJSkExnTA0DHNjzE4lRa6qDO4Q/GxH8Mwf6J5MRM9LTb4
4/zyM2q5OTHFr8SNDR1kFjOq+oQpttQLwNh9w5MCAwEAAaOCAZIwggGOMBIGA1Ud
EwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMH8GCCsGAQUFBwEBBHMwcTAy
BggrBgEFBQcwAYYmaHR0cDovL2lzcmcudHJ1c3RpZC5vY3NwLmlkZW50cnVzdC5j
b20wOwYIKwYBBQUHMAKGL2h0dHA6Ly9hcHBzLmlkZW50cnVzdC5jb20vcm9vdHMv
ZHN0cm9vdGNheDMucDdjMB8GA1UdIwQYMBaAFMSnsaR7LHH62+FLkHX/xBVghYkQ
MFQGA1UdIARNMEswCAYGZ4EMAQIBMD8GCysGAQQBgt8TAQEBMDAwLgYIKwYBBQUH
AgEWImh0dHA6Ly9jcHMucm9vdC14MS5sZXRzZW5jcnlwdC5vcmcwPAYDVR0fBDUw
MzAxoC+gLYYraHR0cDovL2NybC5pZGVudHJ1c3QuY29tL0RTVFJPT1RDQVgzQ1JM
LmNybDATBgNVHR4EDDAKoQgwBoIELm1pbDAdBgNVHQ4EFgQUqEpqYwR93brm0Tm3
pkVl7/Oo7KEwDQYJKoZIhvcNAQELBQADggEBANHIIkus7+MJiZZQsY14cCoBG1hd
v0J20/FyWo5ppnfjL78S2k4s2GLRJ7iD9ZDKErndvbNFGcsW+9kKK/TnY21hp4Dd
ITv8S9ZYQ7oaoqs7HwhEMY9sibED4aXw09xrJZTC9zK1uIfW6t5dHQjuOWv+HHoW
ZnupyxpsEUlEaFb+/SCI4KCSBdAsYxAcsHYI5xxEI4LutHp6s3OT2FuO90WfdsIk
6q78OMSdn875bNjdBYAqxUp2/LEIHfDBkLoQz0hFJmwAbYahqKaLn73PAAm1X2kj
f1w8DdnkabOLGeOVcj9LQ+s67vBykx4anTjURkbqZslUEUsn2k5xeua2zUk=
-----END CERTIFICATE-----

View File

@@ -1,8 +1,7 @@
/* jslint node:true */
'use strict';
var acme = require('./cert/acme.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
caas = require('./cert/caas.js'),
@@ -11,12 +10,15 @@ var acme = require('./cert/acme.js'),
constants = require('./constants.js'),
debug = require('debug')('box:src/certificates'),
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'),
sysinfo = require('./sysinfo.js'),
tld = require('tldjs'),
user = require('./user.js'),
util = require('util'),
waitForDns = require('./waitfordns.js'),
x509 = require('x509');
@@ -61,7 +63,18 @@ function getApi(callback) {
var api = tlsConfig.provider === 'caas' ? caas : acme;
callback(null, api);
var options = { };
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 ? 'admin@cloudron.io' : owner.email; // can error if not activated yet
callback(null, api, options);
});
});
}
@@ -73,54 +86,104 @@ function installAdminCertificate(callback) {
if (tlsConfig.provider === 'caas') return callback();
waitForDns(config.adminFqdn(), sysinfo.getIp(), config.fqdn(), function (error) {
if (error) return callback(error); // this cannot happen because we retry forever
sysinfo.getIp(function (error, ip) {
if (error) return callback(error);
ensureCertificate(config.adminFqdn(), function (error, certFilePath, keyFilePath) {
if (error) { // currently, this can never happen
debug('Error obtaining certificate. Proceed anyway', error);
return callback();
}
var zoneName = tld.getDomain(config.fqdn());
waitForDns(config.adminFqdn(), ip, zoneName, function (error) {
if (error) return callback(error); // this cannot happen because we retry forever
nginx.configureAdmin(certFilePath, keyFilePath, callback);
ensureCertificate(config.adminFqdn(), function (error, certFilePath, keyFilePath) {
if (error) { // currently, this can never happen
debug('Error obtaining certificate. Proceed anyway', error);
return callback();
}
nginx.configureAdmin(certFilePath, keyFilePath, callback);
});
});
});
});
}
function needsRenewalSync(certFilePath) {
var result = safe.child_process.execSync('openssl x509 -checkend %s -in %s', 60 * 60 * 24 * 5, certFilePath);
function isExpiringSync(certFilePath, hours) {
assert.strictEqual(typeof certFilePath, 'string');
assert.strictEqual(typeof hours, 'number');
return result === null; // command errored
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 autoRenew(callback) {
debug('autoRenew: Checking certificates for renewal');
callback = callback || NOOP_CALLBACK;
var filenames = safe.fs.readdirSync(paths.APP_CERTS_DIR);
if (!filenames) {
debug('autoRenew: Error getting filenames: %s', safe.error.message);
return;
}
var certs = filenames.filter(function (f) {
return f.match(/\.cert$/) !== null && needsRenewalSync(path.join(paths.APP_CERTS_DIR, f));
});
debug('autoRenew: %j needs to be renewed', certs);
getApi(function (error, api) {
apps.getAll(function (error, allApps) {
if (error) return callback(error);
async.eachSeries(certs, function iterator(cert, iteratorCallback) {
var domain = cert.match(/^(.*)\.cert$/)[1];
if (domain === 'host') return iteratorCallback(); // cannot renew fallback cert
allApps.push({ location: 'my' }); // inject fake webadmin app
api.getCertificate(domain, function (error) {
if (error) debug('autoRenew: could not renew cert for %s', domain, error);
var expiringApps = [ ];
for (var i = 0; i < allApps.length; i++) {
var appDomain = config.appFqdn(allApps[i].location);
var certFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.key');
iteratorCallback(); // move on to next cert
if (!safe.fs.existsSync(keyFilePath)) {
debug('autoRenew: no existing key file for %s. skipping', appDomain);
continue;
}
if (isExpiringSync(certFilePath, 24 * 30)) { // expired or not found
expiringApps.push(allApps[i]);
}
}
debug('autoRenew: %j needs to be renewed', expiringApps.map(function (a) { return config.appFqdn(a.location); }));
getApi(function (error, api, apiOptions) {
if (error) return callback(error);
async.eachSeries(expiringApps, function iterator(app, iteratorCallback) {
var domain = config.appFqdn(app.location);
debug('autoRenew: 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');
mailer.certificateRenewed(domain, error ? error.message : '');
if (error) {
debug('autoRenew: could not renew cert for %s because %s', domain, error);
// check if we should fallback if we expire in the coming day
if (!isExpiringSync(certFilePath, 24 * 1)) return iteratorCallback();
debug('autoRenew: using fallback certs for %s since it expires soon', domain, error);
certFilePath = 'cert/host.cert';
keyFilePath = 'cert/host.key';
} else {
debug('autoRenew: 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.location === constants.ADMIN_LOCATION ?
nginx.configureAdmin.bind(null, certFilePath, keyFilePath)
: nginx.configureApp.bind(null, app, certFilePath, keyFilePath);
configureFunc(function (ignoredError) {
if (ignoredError) debug('fallbackExpiredCertificates: error reconfiguring app', ignoredError);
iteratorCallback(); // move to next app
});
});
});
});
});
@@ -219,17 +282,17 @@ function ensureCertificate(domain, callback) {
if (fs.existsSync(userCertFilePath) && fs.existsSync(userKeyFilePath)) {
debug('ensureCertificate: %s. certificate already exists at %s', domain, userKeyFilePath);
if (!needsRenewalSync(userCertFilePath)) return callback(null, userCertFilePath, userKeyFilePath);
debug('ensureCertificate: %s cert require renewal', domain);
if (!isExpiringSync(userCertFilePath, 24 * 1)) return callback(null, userCertFilePath, userKeyFilePath);
}
getApi(function (error, api) {
debug('ensureCertificate: %s cert require renewal', domain);
getApi(function (error, api, apiOptions) {
if (error) return callback(error);
debug('ensureCertificate: getting certificate for %s', domain);
debug('ensureCertificate: getting certificate for %s with options %j', domain, apiOptions);
api.getCertificate(domain, function (error, certFilePath, keyFilePath) {
api.getCertificate(domain, apiOptions, function (error, certFilePath, keyFilePath) {
if (error) {
debug('ensureCertificate: could not get certificate. using fallback certs', error);
return callback(null, 'cert/host.cert', 'cert/host.key'); // use fallback certs

View File

@@ -13,14 +13,17 @@ exports = module.exports = {
sendHeartbeat: sendHeartbeat,
updateToLatest: updateToLatest,
update: update,
reboot: reboot,
migrate: migrate,
backup: backup,
retire: retire,
ensureBackup: ensureBackup,
isConfiguredSync: isConfiguredSync,
checkDiskSpace: checkDiskSpace,
events: new (require('events').EventEmitter)(),
EVENT_ACTIVATED: 'activated',
@@ -33,12 +36,14 @@ var apps = require('./apps.js'),
async = require('async'),
backups = require('./backups.js'),
BackupsError = require('./backups.js').BackupsError,
bytes = require('bytes'),
clientdb = require('./clientdb.js'),
config = require('./config.js'),
debug = require('debug')('box:cloudron'),
df = require('node-df'),
fs = require('fs'),
locker = require('./locker.js'),
mailer = require('./mailer.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
progress = require('./progress.js'),
@@ -59,7 +64,8 @@ var apps = require('./apps.js'),
var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'),
BACKUP_BOX_CMD = path.join(__dirname, 'scripts/backupbox.sh'),
BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh'),
INSTALLER_UPDATE_URL = 'http://127.0.0.1:2020/api/v1/installer/update';
INSTALLER_UPDATE_URL = 'http://127.0.0.1:2020/api/v1/installer/update',
RETIRE_CMD = path.join(__dirname, 'scripts/retire.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
@@ -111,6 +117,7 @@ CloudronError.BAD_EMAIL = 'Bad email';
CloudronError.BAD_PASSWORD = 'Bad password';
CloudronError.BAD_NAME = 'Bad name';
CloudronError.BAD_STATE = 'Bad state';
CloudronError.ALREADY_UPTODATE = 'No Update Available';
CloudronError.NOT_FOUND = 'Not found';
function initialize(callback) {
@@ -179,7 +186,7 @@ function setTimeZone(ip, callback) {
superagent.get('http://www.telize.com/geoip/' + ip).end(function (error, result) {
if ((error && !error.response) || result.statusCode !== 200) {
debug('Failed to get geo location', error);
debug('Failed to get geo location: %s', error.message);
return callback(null);
}
@@ -194,10 +201,11 @@ function setTimeZone(ip, callback) {
});
}
function activate(username, password, email, ip, callback) {
function activate(username, password, email, displayName, ip, 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 callback, 'function');
@@ -205,7 +213,7 @@ function activate(username, password, email, ip, callback) {
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, function (error, userObject) {
user.createOwner(username, password, email, displayName, function (error, userObject) {
if (error && error.reason === UserError.ALREADY_EXISTS) return callback(new CloudronError(CloudronError.ALREADY_PROVISIONED));
if (error && error.reason === UserError.BAD_USERNAME) return callback(new CloudronError(CloudronError.BAD_USERNAME));
if (error && error.reason === UserError.BAD_PASSWORD) return callback(new CloudronError(CloudronError.BAD_PASSWORD));
@@ -245,6 +253,7 @@ function getStatus(callback) {
version: config.version(),
boxVersionsUrl: config.get('boxVersionsUrl'),
apiServerOrigin: config.apiServerOrigin(), // used by CaaS tool
provider: config.provider(),
cloudronName: cloudronName
});
});
@@ -256,6 +265,15 @@ function getCloudronDetails(callback) {
if (gCloudronDetails) return callback(null, gCloudronDetails);
if (!config.token()) {
gCloudronDetails = {
region: null,
size: null
};
return callback(null, gCloudronDetails);
}
superagent
.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn())
.query({ token: config.token() })
@@ -283,31 +301,32 @@ function getConfig(callback) {
};
}
// We rely at the moment on the size being specified in 512mb,1gb,...
// TODO provide that number from the appstore
var memory = bytes(result.size) || 0;
settings.getCloudronName(function (error, cloudronName) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
settings.getDeveloperMode(function (error, developerMode) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
callback(null, {
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
isDev: config.isDev(),
fqdn: config.fqdn(),
ip: sysinfo.getIp(),
version: config.version(),
update: updateChecker.getUpdateInfo(),
progress: progress.get(),
isCustomDomain: config.isCustomDomain(),
developerMode: developerMode,
region: result.region,
size: result.size,
memory: memory,
cloudronName: cloudronName
sysinfo.getIp(function (error, ip) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
callback(null, {
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
isDev: config.isDev(),
fqdn: config.fqdn(),
ip: ip,
version: config.version(),
update: updateChecker.getUpdateInfo(),
progress: progress.get(),
isCustomDomain: config.isCustomDomain(),
developerMode: developerMode,
region: result.region,
size: result.size,
memory: os.totalmem(),
provider: config.provider(),
cloudronName: cloudronName
});
});
});
});
@@ -315,8 +334,9 @@ function getConfig(callback) {
}
function sendHeartbeat() {
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat';
if (!config.token()) return;
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat';
superagent.post(url).query({ token: config.token(), version: config.version() }).timeout(10000).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);
@@ -352,16 +372,16 @@ function txtRecordsWithSpf(callback) {
for (i = 0; i < txtRecords.length; i++) {
if (txtRecords[i].indexOf('"v=spf1 ') !== 0) continue; // not SPF
validSpf = txtRecords[i].indexOf(' a:' + config.fqdn() + ' ') !== -1;
validSpf = txtRecords[i].indexOf(' a:' + config.adminFqdn() + ' ') !== -1;
break;
}
if (validSpf) return callback(null, null);
if (i == txtRecords.length) {
txtRecords[i] = '"v=spf1 a:' + config.fqdn() + ' ~all"';
txtRecords[i] = '"v=spf1 a:' + config.adminFqdn() + ' ~all"';
} else {
txtRecords[i] = '"v=spf1 a:' + config.fqdn() + ' ' + txtRecords[i].slice('"v=spf1 '.length);
txtRecords[i] = '"v=spf1 a:' + config.adminFqdn() + ' ' + txtRecords[i].slice('"v=spf1 '.length);
}
return callback(null, txtRecords);
@@ -385,49 +405,55 @@ function addDnsRecords() {
var dkimKey = readDkimPublicKeySync();
if (!dkimKey) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, new Error('internal error failed to read dkim public key')));
var nakedDomainRecord = { subdomain: '', type: 'A', values: [ sysinfo.getIp() ] };
var webadminRecord = { subdomain: 'my', type: 'A', values: [ sysinfo.getIp() ] };
// t=s limits the domainkey to this domain and not it's subdomains
var dkimRecord = { subdomain: DKIM_SELECTOR + '._domainkey', type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
// DMARC requires special setup if report email id is in different domain
var dmarcRecord = { subdomain: '_dmarc', type: 'TXT', values: [ '"v=DMARC1; p=none; pct=100; rua=mailto:' + DMARC_REPORT_EMAIL + '; ruf=' + DMARC_REPORT_EMAIL + '"' ] };
sysinfo.getIp(function (error, ip) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
var records = [ ];
if (config.isCustomDomain()) {
records.push(webadminRecord);
records.push(dkimRecord);
} else {
records.push(nakedDomainRecord);
records.push(webadminRecord);
records.push(dkimRecord);
records.push(dmarcRecord);
}
var webadminRecord = { subdomain: 'my', type: 'A', values: [ ip ] };
// t=s limits the domainkey to this domain and not it's subdomains
var dkimRecord = { subdomain: DKIM_SELECTOR + '._domainkey', type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
// DMARC requires special setup if report email id is in different domain
var dmarcRecord = { subdomain: '_dmarc', type: 'TXT', values: [ '"v=DMARC1; p=none; pct=100; rua=mailto:' + DMARC_REPORT_EMAIL + '; ruf=' + DMARC_REPORT_EMAIL + '"' ] };
debug('addDnsRecords: %j', records);
var records = [ ];
if (config.isCustomDomain()) {
records.push(webadminRecord);
records.push(dkimRecord);
} else {
// for custom domains, we show a nakeddomain.html page
var nakedDomainRecord = { subdomain: '', type: 'A', values: [ ip ] };
async.retry({ times: 10, interval: 20000 }, function (retryCallback) {
txtRecordsWithSpf(function (error, txtRecords) {
if (error) return retryCallback(error);
records.push(nakedDomainRecord);
records.push(webadminRecord);
records.push(dkimRecord);
records.push(dmarcRecord);
}
if (txtRecords) records.push({ subdomain: '', type: 'TXT', values: txtRecords });
debug('addDnsRecords: %j', records);
debug('addDnsRecords: will update %j', records);
async.retry({ times: 10, interval: 20000 }, function (retryCallback) {
txtRecordsWithSpf(function (error, txtRecords) {
if (error) return retryCallback(error);
async.mapSeries(records, function (record, iteratorCallback) {
subdomains.update(record.subdomain, 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);
if (txtRecords) records.push({ subdomain: '', type: 'TXT', values: txtRecords });
retryCallback(error);
debug('addDnsRecords: will update %j', records);
async.mapSeries(records, function (record, iteratorCallback) {
subdomains.update(record.subdomain, 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) {
gUpdatingDns = false;
debug('addDnsRecords: done updating records with error:', error);
callback(error);
});
}, function (error) {
gUpdatingDns = false;
debug('addDnsRecords: done updating records with error:', error);
callback(error);
});
}
@@ -435,49 +461,6 @@ function reboot(callback) {
shell.sudo('reboot', [ REBOOT_CMD ], callback);
}
function migrate(size, region, callback) {
assert.strictEqual(typeof size, 'string');
assert.strictEqual(typeof region, 'string');
assert.strictEqual(typeof callback, 'function');
var error = locker.lock(locker.OP_MIGRATE);
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
function unlock(error) {
if (error) {
debug('Failed to migrate', error);
locker.unlock(locker.OP_MIGRATE);
} else {
debug('Migration initiated successfully');
// do not unlock; cloudron is migrating
}
return;
}
// initiate the migration in the background
backupBoxAndApps(function (error, restoreKey) {
if (error) return unlock(error);
debug('migrate: size %s region %s restoreKey %s', size, region, restoreKey);
superagent
.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/migrate')
.query({ token: config.token() })
.send({ size: size, region: region, restoreKey: restoreKey })
.end(function (error, result) {
if (error && !error.response) return unlock(error);
if (result.statusCode === 409) return unlock(new CloudronError(CloudronError.BAD_STATE));
if (result.statusCode === 404) return unlock(new CloudronError(CloudronError.NOT_FOUND));
if (result.statusCode !== 202) return unlock(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
return unlock(null);
});
});
callback(null);
}
function update(boxUpdateInfo, callback) {
assert.strictEqual(typeof boxUpdateInfo, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -487,8 +470,16 @@ function update(boxUpdateInfo, callback) {
var error = locker.lock(locker.OP_BOX_UPDATE);
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
// 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) {
if (config.version().match(/[-+]/) !== null && config.version().replace(/[-+].*/, '') === boxUpdateInfo.version) {
doShortCircuitUpdate(boxUpdateInfo, function (error) {
if (error) debug('Short-circuit update failed', error);
locker.unlock(locker.OP_BOX_UPDATE);
});
} else if (boxUpdateInfo.upgrade) {
debug('Starting upgrade');
doUpgrade(boxUpdateInfo, function (error) {
if (error) {
@@ -509,6 +500,26 @@ function update(boxUpdateInfo, callback) {
callback(null);
}
function updateToLatest(callback) {
assert.strictEqual(typeof callback, 'function');
var boxUpdateInfo = updateChecker.getUpdateInfo().box;
if (!boxUpdateInfo) return callback(new CloudronError(CloudronError.ALREADY_UPTODATE, 'No update available'));
update(boxUpdateInfo, 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 doUpgrade(boxUpdateInfo, callback) {
assert(boxUpdateInfo !== null && typeof boxUpdateInfo === 'object');
@@ -532,8 +543,8 @@ function doUpgrade(boxUpdateInfo, callback) {
progress.set(progress.UPDATE, 10, 'Updating base system');
// no need to unlock since this is the last thing we ever do on this box
callback(null);
callback();
retire();
});
});
}
@@ -551,53 +562,44 @@ function doUpdate(boxUpdateInfo, callback) {
backupBoxAndApps(function (error) {
if (error) return updateError(error);
// fetch a signed sourceTarballUrl
superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/sourcetarballurl')
.query({ token: config.token(), boxVersion: boxUpdateInfo.version })
.end(function (error, result) {
if (error && !error.response) return updateError(new Error('Network error fetching sourceTarballUrl: ' + error));
if (result.statusCode !== 200) return updateError(new Error('Error fetching sourceTarballUrl status: ' + result.statusCode));
if (!safe.query(result, 'body.url')) return updateError(new Error('Error fetching sourceTarballUrl response: ' + JSON.stringify(result.body)));
// NOTE: the args here are tied to the installer revision, box code and appstore provisioning logic
var args = {
sourceTarballUrl: boxUpdateInfo.sourceTarballUrl,
// NOTE: the args here are tied to the installer revision, box code and appstore provisioning logic
var args = {
sourceTarballUrl: result.body.url,
// this data is opaque to the installer
data: {
token: config.token(),
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
fqdn: config.fqdn(),
tlsCert: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), 'utf8'),
tlsKey: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), 'utf8'),
isCustomDomain: config.isCustomDomain(),
// this data is opaque to the installer
data: {
appstore: {
token: config.token(),
apiServerOrigin: config.apiServerOrigin()
},
caas: {
token: config.token(),
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
fqdn: config.fqdn(),
tlsCert: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), 'utf8'),
tlsKey: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), 'utf8'),
isCustomDomain: config.isCustomDomain(),
webServerOrigin: config.webServerOrigin()
},
appstore: {
token: config.token(),
apiServerOrigin: config.apiServerOrigin()
},
caas: {
token: config.token(),
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin()
},
version: boxUpdateInfo.version,
boxVersionsUrl: config.get('boxVersionsUrl')
}
};
version: boxUpdateInfo.version,
boxVersionsUrl: config.get('boxVersionsUrl')
}
};
debug('updating box %j', args);
debug('updating box %j', args);
superagent.post(INSTALLER_UPDATE_URL).send(args).end(function (error, result) {
if (error && !error.response) return updateError(error);
if (result.statusCode !== 202) return updateError(new Error('Error initiating update: ' + JSON.stringify(result.body)));
superagent.post(INSTALLER_UPDATE_URL).send(args).end(function (error, result) {
if (error && !error.response) return updateError(error);
if (result.statusCode !== 202) return updateError(new Error('Error initiating update: ' + JSON.stringify(result.body)));
progress.set(progress.UPDATE, 10, 'Updating cloudron software');
progress.set(progress.UPDATE, 10, 'Updating cloudron software');
callback(null);
});
callback(null);
});
// Do not add any code here. The installer script will stop the box code any instant
@@ -610,8 +612,8 @@ function backup(callback) {
var error = locker.lock(locker.OP_FULL_BACKUP);
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
// clearing backup ensures tools can 'wait' on progress
progress.clear(progress.BACKUP);
// ensure tools can 'wait' on progress
progress.set(progress.BACKUP, 0, 'Starting');
// start the backup operation in the background
backupBoxAndApps(function (error) {
@@ -624,9 +626,9 @@ function backup(callback) {
}
function ensureBackup(callback) {
callback = callback || function () { };
callback = callback || NOOP_CALLBACK;
backups.getAllPaged(1, 1, function (error, backups) {
backups.getPaged(1, 1, function (error, backups) {
if (error) {
debug('Unable to list backups', error);
return callback(error); // no point trying to backup if appstore is down
@@ -644,7 +646,7 @@ function ensureBackup(callback) {
function backupBoxWithAppBackupIds(appBackupIds, callback) {
assert(util.isArray(appBackupIds));
backups.getBackupUrl(null /* app */, function (error, result) {
backups.getBackupUrl(appBackupIds, function (error, result) {
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));
@@ -681,7 +683,7 @@ function backupBox(callback) {
// this function expects you to have a lock
function backupBoxAndApps(callback) {
callback = callback || function () { }; // callback can be empty for timer triggered backup
callback = callback || NOOP_CALLBACK;
apps.getAll(function (error, allApps) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
@@ -712,10 +714,47 @@ function backupBoxAndApps(callback) {
backupIds = backupIds.filter(function (id) { return id !== null; }); // remove apps in bad state that were never backed up
backupBoxWithAppBackupIds(backupIds, function (error, restoreKey) {
backupBoxWithAppBackupIds(backupIds, function (error, filename) {
progress.set(progress.BACKUP, 100, error ? error.message : '');
callback(error, restoreKey);
callback(error, filename);
});
});
});
}
function checkDiskSpace(callback) {
callback = callback || NOOP_CALLBACK;
debug('Checking disk space');
df(function (error, entries) {
if (error) {
debug('df error %s', error.message);
mailer.outOfDiskSpace(error.message);
return callback();
}
var oos = entries.some(function (entry) {
return (entry.mount === paths.DATA_DIR && entry.capacity >= 0.90) ||
(entry.mount === '/' && entry.used <= (1.25 * 1024 * 1024)); // 1.5G
});
debug('Disk space checked. ok: %s', !oos);
if (oos) mailer.outOfDiskSpace(JSON.stringify(entries, null, 4));
callback();
});
}
function retire(callback) {
callback = callback || NOOP_CALLBACK;
var data = {
isCustomDomain: config.isCustomDomain(),
fqdn: config.fqdn()
};
shell.sudo('retire', [ RETIRE_CMD, JSON.stringify(data) ], callback);
}

View File

@@ -20,7 +20,7 @@ LoadPlugin "table"
</Result>
</Table>
<Table "/sys/fs/cgroup/cpuacct/docker/<%= containerId %>/cpuacct.stat">
<Table "/sys/fs/cgroup/cpuacct/system.slice/docker/<%= containerId %>/cpuacct.stat">
Instance "<%= appId %>-cpu"
Separator " \\n"
<Result>

View File

@@ -17,11 +17,13 @@ exports = module.exports = {
TEST: process.env.BOX_ENV === 'test',
// convenience getters
provider: provider,
apiServerOrigin: apiServerOrigin,
webServerOrigin: webServerOrigin,
fqdn: fqdn,
token: token,
version: version,
setVersion: setVersion,
isCustomDomain: isCustomDomain,
database: database,
@@ -31,11 +33,12 @@ exports = module.exports = {
adminFqdn: adminFqdn,
appFqdn: appFqdn,
zoneName: zoneName,
adminEmail: adminEmail,
isDev: isDev,
// for testing resets to defaults
_reset: initConfig
_reset: _reset
};
var assert = require('assert'),
@@ -68,6 +71,14 @@ function saveSync() {
fs.writeFileSync(cloudronConfigFileName, JSON.stringify(data, null, 4)); // functions are ignored by JSON.stringify
}
function _reset (callback) {
safe.fs.unlinkSync(cloudronConfigFileName);
initConfig();
if (callback) callback();
}
function initConfig() {
// setup defaults
data.fqdn = 'localhost';
@@ -82,6 +93,7 @@ function initConfig() {
data.ldapPort = 3002;
data.oauthProxyPort = 3003;
data.simpleAuthPort = 3004;
data.provider = 'caas';
if (exports.CLOUDRON) {
data.port = 3000;
@@ -98,6 +110,7 @@ function initConfig() {
name: 'boxtest'
};
data.token = 'APPSTORE_TOKEN';
data.adminEmail = 'test@cloudron.foo';
} else {
assert(false, 'Unknown environment. This should not happen!');
}
@@ -136,6 +149,10 @@ function get(key) {
return safe.query(data, key);
}
function adminEmail() {
return '"Cloudron" ' + get('adminEmail');
}
function apiServerOrigin() {
return get('apiServerOrigin');
}
@@ -176,6 +193,10 @@ function version() {
return get('version');
}
function setVersion(version) {
set('version', version);
}
function isCustomDomain() {
return get('isCustomDomain');
}
@@ -194,3 +215,7 @@ function database() {
function isDev() {
return /dev/i.test(get('boxVersionsUrl'));
}
function provider() {
return get('provider');
}

View File

@@ -7,6 +7,8 @@ exports = module.exports = {
ADMIN_NAME: 'Settings',
ADMIN_CLIENT_ID: 'webadmin', // oauth client id
ADMIN_APPID: 'admin' // admin appid (settingsdb)
ADMIN_APPID: 'admin', // admin appid (settingsdb)
DEFAULT_MEMORY_LIMIT: (256 * 1024 * 1024) // see also client.js
};

View File

@@ -25,7 +25,8 @@ var gAutoupdaterJob = null,
gCleanupTokensJob = null,
gDockerVolumeCleanerJob = null,
gSchedulerSyncJob = null,
gCertificateRenewJob = null;
gCertificateRenewJob = null,
gCheckDiskSpaceJob = null;
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
@@ -69,6 +70,14 @@ function recreateJobs(unusedTimeZone, callback) {
timeZone: allSettings[settings.TIME_ZONE_KEY]
});
if (gCheckDiskSpaceJob) gCheckDiskSpaceJob.stop();
gCheckDiskSpaceJob = new CronJob({
cronTime: '00 30 */4 * * *', // every 4 hours
onTick: cloudron.checkDiskSpace,
start: true,
timeZone: allSettings[settings.TIME_ZONE_KEY]
});
if (gBoxUpdateCheckerJob) gBoxUpdateCheckerJob.stop();
gBoxUpdateCheckerJob = new CronJob({
cronTime: '00 */10 * * * *', // every 10 minutes

View File

@@ -118,6 +118,7 @@ function clear(callback) {
require('./authcodedb.js')._clear,
require('./clientdb.js')._clear,
require('./tokendb.js')._clear,
require('./groupdb.js')._clear,
require('./userdb.js')._clear,
require('./settingsdb.js')._clear
], callback);

View File

@@ -30,3 +30,4 @@ DatabaseError.INTERNAL_ERROR = 'Internal error';
DatabaseError.ALREADY_EXISTS = 'Entry already exist';
DatabaseError.NOT_FOUND = 'Record not found';
DatabaseError.BAD_FIELD = 'Invalid field';
DatabaseError.IN_USE = 'In Use';

View File

@@ -13,6 +13,7 @@ exports = module.exports = {
var assert = require('assert'),
config = require('./config.js'),
debug = require('debug')('box:developer'),
tokendb = require('./tokendb.js'),
settings = require('./settings.js'),
superagent = require('superagent'),
@@ -79,6 +80,10 @@ function getNonApprovedApps(callback) {
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/apps';
superagent.get(url).query({ token: config.token(), boxVersion: config.version() }).end(function (error, result) {
if (error && !error.response) return callback(new DeveloperError(DeveloperError.EXTERNAL_ERROR, error));
if (result.statusCode === 401) {
debug('Failed to list apps in development. Appstore token invalid or missing. Returning empty list.', result.body);
return callback(null, []);
}
if (result.statusCode !== 200) return callback(new DeveloperError(DeveloperError.EXTERNAL_ERROR, util.format('App listing failed. %s %j', result.status, result.body)));
callback(null, result.body.apps || []);

View File

@@ -39,7 +39,8 @@ function getZoneByName(dnsConfig, zoneName, callback) {
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
route53.listHostedZones({}, function (error, result) {
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
if (error && error.code === 'AccessDenied') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
if (error) return callback(new SubdomainError(SubdomainError.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
@@ -84,11 +85,9 @@ 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 === 'PriorRequestNotComplete') {
return callback(new SubdomainError(SubdomainError.STILL_BUSY, error.message));
} else if (error) {
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
}
if (error && error.code === 'AccessDenied') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
if (error && error.code === 'PriorRequestNotComplete') return callback(new SubdomainError(SubdomainError.STILL_BUSY, error.message));
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
callback(null, result.ChangeInfo.Id);
});
@@ -131,7 +130,8 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
route53.listResourceRecordSets(params, function (error, result) {
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
if (error && error.code === 'AccessDenied') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
if (error) return callback(new SubdomainError(SubdomainError.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, [ ]);
@@ -175,21 +175,22 @@ 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 SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
if (error && error.message && error.message.indexOf('it was not found') !== -1) {
debug('delSubdomain: resource record set not found.', error);
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
debug('del: resource record set not found.', error);
return callback(new SubdomainError(SubdomainError.NOT_FOUND, error.message));
} else if (error && error.code === 'NoSuchHostedZone') {
debug('delSubdomain: hosted zone not found.', error);
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
debug('del: hosted zone not found.', error);
return callback(new SubdomainError(SubdomainError.NOT_FOUND, error.message));
} else if (error && error.code === 'PriorRequestNotComplete') {
debug('delSubdomain: resource is still busy', error);
return callback(new SubdomainError(SubdomainError.STILL_BUSY, new Error(error)));
debug('del: resource is still busy', error);
return callback(new SubdomainError(SubdomainError.STILL_BUSY, error.message));
} else if (error && error.code === 'InvalidChangeBatch') {
debug('delSubdomain: invalid change batch. No such record to be deleted.');
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
debug('del: invalid change batch. No such record to be deleted.');
return callback(new SubdomainError(SubdomainError.NOT_FOUND, error.message));
} else if (error) {
debug('delSubdomain: error', error);
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
debug('del: error', error);
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
}
callback(null);
@@ -206,6 +207,7 @@ function getChangeStatus(dnsConfig, changeId, callback) {
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
route53.getChange({ Id: changeId }, function (error, result) {
if (error && error.code === 'AccessDenied') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
if (error) return callback(error);
callback(null, result.ChangeInfo.Status);

View File

@@ -4,6 +4,7 @@ var addons = require('./addons.js'),
async = require('async'),
assert = require('assert'),
config = require('./config.js'),
constants = require('./constants.js'),
debug = require('debug')('box:src/docker.js'),
Docker = require('dockerode'),
safe = require('safetydance'),
@@ -17,11 +18,14 @@ exports = module.exports = {
createContainer: createContainer,
startContainer: startContainer,
stopContainer: stopContainer,
stopContainerByName: stopContainer,
stopContainers: stopContainers,
deleteContainer: deleteContainer,
deleteContainerByName: deleteContainer,
deleteImage: deleteImage,
deleteContainers: deleteContainers,
createSubcontainer: createSubcontainer
createSubcontainer: createSubcontainer,
getContainerIdByIp: getContainerIdByIp
};
function connectionInstance() {
@@ -52,7 +56,7 @@ function targetBoxVersion(manifest) {
if ('minBoxVersion' in manifest) return manifest.minBoxVersion;
return '0.0.1';
return '99999.99999.99999'; // compatible with the latest version
}
function pullImage(manifest, callback) {
@@ -128,6 +132,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
isAppContainer = !cmd;
var manifest = app.manifest;
var developmentMode = !!manifest.developmentMode;
var exposedPorts = {}, dockerPortBindings = { };
var stdEnv = [
'CLOUDRON=1',
@@ -153,7 +158,15 @@ function createSubcontainer(app, name, cmd, options, callback) {
dockerPortBindings[containerPort + '/tcp'] = [ { HostIp: '0.0.0.0', HostPort: hostPort + '' } ];
}
var memoryLimit = manifest.memoryLimit || 1024 * 1024 * 200; // 200mb by default
// first check db record, then manifest
var memoryLimit = app.memoryLimit || manifest.memoryLimit;
// ensure we never go below minimum
memoryLimit = memoryLimit < constants.DEFAULT_MEMORY_LIMIT ? constants.DEFAULT_MEMORY_LIMIT : memoryLimit; // 256mb by default
// developerMode does not restrict memory usage
memoryLimit = developmentMode ? 0 : memoryLimit;
// for subcontainers, this should ideally be false. but docker does not allow network sharing if the app container is not running
// this means cloudron exec does not work
var isolatedNetworkNs = true;
@@ -168,7 +181,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
Hostname: isolatedNetworkNs ? (semver.gte(targetBoxVersion(app.manifest), '0.0.77') ? app.location : config.appFqdn(app.location)) : null,
Tty: isAppContainer,
Image: app.manifest.dockerImage,
Cmd: cmd,
Cmd: (isAppContainer && developmentMode) ? [ '/bin/bash', '-c', 'echo "Development mode. Use cloudron exec to debug. Sleeping" && sleep infinity' ] : cmd,
Env: stdEnv.concat(addonEnv).concat(portEnv),
ExposedPorts: isAppContainer ? exposedPorts : { },
Volumes: { // see also ReadonlyRootfs
@@ -186,7 +199,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
MemorySwap: memoryLimit, // Memory + Swap
PortBindings: isAppContainer ? dockerPortBindings : { },
PublishAllPorts: false,
ReadonlyRootfs: semver.gte(targetBoxVersion(app.manifest), '0.0.66'), // see also Volumes in startContainer
ReadonlyRootfs: !developmentMode, // see also Volumes in startContainer
RestartPolicy: {
"Name": isAppContainer ? "always" : "no",
"MaximumRetryCount": 0
@@ -200,9 +213,6 @@ function createSubcontainer(app, name, cmd, options, callback) {
};
containerOptions = _.extend(containerOptions, options);
// older versions wanted a writable /var/log
if (semver.lte(targetBoxVersion(app.manifest), '0.0.71')) containerOptions.Volumes['/var/log'] = {};
debugApp(app, 'Creating container for %s with options %j', app.manifest.dockerImage, containerOptions);
docker.createContainer(containerOptions, callback);
@@ -329,24 +339,53 @@ function deleteImage(manifest, callback) {
var docker = exports.connection;
docker.getImage(dockerImage).inspect(function (error, result) {
var removeOptions = {
force: false, // might be shared with another instance of this app
noprune: false // delete untagged parents
};
// registry v1 used to pull down all *tags*. this meant that deleting image by tag was not enough (since that
// just removes the tag). we used to remove the image by id. this is not required anymore because aliases are
// not created anymore after https://github.com/docker/docker/pull/10571
docker.getImage(dockerImage).remove(removeOptions, function (error) {
if (error && error.statusCode === 404) return callback(null);
if (error && error.statusCode === 409) return callback(null); // another container using the image
if (error) return callback(error);
if (error) debug('Error removing image %s : %j', dockerImage, error);
var removeOptions = {
force: true,
noprune: false
};
// delete image by id because 'docker pull' pulls down all the tags and this is the only way to delete all tags
docker.getImage(result.Id).remove(removeOptions, function (error) {
if (error && error.statusCode === 404) return callback(null);
if (error && error.statusCode === 409) return callback(null); // another container using the image
if (error) debug('Error removing image %s : %j', dockerImage, error);
callback(error);
});
callback(error);
});
}
function getContainerIdByIp(ip, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
debug('get container by ip %s', ip);
var docker = exports.connection;
docker.listNetworks({}, function (error, result) {
if (error) return callback(error);
var bridge;
result.forEach(function (n) {
if (n.Name === 'bridge') bridge = n;
});
if (!bridge) return callback(new Error('Unable to find the bridge network'));
var containerId;
for (var id in bridge.Containers) {
if (bridge.Containers[id].IPv4Address.indexOf(ip) === 0) {
containerId = id;
break;
}
}
if (!containerId) return callback(new Error('No container with that ip'));
debug('found container %s with ip %s', containerId, ip);
callback(null, containerId);
});
}

202
src/groupdb.js Normal file
View File

@@ -0,0 +1,202 @@
'use strict';
exports = module.exports = {
get: get,
getWithMembers: getWithMembers,
getAll: getAll,
add: add,
del: del,
count: count,
getMembers: getMembers,
addMember: addMember,
removeMember: removeMember,
isMember: isMember,
getGroups: getGroups,
setGroups: setGroups,
_clear: clear
};
var assert = require('assert'),
database = require('./database.js'),
DatabaseError = require('./databaseerror');
var GROUPS_FIELDS = [ 'id', 'name' ].join(',');
function get(groupId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ' FROM groups WHERE id = ?', [ groupId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null, result[0]);
});
}
function getWithMembers(groupId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ',GROUP_CONCAT(groupMembers.userId) AS userIds ' +
' FROM groups LEFT OUTER JOIN groupMembers ON groups.id = groupMembers.groupId ' +
' WHERE groups.id = ? ' +
' GROUP BY groups.id', [ groupId ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
var result = results[0];
result.userIds = result.userIds ? result.userIds.split(',') : [ ];
callback(null, result);
});
}
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ' FROM groups', function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, result);
});
}
function add(id, name, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
var data = [ id, name ];
database.query('INSERT INTO groups (id, name) VALUES (?, ?)',
data, function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
function del(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
// also cleanup the groupMembers table
var queries = [];
queries.push({ query: 'DELETE FROM groupMembers WHERE groupId = ?', args: [ id ] });
queries.push({ query: 'DELETE FROM groups WHERE id = ?', args: [ id ] });
database.transaction(queries, function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result[1].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(error);
});
}
function count(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT COUNT(*) AS total FROM groups', function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
return callback(null, result[0].total);
});
}
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) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(error);
});
});
}
function getMembers(groupId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT userId FROM groupMembers WHERE groupId=?', [ groupId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
// if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND)); // need to differentiate group with no members and invalid groupId
callback(error, result.map(function (r) { return r.userId; }));
});
}
function getGroups(userId, callback) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT groupId FROM groupMembers WHERE userId=? ORDER BY groupId', [ userId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
// if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND)); // need to differentiate group with no members and invalid groupId
callback(error, result.map(function (r) { return r.groupId; }));
});
}
function setGroups(userId, groupIds, callback) {
assert.strictEqual(typeof userId, 'string');
assert(Array.isArray(groupIds));
assert.strictEqual(typeof callback, 'function');
var queries = [ ];
queries.push({ query: 'DELETE from groupMembers WHERE userId = ?', args: [ userId ] });
groupIds.forEach(function (gid) {
queries.push({ query: 'INSERT INTO groupMembers (groupId, userId) VALUES (? , ?)', args: [ gid, userId ] });
});
database.transaction(queries, function (error) {
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, error.message));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
function addMember(groupId, userId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO groupMembers (groupId, userId) VALUES (?, ?)', [ groupId, userId ], function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
function removeMember(groupId, userId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM groupMembers WHERE groupId = ? AND userId = ?', [ groupId, userId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null);
});
}
function isMember(groupId, userId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT 1 FROM groupMembers WHERE groupId=? AND userId=?', [ groupId, userId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, result.length !== 0);
});
}

210
src/groups.js Normal file
View File

@@ -0,0 +1,210 @@
/* jshint node:true */
'use strict';
exports = module.exports = {
GroupError: GroupError,
create: create,
remove: remove,
get: get,
getWithMembers: getWithMembers,
getAll: getAll,
getMembers: getMembers,
addMember: addMember,
removeMember: removeMember,
isMember: isMember,
getGroups: getGroups,
setGroups: setGroups,
ADMIN_GROUP_ID: 'admin' // see db migration code and groupdb._clear
};
var assert = require('assert'),
DatabaseError = require('./databaseerror.js'),
groupdb = require('./groupdb.js'),
util = require('util');
// http://dustinsenos.com/articles/customErrorsInNode
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
function GroupError(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(GroupError, Error);
GroupError.INTERNAL_ERROR = 'Internal Error';
GroupError.ALREADY_EXISTS = 'Already Exists';
GroupError.NOT_FOUND = 'Not Found';
GroupError.BAD_NAME = 'Bad name';
GroupError.NOT_EMPTY = 'Not Empty';
GroupError.NOT_ALLOWED = 'Not Allowed';
function validateGroupname(name) {
assert.strictEqual(typeof name, 'string');
var RESERVED = [ 'admins', 'users' ]; // ldap code uses 'users' pseudo group
if (name.length <= 2) return new GroupError(GroupError.BAD_NAME, 'name must be atleast 2 chars');
if (name.length >= 200) return new GroupError(GroupError.BAD_NAME, 'name too long');
if (!/^[A-Za-z0-9_-]*$/.test(name)) return new GroupError(GroupError.BAD_NAME, 'name can only have A-Za-z0-9_-');
if (RESERVED.indexOf(name) !== -1) return new GroupError(GroupError.BAD_NAME, 'name is reserved');
return null;
}
function create(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
var error = validateGroupname(name);
if (error) return callback(error);
groupdb.add(name /* 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));
callback(null, { id: name, name: name });
});
}
function remove(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
// never allow admin group to be deleted
if (id === exports.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));
callback(null);
});
}
function get(id, callback) {
assert.strictEqual(typeof id, 'string');
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));
return callback(null, result);
});
}
function getWithMembers(id, callback) {
assert.strictEqual(typeof id, 'string');
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));
return callback(null, result);
});
}
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
groupdb.getAll(function (error, result) {
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
return callback(null, result);
});
}
function getMembers(groupId, callback) {
assert.strictEqual(typeof groupId, 'string');
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));
return callback(null, result);
});
}
function getGroups(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));
return callback(null, result);
});
}
function setGroups(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));
return callback(null);
});
}
function addMember(groupId, userId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof userId, 'string');
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));
return callback(null);
});
}
function removeMember(groupId, userId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof userId, 'string');
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));
return callback(null);
});
}
function isMember(groupId, userId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof userId, 'string');
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));
return callback(null, result);
});
}

View File

@@ -6,6 +6,7 @@ exports = module.exports = {
};
var assert = require('assert'),
apps = require('./apps.js'),
config = require('./config.js'),
debug = require('debug')('box:ldap'),
user = require('./user.js'),
@@ -28,15 +29,25 @@ var gLogger = {
var GROUP_USERS_DN = 'cn=users,ou=groups,dc=cloudron';
var GROUP_ADMINS_DN = 'cn=admins,ou=groups,dc=cloudron';
function getAppByRequest(req, callback) {
var sourceIp = req.connection.ldap.id.split(':')[0];
if (sourceIp.split('.').length !== 4) return callback(new ldap.InsufficientAccessRightsError('Missing source identifier'));
apps.getByIpAddress(sourceIp, function (error, app) {
// we currently allow access in case we can't find the source app
callback(null, app || null);
});
}
function start(callback) {
assert.strictEqual(typeof callback, 'function');
gServer = ldap.createServer({ log: gLogger });
gServer.search('ou=users,dc=cloudron', function (req, res, next) {
debug('ldap user search: dn %s, scope %s, filter %s', req.dn.toString(), req.scope, req.filter.toString());
debug('user search: dn %s, scope %s, filter %s', req.dn.toString(), req.scope, req.filter.toString());
user.list(function (error, result){
user.list(function (error, result) {
if (error) return next(new ldap.OperationsError(error.toString()));
// send user objects
@@ -54,7 +65,7 @@ function start(callback) {
cn: entry.id,
uid: entry.id,
mail: entry.email,
displayname: entry.username,
displayname: entry.displayName || entry.username,
username: entry.username,
samaccountname: entry.username, // to support ActiveDirectory clients
memberof: groups
@@ -71,7 +82,7 @@ function start(callback) {
});
gServer.search('ou=groups,dc=cloudron', function (req, res, next) {
debug('ldap group search: dn %s, scope %s, filter %s', req.dn.toString(), req.scope, req.filter.toString());
debug('group search: dn %s, scope %s, filter %s', req.dn.toString(), req.scope, req.filter.toString());
user.list(function (error, result){
if (error) return next(new ldap.OperationsError(error.toString()));
@@ -108,21 +119,44 @@ function start(callback) {
gServer.bind('ou=apps,dc=cloudron', function(req, res, next) {
// TODO: validate password
debug('ldap application bind: %s', req.dn.toString());
debug('application bind: %s', req.dn.toString());
res.end();
});
gServer.bind('ou=users,dc=cloudron', function(req, res, next) {
debug('ldap user bind: %s', req.dn.toString());
debug('user bind: %s', req.dn.toString());
if (!req.dn.rdns[0].cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
// extract the common name which might have different attribute names
var attributeName = Object.keys(req.dn.rdns[0])[0];
var commonName = req.dn.rdns[0][attributeName];
if (!commonName) return next(new ldap.NoSuchObjectError(req.dn.toString()));
user.verify(req.dn.rdns[0].cn, req.credentials || '', function (error, result) {
// if mail is specified, enforce mail check, otherwise allow both
var api = (commonName.indexOf('@') === -1) && (attributeName !== 'mail') ? user.verify : user.verifyWithEmail;
// TODO this should be done after we verified the app has access to avoid leakage of user existence
api(commonName, req.credentials || '', function (error, userObject) {
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) return next(new ldap.OperationsError(error));
res.end();
getAppByRequest(req, function (error, app) {
if (error) return next(error);
if (!app) {
debug('no app found for this container, allow access');
return res.end();
}
apps.hasAccessTo(app, userObject, function (error, result) {
if (error) return next(new ldap.OperationsError(error.toString()));
// we return no such object, to avoid leakage of a users existence
if (!result) return next(new ldap.NoSuchObjectError(req.dn.toString()));
res.end();
});
});
});
});

View File

@@ -2,12 +2,15 @@
Dear Admin,
A new version of the app '<%= app.manifest.title %>' installed at <%= app.fqdn %> is available!
A new version <%= updateInfo.manifest.version %> of the app '<%= app.manifest.title %>' installed at <%= app.fqdn %> is available!
Please update at your convenience at <%= webadminUrl %>.
The app will update automatically tonight. Alternately, update immediately at <%= webadminUrl %>.
Changes:
<%= updateInfo.manifest.changelog %>
Thank you,
Update Manager
your Cloudron
<% } else { %>

View File

@@ -2,9 +2,9 @@
Dear Admin,
A new version of Cloudron <%= fqdn %> is available!
Version <%= newBoxVersion %> of Cloudron <%= fqdn %> is now available!
Please update at your convenience at <%= webadminUrl %>.
Your Cloudron will update automatically tonight. Alternately, update immediately at <%= webadminUrl %>.
Changelog:
<% for (var i = 0; i < changelog.length; i++) { %>
@@ -12,7 +12,7 @@ Changelog:
<% } %>
Thank you,
Update Manager
your Cloudron
<% } else { %>

View File

@@ -0,0 +1,16 @@
<%if (format === 'text') { %>
Dear Cloudron Team,
<% if (message) { %>
<%= domain %> was not renewed.
<%- message %>
<% } else { %>
<%= domain %> was renewed.
<% } %>
Thank you,
Your Cloudron
<% } else { %>
<% } %>

View File

@@ -0,0 +1,19 @@
<%if (format === 'text') { %>
Dear Cloudron Team,
<%= fqdn %> is running out of disk space.
Please see some excerpts of the logs below.
Thank you,
Your Cloudron
-------------------------------------
<%- message %>
<% } else { %>
<% } %>

View File

@@ -0,0 +1,23 @@
<%if (format === 'text') { %>
Dear Admin,
User with name '<%= username %>' (<%= email %>) was added in the Cloudron at <%= fqdn %>.
You are receiving this email because you are an Admin of the Cloudron at <%= fqdn %>.
<% if (inviteLink) { %>
As requested, this user has not been sent an invitation email.
To set a password and perform any configuration on behalf of the user, please use this link:
<%= inviteLink %>
<% } %>
Thank you,
User Manager
<% } else { %>
<% } %>

View File

@@ -2,7 +2,7 @@
Dear <%= user.username %>,
Welcome to my Cloudron <%= fqdn %>!
Welcome to our Cloudron <%= fqdn %>!
The Cloudron is our own Smart Server. You can read more about it
at https://www.cloudron.io.
@@ -15,8 +15,12 @@ To get started, create your account by visiting the following page:
When you visit the above page, you will be prompted to enter a new password.
After you have submitted the form, you can login using the new password.
<% if (invitor && invitor.email) { %>
Thank you,
<%= invitor.email %>
<% } else { %>
Thank you
<% } %>
<% } else { %>

View File

@@ -13,14 +13,23 @@ exports = module.exports = {
boxUpdateAvailable: boxUpdateAvailable,
appUpdateAvailable: appUpdateAvailable,
sendInvite: sendInvite,
sendCrashNotification: sendCrashNotification,
appDied: appDied,
outOfDiskSpace: outOfDiskSpace,
certificateRenewed: certificateRenewed,
FEEDBACK_TYPE_FEEDBACK: 'feedback',
FEEDBACK_TYPE_TICKET: 'ticket',
FEEDBACK_TYPE_APP: 'app',
sendFeedback: sendFeedback
FEEDBACK_TYPE_APP_MISSING: 'app_missing',
FEEDBACK_TYPE_APP_ERROR: 'app_error',
sendFeedback: sendFeedback,
_getMailQueue: _getMailQueue,
_clearMailQueue: _clearMailQueue
};
var assert = require('assert'),
@@ -35,7 +44,7 @@ var assert = require('assert'),
path = require('path'),
safe = require('safetydance'),
smtpTransport = require('nodemailer-smtp-transport'),
userdb = require('./userdb.js'),
users = require('./user.js'),
util = require('util'),
_ = require('underscore');
@@ -170,6 +179,9 @@ function sendMails(queue) {
function enqueue(mailOptions) {
assert.strictEqual(typeof mailOptions, 'object');
if (!mailOptions.from) console.error('sender address is missing');
if (!mailOptions.to) console.error('recipient address is missing');
debug('Queued mail for ' + mailOptions.from + ' to ' + mailOptions.to);
gMailQueue.push(mailOptions);
@@ -184,7 +196,7 @@ function render(templateFile, params) {
}
function getAdminEmails(callback) {
userdb.getAllAdmins(function (error, admins) {
users.getAllAdmins(function (error, admins) {
if (error) return callback(error);
var adminEmails = [ ];
@@ -204,7 +216,7 @@ function mailUserEventToAdmins(user, event) {
adminEmails = _.difference(adminEmails, [ user.email ]);
var mailOptions = {
from: config.get('adminEmail'),
from: config.adminEmail(),
to: adminEmails.join(', '),
subject: util.format('%s %s in Cloudron %s', user.username, event, config.fqdn()),
text: render('user_event.ejs', { fqdn: config.fqdn(), username: user.username, email: user.email, event: event, format: 'text' }),
@@ -214,11 +226,11 @@ function mailUserEventToAdmins(user, event) {
});
}
function userAdded(user, invitor) {
function sendInvite(user, invitor) {
assert.strictEqual(typeof user, 'object');
assert(typeof invitor === 'object');
debug('Sending mail for userAdded');
debug('Sending invite mail');
var templateData = {
user: user,
@@ -230,15 +242,37 @@ function userAdded(user, invitor) {
};
var mailOptions = {
from: config.get('adminEmail'),
from: config.adminEmail(),
to: user.email,
subject: util.format('Welcome to Cloudron %s', config.fqdn()),
text: render('welcome_user.ejs', templateData)
};
enqueue(mailOptions);
}
mailUserEventToAdmins(user, 'was added');
function userAdded(user, inviteSent) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof inviteSent, 'boolean');
debug('Sending mail for userAdded %s including invite link', inviteSent ? 'not' : '');
getAdminEmails(function (error, adminEmails) {
if (error) return console.log('Error getting admins', error);
adminEmails = _.difference(adminEmails, [ user.email ]);
var inviteLink = inviteSent ? null : config.adminOrigin() + '/api/v1/session/password/setup.html?reset_token=' + user.resetToken;
var mailOptions = {
from: config.adminEmail(),
to: adminEmails.join(', '),
subject: util.format('%s added in Cloudron %s', user.username, config.fqdn()),
text: render('user_added.ejs', { fqdn: config.fqdn(), username: user.username, email: user.email, inviteLink: inviteLink, format: 'text' }),
};
enqueue(mailOptions);
});
}
function userRemoved(username) {
@@ -249,12 +283,13 @@ function userRemoved(username) {
mailUserEventToAdmins({ username: username }, 'was removed');
}
function adminChanged(user) {
function adminChanged(user, admin) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof admin, 'boolean');
debug('Sending mail for adminChanged');
mailUserEventToAdmins(user, user.admin ? 'is now an admin' : 'is no more an admin');
mailUserEventToAdmins(user, admin ? 'is now an admin' : 'is no more an admin');
}
function passwordReset(user) {
@@ -265,7 +300,7 @@ function passwordReset(user) {
var resetLink = config.adminOrigin() + '/api/v1/session/password/reset.html?reset_token=' + user.resetToken;
var mailOptions = {
from: config.get('adminEmail'),
from: config.adminEmail(),
to: user.email,
subject: 'Password Reset Request',
text: render('password_reset.ejs', { fqdn: config.fqdn(), username: user.username, resetLink: resetLink, format: 'text' })
@@ -283,7 +318,7 @@ function appDied(app) {
if (error) return console.log('Error getting admins', error);
var mailOptions = {
from: config.get('adminEmail'),
from: config.adminEmail(),
to: adminEmails.concat('support@cloudron.io').join(', '),
subject: util.format('App %s is down', app.location),
text: render('app_down.ejs', { fqdn: config.fqdn(), title: app.manifest.title, appFqdn: config.appFqdn(app.location), format: 'text' })
@@ -301,7 +336,7 @@ function boxUpdateAvailable(newBoxVersion, changelog) {
if (error) return console.log('Error getting admins', error);
var mailOptions = {
from: config.get('adminEmail'),
from: config.adminEmail(),
to: adminEmails.join(', '),
subject: util.format('%s has a new update available', config.fqdn()),
text: render('box_update_available.ejs', { fqdn: config.fqdn(), webadminUrl: config.adminOrigin(), newBoxVersion: newBoxVersion, changelog: changelog, format: 'text' })
@@ -319,16 +354,43 @@ function appUpdateAvailable(app, updateInfo) {
if (error) return console.log('Error getting admins', error);
var mailOptions = {
from: config.get('adminEmail'),
from: config.adminEmail(),
to: adminEmails.join(', '),
subject: util.format('%s has a new update available', app.fqdn),
text: render('app_update_available.ejs', { fqdn: config.fqdn(), webadminUrl: config.adminOrigin(), app: app, format: 'text' })
text: render('app_update_available.ejs', { fqdn: config.fqdn(), webadminUrl: config.adminOrigin(), app: app, updateInfo: updateInfo, format: 'text' })
};
enqueue(mailOptions);
});
}
function outOfDiskSpace(message) {
assert.strictEqual(typeof message, 'string');
var mailOptions = {
from: config.adminEmail(),
to: 'admin@cloudron.io',
subject: util.format('[%s] Out of disk space alert', config.fqdn()),
text: render('out_of_disk_space.ejs', { fqdn: config.fqdn(), message: message, format: 'text' })
};
sendMails([ mailOptions ]);
}
function certificateRenewed(domain, message) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof message, 'string');
var mailOptions = {
from: config.adminEmail(),
to: 'admin@cloudron.io',
subject: util.format('[%s] Certificate was %s renewed', domain, message ? 'not' : ''),
text: render('certificate_renewed.ejs', { domain: domain, message: message, format: 'text' })
};
sendMails([ mailOptions ]);
}
// this function bypasses the queue intentionally. it is also expected to work without the mailer module initialized
// crashnotifier should be able to send mail when there is no db
function sendCrashNotification(program, context) {
@@ -336,7 +398,7 @@ function sendCrashNotification(program, context) {
assert.strictEqual(typeof context, 'string');
var mailOptions = {
from: config.get('adminEmail'),
from: config.adminEmail(),
to: 'admin@cloudron.io',
subject: util.format('[%s] %s exited unexpectedly', config.fqdn(), program),
text: render('crash_notification.ejs', { fqdn: config.fqdn(), program: program, context: context, format: 'text' })
@@ -351,10 +413,13 @@ function sendFeedback(user, type, subject, description) {
assert.strictEqual(typeof subject, 'string');
assert.strictEqual(typeof description, 'string');
assert(type === exports.FEEDBACK_TYPE_TICKET || type === exports.FEEDBACK_TYPE_FEEDBACK || type === exports.FEEDBACK_TYPE_APP);
assert(type === exports.FEEDBACK_TYPE_TICKET ||
type === exports.FEEDBACK_TYPE_FEEDBACK ||
type === exports.FEEDBACK_TYPE_APP_MISSING ||
type === exports.FEEDBACK_TYPE_APP_ERROR);
var mailOptions = {
from: config.get('adminEmail'),
from: config.adminEmail(),
to: 'support@cloudron.io',
subject: util.format('[%s] %s - %s', type, config.fqdn(), subject),
text: render('feedback.ejs', { fqdn: config.fqdn(), type: type, user: user, subject: subject, description: description, format: 'text'})
@@ -362,3 +427,13 @@ function sendFeedback(user, type, subject, description) {
enqueue(mailOptions);
}
function _getMailQueue() {
return gMailQueue;
}
function _clearMailQueue(callback) {
gMailQueue = [];
if (callback) callback();
}

View File

@@ -13,6 +13,7 @@ var assert = require('assert'),
shell = require('./shell.js');
exports = module.exports = {
requiresOAuthProxy: requiresOAuthProxy,
configureAdmin: configureAdmin,
configureApp: configureApp,
unconfigureApp: unconfigureApp,
@@ -22,6 +23,19 @@ exports = module.exports = {
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/../setup/start/nginx/appconfig.ejs', { encoding: 'utf8' }),
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh');
function requiresOAuthProxy(app) {
assert.strictEqual(typeof app, 'object');
var tmp = app.accessRestriction;
// if no accessRestriction set, or the app uses one of the auth modules, we do not need the oauth proxy
if (tmp === null) return false;
if (app.manifest.addons['ldap'] || app.manifest.addons['oauth'] || app.manifest.addons['simpleauth']) return false;
// check if any restrictions are set
return !!((tmp.users && tmp.users.length) || (tmp.groups && tmp.groups.length));
}
function configureAdmin(certFilePath, keyFilePath, callback) {
assert.strictEqual(typeof certFilePath, 'string');
assert.strictEqual(typeof keyFilePath, 'string');
@@ -50,7 +64,8 @@ function configureApp(app, certFilePath, keyFilePath, callback) {
assert.strictEqual(typeof callback, 'function');
var sourceDir = path.resolve(__dirname, '..');
var endpoint = app.oauthProxy ? 'oauthproxy' : 'app';
var oauthProxy = requiresOAuthProxy(app);
var endpoint = oauthProxy ? 'oauthproxy' : 'app';
var vhost = config.appFqdn(app.location);
var data = {

View File

@@ -4,7 +4,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
<title> Cloudron Login </title>
<title> <%= title %> </title>
<link href="/api/v1/cloudron/avatar" rel="icon" type="image/png">

View File

@@ -25,10 +25,16 @@ app.controller('Controller', [function () {}]);
<div class="form-group" ng-class="{ 'has-error': resetForm.password.$dirty && resetForm.password.$invalid }">
<label class="control-label" for="inputPassword">New Password</label>
<input type="password" class="form-control" id="inputPassword" ng-model="password" name="password" ng-maxlength="512" ng-minlength="5" autofocus required>
<div class="control-label" ng-show="resetForm.password.$dirty && resetForm.password.$invalid">
<small ng-show="resetForm.password.$dirty && resetForm.password.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
</div>
<input type="password" class="form-control" id="inputPassword" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" autofocus required>
</div>
<div class="form-group" ng-class="{ 'has-error': resetForm.passwordRepeat.$dirty && (resetForm.passwordRepeat.$invalid || password !== passwordRepeat) }">
<div class="form-group" ng-class="{ 'has-error': resetForm.passwordRepeat.$dirty && (password !== passwordRepeat) }">
<label class="control-label" for="inputPasswordRepeat">Repeat Password</label>
<div class="control-label" ng-show="resetForm.passwordRepeat.$dirty && (password !== passwordRepeat)">
<small ng-show="resetForm.passwordRepeat.$dirty && (password !== passwordRepeat)">Passwords don't match</small>
</div>
<input type="password" class="form-control" id="inputPasswordRepeat" ng-model="passwordRepeat" name="passwordRepeat" required>
</div>
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Create" ng-disabled="resetForm.$invalid || password !== passwordRepeat"/>

View File

@@ -13,7 +13,7 @@ app.controller('Controller', [function () {}]);
</script>
<center>
<h1>Hello <%= user.username %> create a password</h1>
<h1>Hello <%= user.username %>, set a password</h1>
</center>
<div class="container" ng-app="Application" ng-controller="Controller">
@@ -25,10 +25,16 @@ app.controller('Controller', [function () {}]);
<div class="form-group" ng-class="{ 'has-error': setupForm.password.$dirty && setupForm.password.$invalid }">
<label class="control-label" for="inputPassword">New Password</label>
<input type="password" class="form-control" id="inputPassword" ng-model="password" name="password" ng-maxlength="512" ng-minlength="5" autofocus required>
<div class="control-label" ng-show="setupForm.password.$dirty && setupForm.password.$invalid">
<small ng-show="setupForm.password.$dirty && setupForm.password.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
</div>
<input type="password" class="form-control" id="inputPassword" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" autofocus required>
</div>
<div class="form-group" ng-class="{ 'has-error': setupForm.passwordRepeat.$dirty && (setupForm.passwordRepeat.$invalid || password !== passwordRepeat) }">
<div class="form-group" ng-class="{ 'has-error': setupForm.passwordRepeat.$dirty && (password !== passwordRepeat) }">
<label class="control-label" for="inputPasswordRepeat">Repeat Password</label>
<div class="control-label" ng-show="setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)">
<small ng-show="setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)">Passwords don't match</small>
</div>
<input type="password" class="form-control" id="inputPasswordRepeat" ng-model="passwordRepeat" name="passwordRepeat" required>
</div>
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Create" ng-disabled="setupForm.$invalid || password !== passwordRepeat"/>

View File

@@ -126,7 +126,7 @@ function authenticate(req, res, next) {
clientdb.getByAppIdAndType(result.id, clientdb.TYPE_PROXY, function (error, result) {
if (error) {
console.error('Unkonwn OAuth client.', error);
console.error('Unknown OAuth client.', error);
return res.send(500, 'Unknown OAuth client.');
}

47
src/password.js Normal file
View File

@@ -0,0 +1,47 @@
/* jslint node:true */
'use strict';
// From https://www.npmjs.com/package/password-generator
exports = module.exports = {
generate: generate,
validate: validate
};
var assert = require('assert'),
generatePassword = require('password-generator');
// http://www.w3resource.com/javascript/form/example4-javascript-form-validation-password.html
// WARNING!!! if this is changed, the UI parts in the setup and account view have to be adjusted!
var gPasswordTestRegExp = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/;
var UPPERCASE_RE = /([A-Z])/g;
var LOWERCASE_RE = /([a-z])/g;
var NUMBER_RE = /([\d])/g;
var SPECIAL_CHAR_RE = /([\?\-])/g;
function isStrongEnough(password) {
var uc = password.match(UPPERCASE_RE);
var lc = password.match(LOWERCASE_RE);
var n = password.match(NUMBER_RE);
var sc = password.match(SPECIAL_CHAR_RE);
return uc && lc && n && sc;
}
function generate() {
var password = '';
while (!isStrongEnough(password)) password = generatePassword(8, false, /[\w\d\?\-]/);
return password;
}
function validate(password) {
assert.strictEqual(typeof password, 'string');
if (!password.match(gPasswordTestRegExp)) return new Error('Password must be 8-30 character with at least one uppercase, one numeric and one special character');
return null;
}

View File

@@ -12,7 +12,6 @@ exports = module.exports = {
NGINX_CERT_DIR: path.join(config.baseDir(), 'data/nginx/cert'),
ADDON_CONFIG_DIR: path.join(config.baseDir(), 'data/addons'),
SCHEDULER_FILE: path.join(config.baseDir(), 'data/addons/scheduler.json'),
DNS_IN_SYNC_FILE: path.join(config.baseDir(), 'data/dns_in_sync'),

View File

@@ -15,6 +15,7 @@ exports = module.exports = {
updateApp: updateApp,
getLogs: getLogs,
getLogStream: getLogStream,
listBackups: listBackups,
stopApp: stopApp,
startApp: startApp,
@@ -43,12 +44,12 @@ function removeInternalAppFields(app) {
health: app.health,
location: app.location,
accessRestriction: app.accessRestriction,
oauthProxy: app.oauthProxy,
lastBackupId: app.lastBackupId,
manifest: app.manifest,
portBindings: app.portBindings,
iconUrl: app.iconUrl,
fqdn: app.fqdn
fqdn: app.fqdn,
memoryLimit: app.memoryLimit
};
}
@@ -75,7 +76,10 @@ function getAppBySubdomain(req, res, next) {
}
function getApps(req, res, next) {
apps.getAll(function (error, allApps) {
assert.strictEqual(typeof req.user, 'object');
var func = req.user.admin ? apps.getAll : apps.getAllByUser.bind(null, req.user);
func(function (error, allApps) {
if (error) return next(new HttpError(500, error));
allApps = allApps.map(removeInternalAppFields);
@@ -115,19 +119,19 @@ function installApp(req, res, next) {
if (typeof data.location !== 'string') return next(new HttpError(400, 'location is required'));
if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
if (typeof data.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction is required'));
if (typeof data.oauthProxy !== 'boolean') return next(new HttpError(400, 'oauthProxy must be a boolean'));
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
if (data.cert && typeof data.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
if (data.key && typeof data.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if (data.cert && !data.key) return next(new HttpError(400, 'key must be provided'));
if (!data.cert && data.key) return next(new HttpError(400, 'cert must be provided'));
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
// allow tests to provide an appId for testing
var appId = (process.env.BOX_ENV === 'test' && typeof data.appId === 'string') ? data.appId : uuid.v4();
debug('Installing app id:%s storeid:%s loc:%s port:%j accessRestriction:%j oauthproxy:%s manifest:%j', appId, data.appStoreId, data.location, data.portBindings, data.accessRestriction, data.oauthProxy, data.manifest);
debug('Installing app id:%s storeid:%s loc:%s port:%j accessRestriction:%j memoryLimit:%s manifest:%j', appId, data.appStoreId, data.location, data.portBindings, data.accessRestriction, data.memoryLimit, data.manifest);
apps.install(appId, data.appStoreId, data.manifest, data.location, data.portBindings || null, data.accessRestriction, data.oauthProxy, data.icon || null, data.cert || null, data.key || null, function (error) {
apps.install(appId, data.appStoreId, data.manifest, data.location, data.portBindings || null, data.accessRestriction, data.icon || null, data.cert || null, data.key || null, data.memoryLimit || 0, function (error) {
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
@@ -159,15 +163,15 @@ function configureApp(req, res, next) {
if (typeof data.location !== 'string') return next(new HttpError(400, 'location is required'));
if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
if (typeof data.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction is required'));
if (typeof data.oauthProxy !== 'boolean') return next(new HttpError(400, 'oauthProxy must be a boolean'));
if (data.cert && typeof data.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
if (data.key && typeof data.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if (data.cert && !data.key) return next(new HttpError(400, 'key must be provided'));
if (!data.cert && data.key) return next(new HttpError(400, 'cert must be provided'));
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
debug('Configuring app id:%s location:%s bindings:%j accessRestriction:%j oauthProxy:%s', req.params.id, data.location, data.portBindings, data.accessRestriction, data.oauthProxy);
debug('Configuring app id:%s location:%s bindings:%j accessRestriction:%j memoryLimit:%s', req.params.id, data.location, data.portBindings, data.accessRestriction, data.memoryLimit);
apps.configure(req.params.id, data.location, data.portBindings || null, data.accessRestriction, data.oauthProxy, data.cert || null, data.key || null, function (error) {
apps.configure(req.params.id, data.location, data.portBindings || null, data.accessRestriction, data.cert || null, data.key || null, data.memoryLimit || 0, function (error) {
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
@@ -357,7 +361,9 @@ function exec(req, res, next) {
var rows = req.query.rows ? parseInt(req.query.rows, 10) : null;
if (isNaN(rows)) return next(new HttpError(400, 'rows must be a number'));
apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns }, function (error, duplexStream) {
var tty = req.query.tty === 'true' ? true : false;
apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
@@ -369,8 +375,22 @@ function exec(req, res, next) {
duplexStream.pipe(res.socket);
res.socket.pipe(duplexStream);
res.on('close', duplexStream.close);
});
}
function listBackups(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
apps.listBackups(page, perPage, req.params.id, function (error, result) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { backups: result }));
});
}

View File

@@ -4,19 +4,26 @@
exports = module.exports = {
get: get,
create: create
create: create,
download: download
};
var backups = require('../backups.js'),
var assert = require('assert'),
backups = require('../backups.js'),
BackupsError = require('../backups.js').BackupsError,
cloudron = require('../cloudron.js'),
CloudronError = require('../cloudron.js').CloudronError,
debug = require('debug')('box:routes/backups'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
function get(req, res, next) {
backups.getAllPaged(1, 5, function (error, result) {
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
backups.getPaged(page, perPage, function (error, result) {
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return next(new HttpError(503, error.message));
if (error) return next(new HttpError(500, error));
@@ -34,3 +41,14 @@ function create(req, res, next) {
next(new HttpSuccess(202, {}));
});
}
function download(req, res, next) {
assert.strictEqual(typeof req.params.backupId, 'string');
backups.getRestoreUrl(req.params.backupId, function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, result));
});
}

View File

@@ -10,7 +10,6 @@ exports = module.exports = {
getProgress: getProgress,
getConfig: getConfig,
update: update,
migrate: migrate,
feedback: feedback
};
@@ -23,8 +22,7 @@ var assert = require('assert'),
debug = require('debug')('box:routes/cloudron'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
superagent = require('superagent'),
updateChecker = require('../updatechecker.js');
superagent = require('superagent');
/**
* Creating an admin user and activate the cloudron.
@@ -42,27 +40,32 @@ function activate(req, res, next) {
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be string'));
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be string'));
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
var username = req.body.username;
var password = req.body.password;
var email = req.body.email;
var displayName = req.body.displayName || '';
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
debug('activate: username:%s ip:%s', username, ip);
cloudron.activate(username, password, email, ip, function (error, info) {
cloudron.activate(username, password, email, displayName, ip, function (error, info) {
if (error && error.reason === CloudronError.ALREADY_PROVISIONED) return next(new HttpError(409, 'Already setup'));
if (error && error.reason === CloudronError.BAD_USERNAME) return next(new HttpError(400, 'Bad username'));
if (error && error.reason === CloudronError.BAD_PASSWORD) return next(new HttpError(400, 'Bad password'));
if (error && error.reason === CloudronError.BAD_EMAIL) return next(new HttpError(400, 'Bad email'));
if (error) return next(new HttpError(500, error));
// only in caas case do we have to notify the api server about activation
if (config.provider() !== 'caas') return next(new HttpSuccess(201, info));
// Now let the api server know we got activated
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/setup/done').query({ setupToken:req.query.setupToken }).end(function (error, result) {
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/setup/done').query({ setupToken: req.query.setupToken }).end(function (error, result) {
if (error && !error.response) return next(new HttpError(500, error));
if (result.statusCode === 403) return next(new HttpError(403, 'Invalid token'));
if (result.statusCode === 409) return next(new HttpError(409, 'Already setup'));
if (result.statusCode !== 201) return next(new HttpError(500, result.text ? result.text.message : 'Internal error'));
if (result.statusCode !== 201) return next(new HttpError(500, result.text || 'Internal error'));
next(new HttpSuccess(201, info));
});
@@ -72,13 +75,16 @@ function activate(req, res, next) {
function setupTokenAuth(req, res, next) {
assert.strictEqual(typeof req.query, 'object');
// skip setupToken auth for non caas case
if (config.provider() !== 'caas') return next();
if (typeof req.query.setupToken !== 'string') return next(new HttpError(400, 'no setupToken provided'));
superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/setup/verify').query({ setupToken:req.query.setupToken }).end(function (error, result) {
if (error && !error.response) return next(new HttpError(500, error));
if (result.statusCode === 403) return next(new HttpError(403, 'Invalid token'));
if (result.statusCode === 409) return next(new HttpError(409, 'Already setup'));
if (result.statusCode !== 200) return next(new HttpError(500, result.text ? result.text.message : 'Internal error'));
if (result.statusCode !== 200) return next(new HttpError(500, result.text || 'Internal error'));
next();
});
@@ -112,25 +118,9 @@ function getConfig(req, res, next) {
}
function update(req, res, next) {
var boxUpdateInfo = updateChecker.getUpdateInfo().box;
if (!boxUpdateInfo) return next(new HttpError(422, 'No update available'));
// this only initiates the update, progress can be checked via the progress route
cloudron.update(boxUpdateInfo, function (error) {
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}
function migrate(req, res, next) {
if (typeof req.body.size !== 'string') return next(new HttpError(400, 'size must be string'));
if (typeof req.body.region !== 'string') return next(new HttpError(400, 'region must be string'));
debug('Migration requested', req.body.size, req.body.region);
cloudron.migrate(req.body.size, req.body.region, function (error) {
cloudron.updateToLatest(function (error) {
if (error && error.reason === CloudronError.ALREADY_UPTODATE) return next(new HttpError(422, error.message));
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
@@ -141,7 +131,10 @@ function migrate(req, res, next) {
function feedback(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
if (req.body.type !== mailer.FEEDBACK_TYPE_FEEDBACK && req.body.type !== mailer.FEEDBACK_TYPE_TICKET && req.body.type !== mailer.FEEDBACK_TYPE_APP) return next(new HttpError(400, 'type must be either "ticket", "feedback" or "app"'));
if (req.body.type !== mailer.FEEDBACK_TYPE_FEEDBACK &&
req.body.type !== mailer.FEEDBACK_TYPE_TICKET &&
req.body.type !== mailer.FEEDBACK_TYPE_APP_MISSING &&
req.body.type !== mailer.FEEDBACK_TYPE_APP_ERROR) return next(new HttpError(400, 'type must be either "ticket", "feedback" or "app_missing" or "app_error"'));
if (typeof req.body.subject !== 'string' || !req.body.subject) return next(new HttpError(400, 'subject must be string'));
if (typeof req.body.description !== 'string' || !req.body.description) return next(new HttpError(400, 'description must be string'));

67
src/routes/groups.js Normal file
View File

@@ -0,0 +1,67 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
get: get,
list: list,
create: create,
remove: remove
};
var assert = require('assert'),
groups = require('../groups.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
groups = require('../groups.js'),
GroupError = groups.GroupError;
function create(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be string'));
groups.create(req.body.name, function (error, group) {
if (error && error.reason === GroupError.BAD_NAME) return next(new HttpError(400, error.message));
if (error && error.reason === GroupError.ALREADY_EXISTS) return next(new HttpError(409, 'Already exists'));
if (error) return next(new HttpError(500, error));
var groupInfo = {
id: group.id,
name: group.name
};
next(new HttpSuccess(201, groupInfo));
});
}
function get(req, res, next) {
assert.strictEqual(typeof req.params.groupId, 'string');
groups.getWithMembers(req.params.groupId, function (error, result) {
if (error && error.reason === GroupError.NOT_FOUND) return next(new HttpError(404, 'No such group'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, result));
});
}
function list(req, res, next) {
groups.getAll(function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { groups: result }));
});
}
function remove(req, res, next) {
assert.strictEqual(typeof req.params.groupId, 'string');
groups.remove(req.params.groupId, function (error) {
if (error && error.reason === GroupError.NOT_FOUND) return next(new HttpError(404, 'Group not found'));
if (error && error.reason === GroupError.NOT_ALLOWED) return next(new HttpError(409, 'Group deletion not allowed'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
});
}

View File

@@ -2,14 +2,14 @@
exports = module.exports = {
apps: require('./apps.js'),
backups: require('./backups.js'),
clients: require('./clients.js'),
cloudron: require('./cloudron.js'),
developer: require('./developer.js'),
graphs: require('./graphs.js'),
groups: require('./groups.js'),
internal: require('./internal.js'),
oauth2: require('./oauth2.js'),
settings: require('./settings.js'),
clients: require('./clients.js'),
backups: require('./backups.js'),
internal: require('./internal.js'),
user: require('./user.js')
};

View File

@@ -3,7 +3,9 @@
'use strict';
exports = module.exports = {
backup: backup
backup: backup,
update: update,
retire: retire
};
var cloudron = require('../cloudron.js'),
@@ -13,7 +15,7 @@ var cloudron = require('../cloudron.js'),
HttpSuccess = require('connect-lastmile').HttpSuccess;
function backup(req, res, next) {
debug('trigger backup');
debug('triggering backup');
// note that cloudron.backup only waits for backup initiation and not for backup to complete
// backup progress can be checked up ny polling the progress api call
@@ -24,3 +26,29 @@ function backup(req, res, next) {
next(new HttpSuccess(202, {}));
});
}
function update(req, res, next) {
debug('triggering update');
// this only initiates the update, progress can be checked via the progress route
cloudron.updateToLatest(function (error) {
if (error && error.reason === CloudronError.ALREADY_UPTODATE) return next(new HttpError(422, error.message));
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}
function retire(req, res, next) {
debug('triggering retire');
// note that cloudron.backup only waits for backup initiation and not for backup to complete
// backup progress can be checked up ny polling the progress api call
cloudron.retire(function (error) {
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}

View File

@@ -150,14 +150,16 @@ function sendErrorPageOrRedirect(req, res, message) {
if (typeof req.query.returnTo !== 'string') {
renderTemplate(res, 'error', {
adminOrigin: config.adminOrigin(),
message: message
message: message,
title: 'Cloudron Error'
});
} else {
var u = url.parse(req.query.returnTo);
if (!u.protocol || !u.host) {
return renderTemplate(res, 'error', {
adminOrigin: config.adminOrigin(),
message: 'Invalid request. returnTo query is not a valid URI. ' + message
message: 'Invalid request. returnTo query is not a valid URI. ' + message,
title: 'Cloudron Error'
});
}
@@ -174,7 +176,8 @@ function sendError(req, res, message) {
renderTemplate(res, 'error', {
adminOrigin: config.adminOrigin(),
message: message
message: message,
title: 'Cloudron Error'
});
}
@@ -191,7 +194,8 @@ function loginForm(req, res) {
csrf: req.csrfToken(),
applicationName: applicationName,
applicationLogo: applicationLogo,
error: req.query.error || null
error: req.query.error || null,
title: applicationName + ' Login'
});
}
@@ -237,7 +241,7 @@ function logout(req, res) {
// Form to enter email address to send a password reset request mail
// -> GET /api/v1/session/password/resetRequest.html
function passwordResetRequestSite(req, res) {
renderTemplate(res, 'password_reset_request', { adminOrigin: config.adminOrigin(), csrf: req.csrfToken() });
renderTemplate(res, 'password_reset_request', { adminOrigin: config.adminOrigin(), csrf: req.csrfToken(), title: 'Cloudron Password Reset' });
}
// This route is used for above form submission
@@ -261,7 +265,7 @@ function passwordResetRequest(req, res, next) {
// -> GET /api/v1/session/password/sent.html
function passwordSentSite(req, res) {
renderTemplate(res, 'password_reset_sent', { adminOrigin: config.adminOrigin() });
renderTemplate(res, 'password_reset_sent', { adminOrigin: config.adminOrigin(), title: 'Cloudron Password Reset' });
}
// -> GET /api/v1/session/password/setup.html
@@ -275,7 +279,8 @@ function passwordSetupSite(req, res, next) {
adminOrigin: config.adminOrigin(),
user: user,
csrf: req.csrfToken(),
resetToken: req.query.reset_token
resetToken: req.query.reset_token,
title: 'Cloudron Password Setup'
});
});
}
@@ -291,7 +296,8 @@ function passwordResetSite(req, res, next) {
adminOrigin: config.adminOrigin(),
user: user,
csrf: req.csrfToken(),
resetToken: req.query.reset_token
resetToken: req.query.reset_token,
title: 'Cloudron Password Reset'
});
});
}
@@ -310,6 +316,7 @@ function passwordReset(req, res, next) {
// setPassword clears the resetToken
user.setPassword(userObject.id, req.body.password, function (error, result) {
if (error && error.reason === UserError.BAD_PASSWORD) return next(new HttpError(406, 'Password does not meet the requirements'));
if (error) return next(new HttpError(500, error));
res.redirect(util.format('%s?accessToken=%s&expiresAt=%s', config.adminOrigin(), result.token, result.expiresAt));
@@ -368,14 +375,17 @@ var authorization = [
if (type === clientdb.TYPE_ADMIN) return next();
if (type === clientdb.TYPE_EXTERNAL) return next();
if (type === clientdb.TYPE_SIMPLE_AUTH) return sendError(req, res, 'Unkonwn OAuth client.');
if (type === clientdb.TYPE_SIMPLE_AUTH) return sendError(req, res, 'Unknown OAuth client.');
appdb.get(req.oauth2.client.appId, function (error, appObject) {
if (error) return sendErrorPageOrRedirect(req, res, 'Invalid request. Unknown app for this client_id.');
if (!apps.hasAccessTo(appObject, req.oauth2.user)) return sendErrorPageOrRedirect(req, res, 'No access to this app.');
apps.hasAccessTo(appObject, req.oauth2.user, function (error, access) {
if (error) return sendError(req, res, 'Internal error');
if (!access) return sendErrorPageOrRedirect(req, res, 'No access to this app.');
next();
next();
});
});
},
gServer.decision({ loadTransaction: false })

File diff suppressed because it is too large Load Diff

View File

@@ -19,15 +19,17 @@ var appdb = require('../../appdb.js'),
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='silly@me.com';
var USERNAME = 'admin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
var server;
function setup(done) {
config.setVersion('1.2.3');
async.series([
server.start.bind(server),
userdb._clear,
database._clear,
function createAdmin(callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
@@ -51,7 +53,7 @@ function setup(done) {
function addApp(callback) {
var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok', addons: { } };
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, null /* accessRestriction */, false /* oauthProxy */, callback);
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, null /* accessRestriction */, 0 /* memoryLimit */, callback);
},
function createSettings(callback) {
@@ -72,35 +74,6 @@ describe('Backups API', function () {
before(setup);
after(cleanup);
describe('get', function () {
it('cannot get backups with appstore superagent failing', function (done) {
var req = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/backups?token=BACKUP_TOKEN').reply(401, {});
superagent.get(SERVER_URL + '/api/v1/backups')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(503);
expect(req.isDone()).to.be.ok();
done();
});
});
it('can get backups', function (done) {
var req = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/backups?token=BACKUP_TOKEN').reply(200, { backups: ['foo', 'bar']});
superagent.get(SERVER_URL + '/api/v1/backups')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(req.isDone()).to.be.ok();
expect(res.body.backups).to.be.an(Array);
expect(res.body.backups[0]).to.eql('foo');
expect(res.body.backups[1]).to.eql('bar');
done();
});
});
});
describe('create', function () {
it('fails due to mising token', function (done) {
superagent.post(SERVER_URL + '/api/v1/backups')

View File

@@ -20,7 +20,7 @@ var async = require('async'),
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='silly@me.com';
var USERNAME = 'admin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null; // authentication token
function cleanup(done) {
@@ -392,7 +392,7 @@ describe('Clients', function () {
var USER_0 = {
userId: uuid.v4(),
username: 'someusername',
password: 'somepassword',
password: 'Strong#$%2345',
email: 'some@email.com',
admin: true,
salt: 'somesalt',

View File

@@ -11,20 +11,21 @@ var async = require('async'),
database = require('../../database.js'),
expect = require('expect.js'),
nock = require('nock'),
os = require('os'),
superagent = require('superagent'),
server = require('../../server.js'),
shell = require('../../shell.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='silly@me.com';
var USERNAME = 'admin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null; // authentication token
var server;
function setup(done) {
nock.cleanAll();
config._reset();
config.set('version', '0.5.0');
config.set('fqdn', 'localhost');
server.start(done);
}
@@ -32,6 +33,8 @@ function cleanup(done) {
database._clear(function (error) {
expect(error).to.not.be.ok();
config._reset();
server.stop(done);
});
}
@@ -62,12 +65,25 @@ describe('Cloudron', function () {
});
});
it('fails due to internal server error on appstore side', function (done) {
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(500, { message: 'this is wrong' });
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: 'someuser', password: 'strong#A3asdf', email: 'admin@foo.bar' })
.end(function (error, result) {
expect(result.statusCode).to.equal(500);
expect(scope.isDone()).to.be.ok();
done();
});
});
it('fails due to empty username', function (done) {
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: '', password: 'somepassword', email: 'admin@foo.bar' })
.send({ username: '', password: 'ADSFsdf$%436', email: 'admin@foo.bar' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
expect(scope.isDone()).to.be.ok();
@@ -93,7 +109,7 @@ describe('Cloudron', function () {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: 'someuser', password: 'somepassword', email: '' })
.send({ username: 'someuser', password: 'ADSF#asd546', email: '' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
expect(scope.isDone()).to.be.ok();
@@ -101,12 +117,12 @@ describe('Cloudron', function () {
});
});
it('fails due to empty name', function (done) {
it('fails due to wrong displayName type', function (done) {
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: 'someuser', password: '', email: 'admin@foo.bar', name: '' })
.send({ username: 'someuser', password: 'ADSF?#asd546', email: 'admin@foo.bar', displayName: 1234 })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
expect(scope.isDone()).to.be.ok();
@@ -119,7 +135,7 @@ describe('Cloudron', function () {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: 'someuser', password: 'somepassword', email: 'invalidemail' })
.send({ username: 'someuser', password: 'ADSF#asd546', email: 'invalidemail' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
expect(scope.isDone()).to.be.ok();
@@ -133,7 +149,7 @@ describe('Cloudron', function () {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: 'someuser', password: 'somepassword', email: 'admin@foo.bar', name: 'tester' })
.send({ username: 'someuser', password: 'ADSF#asd546', email: 'admin@foo.bar', displayName: 'tester' })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
expect(scope1.isDone()).to.be.ok();
@@ -147,7 +163,7 @@ describe('Cloudron', function () {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: 'someuser', password: 'somepassword', email: 'admin@foo.bar' })
.send({ username: 'someuser', password: 'ADSF#asd546', email: 'admin@foo.bar' })
.end(function (error, result) {
expect(result.statusCode).to.equal(409);
expect(scope.isDone()).to.be.ok();
@@ -209,7 +225,7 @@ describe('Cloudron', function () {
expect(result.body.developerMode).to.be.a('boolean');
expect(result.body.size).to.eql(null);
expect(result.body.region).to.eql(null);
expect(result.body.memory).to.eql(0);
expect(result.body.memory).to.eql(os.totalmem());
expect(result.body.cloudronName).to.be.a('string');
done();
@@ -233,7 +249,7 @@ describe('Cloudron', function () {
expect(result.body.developerMode).to.be.a('boolean');
expect(result.body.size).to.eql('1gb');
expect(result.body.region).to.eql('sfo');
expect(result.body.memory).to.eql(1073741824);
expect(result.body.memory).to.eql(os.totalmem());
expect(result.body.cloudronName).to.be.a('string');
expect(scope.isDone()).to.be.ok();
@@ -244,181 +260,6 @@ describe('Cloudron', function () {
});
describe('migrate', function () {
before(function (done) {
async.series([
setup,
function (callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
config._reset();
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash token for further use
token = result.body.token;
callback();
});
},
function setupBackupConfig(callback) {
superagent.post(SERVER_URL + '/api/v1/settings/backup_config')
.send({ provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
callback();
});
}
], done);
});
after(cleanup);
it('fails without token', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo'})
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails without password', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo'})
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with missing size', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with wrong size type', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 4, region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with missing region', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with wrong region type', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 4, password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails when in wrong state', function (done) {
var scope2 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
var scope3 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/backupDone?token=APPSTORE_TOKEN', function (body) {
return body.boxVersion && body.restoreKey && !body.appId && !body.appVersion && body.appBackupIds.length === 0;
})
.reply(200, { id: 'someid' });
var scope1 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/migrate?token=APPSTORE_TOKEN', function (body) {
return body.size && body.region && body.restoreKey;
}).reply(409, {});
injectShellMock();
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
function checkAppstoreServerCalled() {
if (scope1.isDone() && scope2.isDone() && scope3.isDone()) {
restoreShellMock();
return done();
}
setTimeout(checkAppstoreServerCalled, 100);
}
checkAppstoreServerCalled();
});
});
it('succeeds', function (done) {
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/migrate?token=APPSTORE_TOKEN', function (body) {
return body.size && body.region && body.restoreKey;
}).reply(202, {});
var scope2 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/backupDone?token=APPSTORE_TOKEN', function (body) {
return body.boxVersion && body.restoreKey && !body.appId && !body.appVersion && body.appBackupIds.length === 0;
})
.reply(200, { id: 'someid' });
var scope3 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
injectShellMock();
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
function checkAppstoreServerCalled() {
if (scope1.isDone() && scope2.isDone() && scope3.isDone()) {
restoreShellMock();
return done();
}
setTimeout(checkAppstoreServerCalled, 100);
}
checkAppstoreServerCalled();
});
});
});
describe('feedback', function () {
before(function (done) {
async.series([
@@ -500,7 +341,7 @@ describe('Cloudron', function () {
it('succeeds with app type', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'app', subject: 'some subject', description: 'some description' })
.send({ type: 'app_missing', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);

View File

@@ -17,7 +17,7 @@ var async = require('async'),
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='silly@me.com';
var USERNAME = 'admin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null; // authentication token
var server;

View File

@@ -0,0 +1,245 @@
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
var appdb = require('../../appdb.js'),
async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
groups = require('../../groups.js'),
superagent = require('superagent'),
server = require('../../server.js'),
settings = require('../../settings.js'),
tokendb = require('../../tokendb.js'),
nock = require('nock'),
userdb = require('../../userdb.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'admin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var USERNAME_1 = 'user', PASSWORD_1 = 'Foobar?1337', EMAIL_1 ='happy@me.com';
var token, token_1 = null;
var server;
function setup(done) {
async.series([
server.start.bind(server),
database._clear,
function createAdmin(callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash token for further use
token = result.body.token;
callback();
});
},
function (callback) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_1, email: EMAIL_1, invite: false })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
token_1 = tokendb.generateToken();
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, tokendb.PREFIX_USER + USERNAME_1, 'test-client-id', Date.now() + 100000, '*', callback);
});
}
], done);
}
function cleanup(done) {
database._clear(function (error) {
expect(!error).to.be.ok();
server.stop(done);
});
}
describe('Groups API', function () {
before(setup);
after(cleanup);
describe('list', function () {
it('cannot get groups without token', function (done) {
superagent.get(SERVER_URL + '/api/v1/groups')
.end(function (err, res) {
expect(res.statusCode).to.equal(401);
done();
});
});
it('cannot get groups as normal user', function (done) {
superagent.get(SERVER_URL + '/api/v1/groups')
.query({ access_token: token_1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
});
});
it('can get groups', function (done) {
superagent.get(SERVER_URL + '/api/v1/groups')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.groups).to.be.an(Array);
expect(res.body.groups.length).to.be(1);
expect(res.body.groups[0].name).to.eql('admin');
done();
});
});
});
describe('create', function () {
it('fails due to mising token', function (done) {
superagent.post(SERVER_URL + '/api/v1/groups')
.send({ name: 'externals'})
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/groups')
.query({ access_token: token })
.send({ name: 'externals'})
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
done();
});
});
it('fails for already exists', function (done) {
superagent.post(SERVER_URL + '/api/v1/groups')
.query({ access_token: token })
.send({ name: 'externals'})
.end(function (error, result) {
expect(result.statusCode).to.equal(409);
done();
});
});
});
describe('get', function () {
it('cannot get non-existing group', function (done) {
superagent.get(SERVER_URL + '/api/v1/groups/nope')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
done();
});
});
it('cannot get existing group with normal user', function (done) {
superagent.get(SERVER_URL + '/api/v1/groups/admin')
.query({ access_token: token_1 })
.end(function (error, result) {
expect(result.statusCode).to.equal(403);
done();
});
});
it('can get existing group', function (done) {
superagent.get(SERVER_URL + '/api/v1/groups/admin')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.name).to.be('admin');
expect(result.body.userIds.length).to.be(1);
expect(result.body.userIds[0]).to.be(USERNAME);
done();
});
});
});
describe('remove', function () {
it('cannot remove without token', function (done) {
superagent.del(SERVER_URL + '/api/v1/groups/externals')
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('can remove empty group', function (done) {
superagent.del(SERVER_URL + '/api/v1/groups/externals')
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
done();
});
});
it('cannot remove non-empty group', function (done) {
superagent.del(SERVER_URL + '/api/v1/groups/admin')
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(409);
done();
});
});
});
describe('Set groups', function () {
before(function (done) {
async.series([
groups.create.bind(null, 'group0'),
groups.create.bind(null, 'group1')
], done);
});
it('cannot add user to invalid group', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME + '/set_groups')
.query({ access_token: token })
.send({ groupIds: [ 'admin', 'something' ]})
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
done();
});
});
it('can add user to valid group', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME + '/set_groups')
.query({ access_token: token })
.send({ groupIds: [ 'admin', 'group0', 'group1' ]})
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
done();
});
});
it('can remove last user from admin', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME + '/set_groups')
.query({ access_token: token })
.send({ groupIds: [ 'group0', 'group1' ]})
.end(function (error, result) {
expect(result.statusCode).to.equal(403); // not allowed
done();
});
});
});
});

View File

@@ -139,13 +139,13 @@ describe('OAuth2', function () {
var USER_0 = {
id: uuid.v4(),
username: 'someusername',
password: 'somepassword',
password: '@#45Strongpassword',
email: 'some@email.com',
admin: true,
salt: 'somesalt',
createdAt: (new Date()).toUTCString(),
modifiedAt: (new Date()).toUTCString(),
resetToken: hat(256)
resetToken: hat(256),
displayName: ''
};
var APP_0 = {
@@ -155,7 +155,7 @@ describe('OAuth2', function () {
location: 'test',
portBindings: {},
accessRestriction: null,
oauthProxy: true
memoryLimit: 0
};
var APP_1 = {
@@ -165,7 +165,7 @@ describe('OAuth2', function () {
location: 'test1',
portBindings: {},
accessRestriction: { users: [ 'foobar' ] },
oauthProxy: true
memoryLimit: 0
};
var APP_2 = {
@@ -175,7 +175,17 @@ describe('OAuth2', function () {
location: 'test2',
portBindings: {},
accessRestriction: { users: [ USER_0.id ] },
oauthProxy: true
memoryLimit: 0
};
var APP_3 = {
id: 'app3',
appStoreId: '',
manifest: { version: '0.1.0', addons: { } },
location: 'test3',
portBindings: {},
accessRestriction: { groups: [ 'someothergroup', 'admin', 'anothergroup' ] },
memoryLimit: 0
};
// unknown app
@@ -268,6 +278,16 @@ describe('OAuth2', function () {
scope: 'profile'
};
// app with accessRestriction allowing group
var CLIENT_9 = {
id: 'cid-client9',
appId: APP_3.id,
type: clientdb.TYPE_OAUTH,
clientSecret: 'secret9',
redirectURI: 'http://redirect9',
scope: 'profile'
};
// make csrf always succeed for testing
oauth2.csrf = function (req, res, next) {
req.csrfToken = function () { return hat(256); };
@@ -287,11 +307,13 @@ describe('OAuth2', function () {
clientdb.add.bind(null, CLIENT_6.id, CLIENT_6.appId, CLIENT_6.type, CLIENT_6.clientSecret, CLIENT_6.redirectURI, CLIENT_6.scope),
clientdb.add.bind(null, CLIENT_7.id, CLIENT_7.appId, CLIENT_7.type, CLIENT_7.clientSecret, CLIENT_7.redirectURI, CLIENT_7.scope),
clientdb.add.bind(null, CLIENT_8.id, CLIENT_8.appId, CLIENT_8.type, CLIENT_8.clientSecret, CLIENT_8.redirectURI, CLIENT_8.scope),
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.oauthProxy),
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1.accessRestriction, APP_1.oauthProxy),
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2.accessRestriction, APP_2.oauthProxy),
clientdb.add.bind(null, CLIENT_9.id, CLIENT_9.appId, CLIENT_9.type, CLIENT_9.clientSecret, CLIENT_9.redirectURI, CLIENT_9.scope),
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.memoryLimit),
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1.accessRestriction, APP_1.memoryLimit),
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2.accessRestriction, APP_2.memoryLimit),
appdb.add.bind(null, APP_3.id, APP_3.appStoreId, APP_3.manifest, APP_3.location, APP_3.portBindings, APP_3.accessRestriction, APP_3.memoryLimit),
function (callback) {
user.create(USER_0.username, USER_0.password, USER_0.email, true, '', function (error, userObject) {
user.create(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, function (error, userObject) {
expect(error).to.not.be.ok();
// update the global objects to reflect the new user id
@@ -777,7 +799,7 @@ describe('OAuth2', function () {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(200);
expect(body.indexOf('<!-- error tester -->')).to.not.equal(-1);
expect(body.indexOf('Unkonwn OAuth client.')).to.not.equal(-1);
expect(body.indexOf('Unknown OAuth client.')).to.not.equal(-1);
done();
});
@@ -801,6 +823,21 @@ describe('OAuth2', function () {
});
});
it('fails for grant type code with accessRestriction (group)', function (done) { // USER_0 is not an admin
startAuthorizationFlow(CLIENT_9, 'code', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_9.redirectURI + '&client_id=' + CLIENT_9.id + '&response_type=code';
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(200);
expect(body.indexOf('<!-- error tester -->')).to.not.equal(-1);
expect(body.indexOf('No access to this app.')).to.not.equal(-1);
done();
});
});
});
it('fails for grant type token due to accessRestriction', function (done) {
startAuthorizationFlow(CLIENT_6, 'token', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_6.redirectURI + '&client_id=' + CLIENT_6.id + '&response_type=token';
@@ -824,7 +861,7 @@ describe('OAuth2', function () {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(200);
expect(body.indexOf('<!-- error tester -->')).to.not.equal(-1);
expect(body.indexOf('Unkonwn OAuth client.')).to.not.equal(-1);
expect(body.indexOf('Unknown OAuth client.')).to.not.equal(-1);
done();
});
@@ -1239,13 +1276,14 @@ describe('Password', function () {
var USER_0 = {
userId: uuid.v4(),
username: 'someusername',
password: 'somepassword',
password: 'passWord%1234',
email: 'some@email.com',
admin: true,
salt: 'somesalt',
createdAt: (new Date()).toUTCString(),
modifiedAt: (new Date()).toUTCString(),
resetToken: hat(256)
resetToken: hat(256),
displayName: ''
};
// make csrf always succeed for testing
@@ -1405,6 +1443,15 @@ describe('Password', function () {
});
});
it('fails due to weak password', function (done) {
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
.send({ password: 'foobar', resetToken: USER_0.resetToken })
.end(function (error, result) {
expect(result.statusCode).to.equal(406);
done();
});
});
it('succeeds', function (done) {
var scope = nock(config.adminOrigin())
.filteringPath(function (path) {
@@ -1415,7 +1462,7 @@ describe('Password', function () {
.get('/?accessToken=token&expiresAt=1234').reply(200, {});
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
.send({ password: 'somepassword', resetToken: USER_0.resetToken })
.send({ password: 'ASF23$%somepassword', resetToken: USER_0.resetToken })
.end(function (error, result) {
expect(scope.isDone()).to.be.ok();
expect(result.statusCode).to.equal(200);

View File

@@ -22,7 +22,7 @@ var appdb = require('../../appdb.js'),
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='silly@me.com';
var USERNAME = 'admin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
var server;
@@ -56,7 +56,7 @@ function setup(done) {
function addApp(callback) {
var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok' };
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, null /* accessRestriction */, false /* oauthProxy */, callback);
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, null /* accessRestriction */, 0 /* memoryLimit */, callback);
}
], done);
}

View File

@@ -21,7 +21,7 @@ describe('SimpleAuth API', function () {
var SERVER_URL = 'http://localhost:' + config.get('port');
var SIMPLE_AUTH_ORIGIN = 'http://localhost:' + config.get('simpleAuthPort');
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='silly@me.com';
var USERNAME = 'admin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var APP_0 = {
id: 'app0',
@@ -30,7 +30,7 @@ describe('SimpleAuth API', function () {
location: 'test0',
portBindings: {},
accessRestriction: { users: [ 'foobar', 'someone'] },
oauthProxy: true
memoryLimit: 0
};
var APP_1 = {
@@ -40,7 +40,7 @@ describe('SimpleAuth API', function () {
location: 'test1',
portBindings: {},
accessRestriction: { users: [ 'foobar', USERNAME, 'someone' ] },
oauthProxy: true
memoryLimit: 0
};
var APP_2 = {
@@ -50,7 +50,17 @@ describe('SimpleAuth API', function () {
location: 'test2',
portBindings: {},
accessRestriction: null,
oauthProxy: true
memoryLimit: 0
};
var APP_3 = {
id: 'app3',
appStoreId: '',
manifest: { version: '0.1.0', addons: { } },
location: 'test3',
portBindings: {},
accessRestriction: { groups: [ 'someothergroup', 'admin', 'anothergroup' ] },
memoryLimit: 0
};
var CLIENT_0 = {
@@ -98,6 +108,15 @@ describe('SimpleAuth API', function () {
scope: 'user,profile'
};
var CLIENT_5 = {
id: 'someclientid5',
appId: APP_3.id,
type: clientdb.TYPE_SIMPLE_AUTH,
clientSecret: 'someclientsecret5',
redirectURI: '',
scope: 'user,profile'
};
before(function (done) {
async.series([
server.start.bind(server),
@@ -128,9 +147,11 @@ describe('SimpleAuth API', function () {
clientdb.add.bind(null, CLIENT_2.id, CLIENT_2.appId, CLIENT_2.type, CLIENT_2.clientSecret, CLIENT_2.redirectURI, CLIENT_2.scope),
clientdb.add.bind(null, CLIENT_3.id, CLIENT_3.appId, CLIENT_3.type, CLIENT_3.clientSecret, CLIENT_3.redirectURI, CLIENT_3.scope),
clientdb.add.bind(null, CLIENT_4.id, CLIENT_4.appId, CLIENT_4.type, CLIENT_4.clientSecret, CLIENT_4.redirectURI, CLIENT_4.scope),
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.oauthProxy),
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1.accessRestriction, APP_1.oauthProxy),
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2.accessRestriction, APP_2.oauthProxy)
clientdb.add.bind(null, CLIENT_5.id, CLIENT_5.appId, CLIENT_5.type, CLIENT_5.clientSecret, CLIENT_5.redirectURI, CLIENT_5.scope),
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.memoryLimit),
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1.accessRestriction, APP_1.memoryLimit),
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2.accessRestriction, APP_2.memoryLimit),
appdb.add.bind(null, APP_3.id, APP_3.appStoreId, APP_3.manifest, APP_3.location, APP_3.portBindings, APP_3.accessRestriction, APP_3.memoryLimit)
], done);
});
@@ -333,6 +354,37 @@ describe('SimpleAuth API', function () {
});
});
it('succeeds for app with group accessRestriction', function (done) {
var body = {
clientId: CLIENT_5.id,
username: USERNAME,
password: PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(200);
expect(result.body.accessToken).to.be.a('string');
expect(result.body.user).to.be.an('object');
expect(result.body.user.id).to.be.a('string');
expect(result.body.user.username).to.be.a('string');
expect(result.body.user.email).to.be.a('string');
expect(result.body.user.admin).to.be.a('boolean');
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: result.body.accessToken })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.body).to.be.an('object');
expect(result.body.username).to.eql(USERNAME);
done();
});
});
});
it('fails for wrong client credentials', function (done) {
var body = {
clientId: CLIENT_4.id,

View File

@@ -79,7 +79,7 @@ start_mongodb
start_mail
echo -n "Waiting for addons to start"
for i in {1..20}; do
for i in {1..10}; do
echo -n "."
sleep 1
done

View File

@@ -10,6 +10,8 @@ var config = require('../../config.js'),
database = require('../../database.js'),
tokendb = require('../../tokendb.js'),
expect = require('expect.js'),
groups = require('../../groups.js'),
mailer = require('../../mailer.js'),
superagent = require('superagent'),
nock = require('nock'),
server = require('../../server.js'),
@@ -17,16 +19,23 @@ var config = require('../../config.js'),
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME_0 = 'admin', PASSWORD = 'password', EMAIL = 'silly@me.com', EMAIL_0_NEW = 'stupid@me.com';
var USERNAME_0 = 'admin', PASSWORD = 'Foobar?1337', EMAIL_0 = 'silly@me.com', EMAIL_0_NEW = 'stupid@me.com', DISPLAY_NAME_0_NEW = 'New Name';
var USERNAME_1 = 'userTheFirst', EMAIL_1 = 'tao@zen.mac';
var USERNAME_2 = 'userTheSecond', EMAIL_2 = 'user@foo.bar';
var USERNAME_2 = 'userTheSecond', EMAIL_2 = 'user@foo.bar', EMAIL_2_NEW = 'happy@me.com';
var USERNAME_3 = 'userTheThird', EMAIL_3 = 'user3@foo.bar';
var server;
function setup(done) {
server.start(function (error) {
expect(!error).to.be.ok();
userdb._clear(done);
mailer._clearMailQueue();
userdb._clear(function (error) {
expect(error).to.eql(null);
groups.create('somegroupid', done);
});
});
}
@@ -34,10 +43,21 @@ function cleanup(done) {
database._clear(function (error) {
expect(!error).to.be.ok();
mailer._clearMailQueue();
server.stop(done);
});
}
function checkMails(number, done) {
// mails are enqueued async
setTimeout(function () {
expect(mailer._getMailQueue().length).to.equal(number);
mailer._clearMailQueue();
done();
}, 500);
}
describe('User API', function () {
this.timeout(5000);
@@ -85,7 +105,7 @@ describe('User API', function () {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME_0, password: PASSWORD, email: EMAIL })
.send({ username: USERNAME_0, password: PASSWORD, email: EMAIL_0 })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
@@ -113,7 +133,7 @@ describe('User API', function () {
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.username).to.equal(USERNAME_0);
expect(res.body.email).to.equal(EMAIL);
expect(res.body.email).to.equal(EMAIL_0);
expect(res.body.admin).to.be.ok();
// stash for further use
@@ -147,7 +167,7 @@ describe('User API', function () {
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.username).to.equal(USERNAME_0);
expect(res.body.email).to.equal(EMAIL);
expect(res.body.email).to.equal(EMAIL_0);
expect(res.body.admin).to.be.ok();
done();
});
@@ -186,8 +206,9 @@ describe('User API', function () {
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.username).to.equal(USERNAME_0);
expect(res.body.email).to.equal(EMAIL);
expect(res.body.email).to.equal(EMAIL_0);
expect(res.body.admin).to.be.ok();
expect(res.body.displayName).to.be.a('string');
expect(res.body.password).to.not.be.ok();
expect(res.body.salt).to.not.be.ok();
done();
@@ -213,66 +234,91 @@ describe('User API', function () {
});
it('create second user succeeds', function (done) {
mailer._clearMailQueue();
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_1, email: EMAIL_1 })
.send({ username: USERNAME_1, email: EMAIL_1, invite: true })
.end(function (err, res) {
expect(err).to.not.be.ok();
expect(res.statusCode).to.equal(201);
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, tokendb.PREFIX_USER + USERNAME_1, 'test-client-id', Date.now() + 10000, '*', done);
checkMails(2, function () {
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, tokendb.PREFIX_USER + USERNAME_1, 'test-client-id', Date.now() + 10000, '*', done);
});
});
});
it('reinvite unknown user fails', function (done) {
mailer._clearMailQueue();
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_1+USERNAME_1 + '/invite')
.query({ access_token: token })
.send({})
.end(function (err, res) {
expect(err).to.be.an(Error);
expect(res.statusCode).to.equal(404);
checkMails(0, done);
});
});
it('reinvite second user succeeds', function (done) {
mailer._clearMailQueue();
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_1 + '/invite')
.query({ access_token: token })
.send({})
.end(function (err, res) {
expect(err).to.not.be.ok();
expect(res.statusCode).to.equal(200);
checkMails(1, done);
});
});
it('set second user as admin succeeds', function (done) {
// TODO is USERNAME_1 in body and url redundant?
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_1 + '/admin')
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_1 + '/set_groups')
.query({ access_token: token })
.send({ username: USERNAME_1, admin: true })
.send({ groupIds: [ groups.ADMIN_GROUP_ID ] })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_1)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.admin).to.equal(true);
done();
});
});
});
it('remove first user from admins succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/admin')
.query({ access_token: token_1 })
.send({ username: USERNAME_0, admin: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
it('remove second user by first, now normal, user fails', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_1)
it('remove itself from admins fails', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/set_groups')
.query({ access_token: token })
.send({ password: PASSWORD })
.send({ groupIds: [ 'somegroupid' ] })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
});
});
it('remove second user from admins and thus last admin fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_1 + '/admin')
.query({ access_token: token_1 })
.send({ username: USERNAME_1, admin: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
});
});
it('reset first user as admin succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/admin')
.query({ access_token: token_1 })
.send({ username: USERNAME_0, admin: true })
it('remove second user from admins succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_1 + '/set_groups')
.query({ access_token: token })
.send({ groupIds: [ 'somegroupid' ] })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_1)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.admin).to.equal(false);
done();
});
});
});
@@ -296,29 +342,53 @@ describe('User API', function () {
});
});
it('create second and third user', function (done) {
it('create user missing invite fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_2, email: EMAIL_2 })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('create second and third user', function (done) {
mailer._clearMailQueue();
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_2, email: EMAIL_2, invite: false })
.end(function (error, res) {
expect(res.statusCode).to.equal(201);
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_3, email: EMAIL_3 })
.send({ username: USERNAME_3, email: EMAIL_3, invite: true })
.end(function (error, res) {
expect(res.statusCode).to.equal(201);
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_2, tokendb.PREFIX_USER + USERNAME_2, 'test-client-id', Date.now() + 10000, '*', done);
// one mail for first user creation, two mails for second user creation (see 'invite' flag)
checkMails(3, function () {
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_2, tokendb.PREFIX_USER + USERNAME_2, 'test-client-id', Date.now() + 10000, '*', done);
});
});
});
});
it('second user userInfo', function (done) {
it('second user userInfo fails for first user', function (done) {
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_2)
.query({ access_token: token_1 })
.end(function (error, result) {
expect(result.statusCode).to.equal(403);
done();
});
});
it('second user userInfo succeeds for second user', function (done) {
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_2)
.query({ access_token: token_2 })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.username).to.equal(USERNAME_2);
expect(result.body.email).to.equal(EMAIL_2);
@@ -331,16 +401,25 @@ describe('User API', function () {
it('create user with same username should fail', function (done) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_2, email: EMAIL })
.send({ username: USERNAME_2, email: EMAIL_0, invite: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
done();
});
});
it('list users', function (done) {
it('list users fails for normal user', function (done) {
superagent.get(SERVER_URL + '/api/v1/users')
.query({ access_token: token_2 })
.end(function (error, res) {
expect(res.statusCode).to.equal(403);
done();
});
});
it('list users succeeds for admin', function (done) {
superagent.get(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.end(function (error, res) {
expect(error).to.be(null);
expect(res.statusCode).to.equal(200);
@@ -371,7 +450,7 @@ describe('User API', function () {
});
it('admin cannot remove normal user without giving a password', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_1)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
@@ -380,7 +459,7 @@ describe('User API', function () {
});
it('admin cannot remove normal user with empty password', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_1)
.query({ access_token: token })
.send({ password: '' })
.end(function (err, res) {
@@ -390,7 +469,7 @@ describe('User API', function () {
});
it('admin cannot remove normal user with giving wrong password', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_1)
.query({ access_token: token })
.send({ password: PASSWORD + PASSWORD })
.end(function (err, res) {
@@ -400,7 +479,7 @@ describe('User API', function () {
});
it('admin removes normal user', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_1)
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (err, res) {
@@ -422,29 +501,9 @@ describe('User API', function () {
// Change email
it('change email fails due to missing token', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
.send({ password: PASSWORD, email: EMAIL_0_NEW })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('change email fails due to missing password', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
.query({ access_token: token })
.send({ email: EMAIL_0_NEW })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('change email fails due to wrong password', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
.query({ access_token: token })
.send({ password: PASSWORD+PASSWORD, email: EMAIL_0_NEW })
.end(function (error, result) {
expect(result.statusCode).to.equal(403);
expect(result.statusCode).to.equal(401);
done();
});
});
@@ -452,20 +511,93 @@ describe('User API', function () {
it('change email fails due to invalid email', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
.query({ access_token: token })
.send({ password: PASSWORD, email: 'foo@bar' })
.send({ email: 'foo@bar' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('change email succeeds', function (done) {
it('change email for other user fails', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
.query({ access_token: token_2 })
.send({ email: 'foobar@bar.baz' })
.end(function (error, result) {
expect(result.statusCode).to.equal(403);
done();
});
});
it('change user succeeds without email nor displayName', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
.query({ access_token: token })
.send({ password: PASSWORD, email: EMAIL_0_NEW })
.send({})
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
done(error);
done();
});
});
it('change email for own user succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_2)
.query({ access_token: token_2 })
.send({ email: EMAIL_2_NEW })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_2)
.query({ access_token: token_2 })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.username).to.equal(USERNAME_2);
expect(res.body.email).to.equal(EMAIL_2_NEW);
expect(res.body.admin).to.equal(false);
expect(res.body.displayName).to.equal('');
done();
});
});
});
it('change email as admin for other user succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_2)
.query({ access_token: token })
.send({ email: EMAIL_2 })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_2)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.username).to.equal(USERNAME_2);
expect(res.body.email).to.equal(EMAIL_2);
expect(res.body.admin).to.equal(false);
expect(res.body.displayName).to.equal('');
done();
});
});
});
it('change displayName succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
.query({ access_token: token })
.send({ displayName: DISPLAY_NAME_0_NEW })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.username).to.equal(USERNAME_0);
expect(res.body.email).to.equal(EMAIL_0);
expect(res.body.admin).to.be.ok();
expect(res.body.displayName).to.equal(DISPLAY_NAME_0_NEW);
done();
});
});
});
@@ -493,7 +625,7 @@ describe('User API', function () {
it('change password fails due to wrong password', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
.query({ access_token: token })
.send({ password: 'some wrong password', newPassword: 'newpassword' })
.send({ password: 'some wrong password', newPassword: 'MOre#$%34' })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
@@ -513,7 +645,7 @@ describe('User API', function () {
it('change password succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
.query({ access_token: token })
.send({ password: PASSWORD, newPassword: 'new_password' })
.send({ password: PASSWORD, newPassword: 'MOre#$%34' })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();

View File

@@ -9,30 +9,22 @@ exports = module.exports = {
list: listUser,
create: createUser,
changePassword: changePassword,
changeAdmin: changeAdmin,
remove: removeUser,
verifyPassword: verifyPassword,
requireAdmin: requireAdmin
requireAdmin: requireAdmin,
sendInvite: sendInvite,
setGroups: setGroups
};
var assert = require('assert'),
generatePassword = require('../password.js').generate,
groups = require('../groups.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
user = require('../user.js'),
tokendb = require('../tokendb.js'),
UserError = user.UserError;
// http://stackoverflow.com/questions/1497481/javascript-password-generator#1497512
function generatePassword() {
var length = 8,
charset = 'abcdefghijklnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
retVal = '';
for (var i = 0, n = charset.length; i < length; ++i) {
retVal += charset.charAt(Math.floor(Math.random() * n));
}
return retVal;
}
function profile(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
@@ -43,10 +35,18 @@ function profile(req, res, next) {
if (req.user.tokenType === tokendb.TYPE_USER || req.user.tokenType === tokendb.TYPE_DEV) {
result.username = req.user.username;
result.email = req.user.email;
result.admin = req.user.admin;
}
result.displayName = req.user.displayName;
next(new HttpSuccess(200, result));
groups.isMember(groups.ADMIN_GROUP_ID, req.user.id, function (error, isAdmin) {
if (error) return next(new HttpError(500, error));
result.admin = isAdmin;
next(new HttpSuccess(200, result));
});
} else {
next(new HttpSuccess(200, result));
}
}
function createUser(req, res, next) {
@@ -54,12 +54,16 @@ function createUser(req, res, next) {
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be string'));
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
if (typeof req.body.invite !== 'boolean') return next(new HttpError(400, 'invite must be boolean'));
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
var username = req.body.username;
var password = generatePassword();
var email = req.body.email;
var sendInvite = req.body.invite;
var displayName = req.body.displayName || '';
user.create(username, password, email, false /* admin */, req.user /* creator */, function (error, user) {
user.create(username, password, email, displayName, { invitor: req.user, sendInvite: sendInvite }, function (error, user) {
if (error && error.reason === UserError.BAD_USERNAME) return next(new HttpError(400, 'Invalid username'));
if (error && error.reason === UserError.BAD_EMAIL) return next(new HttpError(400, 'Invalid email'));
if (error && error.reason === UserError.BAD_PASSWORD) return next(new HttpError(400, 'Invalid password'));
@@ -80,33 +84,27 @@ function createUser(req, res, next) {
}
function update(req, res, next) {
assert.strictEqual(typeof req.params.userId, 'string');
assert.strictEqual(typeof req.user, 'object');
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
if ('email' in req.body && typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
if (req.user.tokenType !== tokendb.TYPE_USER) return next(new HttpError(403, 'Token type not allowed'));
if (req.user.id !== req.params.userId && !req.user.admin) return next(new HttpError(403, 'Not allowed'));
user.update(req.user.id, req.user.username, req.body.email, function (error) {
if (error && error.reason === UserError.BAD_EMAIL) return next(new HttpError(400, error.message));
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
user.get(req.params.userId, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
});
}
user.update(req.params.userId, result.username, req.body.email || result.email, req.body.displayName || result.displayName, function (error) {
if (error && error.reason === UserError.BAD_EMAIL) return next(new HttpError(400, error.message));
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
if (error) return next(new HttpError(500, error));
function changeAdmin(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'API call requires a username.'));
if (typeof req.body.admin !== 'boolean') return next(new HttpError(400, 'API call requires an admin setting.'));
user.changeAdmin(req.body.username, req.body.admin, function (error) {
if (error && error.reason === UserError.NOT_ALLOWED) return next(new HttpError(403, 'Last admin'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
next(new HttpSuccess(204));
});
});
}
@@ -138,17 +136,25 @@ function listUser(req, res, next) {
function info(req, res, next) {
assert.strictEqual(typeof req.params.userId, 'string');
assert.strictEqual(typeof req.user, 'object');
if (req.user.id !== req.params.userId && !req.user.admin) return next(new HttpError(403, 'Not allowed'));
user.get(req.params.userId, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, {
id: result.id,
username: result.username,
email: result.email,
admin: result.admin
}));
groups.isMember(groups.ADMIN_GROUP_ID, req.params.userId, function (error, isAdmin) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, {
id: result.id,
username: result.username,
email: result.email,
admin: isAdmin,
displayName: result.displayName
}));
});
});
}
@@ -178,12 +184,19 @@ function verifyPassword(req, res, next) {
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'API call requires user password'));
user.verify(req.user.username, req.body.password, function (error) {
if (error && error.reason === UserError.WRONG_PASSWORD) return next(new HttpError(403, 'Password incorrect'));
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(403, 'Password incorrect'));
groups.isMember(groups.ADMIN_GROUP_ID, req.user.id, function (error, isAdmin) {
if (error) return next(new HttpError(500, error));
next();
// Only allow admins or users, operating on themselves
if (req.params.userId && !(req.user.id === req.params.userId || isAdmin)) return next(new HttpError(403, 'Not allowed'));
user.verify(req.user.username, req.body.password, function (error) {
if (error && error.reason === UserError.WRONG_PASSWORD) return next(new HttpError(403, 'Password incorrect'));
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(403, 'Password incorrect'));
if (error) return next(new HttpError(500, error));
next();
});
});
}
@@ -198,3 +211,30 @@ function requireAdmin(req, res, next) {
next();
}
function sendInvite(req, res, next) {
assert.strictEqual(typeof req.params.userId, 'string');
user.sendInvite(req.params.userId, function (error) {
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, {}));
});
}
function setGroups(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.userId, 'string');
if (!Array.isArray(req.body.groupIds)) return next(new HttpError(400, 'API call requires a groups array.'));
// this route is only allowed for admins, so req.user has to be an admin
if (req.user.id === req.params.userId && req.body.groupIds.indexOf(groups.ADMIN_GROUP_ID) === -1) return next(new HttpError(403, 'Admin removing itself from admins is not allowed'));
user.setGroups(req.params.userId, req.body.groupIds, function (error) {
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'One or more groups not found'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
});
}

View File

@@ -12,29 +12,12 @@ var appdb = require('./appdb.js'),
CronJob = require('cron').CronJob,
debug = require('debug')('box:src/scheduler'),
docker = require('./docker.js'),
paths = require('./paths.js'),
safe = require('safetydance'),
_ = require('underscore');
var NOOP_CALLBACK = function (error) { if (error) debug('Unhandled error: ', error); };
// appId -> { schedulerConfig (manifest), cronjobs, containerIds }
var gState = (function loadState() {
var state = safe.JSON.parse(safe.fs.readFileSync(paths.SCHEDULER_FILE, 'utf8'));
return state || { };
})();
function saveState(state) {
// do not save cronJobs
var safeState = { };
for (var appId in state) {
safeState[appId] = {
schedulerConfig: state[appId].schedulerConfig,
containerIds: state[appId].containerIds
};
}
safe.fs.writeFileSync(paths.SCHEDULER_FILE, JSON.stringify(safeState, null, 4), 'utf8');
}
// appId -> { schedulerConfig (manifest), cronjobs }
var gState = { };
function sync(callback) {
assert(!callback || typeof callback === 'function');
@@ -46,17 +29,18 @@ function sync(callback) {
apps.getAll(function (error, allApps) {
if (error) return callback(error);
// stop tasks of apps that went away
var allAppIds = allApps.map(function (app) { return app.id; });
var removedAppIds = _.difference(Object.keys(gState), allAppIds);
if (removedAppIds.length !== 0) debug('sync: stopping jobs of removed apps %j', removedAppIds);
async.eachSeries(removedAppIds, function (appId, iteratorDone) {
stopJobs(appId, gState[appId], true /* killContainers */, iteratorDone);
stopJobs(appId, gState[appId], iteratorDone);
}, function (error) {
if (error) debug('Error stopping jobs : %j', error);
if (error) debug('Error stopping jobs of removed apps', error);
gState = _.omit(gState, removedAppIds);
// start tasks of new apps
debug('sync: checking apps %j', allAppIds);
async.eachSeries(allApps, function (app, iteratorDone) {
var appState = gState[app.id] || null;
var schedulerConfig = app.manifest.addons.scheduler || null;
@@ -67,8 +51,8 @@ function sync(callback) {
return iteratorDone(); // nothing changed
}
var killContainers = appState && !appState.cronJobs ? true : false; // keep the old containers on 'startup'
stopJobs(app.id, appState, killContainers, function (error) {
debug('sync: app %s changed', app.id);
stopJobs(app.id, appState, function (error) {
if (error) debug('Error stopping jobs for %s : %s', app.id, error.message);
if (!schedulerConfig) {
@@ -78,12 +62,9 @@ function sync(callback) {
gState[app.id] = {
schedulerConfig: schedulerConfig,
cronJobs: createCronJobs(app.id, schedulerConfig),
containerIds: { }
cronJobs: createCronJobs(app.id, schedulerConfig)
};
saveState(gState);
iteratorDone();
});
});
@@ -93,23 +74,22 @@ function sync(callback) {
});
}
function killContainer(containerId, callback) {
if (!containerId) return callback();
function killContainer(containerName, callback) {
if (!containerName) return callback();
async.series([
docker.stopContainer.bind(null, containerId),
docker.deleteContainer.bind(null, containerId)
docker.stopContainerByName.bind(null, containerName),
docker.deleteContainerByName.bind(null, containerName)
], function (error) {
if (error) debug('Failed to kill task with containerId %s : %s', containerId, error.message);
if (error) debug('Failed to kill task with name %s : %s', containerName, error.message);
callback(error);
});
}
function stopJobs(appId, appState, killContainers, callback) {
function stopJobs(appId, appState, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof appState, 'object');
assert.strictEqual(typeof killContainers, 'boolean');
assert.strictEqual(typeof callback, 'function');
debug('stopJobs for %s', appId);
@@ -121,9 +101,8 @@ function stopJobs(appId, appState, killContainers, callback) {
appState.cronJobs[taskName].stop();
}
if (!killContainers) return iteratorDone();
killContainer(appState.containerIds[taskName], iteratorDone);
var containerName = appId + '-' + taskName;
killContainer(containerName, iteratorDone);
}, callback);
}
@@ -161,8 +140,6 @@ function doTask(appId, taskName, callback) {
callback = callback || NOOP_CALLBACK;
var appState = gState[appId];
debug('Executing task %s/%s', appId, taskName);
apps.get(appId, function (error, app) {
@@ -173,21 +150,17 @@ function doTask(appId, taskName, callback) {
return callback();
}
if (appState.containerIds[taskName]) debug('task %s/%s has existing container %s. killing it', appId, taskName, appState.containerIds[taskName]);
var containerName = app.id + '-' + taskName;
killContainer(appState.containerIds[taskName], function (error) {
killContainer(containerName, function (error) {
if (error) return callback(error);
debug('Creating createSubcontainer for %s/%s : %s', app.id, taskName, gState[appId].schedulerConfig[taskName].command);
debug('Creating subcontainer for %s/%s : %s', app.id, taskName, gState[appId].schedulerConfig[taskName].command);
// NOTE: if you change container name here, fix addons.js to return correct container names
docker.createSubcontainer(app, app.id + '-' + taskName, [ '/bin/sh', '-c', gState[appId].schedulerConfig[taskName].command ], { } /* options */, function (error, container) {
docker.createSubcontainer(app, containerName, [ '/bin/sh', '-c', gState[appId].schedulerConfig[taskName].command ], { } /* options */, function (error, container) {
if (error) return callback(error);
appState.containerIds[taskName] = container.id;
saveState(gState);
docker.startContainer(container.id, callback);
});
});

View File

@@ -12,8 +12,8 @@ if [[ $# == 1 && "$1" == "--check" ]]; then
exit 0
fi
if [ $# -lt 3 ]; then
echo "Usage: backupapp.sh <appid> <url> <key> [aws session token]"
if [ $# -lt 4 ]; then
echo "Usage: backupapp.sh <appid> <url> <url> <key> [aws session token]"
exit 1
fi
@@ -21,8 +21,9 @@ readonly DATA_DIR="${HOME}/data"
app_id="$1"
backup_url="$2"
backup_key="$3"
session_token="$4"
backup_config_url="$3"
backup_key="$4"
session_token="$5"
readonly now=$(date "+%Y-%m-%dT%H:%M:%S")
readonly app_data_dir="${DATA_DIR}/${app_id}"
readonly app_data_snapshot="${DATA_DIR}/snapshots/${app_id}-${now}"
@@ -42,7 +43,30 @@ for try in `seq 1 5`; do
if tar -cvzf - -C "${app_data_snapshot}" . \
| openssl aes-256-cbc -e -pass "pass:${backup_key}" \
| curl --fail -X PUT "${headers[@]}" --data-binary @- "${backup_url}" 2>"${error_log}"; then
| curl --fail -X PUT ${headers[@]} --data-binary @- "${backup_url}" 2>"${error_log}"; then
break
fi
cat "${error_log}" && rm "${error_log}"
done
if [[ ${try} -eq 5 ]]; then
echo "Backup failed uploading backup tarball"
exit 1
fi
for try in `seq 1 5`; do
echo "Uploading config.json to ${backup_config_url} (try ${try})"
error_log=$(mktemp)
headers=("-H" "Content-Type:")
# federated tokens in CaaS case need session token
if [ ! -z "$session_token" ]; then
headers=(${headers[@]} "-H" "x-amz-security-token: ${session_token}")
fi
if cat "${app_data_snapshot}/config.json" \
| curl --fail -X PUT ${headers[@]} --data @- "${backup_config_url}" 2>"${error_log}"; then
break
fi
cat "${error_log}" && rm "${error_log}"
@@ -51,9 +75,8 @@ done
btrfs subvolume delete "${app_data_snapshot}"
if [[ ${try} -eq 5 ]]; then
echo "Backup failed"
echo "Backup failed uploading config.json"
exit 1
else
echo "Backup successful"
fi

View File

@@ -21,7 +21,7 @@ readonly program_name=$1
echo "${program_name}.log"
echo "-------------------"
journalctl --no-pager -u ${program_name} -n 100
journalctl --all --no-pager -u ${program_name} -n 100
echo
echo
echo "dmesg"
@@ -31,7 +31,7 @@ echo
echo
echo "docker"
echo "------"
journalctl --no-pager -u docker -n 50
journalctl --all --no-pager -u docker -n 50
echo
echo

35
src/scripts/retire.sh Executable file
View File

@@ -0,0 +1,35 @@
#!/bin/bash
# This script is called once at the end of a cloudrons lifetime
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
readonly BOX_SRC_DIR=/home/yellowtent/box
if [[ $# == 1 && "$1" == "--check" ]]; then
echo "OK"
exit 0
fi
echo "Retiring cloudron"
if [[ "${BOX_ENV}" != "cloudron" ]]; then
exit 0
fi
"${BOX_SRC_DIR}/setup/splashpage.sh" --retire --data "$1" # show splash
echo "Stopping apps"
systemctl stop docker # stop the apps
echo "Stopping installer"
systemctl stop cloudron-installer # stop the installer
# do this at the end since stopping the box will kill this script as well
echo "Stopping Cloudron Smartserver"
"${BOX_SRC_DIR}/setup/stop.sh"

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