Compare commits

..

470 Commits

Author SHA1 Message Date
Girish Ramakrishnan ca60d4c8b8 1.8.4 changes 2017-12-07 20:17:35 +05:30
Girish Ramakrishnan 2ce00ca0d7 Fix display of DNS records when not using cloudron-smtp
Fixes #492
2017-12-07 20:13:28 +05:30
Girish Ramakrishnan a57db15b63 Bump mail container for internal email relay fix 2017-12-07 20:12:34 +05:30
Johannes Zellner f14a8b0ab0 add 1.8.3 changes 2017-11-23 02:48:16 +01:00
Girish Ramakrishnan 5f207716e5 1.8.2 changes 2017-11-23 02:48:16 +01:00
Johannes Zellner 010d48035b Setup domain record in settingsdb during dns setup 2017-11-23 02:48:12 +01:00
Girish Ramakrishnan 7e6a83df84 Fix migration callback 2017-11-18 02:11:00 -08:00
Girish Ramakrishnan ec4910a45e Fix restore 2017-11-17 22:35:56 -08:00
Girish Ramakrishnan 6558c78094 change the json blobs to text 2017-11-17 15:52:40 -08:00
Girish Ramakrishnan 5df92d1903 remove dead code 2017-11-17 15:18:06 -08:00
Girish Ramakrishnan 05affa7d26 remove dead code 2017-11-17 15:17:50 -08:00
Girish Ramakrishnan 46c6c5a5a8 remove double .js 2017-11-17 14:50:53 -08:00
Girish Ramakrishnan 75da751c72 1.8.1 changes 2017-11-17 14:50:53 -08:00
Johannes Zellner b84f60671e Also fix the restoreConfigJson migration down script 2017-11-17 23:45:22 +01:00
Johannes Zellner 8dcb06cb02 Fix db migration down step for newConfigJson change 2017-11-17 23:41:22 +01:00
Girish Ramakrishnan 83bf739081 Update the license 2017-11-17 10:46:12 -08:00
Girish Ramakrishnan 48a52fae2e LE agreement URL has changed 2017-11-17 10:35:58 -08:00
Girish Ramakrishnan 0ddbda6068 Fix crash 2017-11-16 15:11:12 -08:00
Girish Ramakrishnan 360fa058ea store format information for restoring
fixes #483
2017-11-16 15:01:27 -08:00
Johannes Zellner 489d2022e6 Do not underline errored links 2017-11-16 23:18:50 +01:00
Girish Ramakrishnan f762d0c0a1 newConfig -> updateConfig 2017-11-16 12:36:07 -08:00
Girish Ramakrishnan 98cad0678d Handle json parse errors with new body-parser module 2017-11-16 11:47:17 -08:00
Girish Ramakrishnan 92acb2954f Rename restoreConfig to manifest in backup table
Only the manifest needs to be preserved in the backup table
2017-11-16 11:25:40 -08:00
Girish Ramakrishnan 00a6e4c982 Show doc url in info dialog
Fixes #486
2017-11-16 10:05:49 -08:00
Girish Ramakrishnan bf9eb4bd87 Switch the default to logs to show some useful information 2017-11-16 10:05:49 -08:00
Girish Ramakrishnan 2f4940acbd update modules 2017-11-16 09:34:00 -08:00
Girish Ramakrishnan 9f7ca552a6 handle various appstore errors 2017-11-16 00:23:34 -08:00
Girish Ramakrishnan 4272d5be8a Send feedback via API
Fixes #484
2017-11-15 23:31:13 -08:00
Girish Ramakrishnan 1babfb6e87 Allow admins to access all apps
Fixes #420
2017-11-15 19:24:11 -08:00
Girish Ramakrishnan 5663cf45f8 remove redundant reset 2017-11-15 19:08:38 -08:00
Girish Ramakrishnan d8cb2d1d25 test: reset is already part of setup 2017-11-15 18:56:27 -08:00
Girish Ramakrishnan 174a60bb07 fix linter warnings 2017-11-15 18:56:27 -08:00
Girish Ramakrishnan 3d7094bf28 Handle error in uploadFile 2017-11-15 18:45:23 -08:00
Girish Ramakrishnan 4d6616930a Fix failing test 2017-11-15 18:41:37 -08:00
Girish Ramakrishnan 24875ba292 Handle all errors and set focus correctly
Fixes #485
2017-11-14 18:26:42 -08:00
Johannes Zellner c58b2677b6 Fixup config tests and do not allow saving random values to the config file
Those will eventually be overwritten by start.sh anyways, we cannot rely
on those
2017-11-15 02:41:40 +01:00
Johannes Zellner 25146e1134 Allow tests to work without a cloudron.conf on disk 2017-11-15 02:40:50 +01:00
Johannes Zellner c0c35964fe Fix backups tests 2017-11-15 02:29:58 +01:00
Johannes Zellner 0bf9ab0a2b No need to put static database config in cloudron.conf 2017-11-15 02:29:36 +01:00
Johannes Zellner 6d86f4cbda Ensure we only save relevant config values 2017-11-15 02:29:07 +01:00
Girish Ramakrishnan d2741bbeb9 Allow mailTo to be configurable
Part of #485
2017-11-14 16:24:34 -08:00
Girish Ramakrishnan 690d02a353 Always show the DNS records in the UI 2017-11-14 15:13:56 -08:00
Johannes Zellner c629db9597 Remove preinstall app bundle support 2017-11-14 23:09:17 +01:00
Girish Ramakrishnan 67fcf85abb Allow restore if already restoring 2017-11-13 18:43:36 -08:00
Girish Ramakrishnan 527eace8f8 Fix j2xml usage 2017-11-13 11:10:42 -08:00
Girish Ramakrishnan e65230b833 update many dev modules 2017-11-13 10:57:36 -08:00
Girish Ramakrishnan 3e8334040b Update many node modules
also, use rimraf instead of del
2017-11-13 10:57:32 -08:00
Girish Ramakrishnan 2bcd3a8e4d Add a hack to stretch the multi-select box a bit 2017-11-12 02:50:28 -08:00
Girish Ramakrishnan e75b85fc3a Bump postgresql container to workaround shm issues
reconfiguring the postgresql configuring seems to fix some shm
issues on docker upgrade
2017-11-11 20:52:34 -08:00
Girish Ramakrishnan c4362d3339 Fix failing ldap test 2017-11-11 17:33:27 -08:00
Girish Ramakrishnan 85e492a632 Fix detection of container id from IP
https://docs.docker.com/engine/api/v1.32/#tag/Network

"Note that it uses a different, smaller representation of a network
than inspecting a single network. For example, the list of containers
attached to the network is not propagated in API versions 1.28 and up."

Verified using:

curl --unix-socket /var/run/docker.sock http::/networks/cloudron
2017-11-11 16:55:43 -08:00
Girish Ramakrishnan b8d4b67043 update aws-sdk and dockerode 2017-11-11 16:38:40 -08:00
Girish Ramakrishnan ffacd31259 bump the node version 2017-11-11 16:25:42 -08:00
Johannes Zellner 19f6da88da Do not disable access control elements if no group was created
There are still users to be selected
2017-11-12 00:09:05 +01:00
Girish Ramakrishnan c0faae4e27 Add more changes for 1.8.0 2017-11-11 11:14:42 -08:00
Girish Ramakrishnan a19c566eea Always show info box that displays app version
Fixes #478
2017-11-11 11:09:59 -08:00
Girish Ramakrishnan 3ec806452c Update node to 6.11.5 2017-11-10 19:25:08 -08:00
Girish Ramakrishnan 0c73cd5219 Update docker to 17.09 2017-11-10 18:49:28 -08:00
Girish Ramakrishnan 9b6bf719ff 1.7.8 changes 2017-11-09 09:40:26 -08:00
Girish Ramakrishnan 25431d3cc4 Fix the spacing 2017-11-09 09:29:42 -08:00
Girish Ramakrishnan e0805df3b1 Only show backup warning if using default location 2017-11-09 09:09:39 -08:00
Girish Ramakrishnan 8392fec570 Remove the bold 2017-11-08 20:57:40 -08:00
Girish Ramakrishnan 1c173ca83f Add UI to select users for access restriction 2017-11-08 20:54:38 -08:00
Girish Ramakrishnan 05a67db761 backup must be stored in ext4
Other file systems like FAT/CIFS can error with cryptic error messages
when saving filenames with special characters such as ':'
2017-11-08 12:26:25 -08:00
Girish Ramakrishnan bb24d5cf9e Order eventlog entries by time 2017-11-08 09:14:55 -08:00
Girish Ramakrishnan 8d2fbe931f Bump max limit to two times ram
part of #466
2017-11-07 10:07:05 -08:00
Girish Ramakrishnan 0a8adaac9f filter out empty usernames from groups
Fixes #472
2017-11-06 11:09:40 -08:00
Girish Ramakrishnan fa6d151325 Fix update mail templates 2017-11-02 21:34:03 -07:00
Girish Ramakrishnan a7296a0339 Rename filename to backupId in backup eventlog 2017-11-02 18:17:08 -07:00
Girish Ramakrishnan a6aee53ec2 Filter out failed backups 2017-11-02 18:13:51 -07:00
Girish Ramakrishnan 963ab2e791 More 1.7.7 changes 2017-11-02 16:30:13 -07:00
Girish Ramakrishnan ca724b8b03 Add cert renewal and user add/remove in weekly digest 2017-11-02 16:30:10 -07:00
Girish Ramakrishnan 88a929c85e Instead of appstore account, include owner alternate email 2017-11-02 15:10:05 -07:00
Girish Ramakrishnan 2bc0270880 1.7.7 changes 2017-11-02 12:18:51 -07:00
Girish Ramakrishnan 014b77b7aa Fix LE cert renewal failures
LE contacts the server by hostname and not by IP. This means that
when installing and reconfiguring the app it hits the default_server
route since nginx configs for the app are not generated at.

When doing in the daily cert renew, the nginx configs exist and we
are unable to renew the certs.
2017-11-02 11:43:43 -07:00
Girish Ramakrishnan 06f8aa8f29 Remove dead code
getNonApprovedCode code flow is ununsed (and broken by design on
the appstore side).
2017-11-02 10:36:30 -07:00
Girish Ramakrishnan a8c64bf9f7 Clarify heartbeat code
heartbeats are not sent for self-hosted cloudrons (only managed ones)
2017-11-02 10:26:21 -07:00
Girish Ramakrishnan 41ef16fbec link to memory limit docs 2017-11-01 09:25:05 -07:00
Girish Ramakrishnan 2a848a481b Add newline 2017-11-01 09:25:05 -07:00
Johannes Zellner 3963d76a80 The update dialog does not contain a form anymore
Fixes #467
2017-11-01 11:55:06 +01:00
Girish Ramakrishnan 8ede37a43d Make the dkim selector dynamic
it has to change with the adminLocation so that multiple cloudrons
can send out emails at the same time.
2017-10-31 12:18:40 -07:00
Girish Ramakrishnan 36534f6bb2 Fix indent 2017-10-31 12:12:02 -07:00
Girish Ramakrishnan 7eddcaf708 Allow setting app memory till memory limit
Fixes #466
2017-10-31 12:12:02 -07:00
Girish Ramakrishnan d8d2572aa1 Keep restarting mysql until it succeeds
MySQL restarts randomly fail on our CI systems. This is easily
reproducible:

root@smartserver:~# cp /tmp/mysql.cnf . && systemctl restart mysql && echo "Yes"
Yes
root@smartserver:~# cp /tmp/mysql.cnf . && systemctl restart mysql && echo "Yes"
Yes
root@smartserver:~# cp /tmp/mysql.cnf . && systemctl restart mysql && echo "Yes"
Job for mysql.service failed. See "systemctl status mysql.service" and "journalctl -xe" for details.

There also seems some apparmor issue:
[ 7389.111704] audit: type=1400 audit(1509404778.110:829): apparmor="DENIED" operation="open" profile="/usr/sbin/mysqld" name="/sys/devices/system/node/" pid=15618 comm="mysqld" requested_mask="r" denied_mask="r" fsuid=112 ouid=0

The apparmor issue is reported in https://bugs.launchpad.net/ubuntu/+source/mysql-5.7/+bug/1610765,
https://bugs.launchpad.net/ubuntu/+source/mysql-5.7/+bug/1658233 and
https://bugs.launchpad.net/ubuntu/+source/apparmor/+bug/1658239
2017-10-30 16:14:20 -07:00
Girish Ramakrishnan 96a98a74ac Move the mysql block
The e2e is failing sporadically with:

==> Changing ownership
==> Adding automated configs
mysql: [Warning] Using a password on the command line interface can be insecure.
ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2)

Maybe the dhparam creation is doing something causing mysql to not respond.
2017-10-30 08:03:47 -07:00
Girish Ramakrishnan d0a244e392 stash adminLocation also 2017-10-29 19:09:03 -07:00
Girish Ramakrishnan f09c89e33f Remove confusing batchSize logic from listDir
This also fixes a bug in removeDir in DO spaces

thanks to @syn for reporting
2017-10-29 19:04:10 -07:00
Johannes Zellner d53f0679e5 Also stash the zoneName to settings 2017-10-29 22:40:15 +01:00
Girish Ramakrishnan 527093ebcb Stash the fqdn in the db for the next multi-domain release 2017-10-29 12:08:27 -07:00
Girish Ramakrishnan bd5835b866 send adminFqdn as well 2017-10-29 09:36:51 -07:00
Girish Ramakrishnan 6dd70c0ef2 acme challenges must be answered by default_server
The challenge must be answered even before app nginx config
is available.
2017-10-28 23:39:03 -07:00
Girish Ramakrishnan acc90e16d7 1.7.6 changes 2017-10-28 21:07:44 -07:00
Girish Ramakrishnan 4b3aca7413 Bump mail container for sogo disconnect fix 2017-10-28 20:58:26 -07:00
Johannes Zellner 8daee764d2 Only require gcdns form input to be valid if that provider is selected 2017-10-28 22:37:56 +02:00
Girish Ramakrishnan 3dedda32d4 Configure http server to only listen on known vhosts/IP
For the rest it returns 404

Fixes #446
2017-10-27 00:10:50 -07:00
Girish Ramakrishnan d127b25f0f Only set the custom https agent for HTTPS minio
Otherwise, we get a Cannot set property ‘agent’ of undefined error
2017-10-26 18:38:45 -07:00
Johannes Zellner 6a2b0eedb3 Add ldap pagination support 2017-10-27 01:25:07 +02:00
Girish Ramakrishnan 8c81a97a4b Check that the backup location has perms to create a directory
The backup itself runs as root and this works fine. But when rotating
the backup, the copy fails because it is unable to create a directory.
2017-10-26 11:41:34 -07:00
Girish Ramakrishnan d9ab1a78d5 Make the my location customizable
Fixes #22
2017-10-25 23:00:43 -07:00
Girish Ramakrishnan 593df8ed49 Do not use ADMIN_LOCATION in tests 2017-10-25 21:38:11 -07:00
Girish Ramakrishnan b30def3620 move prerelease check to appstore 2017-10-25 21:34:56 -07:00
Johannes Zellner 9c02785d49 Support ldap group compare
Fixes #463
2017-10-24 02:00:00 +02:00
Johannes Zellner f747343159 Cleanup unused port bindings after an update 2017-10-23 22:11:33 +02:00
Johannes Zellner 2971910ccf Do not accept port bindings on update route 2017-10-23 22:06:28 +02:00
Johannes Zellner 56534b9647 Add appdb.delPortBinding() 2017-10-23 22:05:43 +02:00
Johannes Zellner a8d26067ee Allow autoupdates if new ports are added
Those will simply be disabled after update and the user has to
enable them through the app configuration
2017-10-20 22:27:48 +02:00
Johannes Zellner 4212e4bb00 Do not show any port binding update ui 2017-10-20 22:27:48 +02:00
Johannes Zellner 7b27ace7bf Update cloudron-setup help url 2017-10-20 22:13:54 +02:00
Girish Ramakrishnan d8944da68d 1.7.5 changes 2017-10-19 12:19:10 -07:00
Girish Ramakrishnan 433d797cb7 Add SMTPS port for apps that require TLS connections for mail relay 2017-10-19 12:15:28 -07:00
Girish Ramakrishnan 0b1d940128 cloudscale -> cloudscale.ch 2017-10-19 07:28:07 -07:00
Johannes Zellner 6016024026 Move restore functions into appropriate scope object 2017-10-18 00:40:02 +02:00
Johannes Zellner e199293229 Further reduce ui flickering on restore 2017-10-18 00:40:02 +02:00
Girish Ramakrishnan 2ebe92fec3 Do not chown mail directory 2017-10-16 23:18:37 -07:00
Girish Ramakrishnan 628cf1e3de bump mail container for superfluous sa-update supervisor file 2017-10-16 21:16:58 -07:00
Girish Ramakrishnan 9e9aaf68f0 No need to migrate mail data anymore 2017-10-16 21:13:57 -07:00
Girish Ramakrishnan b595ca422c 1.7.4 changes 2017-10-16 15:28:36 -07:00
Girish Ramakrishnan 9273a6c726 Add option to disable hardlinks
We can probably remove this later based on the use
2017-10-16 15:22:40 -07:00
Johannes Zellner 76d00d4e65 Render changelog markdown as html in app update dialog 2017-10-17 00:07:58 +02:00
Johannes Zellner 668c03a11b Give visual feedback in the restore dialog when fetching backups 2017-10-16 22:31:49 +02:00
Girish Ramakrishnan 1e72d2d651 remove debugs (too noisy) 2017-10-16 12:34:09 -07:00
Girish Ramakrishnan 89fc8efc67 Save as empty array if find output is empty 2017-10-16 10:54:48 -07:00
Girish Ramakrishnan 241dbf160e Remove Unused required 2017-10-15 14:07:03 -07:00
Girish Ramakrishnan e46bdc2caa Force the copy just like tar --overwrite 2017-10-13 23:23:36 -07:00
Girish Ramakrishnan e1cb91ca76 bump mail container 2017-10-13 22:36:54 -07:00
Girish Ramakrishnan 709c742c46 Fix tests 2017-10-12 21:14:13 -07:00
Girish Ramakrishnan ecad9c499c Port binding conflict can never happen in update route 2017-10-12 21:04:38 -07:00
Girish Ramakrishnan ed0879ffcd Stop the app only after the backup completed
App backup can take a long time or possibly not work at all. For such
cases, do not stop the app or leave it in some errored state.

newConfigJson is the new config to be updated to. This ensures that
the db has correct app info during the update.
2017-10-12 18:10:41 -07:00
Girish Ramakrishnan 61e2878b08 save/restore exec bit in files
this covers the case where user might stash some executable files
that are used by plugins.
2017-10-12 16:18:11 -07:00
Girish Ramakrishnan d97034bfb2 Follow backup format for box backups as well 2017-10-12 11:02:52 -07:00
Girish Ramakrishnan 21942552d6 Clarify the per-app backup flag 2017-10-12 11:02:52 -07:00
Girish Ramakrishnan dd68c8f91f Various backup fixes 2017-10-12 11:02:48 -07:00
Girish Ramakrishnan 28ce5f41e3 handle errors in log stream 2017-10-11 12:55:56 -07:00
Girish Ramakrishnan 5694e676bd Set default rentention to a week 2017-10-11 12:55:55 -07:00
Girish Ramakrishnan db8c5a116f Typo 2017-10-11 10:30:03 -07:00
Girish Ramakrishnan fa39f0fbf3 Add 1.7.3 changes 2017-10-11 00:50:41 -07:00
Girish Ramakrishnan 1444bb038f only upload needs to be retried
copy/delete are already retried in the sdk code
2017-10-11 00:08:41 -07:00
Girish Ramakrishnan ac9e421ecf improved backup progress and logging 2017-10-10 22:49:38 -07:00
Girish Ramakrishnan b60cbe5a55 move constant 2017-10-10 19:47:21 -07:00
Girish Ramakrishnan 56d794745b Sprinkle retries in syncer logic 2017-10-10 14:25:03 -07:00
Girish Ramakrishnan fd3b73bea2 typo in format name 2017-10-10 13:54:54 -07:00
Girish Ramakrishnan 78807782df Various hacks for exoscale-sos
SOS does not like multipart uploads. They just fail randomly.

As a fix, we try to detect filesystem files and skip multipart uploads
for files < 5GB. For > 5GB, we do multipart upload anyways (mostly fails).

The box backup is switched to flat-file for exoscale for the reason
above.
2017-10-10 11:03:20 -07:00
Girish Ramakrishnan 754b29b263 Start out empty if the previous run errored 2017-10-09 20:12:21 -07:00
Girish Ramakrishnan 9f97f48634 Add note on s3ForcePathStyle 2017-10-09 18:46:14 -07:00
Girish Ramakrishnan 815e5d9d9a graphs: Compute width of system graph from total memory
Fixes #452
2017-10-09 14:58:32 -07:00
Girish Ramakrishnan 91ec2eaaf5 sos: "/" must separate bucket and key name 2017-10-09 11:50:22 -07:00
Girish Ramakrishnan f8d3a7cadd Bump mail container (fixes spam crash) 2017-10-06 16:45:21 -07:00
Girish Ramakrishnan d04a09b015 Add note on bumping major infra version 2017-10-06 15:52:04 -07:00
Girish Ramakrishnan 5d997bcc89 Just mark DO Spaces as experimental instead 2017-10-06 14:45:14 -07:00
Girish Ramakrishnan f0dd90a1f5 listObjectsV2 does not work on some S3 providers
specifically, cloudscale does not support it
2017-10-05 12:07:14 -07:00
Girish Ramakrishnan ee8ee8e786 KeyCount is not set on some S3 providers 2017-10-05 11:36:54 -07:00
Girish Ramakrishnan ee1a4411f8 Do not crash if prefix is empty string
('' || undefined) will return undefined ...
2017-10-05 11:08:01 -07:00
Girish Ramakrishnan df6e6cb071 Allow s3 backend to accept self-signed certs
Fixes #316
2017-10-05 10:14:55 -07:00
Girish Ramakrishnan ba5645a20e Disable DO spaces since it is not yet production ready 2017-10-05 09:21:26 -07:00
Girish Ramakrishnan ca502a2d55 Display error code 2017-10-04 22:34:44 -07:00
Girish Ramakrishnan ecd53b48db Display the backup format 2017-10-04 22:11:11 -07:00
Girish Ramakrishnan b9efb0b50b Fix callback invokation 2017-10-04 19:28:40 -07:00
Johannes Zellner 3fb5034ebd Ensure we setup the correct OAuth redirectURI if altDomain is used 2017-10-05 01:10:25 +02:00
Girish Ramakrishnan afed3f3725 Remove duplicate debug 2017-10-04 15:08:26 -07:00
Girish Ramakrishnan b4f14575d7 Add 1.7.1 changes 2017-10-04 14:31:41 -07:00
Johannes Zellner f437a1f48c Only allow dns setup with subdomain if enterprise query argument is provided 2017-10-04 22:25:14 +02:00
Girish Ramakrishnan c3d7d867be Do not set logCallback 2017-10-04 12:32:12 -07:00
Girish Ramakrishnan 96c16cd5d2 remove debug 2017-10-04 11:54:17 -07:00
Girish Ramakrishnan af182e3df6 caas: cache the creds, otherwise we bombard the server 2017-10-04 11:49:38 -07:00
Girish Ramakrishnan d70ff7cd5b Make copy() return event emitter
This way the storage logic does not need to rely on progress
2017-10-04 11:02:50 -07:00
Johannes Zellner 38331e71e2 Ensure all S3 CopySource properties are URI encoded 2017-10-04 19:07:08 +02:00
Johannes Zellner 322a9a18d7 Use multipart copy for s3 and files larger than 5GB 2017-10-04 18:56:23 +02:00
Johannes Zellner 423ef546a9 Merge branch 'user_agent' into 'master'
Added user agent to health checks

See merge request !19
2017-10-04 11:48:02 +00:00
Dennis Schwerdel e3f3241966 Added user agent to health checks 2017-10-04 13:05:00 +02:00
Johannes Zellner eaef384ea5 Improve the invite link display
Fixes #445
2017-10-04 13:03:32 +02:00
Girish Ramakrishnan b85bc3aa01 s3: Must encode copySource
https://github.com/aws/aws-sdk-js/issues/1302
2017-10-03 15:51:05 -07:00
Girish Ramakrishnan 01154d0ae6 s3: better error messages 2017-10-03 14:46:59 -07:00
Girish Ramakrishnan 6494050d66 Make removeDir less noisy 2017-10-03 01:22:37 -07:00
Girish Ramakrishnan 8c7223ceed Fix cleanup logic to use the app backup format
box backup and app backup can have different format
2017-10-03 00:56:34 -07:00
Girish Ramakrishnan 21afc71d89 add tests for storage backends 2017-10-02 23:08:16 -07:00
Girish Ramakrishnan 7bf70956a1 fix tests 2017-10-02 18:42:13 -07:00
Girish Ramakrishnan 9e9b8b095e Provider dhparams.pem to the mail container 2017-10-02 01:51:28 -07:00
Girish Ramakrishnan 0f543e6703 s3: add progress detail
this is a bit of a hack and we should add another way to set the progress
(maybe via backups.setProgress or via a progress callback). this is because
some methods like removeDir can be called from backuptask and from box code.
2017-10-01 18:25:51 -07:00
Girish Ramakrishnan f9973e765c Add backup cleanup eventlog 2017-10-01 10:35:50 -07:00
Girish Ramakrishnan e089851ae9 add debugs 2017-09-30 20:36:08 -07:00
Girish Ramakrishnan c524d68c2f fix crash when cleaning up snapshots 2017-09-30 20:31:41 -07:00
Girish Ramakrishnan 5cccb50a31 fix backup cleanup logic 2017-09-30 18:38:45 -07:00
Girish Ramakrishnan 3d375b687a style: Fix quoting 2017-09-30 18:26:38 -07:00
Girish Ramakrishnan a93d453963 rename flat-file to rsync
not a name I like but cannot come up with anything better

https://en.wikipedia.org/wiki/Flat_file_database

the term 'rsync format' seems to be used in a few places
2017-09-30 14:19:19 -07:00
Girish Ramakrishnan f8ac2d4628 1.7.0 changes 2017-09-30 14:02:06 -07:00
Girish Ramakrishnan d5ba73716b add emptydirs test 2017-09-29 15:29:22 -07:00
Girish Ramakrishnan 954224dafb make syncer track directories 2017-09-29 15:29:18 -07:00
Johannes Zellner 8b341e2bf8 Only make nginx listen on ipv6 connections if it is supported by the system
Could not decide on the ejs formatting, never nice for me
2017-09-29 19:43:37 +02:00
Johannes Zellner 78fb9401ee Add config.hasIPv6() 2017-09-29 19:43:37 +02:00
Girish Ramakrishnan 4a5cbab194 Do not remove parent directory in fs.remove()
Do the pruning in the cleanup logic instead
2017-09-28 20:55:45 -07:00
Girish Ramakrishnan 19999abc50 s3: fix restore 2017-09-28 14:35:49 -07:00
Girish Ramakrishnan 5123b669d7 remove options.concurrency 2017-09-28 12:20:15 -07:00
Girish Ramakrishnan 565c8445e1 make backup progress work for per-app backups 2017-09-28 11:17:48 -07:00
Girish Ramakrishnan 404a019c56 s3: Check IsTruncated before accessing Contents 2017-09-28 10:36:56 -07:00
Girish Ramakrishnan 24dee80aa6 Make box backups always tarball based
this makes cloudron easy to restore. in the future, if required,
we can move out the mail data as a separate virtual app backup
2017-09-28 10:22:10 -07:00
Girish Ramakrishnan ce6df4bf96 Disable encryption for flat-file for now 2017-09-28 09:47:18 -07:00
Girish Ramakrishnan f8f6c7d93e Add progress detail when rotating snapshots 2017-09-28 09:29:46 -07:00
Girish Ramakrishnan bafc6dce98 s3: refactor out directory listing 2017-09-27 21:59:51 -07:00
Girish Ramakrishnan 56ee4d8e25 Remove old cache files when backup settings is changed 2017-09-27 21:04:46 -07:00
Girish Ramakrishnan eeef221b4e Fix race where pipe finishes before file is created
When there are 0 length files, this is easily reproducible.
2017-09-27 19:40:26 -07:00
Girish Ramakrishnan 4674653982 compare size and inode as well 2017-09-27 19:39:03 -07:00
Girish Ramakrishnan a34180c27b Add format to backupsdb
Call remove/removeDir based on the format
2017-09-27 18:02:30 -07:00
Girish Ramakrishnan aa8ce2c62e Use graphite 0.12.0
this fixes an issue where carbon does not startup properly
if a previous pid file was present
2017-09-27 15:35:55 -07:00
Girish Ramakrishnan b3c6b8aa15 do not spawn process just for chown 2017-09-27 15:07:19 -07:00
Girish Ramakrishnan 44a7a2579c rework backup status
* show backup progress even if not initiated by UI
* display backup progress in separate line
2017-09-27 15:07:15 -07:00
Girish Ramakrishnan 39f0e476f2 Start out empty if cache file is missing 2017-09-27 12:09:19 -07:00
Girish Ramakrishnan 003dc0dbaf Add todo 2017-09-27 11:50:49 -07:00
Girish Ramakrishnan e39329218d Make tests work 2017-09-27 11:38:43 -07:00
Girish Ramakrishnan 8d3fbc5432 Save backup logs and fix backup progress 2017-09-26 21:09:00 -07:00
Girish Ramakrishnan 2780de631e writable streams emit finish 2017-09-26 16:43:51 -07:00
Girish Ramakrishnan 399c756735 use exec so that filenames do not have to be escaped 2017-09-26 15:53:42 -07:00
Girish Ramakrishnan 859311f9e5 Process delete commands before add commands
This is required for cases where a dir becomes a file (or vice-versa)
2017-09-26 15:33:54 -07:00
Girish Ramakrishnan a9e89b57d9 merge caas storage into s3 backend 2017-09-26 12:28:33 -07:00
Girish Ramakrishnan 4e68abe51d Handle fs errors 2017-09-26 12:10:58 -07:00
Girish Ramakrishnan 12083f5608 Ignore all special files 2017-09-26 11:41:01 -07:00
Girish Ramakrishnan d1efb2db56 remove bogus mkdir 2017-09-26 11:34:24 -07:00
Girish Ramakrishnan adde28523f Add backup format to the backup UI 2017-09-26 10:46:02 -07:00
Girish Ramakrishnan f122f46fe2 Generate new index file by appending to file 2017-09-26 07:57:20 -07:00
Girish Ramakrishnan ad7fadb4a9 display backup id in the ui 2017-09-26 07:45:23 -07:00
Johannes Zellner be383582e0 Do not rely on external resource in the appstatus page 2017-09-26 15:33:05 +02:00
Girish Ramakrishnan 0a60365143 Initial version of flat-file uploader 2017-09-26 00:17:11 -07:00
Girish Ramakrishnan 2f6cb3e913 set format in the backup ui 2017-09-26 00:01:36 -07:00
Girish Ramakrishnan b0f85678d4 Implement downloadDir for flat-file format 2017-09-23 18:07:26 -07:00
Girish Ramakrishnan e43413e063 implement remove dir in storage backends 2017-09-23 12:34:51 -07:00
Girish Ramakrishnan e39a5c8872 preserve env in backuptask.js 2017-09-22 11:19:44 -07:00
Girish Ramakrishnan fb4b75dd2a Fix typo in comment 2017-09-22 11:19:37 -07:00
Girish Ramakrishnan 3c1ccc5cf4 Add exoscale provider 2017-09-21 17:50:03 -07:00
Girish Ramakrishnan abd66d6524 Add cloudscale as a provider 2017-09-21 17:49:26 -07:00
Girish Ramakrishnan b61b7f80b5 Add DO spaces 2017-09-21 12:25:39 -07:00
Girish Ramakrishnan efa850614d Add a s3-v4-compat provider 2017-09-21 12:13:45 -07:00
Girish Ramakrishnan 21c534c806 Ensure format is set in backupConfig 2017-09-21 09:49:55 -07:00
Girish Ramakrishnan 7e4ff2440c Fix text for manual DNS 2017-09-21 09:10:12 -07:00
Johannes Zellner f415e19f6f Do not unneccesarily mention error in the logs
Not so friendly for log searches
2017-09-21 15:00:35 +02:00
Girish Ramakrishnan 97da8717ca Refactor backup strategy logic into backups.js 2017-09-20 14:09:55 -07:00
Girish Ramakrishnan cbddb79d15 Resolve the id in rotateAppBackup 2017-09-20 09:38:55 -07:00
Johannes Zellner bffb935f0f Also send digest to appstore account owner 2017-09-20 16:33:25 +02:00
Johannes Zellner e50e0f730b Make nginx listen on :: for ipv6 2017-09-20 16:33:25 +02:00
Girish Ramakrishnan 26f33a8e9b Send resolved path to the storage APIs 2017-09-19 21:58:35 -07:00
Girish Ramakrishnan 952b1f6304 Make backuptask call back into backups.js 2017-09-19 20:27:49 -07:00
Girish Ramakrishnan a3293c4c35 Fix tests 2017-09-19 12:43:13 -07:00
Girish Ramakrishnan 4892473eff backupIds do not have extension anymore
this code existed for legacy reasons
2017-09-19 12:34:09 -07:00
Girish Ramakrishnan 221d5f95e1 ensure backupFolder is always set 2017-09-19 12:34:09 -07:00
Girish Ramakrishnan 84649b9471 Bring back backuptask
This is required for various small reasons:

* dir iteration with a way to pass messagein back to the upload() easily
* can be killed independently of box code
* allows us to run sync (blocking) commands in the upload logic
2017-09-19 12:32:38 -07:00
Girish Ramakrishnan 44435559ab Typo 2017-09-19 10:37:45 -07:00
Girish Ramakrishnan c351660a9a Implement backup rotation
Always upload to 'snapshot' dir and then rotate it. This will allow
us to keep pushing incrementally to 'snapshot' and do server side
rotations.
2017-09-18 21:17:34 -07:00
Girish Ramakrishnan 0a24130fd4 Just reset config instead of clearing cache 2017-09-18 19:41:15 -07:00
Girish Ramakrishnan ea13f8f97e Fix checkInstall script 2017-09-18 18:19:27 -07:00
Johannes Zellner d00801d020 Only require service account key for google dns on setup 2017-09-18 23:50:34 +02:00
Girish Ramakrishnan 8ced0aa78e copy: use hardlinks to preserve space 2017-09-18 14:29:48 -07:00
Girish Ramakrishnan f5d32a9178 copyBackup -> copy 2017-09-18 14:29:15 -07:00
Girish Ramakrishnan 7fc45b3215 Refactor out the backup snapshot logic 2017-09-18 12:43:11 -07:00
Girish Ramakrishnan 9bed14a3e8 Enable IP6 in unbound
On some provider (https://www.nine.ch) disabling IPv6 makes unbound
not respond to the DNS queries.

Also, I was unable to test with prefer-ip6 to 'no' because unbound fails:

unbound[5657]: /etc/unbound/unbound.conf.d/cloudron-network.conf:8: error: unknown keyword 'no'
unbound[5657]: read /etc/unbound/unbound.conf failed: 3 errors in configuration file
2017-09-18 11:41:02 -07:00
Girish Ramakrishnan 71233ecd95 Fix undefined variable 2017-09-18 11:14:04 -07:00
Girish Ramakrishnan 02097298c6 Fix indentation 2017-09-18 10:38:30 -07:00
Girish Ramakrishnan be03dd0821 remove unused require 2017-09-18 10:38:26 -07:00
Girish Ramakrishnan 5b77d2f0cf Add commented out debugging section for unbound 2017-09-18 10:38:22 -07:00
Girish Ramakrishnan 781f543e87 Rename API calls in the storage backend 2017-09-17 18:50:29 -07:00
Girish Ramakrishnan 6525a467a2 Rework backuptask into tar.js
This makes it easy to integrate another backup strategy
as the next step
2017-09-17 18:50:26 -07:00
Girish Ramakrishnan 6cddd61a24 Fix style 2017-09-17 18:50:23 -07:00
Girish Ramakrishnan b0ee116004 targz: make sourceDir a string 2017-09-17 18:50:15 -07:00
Girish Ramakrishnan 867a59d5d8 Pull it all to left 2017-09-15 15:47:37 -07:00
Girish Ramakrishnan 6f5085ebc3 Downcase email 2017-09-15 15:45:26 -07:00
Johannes Zellner e8a93dcb1b Add button to send test email
Fixes #419
2017-09-15 14:42:12 +02:00
Girish Ramakrishnan 09fe957cc7 style 2017-09-15 02:07:06 -07:00
Girish Ramakrishnan 020ccc8a99 gcdns: fix update/del confusion
in the DNS api, we always update/del all records of same type
2017-09-15 01:54:39 -07:00
Girish Ramakrishnan 7ed304bed8 Fix cloudflare domain display 2017-09-15 00:50:29 -07:00
Girish Ramakrishnan db1e39be11 Do not overwrite subdomain when location was changed
* Install in subdomain 'test'
* Move to subdomain 'test2'
* Move to another existing subdomain 'www' (this should be detected as conflict)
* Move to subdomain 'www2' (this should not remove 'www'). This is why dnsRecordId exists.
2017-09-14 22:31:48 -07:00
Girish Ramakrishnan f163577264 Typo 2017-09-14 18:38:48 -07:00
Girish Ramakrishnan 9c7080aea1 Show email text for gcdns 2017-09-14 18:33:07 -07:00
Girish Ramakrishnan c05a7c188f Coding style fixes 2017-09-14 18:15:59 -07:00
Girish Ramakrishnan 72e912770a translate network errors to SubdomainError
fixes #391
2017-09-14 16:14:16 -07:00
Girish Ramakrishnan 28c06d0a72 bump mail container 2017-09-14 12:07:53 -07:00
Girish Ramakrishnan 9805daa835 Add google-cloud/dns to shrinkwrap 2017-09-14 10:45:04 -07:00
Girish Ramakrishnan a920fd011c Merge branch 'feature/gcdns' into 'master'
Adding Google Cloud DNS support

See merge request !17
2017-09-14 17:44:20 +00:00
Girish Ramakrishnan 1b979ee1e9 Send rbl status as part of email check 2017-09-13 23:58:54 -07:00
Girish Ramakrishnan 70eae477dc Fix logstream test 2017-09-13 23:01:04 -07:00
Girish Ramakrishnan c16f7c7891 Fix storage tests 2017-09-13 22:50:38 -07:00
Girish Ramakrishnan 63b8a5b658 Add update pattern of wednesday night
Fixes #432, #435
2017-09-13 14:52:31 -07:00
Aleksandr Bogdanov c0bf51b79f A bit more polish 2017-09-13 21:17:40 +02:00
Aleksandr Bogdanov 3d4178b35c Adding Google Cloud DNS to "setupdns" stage 2017-09-13 21:00:29 +02:00
Aleksandr Bogdanov 34878bbc6a Make sure we don't touch records which are not managed by cloudron, but are in the same zone 2017-09-13 20:53:38 +02:00
Girish Ramakrishnan e78d976c8f Fix backup mapping (mail dir has moved) 2017-09-13 09:51:20 -07:00
Girish Ramakrishnan ba9662f3fa Add 1.6.5 changes 2017-09-12 22:32:57 -07:00
Girish Ramakrishnan c8750a3bed merge the logrotate scripts 2017-09-12 22:03:24 -07:00
Girish Ramakrishnan 9710f74250 remove collectd stats when app is uninstalled 2017-09-12 21:34:15 -07:00
Girish Ramakrishnan 52095cb8ab add debugs for timing backup and restore 2017-09-12 15:37:35 -07:00
Aleksandr Bogdanov c612966b41 Better validation 2017-09-12 22:47:46 +02:00
Aleksandr Bogdanov 90cf4f0784 Allowing to select a service account key as a file for gcdns 2017-09-12 22:35:40 +02:00
Aleksandr Bogdanov ec93d564e9 Adding Google Cloud DNS to webadmin 2017-09-12 19:03:23 +02:00
Aleksandr Bogdanov 37f9e60978 Fixing verifyDns 2017-09-12 16:29:07 +02:00
Johannes Zellner ca199961d5 Make settings.value field TEXT
We already store JSON blobs there and the gce dns backend
will require a larger blob for a certificate.
Since we use innodb the storage format in TEXT will only be different
if the data is large
2017-09-11 15:41:07 +02:00
Girish Ramakrishnan fd811ac334 Remove "cloudron" to fit in one line 2017-09-10 17:43:21 -07:00
Girish Ramakrishnan 609c1d3b78 bump mail container
this is also required since we moved the maildir
2017-09-10 00:07:48 -07:00
Girish Ramakrishnan 9906ed37ae Move mail data inside boxdata directory
This also makes the noop backend more useful because it will dump things
in data directory and user can back it up as they see fit.
2017-09-10 00:07:44 -07:00
Girish Ramakrishnan dcdce6d995 Use MAIL_DATA_DIR constant 2017-09-09 22:24:16 -07:00
Girish Ramakrishnan 9026c555f9 snapshots dir is not used anymore 2017-09-09 22:13:15 -07:00
Girish Ramakrishnan 547a80f17b make shell.exec options non-optional 2017-09-09 19:54:31 -07:00
Girish Ramakrishnan 300d3dd545 remove unused requires 2017-09-09 19:23:22 -07:00
Aleksandr Bogdanov 6fce729ed2 Adding Google Cloud DNS 2017-09-09 17:45:26 +02:00
Girish Ramakrishnan d233ee2a83 ask password only for destructive actions 2017-09-08 15:14:37 -07:00
Girish Ramakrishnan 3240a71feb wording 2017-09-08 14:42:54 -07:00
Girish Ramakrishnan 322be9e5ba Add ip blacklist check
Fixes #431
2017-09-08 13:29:32 -07:00
Girish Ramakrishnan e67ecae2d2 typo 2017-09-07 22:01:37 -07:00
Girish Ramakrishnan 75b3e7fc78 resolve symlinks correctly for deletion
part of #394
2017-09-07 21:57:08 -07:00
Girish Ramakrishnan 74c8d8cc6b set label on the redis container
this ensures that redis is stopped when app is stopped and also
helps identifying app related containers easily
2017-09-07 20:09:46 -07:00
Girish Ramakrishnan 51659a8d2d set label on the redis container
this ensures that redis is stopped when app is stopped and also
helps identifying app related containers easily
2017-09-07 19:54:05 -07:00
Girish Ramakrishnan 70acf1a719 Allow app volumes to be symlinked
The initial plan was to make app volumes to be set using a database
field but this makes the app backups non-portable. It also complicates
things wrt to app and server restores.

For now, ignore the problem and let them be symlinked.

Fixes #394
2017-09-07 15:50:34 -07:00
Girish Ramakrishnan 8d2f3b0217 Add note on disabling ssh password auth 2017-09-06 11:36:23 -07:00
Girish Ramakrishnan e498678488 Use node 6.11.3 2017-09-06 09:39:22 -07:00
Girish Ramakrishnan 513517b15e cf dns: filter by type and name in the REST API
Otherwise, we will have to implement pagination
2017-09-05 16:07:14 -07:00
Girish Ramakrishnan a96f8abaca DO DNS: list all pages of the domain 2017-09-05 15:52:59 -07:00
Johannes Zellner f7bcd54ef5 Better ui feedback on the repair mode 2017-09-05 23:11:04 +02:00
Johannes Zellner d58e4f58c7 Add hook to react whenever apps have changed 2017-09-05 23:10:45 +02:00
Girish Ramakrishnan 45f0f2adbe Fix wording 2017-09-05 10:38:33 -07:00
Johannes Zellner 36c72dd935 Sendgrid only has an api key similar postmark
Fixes #411
2017-09-05 11:28:28 +02:00
Girish Ramakrishnan df9e2a7856 Use robotsTxt in install route 2017-09-04 12:59:14 -07:00
Girish Ramakrishnan 2b043aa95f remove unused require 2017-09-04 12:59:05 -07:00
Johannes Zellner c0a09d1494 Add 1.6.4 changes 2017-09-04 18:53:11 +02:00
Johannes Zellner 1c5c4b5705 Improve overflow handling in logs and terminal view 2017-09-04 18:40:16 +02:00
Girish Ramakrishnan b56dcaac68 Only run scheduler when app is healthy
Fixes #393
2017-09-03 18:21:13 -07:00
Girish Ramakrishnan fd91ccc844 Update the unbound anchor key
This helps the unbound recover from any previous out of disk space
situation.

part of #269
2017-09-03 17:48:26 -07:00
Johannes Zellner fca1a70eaa Add initial repair button alongside webterminal
Part of #416
2017-09-01 20:08:22 +02:00
Johannes Zellner ed81b7890c Fixup the test for the password requirement change 2017-09-01 20:08:22 +02:00
Johannes Zellner cb8dcbf3dd Lift the password requirement for app configure/update/restore actions 2017-09-01 20:08:22 +02:00
Johannes Zellner 4bdbf1f62e Fix indentation 2017-09-01 20:08:22 +02:00
Johannes Zellner 47a8b4fdc2 After consuming the accessToken query param, remove it
Fixes #415
2017-09-01 10:25:28 +02:00
Johannes Zellner 5720e90580 Guide the user to use ctrl+v for pasting into webterminal
Fixes #413
2017-08-31 20:52:04 +02:00
Johannes Zellner f98e13d701 Better highlight dropdown menu hovers 2017-08-31 20:52:04 +02:00
Johannes Zellner d5d924861b Fix gravatar margin in navbar 2017-08-31 20:52:04 +02:00
Girish Ramakrishnan b81a92d407 disable ip6 in unbound as well
part of #412
2017-08-31 11:41:35 -07:00
Johannes Zellner 22b0100354 Ensure we don't crash if the terminal socket is not ready yet
Upstream patch submitted https://github.com/sourcelair/xterm.js/pull/933
2017-08-31 20:31:31 +02:00
Johannes Zellner 6eb6eab3f4 Let the browser handle paste keyboard shortcuts
Related to #413
2017-08-31 20:31:31 +02:00
Girish Ramakrishnan 57d5c2cc47 Use IPv4 address to connect to mysql
Fixes #412
2017-08-31 10:59:14 -07:00
Johannes Zellner 6a9eac7a24 Use the correct input change event
Fixes #414
2017-08-31 19:06:02 +02:00
Johannes Zellner e4760a07f0 Give feedback if the relay settings have successfully saved 2017-08-30 11:02:13 +02:00
Johannes Zellner 257e594de0 Allow mail relay provider specific UI
Only contains specific UI for postmark

Part of #411
2017-08-30 10:55:36 +02:00
Girish Ramakrishnan 6fea022a04 remove dead code 2017-08-29 14:47:59 -07:00
Girish Ramakrishnan f34840d127 remove old data migration paths 2017-08-29 13:08:31 -07:00
Girish Ramakrishnan f9706d6a05 Always generate nginx config for webadmin
Part of #406
2017-08-28 21:16:47 -07:00
Girish Ramakrishnan 61f7c1af48 Remove unused error codes 2017-08-28 15:27:17 -07:00
Girish Ramakrishnan 00786dda05 Do not crash if DNS creds do not work during startup
If DNS creds are invalid, then platform.start() keeps crashing on a
mail container update. For now, just log the error and move on.

Part of #406
2017-08-28 14:55:36 -07:00
Girish Ramakrishnan 8b9f44addc 1.6.3 changes 2017-08-28 13:49:15 -07:00
Johannes Zellner 56c7dbb6e4 Do not attempt to reconnect if the debug view is already gone
Fixes #408
2017-08-28 21:06:25 +02:00
Girish Ramakrishnan c47f878203 Set priority for MX records
Fixes #410
2017-08-26 15:54:38 -07:00
Girish Ramakrishnan 8a2107e6eb Show email text for Cloudflare 2017-08-26 15:37:24 -07:00
Girish Ramakrishnan cd9f0f69d8 email dialog has moved to it's own view 2017-08-26 15:36:12 -07:00
Girish Ramakrishnan 1da91b64f6 Filter out possibly sensitive information for normal users
Fixes #407
2017-08-26 14:47:51 -07:00
Johannes Zellner a87dd65c1d Workaround for firefox flexbox bug
Fixes selection while clicking on empty flexbox space.

This only happens in firefox and seems to be a bug in firefox
flexbox implementation, where the first child element with a
non zero size, in a flexbox managed `block` element, has the
`float` property.

Fixes #405
2017-08-24 23:29:42 +02:00
Johannes Zellner 7c63d9e758 Fix typo in css 2017-08-24 23:16:36 +02:00
Girish Ramakrishnan 329bf596ac Indicate that directories can be downloaded 2017-08-24 13:38:50 -07:00
Girish Ramakrishnan 2a57c4269a handle app not found 2017-08-23 13:23:04 -07:00
Girish Ramakrishnan ca8813dce3 1.6.2 changes 2017-08-23 10:43:27 -07:00
Girish Ramakrishnan 3aebf51360 Fix upload of large files to apps
6a0ef7a1c1 broke the upload for apps

e2e test is being added
2017-08-23 10:22:54 -07:00
Johannes Zellner 103f8db8cb Do not expand to fixed pixel size on mobile 2017-08-23 16:57:34 +02:00
Johannes Zellner 04c127b78d Add changes for 1.6.1
Due to regressions we should skip 1.6.0 thus the same changelog
2017-08-23 16:14:30 +02:00
Johannes Zellner 9bef1bcf64 Hijack and demux the container exec stream to be compliant with new
dockerode
2017-08-23 16:04:50 +02:00
Johannes Zellner 718413c089 autocomplete attribute is not respected for username/password fields
Since the cloudflare email input field is above the password field
some browsers will automatically autofill it with the username
as it looks like a login form. So we add a hidden unused input field
which gets autofilled instead :-/
2017-08-23 13:13:00 +02:00
Girish Ramakrishnan a34691df44 Hide the header as well 2017-08-22 09:30:18 -07:00
Girish Ramakrishnan 795e38fe82 file is an object 2017-08-22 09:15:46 -07:00
Johannes Zellner 1d348fb0f3 Do not lose focus on terminal 2017-08-22 16:24:26 +02:00
Johannes Zellner 91f3318879 Implement rightclick menu for terminal text copy 2017-08-22 16:23:06 +02:00
Girish Ramakrishnan c61808f4c6 1.6.0 changes 2017-08-21 16:08:37 -07:00
Girish Ramakrishnan 991b2dad28 bump mail container version
part of #400
2017-08-21 15:54:21 -07:00
Girish Ramakrishnan f3d9a70de7 Only send the stdout stream 2017-08-21 10:46:13 -07:00
Johannes Zellner 60758de10a Fixup package.json linter issues and clean the shrinkwrap 2017-08-21 12:45:15 +02:00
Girish Ramakrishnan 6a0ef7a1c1 Allow larger files to be uploaded
Note that other upload APIs like avatar are still limited to 1m by
the nginx config
2017-08-20 19:15:54 -07:00
Girish Ramakrishnan 7cb451c157 Allow dirs to downloaded as tarballs 2017-08-20 18:54:59 -07:00
Girish Ramakrishnan 3c31c96ad4 Hide the download dialog after download starts 2017-08-20 18:29:11 -07:00
Johannes Zellner 5d73f58631 Show upload progress 2017-08-20 19:32:00 +02:00
Johannes Zellner 4ca7cccdae Give error feedback if the requested file does not exist 2017-08-20 18:50:37 +02:00
Johannes Zellner 82380b6b7c Remove hardcoded /app/data and fix submit for file downloads 2017-08-20 18:09:43 +02:00
Johannes Zellner 979c4e77e3 Fix view bug when terminal reconnects but user has moved on 2017-08-20 18:00:07 +02:00
Johannes Zellner e318fb0c01 Show restat button also in logs view 2017-08-20 17:57:32 +02:00
Girish Ramakrishnan 77d2fb97e5 test: create logrotate dir 2017-08-19 18:57:43 -07:00
Girish Ramakrishnan 24e6c4d963 bump test image 2017-08-19 17:57:21 -07:00
Girish Ramakrishnan 064c5cf7f2 Fix failing test 2017-08-19 17:41:15 -07:00
Girish Ramakrishnan 891542bfb9 move restart button 2017-08-19 17:33:59 -07:00
Girish Ramakrishnan 599702d410 Fix casing 2017-08-19 16:45:20 -07:00
Girish Ramakrishnan 3cb39754fd Make logs button work for apps 2017-08-19 12:52:48 -07:00
Girish Ramakrishnan f04345a99a Move restart button to log view 2017-08-19 12:49:03 -07:00
Johannes Zellner 3d59b8a5b0 Deliver content-length and file not found errors for file downloads 2017-08-19 12:13:04 +02:00
Johannes Zellner cf518b0285 Resize terminal based on initial DOM size
Currently we cannot send new cols,rows on DOM element resize
as they are sent on connect only and a reconnect would loose
current session
2017-08-19 11:32:00 +02:00
Girish Ramakrishnan 52832c881a Add upload and download for the webterminal 2017-08-18 21:19:48 -07:00
Girish Ramakrishnan 537fbff4aa Use ws directly to handle new exec ws route 2017-08-18 19:46:18 -07:00
Johannes Zellner e3040b334d Do not submit injected commands right away but give some space and fix
focus
2017-08-18 20:36:52 +02:00
Johannes Zellner 6c2879d567 Rename debug view to terminal and logs 2017-08-18 20:36:47 +02:00
Johannes Zellner 595c89076f Add postgres, mongo and redis client injection 2017-08-18 11:26:10 -07:00
Johannes Zellner c85f5b15c6 Reenable custom tcp upgrade handling 2017-08-18 11:26:05 -07:00
Johannes Zellner 8fbed7e84b Ensure we only write to the websocket if it is open 2017-08-18 11:26:00 -07:00
Johannes Zellner ee3c5f67af Show mysql addon only if the app uses it 2017-08-18 11:25:54 -07:00
Johannes Zellner 52db28e876 Verify the websocket request 2017-08-18 11:25:49 -07:00
Johannes Zellner 65bc3491f6 enable timeout middleware again and reset it for all upgrade requests 2017-08-18 11:25:45 -07:00
Johannes Zellner 82f512dc27 Rename logs view to debug view 2017-08-18 11:25:37 -07:00
Johannes Zellner 4b41378d08 Ensure app restarts also close the websocket 2017-08-18 11:25:05 -07:00
Johannes Zellner 1fd4e27d92 Fix logs autoscroll 2017-08-18 11:25:00 -07:00
Johannes Zellner 2420fef6b1 Reconnect the terminal on disconnection
This can happen if the app crashes or restarts
2017-08-18 11:24:55 -07:00
Johannes Zellner 50074b936a Integrate the terminal with the logs ui 2017-08-18 11:24:48 -07:00
Johannes Zellner f98e68edc1 Add express-ws node module 2017-08-18 11:24:42 -07:00
Johannes Zellner 83e5daf08c Add xterm.js 2017-08-18 11:24:34 -07:00
Girish Ramakrishnan 53b43ca36b Don't show restore button for noop backend 2017-08-17 20:20:06 -07:00
Girish Ramakrishnan d11842a7f8 Show popup when using noop backend 2017-08-17 19:52:08 -07:00
Girish Ramakrishnan 6746781b46 Add warning for noop backend
Fixes #402
2017-08-17 12:38:52 -07:00
Girish Ramakrishnan 78ec8e5c0c Add field to skip backup for an app
This skips the app from a backup when doing a full box backup and
simply reuses the previous backup.

The app can still be explicitly backed up using 'cloudron backup'
and explicitly restored using 'cloudron restore --backup'.

When restoring the box, it all depends on the app's last backup.

Fixes #311
2017-08-16 16:36:50 -07:00
Johannes Zellner 67a2ba957e Use maxsize logrotate rule instead of size
The current ruleset means rotate the file daily unless the file grows
larger than 1Mb earlier, then rotate once the file reaches that size.

https://serverfault.com/questions/474941/how-to-rotate-log-based-on-an-interval-unless-log-exceeds-a-certain-size
2017-08-16 19:10:49 +02:00
Girish Ramakrishnan 9e558924bb df plugin replaces with _ and not -
Part of #348
2017-08-15 09:32:42 -07:00
Johannes Zellner afcb3dd237 Fix layout issues in oauth views 2017-08-15 13:18:31 +02:00
Johannes Zellner 054de4813d Fix layout issue in update view 2017-08-15 11:04:47 +02:00
Girish Ramakrishnan 57891c64b5 use check_output instead
Aug 14 19:10:46 collectd[12651]: close failed in file object destructor:
Aug 14 19:10:46 collectd[12651]: IOError: [Errno 10] No child processes
2017-08-14 12:31:58 -07:00
Girish Ramakrishnan 26361c037d Merge branch 'mehdi/box-permissions'
Closes MR !14
2017-08-14 10:49:54 -07:00
Girish Ramakrishnan 2048b03431 Removed this file by mistake 2017-08-14 10:44:58 -07:00
Girish Ramakrishnan c12aba6c00 install xfsprogs
on some VPS like scaleway this is not installed.

This is why docker with devicemapper was using ext4 and not devmapper

devmapper: XFS is not supported in your system. Either the kernel doesn't support it or mkfs.xfs is not in your PATH. Defaulting to ext4 filesystem"
2017-08-13 23:15:23 -07:00
Girish Ramakrishnan 0bd0857189 Update many modules
npm WARN deprecated ejs-cli@1.2.0: This has breaking change. (in ejs package) use <= 2.0.0.
npm WARN deprecated node-uuid@1.4.8: Use uuid module instead
npm WARN deprecated minimatch@0.3.0: Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue
npm WARN deprecated minimatch@2.0.10: Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue
npm WARN deprecated minimatch@0.2.14: Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue
npm WARN deprecated graceful-fs@1.2.3: graceful-fs v3.0.0 and before will fail on node releases >= v7.0. Please update to graceful-fs@^4.0.0 as soon as possible. Use 'npm ls graceful-fs' to find it in the tree.
2017-08-13 17:57:48 -07:00
Girish Ramakrishnan 978893250f Update superagent (for doubele callback bug) 2017-08-13 17:38:02 -07:00
mehdi d0f4a76ca2 basic capabilities syntax 2017-08-12 09:42:54 +01:00
Girish Ramakrishnan 755c87b079 Remove extra space 2017-08-11 22:14:53 -07:00
Girish Ramakrishnan 1da073c9bf Display mail server settings inline 2017-08-11 22:12:11 -07:00
Girish Ramakrishnan 96ead77520 Remove the API section 2017-08-11 21:10:18 -07:00
Girish Ramakrishnan 178b04fead Fix broken links to docs 2017-08-11 21:00:51 -07:00
Girish Ramakrishnan 335631ac28 Look for log files in first level and third level as well 2017-08-11 16:24:56 -07:00
Girish Ramakrishnan 42778cb84d Add 1.5.0 changes 2017-08-11 16:24:30 -07:00
Johannes Zellner 2f51088e67 Add logrotate support for *.log files in /run mounts of apps
logrotate config files may contain arbitrary commands which are
exectued as root, thus the config files have to be owned by root.
This is the reason we need the sudo scripts :-/

To test the generated scripts, just run:
$ logrotate /etc/logrotate.conf -v

Fixes #396
2017-08-12 00:04:00 +02:00
Johannes Zellner 378d7aee91 Add docker.inspect() api 2017-08-11 23:31:59 +02:00
Johannes Zellner ac53f8c747 Show app title for log selection 2017-08-11 15:32:58 +02:00
Girish Ramakrishnan 5fe73c5a46 Replace df plugin with custom df plugin
The built-in df plugin cannot do the following:
    * if we choose by type ext4, we want to skip devicemapper (on scaleway)
    * the MountPoint of the appsdata directory is not possible to know at install time

Fixes #398
2017-08-11 01:39:51 -07:00
Johannes Zellner a6f13eee14 Fix further layout issues in the appstore login view 2017-08-11 09:30:07 +02:00
Girish Ramakrishnan 86d23a4d35 Switch default storage backend to overlay2
This does not try to migrate existing cloudrons from devicemapper.
We will possibly do that in a future version.

61e130fb7 takes care of checking that we run on ext4

https://cloudron.io/documentation/server/#using-overlay2-backend-for-docker

Fixes #364
2017-08-10 14:11:03 -07:00
Johannes Zellner b25bb76792 Give some bottom spacing for the log output container 2017-08-10 21:49:27 +02:00
Johannes Zellner 7ba5d1e0d6 Enable automatic scrolling while trailing logs 2017-08-10 21:49:06 +02:00
Girish Ramakrishnan f17bde2d97 Add motd message for cloudron admins about updates
Fixes #351
2017-08-10 12:14:51 -07:00
Girish Ramakrishnan 93cafebfdb Allow options to be set in shell.sudo and shell.exec 2017-08-09 19:32:02 -07:00
Johannes Zellner 5538a91585 We are not using bootstrap rows on toplevel layout anymore 2017-08-09 09:49:52 +02:00
Girish Ramakrishnan 09cb468290 Update node to 6.11.2 2017-08-08 18:41:38 -07:00
Johannes Zellner 3b98eb0543 Add cloudron logstream api test 2017-08-08 20:40:18 +02:00
Johannes Zellner 59936c6fbf Further simplification of the layout code 2017-08-08 18:24:00 +02:00
Johannes Zellner 5a7e636f2d Use flexbox to avoid lots of grid system quirks
Finally we can sanely control navbar and footer
2017-08-08 12:05:46 +02:00
Johannes Zellner 401dc37a50 Ensure we destroy the eventsource when the view changes 2017-08-08 11:52:32 +02:00
Johannes Zellner 9f1af572a0 Make log lines backlog configurable 2017-08-08 10:07:33 +02:00
Johannes Zellner 96e2fa159c Show timestamp in log lines 2017-08-08 09:55:18 +02:00
Johannes Zellner bc49a3e18a Add moment.js to webadmin 2017-08-08 09:55:09 +02:00
Johannes Zellner ae19d8d754 Protect the logs view from normal user access 2017-08-07 21:00:23 +02:00
Johannes Zellner 13b067eb88 Remove log download in support view
This has moved to the logs view
2017-08-07 20:54:18 +02:00
Johannes Zellner af08f4e7b6 Auto scroll the log streamer 2017-08-07 19:43:07 +02:00
Johannes Zellner 534e5781ba Set correct font and fix the follow flag 2017-08-07 18:42:08 +02:00
Johannes Zellner 737e266729 Set the url for the log download button 2017-08-07 18:24:19 +02:00
Johannes Zellner 07a133ebe9 Fix the platform log streaming 2017-08-07 18:18:36 +02:00
Johannes Zellner b0444edf7e Add platform logstream api 2017-08-07 17:09:39 +02:00
Johannes Zellner bcf37d833f Allow to follow app logs 2017-08-07 16:48:27 +02:00
Johannes Zellner e7db2ab137 Add color code to html conversion library 2017-08-07 16:47:47 +02:00
Johannes Zellner 125b416463 Add initial logs view 2017-08-07 13:50:45 +02:00
Girish Ramakrishnan 800468fbe6 Use debug instead 2017-08-04 11:16:00 -07:00
Girish Ramakrishnan 0c1e3ec6a0 use mailFqdn instead of adminFqdn 2017-08-04 07:46:09 -07:00
Johannes Zellner d97ee5d425 Bring back update badge hover 2017-08-04 14:59:06 +02:00
Johannes Zellner a1be30c35a Remove preceeding * or - in changelogs
Should be possibly solved somewhere more central
2017-08-04 12:31:25 +02:00
Johannes Zellner ba3cb3b646 Do not crash the whole cloudron if an email template does not render but report the ejs error 2017-08-04 12:02:50 +02:00
Johannes Zellner daadefe6b9 Ensure we also send the html portion of the digest mail 2017-08-04 12:00:01 +02:00
Girish Ramakrishnan 12c849398e 1.4.1 changes 2017-08-03 13:55:58 -07:00
Girish Ramakrishnan 392492be04 Only collect info on the / mountpoint
The original intention was to collect information on the data
dirs as well but we have long moved away from that design.
On some VPS like scaleway, this ends up collecting info on
devicemapper stuff (which are on ext4, not sure why).

In future, we should collect info of other disks as well (#348)

Fixes #389
2017-08-03 11:45:23 -07:00
Johannes Zellner 4fd0c3c66c Add html version of the digest email 2017-08-03 15:55:36 +02:00
Johannes Zellner 7d2e6d8d4d Add test_data for html email development 2017-08-03 15:55:03 +02:00
Girish Ramakrishnan f3e7249bdc Fix unterminated quote 2017-08-02 14:43:05 -07:00
Johannes Zellner 53afb2606a Change the update notification badge 2017-08-02 21:45:11 +02:00
Girish Ramakrishnan fbce71d031 Do not show the backup region for minio and exoscale
Fixes #392
2017-08-02 11:21:39 -07:00
Johannes Zellner 1adde7d8e8 And another doc links change to fit the new doc layout 2017-08-02 11:30:16 +02:00
Girish Ramakrishnan d23599ba24 Add 1.1.3 2017-08-01 17:21:00 -07:00
Girish Ramakrishnan ac35bcf9f0 add 1.0.x and 1.1.x changelogs 2017-08-01 16:48:51 -07:00
Johannes Zellner e4c5dfda60 Adjust documentation links to fit knowledgebase 2017-08-01 20:38:47 +02:00
Johannes Zellner 99cfe564ae Add repair button to error dialog and improve dns error message 2017-08-01 11:56:39 +02:00
Johannes Zellner 70a3cdc9bc Make it more obvious that the error dialog can be triggered when clicking on the app icon 2017-08-01 11:56:39 +02:00
Girish Ramakrishnan bd52068695 Return error object instead of false 2017-07-31 11:57:23 -07:00
Girish Ramakrishnan ae54b57ca7 Fix wording to match cf website 2017-07-31 10:50:00 -07:00
176 changed files with 19550 additions and 5813 deletions
+220
View File
@@ -888,10 +888,25 @@
[1.0.0]
* Make selfhosting great again
[1.0.1]
* Notification improvements
[1.0.2]
* Notification improvements
[1.1.0]
* Add support for email catch-all
* Support Cloudrons on subdomains
[1.1.1]
* Notification improvements
[1.1.2]
* Notification improvements
[1.1.3]
* Notification improvements
[1.2.0]
* Relay emails optionally via external SMTP server email (mailgun, sendgrid etc)
* (experimental) Preserver the docker storage driver across updates
@@ -918,3 +933,208 @@
* Ensure Cloudron is only be installed on EXT4 root file system (required by Docker)
* Mark app package major releases as blocking and require approval by Cloudron admin
[1.4.1]
* Do not display backup region when using minio and exoscale SOS
* Fix javascript error in email view
* Add html version of the digest email
* Fix issue where collectd was collecting information about devicemapper mounts
[1.5.0]
* Update node to 6.11.2
* Add a new view to display platform and app logs
* Rework web UI to use flexbox
* Add motd message to warn admins that to not run 'apt upgrade'
* Switch default storage backend for new Cloudrons to overlay2
* Add a custom graphite plugin to collect disk usage statistics
* Rotate logs of all apps automatically
[1.6.0]
* Allow apps to have 'network' capability (thanks @mehdi)
* Fix crash in collectd disk usage collection script
* Fix layout issues in update and oauth views
* Use maxsize rule instead of size in lograte configs
* Make it possible to skip backups per-app
* Hide restore button for noop backend
* Add popups and warnings for noop backend
* Add webterminal to shell into apps from the admin UI
* Update Haraka for a few crash fixes
[1.6.1]
* Patch release for 1.6.0 to fix regressions
* Allow apps to have 'network' capability (thanks @mehdi)
* Fix crash in collectd disk usage collection script
* Fix layout issues in update and oauth views
* Use maxsize rule instead of size in lograte configs
* Make it possible to skip backups per-app
* Hide restore button for noop backend
* Add popups and warnings for noop backend
* Add webterminal to shell into apps from the admin UI
* Update Haraka for a few crash fixes
[1.6.2]
* Allow apps to have 'network' capability (thanks @mehdi)
* Fix crash in collectd disk usage collection script
* Fix layout issues in update and oauth views
* Use maxsize rule instead of size in lograte configs
* Make it possible to skip backups per-app
* Hide restore button for noop backend
* Add popups and warnings for noop backend
* Add webterminal to shell into apps from the admin UI
* Update Haraka for a few crash fixes
[1.6.3]
* Fixes selection issue while clicking on empty flexbox space
* Indicate directories can be downloaded in the web terminal
* Do not show app update indicator for normal users
* Display email notice when using Cloudflare DNS
* Set MX records correctly when using Cloudflare DNS
* Fix bug where webterminal can incorrectly appear in main view
* Do not crash if DNS credentials are invalid
[1.6.4]
* More descriptive Postmark email relay form
* Fix file upload in chrome
* Support Ctrl/Cmd+v webterminal pasting
* Ensure unbound always starts up
* Add option to run app in repair mode
[1.6.5]
* DigitalOcean DNS: Add pagination
* Cloudflare DNS: Optimize listing of DNS entries
* Update node to 6.11.3
* App volumes can now be symlinked individually to external storage
* Periodically check if IP is blacklisted and notify admins
* Do not ask password when re-configuring app (since it is non-destructive)
* Move mail data inside boxdata directory. This makes the no-op backend more useful
* Remove collectd stats when app is uninstalled
[1.7.0]
* Add rsync format for backups. This feature allows incremental backups
* Add Google DNS backend (thanks @syn)
* Add DigitalOcean spaces backup storage backend
* Add Cloudscale and Exoscale as supported VPS providers
* Display backup progress and status in the web interface
* Preliminary IPv6 support
* Add IP RBL status to web interface
* Add auto-update pattern `Every wednesday night`
* Update Haraka to 2.8.15. This fixes the issue where emails were bounced with the message 'Send MAIL FROM first'
* Do not overwrite existing subdomain when app's location is changed
* Add button to send test email
* Fix crash in carbon which made graphs disappear on some Cloudrons
[1.7.1]
* Add rsync format for backups. This feature allows incremental backups
* Add Google DNS backend (thanks @syn)
* Add DigitalOcean spaces backup storage backend
* Add Cloudscale and Exoscale as supported VPS providers
* Display backup progress and status in the web interface
* Preliminary IPv6 support
* Add IP RBL status to web interface
* Add auto-update pattern `Every wednesday night`
* Update Haraka to 2.8.15. This fixes the issue where emails were bounced with the message 'Send MAIL FROM first'
* Do not overwrite existing subdomain when app's location is changed
* Add button to send test email
* Fix crash in carbon which made graphs disappear on some Cloudrons
[1.7.2]
* Add rsync format for backups. This feature allows incremental backups
* Add Google DNS backend (thanks @syn)
* Add Cloudscale and Exoscale as supported VPS providers
* Display backup progress and status in the web interface
* Preliminary IPv6 support
* Add IP RBL status to web interface
* Add auto-update pattern `Every wednesday night`
* Update Haraka to 2.8.15. This fixes the issue where emails were bounced with the message 'Send MAIL FROM first'
* Do not overwrite existing subdomain when app's location is changed
* Add button to send test email
* Fix crash in carbon which made graphs disappear on some Cloudrons
* Fix issue where OAuth SSO did not work when alternate domain was used
[1.7.3]
* Add rsync format for backups. This feature allows incremental backups
* Add Google DNS backend (thanks @syn)
* Add Cloudscale and Exoscale as supported VPS providers
* Display backup progress and status in the web interface
* Preliminary IPv6 support
* Add IP RBL status to web interface
* Add auto-update pattern `Every wednesday night`
* Update Haraka to 2.8.15. This fixes the issue where emails were bounced with the message 'Send MAIL FROM first'
* Do not overwrite existing subdomain when app's location is changed
* Add button to send test email
* Fix crash in carbon which made graphs disappear on some Cloudrons
* Fix issue where OAuth SSO did not work when alternate domain was used
[1.7.4]
* Add rsync format for backups. This feature allows incremental backups
* Add Google DNS backend (thanks @syn)
* Add DigitalOcean spaces backup storage backend
* Add Cloudscale and Exoscale as supported VPS providers
* Display backup progress and status in the web interface
* Preliminary IPv6 support
* Add IP RBL status to web interface
* Add auto-update pattern `Every wednesday night`
* Update Haraka to 2.8.15. This fixes the issue where emails were bounced with the message 'Send MAIL FROM first'
* Do not overwrite existing subdomain when app's location is changed
* Add button to send test email
* Fix crash in carbon which made graphs disappear on some Cloudrons
* Fix issue where OAuth SSO did not work when alternate domain was used
* Changelog is now rendered in markdown format
[1.7.5]
* Expose a TLS relay port from mail container for Go applications
[1.7.6]
* Port bindings cannot be configured in update route anymore
* Implement LDAP group compare
* Pre-releases are now offered by appstore and not handled in box code anymore
* LDAP pagination support. This will fix the warnings in NextCloud and Rocket.Chat
* Check if directories can be created in the backup directory
* Do not set the HTTPS agent when using HTTP with minio backup backend
* Fix regression where a new domain config could not be set in the UI
* New mail container release that fixes email sending with SOGo
* Show 404 page for unknown domains
[1.7.7]
* Allow setting app memory till memory limit
* Make the dkim selector dynamic
* Fix issue where app update dialog did not close
* Fix LE cert renewal failures
* Send user and cert info in digest emails
* Send oom, app failures and other important mails to cloudron owner's alt mail
[1.8.0]
* Fix group email bounce when a group has users that have not signed up yet
* Do not restrict app memory limit to 4GB
* Fix display of the latest backup in the weekly digest
* Add UI to select users for access restriction
* Update docker to 17.09
* Update node to 6.11.5
* Display package version of installed apps in the info dialog
[1.8.1]
* Update node modules
* Allow a restore operation if app is already restoring
* Remove pre-install bundle support since it was hardly used
* Make the test email mail address configurable
* Allow admins to access all apps
* Send feedback via appstore API (instead of email)
* Show documentation URL in the app info dialog
* Update Lets Encrypt agrement URL (https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf)
[1.8.2]
* Update node modules
* Allow a restore operation if app is already restoring
* Remove pre-install bundle support since it was hardly used
* Make the test email mail address configurable
* Allow admins to access all apps
* Send feedback via appstore API (instead of email)
* Show documentation URL in the app info dialog
* Update Lets Encrypt agrement URL (https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf)
[1.8.3]
* Ensure domain database record exists
[1.8.4]
* Fix issue where internal email was not delivered when email relay is enabled
* Fix display of DNS records when email relay is enabled
+1 -1
View File
@@ -630,7 +630,7 @@ state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
box
Copyright (C) 2016 Cloudron UG
Copyright (C) 2016,2017 Cloudron UG
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
+10 -9
View File
@@ -39,17 +39,18 @@ apt-get -y install \
rcconf \
swaks \
unattended-upgrades \
unbound
unbound \
xfsprogs
# this ensures that unattended upgades are enabled, if it was disabled during ubuntu install time (see #346)
# debconf-set-selection of unattended-upgrades/enable_auto_updates + dpkg-reconfigure does not work
cp /usr/share/unattended-upgrades/20auto-upgrades /etc/apt/apt.conf.d/20auto-upgrades
echo "==> Installing node.js"
mkdir -p /usr/local/node-6.11.1
curl -sL https://nodejs.org/dist/v6.11.1/node-v6.11.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-6.11.1
ln -sf /usr/local/node-6.11.1/bin/node /usr/bin/node
ln -sf /usr/local/node-6.11.1/bin/npm /usr/bin/npm
mkdir -p /usr/local/node-6.11.5
curl -sL https://nodejs.org/dist/v6.11.5/node-v6.11.5-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-6.11.5
ln -sf /usr/local/node-6.11.5/bin/node /usr/bin/node
ln -sf /usr/local/node-6.11.5/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"
@@ -58,16 +59,16 @@ echo "==> Installing Docker"
# create systemd drop-in file
mkdir -p /etc/systemd/system/docker.service.d
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=devicemapper" > /etc/systemd/system/docker.service.d/cloudron.conf
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=overlay2" > /etc/systemd/system/docker.service.d/cloudron.conf
curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_17.03.1~ce-0~ubuntu-xenial_amd64.deb -o /tmp/docker.deb
curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_17.09.0~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
# apt install with install deps (as opposed to dpkg -i)
apt install -y /tmp/docker.deb
rm /tmp/docker.deb
storage_driver=$(docker info | grep "Storage Driver" | sed 's/.*: //')
if [[ "${storage_driver}" != "devicemapper" ]]; then
echo "Docker is using "${storage_driver}" instead of devicemapper"
if [[ "${storage_driver}" != "overlay2" ]]; then
echo "Docker is using "${storage_driver}" instead of overlay2"
exit 1
fi
+3 -2
View File
@@ -6,9 +6,9 @@ var argv = require('yargs').argv,
autoprefixer = require('gulp-autoprefixer'),
concat = require('gulp-concat'),
cssnano = require('gulp-cssnano'),
del = require('del'),
ejs = require('gulp-ejs'),
gulp = require('gulp'),
rimraf = require('rimraf'),
sass = require('gulp-sass'),
serve = require('gulp-serve'),
sourcemaps = require('gulp-sourcemaps'),
@@ -196,7 +196,8 @@ gulp.task('watch', ['default'], function () {
});
gulp.task('clean', function () {
del.sync(['webadmin/dist', 'setup/splash/website']);
rimraf.sync('webadmin/dist');
rimraf.sync('setup/splash/website');
});
gulp.task('default', ['clean', 'html', 'js', '3rdparty', 'images', 'css'], function () {});
@@ -0,0 +1,16 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN enableBackup BOOLEAN DEFAULT 1', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN enableBackup', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE settings MODIFY value TEXT', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE settings MODIFY value VARCHAR(512)', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,25 @@
'use strict';
// ensure backupFolder and format are not empty
exports.up = function(db, callback) {
db.all('SELECT * FROM settings WHERE name=?', [ 'backup_config' ], function (error, result) {
if (error || result.length === 0) return callback(error);
var value = JSON.parse(result[0].value);
value.format = 'tgz'; // set the format
if (value.provider === 'filesystem' && !value.backupFolder) {
value.backupFolder = '/var/backups'; // set the backupFolder
}
db.runSql('UPDATE settings SET value = ? WHERE name = ?', [ JSON.stringify(value), 'backup_config' ], function (error) {
if (error) console.error('Error setting ownerid ' + JSON.stringify(u) + error);
callback();
});
});
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE backups ADD COLUMN format VARCHAR(16) DEFAULT "tgz"', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE backups DROP COLUMN format', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN newConfigJson TEXT', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN newConfigJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,40 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE backups ADD COLUMN manifestJson TEXT'),
db.runSql.bind(db, 'START TRANSACTION;'),
// fill all the backups with restoreConfigs from current apps
function addManifests(callback) {
console.log('Importing manifests');
db.all('SELECT * FROM backups WHERE type="app"', function (error, backups) {
if (error) return callback(error);
async.eachSeries(backups, function (backup, next) {
var m = backup.restoreConfigJson ? JSON.parse(backup.restoreConfigJson) : null;
if (m) m = JSON.stringify(m.manifest);
db.runSql('UPDATE backups SET manifestJson=? WHERE id=?', [ m, backup.id ], next);
}, callback);
});
},
db.runSql.bind(db, 'COMMIT'),
// remove the restoreConfig
db.runSql.bind(db, 'ALTER TABLE backups DROP COLUMN restoreConfigJson')
], callback);
};
exports.down = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE backups DROP COLUMN manifestJson'),
db.runSql.bind(db, 'ALTER TABLE backups ADD COLUMN restoreConfigJson TEXT'),
], callback);
};
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps CHANGE newConfigJson updateConfigJson TEXT', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps CHANGE updateConfigJson newConfigJson TEXT', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps CHANGE lastBackupId restoreConfigJson TEXT', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps CHANGE restoreConfigJson lastBackupId TEXT', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
+8 -5
View File
@@ -60,7 +60,7 @@ CREATE TABLE IF NOT EXISTS apps(
manifestJson TEXT,
httpPort INTEGER, // this is the nginx proxy port and not manifest.httpPort
location VARCHAR(128) NOT NULL UNIQUE,
dnsRecordId VARCHAR(512), // tracks any id that we got back to track dns updates (unused)
dnsRecordId VARCHAR(512), // tracks any id that we got back to track dns updates
accessRestrictionJson TEXT, // { users: [ ], groups: [ ] }
createdAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
memoryLimit BIGINT DEFAULT 0,
@@ -69,10 +69,12 @@ CREATE TABLE IF NOT EXISTS apps(
sso BOOLEAN DEFAULT 1, // whether user chose to enable SSO
debugModeJson TEXT, // options for development mode
robotsTxt TEXT,
enableBackup BOOLEAN DEFAULT 1,
// the following fields do not belong here, they can be removed when we use a queue for apptask
lastBackupId VARCHAR(128), // used to pass backupId to restore from to apptask
oldConfigJson TEXT, // used to pass old config for apptask
restoreConfigJson VARCHAR(256), // used to pass backupId to restore from to apptask
oldConfigJson TEXT, // used to pass old config for apptask (configure, restore)
updateConfigJson TEXT, // used to pass new config for apptask (update)
PRIMARY KEY(id));
@@ -92,7 +94,7 @@ CREATE TABLE IF NOT EXISTS authcodes(
CREATE TABLE IF NOT EXISTS settings(
name VARCHAR(128) NOT NULL UNIQUE,
value VARCHAR(512),
value TEXT,
PRIMARY KEY(name));
CREATE TABLE IF NOT EXISTS appAddonConfigs(
@@ -109,7 +111,8 @@ CREATE TABLE IF NOT EXISTS backups(
type VARCHAR(16) NOT NULL, /* 'box' or 'app' */
dependsOn TEXT, /* comma separate list of objects this backup depends on */
state VARCHAR(16) NOT NULL,
restoreConfigJson TEXT, /* JSON including the manifest of the backed up app */
manifestJson TEXT, /* to validate if the app can be installed in this version of box */
format VARCHAR(16) DEFAULT "tgz",
PRIMARY KEY (id));
+1718 -1198
View File
File diff suppressed because it is too large Load Diff
+56 -52
View File
@@ -1,99 +1,103 @@
{
"name": "Cloudron",
"name": "cloudron",
"description": "Main code for a cloudron",
"version": "0.0.1",
"private": "true",
"version": "1.0.0",
"private": true,
"author": {
"name": "Cloudron authors"
},
"repository": {
"type": "git"
"type": "git",
"url": "https://git.cloudron.io/cloudron/box.git"
},
"engines": {
"node": ">=4.0.0 <=4.1.1"
},
"engines": [
"node >=4.0.0 <=4.1.1"
],
"dependencies": {
"@google-cloud/dns": "^0.7.0",
"@sindresorhus/df": "^2.1.0",
"async": "^2.1.4",
"aws-sdk": "^2.41.0",
"body-parser": "^1.13.1",
"cloudron-manifestformat": "^2.8.0",
"async": "^2.6.0",
"aws-sdk": "^2.151.0",
"body-parser": "^1.18.2",
"cloudron-manifestformat": "^2.10.0",
"connect-ensure-login": "^0.1.1",
"connect-lastmile": "^0.1.0",
"connect-timeout": "^1.5.0",
"connect-lastmile": "^1.0.2",
"connect-timeout": "^1.9.0",
"cookie-parser": "^1.3.5",
"cookie-session": "^1.1.0",
"cron": "^1.0.9",
"cookie-session": "^1.3.2",
"cron": "^1.3.0",
"csurf": "^1.6.6",
"db-migrate": "^0.10.0-beta.20",
"db-migrate": "^0.10.0-beta.24",
"db-migrate-mysql": "^1.1.10",
"debug": "^2.2.0",
"dockerode": "^2.4.3",
"ejs": "^2.2.4",
"ejs-cli": "^1.2.0",
"express": "^4.12.4",
"express-session": "^1.11.3",
"debug": "^3.1.0",
"dockerode": "^2.5.3",
"ejs": "^2.5.7",
"ejs-cli": "^2.0.0",
"express": "^4.16.2",
"express-session": "^1.15.6",
"gulp-sass": "^3.0.0",
"hat": "0.0.3",
"hock": "https://registry.npmjs.org/hock/-/hock-1.3.2.tgz",
"json": "^9.0.3",
"ldapjs": "^1.0.0",
"mime": "^1.3.4",
"moment-timezone": "^0.5.5",
"morgan": "^1.7.0",
"lodash.chunk": "^4.2.0",
"mime": "^2.0.3",
"moment-timezone": "^0.5.14",
"morgan": "^1.9.0",
"multiparty": "^4.1.2",
"mysql": "^2.7.0",
"node-uuid": "^1.4.3",
"nodemailer": "^4.0.1",
"mysql": "^2.15.0",
"nodemailer": "^4.4.0",
"nodemailer-smtp-transport": "^2.7.4",
"oauth2orize": "^1.0.1",
"oauth2orize": "^1.11.0",
"once": "^1.3.2",
"parse-links": "^0.1.0",
"passport": "^0.2.2",
"passport-http": "^0.2.2",
"passport": "^0.4.0",
"passport-http": "^0.3.0",
"passport-http-bearer": "^1.0.1",
"passport-local": "^1.0.0",
"passport-oauth2-client-password": "^0.1.2",
"password-generator": "^2.0.2",
"password-generator": "^2.2.0",
"progress-stream": "^2.0.0",
"proxy-middleware": "^0.13.0",
"proxy-middleware": "^0.15.0",
"request": "^2.83.0",
"s3-block-read-stream": "^0.2.0",
"safetydance": "^0.2.0",
"semver": "^4.3.6",
"showdown": "^1.6.0",
"safetydance": "^0.7.1",
"semver": "^5.4.1",
"showdown": "^1.8.2",
"split": "^1.0.0",
"superagent": "^1.8.3",
"superagent": "^3.8.1",
"supererror": "^0.7.1",
"tar-fs": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.15.2.tgz",
"tldjs": "^1.6.2",
"tar-fs": "^1.16.0",
"tar-stream": "^1.5.5",
"tldjs": "^2.2.0",
"underscore": "^1.7.0",
"uuid": "^3.1.0",
"valid-url": "^1.0.9",
"validator": "^4.9.0"
"validator": "^9.1.1",
"ws": "^3.3.1"
},
"devDependencies": {
"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": "^3.9.1",
"gulp-autoprefixer": "^4.0.0",
"gulp-concat": "^2.4.3",
"gulp-cssnano": "^2.1.0",
"gulp-ejs": "^1.0.0",
"gulp-ejs": "^3.1.0",
"gulp-sass": "^3.0.0",
"gulp-serve": "^1.0.0",
"gulp-sourcemaps": "^1.5.2",
"gulp-uglify": "^1.1.0",
"gulp-sourcemaps": "^2.6.1",
"gulp-uglify": "^3.0.0",
"hock": "~1.2.0",
"istanbul": "*",
"js2xmlparser": "^1.0.0",
"js2xmlparser": "^3.0.0",
"mocha": "*",
"mock-aws-s3": "^2.4.0",
"nock": "^9.0.2",
"node-sass": "^3.0.0-alpha.0",
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
"nock": "^9.0.14",
"node-sass": "^4.6.1",
"readdirp": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz",
"request": "^2.65.0",
"yargs": "^3.15.0"
"yargs": "^10.0.3"
},
"scripts": {
"migrate_local": "DATABASE_URL=mysql://root:@localhost/box node_modules/.bin/db-migrate up",
+14 -18
View File
@@ -45,6 +45,7 @@ fi
initBaseImage="true"
# provisioning data
domain=""
adminLocation="my"
zoneName=""
provider=""
encryptionKey=""
@@ -63,13 +64,14 @@ baseDataDir=""
# TODO this is still there for the restore case, see other occasions below
versionsUrl="https://s3.amazonaws.com/prod-cloudron-releases/versions.json"
args=$(getopt -o "" -l "domain:,help,skip-baseimage-init,data:,data-dir:,provider:,encryption-key:,restore-url:,tls-provider:,version:,dns-provider:,env:,prerelease,skip-reboot,source-url:" -n "$0" -- "$@")
args=$(getopt -o "" -l "domain:,help,skip-baseimage-init,data:,data-dir:,provider:,encryption-key:,restore-url:,tls-provider:,version:,dns-provider:,env:,admin-location:,prerelease,skip-reboot,source-url:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--domain) domain="$2"; shift 2;;
--help) echo "See https://cloudron.io/references/selfhosting.html on how to install Cloudron"; exit 0;;
--admin-location) adminLocation="$2"; shift 2;;
--help) echo "See https://cloudron.io/documentation/installation/ on how to install Cloudron"; exit 0;;
--provider) provider="$2"; shift 2;;
--encryption-key) encryptionKey="$2"; shift 2;;
--restore-url) restoreUrl="$2"; shift 2;;
@@ -105,13 +107,15 @@ done
# validate arguments in the absence of data
if [[ -z "${dataJson}" ]]; then
if [[ -z "${provider}" ]]; then
echo "--provider is required (azure, digitalocean, ec2, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
echo "--provider is required (azure, cloudscale.ch, digitalocean, ec2, exoscale, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
exit 1
elif [[ \
"${provider}" != "ami" && \
"${provider}" != "azure" && \
"${provider}" != "cloudscale.ch" && \
"${provider}" != "digitalocean" && \
"${provider}" != "ec2" && \
"${provider}" != "exoscale" && \
"${provider}" != "gce" && \
"${provider}" != "lightsail" && \
"${provider}" != "linode" && \
@@ -121,7 +125,7 @@ if [[ -z "${dataJson}" ]]; then
"${provider}" != "vultr" && \
"${provider}" != "generic" \
]]; then
echo "--provider must be one of: azure, digitalocean, ec2, gce, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, gce, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
exit 1
fi
@@ -195,6 +199,7 @@ if [[ -z "${dataJson}" ]]; then
{
"boxVersionsUrl": "${versionsUrl}",
"fqdn": "${domain}",
"adminLocation": "${adminLocation}",
"zoneName": "${zoneName}",
"provider": "${provider}",
"apiServerOrigin": "${apiServerOrigin}",
@@ -209,11 +214,9 @@ if [[ -z "${dataJson}" ]]; then
"provider": "filesystem",
"backupFolder": "/var/backups",
"key": "${encryptionKey}",
"format": "tgz",
"retentionSecs": 172800
},
"updateConfig": {
"prerelease": ${prerelease}
},
"version": "${version}"
}
EOF
@@ -223,6 +226,7 @@ EOF
{
"boxVersionsUrl": "${versionsUrl}",
"fqdn": "${domain}",
"adminLocation": "${adminLocation}",
"zoneName": "${zoneName}",
"provider": "${provider}",
"apiServerOrigin": "${apiServerOrigin}",
@@ -259,17 +263,9 @@ fi
echo "=> Installing version ${version} (this takes some time) ..."
echo "${data}" > "${DATA_FILE}"
# poor mans semver
if [[ ${version} == "0.10"* ]]; then
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" &>> "${LOG_FILE}"; then
echo "Failed to install cloudron. See ${LOG_FILE} for details"
exit 1
fi
else
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" --data-dir "${baseDataDir}" &>> "${LOG_FILE}"; then
echo "Failed to install cloudron. See ${LOG_FILE} for details"
exit 1
fi
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" --data-dir "${baseDataDir}" &>> "${LOG_FILE}"; then
echo "Failed to install cloudron. See ${LOG_FILE} for details"
exit 1
fi
rm "${DATA_FILE}"
+2 -2
View File
@@ -31,8 +31,8 @@ if ! $(cd "${SOURCE_DIR}" && git diff --exit-code >/dev/null); then
exit 1
fi
if [[ "$(node --version)" != "v6.11.1" ]]; then
echo "This script requires node 6.11.1"
if [[ "$(node --version)" != "v6.11.5" ]]; then
echo "This script requires node 6.11.5"
exit 1
fi
+34 -9
View File
@@ -34,13 +34,41 @@ while true; do
esac
done
echo "==> installer: updating docker"
if [[ $(docker version --format {{.Client.Version}}) != "17.09.0-ce" ]]; then
$curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_17.09.0~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
# https://download.docker.com/linux/ubuntu/dists/xenial/stable/binary-amd64/Packages
if [[ $(sha256sum /tmp/docker.deb | cut -d' ' -f1) != "d33f6eb134f0ab0876148bd96de95ea47d583d7f2cddfdc6757979453f9bd9bf" ]]; then
echo "docker binary download is corrupt"
exit 5
fi
echo "Waiting for all dpkg tasks to finish..."
while fuser /var/lib/dpkg/lock; do
sleep 1
done
while ! dpkg --force-confold --configure -a; do
echo "Failed to fix packages. Retry"
sleep 1
done
while ! apt install -y /tmp/docker.deb; do
echo "Failed to install docker. Retry"
sleep 1
done
rm /tmp/docker.deb
fi
echo "==> installer: updating node"
if [[ "$(node --version)" != "v6.11.1" ]]; then
mkdir -p /usr/local/node-6.11.1
$curl -sL https://nodejs.org/dist/v6.11.1/node-v6.11.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-6.11.1
ln -sf /usr/local/node-6.11.1/bin/node /usr/bin/node
ln -sf /usr/local/node-6.11.1/bin/npm /usr/bin/npm
rm -rf /usr/local/node-6.9.2
if [[ "$(node --version)" != "v6.11.5" ]]; then
mkdir -p /usr/local/node-6.11.5
$curl -sL https://nodejs.org/dist/v6.11.5/node-v6.11.5-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-6.11.5
ln -sf /usr/local/node-6.11.5/bin/node /usr/bin/node
ln -sf /usr/local/node-6.11.5/bin/npm /usr/bin/npm
rm -rf /usr/local/node-6.11.3
fi
for try in `seq 1 10`; do
@@ -81,9 +109,6 @@ fi
# ensure we are not inside the source directory, which we will remove now
cd /root
echo "==> installer: updating packages"
# add logic to update apt packages here
echo "==> installer: switching the box code"
rm -rf "${BOX_SRC_DIR}"
mv "${box_src_tmp_dir}" "${BOX_SRC_DIR}"
+5 -9
View File
@@ -6,6 +6,7 @@ json="${source_dir}/../node_modules/.bin/json"
# IMPORTANT: Fix cloudron.js:doUpdate if you add/remove any arg. keep these sorted for readability
arg_api_server_origin=""
arg_fqdn=""
arg_admin_location=""
arg_zone_name=""
arg_is_custom_domain="false"
arg_restore_key=""
@@ -20,9 +21,7 @@ arg_version=""
arg_web_server_origin=""
arg_backup_config=""
arg_dns_config=""
arg_update_config=""
arg_provider=""
arg_app_bundle=""
arg_is_demo="false"
args=$(getopt -o "" -l "data:,retire-reason:,retire-info:" -n "$0" -- "$@")
@@ -46,19 +45,19 @@ while true; do
arg_is_custom_domain=$(echo "$2" | $json isCustomDomain)
[[ "${arg_is_custom_domain}" == "" ]] && arg_is_custom_domain="true"
arg_admin_location=$(echo "$2" | $json adminLocation)
[[ "${arg_admin_location}" == "" ]] && arg_admin_location="my"
# only update/restore have this valid (but not migrate)
arg_api_server_origin=$(echo "$2" | $json apiServerOrigin)
[[ "${arg_api_server_origin}" == "" ]] && arg_api_server_origin="https://api.cloudron.io"
arg_web_server_origin=$(echo "$2" | $json webServerOrigin)
[[ "${arg_web_server_origin}" == "" ]] && arg_web_server_origin="https://cloudron.io"
# TODO check if an where this is used
# TODO check if and where this is used
arg_version=$(echo "$2" | $json version)
# read possibly empty parameters here
arg_app_bundle=$(echo "$2" | $json appBundle)
[[ "${arg_app_bundle}" == "" ]] && arg_app_bundle="[]"
arg_is_demo=$(echo "$2" | $json isDemo)
[[ "${arg_is_demo}" == "" ]] && arg_is_demo="false"
@@ -86,9 +85,6 @@ while true; do
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;;
+3 -4
View File
@@ -7,7 +7,6 @@ readonly SETUP_WEBSITE_DIR="/home/yellowtent/setup/website"
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly box_src_dir="$(realpath ${script_dir}/..)"
readonly PLATFORM_DATA_DIR="/home/yellowtent/platformdata"
readonly ADMIN_LOCATION="my" # keep this in sync with constants.js
echo "Setting up nginx update page"
@@ -19,7 +18,7 @@ fi
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
# keep this is sync with config.js appFqdn()
admin_fqdn=$([[ "${arg_is_custom_domain}" == "true" ]] && echo "${ADMIN_LOCATION}.${arg_fqdn}" || echo "${ADMIN_LOCATION}-${arg_fqdn}")
admin_fqdn=$([[ "${arg_is_custom_domain}" == "true" ]] && echo "${arg_admin_location}.${arg_fqdn}" || echo "${arg_admin_location}-${arg_fqdn}")
admin_origin="https://${admin_fqdn}"
# copy the website
@@ -34,11 +33,11 @@ if [[ "${arg_retire_reason}" != "" || "${existing_infra}" != "${current_infra}"
echo "Showing progress bar on all subdomains in retired mode or infra update. retire: ${arg_retire_reason} existing: ${existing_infra} current: ${current_infra}"
rm -f ${PLATFORM_DATA_DIR}/nginx/applications/*
${box_src_dir}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
-O "{ \"vhost\": \"~^(.+)\$\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\", \"robotsTxtQuoted\": null }" > "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf"
-O "{ \"vhost\": \"~^(.+)\$\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\", \"robotsTxtQuoted\": null, \"hasIPv6\": false }" > "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf"
else
echo "Show progress bar only on admin domain for normal update"
${box_src_dir}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\", \"robotsTxtQuoted\": null }" > "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf"
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\", \"robotsTxtQuoted\": null, \"hasIPv6\": false }" > "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf"
fi
if [[ "${arg_retire_reason}" == "migrate" ]]; then
+80 -83
View File
@@ -7,7 +7,6 @@ echo "==> Cloudron Start"
readonly USER="yellowtent"
readonly HOME_DIR="/home/${USER}"
readonly BOX_SRC_DIR="${HOME_DIR}/box"
readonly OLD_DATA_DIR="${HOME_DIR}/data";
readonly PLATFORM_DATA_DIR="${HOME_DIR}/platformdata" # platform data
readonly APPS_DATA_DIR="${HOME_DIR}/appsdata" # app data
readonly BOX_DATA_DIR="${HOME_DIR}/boxdata" # box data
@@ -42,7 +41,7 @@ systemctl restart apparmor
usermod ${USER} -a -G docker
# preserve the existing storage driver (user might be using overlay2)
storage_driver=$(docker info | grep "Storage Driver" | sed 's/.*: //')
[[ -n "${storage_driver}" ]] || storage_driver="devicemapper" # if the above command fails
[[ -n "${storage_driver}" ]] || storage_driver="overlay2" # if the above command fails
temp_file=$(mktemp)
# create systemd drop-in. some apps do not work with aufs
@@ -73,53 +72,29 @@ fi
mkdir -p "${BOX_DATA_DIR}"
mkdir -p "${APPS_DATA_DIR}"
mkdir -p "${PLATFORM_DATA_DIR}"
# keep these in sync with paths.js
echo "==> Ensuring directories"
if [[ ! -d "${PLATFORM_DATA_DIR}/mail" ]]; then
if [[ -d "${OLD_DATA_DIR}/mail" ]]; then
echo "==> Migrate old mail data"
# Migrate mail data to new format
docker stop mail || true # otherwise the move below might fail if mail container writes in the middle
mkdir -p "${PLATFORM_DATA_DIR}/mail"
# we can't move the whole folder as it is a btrfs subvolume mount
mv -f "${OLD_DATA_DIR}/mail/"* "${PLATFORM_DATA_DIR}/mail/" # this used to be mail container's run directory
else
echo "==> Create new mail data dir"
mkdir -p "${PLATFORM_DATA_DIR}/mail"
fi
fi
mkdir -p "${PLATFORM_DATA_DIR}/graphite"
mkdir -p "${PLATFORM_DATA_DIR}/mail/dkim"
mkdir -p "${PLATFORM_DATA_DIR}/mysql"
mkdir -p "${PLATFORM_DATA_DIR}/postgresql"
mkdir -p "${PLATFORM_DATA_DIR}/mongodb"
mkdir -p "${PLATFORM_DATA_DIR}/snapshots"
mkdir -p "${PLATFORM_DATA_DIR}/addons/mail"
mkdir -p "${PLATFORM_DATA_DIR}/collectd/collectd.conf.d"
mkdir -p "${PLATFORM_DATA_DIR}/logrotate.d"
mkdir -p "${PLATFORM_DATA_DIR}/acme"
mkdir -p "${PLATFORM_DATA_DIR}/backup"
mkdir -p "${BOX_DATA_DIR}/appicons"
mkdir -p "${BOX_DATA_DIR}/certs"
mkdir -p "${BOX_DATA_DIR}/acme" # acme keys
mkdir -p "${BOX_DATA_DIR}/mail/dkim"
# ensure backups folder exists and is writeable
mkdir -p /var/backups
chmod 777 /var/backups
echo "==> Check for old btrfs volumes"
if mountpoint -q "${OLD_DATA_DIR}"; then
echo "==> Cleanup btrfs volumes"
# First stop all container to be able to unmount
docker ps -q | xargs docker stop
umount "${OLD_DATA_DIR}"
rm -rf "/root/user_data.img"
else
echo "==> No btrfs volumes found";
fi
echo "==> Configuring journald"
sed -e "s/^#SystemMaxUse=.*$/SystemMaxUse=100M/" \
-e "s/^#ForwardToSyslog=.*$/ForwardToSyslog=no/" \
@@ -145,7 +120,10 @@ echo "==> Setting up unbound"
# DO uses Google nameservers by default. This causes RBL queries to fail (host 2.0.0.127.zen.spamhaus.org)
# We do not use dnsmasq because it is not a recursive resolver and defaults to the value in the interfaces file (which is Google DNS!)
# We listen on 0.0.0.0 because there is no way control ordering of docker (which creates the 172.18.0.0/16) and unbound
echo -e "server:\n\tinterface: 0.0.0.0\n\taccess-control: 127.0.0.1 allow\n\taccess-control: 172.18.0.1/16 allow\n\tcache-max-negative-ttl: 30\n\tcache-max-ttl: 300" > /etc/unbound/unbound.conf.d/cloudron-network.conf
# If IP6 is not enabled, dns queries seem to fail on some hosts
echo -e "server:\n\tinterface: 0.0.0.0\n\tdo-ip6: yes\n\taccess-control: 127.0.0.1 allow\n\taccess-control: 172.18.0.1/16 allow\n\tcache-max-negative-ttl: 30\n\tcache-max-ttl: 300\n\t#logfile: /var/log/unbound.log\n\t#verbosity: 10" > /etc/unbound/unbound.conf.d/cloudron-network.conf
# update the root anchor after a out-of-disk-space situation (see #269)
unbound-anchor -a /var/lib/unbound/root.key
echo "==> Adding systemd services"
cp -r "${script_dir}/start/systemd/." /etc/systemd/system/
@@ -170,9 +148,17 @@ cp "${script_dir}/start/sudoers" /etc/sudoers.d/${USER}
echo "==> Configuring collectd"
rm -rf /etc/collectd
ln -sfF "${PLATFORM_DATA_DIR}/collectd" /etc/collectd
cp "${script_dir}/start/collectd.conf" "${PLATFORM_DATA_DIR}/collectd/collectd.conf"
cp "${script_dir}/start/collectd/collectd.conf" "${PLATFORM_DATA_DIR}/collectd/collectd.conf"
systemctl restart collectd
echo "==> Configuring logrotate"
if ! grep -q "^include ${PLATFORM_DATA_DIR}/logrotate.d" /etc/logrotate.conf; then
echo -e "\ninclude ${PLATFORM_DATA_DIR}/logrotate.d\n" >> /etc/logrotate.conf
fi
echo "==> Adding motd message for admins"
cp "${script_dir}/start/cloudron-motd" /etc/update-motd.d/92-cloudron
echo "==> Configuring nginx"
# link nginx config to system config
unlink /etc/nginx 2>/dev/null || rm -rf /etc/nginx
@@ -200,7 +186,11 @@ if [[ ! -f /etc/mysql/mysql.cnf ]] || ! diff -q "${script_dir}/start/mysql.cnf"
echo "Waiting for mysql jobs..."
sleep 1
done
systemctl restart mysql
while true; do
if systemctl restart mysql; then break; fi
echo "Restarting MySql again after sometime since this fails randomly"
sleep 1
done
else
systemctl start mysql
fi
@@ -212,20 +202,31 @@ mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
if [[ -n "${arg_restore_url}" ]]; then
set_progress "30" "Downloading restore data"
decrypt=""
if [[ "${arg_restore_url}" == *.tar.gz.enc || -n "${arg_restore_key}" ]]; then
echo "==> Downloading encrypted backup: ${arg_restore_url} and key: ${arg_restore_key}"
decrypt=(openssl aes-256-cbc -d -nosalt -pass "pass:${arg_restore_key}")
else
echo "==> Downloading backup: ${arg_restore_url}"
decrypt=(cat -)
fi
readonly restore_dir="${arg_restore_url#file://}"
while true; do
if $curl -L "${arg_restore_url}" | "${decrypt[@]}" \
| tar -zxf - --overwrite --transform="s,^box/\?,boxdata/," --transform="s,^mail/\?,platformdata/mail/," --show-transformed-names -C "${HOME_DIR}"; then break; fi
echo "Failed to download data, trying again"
done
if [[ -d "${restore_dir}" ]]; then # rsync backup
echo "==> Copying backup: ${restore_dir}"
if [[ $(stat -c "%d" "${BOX_DATA_DIR}") == $(stat -c "%d" "${restore_dir}") ]]; then
cp -rfl "${restore_dir}/." "${BOX_DATA_DIR}"
else
cp -rf "${restore_dir}/." "${BOX_DATA_DIR}"
fi
else # tgz backup
decrypt=""
if [[ "${arg_restore_url}" == *.tar.gz.enc || -n "${arg_restore_key}" ]]; then
echo "==> Downloading encrypted backup: ${arg_restore_url} and key: ${arg_restore_key}"
decrypt=(openssl aes-256-cbc -d -nosalt -pass "pass:${arg_restore_key}")
elif [[ "${arg_restore_url}" == *.tar.gz ]]; then
echo "==> Downloading backup: ${arg_restore_url}"
decrypt=(cat -)
fi
while true; do
if $curl -L "${arg_restore_url}" | "${decrypt[@]}" \
| tar -zxf - --overwrite -C "${BOX_DATA_DIR}"; then break; fi
echo "Failed to download data, trying again"
done
fi
set_progress "35" "Setting up MySQL"
if [[ -f "${BOX_DATA_DIR}/box.mysqldump" ]]; then
@@ -238,9 +239,27 @@ set_progress "40" "Migrating data"
sudo -u "${USER}" -H bash <<EOF
set -eu
cd "${BOX_SRC_DIR}"
BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@localhost/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up
BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up
EOF
echo "==> Adding automated configs"
mysql -u root -p${mysql_root_password} -e "REPLACE INTO settings (name, value) VALUES (\"domain\", '{ \"fqdn\": \"$arg_fqdn\", \"zoneName\": \"$arg_zone_name\", \"adminLocation\": \"$arg_admin_location\" }')" box
if [[ ! -z "${arg_backup_config}" ]]; then
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO settings (name, value) VALUES (\"backup_config\", '$arg_backup_config')" box
fi
if [[ ! -z "${arg_dns_config}" ]]; then
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO settings (name, value) VALUES (\"dns_config\", '$arg_dns_config')" box
fi
if [[ ! -z "${arg_tls_config}" ]]; then
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO settings (name, value) VALUES (\"tls_config\", '$arg_tls_config')" box
fi
echo "==> Creating cloudron.conf"
cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
{
@@ -249,18 +268,11 @@ cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
"apiServerOrigin": "${arg_api_server_origin}",
"webServerOrigin": "${arg_web_server_origin}",
"fqdn": "${arg_fqdn}",
"adminLocation": "${arg_admin_location}",
"zoneName": "${arg_zone_name}",
"isCustomDomain": ${arg_is_custom_domain},
"provider": "${arg_provider}",
"isDemo": ${arg_is_demo},
"database": {
"hostname": "localhost",
"username": "root",
"password": "${mysql_root_password}",
"port": 3306,
"name": "box"
},
"appBundle": ${arg_app_bundle}
"isDemo": ${arg_is_demo}
}
CONF_END
# pass these out-of-band because they have new lines which interfere with json
@@ -276,39 +288,24 @@ cat > "${BOX_SRC_DIR}/webadmin/dist/config.json" <<CONF_END
}
CONF_END
if [[ ! -f "${BOX_DATA_DIR}/dhparams.pem" ]]; then
echo "==> Generating dhparams (takes forever)"
openssl dhparam -out "${BOX_DATA_DIR}/dhparams.pem" 2048
cp "${BOX_DATA_DIR}/dhparams.pem" "${PLATFORM_DATA_DIR}/addons/mail/dhparams.pem"
else
cp "${BOX_DATA_DIR}/dhparams.pem" "${PLATFORM_DATA_DIR}/addons/mail/dhparams.pem"
fi
echo "==> Changing ownership"
chown "${USER}:${USER}" -R "${CONFIG_DIR}"
chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/nginx" "${PLATFORM_DATA_DIR}/collectd" "${PLATFORM_DATA_DIR}/addons" "${PLATFORM_DATA_DIR}/acme"
chown "${USER}:${USER}" -R "${BOX_DATA_DIR}"
chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/mail/dkim" # this is owned by box currently since it generates the keys
chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/nginx" "${PLATFORM_DATA_DIR}/collectd" "${PLATFORM_DATA_DIR}/logrotate.d" "${PLATFORM_DATA_DIR}/addons" "${PLATFORM_DATA_DIR}/acme" "${PLATFORM_DATA_DIR}/backup"
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}/INFRA_VERSION" 2>/dev/null || true
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}"
echo "==> Adding automated configs"
if [[ ! -z "${arg_backup_config}" ]]; then
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO settings (name, value) VALUES (\"backup_config\", '$arg_backup_config')" box
fi
if [[ ! -z "${arg_dns_config}" ]]; then
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO settings (name, value) VALUES (\"dns_config\", '$arg_dns_config')" box
fi
if [[ ! -z "${arg_update_config}" ]]; then
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO settings (name, value) VALUES (\"update_config\", '$arg_update_config')" box
fi
if [[ ! -z "${arg_tls_config}" ]]; then
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO settings (name, value) VALUES (\"tls_config\", '$arg_tls_config')" box
fi
echo "==> Generating dhparams (takes forever)"
if [[ ! -f "${BOX_DATA_DIR}/dhparams.pem" ]]; then
openssl dhparam -out "${BOX_DATA_DIR}/dhparams.pem" 2048
fi
# do not chown the boxdata/mail directory; dovecot gets upset
chown "${USER}:${USER}" "${BOX_DATA_DIR}"
find "${BOX_DATA_DIR}" -mindepth 1 -maxdepth 1 -not -path "${BOX_DATA_DIR}/mail" -exec chown -R "${USER}:${USER}" {} \;
chown "${USER}:${USER}" -R "${BOX_DATA_DIR}/mail/dkim" # this is owned by box currently since it generates the keys
set_progress "60" "Starting Cloudron"
systemctl start cloudron.target
+15
View File
@@ -0,0 +1,15 @@
#!/bin/sh
# motd hook to remind admins about updates
printf "\t\t\tNOTE TO CLOUDRON ADMINS\n"
printf "\t\t\t-----------------------\n"
printf "Please do not run apt upgrade manually as it will update packages that\n"
printf "Cloudron relies on and may break your installation. Ubuntu security updates\n"
printf "are automatically installed on this server every night.\n"
printf "\n"
printf "Read more at https://cloudron.io/documentation/security/#os-updates\n"
if grep -q "^PasswordAuthentication yes" /etc/ssh/sshd_config; then
printf "\nPlease disable password based SSH access to secure your server. Read more at\n"
printf "https://cloudron.io/documentation/security/#securing-ssh-access\n"
fi
@@ -89,7 +89,7 @@ LoadPlugin cpu
#LoadPlugin curl_json
#LoadPlugin curl_xml
#LoadPlugin dbi
LoadPlugin df
#LoadPlugin df
#LoadPlugin disk
#LoadPlugin dns
#LoadPlugin email
@@ -138,9 +138,9 @@ LoadPlugin nginx
#LoadPlugin powerdns
#LoadPlugin processes
#LoadPlugin protocols
#<LoadPlugin python>
# Globals true
#</LoadPlugin>
<LoadPlugin python>
Globals true
</LoadPlugin>
#LoadPlugin rrdcached
#LoadPlugin rrdtool
#LoadPlugin sensors
@@ -192,16 +192,6 @@ LoadPlugin write_graphite
</Aggregation>
</Plugin>
<Plugin df>
FSType "ext4"
ReportByDevice true
IgnoreSelected false
ValuesAbsolute true
ValuesPercentage true
</Plugin>
<Plugin interface>
Interface "eth0"
IgnoreSelected false
@@ -243,6 +233,17 @@ LoadPlugin write_graphite
</File>
</Plugin>
<Plugin python>
# https://blog.dbrgn.ch/2017/3/10/write-a-collectd-python-plugin/
ModulePath "/home/yellowtent/box/setup/start/collectd/"
LogTraces false # enable this to get traces in /var/log/collectd.log
Interactive false
Import "df"
# <Module df>
# </Module>
</Plugin>
<Plugin write_graphite>
<Node "graphing">
Host "localhost"
+36
View File
@@ -0,0 +1,36 @@
import collectd,os,subprocess
# https://blog.dbrgn.ch/2017/3/10/write-a-collectd-python-plugin/
disks = []
def init():
global disks
lines = [s.split() for s in subprocess.check_output(["df", "--type=ext4", "--output=source,target,size,used,avail"]).splitlines()]
disks = lines[1:] # strip header
collectd.info('custom df plugin initialized with %s' % disks)
def read():
for d in disks:
device = d[0]
if 'devicemapper' in d[1] or not device.startswith('/dev/'): continue
instance = device[len('/dev/'):].replace('/', '_') # see #348
try:
st = os.statvfs(d[1]) # handle disk removal
except:
continue
val = collectd.Values(type='df_complex', plugin='df', plugin_instance=instance)
free = st.f_bavail * st.f_frsize # bavail is for non-root user. bfree is total
val.dispatch(values=[free], type_instance='free')
reserved = (st.f_bfree - st.f_bavail) * st.f_frsize # root took these
val.dispatch(values=[reserved], type_instance='reserved')
used = (st.f_blocks - st.f_bfree) * st.f_frsize
val.dispatch(values=[used], type_instance='used')
collectd.register_init(init)
collectd.register_read(read)
+51 -5
View File
@@ -4,13 +4,54 @@ map $http_upgrade $connection_upgrade {
'' close;
}
# http server
server {
<% if (vhost) { %>
listen 443 http2;
listen 80;
<% if (hasIPv6) { -%>
listen [::]:80;
<% } -%>
<% if (vhost) { -%>
server_name <%= vhost %>;
<% } else { %>
<% } else { -%>
# IP based access from collectd or initial cloudron setup. TODO: match the IPv6 address
server_name "~^\d+\.\d+\.\d+\.\d+$";
# collectd
location /nginx_status {
stub_status on;
access_log off;
allow 127.0.0.1;
deny all;
}
<% } -%>
# acme challenges (for cert renewal where the vhost config exists)
location /.well-known/acme-challenge/ {
default_type text/plain;
alias /home/yellowtent/platformdata/acme/;
}
location / {
# redirect everything to HTTPS
return 301 https://$host$request_uri;
}
}
# https server
server {
<% if (vhost) { -%>
server_name <%= vhost %>;
listen 443 http2;
<% if (hasIPv6) { -%>
listen [::]:443 http2;
<% } -%>
<% } else { -%>
listen 443 http2 default_server;
<% } %>
<% if (hasIPv6) { -%>
listen [::]:443 http2 default_server;
<% } -%>
<% } -%>
ssl on;
# paths are relative to prefix and not to this file
@@ -80,7 +121,7 @@ server {
# No buffering to temp files, it fails for large downloads
proxy_max_temp_file_size 0;
# Disable check to allow unlimited body sizes
# Disable check to allow unlimited body sizes. this allows apps to accept whatever size they want
client_max_body_size 0;
<% if (robotsTxtQuoted) { %>
@@ -107,6 +148,11 @@ server {
proxy_read_timeout 30m;
}
location ~ ^/api/v1/apps/.*/upload$ {
proxy_pass http://127.0.0.1:3000;
client_max_body_size 0;
}
# graphite paths (uncomment block below and visit /graphite/index.html)
# location ~ ^/(graphite|content|metrics|dashboard|render|browser|composer)/ {
# proxy_pass http://127.0.0.1:8000;
+7 -13
View File
@@ -36,27 +36,21 @@ http {
# zones for rate limiting
limit_req_zone $binary_remote_addr zone=admin_login:10m rate=10r/s; # 10 request a second
# HTTP server
# default http server that returns 404 for any domain we are not listening on
server {
listen 80;
listen 80 default_server;
listen [::]:80 default_server;
server_name does_not_match_anything;
# collectd
location /nginx_status {
stub_status on;
access_log off;
allow 127.0.0.1;
deny all;
}
# acme challenges
# acme challenges (for app installation and re-configure when the vhost config does not exist)
location /.well-known/acme-challenge/ {
default_type text/plain;
alias /home/yellowtent/platformdata/acme/;
}
location / {
# redirect everything to HTTPS
return 301 https://$host$request_uri;
return 404;
}
}
+8 -4
View File
@@ -13,8 +13,8 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reloadnginx.sh
Defaults!/home/yellowtent/box/src/scripts/reboot.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reboot.sh
Defaults!/home/yellowtent/box/src/scripts/reloadcollectd.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reloadcollectd.sh
Defaults!/home/yellowtent/box/src/scripts/configurecollectd.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/configurecollectd.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
@@ -28,5 +28,9 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/update.sh
Defaults!/home/yellowtent/box/src/scripts/authorized_keys.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/authorized_keys.sh
Defaults!/home/yellowtent/box/src/scripts/node.sh env_keep="HOME BOX_ENV NODE_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/node.sh
Defaults!/home/yellowtent/box/src/scripts/configurelogrotate.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/configurelogrotate.sh
Defaults!/home/yellowtent/box/src/backuptask.js env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD:SETENV: /home/yellowtent/box/src/backuptask.js
+5 -3
View File
@@ -20,7 +20,6 @@ var appdb = require('./appdb.js'),
async = require('async'),
clients = require('./clients.js'),
config = require('./config.js'),
constants = require('./constants.js'),
ClientsError = clients.ClientsError,
debug = require('debug')('box:addons'),
docker = require('./docker.js'),
@@ -249,7 +248,7 @@ function setupOauth(app, options, callback) {
if (!app.sso) return callback(null);
var appId = app.id;
var redirectURI = 'https://' + config.appFqdn(app.location);
var redirectURI = 'https://' + (app.altDomain || config.appFqdn(app.location));
var scope = 'profile';
clients.delByAppIdAndType(appId, clients.TYPE_OAUTH, function (error) { // remove existing creds
@@ -364,6 +363,7 @@ function setupSendMail(app, options, callback) {
var env = [
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
{ name: 'MAIL_SMTP_PORT', value: '2525' },
{ name: 'MAIL_SMTPS_PORT', value: '4650' },
{ name: 'MAIL_SMTP_USERNAME', value: mailbox.name },
{ name: 'MAIL_SMTP_PASSWORD', value: password },
{ name: 'MAIL_FROM', value: mailbox.name + '@' + config.fqdn() },
@@ -645,7 +645,9 @@ function setupRedis(app, options, callback) {
}
const tag = infra.images.redis.tag, redisName = 'redis-' + app.id;
// note that we do not add appId label because this interferes with the stop/start app logic
const cmd = `docker run --restart=always -d --name=${redisName} \
--label=location=${app.location} \
--net cloudron \
--net-alias ${redisName} \
-m ${memoryLimit/2} \
@@ -692,7 +694,7 @@ function teardownRedis(app, options, callback) {
safe.fs.unlinkSync(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
shell.sudo('teardownRedis', [ RMAPPDIR_CMD, app.id + '/redis' ], function (error, stdout, stderr) {
shell.sudo('teardownRedis', [ RMAPPDIR_CMD, app.id + '/redis', true /* delete directory */ ], function (error, stdout, stderr) {
if (error) return callback(new Error('Error removing redis data:' + error));
appdb.unsetAddonConfig(app.id, 'redis', callback);
+31 -18
View File
@@ -10,6 +10,7 @@ exports = module.exports = {
update: update,
getAll: getAll,
getPortBindings: getPortBindings,
delPortBinding: delPortBinding,
setAddonConfig: setAddonConfig,
getAddonConfig: getAddonConfig,
@@ -59,8 +60,8 @@ 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.oldConfigJson', 'apps.memoryLimit', 'apps.altDomain',
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt' ].join(',');
'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit', 'apps.altDomain',
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
@@ -75,6 +76,14 @@ function postProcess(result) {
result.oldConfig = safe.JSON.parse(result.oldConfigJson);
delete result.oldConfigJson;
assert(result.updateConfigJson === null || typeof result.updateConfigJson === 'string');
result.updateConfig = safe.JSON.parse(result.updateConfigJson);
delete result.updateConfigJson;
assert(result.restoreConfigJson === null || typeof result.restoreConfigJson === 'string');
result.restoreConfig = safe.JSON.parse(result.restoreConfigJson);
delete result.restoreConfigJson;
assert(result.hostPorts === null || typeof result.hostPorts === 'string');
assert(result.environmentVariables === null || typeof result.environmentVariables === 'string');
@@ -98,6 +107,7 @@ function postProcess(result) {
result.xFrameOptions = result.xFrameOptions || 'SAMEORIGIN';
result.sso = !!result.sso; // make it bool
result.enableBackup = !!result.enableBackup; // make it bool
assert(result.debugModeJson === null || typeof result.debugModeJson === 'string');
result.debugMode = safe.JSON.parse(result.debugModeJson);
@@ -187,14 +197,14 @@ function add(id, appStoreId, manifest, location, portBindings, data, callback) {
var altDomain = data.altDomain || null;
var xFrameOptions = data.xFrameOptions || '';
var installationState = data.installationState || exports.ISTATE_PENDING_INSTALL;
var lastBackupId = data.lastBackupId || null; // used when cloning
var restoreConfigJson = data.restoreConfig ? JSON.stringify(data.restoreConfig) : null; // used when cloning
var sso = 'sso' in data ? data.sso : null;
var debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null;
var queries = [];
queries.push({
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, lastBackupId, sso, debugModeJson) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, lastBackupId, sso, debugModeJson ]
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, restoreConfigJson, sso, debugModeJson) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, restoreConfigJson, sso, debugModeJson ]
});
Object.keys(portBindings).forEach(function (env) {
@@ -247,6 +257,18 @@ function getPortBindings(id, callback) {
});
}
function delPortBinding(hostPort, callback) {
assert.strictEqual(typeof hostPort, 'number');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM appPortBindings WHERE hostPort=?', [ hostPort ], 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 del(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -304,17 +326,8 @@ function updateWithConstraints(id, app, constraints, callback) {
var fields = [ ], values = [ ];
for (var p in app) {
if (p === 'manifest') {
fields.push('manifestJson = ?');
values.push(JSON.stringify(app[p]));
} else if (p === 'oldConfig') {
fields.push('oldConfigJson = ?');
values.push(JSON.stringify(app[p]));
} else if (p === 'accessRestriction') {
fields.push('accessRestrictionJson = ?');
values.push(JSON.stringify(app[p]));
} else if (p === 'debugMode') {
fields.push('debugModeJson = ?');
if (p === 'manifest' || p === 'oldConfig' || p === 'updateConfig' || p === 'restoreConfig' || p === 'accessRestriction' || p === 'debugMode') {
fields.push(`${p}Json = ?`);
values.push(JSON.stringify(app[p]));
} else if (p !== 'portBindings') {
fields.push(p + ' = ?');
@@ -367,14 +380,14 @@ function setInstallationCommand(appId, installationState, values, callback) {
// Rules are:
// uninstall is allowed in any state
// force update is allowed in any state including pending_uninstall! (for better or worse)
// restore is allowed from installed or error state
// restore is allowed from installed or error state or currently restoring
// configure is allowed in installed state or currently configuring or in error state
// update and backup are allowed only in installed state
if (installationState === exports.ISTATE_PENDING_UNINSTALL || installationState === exports.ISTATE_PENDING_FORCE_UPDATE) {
updateWithConstraints(appId, values, '', callback);
} else if (installationState === exports.ISTATE_PENDING_RESTORE) {
updateWithConstraints(appId, values, 'AND (installationState = "installed" OR installationState = "error")', callback);
updateWithConstraints(appId, values, 'AND (installationState = "installed" OR installationState = "error" OR installationState = "pending_restore")', callback);
} else if (installationState === exports.ISTATE_PENDING_UPDATE || installationState === exports.ISTATE_PENDING_BACKUP) {
updateWithConstraints(appId, values, 'AND installationState = "installed"', callback);
} else if (installationState === exports.ISTATE_PENDING_CONFIGURE) {
+11 -11
View File
@@ -94,19 +94,20 @@ function checkAppHealth(app, callback) {
superagent
.get(healthCheckUrl)
.set('Host', app.fqdn) // required for some apache configs with rewrite rules
.set('User-Agent', 'Mozilla') // required for some apps (e.g. minio)
.redirects(0)
.timeout(HEALTHCHECK_INTERVAL)
.end(function (error, res) {
if (error && !error.response) {
debugApp(app, 'not alive (network error): %s', error.message);
setHealth(app, appdb.HEALTH_UNHEALTHY, callback);
} else if (res.statusCode >= 400) { // 2xx and 3xx are ok
debugApp(app, 'not alive : %s', error || res.status);
setHealth(app, appdb.HEALTH_UNHEALTHY, callback);
} else {
setHealth(app, appdb.HEALTH_HEALTHY, callback);
}
});
if (error && !error.response) {
debugApp(app, 'not alive (network error): %s', error.message);
setHealth(app, appdb.HEALTH_UNHEALTHY, callback);
} else if (res.statusCode >= 400) { // 2xx and 3xx are ok
debugApp(app, 'not alive : %s', error || res.status);
setHealth(app, appdb.HEALTH_UNHEALTHY, callback);
} else {
setHealth(app, appdb.HEALTH_HEALTHY, callback);
}
});
});
}
@@ -156,7 +157,6 @@ function processDockerEvents() {
stream.setEncoding('utf8');
stream.on('data', function (data) {
var ev = JSON.parse(data);
debug('Container ' + ev.id + ' went OOM');
appdb.getByContainerId(ev.id, function (error, app) { // this can error for addons
var program = error || !app.appStoreId ? ev.id : app.appStoreId;
var context = JSON.stringify(ev);
+150 -52
View File
@@ -30,13 +30,16 @@ exports = module.exports = {
checkManifestConstraints: checkManifestConstraints,
updateApps: updateApps,
autoupdateApps: autoupdateApps,
restoreInstalledApps: restoreInstalledApps,
configureInstalledApps: configureInstalledApps,
getAppConfig: getAppConfig,
downloadFile: downloadFile,
uploadFile: uploadFile,
// exported for testing
_validateHostname: validateHostname,
_validatePortBindings: validatePortBindings,
@@ -62,6 +65,7 @@ var addons = require('./addons.js'),
groups = require('./groups.js'),
mailboxdb = require('./mailboxdb.js'),
manifestFormat = require('cloudron-manifestformat'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
@@ -70,10 +74,11 @@ var addons = require('./addons.js'),
split = require('split'),
superagent = require('superagent'),
taskmanager = require('./taskmanager.js'),
TransformStream = require('stream').Transform,
updateChecker = require('./updatechecker.js'),
url = require('url'),
util = require('util'),
uuid = require('node-uuid'),
uuid = require('uuid'),
validator = require('validator');
// http://dustinsenos.com/articles/customErrorsInNode
@@ -114,7 +119,7 @@ AppsError.BAD_CERTIFICATE = 'Invalid certificate';
// https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
// We are validating the validity of the location-fqdn as host name
function validateHostname(location, fqdn) {
var RESERVED_LOCATIONS = [ constants.ADMIN_LOCATION, constants.API_LOCATION, constants.SMTP_LOCATION, constants.IMAP_LOCATION, constants.MAIL_LOCATION, constants.POSTMAN_LOCATION ];
var RESERVED_LOCATIONS = [ config.adminLocation(), constants.API_LOCATION, constants.SMTP_LOCATION, constants.IMAP_LOCATION, config.mailLocation(), constants.POSTMAN_LOCATION ];
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new AppsError(AppsError.BAD_FIELD, location + ' is reserved');
@@ -204,7 +209,7 @@ function validateMemoryLimit(manifest, memoryLimit) {
assert.strictEqual(typeof memoryLimit, 'number');
var min = manifest.memoryLimit || constants.DEFAULT_MEMORY_LIMIT;
var max = (4096 * 1024 * 1024);
var max = os.totalmem() * 2; // this will overallocate since we don't allocate equal swap always (#466)
// 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
@@ -254,6 +259,12 @@ function validateRobotsTxt(robotsTxt) {
return null;
}
function validateBackupFormat(format) {
if (format === 'tgz' || format == 'rsync') return null;
return new AppsError(AppsError.BAD_FIELD, 'Invalid backup format');
}
function getDuplicateErrorDetails(location, portBindings, error) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof portBindings, 'object');
@@ -304,12 +315,18 @@ function hasAccessTo(app, user, callback) {
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);
groups.getGroups(user.id, function (error, groupIds) {
if (error) return callback(null, false);
async.some(app.accessRestriction.groups, function (groupId, iteratorDone) {
groups.isMember(groupId, user.id, iteratorDone);
}, function (error, result) {
callback(null, !error && result);
const isAdmin = groupIds.indexOf(constants.ADMIN_GROUP_ID) !== -1;
if (isAdmin) return callback(null, true); // admins can always access any app
if (!app.accessRestriction.groups) return callback(null, false);
if (app.accessRestriction.groups.some(function (gid) { return groupIds.indexOf(gid) !== -1; })) return callback(null, true);
callback(null, false);
});
}
@@ -415,7 +432,9 @@ function install(data, auditSource, callback) {
sso = 'sso' in data ? data.sso : null,
debugMode = data.debugMode || null,
robotsTxt = data.robotsTxt || null,
backupId = data.backupId || null;
enableBackup = 'enableBackup' in data ? data.enableBackup : true,
backupId = data.backupId || null,
backupFormat = data.backupFormat || 'tgz';
assert(data.appStoreId || data.manifest); // atleast one of them is required
@@ -449,6 +468,9 @@ function install(data, auditSource, callback) {
error = validateRobotsTxt(robotsTxt);
if (error) return callback(error);
error = validateBackupFormat(backupFormat);
if (error) return callback(error);
if ('sso' in data && !('optionalSso' in manifest)) return callback(new AppsError(AppsError.BAD_FIELD, 'sso can only be specified for apps with optionalSso'));
// if sso was unspecified, enable it by default if possible
if (sso === null) sso = !!manifest.addons['ldap'] || !!manifest.addons['oauth'];
@@ -484,7 +506,9 @@ function install(data, auditSource, callback) {
sso: sso,
debugMode: debugMode,
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app',
lastBackupId: backupId
restoreConfig: backupId ? { backupId: backupId, backupFormat: backupFormat } : null,
enableBackup: enableBackup,
robotsTxt: robotsTxt
};
appdb.add(appId, appStoreId, manifest, location, portBindings, data, function (error) {
@@ -583,6 +607,8 @@ function configure(appId, data, auditSource, callback) {
}
}
if ('enableBackup' in data) values.enableBackup = data.enableBackup;
values.oldConfig = getAppConfig(app);
debug('Will configure app with id:%s values:%j', appId, values);
@@ -620,7 +646,7 @@ function update(appId, data, auditSource, callback) {
downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
if (error) return callback(error);
var values = { };
var updateConfig = { };
error = manifestFormat.parse(manifest);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest error:' + error.message));
@@ -628,13 +654,7 @@ function update(appId, data, auditSource, callback) {
error = checkManifestConstraints(manifest);
if (error) return callback(error);
values.manifest = manifest;
if ('portBindings' in data) {
values.portBindings = data.portBindings;
error = validatePortBindings(data.portBindings, values.manifest.tcpPorts);
if (error) return callback(error);
}
updateConfig.manifest = manifest;
if ('icon' in data) {
if (data.icon) {
@@ -654,26 +674,23 @@ function update(appId, data, auditSource, callback) {
// 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 !== values.manifest.id) {
if (app.manifest.id !== updateConfig.manifest.id) {
if (!data.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
values.appStoreId = '';
updateConfig.appStoreId = '';
}
// do not update apps in debug mode
if (app.debugMode && !data.force) return callback(new AppsError(AppsError.BAD_STATE, 'debug mode enabled. force to override'));
// Ensure we update the memory limit in case the new app requires more memory as a minimum
// 0 and -1 are special values for memory limit indicating unset and unlimited
if (app.memoryLimit > 0 && values.manifest.memoryLimit && app.memoryLimit < values.manifest.memoryLimit) {
values.memoryLimit = values.manifest.memoryLimit;
// 0 and -1 are special updateConfig for memory limit indicating unset and unlimited
if (app.memoryLimit > 0 && updateConfig.manifest.memoryLimit && app.memoryLimit < updateConfig.manifest.memoryLimit) {
updateConfig.memoryLimit = updateConfig.manifest.memoryLimit;
}
values.oldConfig = getAppConfig(app);
appdb.setInstallationCommand(appId, data.force ? appdb.ISTATE_PENDING_FORCE_UPDATE : appdb.ISTATE_PENDING_UPDATE, values, function (error) {
appdb.setInstallationCommand(appId, data.force ? appdb.ISTATE_PENDING_FORCE_UPDATE : appdb.ISTATE_PENDING_UPDATE, { updateConfig: updateConfig }, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails('' /* location cannot conflict */, values.portBindings, error));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
taskmanager.restartAppTask(appId);
@@ -754,22 +771,22 @@ function restore(appId, data, auditSource, callback) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
// for empty or null backupId, use existing manifest to mimic a reinstall
var func = data.backupId ? backups.getRestoreConfig.bind(null, data.backupId) : function (next) { return next(null, { manifest: app.manifest }); };
var func = data.backupId ? backups.get.bind(null, data.backupId) : function (next) { return next(null, { manifest: app.manifest }); };
func(function (error, restoreConfig) {
func(function (error, backupInfo) {
if (error && error.reason === BackupsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
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));
if (!restoreConfig) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore config'));
if (!backupInfo.manifest) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore manifest'));
// re-validate because this new box version may not accept old configs
error = checkManifestConstraints(restoreConfig.manifest);
error = checkManifestConstraints(backupInfo.manifest);
if (error) return callback(error);
var values = {
lastBackupId: data.backupId || null, // when null, apptask simply reinstalls
manifest: restoreConfig.manifest,
restoreConfig: data.backupId ? { backupId: data.backupId, backupFormat: backupInfo.format } : null, // when null, apptask simply reinstalls
manifest: backupInfo.manifest,
oldConfig: getAppConfig(app)
};
@@ -808,24 +825,24 @@ function clone(appId, data, auditSource, callback) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
backups.getRestoreConfig(backupId, function (error, restoreConfig) {
backups.get(backupId, function (error, backupInfo) {
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
if (error && error.reason === BackupsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!restoreConfig) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore config'));
if (!backupInfo.manifest) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore config'));
// re-validate because this new box version may not accept old configs
error = checkManifestConstraints(restoreConfig.manifest);
error = checkManifestConstraints(backupInfo.manifest);
if (error) return callback(error);
error = validateHostname(location, config.fqdn());
if (error) return callback(error);
error = validatePortBindings(portBindings, restoreConfig.manifest.tcpPorts);
error = validatePortBindings(portBindings, backupInfo.manifest.tcpPorts);
if (error) return callback(error);
var newAppId = uuid.v4(), appStoreId = app.appStoreId, manifest = restoreConfig.manifest;
var newAppId = uuid.v4(), appStoreId = app.appStoreId, manifest = backupInfo.manifest;
appstore.purchase(newAppId, appStoreId, function (error) {
if (error && error.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
@@ -838,7 +855,7 @@ function clone(appId, data, auditSource, callback) {
memoryLimit: app.memoryLimit,
accessRestriction: app.accessRestriction,
xFrameOptions: app.xFrameOptions,
lastBackupId: backupId,
restoreConfig: { backupId: backupId, backupFormat: backupInfo.format },
sso: !!app.sso,
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app'
};
@@ -995,23 +1012,18 @@ function exec(appId, options, callback) {
});
}
function updateApps(updateInfo, auditSource, callback) { // updateInfo is { appId -> { manifest } }
function autoupdateApps(updateInfo, auditSource, callback) { // updateInfo is { appId -> { manifest } }
assert.strictEqual(typeof updateInfo, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
function canAutoupdateApp(app, newManifest) {
if ((semver.major(app.manifest.version) !== 0) && (semver.major(app.manifest.version) !== semver.major(newManifest.version))) return false; // major changes are blocking
if ((semver.major(app.manifest.version) !== 0) && (semver.major(app.manifest.version) !== semver.major(newManifest.version))) return new Error('Major version change'); // major changes are blocking
var newTcpPorts = newManifest.tcpPorts || { };
var oldTcpPorts = app.manifest.tcpPorts || { };
var portBindings = app.portBindings; // this is never null
for (var env in newTcpPorts) {
if (!(env in oldTcpPorts)) return new Error(env + ' is required from user');
}
for (env in portBindings) {
for (var env in portBindings) {
if (!(env in newTcpPorts)) return new Error(env + ' was in use but new update removes it');
}
@@ -1026,7 +1038,7 @@ function updateApps(updateInfo, auditSource, callback) { // updateInfo is { appI
if (error) {
debug('Cannot autoupdate app %s : %s', appId, error.message);
return iteratorDone();
}
}
error = canAutoupdateApp(app, updateInfo[appId].manifest);
if (error) {
@@ -1096,10 +1108,14 @@ function restoreInstalledApps(callback) {
async.map(apps, function (app, iteratorDone) {
debug('marking %s for restore', app.location || app.id);
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_RESTORE, { oldConfig: null }, function (error) {
if (error) debug('did not mark %s for restore', app.location || app.id, error);
backups.getByAppIdPaged(1, 1, app.id, function (error, results) {
var restoreConfig = !error && results.length ? { backupId: results[0].id, backupFormat: results[0].format } : null;
iteratorDone(); // always succeed
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_RESTORE, { restoreConfig: restoreConfig, oldConfig: null }, function (error) {
if (error) debug('did not mark %s for restore', app.location || app.id, error);
iteratorDone(); // always succeed
});
});
}, callback);
});
@@ -1122,3 +1138,85 @@ function configureInstalledApps(callback) {
}, callback);
});
}
function downloadFile(appId, filePath, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof filePath, 'string');
assert.strictEqual(typeof callback, 'function');
exec(appId, { cmd: [ 'stat', '--printf=%F-%s', filePath ], tty: true }, function (error, stream) {
if (error) return callback(error);
var data = '';
stream.setEncoding('utf8');
stream.on('data', function (d) { data += d; });
stream.on('end', function () {
var parts = data.split('-');
if (parts.length !== 2) return callback(new AppsError(AppsError.NOT_FOUND, 'file does not exist'));
var type = parts[0], filename, cmd, size;
if (type === 'regular file') {
cmd = [ 'cat', filePath ];
size = parseInt(parts[1], 10);
filename = path.basename(filePath);
if (isNaN(size)) return callback(new AppsError(AppsError.NOT_FOUND, 'file does not exist'));
} else if (type === 'directory') {
cmd = ['tar', 'zcf', '-', '-C', filePath, '.'];
filename = path.basename(filePath) + '.tar.gz';
size = 0; // unknown
} else {
return callback(new AppsError(AppsError.NOT_FOUND, 'only files or dirs can be downloaded'));
}
exec(appId, { cmd: cmd , tty: false }, function (error, stream) {
if (error) return callback(error);
var stdoutStream = new TransformStream({
transform: function (chunk, ignoredEncoding, callback) {
this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
while (true) {
if (this._buffer.length < 8) break; // header is 8 bytes
var type = this._buffer.readUInt8(0);
var len = this._buffer.readUInt32BE(4);
if (this._buffer.length < (8 + len)) break; // not enough
var payload = this._buffer.slice(8, 8 + len);
this._buffer = this._buffer.slice(8+len); // consumed
if (type === 1) this.push(payload);
}
callback();
}
});
stream.pipe(stdoutStream);
return callback(null, stdoutStream, { filename: filename, size: size });
});
});
});
}
function uploadFile(appId, sourceFilePath, destFilePath, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof sourceFilePath, 'string');
assert.strictEqual(typeof destFilePath, 'string');
assert.strictEqual(typeof callback, 'function');
exec(appId, { cmd: [ 'bash', '-c', 'cat - > ' + destFilePath ], tty: false }, function (error, stream) {
if (error) return callback(error);
var readFile = fs.createReadStream(sourceFilePath);
readFile.on('error', callback);
readFile.pipe(stream);
callback(null);
});
}
+54 -7
View File
@@ -11,6 +11,10 @@ exports = module.exports = {
getAppUpdate: getAppUpdate,
getBoxUpdate: getBoxUpdate,
getAccount: getAccount,
sendFeedback: sendFeedback,
AppstoreError: AppstoreError
};
@@ -162,16 +166,17 @@ function sendAliveStatus(data, callback) {
provider: result[settings.TLS_CONFIG_KEY].provider
},
backupConfig: {
provider: result[settings.BACKUP_CONFIG_KEY].provider
provider: result[settings.BACKUP_CONFIG_KEY].provider,
hardlinks: !result[settings.BACKUP_CONFIG_KEY].noHardlinks
},
mailConfig: {
enabled: result[settings.MAIL_CONFIG_KEY].enabled
},
mailRelay: {
provider: result[settings.MAIL_RELAY_KEY].provider
},
mailCatchAll: {
count: result[settings.CATCH_ALL_ADDRESS_KEY].length
},
mailRelay: {
provider: result[settings.MAIL_RELAY_KEY].provider
},
mailCatchAll: {
count: result[settings.CATCH_ALL_ADDRESS_KEY].length
},
autoupdatePattern: result[settings.AUTOUPDATE_PATTERN_KEY],
timeZone: result[settings.TIME_ZONE_KEY],
@@ -180,6 +185,7 @@ function sendAliveStatus(data, callback) {
var data = {
domain: config.fqdn(),
version: config.version(),
adminFqdn: config.adminFqdn(),
provider: config.provider(),
backendSettings: backendSettings,
machine: {
@@ -245,3 +251,44 @@ function getAppUpdate(app, callback) {
});
});
}
function getAccount(callback) {
assert.strictEqual(typeof callback, 'function');
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId;
superagent.get(url).query({ accessToken: appstoreConfig.token }).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
// { profile: { id, email, groupId, billing, firstName, lastName, company, street, city, zip, state, country } }
callback(null, result.body.profile);
});
});
}
function sendFeedback(info, callback) {
assert.strictEqual(typeof info, 'object');
assert.strictEqual(typeof info.email, 'string');
assert.strictEqual(typeof info.displayName, 'string');
assert.strictEqual(typeof info.type, 'string');
assert.strictEqual(typeof info.subject, 'string');
assert.strictEqual(typeof info.description, 'string');
assert.strictEqual(typeof callback, 'function');
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/feedback';
superagent.post(url).query({ accessToken: appstoreConfig.token }).send(info).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
callback(null);
});
});
}
+127 -50
View File
@@ -35,6 +35,7 @@ var addons = require('./addons.js'),
certificates = require('./certificates.js'),
config = require('./config.js'),
database = require('./database.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:apptask'),
docker = require('./docker.js'),
ejs = require('ejs'),
@@ -42,6 +43,7 @@ var addons = require('./addons.js'),
manifestFormat = require('cloudron-manifestformat'),
net = require('net'),
nginx = require('./nginx.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
@@ -55,7 +57,9 @@ var addons = require('./addons.js'),
_ = require('underscore');
var COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd.config.ejs', { encoding: 'utf8' }),
RELOAD_COLLECTD_CMD = path.join(__dirname, 'scripts/reloadcollectd.sh'),
CONFIGURE_COLLECTD_CMD = path.join(__dirname, 'scripts/configurecollectd.sh'),
LOGROTATE_CONFIG_EJS = fs.readFileSync(__dirname + '/logrotate.ejs', { encoding: 'utf8' }),
CONFIGURE_LOGROTATE_CMD = path.join(__dirname, 'scripts/configurelogrotate.sh'),
RMAPPDIR_CMD = path.join(__dirname, 'scripts/rmappdir.sh'),
CREATEAPPDIR_CMD = path.join(__dirname, 'scripts/createappdir.sh');
@@ -127,7 +131,7 @@ function deleteContainers(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'deleting containers');
debugApp(app, 'deleting app containers (app, scheduler)');
docker.deleteContainers(app.id, function (error) {
if (error) return callback(new Error('Error deleting container: ' + error));
@@ -143,11 +147,12 @@ function createVolume(app, callback) {
shell.sudo('createVolume', [ CREATEAPPDIR_CMD, app.id ], callback);
}
function deleteVolume(app, callback) {
function deleteVolume(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
shell.sudo('deleteVolume', [ RMAPPDIR_CMD, app.id ], callback);
shell.sudo('deleteVolume', [ RMAPPDIR_CMD, app.id, !!options.removeDirectory ], callback);
}
function addCollectdProfile(app, callback) {
@@ -157,7 +162,7 @@ function addCollectdProfile(app, callback) {
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);
shell.sudo('addCollectdProfile', [ RELOAD_COLLECTD_CMD ], callback);
shell.sudo('addCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'add', app.id ], callback);
});
}
@@ -167,17 +172,41 @@ function removeCollectdProfile(app, callback) {
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);
shell.sudo('removeCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'remove', app.id ], callback);
});
}
function verifyManifest(app, callback) {
function addLogrotateConfig(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Verifying manifest');
docker.inspect(app.containerId, function (error, result) {
if (error) return callback(error);
var runVolume = result.Mounts.find(function (mount) { return mount.Destination === '/run'; });
if (!runVolume) return callback(new Error('App does not have /run mounted'));
// logrotate configs can have arbitrary commands, so the config files must be owned by root
var logrotateConf = ejs.render(LOGROTATE_CONFIG_EJS, { volumePath: runVolume.Source });
var tmpFilePath = path.join(os.tmpdir(), app.id + '.logrotate');
fs.writeFile(tmpFilePath, logrotateConf, function (error) {
if (error) return callback(error);
shell.sudo('addLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'add', app.id, tmpFilePath ], callback);
});
});
}
function removeLogrotateConfig(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
shell.sudo('removeLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'remove', app.id ], callback);
}
function verifyManifest(manifest, callback) {
assert.strictEqual(typeof manifest, 'object');
assert.strictEqual(typeof callback, 'function');
var manifest = app.manifest;
var error = manifestFormat.parse(manifest);
if (error) return callback(new Error(util.format('Manifest error: %s', error.message)));
@@ -210,7 +239,7 @@ function downloadIcon(app, callback) {
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, app.id + '.png'), res.body)) return retryCallback(new Error('Error saving icon:' + safe.error.message));
retryCallback(null);
});
});
}, callback);
}
@@ -242,6 +271,7 @@ function registerSubdomain(app, overwrite, callback) {
}, function (error, result) {
if (error || result instanceof Error) return callback(error || result);
// dnsRecordId tracks whether we created this DNS record so that we can unregister later
updateApp(app, { dnsRecordId: result }, callback);
});
});
@@ -258,6 +288,11 @@ function unregisterSubdomain(app, location, callback) {
return callback(null);
}
if (!app.dnsRecordId) {
debugApp(app, 'Skip unregister of record not created by cloudron');
return callback(null);
}
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
@@ -349,30 +384,34 @@ function updateApp(app, values, callback) {
// - setup addons (requires the above volume)
// - setup the container (requires image, volumes, addons)
// - setup collectd (requires container id)
// restore is also handled here since restore is just an install with some oldConfig to clean up
function install(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
const backupId = app.lastBackupId, isRestoring = app.installationState === appdb.ISTATE_PENDING_RESTORE;
const restoreConfig = app.restoreConfig, isRestoring = app.installationState === appdb.ISTATE_PENDING_RESTORE;
async.series([
verifyManifest.bind(null, app),
// this protects against the theoretical possibility of an app being marked for install/restore from
// a previous version of box code
verifyManifest.bind(null, app.manifest),
// teardown for re-installs
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
unconfigureNginx.bind(null, app),
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
deleteContainers.bind(null, app),
// oldConfig can be null during upgrades
addons.teardownAddons.bind(null, app, app.oldConfig ? app.oldConfig.manifest.addons : app.manifest.addons),
deleteVolume.bind(null, app),
deleteVolume.bind(null, app, { removeDirectory: false }), // do not remove any symlinked volume
// for restore case
function deleteImageIfChanged(done) {
if (!app.oldConfig || (app.oldConfig.manifest.dockerImage === app.manifest.dockerImage)) return done();
if (!app.oldConfig || (app.oldConfig.manifest.dockerImage === app.manifest.dockerImage)) return done();
docker.deleteImage(app.oldConfig.manifest, done);
docker.deleteImage(app.oldConfig.manifest, done);
},
reserveHttpPort.bind(null, app),
@@ -390,7 +429,7 @@ function install(app, callback) {
createVolume.bind(null, app),
function restoreFromBackup(next) {
if (!backupId) {
if (!restoreConfig) {
async.series([
updateApp.bind(null, app, { installationProgress: '60, Setting up addons' }),
addons.setupAddons.bind(null, app, app.manifest.addons),
@@ -398,7 +437,7 @@ function install(app, callback) {
} else {
async.series([
updateApp.bind(null, app, { installationProgress: '60, Download backup and restoring addons' }),
backups.restoreApp.bind(null, app, app.manifest.addons, backupId),
backups.restoreApp.bind(null, app, app.manifest.addons, restoreConfig),
], next);
}
},
@@ -406,6 +445,9 @@ function install(app, callback) {
updateApp.bind(null, app, { installationProgress: '70, Creating container' }),
createContainer.bind(null, app),
updateApp.bind(null, app, { installationProgress: '75, Setting up logrotate config' }),
addLogrotateConfig.bind(null, app),
updateApp.bind(null, app, { installationProgress: '80, Setting up collectd profile' }),
addCollectdProfile.bind(null, app),
@@ -415,7 +457,7 @@ function install(app, callback) {
exports._waitForDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Waiting for External Domain setup' }),
exports._waitForAltDomainDnsPropagation.bind(null, app), // required when restoring and !lastBackupId
exports._waitForAltDomainDnsPropagation.bind(null, app), // required when restoring and !restoreConfig
updateApp.bind(null, app, { installationProgress: '95, Configure nginx' }),
configureNginx.bind(null, app),
@@ -438,11 +480,9 @@ function backup(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var prefix = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
async.series([
updateApp.bind(null, app, { installationProgress: '10, Backing up' }),
backups.backupApp.bind(null, app, app.manifest, prefix),
backups.backupApp.bind(null, app, app.manifest),
// done!
function (callback) {
@@ -463,15 +503,18 @@ function configure(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
// oldConfig can be null during an infra update
var locationChanged = app.oldConfig && app.oldConfig.location !== app.location;
async.series([
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
unconfigureNginx.bind(null, app),
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
deleteContainers.bind(null, app),
function (next) {
// oldConfig can be null during an infra update
if (!app.oldConfig || app.oldConfig.location === app.location) return next();
if (!locationChanged) return next();
unregisterSubdomain(app, app.oldConfig.location, next);
},
@@ -481,7 +524,7 @@ function configure(app, callback) {
downloadIcon.bind(null, app),
updateApp.bind(null, app, { installationProgress: '35, Registering subdomain' }),
registerSubdomain.bind(null, app, true /* overwrite */),
registerSubdomain.bind(null, app, !locationChanged /* overwrite */), // if location changed, do not overwrite to detect conflicts
updateApp.bind(null, app, { installationProgress: '40, Downloading image' }),
docker.downloadImage.bind(null, app.manifest),
@@ -496,6 +539,9 @@ function configure(app, callback) {
updateApp.bind(null, app, { installationProgress: '60, Creating container' }),
createContainer.bind(null, app),
updateApp.bind(null, app, { installationProgress: '65, Setting up logrotate config' }),
addLogrotateConfig.bind(null, app),
updateApp.bind(null, app, { installationProgress: '70, Add collectd profile' }),
addCollectdProfile.bind(null, app),
@@ -529,47 +575,72 @@ function update(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Updating to %s', safe.query(app, 'manifest.version'));
debugApp(app, `Updating to ${app.updateConfig.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));
var unusedAddons = _.omit(app.manifest.addons, Object.keys(app.updateConfig.manifest.addons));
async.series([
// this protects against the theoretical possibility of an app being marked for update from
// a previous version of box code
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: '25, Cleaning up old install' }),
removeCollectdProfile.bind(null, app),
stopApp.bind(null, app),
deleteContainers.bind(null, app),
function deleteImageIfChanged(done) {
if (app.oldConfig.manifest.dockerImage === app.manifest.dockerImage) return done();
docker.deleteImage(app.oldConfig.manifest, done);
},
verifyManifest.bind(null, app.updateConfig.manifest),
function (next) {
if (app.installationState === appdb.ISTATE_PENDING_FORCE_UPDATE) return next(null);
var prefix = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
async.series([
updateApp.bind(null, app, { installationProgress: '30, Backing up app' }),
backups.backupApp.bind(null, app, app.oldConfig.manifest, prefix)
updateApp.bind(null, app, { installationProgress: '15, Backing up app' }),
backups.backupApp.bind(null, app, app.manifest)
], next);
},
// 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: '25, Downloading image' }),
docker.downloadImage.bind(null, app.updateConfig.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: '35, Cleaning up old install' }),
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
deleteContainers.bind(null, app),
function deleteImageIfChanged(done) {
if (app.manifest.dockerImage === app.updateConfig.manifest.dockerImage) return done();
docker.deleteImage(app.manifest, done);
},
// only delete unused addons after backup
addons.teardownAddons.bind(null, app, unusedAddons),
// free unused ports
function (next) {
// make sure we always have objects
var currentPorts = app.portBindings || {};
var newPorts = app.updateConfig.manifest.tcpPorts || {};
async.each(Object.keys(currentPorts), function (portName, callback) {
if (newPorts[portName]) return callback(); // port still in use
appdb.delPortBinding(currentPorts[portName], function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) console.error('Portbinding does not exist in database.');
else if (error) return next(error);
// also delete from app object for further processing (the db is updated in the next step)
delete app.portBindings[portName];
callback();
});
}, next);
},
// switch over to the new config. manifest, memoryLimit, portBindings, appstoreId are updated here
updateApp.bind(null, app, app.updateConfig),
updateApp.bind(null, app, { installationProgress: '45, Downloading icon' }),
downloadIcon.bind(null, app),
@@ -579,6 +650,9 @@ function update(app, callback) {
updateApp.bind(null, app, { installationProgress: '80, Creating container' }),
createContainer.bind(null, app),
updateApp.bind(null, app, { installationProgress: '85, Setting up logrotate config' }),
addLogrotateConfig.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Add collectd profile' }),
addCollectdProfile.bind(null, app),
@@ -587,7 +661,7 @@ function update(app, callback) {
// done!
function (callback) {
debugApp(app, 'updated');
updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '', health: null }, callback);
updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '', health: null, updateConfig: null }, callback);
}
], function seriesDone(error) {
if (error) {
@@ -608,6 +682,9 @@ function uninstall(app, callback) {
updateApp.bind(null, app, { installationProgress: '0, Remove collectd profile' }),
removeCollectdProfile.bind(null, app),
updateApp.bind(null, app, { installationProgress: '5, Remove logrotate config' }),
removeLogrotateConfig.bind(null, app),
updateApp.bind(null, app, { installationProgress: '10, Stopping app' }),
stopApp.bind(null, app),
@@ -618,7 +695,7 @@ function uninstall(app, callback) {
addons.teardownAddons.bind(null, app, app.manifest.addons),
updateApp.bind(null, app, { installationProgress: '40, Deleting volume' }),
deleteVolume.bind(null, app),
deleteVolume.bind(null, app, { removeDirectory: true }),
updateApp.bind(null, app, { installationProgress: '50, Deleting image' }),
docker.deleteImage.bind(null, app.manifest),
+35 -28
View File
@@ -2,7 +2,9 @@
exports = module.exports = {
initialize: initialize,
uninitialize: uninitialize
uninitialize: uninitialize,
accessTokenAuth: accessTokenAuth
};
var assert = require('assert'),
@@ -23,22 +25,22 @@ var assert = require('assert'),
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
passport.serializeUser(function (user, callback) {
callback(null, user.id);
});
passport.deserializeUser(function(userId, callback) {
user.get(userId, function (error, result) {
if (error) return callback(error);
var md5 = crypto.createHash('md5').update(result.alternateEmail || result.email).digest('hex');
result.gravatar = 'https://www.gravatar.com/avatar/' + md5 + '.jpg?s=24&d=mm';
callback(null, result);
});
});
passport.use(new LocalStrategy(function (username, password, callback) {
if (username.indexOf('@') === -1) {
user.verifyWithUsername(username, password, function (error, result) {
@@ -58,7 +60,7 @@ function initialize(callback) {
});
}
}));
passport.use(new BasicStrategy(function (username, password, callback) {
if (username.indexOf('cid-') === 0) {
debug('BasicStrategy: detected client id %s instead of username:password', username);
@@ -80,7 +82,7 @@ function initialize(callback) {
});
}
}));
passport.use(new ClientPasswordStrategy(function (clientId, clientSecret, callback) {
clients.get(clientId, function(error, client) {
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
@@ -89,30 +91,35 @@ function initialize(callback) {
return callback(null, client);
});
}));
passport.use(new BearerStrategy(function (accessToken, callback) {
tokendb.get(accessToken, function (error, token) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
// scopes here can define what capabilities that token carries
// passport put the 'info' object into req.authInfo, where we can further validate the scopes
var info = { scope: token.scope };
user.get(token.identifier, function (error, user) {
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
callback(null, user, info);
});
});
}));
passport.use(new BearerStrategy(accessTokenAuth));
callback(null);
}
function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
callback(null);
}
function accessTokenAuth(accessToken, callback) {
assert.strictEqual(typeof accessToken, 'string');
assert.strictEqual(typeof callback, 'function');
tokendb.get(accessToken, function (error, token) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
// scopes here can define what capabilities that token carries
// passport put the 'info' object into req.authInfo, where we can further validate the scopes
var info = { scope: token.scope };
user.get(token.identifier, function (error, user) {
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
callback(null, user, info);
});
});
}
+32 -42
View File
@@ -6,7 +6,7 @@ var assert = require('assert'),
safe = require('safetydance'),
util = require('util');
var BACKUPS_FIELDS = [ 'id', 'creationTime', 'version', 'type', 'dependsOn', 'state', 'restoreConfigJson' ];
var BACKUPS_FIELDS = [ 'id', 'creationTime', 'version', 'type', 'dependsOn', 'state', 'manifestJson', 'format' ];
exports = module.exports = {
add: add,
@@ -34,8 +34,8 @@ function postProcess(result) {
result.dependsOn = result.dependsOn ? result.dependsOn.split(',') : [ ];
result.restoreConfig = result.restoreConfigJson ? safe.JSON.parse(result.restoreConfigJson) : null;
delete result.restoreConfigJson;
result.manifest = result.manifestJson ? safe.JSON.parse(result.manifestJson) : null;
delete result.manifestJson;
}
function getByTypeAndStatePaged(type, state, page, perPage, callback) {
@@ -47,12 +47,12 @@ function getByTypeAndStatePaged(type, state, page, perPage, callback) {
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? AND state = ? ORDER BY creationTime DESC LIMIT ?,?',
[ type, state, (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(function (result) { postProcess(result); });
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
callback(null, results);
});
}
function getByTypePaged(type, page, perPage, callback) {
@@ -63,12 +63,12 @@ function getByTypePaged(type, page, perPage, callback) {
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? ORDER BY creationTime DESC LIMIT ?,?',
[ type, (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(function (result) { postProcess(result); });
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
callback(null, results);
});
}
function getByAppIdPaged(page, perPage, appId, callback) {
@@ -80,12 +80,12 @@ function getByAppIdPaged(page, perPage, appId, callback) {
// box versions (0.93.x and below) used to use appbackup_ prefix
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? AND state = ? AND id LIKE ? ORDER BY creationTime DESC LIMIT ?,?',
[ exports.BACKUP_TYPE_APP, exports.BACKUP_STATE_NORMAL, '%app%\\_' + appId + '\\_%', (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(function (result) { postProcess(result); });
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
callback(null, results);
});
}
function get(id, callback) {
@@ -94,13 +94,13 @@ function get(id, callback) {
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE id = ? ORDER BY creationTime DESC',
[ id ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
postProcess(result[0]);
postProcess(result[0]);
callback(null, result[0]);
});
callback(null, result[0]);
});
}
function add(backup, callback) {
@@ -109,20 +109,21 @@ function add(backup, callback) {
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 backup.restoreConfig, 'object');
assert.strictEqual(typeof backup.manifest, 'object');
assert.strictEqual(typeof backup.format, 'string');
assert.strictEqual(typeof callback, 'function');
var creationTime = backup.creationTime || new Date(); // allow tests to set the time
var restoreConfig = backup.restoreConfig ? JSON.stringify(backup.restoreConfig) : '';
var manifestJson = JSON.stringify(backup.manifest);
database.query('INSERT INTO backups (id, version, type, creationTime, state, dependsOn, restoreConfigJson) VALUES (?, ?, ?, ?, ?, ?, ?)',
[ backup.id, backup.version, backup.type, creationTime, exports.BACKUP_STATE_NORMAL, backup.dependsOn.join(','), restoreConfig ],
database.query('INSERT INTO backups (id, version, type, creationTime, state, dependsOn, manifestJson, format) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[ backup.id, backup.version, backup.type, creationTime, exports.BACKUP_STATE_NORMAL, backup.dependsOn.join(','), manifestJson, backup.format ],
function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
callback(null);
});
}
function update(id, backup, callback) {
@@ -158,19 +159,8 @@ function del(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
get(id, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback();
if (error) return callback(error);
var whereClause = [ 'id=?' ], whereArgs = [ result.id ];
result.dependsOn.forEach(function (id) {
whereClause.push('id=?');
whereArgs.push(id);
});
database.query('DELETE FROM backups WHERE ' + whereClause.join(' OR '), whereArgs, function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
database.query('DELETE FROM backups WHERE id=?', [ id ], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
+617 -192
View File
File diff suppressed because it is too large Load Diff
+15 -69
View File
@@ -1,7 +1,12 @@
#!/usr/bin/env node
#!/bin/bash
':' //# comment; exec /usr/bin/env node --max_old_space_size=300 "$0" "$@"
// to understand the above hack read http://sambal.org/2014/02/passing-options-node-shebang-line/
'use strict';
if (process.argv[2] === '--check') return console.log('OK');
require('supererror')({ splatchError: true });
// remove timestamp from debug() based output
@@ -10,29 +15,11 @@ require('debug').formatArgs = function formatArgs(args) {
};
var assert = require('assert'),
BackupsError = require('./backups.js').BackupsError,
caas = require('./storage/caas.js'),
backups = require('./backups.js'),
database = require('./database.js'),
debug = require('debug')('box:backuptask'),
filesystem = require('./storage/filesystem.js'),
noop = require('./storage/noop.js'),
path = require('path'),
paths = require('./paths.js'),
s3 = require('./storage/s3.js'),
safe = require('safetydance'),
settings = require('./settings.js');
function api(provider) {
switch (provider) {
case 'caas': return caas;
case 's3': return s3;
case 'filesystem': return filesystem;
case 'minio': return s3;
case 'exoscale-sos': return s3;
case 'noop': return noop;
default: return null;
}
}
safe = require('safetydance');
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -40,52 +27,12 @@ function initialize(callback) {
database.initialize(callback);
}
function backupApp(backupId, appId, callback) {
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
debug('Start app backup with id %s for %s', backupId, appId);
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
var backupMapping = [{
source: path.join(paths.APPS_DATA_DIR, appId),
destination: '.'
}];
api(backupConfig.provider).backup(backupConfig, backupId, backupMapping, callback);
});
}
function backupBox(backupId, callback) {
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof callback, 'function');
debug('Start box backup with id %s', backupId);
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
var backupMapping = [{
source: paths.BOX_DATA_DIR,
destination: 'box'
}, {
source: path.join(paths.PLATFORM_DATA_DIR, 'mail'),
destination: 'mail'
}];
api(backupConfig.provider).backup(backupConfig, backupId, backupMapping, callback);
});
}
// Main process starts here
var backupId = process.argv[2];
var appId = process.argv[3];
var format = process.argv[3];
var dataDir = process.argv[4];
if (appId) debug('Backuptask for the app %s with id %s', appId, backupId);
else debug('Backuptask for the whole Cloudron with id %s', backupId);
debug(`Backing up ${dataDir} to ${backupId}`);
process.on('SIGTERM', function () {
process.exit(0);
@@ -94,7 +41,9 @@ process.on('SIGTERM', function () {
initialize(function (error) {
if (error) throw error;
function resultHandler(error) {
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, '');
backups.upload(backupId, format, dataDir, function resultHandler(error) {
if (error) debug('completed with error', error);
debug('completed');
@@ -104,8 +53,5 @@ initialize(function (error) {
// https://nodejs.org/api/process.html are exit codes used by node. apps.js uses the value below
// to check apptask crashes
process.exit(error ? 50 : 0);
}
if (appId) backupApp(backupId, appId, resultHandler);
else backupBox(backupId, resultHandler);
});
});
+1 -1
View File
@@ -16,7 +16,7 @@ var assert = require('assert'),
var CA_PROD = 'https://acme-v01.api.letsencrypt.org',
CA_STAGING = 'https://acme-staging.api.letsencrypt.org',
LE_AGREEMENT = 'https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf';
LE_AGREEMENT = 'https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf';
exports = module.exports = {
getCertificate: getCertificate,
+2 -2
View File
@@ -177,7 +177,7 @@ function renewAll(auditSource, callback) {
apps.getAll(function (error, allApps) {
if (error) return callback(error);
allApps.push({ location: constants.ADMIN_LOCATION }); // inject fake webadmin app
allApps.push({ location: config.adminLocation() }); // inject fake webadmin app
var expiringApps = [ ];
for (var i = 0; i < allApps.length; i++) {
@@ -239,7 +239,7 @@ function renewAll(auditSource, callback) {
}
// 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 ?
var configureFunc = app.location === config.adminLocation() ?
nginx.configureAdmin.bind(null, certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn())
: nginx.configureApp.bind(null, app, certFilePath, keyFilePath);
+1 -1
View File
@@ -45,7 +45,7 @@ var appdb = require('./appdb.js'),
hat = require('hat'),
tokendb = require('./tokendb.js'),
util = require('util'),
uuid = require('node-uuid');
uuid = require('uuid');
function ClientsError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
+66 -78
View File
@@ -12,7 +12,7 @@ exports = module.exports = {
dnsSetup: dnsSetup,
getLogs: getLogs,
sendHeartbeat: sendHeartbeat,
sendCaasHeartbeat: sendCaasHeartbeat,
updateToLatest: updateToLatest,
reboot: reboot,
@@ -52,6 +52,7 @@ var appdb = require('./appdb.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
SettingsError = settings.SettingsError,
settingsdb = require('./settingsdb.js'),
shell = require('./shell.js'),
spawn = require('child_process').spawn,
split = require('split'),
@@ -63,7 +64,6 @@ var appdb = require('./appdb.js'),
updateChecker = require('./updatechecker.js'),
user = require('./user.js'),
UserError = user.UserError,
user = require('./user.js'),
util = require('util'),
_ = require('underscore');
@@ -127,7 +127,6 @@ function initialize(callback) {
async.series([
certificates.initialize,
settings.initialize,
installAppBundle,
configureDefaultServer,
onDomainConfigured
], function (error) {
@@ -185,12 +184,18 @@ function dnsSetup(dnsConfig, domain, zoneName, callback) {
config.setFqdn(domain); // set fqdn only after dns config is valid, otherwise cannot re-setup if we failed
config.setZoneName(zoneName);
async.series([ // do not block
onDomainConfigured,
configureWebadmin
], NOOP_CALLBACK);
// upsert the temporary domain record in settings db
// This can be removed after this release
settingsdb.set('domain', JSON.stringify({ fqdn: domain, zoneName: zoneName }), function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
callback();
async.series([ // do not block
onDomainConfigured,
configureWebadmin
], NOOP_CALLBACK);
callback();
});
});
}
@@ -236,24 +241,32 @@ function configureWebadmin(callback) {
callback(error);
}
sysinfo.getPublicIp(function (error, ip) {
if (error) return done(error);
function configureNginx(error) {
debug('configureNginx: dns update:%j', error);
addDnsRecords(ip, function (error) {
certificates.ensureCertificate({ location: config.adminLocation() }, function (error, certFilePath, keyFilePath) {
if (error) return done(error);
gWebadminStatus.tls = true;
nginx.configureAdmin(certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), done);
});
}
// update the DNS. configure nginx regardless of whether it succeeded so that
// box is accessible even if dns creds are invalid
sysinfo.getPublicIp(function (error, ip) {
if (error) return configureNginx(error);
addDnsRecords(ip, function (error) {
if (error) return configureNginx(error);
subdomains.waitForDns(config.adminFqdn(), ip, 'A', { interval: 30000, times: 50000 }, function (error) {
if (error) return done(error);
if (error) return configureNginx(error);
gWebadminStatus.dns = true;
certificates.ensureCertificate({ location: constants.ADMIN_LOCATION }, function (error, certFilePath, keyFilePath) {
if (error) return done(error);
gWebadminStatus.tls = true;
nginx.configureAdmin(certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), done);
});
configureNginx();
});
});
});
@@ -410,9 +423,12 @@ function getConfig(callback) {
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
fqdn: config.fqdn(),
adminLocation: config.adminLocation(),
adminFqdn: config.adminFqdn(),
mailFqdn: config.mailFqdn(),
version: config.version(),
update: updateChecker.getUpdateInfo(),
progress: progress.get(),
progress: progress.getAll(),
isCustomDomain: config.isCustomDomain(),
isDemo: config.isDemo(),
developerMode: developerMode,
@@ -430,8 +446,8 @@ function getConfig(callback) {
});
}
function sendHeartbeat() {
if (config.provider() !== 'caas') return;
function sendCaasHeartbeat() {
assert(config.provider() === 'caas', 'Heartbeat is only sent for managed cloudrons');
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat';
superagent.post(url).query({ token: config.token(), version: config.version() }).timeout(30 * 1000).end(function (error, result) {
@@ -531,9 +547,9 @@ function addDnsRecords(ip, callback) {
var dkimKey = readDkimPublicKeySync();
if (!dkimKey) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, new Error('Failed to read dkim public key')));
var webadminRecord = { subdomain: constants.ADMIN_LOCATION, type: 'A', values: [ ip ] };
var webadminRecord = { subdomain: config.adminLocation(), type: 'A', values: [ ip ] };
// t=s limits the domainkey to this domain and not it's subdomains
var dkimRecord = { subdomain: constants.DKIM_SELECTOR + '._domainkey', type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
var dkimRecord = { subdomain: config.dkimSelector() + '._domainkey', type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
var records = [ ];
if (config.isCustomDomain()) {
@@ -568,7 +584,8 @@ function addDnsRecords(ip, callback) {
});
});
}, function (error) {
debug('addDnsRecords: done updating records with error:', error);
if (error) debug('addDnsRecords: done updating records with error:', error);
else debug('addDnsRecords: done');
callback(error);
});
@@ -662,19 +679,19 @@ function doUpgrade(boxUpdateInfo, callback) {
if (error) return upgradeError(error);
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/upgrade')
.query({ token: config.token() })
.send({ version: boxUpdateInfo.version })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return upgradeError(new Error('Network error making upgrade request: ' + error));
if (result.statusCode !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
.query({ token: config.token() })
.send({ version: boxUpdateInfo.version })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return upgradeError(new Error('Network error making upgrade request: ' + error));
if (result.statusCode !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
progress.set(progress.UPDATE, 10, 'Updating base system');
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();
retire('upgrade');
});
// no need to unlock since this is the last thing we ever do on this box
callback();
retire('upgrade');
});
});
}
@@ -698,6 +715,7 @@ function doUpdate(boxUpdateInfo, callback) {
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
fqdn: config.fqdn(),
adminLocation: config.adminLocation(),
tlsCert: config.tlsCert(),
tlsKey: config.tlsKey(),
isCustomDomain: config.isCustomDomain(),
@@ -729,36 +747,6 @@ function doUpdate(boxUpdateInfo, callback) {
});
}
function installAppBundle(callback) {
assert.strictEqual(typeof callback, 'function');
if (fs.existsSync(paths.FIRST_RUN_FILE)) return callback();
var bundle = config.get('appBundle');
debug('initialize: installing app bundle on first run: %j', bundle);
if (!bundle || bundle.length === 0) return callback();
async.eachSeries(bundle, function (appInfo, iteratorCallback) {
debug('autoInstall: installing %s at %s', appInfo.appstoreId, appInfo.location);
var data = {
appStoreId: appInfo.appstoreId,
location: appInfo.location,
portBindings: appInfo.portBindings || null,
accessRestriction: appInfo.accessRestriction || null,
};
apps.install(data, { userId: null, username: 'autoinstaller' }, iteratorCallback);
}, function (error) {
if (error) debug('autoInstallApps: ', error);
fs.writeFileSync(paths.FIRST_RUN_FILE, 'been there, done that', 'utf8');
callback();
});
}
function checkDiskSpace(callback) {
callback = callback || NOOP_CALLBACK;
@@ -836,20 +824,20 @@ function doMigrate(options, callback) {
debug('migrate: domain: %s size %s region %s', options.domain, options.size, options.region);
superagent
.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/migrate')
.query({ token: config.token() })
.send(options)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return unlock(error); // network 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)));
.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/migrate')
.query({ token: config.token() })
.send(options)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return unlock(error); // network 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)));
progress.set(progress.MIGRATE, 10, 'Migrating');
progress.set(progress.MIGRATE, 10, 'Migrating');
retire('migrate', _.pick(options, 'domain', 'size', 'region'));
});
retire('migrate', _.pick(options, 'domain', 'size', 'region'));
});
});
callback(null);
+74 -34
View File
@@ -17,6 +17,7 @@ exports = module.exports = {
apiServerOrigin: apiServerOrigin,
webServerOrigin: webServerOrigin,
fqdn: fqdn,
zoneName: zoneName,
setFqdn: setFqdn,
token: token,
version: version,
@@ -28,11 +29,14 @@ exports = module.exports = {
adminOrigin: adminOrigin,
internalAdminOrigin: internalAdminOrigin,
sysadminOrigin: sysadminOrigin, // caas routes
adminLocation: adminLocation,
adminFqdn: adminFqdn,
mailLocation: mailLocation,
mailFqdn: mailFqdn,
appFqdn: appFqdn,
zoneName: zoneName,
setZoneName: setZoneName,
hasIPv6: hasIPv6,
dkimSelector: dkimSelector,
isDemo: isDemo,
@@ -44,13 +48,17 @@ exports = module.exports = {
};
var assert = require('assert'),
constants = require('./constants.js'),
debug = require('debug')('box:config.js'),
fs = require('fs'),
path = require('path'),
safe = require('safetydance'),
tld = require('tldjs'),
_ = require('underscore');
// assert on unknown environment can't proceed
assert(exports.CLOUDRON || exports.TEST, 'Unknown environment. This should not happen!');
var homeDir = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
var data = { };
@@ -62,8 +70,25 @@ function baseDir() {
var cloudronConfigFileName = path.join(baseDir(), 'configs/cloudron.conf');
// only tests can run without a config file on disk, they use the defaults with runtime overrides
if (exports.CLOUDRON) assert(fs.existsSync(cloudronConfigFileName), 'No cloudron.conf found, cannot proceed');
function saveSync() {
fs.writeFileSync(cloudronConfigFileName, JSON.stringify(data, null, 4)); // functions are ignored by JSON.stringify
// only save values we want to have in the cloudron.conf, see start.sh
var conf = {
version: data.version,
token: data.token,
apiServerOrigin: data.apiServerOrigin,
webServerOrigin: data.webServerOrigin,
fqdn: data.fqdn,
zoneName: data.zoneName,
adminLocation: data.adminLocation,
isCustomDomain: data.isCustomDomain,
provider: data.provider,
isDemo: data.isDemo
};
fs.writeFileSync(cloudronConfigFileName, JSON.stringify(conf, null, 4)); // functions are ignored by JSON.stringify
}
function _reset(callback) {
@@ -76,45 +101,41 @@ function _reset(callback) {
function initConfig() {
// setup defaults
data.fqdn = 'localhost';
data.fqdn = '';
data.zoneName = '';
data.adminLocation = 'my';
data.port = 3000;
data.token = null;
data.version = null;
data.isCustomDomain = true;
data.apiServerOrigin = null;
data.webServerOrigin = null;
data.smtpPort = 2525; // // this value comes from mail container
data.provider = 'caas';
data.smtpPort = 2525; // this value comes from mail container
data.sysadminPort = 3001;
data.ldapPort = 3002;
data.provider = 'caas';
data.appBundle = [ ];
if (exports.CLOUDRON) {
data.port = 3000;
data.apiServerOrigin = null;
data.database = null;
} else if (exports.TEST) {
// keep in sync with start.sh
data.database = {
hostname: '127.0.0.1',
username: 'root',
password: 'password',
port: 3306,
name: 'box'
};
// overrides for local testings
if (exports.TEST) {
data.port = 5454;
data.apiServerOrigin = 'http://localhost:6060'; // hock doesn't support https
data.database = {
hostname: 'localhost',
username: 'root',
password: '',
port: 3306,
name: 'boxtest'
};
data.token = 'APPSTORE_TOKEN';
} else {
assert(false, 'Unknown environment. This should not happen!');
data.apiServerOrigin = 'http://localhost:6060'; // hock doesn't support https
data.database.password = '';
data.database.name = 'boxtest';
}
if (safe.fs.existsSync(cloudronConfigFileName)) {
var existingData = safe.JSON.parse(safe.fs.readFileSync(cloudronConfigFileName, 'utf8'));
_.extend(data, existingData); // overwrite defaults with saved config
return;
}
saveSync();
// overwrite defaults with saved config
var existingData = safe.JSON.parse(safe.fs.readFileSync(cloudronConfigFileName, 'utf8'));
_.extend(data, existingData);
}
initConfig();
@@ -175,16 +196,24 @@ function appFqdn(location) {
return isCustomDomain() ? location + '.' + fqdn() : location + '-' + fqdn();
}
function adminFqdn() {
return appFqdn(constants.ADMIN_LOCATION);
function mailLocation() {
return get('adminLocation'); // not a typo! should be same as admin location until we figure out certificates
}
function mailFqdn() {
return appFqdn(constants.MAIL_LOCATION);
return appFqdn(mailLocation());
}
function adminLocation() {
return get('adminLocation');
}
function adminFqdn() {
return appFqdn(adminLocation());
}
function adminOrigin() {
return 'https://' + appFqdn(constants.ADMIN_LOCATION);
return 'https://' + appFqdn(adminLocation());
}
function internalAdminOrigin() {
@@ -232,3 +261,14 @@ function tlsKey() {
var keyFile = path.join(baseDir(), 'configs/host.key');
return safe.fs.readFileSync(keyFile, 'utf8');
}
function hasIPv6() {
const IPV6_PROC_FILE = '/proc/net/if_inet6';
return fs.existsSync(IPV6_PROC_FILE);
}
function dkimSelector() {
var loc = adminLocation();
return loc === 'my' ? 'cloudron' : `cloudron-${loc.replace(/\./g, '')}`;
}
-5
View File
@@ -1,12 +1,9 @@
'use strict';
// default admin installation location. keep in sync with ADMIN_LOCATION in setup/start.sh and BOX_ADMIN_LOCATION in appstore constants.js
exports = module.exports = {
ADMIN_LOCATION: 'my',
API_LOCATION: 'api', // this is unused but reserved for future use (#403)
SMTP_LOCATION: 'smtp',
IMAP_LOCATION: 'imap',
MAIL_LOCATION: 'my', // not a typo! should be same as admin location until we figure out certificates
POSTMAN_LOCATION: 'postman', // used in dovecot bounces
// These are combined into one array because users and groups become mailboxes
@@ -36,8 +33,6 @@ exports = module.exports = {
DEMO_USERNAME: 'cloudron',
DKIM_SELECTOR: 'cloudron',
AUTOUPDATE_PATTERN_NEVER: 'never'
};
+19 -17
View File
@@ -35,7 +35,7 @@ var gAliveJob = null, // send periodic stats
gCleanupTokensJob = null,
gDockerVolumeCleanerJob = null,
gDynamicDNSJob = null,
gHeartbeatJob = null, // for CaaS health check
gCaasHeartbeatJob = null, // for CaaS health check
gSchedulerSyncJob = null,
gDigestEmailJob = null;
@@ -53,18 +53,20 @@ var AUDIT_SOURCE = { userId: null, username: 'cron' };
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
gHeartbeatJob = new CronJob({
cronTime: '00 */1 * * * *', // every minute
onTick: cloudron.sendHeartbeat,
start: false
});
// hack: send the first heartbeat only after we are running for 60 seconds
// required as we end up sending a heartbeat and then cloudron-setup reboots the server
setTimeout(function () {
if (!gHeartbeatJob) return; // already uninitalized
gHeartbeatJob.start();
cloudron.sendHeartbeat();
}, 1000 * 60);
if (config.provider() === 'caas') {
gCaasHeartbeatJob = new CronJob({
cronTime: '00 */1 * * * *', // every minute
onTick: cloudron.sendCaasHeartbeat,
start: false
});
// hack: send the first heartbeat only after we are running for 60 seconds
// required as we end up sending a heartbeat and then cloudron-setup reboots the server
setTimeout(function () {
if (!gCaasHeartbeatJob) return; // already uninitalized
gCaasHeartbeatJob.start();
cloudron.sendCaasHeartbeat();
}, 1000 * 60);
}
var randomHourMinute = Math.floor(60*Math.random());
gAliveJob = new CronJob({
@@ -139,7 +141,7 @@ function recreateJobs(tz) {
if (gCleanupBackupsJob) gCleanupBackupsJob.stop();
gCleanupBackupsJob = new CronJob({
cronTime: '00 45 */6 * * *', // every 6 hours. try not to overlap with ensureBackup job
onTick: backups.cleanup,
onTick: backups.cleanup.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
start: true,
timeZone: tz
});
@@ -208,7 +210,7 @@ function autoupdatePatternChanged(pattern) {
}
} else if (updateInfo.apps) {
debug('Starting app update to %j', updateInfo.apps);
apps.updateApps(updateInfo.apps, AUDIT_SOURCE, NOOP_CALLBACK);
apps.autoupdateApps(updateInfo.apps, AUDIT_SOURCE, NOOP_CALLBACK);
} else {
debug('No auto updates available');
}
@@ -252,8 +254,8 @@ function uninitialize(callback) {
if (gAppUpdateCheckerJob) gAppUpdateCheckerJob.stop();
gAppUpdateCheckerJob = null;
if (gHeartbeatJob) gHeartbeatJob.stop();
gHeartbeatJob = null;
if (gCaasHeartbeatJob) gCaasHeartbeatJob.stop();
gCaasHeartbeatJob = null;
if (gAliveJob) gAliveJob.stop();
gAliveJob = null;
+1 -21
View File
@@ -7,19 +7,15 @@ exports = module.exports = {
isEnabled: isEnabled,
setEnabled: setEnabled,
issueDeveloperToken: issueDeveloperToken,
getNonApprovedApps: getNonApprovedApps
issueDeveloperToken: issueDeveloperToken
};
var assert = require('assert'),
clients = require('./clients.js'),
config = require('./config.js'),
constants = require('./constants.js'),
debug = require('debug')('box:developer'),
eventlog = require('./eventlog.js'),
tokendb = require('./tokendb.js'),
settings = require('./settings.js'),
superagent = require('superagent'),
util = require('util');
function DeveloperError(reason, errorOrMessage) {
@@ -84,19 +80,3 @@ function issueDeveloperToken(user, auditSource, callback) {
callback(null, { token: token, expiresAt: new Date(expiresAt).toISOString() });
});
}
function getNonApprovedApps(callback) {
assert.strictEqual(typeof callback, 'function');
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/apps';
superagent.get(url).query({ token: config.token(), boxVersion: config.version() }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new DeveloperError(DeveloperError.EXTERNAL_ERROR, error));
if (result.statusCode === 401 || result.statusCode === 403) {
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 || []);
});
}
+25 -18
View File
@@ -33,31 +33,38 @@ function maybeSend(callback) {
var hasSubscription = result && result.plan.id !== 'free' && result.plan.id !== 'undecided';
eventlog.getByActionLastWeek(eventlog.ACTION_APP_UPDATE, function (error, appUpdates) {
eventlog.getByCreationTime(new Date(new Date() - 7*86400000), function (error, events) {
if (error) return callback(error);
eventlog.getByActionLastWeek(eventlog.ACTION_UPDATE, function (error, boxUpdates) {
if (error) return callback(error);
var appUpdates = events.filter(function (e) { return e.action === eventlog.ACTION_APP_UPDATE; }).map(function (e) { return e.data; });
var boxUpdates = events.filter(function (e) { return e.action === eventlog.ACTION_UPDATE; }).map(function (e) { return e.data; });
var certRenewals = events.filter(function (e) { return e.action === eventlog.ACTION_CERTIFICATE_RENEWAL; }).map(function (e) { return e.data; });
var usersAdded = events.filter(function (e) { return e.action === eventlog.ACTION_USER_ADD; }).map(function (e) { return e.data; });
var usersRemoved = events.filter(function (e) { return e.action === eventlog.ACTION_USER_REMOVE; }).map(function (e) { return e.data; });
var finishedBackups = events.filter(function (e) { return e.action === eventlog.ACTION_BACKUP_FINISH && !e.errorMessage; }).map(function (e) { return e.data; });
var info = {
hasSubscription: hasSubscription,
if (error) return callback(error);
pendingAppUpdates: pendingAppUpdates,
pendingBoxUpdate: updateInfo.box || null,
var info = {
hasSubscription: hasSubscription,
finishedAppUpdates: (appUpdates || []).map(function (e) { return e.data; }),
finishedBoxUpdates: (boxUpdates || []).map(function (e) { return e.data; })
};
pendingAppUpdates: pendingAppUpdates,
pendingBoxUpdate: updateInfo.box || null,
if (info.pendingAppUpdates.length || info.pendingBoxUpdate || info.finishedAppUpdates.length || info.finishedBoxUpdates.length) {
debug('maybeSend: sending digest email', info);
mailer.sendDigest(info);
} else {
debug('maybeSend: nothing happened, NOT sending digest email');
}
finishedAppUpdates: appUpdates,
finishedBoxUpdates: boxUpdates,
callback();
});
certRenewals: certRenewals,
finishedBackups: finishedBackups, // only the successful backups
usersAdded: usersAdded,
usersRemoved: usersRemoved // unused because we don't have username to work with
};
// always send digest for backup failure notification
debug('maybeSend: sending digest email', info);
mailer.sendDigest(info);
callback();
});
});
});
+3 -3
View File
@@ -38,7 +38,7 @@ function add(dnsConfig, zoneName, subdomain, type, values, callback) {
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (error && !error.response) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 400) return callback(new SubdomainError(SubdomainError.BAD_FIELD, result.body.message));
if (result.statusCode === 420) return callback(new SubdomainError(SubdomainError.STILL_BUSY));
if (result.statusCode !== 201) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
@@ -63,7 +63,7 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
.query({ token: dnsConfig.token, type: type })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (error && !error.response) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
return callback(null, result.body.values);
@@ -102,7 +102,7 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (error && !error.response) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 400) return callback(new SubdomainError(SubdomainError.BAD_FIELD, result.body.message));
if (result.statusCode === 420) return callback(new SubdomainError(SubdomainError.STILL_BUSY));
if (result.statusCode === 404) return callback(new SubdomainError(SubdomainError.NOT_FOUND));
+12 -4
View File
@@ -66,18 +66,18 @@ function getDNSRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, cal
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName;
superagent.get(CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records')
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.query({ type: type, name: fqdn })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName;
var tmp = result.body.result.filter(function (record) {
return (record.type === type && record.name === fqdn);
});
var tmp = result.body.result;
return callback(null, tmp);
});
@@ -109,10 +109,18 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
var i = 0;
async.eachSeries(values, function (value, callback) {
var priority = null;
if (type === 'MX') {
priority = value.split(' ')[0];
value = value.split(' ')[1];
}
var data = {
type: type,
name: fqdn,
content: value,
priority: priority,
ttl: 120 // 1 means "automatic" (meaning 300ms) and 120 is the lowest supported
};
+66 -49
View File
@@ -10,9 +10,10 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
constants = require('../constants.js'),
config = require('../config.js'),
debug = require('debug')('box:dns/digitalocean'),
dns = require('dns'),
safe = require('safetydance'),
SubdomainError = require('../subdomains.js').SubdomainError,
superagent = require('superagent'),
util = require('util');
@@ -30,22 +31,34 @@ function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
superagent.get(DIGITALOCEAN_ENDPOINT + '/v2/domains/' + zoneName + '/records')
.set('Authorization', 'Bearer ' + dnsConfig.token)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode === 404) return callback(new SubdomainError(SubdomainError.NOT_FOUND, formatError(result)));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, formatError(result)));
var nextPage = null, matchingRecords = [];
var tmp = result.body.domain_records.filter(function (record) {
return (record.type === type && record.name === subdomain);
});
async.doWhilst(function (iteratorDone) {
var url = nextPage ? nextPage : DIGITALOCEAN_ENDPOINT + '/v2/domains/' + zoneName + '/records';
debug('getInternal: %j', tmp);
superagent.get(url)
.set('Authorization', 'Bearer ' + dnsConfig.token)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 404) return callback(new SubdomainError(SubdomainError.NOT_FOUND, formatError(result)));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, formatError(result)));
return callback(null, tmp);
matchingRecords = matchingRecords.concat(result.body.domain_records.filter(function (record) {
return (record.type === type && record.name === subdomain);
}));
nextPage = (result.body.links && result.body.links.pages) ? result.body.links.pages.next : null;
iteratorDone();
});
}, function () { return !!nextPage; }, function (error) {
if (error) return callback(error);
debug('getInternal: %j', matchingRecords);
return callback(null, matchingRecords);
});
}
@@ -65,7 +78,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
if (error) return callback(error);
// used to track available records to update instead of create
var i = 0;
var i = 0, recordIds = [];
async.eachSeries(values, function (value, callback) {
var priority = null;
@@ -85,38 +98,42 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
if (i >= result.length) {
superagent.post(DIGITALOCEAN_ENDPOINT + '/v2/domains/' + zoneName + '/records')
.set('Authorization', 'Bearer ' + dnsConfig.token)
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode === 403 || result.statusCode === 401) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, formatError(result)));
if (result.statusCode === 422) return callback(new SubdomainError(SubdomainError.BAD_FIELD, result.body.message));
if (result.statusCode !== 201) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, formatError(result)));
.set('Authorization', 'Bearer ' + dnsConfig.token)
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, formatError(result)));
if (result.statusCode === 422) return callback(new SubdomainError(SubdomainError.BAD_FIELD, result.body.message));
if (result.statusCode !== 201) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, formatError(result)));
return callback(null);
});
recordIds.push(safe.query(result.body, 'domain_record.id'));
return callback(null);
});
} else {
superagent.put(DIGITALOCEAN_ENDPOINT + '/v2/domains/' + zoneName + '/records/' + result[i].id)
.set('Authorization', 'Bearer ' + dnsConfig.token)
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
.set('Authorization', 'Bearer ' + dnsConfig.token)
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
// increment, as we have consumed the record
++i;
++i;
if (error && !error.response) return callback(error);
if (result.statusCode === 403 || result.statusCode === 401) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, formatError(result)));
if (result.statusCode === 422) return callback(new SubdomainError(SubdomainError.BAD_FIELD, result.body.message));
if (result.statusCode !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, formatError(result)));
if (error && !error.response) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, formatError(result)));
if (result.statusCode === 422) return callback(new SubdomainError(SubdomainError.BAD_FIELD, result.body.message));
if (result.statusCode !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, formatError(result)));
return callback(null);
});
recordIds.push(safe.query(result.body, 'domain_record.id'));
return callback(null);
});
}
}, function (error) {
}, function (error, id) {
if (error) return callback(error);
callback(null, 'unused');
callback(null, '' + recordIds[0]); // DO ids are integers
});
});
}
@@ -166,18 +183,18 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
// FIXME we only handle the first one currently
superagent.del(DIGITALOCEAN_ENDPOINT + '/v2/domains/' + zoneName + '/records/' + tmp[0].id)
.set('Authorization', 'Bearer ' + dnsConfig.token)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode === 404) return callback(null);
if (result.statusCode === 403 || result.statusCode === 401) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 204) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, formatError(result)));
.set('Authorization', 'Bearer ' + dnsConfig.token)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 404) return callback(null);
if (result.statusCode === 403 || result.statusCode === 401) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 204) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, formatError(result)));
debug('del: done');
debug('del: done');
return callback(null);
});
return callback(null);
});
});
}
@@ -204,7 +221,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Domain nameservers are not set to Digital Ocean'));
}
const name = constants.ADMIN_LOCATION + (fqdn === zoneName ? '' : '.' + fqdn.slice(0, - zoneName.length - 1));
const name = config.adminLocation() + (fqdn === zoneName ? '' : '.' + fqdn.slice(0, - zoneName.length - 1));
upsert(credentials, zoneName, name, 'A', [ ip ], function (error, changeId) {
if (error) return callback(error);
+201
View File
@@ -0,0 +1,201 @@
'use strict';
exports = module.exports = {
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
config = require('../config.js'),
debug = require('debug')('box:dns/gcdns'),
dns = require('dns'),
GCDNS = require('@google-cloud/dns'),
SubdomainError = require('../subdomains.js').SubdomainError,
util = require('util'),
_ = require('underscore');
function getDnsCredentials(dnsConfig) {
assert.strictEqual(typeof dnsConfig, 'object');
var config = {
provider: dnsConfig.provider,
projectId: dnsConfig.projectId,
keyFilename: dnsConfig.keyFilename,
email: dnsConfig.email
};
if (dnsConfig.credentials) {
config.credentials = {
client_email: dnsConfig.credentials.client_email,
private_key: dnsConfig.credentials.private_key
};
}
return config;
}
function getZoneByName(dnsConfig, zoneName, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof callback, 'function');
var gcdns = GCDNS(getDnsCredentials(dnsConfig));
gcdns.getZones(function (error, zones) {
if (error && error.message === 'invalid_grant') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, 'The key was probably revoked'));
if (error && error.reason === 'No such domain') return callback(new SubdomainError(SubdomainError.NOT_FOUND, error.message));
if (error && error.code === 403) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
if (error && error.code === 404) return callback(new SubdomainError(SubdomainError.NOT_FOUND, error.message));
if (error) {
debug('gcdns.getZones', error);
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error));
}
var zone = zones.filter(function (zone) {
return zone.metadata.dnsName.slice(0, -1) === zoneName; // the zone name contains a '.' at the end
})[0];
if (!zone) return callback(new SubdomainError(SubdomainError.NOT_FOUND, 'no such zone'));
callback(null, zone); //zone.metadata ~= {name="", dnsName="", nameServers:[]}
});
}
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('add: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
getZoneByName(getDnsCredentials(dnsConfig), zoneName, function (error, zone) {
if (error) return callback(error);
var domain = (subdomain ? subdomain + '.' : '') + zoneName + '.';
zone.getRecords({ type: type, name: domain }, function (error, oldRecords) {
if (error && error.code === 403) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
if (error) {
debug('upsert->zone.getRecords', error);
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
}
var newRecord = zone.record(type, {
name: domain,
data: values,
ttl: 1
});
zone.createChange({ delete: oldRecords, add: newRecord }, function(error, change) {
if (error && error.code === 403) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
if (error && error.code === 412) return callback(new SubdomainError(SubdomainError.STILL_BUSY, error.message));
if (error) {
debug('upsert->zone.createChange', error);
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
}
callback(null, change.id);
});
});
});
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
getZoneByName(getDnsCredentials(dnsConfig), zoneName, function (error, zone) {
if (error) return callback(error);
var params = {
name: (subdomain ? subdomain + '.' : '') + zoneName + '.',
type: type
};
zone.getRecords(params, function (error, records) {
if (error && error.code === 403) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error));
if (records.length === 0) return callback(null, [ ]);
return callback(null, records[0].data);
});
});
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
getZoneByName(getDnsCredentials(dnsConfig), zoneName, function (error, zone) {
if (error) return callback(error);
var domain = (subdomain ? subdomain + '.' : '') + zoneName + '.';
zone.getRecords({ type: type, name: domain }, function(error, oldRecords) {
if (error && error.code === 403) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
if (error) {
debug('del->zone.getRecords', error);
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
}
zone.deleteRecords(oldRecords, function (error, change) {
if (error && error.code === 403) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
if (error && error.code === 412) return callback(new SubdomainError(SubdomainError.STILL_BUSY, error.message));
if (error) {
debug('del->zone.createChange', error);
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
}
callback(null, change.id);
});
});
});
}
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
var credentials = getDnsCredentials(dnsConfig);
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolveNs(zoneName, function (error, resolvedNS) {
if (error && error.code === 'ENOTFOUND') return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !resolvedNS) return callback(new SubdomainError(SubdomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
getZoneByName(credentials, zoneName, function (error, zone) {
if (error) return callback(error);
var definedNS = zone.metadata.nameServers.sort().map(function(r) { return r.replace(/\.$/, ''); });
if (!_.isEqual(definedNS, resolvedNS.sort())) {
debug('verifyDnsConfig: %j and %j do not match', resolvedNS, definedNS);
return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Domain nameservers are not set to Google Cloud DNS'));
}
const name = config.adminLocation() + (fqdn === zoneName ? '' : '.' + fqdn.slice(0, - zoneName.length - 1));
upsert(credentials, zoneName, name, 'A', [ ip ], function (error, changeId) {
if (error) return callback(error);
debug('verifyDnsConfig: A record added with change id %s', changeId);
callback(null, credentials);
});
});
});
}
+2 -2
View File
@@ -10,7 +10,7 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
constants = require('../constants.js'),
config = require('../config.js'),
debug = require('debug')('box:dns/manual'),
dig = require('../dig.js'),
dns = require('dns'),
@@ -58,7 +58,7 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
var adminDomain = constants.ADMIN_LOCATION + '.' + domain;
var adminDomain = config.adminLocation() + '.' + domain;
dns.resolveNs(zoneName, function (error, nameservers) {
if (error || !nameservers) return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Unable to get nameservers'));
+2 -2
View File
@@ -13,7 +13,7 @@ exports = module.exports = {
var assert = require('assert'),
AWS = require('aws-sdk'),
constants = require('../constants.js'),
config = require('../config.js'),
debug = require('debug')('box:dns/route53'),
dns = require('dns'),
SubdomainError = require('../subdomains.js').SubdomainError,
@@ -247,7 +247,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Domain nameservers are not set to Route53'));
}
const name = constants.ADMIN_LOCATION + (fqdn === zoneName ? '' : '.' + fqdn.slice(0, - zoneName.length - 1));
const name = config.adminLocation() + (fqdn === zoneName ? '' : '.' + fqdn.slice(0, - zoneName.length - 1));
upsert(credentials, zoneName, name, 'A', [ ip ], function (error, changeId) {
if (error) return callback(error);
+32 -21
View File
@@ -14,6 +14,7 @@ exports = module.exports = {
deleteContainers: deleteContainers,
createSubcontainer: createSubcontainer,
getContainerIdByIp: getContainerIdByIp,
inspect: inspect,
execContainer: execContainer
};
@@ -185,9 +186,9 @@ function createSubcontainer(app, name, cmd, options, callback) {
'/run': {}
},
Labels: {
"location": app.location,
"appId": app.id,
"isSubcontainer": String(!isAppContainer)
'location': app.location,
'appId': app.id,
'isSubcontainer': String(!isAppContainer)
},
HostConfig: {
Binds: addons.getBindsSync(app, app.manifest.addons),
@@ -197,20 +198,28 @@ function createSubcontainer(app, name, cmd, options, callback) {
PublishAllPorts: false,
ReadonlyRootfs: app.debugMode ? !!app.debugMode.readonlyRootfs : true,
RestartPolicy: {
"Name": isAppContainer ? "always" : "no",
"MaximumRetryCount": 0
'Name': isAppContainer ? 'always' : 'no',
'MaximumRetryCount': 0
},
CpuShares: 512, // relative to 1024 for system processes
VolumesFrom: isAppContainer ? null : [ app.containerId + ":rw" ],
VolumesFrom: isAppContainer ? null : [ app.containerId + ':rw' ],
NetworkMode: 'cloudron',
Dns: ['172.18.0.1'], // use internal dns
DnsSearch: ['.'], // use internal dns
SecurityOpt: enableSecurityOpt ? [ "apparmor=docker-cloudron-app" ] : null // profile available only on cloudron
SecurityOpt: enableSecurityOpt ? [ 'apparmor=docker-cloudron-app' ] : null // profile available only on cloudron
}
};
var capabilities = manifest.capabilities || [];
if (capabilities.includes('net_admin')) {
containerOptions.HostConfig.CapAdd = [
'NET_ADMIN'
];
}
containerOptions = _.extend(containerOptions, options);
debugApp(app, 'Creating container for %s with options %j', app.manifest.dockerImage, containerOptions);
debugApp(app, 'Creating container for %s', app.manifest.dockerImage);
docker.createContainer(containerOptions, callback);
});
@@ -358,35 +367,37 @@ 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) {
docker.getNetwork('cloudron').inspect(function (error, bridge) {
if (error && error.statusCode === 404) return callback(new Error('Unable to find the cloudron network'));
if (error) return callback(error);
var bridge;
result.forEach(function (n) {
if (n.Name === 'cloudron') bridge = n;
});
if (!bridge) return callback(new Error('Unable to find the cloudron network'));
var containerId;
for (var id in bridge.Containers) {
if (bridge.Containers[id].IPv4Address.indexOf(ip) === 0) {
if (bridge.Containers[id].IPv4Address.indexOf(ip + '/16') === 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);
});
}
function inspect(containerId, callback) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof callback, 'function');
var container = exports.connection.getContainer(containerId);
container.inspect(function (error, result) {
if (error) return callback(error);
callback(null, result);
});
}
function execContainer(containerId, cmd, options, callback) {
assert.strictEqual(typeof containerId, 'string');
assert(util.isArray(cmd));
+101 -2
View File
@@ -3,6 +3,7 @@
exports = module.exports = {
verifyRelay: verifyRelay,
getStatus: getStatus,
checkRblStatus: checkRblStatus,
EmailError: EmailError
};
@@ -14,6 +15,7 @@ var assert = require('assert'),
constants = require('./constants.js'),
debug = require('debug')('box:email'),
dig = require('./dig.js'),
mailer = require('./mailer.js'),
net = require('net'),
nodemailer = require('nodemailer'),
safe = require('safetydance'),
@@ -23,6 +25,8 @@ var assert = require('assert'),
util = require('util'),
_ = require('underscore');
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
const digOptions = { server: '127.0.0.1', port: 53, timeout: 5000 };
function EmailError(reason, errorOrMessage) {
@@ -128,7 +132,7 @@ function verifyRelay(relay, callback) {
function checkDkim(callback) {
var dkim = {
domain: constants.DKIM_SELECTOR + '._domainkey.' + config.fqdn(),
domain: config.dkimSelector() + '._domainkey.' + config.fqdn(),
type: 'TXT',
expected: null,
value: null,
@@ -259,6 +263,100 @@ function checkPtr(callback) {
});
}
// https://raw.githubusercontent.com/jawsome/node-dnsbl/master/list.json
const RBL_LIST = [
{
"name": "Barracuda",
"dns": "b.barracudacentral.org",
"site": "http://www.barracudacentral.org/rbl/removal-request"
},
{
"name": "SpamCop",
"dns": "bl.spamcop.net",
"site": "http://spamcop.net"
},
{
"name": "Sorbs Aggregate Zone",
"dns": "dnsbl.sorbs.net",
"site": "http://dnsbl.sorbs.net/"
},
{
"name": "Sorbs spam.dnsbl Zone",
"dns": "spam.dnsbl.sorbs.net",
"site": "http://sorbs.net"
},
{
"name": "Composite Blocking List",
"dns": "cbl.abuseat.org",
"site": "http://www.abuseat.org"
},
{
"name": "SpamHaus Zen",
"dns": "zen.spamhaus.org",
"site": "http://spamhaus.org"
},
{
"name": "Multi SURBL",
"dns": "multi.surbl.org",
"site": "http://www.surbl.org"
},
{
"name": "Spam Cannibal",
"dns": "bl.spamcannibal.org",
"site": "http://www.spamcannibal.org/cannibal.cgi"
},
{
"name": "dnsbl.abuse.ch",
"dns": "spam.abuse.ch",
"site": "http://dnsbl.abuse.ch/"
},
{
"name": "The Unsubscribe Blacklist(UBL)",
"dns": "ubl.unsubscore.com ",
"site": "http://www.lashback.com/blacklist/"
},
{
"name": "UCEPROTECT Network",
"dns": "dnsbl-1.uceprotect.net",
"site": "http://www.uceprotect.net/en"
}
];
function checkRblStatus(callback) {
assert.strictEqual(typeof callback, 'function');
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error, ip);
var flippedIp = ip.split('.').reverse().join('.');
// https://tools.ietf.org/html/rfc5782
async.map(RBL_LIST, function (rblServer, iteratorDone) {
dig.resolve(flippedIp + '.' + rblServer.dns, 'A', digOptions, function (error, records) {
if (error || !records) return iteratorDone(null, null); // not listed
debug('checkRblStatus: %s (ip: %s) is in the blacklist of %j', config.fqdn(), flippedIp, rblServer);
var result = _.extend({ }, rblServer);
dig.resolve(flippedIp + '.' + rblServer.dns, 'TXT', digOptions, function (error, txtRecords) {
result.txtRecords = error || !txtRecords ? 'No txt record' : txtRecords;
debug('checkRblStatus: %s (error: %s) (txtRecords: %j)', config.fqdn(), error, txtRecords);
return iteratorDone(null, result);
});
});
}, function (ignoredError, blacklistedServers) {
blacklistedServers = blacklistedServers.filter(function(b) { return b !== null; });
debug('checkRblStatus: %s (ip: %s) servers: %j', config.fqdn(), ip, blacklistedServers);
return callback(null, { status: blacklistedServers.length === 0, ip: ip, servers: blacklistedServers });
});
});
}
function getStatus(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -290,7 +388,8 @@ function getStatus(callback) {
recordResult('dns.spf', checkSpf),
recordResult('dns.dkim', checkDkim),
recordResult('dns.ptr', checkPtr),
recordResult('relay', checkOutboundPort25)
recordResult('relay', checkOutboundPort25),
recordResult('rbl', checkRblStatus)
);
} else {
checks.push(recordResult('relay', checkSmtpRelay.bind(null, relay)));
+10 -9
View File
@@ -6,7 +6,7 @@ exports = module.exports = {
add: add,
get: get,
getAllPaged: getAllPaged,
getByActionLastWeek: getByActionLastWeek,
getByCreationTime: getByCreationTime,
cleanup: cleanup,
// keep in sync with webadmin index.js filter and CLI tool
@@ -20,6 +20,7 @@ exports = module.exports = {
ACTION_APP_LOGIN: 'app.login',
ACTION_BACKUP_FINISH: 'backup.finish',
ACTION_BACKUP_START: 'backup.start',
ACTION_BACKUP_CLEANUP: 'backup.cleanup',
ACTION_CERTIFICATE_RENEWAL: 'certificate.renew',
ACTION_CLI_MODE: 'settings.climode',
ACTION_START: 'cloudron.start',
@@ -35,7 +36,7 @@ var assert = require('assert'),
debug = require('debug')('box:eventlog'),
eventlogdb = require('./eventlogdb.js'),
util = require('util'),
uuid = require('node-uuid');
uuid = require('uuid');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
@@ -97,21 +98,21 @@ function getAllPaged(action, search, page, perPage, callback) {
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
eventlogdb.getAllPaged(action, search, page, perPage, function (error, boxes) {
eventlogdb.getAllPaged(action, search, page, perPage, function (error, events) {
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
callback(null, boxes);
callback(null, events);
});
}
function getByActionLastWeek(action, callback) {
assert(typeof action === 'string' || action === null);
function getByCreationTime(creationTime, callback) {
assert(util.isDate(creationTime));
assert.strictEqual(typeof callback, 'function');
eventlogdb.getByActionLastWeek(action, function (error, boxes) {
eventlogdb.getByCreationTime(creationTime, function (error, events) {
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
callback(null, boxes);
callback(null, events);
});
}
@@ -119,7 +120,7 @@ function cleanup(callback) {
callback = callback || NOOP_CALLBACK;
var d = new Date();
d.setDate(d.getDate() - 7); // 7 days ago
d.setDate(d.getDate() - 10); // 10 days ago
// only cleanup high frequency events
var actions = [
+5 -5
View File
@@ -3,7 +3,7 @@
exports = module.exports = {
get: get,
getAllPaged: getAllPaged,
getByActionLastWeek: getByActionLastWeek,
getByCreationTime: getByCreationTime,
add: add,
count: count,
delByCreationTime: delByCreationTime,
@@ -73,12 +73,12 @@ function getAllPaged(action, search, page, perPage, callback) {
});
}
function getByActionLastWeek(action, callback) {
assert(typeof action === 'string' || action === null);
function getByCreationTime(creationTime, callback) {
assert(util.isDate(creationTime));
assert.strictEqual(typeof callback, 'function');
var query = 'SELECT ' + EVENTLOGS_FIELDS + ' FROM eventlog WHERE action=? AND creationTime >= DATE_SUB(NOW(), INTERVAL 1 WEEK) ORDER BY creationTime DESC';
database.query(query, [ action ], function (error, results) {
var query = 'SELECT ' + EVENTLOGS_FIELDS + ' FROM eventlog WHERE creationTime >= ? ORDER BY creationTime DESC';
database.query(query, [ creationTime ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(postProcess);
+1 -1
View File
@@ -25,7 +25,7 @@ var assert = require('assert'),
DatabaseError = require('./databaseerror.js'),
groupdb = require('./groupdb.js'),
util = require('util'),
uuid = require('node-uuid');
uuid = require('uuid');
// http://dustinsenos.com/articles/customErrorsInNode
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
+5 -5
View File
@@ -5,9 +5,9 @@
// Do not require anything here!
exports = module.exports = {
// a major version makes all apps restore from backup
// a major version makes all apps restore from backup. #451 must be fixed before we do this.
// a minor version makes all apps re-configure themselves
'version': '48.5.0',
'version': '48.8.0',
'baseImages': [ 'cloudron/base:0.10.0' ],
@@ -15,10 +15,10 @@ exports = module.exports = {
// This is because we upgrade using dumps instead of mysql_upgrade, pg_upgrade etc
'images': {
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:0.18.0' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:0.17.0' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:0.17.1' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:0.13.0' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:0.11.0' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.36.2' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:0.11.0' }
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.39.1' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:0.12.0' }
}
};
+2 -3
View File
@@ -70,14 +70,13 @@ function cleanupTmpVolume(containerInfo, callback) {
docker.getContainer(containerInfo.Id).exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true, Tty: false }, function (error, execContainer) {
if (error) return callback(new Error('Failed to exec container : ' + error.message));
execContainer.start(function(err, stream) {
execContainer.start({ hijack: true }, function (error, stream) {
if (error) return callback(new Error('Failed to start exec container : ' + error.message));
stream.on('error', callback);
stream.on('end', callback);
stream.setEncoding('utf8');
stream.pipe(process.stdout);
docker.modem.demuxStream(stream, process.stdout, process.stderr);
});
});
}
+123 -15
View File
@@ -61,12 +61,74 @@ function getUsersWithAccessToApp(req, callback) {
});
}
// helper function to deal with pagination
function finalSend(results, req, res, next) {
var min = 0;
var max = results.length;
var cookie = null;
var pageSize = 0;
// check if this is a paging request, if so get the cookie for session info
req.controls.forEach(function (control) {
if (control.type === ldap.PagedResultsControl.OID) {
pageSize = control.value.size;
cookie = control.value.cookie;
}
});
function sendPagedResults(start, end) {
start = (start < min) ? min : start;
end = (end > max || end < min) ? max : end;
var i;
for (i = start; i < end; i++) {
res.send(results[i]);
}
return i;
}
if (cookie && Buffer.isBuffer(cookie)) {
// we have pagination
var first = min;
if (cookie.length !== 0) {
first = parseInt(cookie.toString(), 10);
}
var last = sendPagedResults(first, first + pageSize);
var resultCookie;
if (last < max) {
resultCookie = new Buffer(last.toString());
} else {
resultCookie = new Buffer('');
}
res.controls.push(new ldap.PagedResultsControl({
value: {
size: pageSize, // correctness not required here
cookie: resultCookie
}
}));
} else {
// no pagination simply send all
results.forEach(function (result) {
res.send(result);
});
}
// all done
res.end();
next();
}
function userSearch(req, res, next) {
debug('user search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id);
getUsersWithAccessToApp(req, function (error, result) {
if (error) return next(error);
var results = [];
// send user objects
result.forEach(function (entry) {
// skip entries with empty username. Some apps like owncloud can't deal with this
@@ -109,11 +171,11 @@ function userSearch(req, res, next) {
if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString()));
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) {
res.send(obj);
results.push(obj);
}
});
res.end();
finalSend(results, req, res, next);
});
}
@@ -123,6 +185,8 @@ function groupSearch(req, res, next) {
getUsersWithAccessToApp(req, function (error, result) {
if (error) return next(error);
var results = [];
var groups = [{
name: 'users',
admin: false
@@ -149,11 +213,43 @@ function groupSearch(req, res, next) {
if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString()));
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) {
res.send(obj);
results.push(obj);
}
});
res.end();
finalSend(results, req, res, next);
});
}
function groupUsersCompare(req, res, next) {
debug('group users compare: dn %s, attribute %s, value %s (from %s)', req.dn.toString(), req.attribute, req.value, req.connection.ldap.id);
getUsersWithAccessToApp(req, function (error, result) {
if (error) return next(error);
// we only support memberuid here, if we add new group attributes later add them here
if (req.attribute === 'memberuid') {
var found = result.find(function (u) { return u.id === req.value; });
if (found) return res.end(true);
}
res.end(false);
});
}
function groupAdminsCompare(req, res, next) {
debug('group admins compare: dn %s, attribute %s, value %s (from %s)', req.dn.toString(), req.attribute, req.value, req.connection.ldap.id);
getUsersWithAccessToApp(req, function (error, result) {
if (error) return next(error);
// we only support memberuid here, if we add new group attributes later add them here
if (req.attribute === 'memberuid') {
var found = result.find(function (u) { return u.id === req.value; });
if (found && found.admin) return res.end(true);
}
res.end(false);
});
}
@@ -161,6 +257,7 @@ function mailboxSearch(req, res, next) {
debug('mailbox search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id);
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
var name = req.dn.rdns[0].attrs.cn.value.toLowerCase();
// allow login via email
var parts = name.split('@');
@@ -188,9 +285,11 @@ function mailboxSearch(req, res, next) {
var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null);
if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString()));
if (lowerCaseFilter.matches(obj.attributes)) res.send(obj);
res.end();
if (lowerCaseFilter.matches(obj.attributes)) {
finalSend([ obj ], req, res, next);
} else {
res.end();
}
});
}
@@ -198,6 +297,7 @@ function mailAliasSearch(req, res, next) {
debug('mail alias get: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id);
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
mailboxdb.getAlias(req.dn.rdns[0].attrs.cn.value.toLowerCase(), function (error, alias) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.toString()));
@@ -218,9 +318,11 @@ function mailAliasSearch(req, res, next) {
var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null);
if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString()));
if (lowerCaseFilter.matches(obj.attributes)) res.send(obj);
res.end();
if (lowerCaseFilter.matches(obj.attributes)) {
finalSend([ obj ], req, res, next);
} else {
res.end();
}
});
}
@@ -228,6 +330,7 @@ function mailingListSearch(req, res, next) {
debug('mailing list get: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id);
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
mailboxdb.getGroup(req.dn.rdns[0].attrs.cn.value.toLowerCase(), function (error, group) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.toString()));
@@ -248,9 +351,11 @@ function mailingListSearch(req, res, next) {
var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null);
if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString()));
if (lowerCaseFilter.matches(obj.attributes)) res.send(obj);
res.end();
if (lowerCaseFilter.matches(obj.attributes)) {
finalSend([ obj ], req, res, next);
} else {
res.end();
}
});
}
@@ -370,14 +475,17 @@ function start(callback) {
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailbox);
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailbox);
gServer.compare('cn=users,ou=groups,dc=cloudron', groupUsersCompare);
gServer.compare('cn=admins,ou=groups,dc=cloudron', groupAdminsCompare);
// this is the bind for addons (after bind, they might search and authenticate)
gServer.bind('ou=addons,dc=cloudron', function(req, res, next) {
gServer.bind('ou=addons,dc=cloudron', function(req, res /*, next */) {
debug('addons bind: %s', req.dn.toString()); // note: cn can be email or id
res.end();
});
// this is the bind for apps (after bind, they might search and authenticate user)
gServer.bind('ou=apps,dc=cloudron', function(req, res, next) {
gServer.bind('ou=apps,dc=cloudron', function(req, res /*, next */) {
// TODO: validate password
debug('application bind: %s', req.dn.toString());
res.end();
+11
View File
@@ -0,0 +1,11 @@
# Generated by apptask for the /run mount
<%= volumePath %>/*.log <%= volumePath %>/*/*.log <%= volumePath %>/*/*/*.log {
rotate 7
daily
compress
maxsize=1M
missingok
delaycompress
copytruncate
}
+35 -3
View File
@@ -2,13 +2,14 @@
Dear Cloudron Admin,
a new version <%= updateInfo.manifest.version %> of the app '<%= app.manifest.title %>' installed at <%= app.fqdn %> is available!
The app will update automatically tonight. Alternately, update immediately at <%= webadminUrl %>.
A new version <%= updateInfo.manifest.version %> of the app '<%= app.manifest.title %>' installed at <%= app.fqdn %> is available!
Changes:
<%= updateInfo.manifest.changelog %>
<% if (!hasSubscription) { -%>
*Keep your Cloudron automatically up-to-date and secure by upgrading to a paid plan at* <%= webadminUrl %>/#/settings
<% } -%>
Powered by https://cloudron.io
@@ -16,4 +17,35 @@ Sent at: <%= new Date().toUTCString() %>
<% } else { %>
<center>
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
<h3>Dear <%= cloudronName %> Admin,</h3>
<div style="width: 650px; text-align: left;">
<p>
A new version <%= updateInfo.manifest.version %> of the app '<%= app.manifest.title %>' installed at <%= app.fqdn %> is available!
</p>
<h5>Changelog:</h5>
<%- changelogHTML %>
<br/>
<% if (!hasSubscription) { %>
<p>Keep your Cloudron automatically up-to-date and secure by upgrading to a <a href="<%= webadminUrl %>/#/settings">paid plan</a>.</p>
<% } %>
<br/>
</div>
<div style="font-size: 10px; color: #333333; background: #ffffff;">
Powered by <a href="https://cloudron.io">Cloudron</a>.
</div>
</center>
<img src="https://analytics.cloudron.io/piwik.php?idsite=2&rec=1&e_c=CloudronEmail&e_a=update" style="border:0" alt="" />
<% } %>
+12 -9
View File
@@ -4,15 +4,18 @@ Dear <%= cloudronName %> Admin,
Version <%= newBoxVersion %> for Cloudron <%= fqdn %> is now available!
Your Cloudron will update automatically tonight. Alternately, update immediately at <%= webadminUrl %>.
Changelog:
<% for (var i = 0; i < changelog.length; i++) { %>
* <%- changelog[i] %>
<% } %>
Thank you,
your Cloudron
<% if (!hasSubscription) { -%>
*Keep your Cloudron automatically up-to-date and secure by upgrading to a paid plan at* <%= webadminUrl %>/#/settings
<% } -%>
Powered by https://cloudron.io
Sent at: <%= new Date().toUTCString() %>
<% } else { %>
@@ -27,11 +30,6 @@ your Cloudron
Version <b><%= newBoxVersion %></b> for Cloudron <%= fqdn %> is now available!
</p>
<p>
Your Cloudron will update automatically tonight.<br/>
Alternately, update immediately <a href="<%= webadminUrl %>">here</a>.
</p>
<h5>Changelog:</h5>
<ul>
<% for (var i = 0; i < changelogHTML.length; i++) { %>
@@ -40,6 +38,11 @@ your Cloudron
</ul>
<br/>
<% if (!hasSubscription) { %>
<p>Keep your Cloudron automatically up-to-date and secure by upgrading to a <a href="<%= webadminUrl %>/#/settings">paid plan</a>.</p>
<% } %>
<br/>
</div>
+132 -1
View File
@@ -2,7 +2,19 @@
Dear <%= cloudronName %> Admin,
This is the weekly summary of activities on your Cloudron <%= fqdn %>.
This is a summary of the activities on your Cloudron <%= fqdn %>.
<% if (info.usersAdded.length) { -%>
The following users were added:
<% for (var i = 0; i < info.usersAdded.length; i++) { -%>
* <%- info.usersAdded[i].email %>
<% }} -%>
<% if (info.certRenewals.length) { -%>
The certificates of the following apps was renewed:
<% for (var i = 0; i < info.certRenewals.length; i++) { -%>
* <%- info.certRenewals[i].domain %> - <%- info.certRenewals[i].errorMessage || 'Success' %>
<% }} -%>
<% if (info.pendingBoxUpdate) { -%>
Cloudron v<%- info.pendingBoxUpdate.version %> is available:
@@ -33,6 +45,14 @@ The following apps were updated:
<% for (var j = 0; j < info.finishedAppUpdates[i].toManifest.changelog.trim().split('\n').length; j++) { -%>
<%= info.finishedAppUpdates[i].toManifest.changelog.trim().split('\n')[j] %>
<% }}} -%>
<% if (info.finishedBackups.length) { -%>
Last successful backup: <%- info.finishedBackups[0].backupId || info.finishedBackups[0].filename %>
<% } else { -%>
This Cloudron did **not** backup successfully in the last week!
<% } -%>
<% if (!info.hasSubscription) { -%>
*Keep your Cloudron automatically up-to-date and secure by upgrading to a paid plan at* <%= webadminUrl %>/#/settings
@@ -44,4 +64,115 @@ Sent at: <%= new Date().toUTCString() %>
<% } else { %>
<center>
<div style="max-width: 800px; text-align: left; border: 1px solid lightgray; padding: 20px;">
<center>
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
</center>
<br/>
<p>This is a summary of the activities on your Cloudron <a href="<%= webadminUrl %>"><%= cloudronName %></a> last week.</p>
<% if (info.usersAdded.length) { -%>
<p><b>The following users were added:</b></p>
<ul>
<% for (var i = 0; i < info.usersAdded.length; i++) { %>
<li><%- info.usersAdded[i].email %></li>
<% } %>
</ul>
<% } %>
<% if (info.certRenewals.length) { -%>
<p><b>The certificates of the following apps were renewed:</b></p>
<ul>
<% for (var i = 0; i < info.certRenewals.length; i++) { %>
<li><%- info.certRenewals[i].domain %> - <%- info.certRenewals[i].errorMessage || 'Success' %></li>
<% } %>
</ul>
<% } %>
<% if (info.pendingBoxUpdate) { -%>
<p><b>Cloudron v<%- info.pendingBoxUpdate.version %> is available:</b></p>
<ul>
<% for (var i = 0; i < info.pendingBoxUpdate.changelog.length; i++) { %>
<li><%- info.pendingBoxUpdate.changelog[i].replace(/^[\*,-] /, '') %></li>
<% } %>
</ul>
<% } %>
<% if (info.pendingAppUpdates.length) { %>
<p><b>Available app updates:</b></p>
<ul>
<% for (var i = 0; i < info.pendingAppUpdates.length; i++) { %>
<li>
<b><%= info.pendingAppUpdates[i].manifest.title %></b>
<ul>
<% for (var j = 0; j < info.pendingAppUpdates[i].manifest.changelog.trim().split('\n').length; j++) { %>
<li><%= info.pendingAppUpdates[i].manifest.changelog.trim().split('\n')[j].replace(/^[\*,-] /, '') %></li>
<% } %>
</ul>
</li>
<% } %>
</ul>
<% } %>
<% if (info.finishedBoxUpdates.length) { %>
<p><b>Your Cloudron was updated with the following releases:</b></p>
<ul>
<% for (var i = 0; i < info.finishedBoxUpdates.length; i++) { %>
<li>
<b><%= info.finishedBoxUpdates[i].boxUpdateInfo.version %></b>
<ul>
<% for (var j = 0; j < info.finishedBoxUpdates[i].boxUpdateInfo.changelog.length; j++) { %>
<li><%= info.finishedBoxUpdates[i].boxUpdateInfo.changelog[j].replace(/^[\*,-] /, '') %></li>
<% } %>
</ul>
</li>
<% } %>
</ul>
<% } %>
<% if (info.finishedAppUpdates.length) { %>
<p><b>The following apps were updated:</b></p>
<ul>
<% for (var i = 0; i < info.finishedAppUpdates.length; i++) { %>
<li>
<b><%= info.finishedAppUpdates[i].toManifest.title %></b> (package v<%= info.finishedAppUpdates[i].toManifest.version %>)
<ul>
<% for (var j = 0; j < info.finishedAppUpdates[i].toManifest.changelog.trim().split('\n').length; j++) { -%>
<li><%= info.finishedAppUpdates[i].toManifest.changelog.trim().split('\n')[j].replace(/^[\*,-] /, '') %></li>
<% } %>
</ul>
</li>
<% } %>
</ul>
<% } %>
<% if (info.finishedBackups.length) { %>
<p><b>Last successful backup : </b> <%= info.finishedBackups[0].backupId || info.finishedBackups[0].filename %> </p>
<% } else { %>
<p><b>This Cloudron did not backup successfully in the last week!</b></p>
<% } %>
<br/>
<% if (!info.hasSubscription) { %>
Keep your Cloudron automatically up-to-date and secure by upgrading to a <a href="<%= webadminUrl %>/#/settings">paid plan</a>.
<% } %>
<br/>
<br/>
<br/>
<center>
<small>
Powered by <a href="https://cloudron.io">Cloudron</a><br/>
Sent on <%= new Date().toUTCString() %>
</small>
</center>
</div>
</center>
<img src="https://analytics.cloudron.io/piwik.php?idsite=2&rec=1&e_c=CloudronEmail&e_a=digest" style="border:0" alt="" />
<% } %>
-14
View File
@@ -1,14 +0,0 @@
<%if (format === 'text') { %>
New <%= type %> from <%= fqdn %>.
Sender: <%= user.email %>
Sent at: <%= new Date().toUTCString() %>
Subject: <%= subject %>
-----------------------------------------------------------
<%= description %>
<% } else { %>
<% } %>
+11
View File
@@ -0,0 +1,11 @@
<%if (format === 'text') { %>
Test email from <%= fqdn %>,
If you can read this, your Cloudron email settings are good.
Sent at: <%= new Date().toUTCString() %>
<% } else { %>
<% } %>
+55
View File
@@ -0,0 +1,55 @@
{
"format": "html",
"webadminUrl": "https://my.cloudron.io",
"fqdn": "my.cloudron.io",
"cloudronName": "Smartserver",
"cloudronAvatarUrl": "https://cloudron.io/img/logo.png",
"info": {
"pendingBoxUpdate": {
"version": "1.3.7",
"changelog": [
"Feature one",
"Feature two"
]
},
"pendingAppUpdates": [{
"manifest": {
"title": "Wordpress",
"version": "1.2.3",
"changelog": "* This has changed\n * and that as well"
}
}],
"finishedBoxUpdates": [{
"boxUpdateInfo": {
"version": "1.0.1",
"changelog": [
"Feature one",
"Feature two"
]
}
}, {
"boxUpdateInfo": {
"version": "1.0.2",
"changelog": [
"Feature one",
"Feature two",
"Feature three"
]
}
}],
"finishedAppUpdates": [{
"toManifest": {
"title": "Rocket.Chat",
"version": "0.2.1",
"changelog": "* This has changed\n * and that as well\n * some more"
}
}, {
"toManifest": {
"title": "Redmine",
"version": "1.2.1",
"changelog": "* This has changed\n * and that as well\n * some more"
}
}],
"hasSubscription": false
}
}
+2 -1
View File
@@ -134,7 +134,8 @@ function getGroup(name, callback) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
database.query('SELECT users.username FROM groupMembers INNER JOIN users ON groupMembers.userId = users.id WHERE groupMembers.groupId = ?', [ results[0].ownerId ], function (error, memberList) {
// username can be null if the user has not signed up with the invite yet
database.query('SELECT users.username FROM groupMembers INNER JOIN users ON groupMembers.userId = users.id WHERE groupMembers.groupId = ? AND users.username IS NOT NULL', [ results[0].ownerId ], function (error, memberList) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results[0].members = memberList.map(function (m) { return m.username; });
+91 -51
View File
@@ -23,12 +23,7 @@ exports = module.exports = {
certificateRenewalError: certificateRenewalError,
FEEDBACK_TYPE_FEEDBACK: 'feedback',
FEEDBACK_TYPE_TICKET: 'ticket',
FEEDBACK_TYPE_APP_MISSING: 'app_missing',
FEEDBACK_TYPE_APP_ERROR: 'app_error',
FEEDBACK_TYPE_UPGRADE_REQUEST: 'upgrade_request',
sendFeedback: sendFeedback,
sendTestMail: sendTestMail,
_getMailQueue: _getMailQueue,
_clearMailQueue: _clearMailQueue
@@ -149,7 +144,15 @@ function render(templateFile, params) {
assert.strictEqual(typeof templateFile, 'string');
assert.strictEqual(typeof params, 'object');
return ejs.render(safe.fs.readFileSync(path.join(MAIL_TEMPLATES_DIR, templateFile), 'utf8'), params);
var content = null;
try {
content = ejs.render(safe.fs.readFileSync(path.join(MAIL_TEMPLATES_DIR, templateFile), 'utf8'), params);
} catch (e) {
debug(`Error rendering ${templateFile}`, e);
}
return content;
}
function getAdminEmails(callback) {
@@ -159,6 +162,7 @@ function getAdminEmails(callback) {
if (admins.length === 0) return callback(new Error('No admins on this cloudron')); // box not activated yet
var adminEmails = [ ];
if (admins[0].alternateEmail) adminEmails.push(admins[0].alternateEmail);
admins.forEach(function (admin) { adminEmails.push(admin.email); });
callback(null, adminEmails);
@@ -232,7 +236,7 @@ function userAdded(user, inviteSent) {
debug('Sending mail for userAdded %s including invite link', inviteSent ? 'not' : '');
getAdminEmails(function (error, adminEmails) {
if (error) return console.log('Error getting admins', error);
if (error) return debug('Error getting admins', error);
adminEmails = _.difference(adminEmails, [ user.email ]);
@@ -329,7 +333,7 @@ function appDied(app) {
debug('Sending mail for app %s @ %s died', app.id, app.fqdn);
getAdminEmails(function (error, adminEmails) {
if (error) return console.log('Error getting admins', error);
if (error) return debug('Error getting admins', error);
var mailOptions = {
from: mailConfig().from,
@@ -342,12 +346,13 @@ function appDied(app) {
});
}
function boxUpdateAvailable(newBoxVersion, changelog) {
function boxUpdateAvailable(hasSubscription, newBoxVersion, changelog) {
assert.strictEqual(typeof hasSubscription, 'boolean');
assert.strictEqual(typeof newBoxVersion, 'string');
assert(util.isArray(changelog));
getAdminEmails(function (error, adminEmails) {
if (error) return console.log('Error getting admins', error);
if (error) return debug('Error getting admins', error);
settings.getCloudronName(function (error, cloudronName) {
if (error) {
@@ -361,6 +366,7 @@ function boxUpdateAvailable(newBoxVersion, changelog) {
fqdn: config.fqdn(),
webadminUrl: config.adminOrigin(),
newBoxVersion: newBoxVersion,
hasSubscription: hasSubscription,
changelog: changelog,
changelogHTML: changelog.map(function (e) { return converter.makeHtml(e); }),
cloudronName: cloudronName,
@@ -373,7 +379,7 @@ function boxUpdateAvailable(newBoxVersion, changelog) {
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
var mailOptions = {
var mailOptions = {
from: mailConfig().from,
to: adminEmails.join(', '),
subject: util.format('%s has a new update available', config.fqdn()),
@@ -386,29 +392,13 @@ function boxUpdateAvailable(newBoxVersion, changelog) {
});
}
function appUpdateAvailable(app, updateInfo) {
function appUpdateAvailable(app, hasSubscription, info) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof updateInfo, 'object');
getAdminEmails(function (error, adminEmails) {
if (error) return console.log('Error getting admins', error);
var mailOptions = {
from: mailConfig().from,
to: adminEmails.join(', '),
subject: util.format('[%s] Update available for %s', config.fqdn(), app.fqdn),
text: render('app_update_available.ejs', { fqdn: config.fqdn(), webadminUrl: config.adminOrigin(), app: app, updateInfo: updateInfo, format: 'text' })
};
enqueue(mailOptions);
});
}
function sendDigest(info) {
assert.strictEqual(typeof hasSubscription, 'boolean');
assert.strictEqual(typeof info, 'object');
getAdminEmails(function (error, adminEmails) {
if (error) return console.log('Error getting admins', error);
if (error) return debug('Error getting admins', error);
settings.getCloudronName(function (error, cloudronName) {
if (error) {
@@ -416,11 +406,70 @@ function sendDigest(info) {
cloudronName = 'Cloudron';
}
var mailOptions = {
var converter = new showdown.Converter();
var templateData = {
fqdn: config.fqdn(),
webadminUrl: config.adminOrigin(),
hasSubscription: hasSubscription,
app: app,
updateInfo: info,
changelogHTML: converter.makeHtml(info.manifest.changelog),
cloudronName: cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
var mailOptions = {
from: mailConfig().from,
to: adminEmails.join(', '),
subject: util.format('App %s has a new update available', app.fqdn),
text: render('app_update_available.ejs', templateDataText),
html: render('app_update_available.ejs', templateDataHTML)
};
enqueue(mailOptions);
});
});
}
function sendDigest(info) {
assert.strictEqual(typeof info, 'object');
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
settings.getCloudronName(function (error, cloudronName) {
if (error) {
debug(error);
cloudronName = 'Cloudron';
}
var templateData = {
fqdn: config.fqdn(),
webadminUrl: config.adminOrigin(),
cloudronName: cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar',
info: info
};
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
var mailOptions = {
from: mailConfig().from,
to: adminEmails.join(', '),
subject: util.format('[%s] Cloudron - Weekly activity digest', config.fqdn()),
text: render('digest.ejs', { fqdn: config.fqdn(), webadminUrl: config.adminOrigin(), cloudronName: cloudronName, info: info, format: 'text' })
text: render('digest.ejs', templateDataText),
html: render('digest.ejs', templateDataHTML)
};
enqueue(mailOptions);
@@ -432,7 +481,7 @@ function outOfDiskSpace(message) {
assert.strictEqual(typeof message, 'string');
getAdminEmails(function (error, adminEmails) {
if (error) return console.log('Error getting admins', error);
if (error) return debug('Error getting admins', error);
var mailOptions = {
from: mailConfig().from,
@@ -449,7 +498,7 @@ function backupFailed(error) {
var message = splatchError(error);
getAdminEmails(function (error, adminEmails) {
if (error) return console.log('Error getting admins', error);
if (error) return debug('Error getting admins', error);
var mailOptions = {
from: mailConfig().from,
@@ -467,7 +516,7 @@ function certificateRenewalError(domain, message) {
assert.strictEqual(typeof message, 'string');
getAdminEmails(function (error, adminEmails) {
if (error) return console.log('Error getting admins', error);
if (error) return debug('Error getting admins', error);
var mailOptions = {
from: mailConfig().from,
@@ -485,7 +534,7 @@ function oomEvent(program, context) {
assert.strictEqual(typeof context, 'string');
getAdminEmails(function (error, adminEmails) {
if (error) return console.log('Error getting admins', error);
if (error) return debug('Error getting admins', error);
var mailOptions = {
from: mailConfig().from,
@@ -517,23 +566,14 @@ function unexpectedExit(program, context, callback) {
sendMails([ mailOptions ], callback);
}
function sendFeedback(user, type, subject, description) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof type, 'string');
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_MISSING ||
type === exports.FEEDBACK_TYPE_UPGRADE_REQUEST ||
type === exports.FEEDBACK_TYPE_APP_ERROR);
function sendTestMail(email) {
assert.strictEqual(typeof email, 'string');
var mailOptions = {
from: mailConfig().from,
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'})
to: email,
subject: util.format('Test Email from %s', config.fqdn()),
text: render('test.ejs', { fqdn: config.fqdn(), format: 'text'})
};
enqueue(mailOptions);
+2
View File
@@ -32,6 +32,7 @@ function configureAdmin(certFilePath, keyFilePath, configFileName, vhost, callba
sourceDir: path.resolve(__dirname, '..'),
adminOrigin: config.adminOrigin(),
vhost: vhost, // if vhost is empty it will become the default_server
hasIPv6: config.hasIPv6(),
endpoint: 'admin',
certFilePath: certFilePath,
keyFilePath: keyFilePath,
@@ -60,6 +61,7 @@ function configureApp(app, certFilePath, keyFilePath, callback) {
sourceDir: sourceDir,
adminOrigin: config.adminOrigin(),
vhost: vhost,
hasIPv6: config.hasIPv6(),
port: app.httpPort,
endpoint: endpoint,
certFilePath: certFilePath,
+46 -43
View File
@@ -15,64 +15,67 @@ app.controller('Controller', ['$scope', function ($scope) {
</script>
<center>
<div class="layout-content">
<center>
<br/>
<h4>Hello <%= (user && user.email) ? user.email : '' %>, welcome to <%= cloudronName %>.</h4>
<h2>Setup your account and password.</h2>
</center>
</center>
<div class="container" ng-app="Application" ng-controller="Controller">
<div class="container" ng-app="Application" ng-controller="Controller">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<form action="/api/v1/session/account/setup" method="post" name="setupForm" autocomplete="off" role="form" novalidate>
<input type="password" style="display: none;">
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
<input type="hidden" name="resetToken" value="<%= resetToken %>"/>
<div class="col-md-6 col-md-offset-3">
<form action="/api/v1/session/account/setup" method="post" name="setupForm" autocomplete="off" role="form" novalidate>
<input type="password" style="display: none;">
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
<input type="hidden" name="resetToken" value="<%= resetToken %>"/>
<center><p class="has-error"><%= error %></p></center>
<center><p class="has-error"><%= error %></p></center>
<% if (user && user.username) { %>
<div class="form-group"">
<label class="control-label">Username</label>
<input type="text" class="form-control" ng-model="username" name="username" readonly required>
</div>
<div class="form-group"">
<label class="control-label">Username</label>
<input type="text" class="form-control" ng-model="username" name="username" readonly required>
</div>
<% } else { %>
<div class="form-group" ng-class="{ 'has-error': (setupForm.username.$dirty && setupForm.username.$invalid) }">
<label class="control-label">Username</label>
<div class="control-label" ng-show="setupForm.username.$dirty && setupForm.username.$invalid">
<small ng-show="setupForm.username.$error.minlength">The username is too short</small>
<small ng-show="setupForm.username.$error.maxlength">The username is too long</small>
<small ng-show="setupForm.username.$dirty && setupForm.username.$invalid">Not a valid username</small>
</div>
<input type="text" class="form-control" ng-model="username" name="username" required autofocus>
</div>
<div class="form-group" ng-class="{ 'has-error': (setupForm.username.$dirty && setupForm.username.$invalid) }">
<label class="control-label">Username</label>
<div class="control-label" ng-show="setupForm.username.$dirty && setupForm.username.$invalid">
<small ng-show="setupForm.username.$error.minlength">The username is too short</small>
<small ng-show="setupForm.username.$error.maxlength">The username is too long</small>
<small ng-show="setupForm.username.$dirty && setupForm.username.$invalid">Not a valid username</small>
</div>
<input type="text" class="form-control" ng-model="username" name="username" required autofocus>
</div>
<% } %>
<div class="form-group">
<label class="control-label">Display Name</label>
<input type="displayName" class="form-control" ng-model="displayName" name="displayName" required>
</div>
<div class="form-group">
<label class="control-label">Display Name</label>
<input type="displayName" class="form-control" ng-model="displayName" name="displayName" required>
</div>
<div class="form-group" ng-class="{ 'has-error': (setupForm.password.$dirty && setupForm.password.$invalid) }">
<label class="control-label">New Password</label>
<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" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" required>
</div>
<div class="form-group" ng-class="{ 'has-error': (setupForm.password.$dirty && setupForm.password.$invalid) }">
<label class="control-label">New Password</label>
<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" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" required>
</div>
<div class="form-group" ng-class="{ 'has-error': (setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)) }">
<label class="control-label">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" ng-model="passwordRepeat" name="passwordRepeat" required>
</div>
<div class="form-group" ng-class="{ 'has-error': (setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)) }">
<label class="control-label">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" 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"/>
</form>
</div>
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Create" ng-disabled="setupForm.$invalid || password !== passwordRepeat"/>
</form>
</div>
</div>
</div>
</div>
<% include footer %>
+14 -13
View File
@@ -2,25 +2,26 @@
<!-- error tester -->
<br/>
<div class="layout-content">
<div class="container">
<div class="container" style="margin-top: 50px;">
<div class="row">
<div class="col-md-2"></div>
<div class="col-md-8">
<div class="alert alert-danger">
<%- message %>
</div>
<div class="col-md-2"></div>
<div class="col-md-8">
<div class="alert alert-danger">
<%- message %>
</div>
<div class="col-md-2"></div>
</div>
<div class="col-md-2"></div>
</div>
<div class="row">
<div class="col-md-2"></div>
<div class="col-md-8 text-center">
<a href="<%- adminOrigin %>">Back</a>
</div>
<div class="col-md-2"></div>
<div class="col-md-2"></div>
<div class="col-md-8 text-center">
<a href="<%- adminOrigin %>">Back</a>
</div>
<div class="col-md-2"></div>
</div>
</div>
</div>
<% include footer %>
+6 -4
View File
@@ -1,9 +1,11 @@
<footer class="text-center">
<span class="text-muted">&copy; 2017 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fa fa-twitter"></i></a></span>
<span class="text-muted"><a href="https://chat.cloudron.io" target="_blank">Chat <i class="fa fa-comments"></i></a></span>
<span class="text-muted">&copy; 2017 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fa fa-twitter"></i></a></span>
<span class="text-muted"><a href="https://chat.cloudron.io" target="_blank">Chat <i class="fa fa-comments"></i></a></span>
</footer>
</body>
</div>
</body>
</html>
+8 -7
View File
@@ -27,14 +27,15 @@
</head>
<body class="oauth">
<body>
<div class="layout-root">
<!-- Navigation -->
<nav class="navbar navbar-default navbar-static-top shadow" role="navigation" style="margin-bottom: 0">
<div class="container-fluid">
<div class="navbar-header">
<a href="/" class="navbar-brand navbar-brand-icon"><img src="/api/v1/cloudron/avatar?<%= Math.random() %>" width="40" height="40"/></a>
<a href="/" class="navbar-brand"><%= cloudronName %></a>
</div>
<div class="container-fluid">
<div class="navbar-header">
<a href="/" class="navbar-brand navbar-brand-icon"><img src="/api/v1/cloudron/avatar?<%= Math.random() %>" width="40" height="40"/></a>
<a href="/" class="navbar-brand"><%= cloudronName %></a>
</div>
</div>
</nav>
+33 -37
View File
@@ -2,45 +2,41 @@
<!-- login tester -->
<div class="container">
<div class="layout-content">
<div class="card" style="padding: 20px; margin-top: 50px; max-width: 620px;">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="card">
<div class="row">
<div class="col-md-12" style="text-align: center;">
<img width="128" height="128" src="<%= applicationLogo %>?<%= Math.random() %>"/>
<h1><small>Login to</small> <%= applicationName %></h1>
<br/>
</div>
</div>
<br/>
<% if (error) { %>
<div class="row">
<div class="col-md-12">
<h4 class="has-error"><%= error %></h4>
</div>
</div>
<% } %>
<div class="row">
<div class="col-md-12">
<form id="loginForm" action="" method="post">
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
<div class="form-group">
<label class="control-label" for="inputUsername">Username or Email</label>
<input type="text" class="form-control" id="inputUsername" name="username" value="<%= username %>" autofocus required>
</div>
<div class="form-group">
<label class="control-label" for="inputPassword">Password</label>
<input type="password" class="form-control" name="password" id="inputPassword" value="<%= password %>" required>
</div>
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Sign in"/>
</form>
<a href="/api/v1/session/password/resetRequest.html">Reset your password</a>
</div>
</div>
</div>
</div>
<div class="col-md-12" style="text-align: center;">
<img width="128" height="128" src="<%= applicationLogo %>?<%= Math.random() %>"/>
<h1><small>Login to</small> <%= applicationName %></h1>
<br/>
</div>
</div>
<br/>
<% if (error) { -%>
<div class="row">
<div class="col-md-12">
<h4 class="has-error"><%= error %></h4>
</div>
</div>
<% } -%>
<div class="row">
<div class="col-md-12">
<form id="loginForm" action="" method="post">
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
<div class="form-group">
<label class="control-label" for="inputUsername">Username or Email</label>
<input type="text" class="form-control" id="inputUsername" name="username" value="<%= username %>" autofocus required>
</div>
<div class="form-group">
<label class="control-label" for="inputPassword">Password</label>
<input type="password" class="form-control" name="password" id="inputPassword" value="<%= password %>" required>
</div>
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Sign in"/>
</form>
<a href="/api/v1/session/password/resetRequest.html">Reset your password</a>
</div>
</div>
</div>
</div>
<script>
+31 -26
View File
@@ -12,36 +12,41 @@ app.controller('Controller', [function () {}]);
</script>
<center>
<h1>Hello <%= user.username %>, set a new password</h1>
</center>
<div class="layout-content">
<div class="container" ng-app="Application" ng-controller="Controller">
<center>
<h2>Hello <%= user.username %>, set a new password</h2>
</center>
<br/>
<div class="container" ng-app="Application" ng-controller="Controller">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<form action="/api/v1/session/password/reset" method="post" name="resetForm" autocomplete="off" role="form" novalidate>
<input type="password" style="display: none;">
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
<input type="hidden" name="resetToken" value="<%= resetToken %>"/>
<div class="col-md-6 col-md-offset-3">
<form action="/api/v1/session/password/reset" method="post" name="resetForm" autocomplete="off" role="form" novalidate>
<input type="password" style="display: none;">
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
<input type="hidden" name="resetToken" value="<%= resetToken %>"/>
<div class="form-group" ng-class="{ 'has-error': resetForm.password.$dirty && resetForm.password.$invalid }">
<label class="control-label" for="inputPassword">New Password</label>
<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 && (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"/>
</form>
</div>
<div class="form-group" ng-class="{ 'has-error': resetForm.password.$dirty && resetForm.password.$invalid }">
<label class="control-label" for="inputPassword">New Password</label>
<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 && (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"/>
</form>
</div>
</div>
</div>
</div>
<% include footer %>
+19 -16
View File
@@ -2,26 +2,29 @@
<!-- tester -->
<center>
<h1>Reset your password</h1>
</center>
<div class="layout-content">
<br/>
<center>
<h2>Reset your password</h2>
</center>
<div class="container">
<br/>
<div class="container">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<form action="/api/v1/session/password/resetRequest" method="post" autocomplete="off">
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
<div class="form-group">
<label class="control-label" for="inputIdentifier">Username or Email</label>
<input type="text" class="form-control" id="inputIdentifier" name="identifier" autofocus required>
</div>
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Reset"/>
</form>
<a href="/api/v1/session/login">Login</a>
</div>
<div class="col-md-6 col-md-offset-3">
<form action="/api/v1/session/password/resetRequest" method="post" autocomplete="off">
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
<div class="form-group">
<label class="control-label" for="inputIdentifier">Username or Email</label>
<input type="text" class="form-control" id="inputIdentifier" name="identifier" autofocus required>
</div>
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Reset"/>
</form>
<a href="/api/v1/session/login">Login</a>
</div>
</div>
</div>
</div>
<% include footer %>
+14 -11
View File
@@ -2,21 +2,24 @@
<!-- tester -->
<center>
<h1>Password reset successful</h1>
</center>
<div class="layout-content">
<br/>
<center>
<h2>Password reset successful</h2>
</center>
<div class="container">
<br/>
<div class="container">
<div class="row">
<center class="col-md-6 col-md-offset-3">
<p>An email was sent to you with a link to set a new password.</p>
<br/>
<br/>
If you have not received any email, simply <a href="/api/v1/session/password/resetRequest.html">try again</a>.
</center>
<div class="col-md-6 col-md-offset-3 text-center">
<p>An email was sent to you with a link to set a new password.</p>
<br/>
<br/>
If you have not received any email, simply <a href="/api/v1/session/password/resetRequest.html">try again</a>.
</div>
</div>
</div>
</div>
<% include footer %>
+7 -4
View File
@@ -7,7 +7,8 @@ var config = require('./config.js'),
exports = module.exports = {
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
INFRA_VERSION_FILE: path.join(config.baseDir(), 'platformdata/INFRA_VERSION'),
BACKUP_RESULT_FILE: path.join(config.baseDir(), 'platformdata/backupresult'),
BACKUP_RESULT_FILE: path.join(config.baseDir(), 'platformdata/backup/result.txt'),
BACKUP_LOG_FILE: path.join(config.baseDir(), 'platformdata/backup/logs.txt'),
OLD_DATA_DIR: path.join(config.baseDir(), 'data'),
PLATFORM_DATA_DIR: path.join(config.baseDir(), 'platformdata'),
@@ -17,16 +18,18 @@ exports = module.exports = {
ACME_CHALLENGES_DIR: path.join(config.baseDir(), 'platformdata/acme'),
ADDON_CONFIG_DIR: path.join(config.baseDir(), 'platformdata/addons'),
COLLECTD_APPCONFIG_DIR: path.join(config.baseDir(), 'platformdata/collectd/collectd.conf.d'),
MAIL_DATA_DIR: path.join(config.baseDir(), 'platformdata/mail'),
LOGROTATE_CONFIG_DIR: path.join(config.baseDir(), 'platformdata/logrotate.d'),
NGINX_CONFIG_DIR: path.join(config.baseDir(), 'platformdata/nginx'),
NGINX_APPCONFIG_DIR: path.join(config.baseDir(), 'platformdata/nginx/applications'),
NGINX_CERT_DIR: path.join(config.baseDir(), 'platformdata/nginx/cert'),
BACKUP_INFO_DIR: path.join(config.baseDir(), 'platformdata/backup'),
SNAPSHOT_INFO_FILE: path.join(config.baseDir(), 'platformdata/backup/snapshot-info.json'),
// this is not part of appdata because an icon may be set before install
ACME_ACCOUNT_KEY_FILE: path.join(config.baseDir(), 'boxdata/acme/acme.key'),
APP_ICONS_DIR: path.join(config.baseDir(), 'boxdata/appicons'),
MAIL_DATA_DIR: path.join(config.baseDir(), 'boxdata/mail'),
ACME_ACCOUNT_KEY_FILE: path.join(config.baseDir(), 'boxdata/acme/acme.key'),
APP_CERTS_DIR: path.join(config.baseDir(), 'boxdata/certs'),
CLOUDRON_AVATAR_FILE: path.join(config.baseDir(), 'boxdata/avatar.png'),
FIRST_RUN_FILE: path.join(config.baseDir(), 'boxdata/first_run'),
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'boxdata/updatechecker.json')
};
+13 -11
View File
@@ -162,7 +162,7 @@ function startMysql(callback) {
const memoryLimit = (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256;
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mysql_vars.sh',
'MYSQL_ROOT_PASSWORD=' + rootPassword +'\nMYSQL_ROOT_HOST=172.18.0.1', 'utf8')) {
'MYSQL_ROOT_PASSWORD=' + rootPassword +'\nMYSQL_ROOT_HOST=172.18.0.1', 'utf8')) {
return callback(new Error('Could not create mysql var file:' + safe.error.message));
}
@@ -238,7 +238,7 @@ function createMailConfig(callback) {
assert.strictEqual(typeof callback, 'function');
const fqdn = config.fqdn();
const mailFqdn = config.adminFqdn();
const mailFqdn = config.mailFqdn();
const alertsFrom = 'no-reply@' + config.fqdn();
debug('createMailConfig: generating mail config');
@@ -254,17 +254,17 @@ function createMailConfig(callback) {
var mailFromValidation = result[settings.MAIL_FROM_VALIDATION_KEY];
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/mail.ini',
`mail_domain=${fqdn}\nmail_server_name=${mailFqdn}\nalerts_from=${alertsFrom}\nalerts_to=${alertsTo}\ncatch_all=${catchAll}\nmail_from_validation=${mailFromValidation}\n`, 'utf8')) {
`mail_domain=${fqdn}\nmail_server_name=${mailFqdn}\nalerts_from=${alertsFrom}\nalerts_to=${alertsTo}\ncatch_all=${catchAll}\nmail_from_validation=${mailFromValidation}\ndkim_selector=${config.dkimSelector()}\n`, 'utf8')) {
return callback(new Error('Could not create mail var file:' + safe.error.message));
}
var relay = result[settings.MAIL_RELAY_KEY];
const enabled = relay.provider !== 'cloudron-smtp' ? true : false,
host = relay.host || '',
port = relay.port || 25,
username = relay.username || '',
password = relay.password || '';
host = relay.host || '',
port = relay.port || 25,
username = relay.username || '',
password = relay.password || '';
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/smtp_forward.ini',
`enable_outbound=${enabled}\nhost=${host}\nport=${port}\nenable_tls=true\nauth_type=plain\nauth_user=${username}\nauth_pass=${password}`, 'utf8')) {
@@ -283,13 +283,13 @@ function startMail(callback) {
// mail container uses /app/data for backed up data and /run for restart-able data
const tag = infra.images.mail.tag;
const dataDir = paths.PLATFORM_DATA_DIR;
const memoryLimit = Math.max((1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 128, 256);
// admin and mail share the same certificate
certificates.getAdminCertificate(function (error, cert, key) {
if (error) return callback(error);
// the setup script copies dhparams.pem to /addons/mail
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/tls_cert.pem', cert)) return callback(new Error('Could not create cert file:' + safe.error.message));
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/tls_key.pem', key)) return callback(new Error('Could not create key file:' + safe.error.message));
@@ -311,8 +311,8 @@ function startMail(callback) {
--dns 172.18.0.1 \
--dns-search=. \
--env ENABLE_MDA=${mailConfig.enabled} \
-v "${dataDir}/mail:/app/data" \
-v "${dataDir}/addons/mail:/etc/mail" \
-v "${paths.MAIL_DATA_DIR}:/app/data" \
-v "${paths.PLATFORM_DATA_DIR}/addons/mail:/etc/mail" \
${ports} \
--read-only -v /run -v /tmp ${tag}`;
@@ -329,7 +329,9 @@ function startMail(callback) {
async.mapSeries(records, function (record, iteratorCallback) {
subdomains.upsert(record.subdomain, record.type, record.values, iteratorCallback);
}, callback);
}, NOOP_CALLBACK); // do not crash if DNS creds do not work in startup sequence
callback();
});
});
});
+14 -3
View File
@@ -2,8 +2,9 @@
exports = module.exports = {
set: set,
setDetail: setDetail,
clear: clear,
get: get,
getAll: getAll,
UPDATE: 'update',
BACKUP: 'backup',
@@ -29,12 +30,22 @@ function set(tag, percent, message) {
progress[tag] = {
percent: percent,
message: message
message: message,
detail: ''
};
debug('%s: %s %s', tag, percent, message);
}
function setDetail(tag, detail) {
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof detail, 'string');
if (!progress[tag]) return debug('unable to set detail %s', detail);
progress[tag].detail = detail;
}
function clear(tag) {
assert.strictEqual(typeof tag, 'string');
@@ -43,6 +54,6 @@ function clear(tag) {
debug('clearing %s', tag);
}
function get() {
function getAll() {
return progress;
}
+107 -7
View File
@@ -17,8 +17,12 @@ exports = module.exports = {
stopApp: stopApp,
startApp: startApp,
exec: exec,
execWebSocket: execWebSocket,
cloneApp: cloneApp
cloneApp: cloneApp,
uploadFile: uploadFile,
downloadFile: downloadFile
};
var apps = require('../apps.js'),
@@ -30,7 +34,8 @@ var apps = require('../apps.js'),
HttpSuccess = require('connect-lastmile').HttpSuccess,
paths = require('../paths.js'),
safe = require('safetydance'),
util = require('util');
util = require('util'),
WebSocket = require('ws');
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
@@ -47,7 +52,6 @@ function removeInternalAppFields(app) {
health: app.health,
location: app.location,
accessRestriction: app.accessRestriction,
lastBackupId: app.lastBackupId,
manifest: app.manifest,
portBindings: app.portBindings,
iconUrl: app.iconUrl,
@@ -58,7 +62,8 @@ function removeInternalAppFields(app) {
xFrameOptions: app.xFrameOptions,
sso: app.sso,
debugMode: app.debugMode,
robotsTxt: app.robotsTxt
robotsTxt: app.robotsTxt,
enableBackup: app.enableBackup
};
}
@@ -115,6 +120,7 @@ function installApp(req, res, next) {
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
if (data.backupId && typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be string or null'));
if (data.backupFormat && typeof data.backupFormat !== 'string') return next(new HttpError(400, 'backupFormat must be string or null'));
// falsy values in cert and key unset the cert
if (data.key && typeof data.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
@@ -130,6 +136,7 @@ function installApp(req, res, next) {
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
if ('sso' in data && typeof data.sso !== 'boolean') return next(new HttpError(400, 'sso must be a boolean'));
if ('enableBackup' in data && typeof data.enableBackup !== 'boolean') return next(new HttpError(400, 'enableBackup must be a boolean'));
if (('debugMode' in data) && typeof data.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object'));
@@ -171,6 +178,8 @@ function configureApp(req, res, next) {
if (data.altDomain && typeof data.altDomain !== 'string') return next(new HttpError(400, 'altDomain must be a string'));
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
if ('enableBackup' in data && typeof data.enableBackup !== 'boolean') return next(new HttpError(400, 'enableBackup must be a boolean'));
if (('debugMode' in data) && typeof data.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object'));
if (data.robotsTxt && typeof data.robotsTxt !== 'string') return next(new HttpError(400, 'robotsTxt must be a string'));
@@ -304,17 +313,15 @@ function updateApp(req, res, next) {
if ('appStoreId' in data && typeof data.appStoreId !== 'string') return next(new HttpError(400, 'appStoreId must be a string'));
if (!data.manifest && !data.appStoreId) return next(new HttpError(400, 'appStoreId or manifest is required'));
if ('portBindings' in data && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
if ('force' in data && typeof data.force !== 'boolean') return next(new HttpError(400, 'force must be a boolean'));
debug('Update app id:%s to manifest:%j with portBindings:%j', req.params.id, data.manifest, data.portBindings);
debug('Update app id:%s to manifest:%j', req.params.id, data.manifest);
apps.update(req.params.id, req.body, auditSource(req), function (error) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, { }));
@@ -455,6 +462,58 @@ function exec(req, res, next) {
});
}
function execWebSocket(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
debug('Execing websocket into app id:%s and cmd:%s', req.params.id, req.query.cmd);
var cmd = null;
if (req.query.cmd) {
cmd = safe.JSON.parse(req.query.cmd);
if (!util.isArray(cmd) || cmd.length < 1) return next(new HttpError(400, 'cmd must be array with atleast size 1'));
}
var columns = req.query.columns ? parseInt(req.query.columns, 10) : null;
if (isNaN(columns)) return next(new HttpError(400, 'columns must be a number'));
var rows = req.query.rows ? parseInt(req.query.rows, 10) : null;
if (isNaN(rows)) return next(new HttpError(400, 'rows must be a number'));
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));
console.log('Connected to terminal');
req.clearTimeout();
res.handleUpgrade(function (ws) {
duplexStream.on('end', function () { ws.close(); });
duplexStream.on('close', function () { ws.close(); });
duplexStream.on('error', function (error) {
console.error('duplexStream error:', error);
});
duplexStream.on('data', function (data) {
if (ws.readyState !== WebSocket.OPEN) return;
ws.send(data.toString());
});
ws.on('error', function (error) {
console.error('websocket error:', error);
});
ws.on('message', function (msg) {
duplexStream.write(msg);
});
ws.on('close', function () {
// Clean things up, if any?
});
});
});
}
function listBackups(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
@@ -471,3 +530,44 @@ function listBackups(req, res, next) {
next(new HttpSuccess(200, { backups: result }));
});
}
function uploadFile(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
debug('uploadFile: %s %j -> %s', req.params.id, req.files, req.query.file);
if (typeof req.query.file !== 'string' || !req.query.file) return next(new HttpError(400, 'file query argument must be provided'));
if (!req.files.file) return next(new HttpError(400, 'file must be provided as multipart'));
apps.uploadFile(req.params.id, req.files.file.path, req.query.file, function (error) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
debug('uploadFile: done');
next(new HttpSuccess(202, {}));
});
}
function downloadFile(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
debug('downloadFile: ', req.params.id, req.query.file);
if (typeof req.query.file !== 'string' || !req.query.file) return next(new HttpError(400, 'file query argument must be provided'));
apps.downloadFile(req.params.id, req.query.file, function (error, stream, info) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
var headers = {
'Content-Type': 'application/octet-stream',
'Content-Disposition': 'attachment; filename="' + info.filename + '"'
};
if (info.size) headers['Content-Length'] = info.size;
res.writeHead(200, headers);
stream.pipe(res);
});
}
+84 -24
View File
@@ -14,10 +14,14 @@ exports = module.exports = {
update: update,
feedback: feedback,
checkForUpdates: checkForUpdates,
getLogs: getLogs
getLogs: getLogs,
getLogStream: getLogStream,
sendTestMail: sendTestMail
};
var assert = require('assert'),
var appstore = require('../appstore.js'),
AppstoreError = require('../appstore.js').AppstoreError,
assert = require('assert'),
async = require('async'),
cloudron = require('../cloudron.js'),
CloudronError = cloudron.CloudronError,
@@ -64,13 +68,13 @@ function activate(req, res, next) {
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/setup/done').query({ setupToken: req.query.setupToken })
.timeout(30 * 1000)
.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 || 'Internal error'));
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 || 'Internal error'));
next(new HttpSuccess(201, info));
});
next(new HttpSuccess(201, info));
});
});
}
@@ -98,15 +102,15 @@ function setupTokenAuth(req, res, next) {
if (typeof req.query.setupToken !== 'string' || !req.query.setupToken) return next(new HttpError(400, 'setupToken must be a non empty string'));
superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/setup/verify').query({ setupToken:req.query.setupToken })
.timeout(30 * 1000)
.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 || 'Internal error'));
.timeout(30 * 1000)
.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 || 'Internal error'));
next();
});
next();
});
} else {
next();
}
@@ -140,7 +144,7 @@ function getStatus(req, res, next) {
}
function getProgress(req, res, next) {
return next(new HttpSuccess(200, progress.get()));
return next(new HttpSuccess(200, progress.getAll()));
}
function reboot(req, res, next) {
@@ -183,6 +187,10 @@ function getConfig(req, res, next) {
cloudron.getConfig(function (error, cloudronConfig) {
if (error) return next(new HttpError(500, error));
if (!req.user.admin) {
cloudronConfig = _.pick(cloudronConfig, 'apiServerOrigin', 'webServerOrigin', 'fqdn', 'version', 'progress', 'isCustomDomain', 'isDemo', 'cloudronName', 'provider');
}
next(new HttpSuccess(200, cloudronConfig));
});
}
@@ -218,17 +226,20 @@ function checkForUpdates(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_MISSING &&
req.body.type !== mailer.FEEDBACK_TYPE_UPGRADE_REQUEST &&
req.body.type !== mailer.FEEDBACK_TYPE_APP_ERROR) return next(new HttpError(400, 'type must be either "ticket", "feedback", "app_missing", "app_error" or "upgrade_request"'));
const VALID_TYPES = [ 'feedback', 'ticket', 'app_missing', 'app_error', 'upgrade_request' ];
if (typeof req.body.type !== 'string' || !req.body.type) return next(new HttpError(400, 'type must be string'));
if (VALID_TYPES.indexOf(req.body.type) === -1) return next(new HttpError(400, 'unknown type'));
if (typeof req.body.subject !== 'string' || !req.body.subject) return next(new HttpError(400, 'subject must be string'));
if (typeof req.body.description !== 'string' || !req.body.description) return next(new HttpError(400, 'description must be string'));
mailer.sendFeedback(req.user, req.body.type, req.body.subject, req.body.description);
appstore.sendFeedback(_.extend(req.body, { email: req.user.alternateEmail || req.user.email, displayName: req.user.displayName }), function (error) {
if (error && error.reason === AppstoreError.BILLING_REQUIRED) return next(new HttpError(402, 'Login to App Store to create support tickets. You can also email support@cloudron.io'));
if (error) return next(new HttpError(503, 'Error contacting cloudron.io. Please email support@cloudron.io'));
next(new HttpSuccess(201, {}));
});
next(new HttpSuccess(201, {}));
}
function getLogs(req, res, next) {
@@ -257,3 +268,52 @@ function getLogs(req, res, next) {
logStream.pipe(res);
});
}
function getLogStream(req, res, next) {
var lines = req.query.lines ? parseInt(req.query.lines, 10) : -10; // we ignore last-event-id
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
var units = req.query.units || 'all';
function sse(id, data) { return 'id: ' + id + '\ndata: ' + data + '\n\n'; }
if (req.headers.accept !== 'text/event-stream') return next(new HttpError(400, 'This API call requires EventStream'));
var options = {
lines: lines,
follow: true,
units: units.split(','),
format: req.query.format
};
cloudron.getLogs(options, function (error, logStream) {
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(404, 'Invalid type'));
if (error) return next(new HttpError(500, error));
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no', // disable nginx buffering
'Access-Control-Allow-Origin': '*'
});
res.write('retry: 3000\n');
res.on('close', logStream.close);
logStream.on('data', function (data) {
var obj = JSON.parse(data);
res.write(sse(obj.monotonicTimestamp, JSON.stringify(obj))); // send timestamp as id
});
logStream.on('end', res.end.bind(res));
logStream.on('error', res.end.bind(res, null));
});
}
function sendTestMail(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.email || typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be a non-empty string'));
mailer.sendTestMail(req.body.email);
next(new HttpSuccess(202));
}
+1 -8
View File
@@ -4,8 +4,7 @@ exports = module.exports = {
enabled: enabled,
setEnabled: setEnabled,
status: status,
login: login,
apps: apps
login: login
};
var developer = require('../developer.js'),
@@ -52,9 +51,3 @@ function login(req, res, next) {
})(req, res, next);
}
function apps(req, res, next) {
developer.getNonApprovedApps(function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { apps: result }));
});
}
+21
View File
@@ -3,6 +3,7 @@
var appdb = require('../appdb'),
apps = require('../apps'),
assert = require('assert'),
auth = require('../auth.js'),
authcodedb = require('../authcodedb'),
clients = require('../clients'),
ClientsError = clients.ClientsError,
@@ -533,6 +534,25 @@ function scope(requestedScope) {
];
}
function websocketAuth(requestedScopes, req, res, next) {
assert(Array.isArray(requestedScopes));
if (typeof req.query.access_token !== 'string') return next(new HttpError(401, 'Unauthorized'));
auth.accessTokenAuth(req.query.access_token, function (error, user, info) {
if (error) return next(new HttpError(500, error.message));
if (!user) return next(new HttpError(401, 'Unauthorized'));
req.user = user;
req.authInfo = info;
var error = validateRequestedScopes(req, requestedScopes);
if (error) return next(new HttpError(401, error.message));
next();
});
}
// Cross-site request forgery protection middleware for login form
var csrf = [
middleware.csrf(),
@@ -559,5 +579,6 @@ exports = module.exports = {
token: token,
validateRequestedScopes: validateRequestedScopes,
scope: scope,
websocketAuth: websocketAuth,
csrf: csrf
};
+2
View File
@@ -274,6 +274,8 @@ function setBackupConfig(req, res, next) {
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if (typeof req.body.retentionSecs !== 'number') return next(new HttpError(400, 'retentionSecs is required'));
if ('key' in req.body && typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if (typeof req.body.format !== 'string') return next(new HttpError(400, 'format must be a string'));
if ('acceptSelfSignedCerts' in req.body && typeof req.body.acceptSelfSignedCerts !== 'boolean') return next(new HttpError(400, 'format must be a boolean'));
settings.setBackupConfig(req.body, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
+31 -32
View File
@@ -22,7 +22,7 @@ var appdb = require('../../appdb.js'),
hock = require('hock'),
http = require('http'),
https = require('https'),
js2xml = require('js2xmlparser'),
js2xml = require('js2xmlparser').parse,
ldap = require('../../ldap.js'),
net = require('net'),
nock = require('nock'),
@@ -34,14 +34,14 @@ var appdb = require('../../appdb.js'),
taskmanager = require('../../taskmanager.js'),
tokendb = require('../../tokendb.js'),
url = require('url'),
uuid = require('node-uuid'),
uuid = require('uuid'),
_ = require('underscore');
var SERVER_URL = 'http://localhost:' + config.get('port');
// Test image information
var TEST_IMAGE_REPO = 'cloudron/test';
var TEST_IMAGE_TAG = '24.0.1';
var TEST_IMAGE_TAG = '25.2.0';
var TEST_IMAGE = TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG;
// var TEST_IMAGE_ID = child_process.execSync('docker inspect --format={{.Id}} ' + TEST_IMAGE).toString('utf8').trim();
@@ -269,7 +269,6 @@ describe('App API', function () {
it('app install fails - missing manifest', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('appStoreId or manifest is required');
@@ -280,7 +279,7 @@ describe('App API', function () {
it('app install fails - null manifest', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: null, password: PASSWORD })
.send({ manifest: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('appStoreId or manifest is required');
@@ -291,7 +290,7 @@ describe('App API', function () {
it('app install fails - bad manifest format', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: 'epic', password: PASSWORD })
.send({ manifest: 'epic' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('manifest must be an object');
@@ -302,7 +301,7 @@ describe('App API', function () {
it('app install fails - empty appStoreId format', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: null, appStoreId: '', password: PASSWORD })
.send({ manifest: null, appStoreId: '' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('appStoreId or manifest is required');
@@ -323,7 +322,7 @@ describe('App API', function () {
it('app install fails - invalid location', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: '!awesome', accessRestriction: null })
.send({ manifest: APP_MANIFEST, location: '!awesome', accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('Hostname can only contain alphanumerics and hyphen');
@@ -334,7 +333,7 @@ describe('App API', function () {
it('app install fails - invalid location type', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: 42, accessRestriction: null })
.send({ manifest: APP_MANIFEST, location: 42, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('location is required');
@@ -345,10 +344,10 @@ describe('App API', function () {
it('app install fails - reserved admin location', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: constants.ADMIN_LOCATION, accessRestriction: null })
.send({ manifest: APP_MANIFEST, location: 'my', accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql(constants.ADMIN_LOCATION + ' is reserved');
expect(res.body.message).to.eql('my is reserved');
done();
});
});
@@ -356,7 +355,7 @@ describe('App API', function () {
it('app install fails - reserved api location', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: constants.API_LOCATION, accessRestriction: null })
.send({ manifest: APP_MANIFEST, location: constants.API_LOCATION, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql(constants.API_LOCATION + ' is reserved');
@@ -367,7 +366,7 @@ describe('App API', function () {
it('app install fails - portBindings must be object', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: 23, accessRestriction: null })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: 23, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('portBindings must be an object');
@@ -378,7 +377,7 @@ describe('App API', function () {
it('app install fails - accessRestriction is required', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: {} })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: {} })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('accessRestriction is required');
@@ -389,7 +388,7 @@ describe('App API', function () {
it('app install fails - accessRestriction type is wrong', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: {}, accessRestriction: '' })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: {}, accessRestriction: '' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('accessRestriction is required');
@@ -400,7 +399,7 @@ describe('App API', function () {
it('app install fails for non admin', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token_1 })
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: null })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
@@ -412,7 +411,7 @@ describe('App API', function () {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: { users: [ 'someuser' ], groups: [] } })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, portBindings: null, accessRestriction: { users: [ 'someuser' ], groups: [] } })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(fake.isDone()).to.be.ok();
@@ -426,7 +425,7 @@ describe('App API', function () {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: null })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(503);
expect(fake1.isDone()).to.be.ok();
@@ -442,7 +441,7 @@ describe('App API', function () {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: { users: [ 'someuser' ], groups: [] } })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, portBindings: null, accessRestriction: { users: [ 'someuser' ], groups: [] } })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(res.body.id).to.be.a('string');
@@ -455,7 +454,7 @@ describe('App API', function () {
it('app install fails because of conflicting location', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: null })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
done();
@@ -565,7 +564,7 @@ describe('App API', function () {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, password: PASSWORD, location: APP_LOCATION_2, portBindings: null, accessRestriction: null })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION_2, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(res.body.id).to.be.a('string');
@@ -649,7 +648,7 @@ describe('App installation', function () {
awsHockInstance
.get('/2013-04-01/hostedzone')
.max(Infinity)
.reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { arrayMap: { HostedZones: 'HostedZone'} }), { 'Content-Type': 'application/xml' })
.reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { wrapHandlers: { HostedZones: () => 'HostedZone'} }), { 'Content-Type': 'application/xml' })
.filteringPathRegEx(/name=[^&]*/, 'name=location')
.get('/2013-04-01/hostedzone/ZONEID/rrset?maxitems=1&name=location&type=A')
.max(Infinity)
@@ -695,7 +694,7 @@ describe('App installation', function () {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, password: PASSWORD, location: APP_LOCATION, portBindings: { ECHO_SERVER_PORT: 7171 }, accessRestriction: null })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, portBindings: { ECHO_SERVER_PORT: 7171 }, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(fake1.isDone()).to.be.ok();
@@ -986,7 +985,7 @@ describe('App installation', function () {
it('cannot reconfigure app with bad location', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ password: PASSWORD, location: 1234, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null })
.send({ location: 1234, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
@@ -996,7 +995,7 @@ describe('App installation', function () {
it('cannot reconfigure app with bad accessRestriction', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ password: PASSWORD, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: false })
.send({ portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
@@ -1006,7 +1005,7 @@ describe('App installation', function () {
it('cannot reconfigure app with only the cert, no key', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, cert: validCert1 })
.send({ location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, cert: validCert1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
@@ -1016,7 +1015,7 @@ describe('App installation', function () {
it('cannot reconfigure app with only the key, no cert', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, key: validKey1 })
.send({ location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, key: validKey1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
@@ -1026,7 +1025,7 @@ describe('App installation', function () {
it('cannot reconfigure app with cert not being a string', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, cert: 1234, key: validKey1 })
.send({ location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, cert: 1234, key: validKey1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
@@ -1036,7 +1035,7 @@ describe('App installation', function () {
it('cannot reconfigure app with key not being a string', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, cert: validCert1, key: 1234 })
.send({ location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, cert: validCert1, key: 1234 })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
@@ -1046,7 +1045,7 @@ describe('App installation', function () {
it('non admin cannot reconfigure app', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token_1 })
.send({ password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null })
.send({ location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
@@ -1056,7 +1055,7 @@ describe('App installation', function () {
it('can reconfigure app', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 } })
.send({ location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 } })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
checkConfigureStatus(0, done);
@@ -1098,7 +1097,7 @@ describe('App installation', function () {
it('can reconfigure app with custom certificate', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, cert: validCert1, key: validKey1 })
.send({ location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, cert: validCert1, key: validKey1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
checkConfigureStatus(0, done);
+32 -45
View File
@@ -10,20 +10,16 @@ var appdb = require('../../appdb.js'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
hock = require('hock'),
http = require('http'),
nock = require('nock'),
superagent = require('superagent'),
server = require('../../server.js'),
settings = require('../../settings.js'),
url = require('url');
settings = require('../../settings.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
var server;
function setup(done) {
nock.cleanAll();
config._reset();
@@ -40,19 +36,19 @@ function setup(done) {
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();
.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;
// stash token for further use
token = result.body.token;
callback();
});
callback();
});
},
function addApp(callback) {
@@ -61,7 +57,7 @@ function setup(done) {
},
function createSettings(callback) {
settings.setBackupConfig({ provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' }, callback);
settings.setBackupConfig({ provider: 'filesystem', backupFolder: '/tmp', format: 'tgz' }, callback);
}
], done);
}
@@ -75,19 +71,12 @@ function cleanup(done) {
}
describe('Backups API', function () {
var apiHockInstance = hock.createHock({ throwOnUnmatched: false }), apiHockServer;
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey' } }, { 'Content-Type': 'application/json' });
before(setup);
before(function (done) {
apiHockInstance
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey' } }, { 'Content-Type': 'application/json' });
var port = parseInt(url.parse(config.apiServerOrigin()).port, 10);
apiHockServer = http.createServer(apiHockInstance.handler).listen(port, done);
});
after(function (done) {
apiHockServer.close();
done();
});
after(cleanup);
@@ -95,37 +84,35 @@ describe('Backups API', function () {
describe('create', function () {
it('fails due to mising token', function (done) {
superagent.post(SERVER_URL + '/api/v1/backups')
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails due to wrong token', function (done) {
superagent.post(SERVER_URL + '/api/v1/backups')
.query({ access_token: token.toUpperCase() })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
.query({ access_token: token.toUpperCase() })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/backups')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
function checkAppstoreServerCalled() {
apiHockInstance.done(function (error) {
if (!error) return done();
function checkAppstoreServerCalled() {
if (scope1.isDone()) return done();
setTimeout(checkAppstoreServerCalled, 100);
});
}
}
checkAppstoreServerCalled();
});
checkAppstoreServerCalled();
});
});
});
});
+1 -1
View File
@@ -12,7 +12,7 @@ var async = require('async'),
database = require('../../database.js'),
oauth2 = require('../oauth2.js'),
expect = require('expect.js'),
uuid = require('node-uuid'),
uuid = require('uuid'),
nock = require('nock'),
hat = require('hat'),
superagent = require('superagent'),
+407 -290
View File
@@ -9,20 +9,22 @@ var async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
http = require('http'),
locker = require('../../locker.js'),
nock = require('nock'),
os = require('os'),
superagent = require('superagent'),
server = require('../../server.js'),
settings = require('../../settings.js'),
shell = require('../../shell.js');
shell = require('../../shell.js'),
tokendb = require('../../tokendb.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null; // authentication token
var USERNAME_1 = 'userTheFirst', EMAIL_1 = 'taO@zen.mac', userId_1, token_1;
var server;
function setup(done) {
nock.cleanAll();
config._reset();
@@ -31,7 +33,7 @@ function setup(done) {
server.start(function (error) {
if (error) return done(error);
settings.setBackupConfig({ provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' }, done);
settings.setBackupConfig({ provider: 'filesystem', backupFolder: '/tmp', format: 'tgz' }, done);
});
}
@@ -64,89 +66,89 @@ describe('Cloudron', function () {
it('fails due to missing setupToken', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.send({ username: '', password: 'somepassword', email: 'admin@foo.bar' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
.send({ username: '', password: 'somepassword', email: 'admin@foo.bar' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
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();
});
.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: 'ADSFsdf$%436', email: 'admin@foo.bar' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
expect(scope.isDone()).to.be.ok();
done();
});
.query({ setupToken: 'somesetuptoken' })
.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();
done();
});
});
it('fails due to empty password', 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' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
expect(scope.isDone()).to.be.ok();
done();
});
.query({ setupToken: 'somesetuptoken' })
.send({ username: 'someuser', password: '', email: 'admin@foo.bar' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
expect(scope.isDone()).to.be.ok();
done();
});
});
it('fails due to empty email', 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: 'ADSF#asd546', email: '' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
expect(scope.isDone()).to.be.ok();
done();
});
.query({ setupToken: 'somesetuptoken' })
.send({ username: 'someuser', password: 'ADSF#asd546', email: '' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
expect(scope.isDone()).to.be.ok();
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: 'ADSF?#asd546', email: 'admin@foo.bar', displayName: 1234 })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
expect(scope.isDone()).to.be.ok();
done();
});
.query({ setupToken: 'somesetuptoken' })
.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();
done();
});
});
it('fails due to invalid email', 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: 'ADSF#asd546', email: 'invalidemail' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
expect(scope.isDone()).to.be.ok();
done();
});
.query({ setupToken: 'somesetuptoken' })
.send({ username: 'someuser', password: 'ADSF#asd546', email: 'invalidemail' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
expect(scope.isDone()).to.be.ok();
done();
});
});
it('succeeds', function (done) {
@@ -154,27 +156,27 @@ describe('Cloudron', function () {
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: '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();
expect(scope2.isDone()).to.be.ok();
done();
});
.query({ setupToken: 'somesetuptoken' })
.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();
expect(scope2.isDone()).to.be.ok();
done();
});
});
it('fails the second time', 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: 'ADSF#asd546', email: 'admin@foo.bar' })
.end(function (error, result) {
expect(result.statusCode).to.equal(409);
expect(scope.isDone()).to.be.ok();
done();
});
.query({ setupToken: 'somesetuptoken' })
.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();
done();
});
});
});
@@ -187,22 +189,36 @@ describe('Cloudron', function () {
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();
.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;
// stash token for further use
token = result.body.token;
callback();
});
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();
userId_1 = result.body.id;
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, userId_1, 'test-client-id', Date.now() + 100000, '*', callback);
});
}
], done);
});
@@ -210,60 +226,85 @@ describe('Cloudron', function () {
it('cannot get without token', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/config')
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('succeeds without appstore', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/config')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.apiServerOrigin).to.eql('http://localhost:6060');
expect(result.body.webServerOrigin).to.eql(null);
expect(result.body.fqdn).to.eql(config.fqdn());
expect(result.body.isCustomDomain).to.eql(true);
expect(result.body.progress).to.be.an('object');
expect(result.body.update).to.be.an('object');
expect(result.body.version).to.eql(config.version());
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(os.totalmem());
expect(result.body.cloudronName).to.be.a('string');
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.apiServerOrigin).to.eql('http://localhost:6060');
expect(result.body.webServerOrigin).to.eql(null);
expect(result.body.fqdn).to.eql(config.fqdn());
expect(result.body.isCustomDomain).to.eql(true);
expect(result.body.progress).to.be.an('object');
expect(result.body.update).to.be.an('object');
expect(result.body.version).to.eql(config.version());
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(os.totalmem());
expect(result.body.cloudronName).to.be.a('string');
done();
});
done();
});
});
it('succeeds', function (done) {
it('succeeds (admin)', function (done) {
var scope = nock(config.apiServerOrigin())
.get('/api/v1/boxes/localhost?token=' + config.token())
.reply(200, { box: { region: 'sfo', size: '1gb' }, user: { }});
.get('/api/v1/boxes/localhost?token=' + config.token())
.reply(200, { box: { region: 'sfo', size: '1gb' }, user: { }});
superagent.get(SERVER_URL + '/api/v1/cloudron/config')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.apiServerOrigin).to.eql('http://localhost:6060');
expect(result.body.webServerOrigin).to.eql(null);
expect(result.body.fqdn).to.eql(config.fqdn());
expect(result.body.isCustomDomain).to.eql(true);
expect(result.body.progress).to.be.an('object');
expect(result.body.update).to.be.an('object');
expect(result.body.version).to.eql(config.version());
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(os.totalmem());
expect(result.body.cloudronName).to.be.a('string');
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.apiServerOrigin).to.eql('http://localhost:6060');
expect(result.body.webServerOrigin).to.eql(null);
expect(result.body.fqdn).to.eql(config.fqdn());
expect(result.body.isCustomDomain).to.eql(true);
expect(result.body.progress).to.be.an('object');
expect(result.body.update).to.be.an('object');
expect(result.body.version).to.eql(config.version());
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(os.totalmem());
expect(result.body.cloudronName).to.be.a('string');
expect(result.body.provider).to.be.a('string');
expect(scope.isDone()).to.be.ok();
expect(scope.isDone()).to.be.ok();
done();
});
done();
});
});
it('succeeds (non-admin)', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/config')
.query({ access_token: token_1 })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.apiServerOrigin).to.eql('http://localhost:6060');
expect(result.body.webServerOrigin).to.eql(null);
expect(result.body.fqdn).to.eql(config.fqdn());
expect(result.body.isCustomDomain).to.eql(true);
expect(result.body.progress).to.be.an('object');
expect(result.body.version).to.eql(config.version());
expect(result.body.cloudronName).to.be.a('string');
expect(result.body.provider).to.be.a('string');
expect(result.body.update).to.be(undefined);
expect(result.body.size).to.be(undefined);
expect(result.body.region).to.be(undefined);
expect(result.body.memory).to.be(undefined);
done();
});
});
});
@@ -278,18 +319,18 @@ describe('Cloudron', function () {
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(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
.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;
// stash token for further use
token = result.body.token;
callback();
});
callback();
});
}
], done);
});
@@ -301,73 +342,73 @@ describe('Cloudron', function () {
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();
});
.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();
});
.send({ size: 'small', region: 'sfo'})
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds without 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(202);
done();
});
.send({ region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
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();
});
.send({ size: 4, region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds without 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(202);
done();
});
.send({ size: 'small', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
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();
});
.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' } });
.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' });
.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) {
@@ -377,22 +418,22 @@ describe('Cloudron', function () {
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);
.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();
function checkAppstoreServerCalled() {
if (scope1.isDone() && scope2.isDone() && scope3.isDone()) {
restoreShellMock();
return done();
}
setTimeout(checkAppstoreServerCalled, 100);
}
setTimeout(checkAppstoreServerCalled, 100);
}
checkAppstoreServerCalled();
});
checkAppstoreServerCalled();
});
});
it('succeeds', function (done) {
@@ -401,34 +442,34 @@ describe('Cloudron', function () {
}).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' });
.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' } });
.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);
.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();
function checkAppstoreServerCalled() {
if (scope1.isDone() && scope2.isDone() && scope3.isDone()) {
restoreShellMock();
return done();
}
setTimeout(checkAppstoreServerCalled, 100);
}
setTimeout(checkAppstoreServerCalled, 100);
}
checkAppstoreServerCalled();
});
checkAppstoreServerCalled();
});
});
});
@@ -441,21 +482,19 @@ describe('Cloudron', function () {
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();
.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;
// stash token for further use
token = result.body.token;
callback();
});
callback();
});
},
], done);
});
@@ -464,111 +503,189 @@ describe('Cloudron', function () {
it('fails without token', function (done) {
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ type: 'ticket', subject: 'some subject', description: 'some description' })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
.send({ type: 'ticket', subject: 'some subject', description: 'some description' })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails without type', function (done) {
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
.send({ subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty type', function (done) {
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ type: '', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
.send({ type: '', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with unknown type', function (done) {
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ type: 'foobar', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds with ticket type', function (done) {
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ type: 'ticket', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
done();
});
});
it('succeeds with app type', function (done) {
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ type: 'app_missing', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
done();
});
.send({ type: 'foobar', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails without description', function (done) {
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ type: 'ticket', subject: 'some subject' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
.send({ type: 'ticket', subject: 'some subject' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty subject', function (done) {
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ type: 'ticket', subject: '', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
.send({ type: 'ticket', subject: '', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty description', function (done) {
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ type: 'ticket', subject: 'some subject', description: '' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds with feedback type', function (done) {
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ type: 'feedback', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
done();
});
.send({ type: 'ticket', subject: 'some subject', description: '' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails without subject', function (done) {
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ type: 'ticket', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
.send({ type: 'ticket', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds with ticket type', function (done) {
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/exchangeBoxTokenWithUserToken?token=APPSTORE_TOKEN').reply(201, { userId: 'USER_ID', cloudronId: 'CLOUDRON_ID', token: 'ACCESS_TOKEN' });
var scope2 = nock(config.apiServerOrigin())
.filteringRequestBody(function (/* unusedBody */) { return ''; }) // strip out body
.post('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/feedback?accessToken=ACCESS_TOKEN')
.reply(201, { });
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ type: 'ticket', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
done();
});
});
it('succeeds with app type', function (done) {
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/exchangeBoxTokenWithUserToken?token=APPSTORE_TOKEN').reply(201, { userId: 'USER_ID', cloudronId: 'CLOUDRON_ID', token: 'ACCESS_TOKEN' });
var scope2 = nock(config.apiServerOrigin())
.filteringRequestBody(function (/* unusedBody */) { return ''; }) // strip out body
.post('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/feedback?accessToken=ACCESS_TOKEN')
.reply(201, { });
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ type: 'app_missing', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
done();
});
});
});
describe('logs', 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, {});
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();
});
},
], done);
});
after(cleanup);
it('logStream - requires event-stream accept header', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/logstream')
.query({ access_token: token, fromLine: 0 })
.end(function (err, res) {
expect(res.statusCode).to.be(400);
done();
});
});
it('logStream - stream logs', function (done) {
var options = {
port: config.get('port'), host: 'localhost', path: '/api/v1/cloudron/logstream?units=all&lines=10&access_token=' + token,
headers: { 'Accept': 'text/event-stream', 'Connection': 'keep-alive' }
};
// superagent doesn't work. maybe https://github.com/visionmedia/superagent/issues/420
var req = http.get(options, function (res) {
var data = '';
res.on('data', function (d) { data += d.toString('utf8'); });
setTimeout(function checkData() {
var dataMessageFound = false;
expect(data.length).to.not.be(0);
data.split('\n').forEach(function (line) {
if (line.indexOf('id: ') === 0) {
expect(parseInt(line.substr('id: '.length), 10)).to.be.a('number');
} else if (line.indexOf('data: ') === 0) {
var message = JSON.parse(line.slice('data: '.length)).message;
if (Array.isArray(message) || typeof message === 'string') dataMessageFound = true;
}
});
expect(dataMessageFound).to.be.ok();
req.abort();
done();
}, 1000);
res.on('error', done);
});
req.on('error', done);
});
});
});
+1 -1
View File
@@ -7,7 +7,7 @@
'use strict';
var expect = require('expect.js'),
uuid = require('node-uuid'),
uuid = require('uuid'),
async = require('async'),
hat = require('hat'),
urlParse = require('url').parse,
+1 -1
View File
@@ -70,7 +70,7 @@ describe('REST API', function () {
.send("some invalid non-strict json")
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
expect(result.body.message).to.be('Bad JSON');
expect(result.body.message).to.be('Failed to parse body');
done();
});
});
+36 -26
View File
@@ -12,18 +12,23 @@ var appdb = require('../../appdb.js'),
expect = require('expect.js'),
hock = require('hock'),
http = require('http'),
MockS3 = require('mock-aws-s3'),
nock = require('nock'),
superagent = require('superagent'),
os = require('os'),
path = require('path'),
rimraf = require('rimraf'),
s3 = require('../../storage/s3.js'),
safe = require('safetydance'),
server = require('../../server.js'),
settings = require('../../settings.js'),
settingsdb = require('../../settingsdb.js'),
superagent = require('superagent'),
url = require('url');
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
var server;
function setup(done) {
config.setVersion('1.2.3');
@@ -37,19 +42,16 @@ function setup(done) {
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();
.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();
});
callback();
});
},
function addApp(callback) {
@@ -58,12 +60,20 @@ function setup(done) {
},
function createSettings(callback) {
settings.setBackupConfig({ provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' }, callback);
MockS3.config.basePath = path.join(os.tmpdir(), 's3-sysadmin-test-buckets/');
s3._mockInject(MockS3);
safe.fs.mkdirSync('/tmp/box-sysadmin-test');
settingsdb.set(settings.BACKUP_CONFIG_KEY, JSON.stringify({ provider: 'caas', token: 'BACKUP_TOKEN', key: 'key', prefix: 'boxid', format: 'tgz'}), callback);
}
], done);
}
function cleanup(done) {
s3._mockRestore();
rimraf.sync(MockS3.config.basePath);
database._clear(function (error) {
expect(!error).to.be.ok();
@@ -93,19 +103,19 @@ describe('Internal API', function () {
describe('backup', function () {
it('succeeds', function (done) {
superagent.post(config.sysadminOrigin() + '/api/v1/backup')
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
function checkAppstoreServerCalled() {
apiHockInstance.done(function (error) {
if (!error) return done();
function checkAppstoreServerCalled() {
apiHockInstance.done(function (error) {
if (!error) return done();
setTimeout(checkAppstoreServerCalled, 100);
});
}
setTimeout(checkAppstoreServerCalled, 100);
});
}
checkAppstoreServerCalled();
});
checkAppstoreServerCalled();
});
});
});
});
+2 -2
View File
@@ -145,8 +145,8 @@ function doTask(appId, taskName, callback) {
apps.get(appId, function (error, app) {
if (error) return callback(error);
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
debug('task %s skipped. app %s is not installed/running', taskName, app.id);
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING || app.health !== appdb.HEALTH_HEALTHY) {
debug('task %s skipped. app %s is not installed/running/healthy', taskName, app.id);
return callback();
}
@@ -12,6 +12,9 @@ if [[ $# == 1 && "$1" == "--check" ]]; then
exit 0
fi
cmd="$1"
appid="$2"
if [[ "${BOX_ENV}" == "cloudron" ]]; then
# when restoring the cloudron with many apps, the apptasks rush in to restart
# collectd which makes systemd/collectd very unhappy and puts the collectd in
@@ -19,10 +22,17 @@ if [[ "${BOX_ENV}" == "cloudron" ]]; then
for i in {1..10}; do
echo "Restarting collectd"
if systemctl restart collectd; then
exit 0
break
fi
echo "Failed to reload collectd. Maybe some other apptask is restarting it"
sleep $((RANDOM%30))
done
# delete old stats when uninstalling an app
if [[ "${cmd}" == "remove" ]]; then
echo "Removing collectd stats of ${appid}"
rm -rf ${HOME}/platformdata/graphite/whisper/collectd/localhost/*${appid}*
fi
fi
+40
View File
@@ -0,0 +1,40 @@
#!/bin/bash
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
if [[ $# -eq 0 ]]; then
echo "No arguments supplied"
exit 1
fi
if [[ "$1" == "--check" ]]; then
echo "OK"
exit 0
fi
cmd="$1"
appid="$2"
if [[ "${cmd}" == "add" ]]; then
# TODO prevent this script from moving the file from $1 into a random dir with using a relative ../ path
if [[ "${BOX_ENV}" == "cloudron" ]]; then
readonly destination_file_path="${HOME}/platformdata/logrotate.d/${appid}"
else
readonly destination_file_path="${HOME}/.cloudron_test/platformdata/logrotate.d/${appid}"
fi
mv "${3}" "${destination_file_path}"
chown root:root "${destination_file_path}"
elif [[ "${cmd}" == "remove" ]]; then
if [[ "${BOX_ENV}" == "cloudron" ]]; then
rm -rf "${HOME}/platformdata/logrotate.d/${appid}"
else
rm -rf "${HOME}/.cloudron_test/platformdata/logrotate.d/${appid}"
fi
fi
-23
View File
@@ -1,23 +0,0 @@
#!/bin/bash
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
if [[ $# -eq 0 ]]; then
echo "No arguments supplied"
exit 1
fi
if [[ "$1" == "--check" ]]; then
echo "OK"
exit 0
fi
echo "Running node with memory constraints"
# note BOX_ENV and NODE_ENV are derived from parent process
exec env "DEBUG=box*,connect-lastmile" /usr/bin/node --max_old_space_size=300 "$@"
+14 -3
View File
@@ -17,10 +17,21 @@ if [[ "$1" == "--check" ]]; then
exit 0
fi
# this script is called from redis addon as well!
appid="$1"
rmdir="$2"
if [[ "${BOX_ENV}" == "cloudron" ]]; then
readonly app_data_dir="${HOME}/appsdata/$1"
rm -rf "${app_data_dir}"
readonly app_data_dir="${HOME}/appsdata/${appid}"
else
readonly app_data_dir="${HOME}/.cloudron_test/appsdata/$1"
readonly app_data_dir="${HOME}/.cloudron_test/appsdata/${appid}"
fi
# the approach below ensures symlinked contents are also deleted
find -H "${app_data_dir}" -mindepth 1 -delete || true # -H means resolve symlink in args
if [[ "${rmdir}" == "true" ]]; then
rm -rf "${app_data_dir}"
fi
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env node
'use strict';
require('supererror')({ splatchError: true });
var tar = require('tar-fs');
var sourceDir = process.argv[2];
if (sourceDir === '--check') return console.log('OK');
process.stderr.write('Packing ' + sourceDir + '\n');
tar.pack('/', {
dereference: false, // pack the symlink and not what it points to
entries: [ sourceDir ],
map: function(header) {
header.name = header.name.replace(new RegExp('^' + sourceDir + '(/?)'), '.$1'); // make paths relative
return header;
},
strict: false // do not error for unknown types (skip fifo, char/block devices)
}).pipe(process.stdout);
+53 -38
View File
@@ -19,7 +19,8 @@ var assert = require('assert'),
middleware = require('./middleware'),
passport = require('passport'),
path = require('path'),
routes = require('./routes/index.js');
routes = require('./routes/index.js'),
ws = require('ws');
var gHttpServer = null;
var gSysadminHttpServer = null;
@@ -28,6 +29,8 @@ function initializeExpressSync() {
var app = express();
var httpServer = http.createServer(app);
const wsServer = new ws.Server({ noServer: true }); // in noServer mode, we have to handle 'upgrade' and call handleUpgrade
var QUERY_LIMIT = '1mb', // max size for json and urlencoded queries (see also client_max_body_size in nginx)
FIELD_LIMIT = 2 * 1024 * 1024; // max fields that can appear in multipart
@@ -56,29 +59,29 @@ function initializeExpressSync() {
router.del = router.delete; // amend router.del for readability further on
app
.use(middleware.timeout(REQUEST_TIMEOUT))
.use(json)
.use(urlencoded)
.use(middleware.cookieParser())
.use(middleware.cors({ origins: [ '*' ], allowCredentials: false }))
.use(middleware.session({
secret: hat(128), // we only use the session during oauth, and already have an in-memory session store, so we can safely change that during restarts
resave: true,
saveUninitialized: true,
cookie: {
path: '/',
httpOnly: true,
secure: process.env.BOX_ENV !== 'test',
maxAge: 600000
}
}))
.use(passport.initialize())
.use(passport.session())
.use(router)
.use(middleware.lastMile());
.use(middleware.timeout(REQUEST_TIMEOUT))
.use(json)
.use(urlencoded)
.use(middleware.cookieParser())
.use(middleware.cors({ origins: [ '*' ], allowCredentials: false }))
.use(middleware.session({
secret: hat(128), // we only use the session during oauth, and already have an in-memory session store, so we can safely change that during restarts
resave: true,
saveUninitialized: true,
cookie: {
path: '/',
httpOnly: true,
secure: process.env.BOX_ENV !== 'test',
maxAge: 600000
}
}))
.use(passport.initialize())
.use(passport.session())
.use(router)
.use(middleware.lastMile());
// NOTE: these limits have to be in sync with nginx limits
var FILE_SIZE_LIMIT = '1mb', // max file size that can be uploaded (see also client_max_body_size in nginx)
var FILE_SIZE_LIMIT = '256mb', // max file size that can be uploaded (see also client_max_body_size in nginx)
FILE_TIMEOUT = 60 * 1000; // increased timeout for file uploads (1 min)
var multipart = middleware.multipart({ maxFieldsSize: FIELD_LIMIT, limit: FILE_SIZE_LIMIT, timeout: FILE_TIMEOUT });
@@ -105,22 +108,23 @@ function initializeExpressSync() {
router.post('/api/v1/developer', developerScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.developer.setEnabled);
router.get ('/api/v1/developer', developerScope, routes.developer.enabled, routes.developer.status);
router.post('/api/v1/developer/login', routes.developer.enabled, routes.developer.login);
router.get ('/api/v1/developer/apps', developerScope, routes.developer.enabled, routes.developer.apps);
// cloudron routes
router.get ('/api/v1/cloudron/config', cloudronScope, routes.cloudron.getConfig);
router.post('/api/v1/cloudron/update', cloudronScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.update);
router.post('/api/v1/cloudron/update', cloudronScope, routes.user.requireAdmin, routes.cloudron.update);
router.post('/api/v1/cloudron/check_for_updates', cloudronScope, routes.user.requireAdmin, routes.cloudron.checkForUpdates);
router.post('/api/v1/cloudron/reboot', cloudronScope, routes.user.requireAdmin, routes.cloudron.reboot);
router.post('/api/v1/cloudron/migrate', cloudronScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.migrate);
router.get ('/api/v1/cloudron/graphs', cloudronScope, routes.user.requireAdmin, routes.graphs.getGraphs);
router.get ('/api/v1/cloudron/disks', cloudronScope, routes.user.requireAdmin, routes.cloudron.getDisks);
router.get ('/api/v1/cloudron/logs', cloudronScope, routes.user.requireAdmin, routes.cloudron.getLogs);
router.get ('/api/v1/cloudron/logstream', cloudronScope, routes.user.requireAdmin, routes.cloudron.getLogStream);
router.get ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, routes.user.requireAdmin, routes.ssh.getAuthorizedKeys);
router.put ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, routes.user.requireAdmin, routes.ssh.addAuthorizedKey);
router.get ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, routes.user.requireAdmin, routes.ssh.getAuthorizedKey);
router.del ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, routes.user.requireAdmin, routes.ssh.delAuthorizedKey);
router.get ('/api/v1/cloudron/eventlog', settingsScope, routes.user.requireAdmin, routes.eventlog.get);
router.get ('/api/v1/cloudron/eventlog', cloudronScope, routes.user.requireAdmin, routes.eventlog.get);
router.post('/api/v1/cloudron/send_test_mail', cloudronScope, routes.user.requireAdmin, routes.cloudron.sendTestMail);
// profile api, working off the user behind the provided token
router.get ('/api/v1/profile', profileScope, routes.profile.get);
@@ -178,8 +182,8 @@ function initializeExpressSync() {
router.post('/api/v1/apps/install', appsScope, routes.user.requireAdmin, routes.apps.installApp);
router.post('/api/v1/apps/:id/uninstall', appsScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.apps.uninstallApp);
router.post('/api/v1/apps/:id/configure', appsScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.apps.configureApp);
router.post('/api/v1/apps/:id/update', appsScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.apps.updateApp);
router.post('/api/v1/apps/:id/configure', appsScope, routes.user.requireAdmin, routes.apps.configureApp);
router.post('/api/v1/apps/:id/update', appsScope, routes.user.requireAdmin, routes.apps.updateApp);
router.post('/api/v1/apps/:id/restore', appsScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.apps.restoreApp);
router.post('/api/v1/apps/:id/backup', appsScope, routes.user.requireAdmin, routes.apps.backupApp);
router.get ('/api/v1/apps/:id/backups', appsScope, routes.user.requireAdmin, routes.apps.listBackups);
@@ -188,7 +192,11 @@ function initializeExpressSync() {
router.get ('/api/v1/apps/:id/logstream', appsScope, routes.user.requireAdmin, routes.apps.getLogStream);
router.get ('/api/v1/apps/:id/logs', appsScope, routes.user.requireAdmin, routes.apps.getLogs);
router.get ('/api/v1/apps/:id/exec', routes.developer.enabled, appsScope, routes.user.requireAdmin, routes.apps.exec);
// websocket cannot do bearer authentication
router.get ('/api/v1/apps/:id/execws', routes.oauth2.websocketAuth.bind(null, [ clients.SCOPE_APPS ]), routes.user.requireAdmin, routes.apps.execWebSocket);
router.post('/api/v1/apps/:id/clone', appsScope, routes.user.requireAdmin, routes.apps.cloneApp);
router.get ('/api/v1/apps/:id/download', appsScope, routes.user.requireAdmin, routes.apps.downloadFile);
router.post('/api/v1/apps/:id/upload', appsScope, routes.user.requireAdmin, multipart, routes.apps.uploadFile);
// settings routes (these are for the settings tab - avatar & name have public routes for normal users. see above)
router.get ('/api/v1/settings/autoupdate_pattern', settingsScope, routes.user.requireAdmin, routes.settings.getAutoupdatePattern);
@@ -236,12 +244,19 @@ function initializeExpressSync() {
// create a node response object for express
var res = new http.ServerResponse({});
res.assignSocket(socket);
res.sendUpgradeHandshake = function () { // could extend express.response as well
socket.write('HTTP/1.1 101 TCP Handshake\r\n' +
'Upgrade: tcp\r\n' +
'Connection: Upgrade\r\n' +
'\r\n');
};
if (req.headers.upgrade === 'websocket') {
res.handleUpgrade = function (callback) {
wsServer.handleUpgrade(req, socket, head, callback);
};
} else {
res.sendUpgradeHandshake = function () { // could extend express.response as well
socket.write('HTTP/1.1 101 TCP Handshake\r\n' +
'Upgrade: tcp\r\n' +
'Connection: Upgrade\r\n' +
'\r\n');
};
}
// route through express middleware. if we provide no callback, express will provide a 'finalhandler'
// TODO: it's not clear if socket needs to be destroyed
@@ -268,11 +283,11 @@ function initializeSysadminExpressSync() {
router.del = router.delete; // amend router.del for readability further on
app
.use(middleware.timeout(REQUEST_TIMEOUT))
.use(json)
.use(urlencoded)
.use(router)
.use(middleware.lastMile());
.use(middleware.timeout(REQUEST_TIMEOUT))
.use(json)
.use(urlencoded)
.use(router)
.use(middleware.lastMile());
// Sysadmin routes
router.post('/api/v1/backup', routes.sysadmin.backup);

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