Compare commits

..

171 Commits

Author SHA1 Message Date
Johannes Zellner 9854598648 Fix typo to repair oauth and simple auth login
Second time breakage, time for a test ;-)
2015-10-13 21:55:02 +02:00
Johannes Zellner 1e7e2e5e97 Remove decision dialog related route 2015-10-13 20:39:08 +02:00
Johannes Zellner 081e496878 Remove unused oauth decision dialog 2015-10-13 20:32:27 +02:00
Johannes Zellner aaff7f463a Cleanup the authorization endpoint 2015-10-13 18:23:32 +02:00
Girish Ramakrishnan 55f937bf51 SIMPLE_AUTH_URL -> SIMPLE_AUTH_ORIGIN 2015-10-13 08:40:41 -07:00
Johannes Zellner d5d1d061bb We also allow non admins to use the webadmin 2015-10-13 15:13:36 +02:00
Johannes Zellner bc6f602891 Remove unused angular filter for accessRestrictionLabel 2015-10-13 15:11:30 +02:00
Johannes Zellner ca461057e7 Also update the test image id 2015-10-13 14:24:53 +02:00
Johannes Zellner b1c5c2468a Fix test to support docker api up to 1.19 and v1.20 2015-10-13 14:24:41 +02:00
Johannes Zellner 562ce3192f Print error when apptask.pullImage() failed 2015-10-13 13:25:43 +02:00
Johannes Zellner 3787dd98b4 Do not crash if a boxVersionsUrl is not set
This prevents test failures when the cron job runs
2015-10-13 13:22:23 +02:00
Johannes Zellner 6c667e4325 Remove console.log 2015-10-13 13:06:50 +02:00
Johannes Zellner 0eec693a85 Update TEST_IMAGE_TAG 2015-10-13 12:30:02 +02:00
Johannes Zellner c3bf672c2a Ensure we deal with booleans 2015-10-13 12:29:40 +02:00
Johannes Zellner c3a3b6412f Support oauthProxy in webadmin 2015-10-13 11:49:50 +02:00
Johannes Zellner 44291b842a Fix apps-test.js 2015-10-13 10:41:57 +02:00
Johannes Zellner 36cf502b56 Addons take longer to startup 2015-10-13 10:41:57 +02:00
Johannes Zellner 2df77cf280 Fix settings-test.js 2015-10-13 10:41:57 +02:00
Johannes Zellner a453e49c27 Fix backups-test.js 2015-10-13 10:41:57 +02:00
Johannes Zellner e34c34de46 Fixup the apptask-test.js 2015-10-13 10:41:57 +02:00
Johannes Zellner 8dc5bf96e3 Fix apps-test.js 2015-10-13 10:41:57 +02:00
Johannes Zellner d2c3e1d1ae Fix database tests 2015-10-13 10:41:57 +02:00
Johannes Zellner 4eab101b78 use app.oauthProxy instead of app.accessRestriction 2015-10-13 10:41:57 +02:00
Johannes Zellner e460d6d15b Add apps.oauthProxy 2015-10-13 10:41:57 +02:00
Girish Ramakrishnan 3012f68a56 pullImage: handle stream error 2015-10-12 21:56:34 -07:00
Girish Ramakrishnan 1909050be2 remove redundant log 2015-10-12 21:54:25 -07:00
Girish Ramakrishnan d4c62c7295 check for 200 instead of 201 2015-10-12 21:54:18 -07:00
Girish Ramakrishnan 4eb3d1b918 login must return 200 2015-10-12 20:21:27 -07:00
Girish Ramakrishnan fb6bf50e48 signal redis to backup using SAVE 2015-10-12 13:30:58 -07:00
Johannes Zellner d8213f99b1 Ensure we only set the visibility in the progress bar on app restore to not break the layout 2015-10-12 20:32:43 +02:00
Johannes Zellner 7d7b759930 Add navbar with avatar and name to oauth views 2015-10-12 19:56:04 +02:00
Johannes Zellner 6f2bc555e0 Make application name and cloudron name more apparent in oauth login 2015-10-12 17:26:02 +02:00
Johannes Zellner a8c43ddf4a Show app icon instead of cloudron avatar in oauth login 2015-10-12 16:50:49 +02:00
Johannes Zellner 3eabc27877 Make app icon url public to be used in oauth login screen 2015-10-12 16:49:55 +02:00
Johannes Zellner ad379bd766 Support the new oauth client id prefix 2015-10-12 15:18:51 +02:00
Johannes Zellner c1047535d4 Update to new manifestformat 2015-10-12 13:22:56 +02:00
Girish Ramakrishnan 10142cc00b make a note of appid format 2015-10-11 14:19:38 -07:00
Girish Ramakrishnan 5e1487d12a appId format has changed in clientdb 2015-10-11 14:16:38 -07:00
Girish Ramakrishnan 39e0c13701 apptest: remove mail addon 2015-10-11 13:53:50 -07:00
Girish Ramakrishnan c80d984ee6 start the mail addon 2015-10-11 13:48:23 -07:00
Girish Ramakrishnan 3e474767d1 print the values otherwise it gets very confusing 2015-10-11 13:45:02 -07:00
Girish Ramakrishnan e2b954439c ensure redis container is stopped before removing it
this is required for the configure/update case where the redis container
might be holding some data in memory.

sending redis SIGTERM will make it commit to disk.
2015-10-11 12:08:35 -07:00
Girish Ramakrishnan 950d1eb5c3 remove associated volumes
note that this does not remove host mounts
2015-10-11 11:37:23 -07:00
Girish Ramakrishnan f48a2520c3 remove RSTATE_ERROR
if startContainer failed, it will still returning success because
it running the db update result
2015-10-11 11:18:30 -07:00
Girish Ramakrishnan f366d41034 regenerate shrinkwrap with --no-optional for ldapjs 2015-10-11 10:39:41 -07:00
Girish Ramakrishnan b3c40e1ba7 test image is now 5.0.0 2015-10-11 10:03:42 -07:00
Girish Ramakrishnan b686d6e011 use latest docker images 2015-10-11 10:03:42 -07:00
Johannes Zellner 5663198bfb Do not log with morgan during test runs for simple auth server 2015-10-11 17:52:22 +02:00
Johannes Zellner bad50fd78b Merge branch 'simpleauth' 2015-10-11 17:40:10 +02:00
Johannes Zellner 9bd43e3f74 Update to new manifestformat version 1.9.0 2015-10-11 17:19:39 +02:00
Johannes Zellner f0fefab8ad Prefix morgan logs with source 2015-10-11 17:19:39 +02:00
Johannes Zellner 449231c791 Do not show http request logs during tests 2015-10-11 17:19:39 +02:00
Johannes Zellner bd161ec677 Remove serving up webadmin/ in test case 2015-10-11 17:19:39 +02:00
Johannes Zellner 8040d4ac2d Add simpleauth logging middleware 2015-10-11 17:19:39 +02:00
Johannes Zellner 06d7820566 read ldap port from config.js 2015-10-11 17:19:39 +02:00
Johannes Zellner 4818a3feee Specify addon env vars for simple auth 2015-10-11 17:19:39 +02:00
Johannes Zellner 9fcb2c0733 Fix the check install to keep up with the base image version 2015-10-11 17:19:39 +02:00
Johannes Zellner 6906b4159a Revert "Attach accessTokens to req for further use"
This reverts commit 895812df1e9226415640b74a001c1f8c1affab01.
2015-10-11 17:19:39 +02:00
Johannes Zellner 763b9309f6 Fixup the simple auth unit tests 2015-10-11 17:19:39 +02:00
Johannes Zellner 2bb4d1c22b Remove the simpleauth route handler 2015-10-11 17:19:39 +02:00
Johannes Zellner 23303363ee Move simple auth to separate express server 2015-10-11 17:19:39 +02:00
Johannes Zellner 79c17abad2 Add simpleAuthPort to config.js 2015-10-11 17:19:39 +02:00
Johannes Zellner 3234e0e3f0 Fixup the simple auth logout route and add tests 2015-10-11 17:19:39 +02:00
Johannes Zellner 982cd1e1f3 Attach accessTokens to req for further use
This helps with extracting the token, which can come
from various places like headers, body or query
2015-10-11 17:19:38 +02:00
Johannes Zellner df39fc86a4 add simple auth login unit tests 2015-10-11 17:19:38 +02:00
Johannes Zellner 870e0c4144 Fixup the simple login routes for unknown clients 2015-10-11 17:19:38 +02:00
Johannes Zellner 57704b706e Handle 404 in case subdomain does not exist on delete attempt 2015-10-11 17:19:38 +02:00
Johannes Zellner 223e0dfd1f Add SIMPLE_AUTH_ORIGIN 2015-10-11 17:19:38 +02:00
Johannes Zellner 51c438b0b6 Return correct error codes 2015-10-11 17:19:38 +02:00
Girish Ramakrishnan 93d210a754 Bump the graphite image 2015-10-10 09:57:07 -07:00
Girish Ramakrishnan 265ee15ac7 fix oldConfig madness
There is a crash when:
* App is configured. So, oldConfig now has {loc, access, portb }
* Cloudron is restored. The restore code path accesses the oldConfig.manifest.addons.

oldConfig is basically a messaging passing thing. It's not really a
db field. With that spirit, we simply pass an empty message in setup_infra.sh
2015-10-09 11:59:25 -07:00
Girish Ramakrishnan d0da47e0b3 fix comment 2015-10-09 11:48:59 -07:00
Girish Ramakrishnan 0e8553d1a7 code path applies to upgraded cloudrons as well 2015-10-09 11:08:54 -07:00
Girish Ramakrishnan 9229dd2fd5 Add oldConfigJson in schema file 2015-10-09 11:08:16 -07:00
Girish Ramakrishnan c2a8744240 fix typo 2015-10-09 10:04:50 -07:00
Girish Ramakrishnan bc7e07f6a6 mail: not required to expose port 25 2015-10-09 09:56:37 -07:00
Girish Ramakrishnan bfd6f8965e print mail server ip 2015-10-09 09:50:50 -07:00
Girish Ramakrishnan eb1e4a1aea mail now runs on port 2500 2015-10-09 09:29:17 -07:00
Girish Ramakrishnan dc3e8a9cb5 mail now runs on port 2500 2015-10-09 09:13:28 -07:00
Johannes Zellner 494bcc1711 prefix oauth client id and app ids 2015-10-09 11:45:53 +02:00
Johannes Zellner 7e071d9f23 add simpleauth addon hooks 2015-10-09 11:44:32 +02:00
Johannes Zellner 6f821222db Add simple auth routes 2015-10-09 11:37:46 +02:00
Johannes Zellner 6e464dbc81 Add simple auth route handlers 2015-10-09 11:37:39 +02:00
Johannes Zellner be8ef370c6 Add simple auth logic for login/logout 2015-10-09 11:37:17 +02:00
Johannes Zellner 39a05665b0 Update node modules to support v4.1.1
The sqlite3 versions we had used so far did not work with
new node versions
2015-10-09 10:15:10 +02:00
Girish Ramakrishnan 737e22116a Reword upgrade warning 2015-10-08 16:31:44 -07:00
Girish Ramakrishnan 43e1e4829f new test image 3.0.0 2015-10-08 16:07:14 -07:00
Girish Ramakrishnan c95778178f make rootfs readonly based on targetBoxVersion 2015-10-08 11:48:33 -07:00
Girish Ramakrishnan 04870313b7 Launch apps with readonly rootfs
We explicitly mark /tmp, /run and /var/log as writable volumes.
Docker creates such volumes in it's own volumes directory. Note
that these volumes are separate from host binds (/app/data).

When removing the container the docker created volumes are
removed (but not host binds).

Fixes #196
2015-10-08 11:33:17 -07:00
Girish Ramakrishnan 6ca040149c run addons as readonly 2015-10-08 11:07:28 -07:00
Girish Ramakrishnan e487b9d46b update mail image 2015-10-08 11:06:29 -07:00
Girish Ramakrishnan 1375e16ad2 mongodb: readonly rootfs 2015-10-08 10:24:15 -07:00
Girish Ramakrishnan 312f1f0085 mysql: readonly rootfs 2015-10-08 09:43:05 -07:00
Girish Ramakrishnan 721900fc47 postgresql: readonly rootfs 2015-10-08 09:20:25 -07:00
Girish Ramakrishnan 2d815a92a3 redis: use readonly rootfs 2015-10-08 09:00:43 -07:00
Girish Ramakrishnan 1c192b7c11 pass options param in setup call 2015-10-08 02:08:27 -07:00
Girish Ramakrishnan 4a887336bc Do not send app down mails for dev mode apps
Fixes #501
2015-10-07 18:46:48 -07:00
Girish Ramakrishnan 8f6521f942 pass addon options to all functions 2015-10-07 16:10:08 -07:00
Girish Ramakrishnan fbdfaa4dc7 rename setup and teardown functions of oauth addon 2015-10-07 15:55:57 -07:00
Girish Ramakrishnan bf4290db3e remove token addon, its a relic of the past 2015-10-07 15:44:55 -07:00
Johannes Zellner 94ad633128 Also unset the returnTo after login 2015-10-01 16:26:17 +02:00
Johannes Zellner c552917991 Reset the target url after oauth login
This is required for the cloudron button to work for users
which are not logged in
2015-10-01 16:16:29 +02:00
Johannes Zellner a7ee8c853e Keep checkInstall in sync 2015-09-30 16:12:51 +02:00
Girish Ramakrishnan 29e4879451 fix test image version 2015-09-29 20:22:38 -07:00
Girish Ramakrishnan 8b92344808 redirect stderr 2015-09-29 19:23:39 -07:00
Girish Ramakrishnan 0877cec2e6 Fix EE leak warning 2015-09-29 14:40:23 -07:00
Girish Ramakrishnan b1ca577be7 use newer test image that dies immediately on stop/term 2015-09-29 14:33:07 -07:00
Girish Ramakrishnan 9b484f5ac9 new version of mysql prints error with -p 2015-09-29 14:13:58 -07:00
Girish Ramakrishnan b6a9fd81da refactor our test docker image details 2015-09-29 13:59:17 -07:00
Girish Ramakrishnan f19113f88e rename test iamge under cloudron/ 2015-09-29 12:52:54 -07:00
Girish Ramakrishnan 3837bee51f retry pulling image
fixes #497
2015-09-29 12:47:03 -07:00
Girish Ramakrishnan 89c3296632 debug the status code as well 2015-09-28 23:18:50 -07:00
Girish Ramakrishnan db55f0696e stringify object when appending to string 2015-09-28 23:10:09 -07:00
Girish Ramakrishnan 03d4ae9058 new base image 0.4.0 2015-09-28 19:33:58 -07:00
Girish Ramakrishnan f8b41b703c Use fqdn to generate domain name of txt records 2015-09-28 17:20:59 -07:00
Girish Ramakrishnan 2a989e455c Ensure TXT records are added as dotted domains
Fixes #498
2015-09-28 16:35:58 -07:00
Girish Ramakrishnan cd24decca0 Send dns status requests in series
And abort status checking after the first one fails. Otherwise, this
bombards the appstore unnecessarily. And checks for status of other
things unnecessarily.
2015-09-28 16:23:39 -07:00
Girish Ramakrishnan f39842a001 ldap: allow non-anonymous searches
Add LDAP_BIND_DN and LDAP_BIND_PASSWORD that allow
apps to bind before a search. There appear to be two kinds of
ldap flows:

1. App simply binds using cn=<username>,$LDAP_USERS_BASE_DN. This
   works swimmingly today.

2. App searches the username under a "bind_dn" using some admin
   credentials. It takes the result and uses the first dn in the
   result as the user dn. It then binds as step 1.

This commit tries to help out the case 2) apps. These apps really
insist on having some credentials for searching.
2015-09-25 21:28:47 -07:00
Girish Ramakrishnan 2a39526a4c Remove old app ids from updatechecker state
Fixes #472
2015-09-22 22:46:14 -07:00
Girish Ramakrishnan ded5d4c98b debug message when notification is skipped 2015-09-22 22:41:42 -07:00
Girish Ramakrishnan a0ca59c3f2 Fix typo 2015-09-22 20:22:17 -07:00
Girish Ramakrishnan 53cfc49807 Save version instead of boolean so we get notified when version changes
part of #472
2015-09-22 16:11:15 -07:00
Girish Ramakrishnan 942eb579e4 save/restore notification state of updatechecker
part of #472
2015-09-22 16:11:04 -07:00
Girish Ramakrishnan 5819cfe412 Fix progress message 2015-09-22 13:02:09 -07:00
Johannes Zellner 5cb62ca412 Remove start/stop buttons in webadmin
Fixes #495
2015-09-22 22:00:42 +02:00
Johannes Zellner df10c245de app.js is no more 2015-09-22 22:00:42 +02:00
Girish Ramakrishnan 4a804dc52b Do a complete backup for updates
The backup cron job ensures backups every 4 hours which simply does
a 'box' backup listing. If we do only a 'box' backup during update,
this means that this cron job skips doing a backup and thus the apps
are not backed up.

This results in the janitor on the CaaS side complaining that the
app backups are too old.

Since we don't stop apps anymore during updates, it makes sense
to simply backup everything for updates as well. This is probably
what the user wants anyway.
2015-09-22 12:51:58 -07:00
Girish Ramakrishnan ed2f25a998 better debugs 2015-09-21 16:02:58 -07:00
Girish Ramakrishnan 7510c9fe29 Fix typo 2015-09-21 15:57:06 -07:00
Girish Ramakrishnan 78a1d53728 copy old backup as failed/errored apps
This ensures that
a) we don't get emails from janitor about bad app backups
b) that the backups are persisted over the s3 lifecycle

Fixes #493
2015-09-21 15:03:10 -07:00
Girish Ramakrishnan e9b078cd58 add backups.copyLastBackup 2015-09-21 14:14:43 -07:00
Girish Ramakrishnan dd8b928684 aws: add copyObject 2015-09-21 14:02:00 -07:00
Girish Ramakrishnan 185b574bdc Add custom apparmor profile for cloudron apps
Docker generates an apparmor profile on the fly under /etc/apparmor.d/docker.
This profile gets overwritten on every docker daemon start.

This profile allows processes to ptrace themselves. This is required by
circus (python process manager) for reasons unknown to me. It floods the logs
with
    audit[7623]: <audit-1400> apparmor="DENIED" operation="ptrace" profile="docker-default" pid=7623 comm="python3.4" requested_mask="trace" denied_mask="trace" peer="docker-default"

This is easily tested using:
    docker run -it cloudron/base:0.3.3 /bin/bash
        a) now do ps
        b) journalctl should show error log as above

    docker run --security-opt=apparmor:docker-cloudron-app -it cloudron/base:0.3.3 /bin/bash
        a) now do ps
        b) no error!

Note that despite this, the process may not have ability to ptrace since it does not
have CAP_PTRACE. Also, security-opt is the profile name (inside the apparmor config file)
and not the filename.

References:
    https://groups.google.com/forum/#!topic/docker-user/xvxpaceTCyw
    https://github.com/docker/docker/issues/7276
    https://bugs.launchpad.net/ubuntu/+source/docker.io/+bug/1320869

This is an infra update because we need to recreate containers to get the right profile.

Fixes #492
2015-09-21 11:01:44 -07:00
Girish Ramakrishnan a89726a8c6 Add custom debug.formatArgs to remove timestamp prefix in logs
Fixes #490

See also:
https://github.com/visionmedia/debug/issues/216
2015-09-21 09:05:14 -07:00
Girish Ramakrishnan c80aca27e6 remove unnecessary supererror call 2015-09-21 09:04:16 -07:00
Girish Ramakrishnan 029acab333 use correct timezone in updater
fixes #491
2015-09-18 14:46:44 -07:00
Girish Ramakrishnan 4f9f10e130 timezone detection is based on browser location/ip and not cloudron region intentionally 2015-09-18 13:40:22 -07:00
Girish Ramakrishnan 9ba11d2e14 print body on failure 2015-09-18 12:03:48 -07:00
Girish Ramakrishnan 23a5a1f79f timezone is already determined automatically using activation 2015-09-18 12:02:36 -07:00
Girish Ramakrishnan e8dc617d40 print tz for debugging 2015-09-18 10:51:52 -07:00
Girish Ramakrishnan d56794e846 clear backup progress when initiating backup
this ensures that tools can do:
1. backup
2. wait_for_backup

without the synchronous clear, we might get the progress state of
an earlier backup.
2015-09-17 21:17:59 -07:00
Girish Ramakrishnan 2663ec7da0 cloudron.backup does not wait for backup to complete 2015-09-17 16:35:59 -07:00
Girish Ramakrishnan eec4ae98cd add comment for purpose on internal server 2015-09-17 16:27:46 -07:00
Girish Ramakrishnan c31a0f4e09 Store dates as iso strings in database
ideally, the database schema should be TIMESTAMP
2015-09-17 13:51:55 -07:00
Girish Ramakrishnan 739db23514 Use the default timezone in settings
Fixes #485
2015-09-16 16:36:08 -07:00
Girish Ramakrishnan 8598fb444b store timezone in config.js (part of provision data) 2015-09-16 15:54:56 -07:00
Girish Ramakrishnan 0b630ff504 Remove debug that is flooding the logs 2015-09-16 10:50:15 -07:00
Girish Ramakrishnan 84169dea3d Do not set process.env.NODE_TLS_REJECT_UNAUTHORIZED
Doing so will affect all https requests which is dangerous.

We have these options to solve this:
1. Use superagent.ca(). Appstore already provides wildcard certs
   for dev, staging signed with appstore_ca. But we then need to
   send across the appstore_ca cert across in the provision call.
   This is a bit of work.

2. Convert superagent into https.request calls and use the
   rejectUnauthorized option.

3. Simply use http. This is what is done in this commit.

Fixes #488
2015-09-16 10:36:03 -07:00
Girish Ramakrishnan d83b5de47a reserve the ldap and oauthproxy port 2015-09-16 10:12:59 -07:00
Girish Ramakrishnan 2719c4240f Get oauth proxy port from the configs 2015-09-16 10:06:34 -07:00
Johannes Zellner d749756b53 Do not show the update action button in non mobile view 2015-09-16 09:36:46 +02:00
Johannes Zellner 0401c61c15 Add tooltip text for the app action icons 2015-09-16 09:36:22 +02:00
Johannes Zellner 34f45da2de Show indicator when app update is available
Fixes #489
2015-09-16 09:28:43 +02:00
Girish Ramakrishnan baecbf783c journalctl seems to barf on this debug 2015-09-15 20:50:22 -07:00
Girish Ramakrishnan 2f141cd6e0 Make the times absurdly high but that is how long in takes 2015-09-15 18:56:25 -07:00
Girish Ramakrishnan 1296299d02 error is undefined 2015-09-15 18:27:09 -07:00
Girish Ramakrishnan 998ac74d32 oldConfig.location can be null
If we had an update, location is not part of oldConfig. if we now do
an infra update, location is undefined.
2015-09-15 18:08:29 -07:00
Girish Ramakrishnan b4a34e6432 Explicity debug the fields
for some reason, journalctl barfs on this line
2015-09-15 14:55:20 -07:00
Girish Ramakrishnan e70c9d55db apptask: retry for external error as well 2015-09-14 21:45:27 -07:00
Girish Ramakrishnan 268aee6265 Return busy code for 420 response 2015-09-14 21:44:44 -07:00
Girish Ramakrishnan 1ba7b0e0fb context is raw text 2015-09-14 17:25:27 -07:00
Girish Ramakrishnan 72788fdb11 add note on how to test the oom 2015-09-14 17:20:30 -07:00
Girish Ramakrishnan 435afec13c Print OOM context 2015-09-14 17:18:11 -07:00
Girish Ramakrishnan 2cb1877669 Do not reconnect for now 2015-09-14 17:10:49 -07:00
Girish Ramakrishnan edd672cba7 fix typo 2015-09-14 17:07:44 -07:00
Girish Ramakrishnan 991f37fe05 Provide app information if possible 2015-09-14 17:06:04 -07:00
Girish Ramakrishnan c147d8004b Add appdb.getByContainerId 2015-09-14 17:01:04 -07:00
Girish Ramakrishnan cdcc4dfda8 Get notification on app oom
currently, oom events arrive a little late :
https://github.com/docker/docker/issues/16074

fixes #489
2015-09-14 16:51:32 -07:00
Girish Ramakrishnan 2eaba686fb apphealthmonitor.js is not executable 2015-09-14 16:51:32 -07:00
Girish Ramakrishnan 236032b4a6 Remove supererror setup in oauthproxy and apphealthmonitor 2015-09-14 16:49:10 -07:00
Girish Ramakrishnan 5fcba59b3e set memory limits for addons
mysql, postgresql, mongodb - 100m each
mail, graphite, redis (each instance) - 75m

For reference, in yellowtent:
mongo - 5m
postgresql - 33m
mysql - 3.5m
mail: 26m
graphite - 26m
redis - 32m
2015-09-14 13:47:45 -07:00
59 changed files with 4813 additions and 2776 deletions
+11 -1
View File
@@ -4,10 +4,17 @@
require('supererror')({ splatchError: true });
// remove timestamp from debug() based output
require('debug').formatArgs = function formatArgs() {
arguments[0] = this.namespace + ' ' + arguments[0];
return arguments;
};
var appHealthMonitor = require('./src/apphealthmonitor.js'),
async = require('async'),
config = require('./src/config.js'),
ldap = require('./src/ldap.js'),
simpleauth = require('./src/simpleauth.js'),
oauthproxy = require('./src/oauthproxy.js'),
server = require('./src/server.js');
@@ -29,8 +36,9 @@ console.log();
async.series([
server.start,
ldap.start,
simpleauth.start,
appHealthMonitor.start,
oauthproxy.start.bind(null, 4000 /* port */)
oauthproxy.start
], function (error) {
if (error) {
console.error('Error starting server', error);
@@ -43,6 +51,7 @@ var NOOP_CALLBACK = function () { };
process.on('SIGINT', function () {
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
simpleauth.stop(NOOP_CALLBACK);
oauthproxy.stop(NOOP_CALLBACK);
setTimeout(process.exit.bind(process), 3000);
});
@@ -50,6 +59,7 @@ process.on('SIGINT', function () {
process.on('SIGTERM', function () {
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
simpleauth.stop(NOOP_CALLBACK);
oauthproxy.stop(NOOP_CALLBACK);
setTimeout(process.exit.bind(process), 3000);
});
+6
View File
@@ -4,6 +4,12 @@
require('supererror')({ splatchError: true });
// remove timestamp from debug() based output
require('debug').formatArgs = function formatArgs() {
arguments[0] = this.namespace + ' ' + arguments[0];
return arguments;
};
var assert = require('assert'),
debug = require('debug')('box:janitor'),
async = require('async'),
@@ -0,0 +1,17 @@
dbm = dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN oauthProxy BOOLEAN DEFAULT 0', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN oauthProxy', function (error) {
if (error) console.error(error);
callback(error);
});
};
+5 -1
View File
@@ -30,7 +30,7 @@ CREATE TABLE IF NOT EXISTS tokens(
CREATE TABLE IF NOT EXISTS clients(
id VARCHAR(128) NOT NULL UNIQUE,
appId VARCHAR(128) NOT NULL,
appId VARCHAR(128) NOT NULL, // this is for the form <type>-appId to allow easy clearing of tokens of a type
clientSecret VARCHAR(512) NOT NULL,
redirectURI VARCHAR(512) NOT NULL,
scope VARCHAR(512) NOT NULL,
@@ -49,10 +49,14 @@ CREATE TABLE IF NOT EXISTS apps(
location VARCHAR(128) NOT NULL UNIQUE,
dnsRecordId VARCHAR(512),
accessRestriction VARCHAR(512),
oauthProxy BOOLEAN DEFAULT 0,
createdAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
lastBackupId VARCHAR(128),
lastBackupConfigJson VARCHAR(2048), // used for appstore and non-appstore installs. it's here so it's easy to do REST validation
oldConfigJson VARCHAR(2048), // used to pass old config for apptask
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS appPortBindings(
+3496 -2274
View File
File diff suppressed because it is too large Load Diff
+1 -4
View File
@@ -12,14 +12,11 @@
"engines": [
"node >= 0.12.0"
],
"bin": {
"cloudron": "./app.js"
},
"dependencies": {
"async": "^1.2.1",
"aws-sdk": "^2.1.46",
"body-parser": "^1.13.1",
"cloudron-manifestformat": "^1.7.0",
"cloudron-manifestformat": "^1.9.1",
"connect-ensure-login": "^0.1.1",
"connect-lastmile": "0.0.13",
"connect-timeout": "^1.5.0",
+8 -8
View File
@@ -3,15 +3,15 @@
# If you change the infra version, be sure to put a warning
# in the change log
INFRA_VERSION=8
INFRA_VERSION=16
# WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
# These constants are used in the installer script as well
BASE_IMAGE=cloudron/base:0.3.3
MYSQL_IMAGE=cloudron/mysql:0.3.3
POSTGRESQL_IMAGE=cloudron/postgresql:0.3.2
MONGODB_IMAGE=cloudron/mongodb:0.3.2
REDIS_IMAGE=cloudron/redis:0.3.2 # if you change this, fix src/addons.js as well
MAIL_IMAGE=cloudron/mail:0.3.2
GRAPHITE_IMAGE=cloudron/graphite:0.3.4
BASE_IMAGE=cloudron/base:0.6.0
MYSQL_IMAGE=cloudron/mysql:0.6.0
POSTGRESQL_IMAGE=cloudron/postgresql:0.6.0
MONGODB_IMAGE=cloudron/mongodb:0.6.0
REDIS_IMAGE=cloudron/redis:0.6.1 # if you change this, fix src/addons.js as well
MAIL_IMAGE=cloudron/mail:0.6.0
GRAPHITE_IMAGE=cloudron/graphite:0.6.0
+4
View File
@@ -26,6 +26,10 @@ cp "${container_files}/sudoers" /etc/sudoers.d/yellowtent
rm -rf /etc/collectd
ln -sfF "${DATA_DIR}/collectd" /etc/collectd
########## apparmor docker profile
cp "${container_files}/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
systemctl restart apparmor
########## nginx
# link nginx config to system config
unlink /etc/nginx 2>/dev/null || rm -rf /etc/nginx
@@ -0,0 +1,32 @@
#include <tunables/global>
profile docker-cloudron-app flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
ptrace peer=@{profile_name},
network,
capability,
file,
umount,
deny @{PROC}/sys/fs/** wklx,
deny @{PROC}/sysrq-trigger rwklx,
deny @{PROC}/mem rwklx,
deny @{PROC}/kmem rwklx,
deny @{PROC}/sys/kernel/[^s][^h][^m]* wklx,
deny @{PROC}/sys/kernel/*/** wklx,
deny mount,
deny /sys/[^f]*/** wklx,
deny /sys/f[^s]*/** wklx,
deny /sys/fs/[^c]*/** wklx,
deny /sys/fs/c[^g]*/** wklx,
deny /sys/fs/cg[^r]*/** wklx,
deny /sys/firmware/efi/efivars/** rwklx,
deny /sys/kernel/security/** rwklx,
}
+1 -1
View File
@@ -69,7 +69,7 @@ server {
}
<% } else if ( endpoint === 'oauthproxy' ) { %>
proxy_pass http://127.0.0.1:4000;
proxy_pass http://127.0.0.1:3003;
proxy_set_header X-Cloudron-Proxy-Port <%= port %>;
<% } else if ( endpoint === 'app' ) { %>
proxy_pass http://127.0.0.1:<%= port %>;
+20 -5
View File
@@ -28,19 +28,24 @@ fi
# graphite
graphite_container_id=$(docker run --restart=always -d --name="graphite" \
-m 75m \
--memory-swap 150m \
-p 127.0.0.1:2003:2003 \
-p 127.0.0.1:2004:2004 \
-p 127.0.0.1:8000:8000 \
-v "${DATA_DIR}/graphite:/app/data" \
--read-only -v /tmp -v /run -v /var/log \
"${GRAPHITE_IMAGE}")
echo "Graphite container id: ${graphite_container_id}"
# mail
# mail (MAIL_SMTP_PORT is 2500 in addons.js. used in mailer.js as well)
mail_container_id=$(docker run --restart=always -d --name="mail" \
-p 127.0.0.1:25:25 \
-m 75m \
--memory-swap 150m \
-h "${arg_fqdn}" \
-e "DOMAIN_NAME=${arg_fqdn}" \
-v "${DATA_DIR}/box/mail:/app/data" \
--read-only -v /tmp -v /run -v /var/log \
"${MAIL_IMAGE}")
echo "Mail container id: ${mail_container_id}"
@@ -52,9 +57,12 @@ readonly MYSQL_ROOT_PASSWORD='${mysql_addon_root_password}'
readonly MYSQL_ROOT_HOST='${docker0_ip}'
EOF
mysql_container_id=$(docker run --restart=always -d --name="mysql" \
-m 100m \
--memory-swap 200m \
-h "${arg_fqdn}" \
-v "${DATA_DIR}/mysql:/var/lib/mysql" \
-v "${DATA_DIR}/addons/mysql_vars.sh:/etc/mysql/mysql_vars.sh:ro" \
--read-only -v /tmp -v /run -v /var/log \
"${MYSQL_IMAGE}")
echo "MySQL container id: ${mysql_container_id}"
@@ -64,9 +72,12 @@ cat > "${DATA_DIR}/addons/postgresql_vars.sh" <<EOF
readonly POSTGRESQL_ROOT_PASSWORD='${postgresql_addon_root_password}'
EOF
postgresql_container_id=$(docker run --restart=always -d --name="postgresql" \
-m 100m \
--memory-swap 200m \
-h "${arg_fqdn}" \
-v "${DATA_DIR}/postgresql:/var/lib/postgresql" \
-v "${DATA_DIR}/addons/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh:ro" \
--read-only -v /tmp -v /run -v /var/log \
"${POSTGRESQL_IMAGE}")
echo "PostgreSQL container id: ${postgresql_container_id}"
@@ -76,19 +87,23 @@ cat > "${DATA_DIR}/addons/mongodb_vars.sh" <<EOF
readonly MONGODB_ROOT_PASSWORD='${mongodb_addon_root_password}'
EOF
mongodb_container_id=$(docker run --restart=always -d --name="mongodb" \
-m 100m \
--memory-swap 200m \
-h "${arg_fqdn}" \
-v "${DATA_DIR}/mongodb:/var/lib/mongodb" \
-v "${DATA_DIR}/addons/mongodb_vars.sh:/etc/mongodb_vars.sh:ro" \
--read-only -v /tmp -v /run -v /var/log \
"${MONGODB_IMAGE}")
echo "Mongodb container id: ${mongodb_container_id}"
# only touch apps in installed state. any other state is just resumed by the taskmanager
if [[ "${infra_version}" == "none" ]]; then
# if no existing infra was found (for new and restoring cloudons), download app backups
# if no existing infra was found (for new, upgraded and restored cloudons), download app backups
echo "Marking installed apps for restore"
mysql -u root -ppassword -e 'UPDATE apps SET installationState = "pending_restore" WHERE installationState = "installed"' box
mysql -u root -ppassword -e 'UPDATE apps SET installationState = "pending_restore", oldConfigJson = NULL WHERE installationState = "installed"' box
else
# if existing infra was found, just mark apps for reconfiguration
mysql -u root -ppassword -e 'UPDATE apps SET installationState = "pending_configure" WHERE installationState = "installed"' box
mysql -u root -ppassword -e 'UPDATE apps SET installationState = "pending_configure", oldConfigJson = NULL WHERE installationState = "installed"' box
fi
echo -n "${INFRA_VERSION}" > "${DATA_DIR}/INFRA_VERSION"
+169 -88
View File
@@ -11,8 +11,8 @@ exports = module.exports = {
getBindsSync: getBindsSync,
// exported for testing
_allocateOAuthCredentials: allocateOAuthCredentials,
_removeOAuthCredentials: removeOAuthCredentials
_setupOauth: setupOauth,
_teardownOauth: teardownOauth
};
var appdb = require('./appdb.js'),
@@ -35,27 +35,26 @@ var appdb = require('./appdb.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
spawn = child_process.spawn,
tokendb = require('./tokendb.js'),
util = require('util'),
uuid = require('node-uuid'),
vbox = require('./vbox.js');
var NOOP = function (app, callback) { return callback(); };
var NOOP = function (app, options, callback) { return callback(); };
// setup can be called multiple times for the same app (configure crash restart) and existing data must not be lost
// teardown is destructive. app data stored with the addon is lost
var KNOWN_ADDONS = {
oauth: {
setup: allocateOAuthCredentials,
teardown: removeOAuthCredentials,
setup: setupOauth,
teardown: teardownOauth,
backup: NOOP,
restore: allocateOAuthCredentials
restore: setupOauth
},
token: {
setup: allocateAccessToken,
teardown: removeAccessToken,
simpleauth: {
setup: setupSimpleAuth,
teardown: teardownSimpleAuth,
backup: NOOP,
restore: allocateAccessToken
restore: setupSimpleAuth
},
ldap: {
setup: setupLdap,
@@ -90,7 +89,7 @@ var KNOWN_ADDONS = {
redis: {
setup: setupRedis,
teardown: teardownRedis,
backup: NOOP, // no backup because we store redis as part of app's volume
backup: backupRedis,
restore: setupRedis // same thing
},
localstorage: {
@@ -128,9 +127,9 @@ function setupAddons(app, addons, callback) {
async.eachSeries(Object.keys(addons), function iterator(addon, iteratorCallback) {
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new Error('No such addon:' + addon));
debugApp(app, 'Setting up addon %s', addon);
debugApp(app, 'Setting up addon %s with options %j', addon, addons[addon]);
KNOWN_ADDONS[addon].setup(app, iteratorCallback);
KNOWN_ADDONS[addon].setup(app, addons[addon], iteratorCallback);
}, callback);
}
@@ -146,9 +145,9 @@ function teardownAddons(app, addons, callback) {
async.eachSeries(Object.keys(addons), function iterator(addon, iteratorCallback) {
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new Error('No such addon:' + addon));
debugApp(app, 'Tearing down addon %s', addon);
debugApp(app, 'Tearing down addon %s with options %j', addon, addons[addon]);
KNOWN_ADDONS[addon].teardown(app, iteratorCallback);
KNOWN_ADDONS[addon].teardown(app, addons[addon], iteratorCallback);
}, callback);
}
@@ -166,7 +165,7 @@ function backupAddons(app, addons, callback) {
async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) {
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new Error('No such addon:' + addon));
KNOWN_ADDONS[addon].backup(app, iteratorCallback);
KNOWN_ADDONS[addon].backup(app, addons[addon], iteratorCallback);
}, callback);
}
@@ -184,7 +183,7 @@ function restoreAddons(app, addons, callback) {
async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) {
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new Error('No such addon:' + addon));
KNOWN_ADDONS[addon].restore(app, iteratorCallback);
KNOWN_ADDONS[addon].restore(app, addons[addon], iteratorCallback);
}, callback);
}
@@ -236,22 +235,24 @@ function getBindsSync(app, addons) {
return binds;
}
function allocateOAuthCredentials(app, callback) {
function setupOauth(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var appId = app.id;
var id = 'cid-addon-' + uuid.v4();
var id = 'cid-addon-oauth-' + uuid.v4();
var clientSecret = hat(256);
var redirectURI = 'https://' + config.appFqdn(app.location);
var scope = 'profile,roleUser';
debugApp(app, 'allocateOAuthCredentials: id:%s clientSecret:%s', id, clientSecret);
debugApp(app, 'setupOauth: id:%s clientSecret:%s', id, clientSecret);
clientdb.delByAppId('addon-' + appId, function (error) { // remove existing creds
// ensure 'addon-oauth-' is in sync with oauth.js
clientdb.delByAppId('addon-oauth-' + appId, function (error) { // remove existing creds
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
clientdb.add(id, 'addon-' + appId, clientSecret, redirectURI, scope, function (error) {
clientdb.add(id, 'addon-oauth-' + appId, clientSecret, redirectURI, scope, function (error) {
if (error) return callback(error);
var env = [
@@ -267,29 +268,80 @@ function allocateOAuthCredentials(app, callback) {
});
}
function removeOAuthCredentials(app, callback) {
function teardownOauth(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'removeOAuthCredentials');
debugApp(app, 'teardownOauth');
clientdb.delByAppId('addon-' + app.id, function (error) {
clientdb.delByAppId('addon-oauth-' + app.id, function (error) {
if (error && error.reason !== DatabaseError.NOT_FOUND) console.error(error);
appdb.unsetAddonConfig(app.id, 'oauth', callback);
});
}
function setupLdap(app, callback) {
function setupSimpleAuth(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var appId = app.id;
var id = 'cid-addon-simpleauth-' + uuid.v4();
var scope = 'profile,roleUser';
debugApp(app, 'setupSimpleAuth: id:%s', id);
// ensure 'addon-simpleauth-' is in sync with oauth.js
clientdb.delByAppId('addon-simpleauth-' + appId, function (error) { // remove existing creds
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
clientdb.add(id, 'addon-simpleauth-' + appId, '', '', scope, function (error) {
if (error) return callback(error);
var env = [
'SIMPLE_AUTH_SERVER=172.17.42.1',
'SIMPLE_AUTH_PORT=' + config.get('simpleAuthPort'),
'SIMPLE_AUTH_URL=http://172.17.42.1:' + config.get('simpleAuthPort'), // obsolete, remove
'SIMPLE_AUTH_ORIGIN=http://172.17.42.1:' + config.get('simpleAuthPort'),
'SIMPLE_AUTH_CLIENT_ID=' + id
];
debugApp(app, 'Setting simple auth addon config to %j', env);
appdb.setAddonConfig(appId, 'simpleauth', env, callback);
});
});
}
function teardownSimpleAuth(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'teardownSimpleAuth');
clientdb.delByAppId('addon-simpleauth-' + app.id, function (error) {
if (error && error.reason !== DatabaseError.NOT_FOUND) console.error(error);
appdb.unsetAddonConfig(app.id, 'simpleauth', callback);
});
}
function setupLdap(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var env = [
'LDAP_SERVER=172.17.42.1',
'LDAP_PORT=3002',
'LDAP_URL=ldap://172.17.42.1:3002',
'LDAP_PORT=' + config.get('ldapPort'),
'LDAP_URL=ldap://172.17.42.1:' + config.get('ldapPort'),
'LDAP_USERS_BASE_DN=ou=users,dc=cloudron',
'LDAP_GROUPS_BASE_DN=ou=groups,dc=cloudron'
'LDAP_GROUPS_BASE_DN=ou=groups,dc=cloudron',
'LDAP_BIND_DN=cn='+ app.id + ',ou=apps,dc=cloudron',
'LDAP_BIND_PASSWORD=' + hat(256) // this is ignored
];
debugApp(app, 'Setting up LDAP');
@@ -297,8 +349,9 @@ function setupLdap(app, callback) {
appdb.setAddonConfig(app.id, 'ldap', env, callback);
}
function teardownLdap(app, callback) {
function teardownLdap(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Tearing down LDAP');
@@ -306,13 +359,14 @@ function teardownLdap(app, callback) {
appdb.unsetAddonConfig(app.id, 'ldap', callback);
}
function setupSendMail(app, callback) {
function setupSendMail(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var env = [
'MAIL_SMTP_SERVER=mail',
'MAIL_SMTP_PORT=25',
'MAIL_SMTP_PORT=2500', // if you change this, change the mail container
'MAIL_SMTP_USERNAME=' + (app.location || app.id), // use app.id for bare domains
'MAIL_DOMAIN=' + config.fqdn()
];
@@ -322,8 +376,9 @@ function setupSendMail(app, callback) {
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
}
function teardownSendMail(app, callback) {
function teardownSendMail(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Tearing down sendmail');
@@ -331,8 +386,9 @@ function teardownSendMail(app, callback) {
appdb.unsetAddonConfig(app.id, 'sendmail', callback);
}
function setupMySql(app, callback) {
function setupMySql(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Setting up mysql');
@@ -365,7 +421,11 @@ function setupMySql(app, callback) {
});
}
function teardownMySql(app, callback) {
function teardownMySql(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var container = docker.getContainer('mysql');
var cmd = [ '/addons/mysql/service.sh', 'remove', app.id ];
@@ -387,7 +447,7 @@ function teardownMySql(app, callback) {
});
}
function backupMySql(app, callback) {
function backupMySql(app, options, callback) {
debugApp(app, 'Backing up mysql');
callback = once(callback); // ChildProcess exit may or may not be called after error
@@ -406,10 +466,10 @@ function backupMySql(app, callback) {
cp.stderr.pipe(process.stderr);
}
function restoreMySql(app, callback) {
function restoreMySql(app, options, callback) {
callback = once(callback); // ChildProcess exit may or may not be called after error
setupMySql(app, function (error) {
setupMySql(app, options, function (error) {
if (error) return callback(error);
debugApp(app, 'restoreMySql');
@@ -431,8 +491,9 @@ function restoreMySql(app, callback) {
});
}
function setupPostgreSql(app, callback) {
function setupPostgreSql(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Setting up postgresql');
@@ -465,7 +526,11 @@ function setupPostgreSql(app, callback) {
});
}
function teardownPostgreSql(app, callback) {
function teardownPostgreSql(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var container = docker.getContainer('postgresql');
var cmd = [ '/addons/postgresql/service.sh', 'remove', app.id ];
@@ -487,7 +552,7 @@ function teardownPostgreSql(app, callback) {
});
}
function backupPostgreSql(app, callback) {
function backupPostgreSql(app, options, callback) {
debugApp(app, 'Backing up postgresql');
callback = once(callback); // ChildProcess exit may or may not be called after error
@@ -506,10 +571,10 @@ function backupPostgreSql(app, callback) {
cp.stderr.pipe(process.stderr);
}
function restorePostgreSql(app, callback) {
function restorePostgreSql(app, options, callback) {
callback = once(callback); // ChildProcess exit may or may not be called after error
setupPostgreSql(app, function (error) {
setupPostgreSql(app, options, function (error) {
if (error) return callback(error);
debugApp(app, 'restorePostgreSql');
@@ -531,8 +596,9 @@ function restorePostgreSql(app, callback) {
});
}
function setupMongoDb(app, callback) {
function setupMongoDb(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Setting up mongodb');
@@ -565,7 +631,11 @@ function setupMongoDb(app, callback) {
});
}
function teardownMongoDb(app, callback) {
function teardownMongoDb(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var container = docker.getContainer('mongodb');
var cmd = [ '/addons/mongodb/service.sh', 'remove', app.id ];
@@ -587,7 +657,7 @@ function teardownMongoDb(app, callback) {
});
}
function backupMongoDb(app, callback) {
function backupMongoDb(app, options, callback) {
debugApp(app, 'Backing up mongodb');
callback = once(callback); // ChildProcess exit may or may not be called after error
@@ -606,10 +676,10 @@ function backupMongoDb(app, callback) {
cp.stderr.pipe(process.stderr);
}
function restoreMongoDb(app, callback) {
function restoreMongoDb(app, options, callback) {
callback = once(callback); // ChildProcess exit may or may not be called after error
setupMongoDb(app, function (error) {
setupMongoDb(app, options, function (error) {
if (error) return callback(error);
debugApp(app, 'restoreMongoDb');
@@ -648,8 +718,30 @@ function forwardRedisPort(appId, callback) {
});
}
function stopAndRemoveRedis(container, callback) {
function ignoreError(func) {
return function (callback) {
func(function (error) {
if (error) debug('stopAndRemoveRedis: Ignored error:', error);
callback();
});
};
}
// stopping redis with SIGTERM makes it commit the database to disk
async.series([
ignoreError(container.stop.bind(container, { t: 10 })),
ignoreError(container.wait.bind(container)),
ignoreError(container.remove.bind(container, { force: true, v: true }))
], callback);
}
// Ensures that app's addon redis container is running. Can be called when named container already exists/running
function setupRedis(app, callback) {
function setupRedis(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var redisPassword = generatePassword(64, false /* memorable */);
var redisVarsFile = path.join(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
var redisDataDir = path.join(paths.DATA_DIR, app.id + '/redis');
@@ -664,9 +756,13 @@ function setupRedis(app, callback) {
name: 'redis-' + app.id,
Hostname: config.appFqdn(app.location),
Tty: true,
Image: 'cloudron/redis:0.3.2', // if you change this, fix setup/INFRA_VERSION as well
Image: 'cloudron/redis:0.6.1', // if you change this, fix setup/INFRA_VERSION as well
Cmd: null,
Volumes: {},
Volumes: {
'/tmp': {},
'/run': {},
'/var/log': {}
},
VolumesFrom: []
};
@@ -677,11 +773,14 @@ function setupRedis(app, callback) {
redisVarsFile + ':/etc/redis/redis_vars.sh:ro',
redisDataDir + ':/var/lib/redis:rw'
],
Memory: 1024 * 1024 * 75, // 100mb
MemorySwap: 1024 * 1024 * 75 * 2, // 150mb
// On Mac (boot2docker), we have to export the port to external world for port forwarding from Mac to work
// On linux, export to localhost only for testing purposes and not for the app itself
PortBindings: {
'6379/tcp': [{ HostPort: '0', HostIp: isMac ? '0.0.0.0' : '127.0.0.1' }]
},
ReadonlyRootfs: true,
RestartPolicy: {
'Name': 'always',
'MaximumRetryCount': 0
@@ -696,7 +795,7 @@ function setupRedis(app, callback) {
];
var redisContainer = docker.getContainer(createOptions.name);
redisContainer.remove({ force: true, v: false }, function (ignoredError) {
stopAndRemoveRedis(redisContainer, function () {
docker.createContainer(createOptions, function (error) {
if (error && error.statusCode !== 409) return callback(error); // if not already created
@@ -713,12 +812,16 @@ function setupRedis(app, callback) {
});
}
function teardownRedis(app, callback) {
function teardownRedis(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var container = docker.getContainer('redis-' + app.id);
var removeOptions = {
force: true, // kill container if it's running
v: false // removes volumes associated with the container
v: true // removes volumes associated with the container
};
container.remove(removeOptions, function (error) {
@@ -736,40 +839,18 @@ function teardownRedis(app, callback) {
});
}
function allocateAccessToken(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
function backupRedis(app, options, callback) {
debugApp(app, 'Backing up redis');
var token = tokendb.generateToken();
var expiresAt = Number.MAX_SAFE_INTEGER; // basically never expire
var scopes = 'profile,users'; // TODO This should be put into the manifest and the user should know those
var clientId = ''; // meaningless for apps so far
callback = once(callback); // ChildProcess exit may or may not be called after error
tokendb.delByIdentifier(tokendb.PREFIX_APP + app.id, function (error) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
tokendb.add(token, tokendb.PREFIX_APP + app.id, clientId, expiresAt, scopes, function (error) {
if (error) return callback(error);
var env = [
'CLOUDRON_TOKEN=' + token
];
debugApp(app, 'Setting token addon config to %j', env);
appdb.setAddonConfig(appId, 'token', env, callback);
});
var cp = spawn('/usr/bin/docker', [ 'exec', 'redis-' + app.id, '/addons/redis/service.sh', 'backup' ]);
cp.on('error', callback);
cp.on('exit', function (code, signal) {
debugApp(app, 'backupRedis: done. code:%s signal:%s', code, signal);
if (!callback.called) callback(code ? 'backupRedis failed with status ' + code : null);
});
cp.stdout.pipe(process.stdout);
cp.stderr.pipe(process.stderr);
}
function removeAccessToken(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
tokendb.delByIdentifier(tokendb.PREFIX_APP + app.id, function (error) {
if (error && error.reason !== DatabaseError.NOT_FOUND) console.error(error);
appdb.unsetAddonConfig(app.id, 'token', callback);
});
}
+25 -6
View File
@@ -6,6 +6,7 @@ exports = module.exports = {
get: get,
getBySubdomain: getBySubdomain,
getByHttpPort: getByHttpPort,
getByContainerId: getByContainerId,
add: add,
exists: exists,
del: del,
@@ -39,7 +40,6 @@ exports = module.exports = {
RSTATE_PENDING_START: 'pending_start',
RSTATE_PENDING_STOP: 'pending_stop',
RSTATE_STOPPED: 'stopped', // app stopped by use
RSTATE_ERROR: 'error',
// run codes (keep in sync in UI)
HEALTH_HEALTHY: 'healthy',
@@ -59,11 +59,11 @@ var assert = require('assert'),
var APPS_FIELDS = [ 'id', 'appStoreId', 'installationState', 'installationProgress', 'runState',
'health', 'containerId', 'manifestJson', 'httpPort', 'location', 'dnsRecordId',
'accessRestriction', 'lastBackupId', 'lastBackupConfigJson', 'oldConfigJson' ].join(',');
'accessRestriction', 'lastBackupId', 'lastBackupConfigJson', 'oldConfigJson', 'oauthProxy' ].join(',');
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.accessRestriction', 'apps.lastBackupId', 'apps.lastBackupConfigJson', 'apps.oldConfigJson' ].join(',');
'apps.accessRestriction', 'apps.lastBackupId', 'apps.lastBackupConfigJson', 'apps.oldConfigJson', 'apps.oauthProxy' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
@@ -95,6 +95,8 @@ function postProcess(result) {
for (var i = 0; i < environmentVariables.length; i++) {
result.portBindings[environmentVariables[i]] = parseInt(hostPorts[i], 10);
}
result.oauthProxy = !!result.oauthProxy;
}
function get(id, callback) {
@@ -145,6 +147,22 @@ function getByHttpPort(httpPort, callback) {
});
}
function getByContainerId(containerId, callback) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId WHERE containerId = ? GROUP BY apps.id', [ containerId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
postProcess(result[0]);
callback(null, result[0]);
});
}
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -160,7 +178,7 @@ function getAll(callback) {
});
}
function add(id, appStoreId, manifest, location, portBindings, accessRestriction, callback) {
function add(id, appStoreId, manifest, location, portBindings, accessRestriction, oauthProxy, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof appStoreId, 'string');
assert(manifest && typeof manifest === 'object');
@@ -168,6 +186,7 @@ function add(id, appStoreId, manifest, location, portBindings, accessRestriction
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(typeof accessRestriction, 'string');
assert.strictEqual(typeof oauthProxy, 'boolean');
assert.strictEqual(typeof callback, 'function');
portBindings = portBindings || { };
@@ -176,8 +195,8 @@ function add(id, appStoreId, manifest, location, portBindings, accessRestriction
var queries = [ ];
queries.push({
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestriction) VALUES (?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, exports.ISTATE_PENDING_INSTALL, location, accessRestriction ]
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestriction, oauthProxy) VALUES (?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, exports.ISTATE_PENDING_INSTALL, location, accessRestriction, oauthProxy ]
});
Object.keys(portBindings).forEach(function (env) {
Executable → Regular
+51 -3
View File
@@ -1,7 +1,5 @@
'use strict';
require('supererror')({ splatchError: true });
var appdb = require('./appdb.js'),
assert = require('assert'),
async = require('async'),
@@ -21,6 +19,7 @@ var HEALTHCHECK_INTERVAL = 10 * 1000; // every 10 seconds. this needs to be smal
var UNHEALTHY_THRESHOLD = 3 * 60 * 1000; // 3 minutes
var gHealthInfo = { }; // { time, emailSent }
var gRunTimeout = null;
var gDockerEventStream = null;
function debugApp(app) {
assert(!app || typeof app === 'object');
@@ -47,7 +46,7 @@ function setHealth(app, health, callback) {
debugApp(app, 'marking as unhealthy since not seen for more than %s minutes', UNHEALTHY_THRESHOLD/(60 * 1000));
mailer.appDied(app);
if (app.appStoreId !== '') mailer.appDied(app); // do not send mails for dev apps
gHealthInfo[app.id].emailSent = true;
} else {
debugApp(app, 'waiting for sometime to update the app health');
@@ -124,11 +123,58 @@ function run() {
});
}
/*
OOM can be tested using stress tool like so:
docker run -ti -m 100M cloudron/base:0.3.3 /bin/bash
apt-get update && apt-get install stress
stress --vm 1 --vm-bytes 200M --vm-hang 0
*/
function processDockerEvents() {
// note that for some reason, the callback is called only on the first event
debug('Listening for docker events');
docker.getEvents({ filters: JSON.stringify({ event: [ 'oom' ] }) }, function (error, stream) {
if (error) return console.error(error);
gDockerEventStream = stream;
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) {
var program = error || !app.appStoreId ? ev.id : app.appStoreId;
var context = JSON.stringify(ev);
if (app) context = context + '\n\n' + JSON.stringify(app, null, 4) + '\n';
debug('OOM Context: %s', context);
// do not send mails for dev apps
if (app.appStoreId !== '') mailer.sendCrashNotification(program, context); // app can be null if it's an addon crash
});
});
stream.on('error', function (error) {
console.error('Error reading docker events', error);
gDockerEventStream = null; // TODO: reconnect?
});
stream.on('end', function () {
console.error('Docke event stream ended');
gDockerEventStream = null; // TODO: reconnect?
stream.end();
});
});
}
function start(callback) {
assert.strictEqual(typeof callback, 'function');
debug('Starting apphealthmonitor');
processDockerEvents();
run();
callback();
}
@@ -136,5 +182,7 @@ function stop(callback) {
assert.strictEqual(typeof callback, 'function');
clearTimeout(gRunTimeout);
gDockerEventStream.end();
callback();
}
+79 -33
View File
@@ -150,6 +150,9 @@ function validatePortBindings(portBindings, tcpPorts) {
2020, /* install server */
config.get('port'), /* app server (lo) */
config.get('internalPort'), /* internal app server (lo) */
config.get('ldapPort'), /* ldap server (lo) */
config.get('oauthProxyPort'), /* oauth proxy server (lo) */
config.get('simpleAuthPort'), /* simple auth server (lo) */
3306, /* mysql (lo) */
8000 /* graphite (lo) */
];
@@ -278,13 +281,14 @@ function purchase(appStoreId, callback) {
});
}
function install(appId, appStoreId, manifest, location, portBindings, accessRestriction, icon, callback) {
function install(appId, appStoreId, manifest, location, portBindings, accessRestriction, oauthProxy, icon, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof appStoreId, 'string');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(typeof accessRestriction, 'string');
assert.strictEqual(typeof oauthProxy, 'boolean');
assert(!icon || typeof icon === 'string');
assert.strictEqual(typeof callback, 'function');
@@ -316,7 +320,7 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
purchase(appStoreId, function (error) {
if (error) return callback(error);
appdb.add(appId, appStoreId, manifest, location.toLowerCase(), portBindings, accessRestriction, function (error) {
appdb.add(appId, appStoreId, manifest, location.toLowerCase(), portBindings, accessRestriction, oauthProxy, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location.toLowerCase(), portBindings, error));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -327,11 +331,12 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
});
}
function configure(appId, location, portBindings, accessRestriction, callback) {
function configure(appId, location, portBindings, accessRestriction, oauthProxy, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(typeof accessRestriction, 'string');
assert.strictEqual(typeof oauthProxy, 'boolean');
assert.strictEqual(typeof callback, 'function');
var error = validateHostname(location, config.fqdn());
@@ -350,12 +355,14 @@ function configure(appId, location, portBindings, accessRestriction, callback) {
var values = {
location: location.toLowerCase(),
accessRestriction: accessRestriction,
oauthProxy: oauthProxy,
portBindings: portBindings,
oldConfig: {
location: app.location,
accessRestriction: app.accessRestriction,
portBindings: app.portBindings
portBindings: app.portBindings,
oauthProxy: app.oauthProxy
}
};
@@ -509,6 +516,7 @@ function restore(appId, callback) {
oldConfig: {
location: app.location,
accessRestriction: app.accessRestriction,
oauthProxy: app.oauthProxy,
portBindings: app.portBindings,
manifest: app.manifest
}
@@ -688,33 +696,35 @@ function autoupdateApps(updateInfo, callback) { // updateInfo is { appId -> { ma
}, callback);
}
function backupApp(app, addonsToBackup, callback) {
function canBackupApp(app) {
// only backup apps that are installed or pending configure or called from apptask. Rest of them are in some
// state not good for consistent backup (i.e addons may not have been setup completely)
return (app.installationState === appdb.ISTATE_INSTALLED && app.health === appdb.HEALTH_HEALTHY) ||
app.installationState === appdb.ISTATE_PENDING_CONFIGURE ||
app.installationState === appdb.ISTATE_PENDING_BACKUP || // called from apptask
app.installationState === appdb.ISTATE_PENDING_UPDATE; // called from apptask
}
// set the 'creation' date of lastBackup so that the backup persists across time based archival rules
// s3 does not allow changing creation time, so copying the last backup is easy way out for now
function reuseOldBackup(app, callback) {
assert.strictEqual(typeof app.lastBackupId, 'string');
assert.strictEqual(typeof callback, 'function');
backups.copyLastBackup(app, function (error, newBackupId) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
debugApp(app, 'reuseOldBackup: reused old backup %s as %s', app.lastBackupId, newBackupId);
callback(null, newBackupId);
});
}
function createNewBackup(app, addonsToBackup, callback) {
assert.strictEqual(typeof app, 'object');
assert(!addonsToBackup || typeof addonsToBackup, 'object');
assert.strictEqual(typeof callback, 'function');
function canBackupApp(app) {
// only backup apps that are installed or pending configure. Rest of them are in some
// state not good for consistent backup (i.e addons may not have been setup completely)
return (app.installationState === appdb.ISTATE_INSTALLED && app.health === appdb.HEALTH_HEALTHY) ||
app.installationState === appdb.ISTATE_PENDING_CONFIGURE ||
app.installationState === appdb.ISTATE_PENDING_BACKUP ||
app.installationState === appdb.ISTATE_PENDING_UPDATE; // called from apptask
}
if (!canBackupApp(app)) return callback(new AppsError(AppsError.BAD_STATE, 'App not healthy'));
var appConfig = {
manifest: app.manifest,
location: app.location,
portBindings: app.portBindings,
accessRestriction: app.accessRestriction
};
if (!safe.fs.writeFileSync(path.join(paths.DATA_DIR, app.id + '/config.json'), JSON.stringify(appConfig), 'utf8')) {
return callback(safe.error);
}
backups.getBackupUrl(app, function (error, result) {
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -729,13 +739,50 @@ function backupApp(app, addonsToBackup, callback) {
], function (error) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
debugApp(app, 'backupApp: successful id:%s', result.id);
callback(null, result.id);
});
});
}
setRestorePoint(app.id, result.id, appConfig, function (error) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
function backupApp(app, addonsToBackup, callback) {
assert.strictEqual(typeof app, 'object');
assert(!addonsToBackup || typeof addonsToBackup, 'object');
assert.strictEqual(typeof callback, 'function');
return callback(null, result.id);
});
var appConfig = null, backupFunction;
if (!canBackupApp(app)) {
if (!app.lastBackupId) {
debugApp(app, 'backupApp: cannot backup app');
return callback(new AppsError(AppsError.BAD_STATE, 'App not healthy and never backed up previously'));
}
appConfig = app.lastBackupConfig;
backupFunction = reuseOldBackup.bind(null, app);
} else {
appConfig = {
manifest: app.manifest,
location: app.location,
portBindings: app.portBindings,
accessRestriction: app.accessRestriction,
oauthProxy: app.oauthProxy
};
backupFunction = createNewBackup.bind(null, app, addonsToBackup);
if (!safe.fs.writeFileSync(path.join(paths.DATA_DIR, app.id + '/config.json'), JSON.stringify(appConfig), 'utf8')) {
return callback(safe.error);
}
}
backupFunction(function (error, backupId) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
debugApp(app, 'backupApp: successful id:%s', backupId);
setRestorePoint(app.id, backupId, appConfig, function (error) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
return callback(null, backupId);
});
});
}
@@ -778,4 +825,3 @@ function restoreApp(app, addonsToRestore, callback) {
});
});
}
+66 -34
View File
@@ -25,6 +25,12 @@ exports = module.exports = {
require('supererror')({ splatchError: true });
// remove timestamp from debug() based output
require('debug').formatArgs = function formatArgs() {
arguments[0] = this.namespace + ' ' + arguments[0];
return arguments;
};
var addons = require('./addons.js'),
appdb = require('./appdb.js'),
apps = require('./apps.js'),
@@ -45,6 +51,7 @@ var addons = require('./addons.js'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
semver = require('semver'),
shell = require('./shell.js'),
SubdomainError = require('./subdomainerror.js'),
subdomains = require('./subdomains.js'),
@@ -73,6 +80,14 @@ function debugApp(app, args) {
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function targetBoxVersion(manifest) {
if ('targetBoxVersion' in manifest) return manifest.targetBoxVersion;
if ('minBoxVersion' in manifest) return manifest.minBoxVersion;
return '0.0.1';
}
// We expect conflicts to not happen despite closing the port (parallel app installs, app update does not reconfigure nginx etc)
// https://tools.ietf.org/html/rfc6056#section-3.5 says linux uses random ephemeral port allocation
function getFreePort(callback) {
@@ -94,7 +109,7 @@ function configureNginx(app, callback) {
if (error) return callback(error);
var sourceDir = path.resolve(__dirname, '..');
var endpoint = app.accessRestriction ? 'oauthproxy' : 'app';
var endpoint = app.oauthProxy ? 'oauthproxy' : 'app';
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, { sourceDir: sourceDir, adminOrigin: config.adminOrigin(), vhost: config.appFqdn(app.location), port: freePort, endpoint: endpoint });
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
@@ -126,23 +141,21 @@ function unconfigureNginx(app, callback) {
vbox.unforwardFromHostToVirtualBox(app.id + '-http');
}
function downloadImage(app, callback) {
debugApp(app, 'downloadImage %s', app.manifest.dockerImage);
function pullImage(app, callback) {
docker.pull(app.manifest.dockerImage, function (err, stream) {
if (err) return callback(new Error('Error connecting to docker'));
if (err) return callback(new Error('Error connecting to docker. statusCode: %s' + err.statusCode));
// https://github.com/dotcloud/docker/issues/1074 says each status message
// is emitted as a chunk
stream.on('data', function (chunk) {
var data = safe.JSON.parse(chunk) || { };
debugApp(app, 'downloadImage data: %j', data);
debugApp(app, 'pullImage data: %j', data);
// The information here is useless because this is per layer as opposed to per image
if (data.status) {
debugApp(app, 'progress: %s', data.status); // progressDetail { current, total }
// debugApp(app, 'progress: %s', data.status); // progressDetail { current, total }
} else if (data.error) {
debugApp(app, 'error detail: %s', data.errorDetail.message);
debugApp(app, 'pullImage error detail: %s', data.errorDetail.message);
}
});
@@ -152,25 +165,40 @@ function downloadImage(app, callback) {
var image = docker.getImage(app.manifest.dockerImage);
image.inspect(function (err, data) {
if (err) {
return callback(new Error('Error inspecting image:' + err.message));
}
if (!data || !data.Config) {
return callback(new Error('Missing Config in image:' + JSON.stringify(data, null, 4)));
}
if (!data.Config.Entrypoint && !data.Config.Cmd) {
return callback(new Error('Only images with entry point are allowed'));
}
if (err) return callback(new Error('Error inspecting image:' + err.message));
if (!data || !data.Config) return callback(new Error('Missing Config in image:' + JSON.stringify(data, null, 4)));
if (!data.Config.Entrypoint && !data.Config.Cmd) return callback(new Error('Only images with entry point are allowed'));
debugApp(app, 'This image exposes ports: %j', data.Config.ExposedPorts);
return callback(null);
callback(null);
});
});
stream.on('error', function (error) {
debugApp(app, 'pullImage error : %j', error);
callback(error);
});
});
}
function downloadImage(app, callback) {
debugApp(app, 'downloadImage %s', app.manifest.dockerImage);
var attempt = 1;
async.retry({ times: 5, interval: 15000 }, function (retryCallback) {
debugApp(app, 'Downloading image. attempt: %s', attempt++);
pullImage(app, function (error) {
if (error) console.error(error);
retryCallback(error);
});
}, callback);
}
function createContainer(app, callback) {
appdb.getPortBindings(app.id, function (error, portBindings) {
if (error) return callback(error);
@@ -204,7 +232,12 @@ function createContainer(app, callback) {
Image: app.manifest.dockerImage,
Cmd: null,
Env: env.concat(addonEnv),
ExposedPorts: exposedPorts
ExposedPorts: exposedPorts,
Volumes: { // see also ReadonlyRootfs
'/tmp': {},
'/run': {},
'/var/log': {}
}
};
debugApp(app, 'Creating container for %s', app.manifest.dockerImage);
@@ -225,7 +258,7 @@ function deleteContainer(app, callback) {
var removeOptions = {
force: true, // kill container if it's running
v: false // removes volumes associated with the container
v: true // removes volumes associated with the container (but not host mounts)
};
container.remove(removeOptions, function (error) {
@@ -274,13 +307,13 @@ function allocateOAuthProxyCredentials(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (!app.accessRestriction) return callback(null);
if (!app.oauthProxy) return callback(null);
var appId = 'proxy-' + app.id;
var id = 'cid-proxy-' + uuid.v4();
var clientSecret = hat(256);
var redirectURI = 'https://' + config.appFqdn(app.location);
var scope = 'profile,' + app.accessRestriction;
var scope = 'profile,roleUser';
clientdb.add(id, appId, clientSecret, redirectURI, scope, callback);
}
@@ -341,12 +374,14 @@ function startContainer(app, callback) {
MemorySwap: memoryLimit, // Memory + Swap
PortBindings: dockerPortBindings,
PublishAllPorts: false,
ReadonlyRootfs: semver.gte(targetBoxVersion(app.manifest), '0.0.66'), // see also Volumes in startContainer
Links: addons.getLinksSync(app, app.manifest.addons),
RestartPolicy: {
"Name": "always",
"MaximumRetryCount": 0
},
CpuShares: 512 // relative to 1024 for system processes
CpuShares: 512, // relative to 1024 for system processes
SecurityOpt: config.CLOUDRON ? [ "apparmor:docker-cloudron-app" ] : null // profile available only on cloudron
};
var container = docker.getContainer(app.containerId);
@@ -429,11 +464,11 @@ function registerSubdomain(app, callback) {
// need to register it so that we have a dnsRecordId to wait for it to complete
var record = { subdomain: app.location, type: 'A', value: sysinfo.getIp() };
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Registering subdomain location [%s]', app.location);
subdomains.add(record, function (error, changeId) {
if (error && error.reason === SubdomainError.STILL_BUSY) return retryCallback(error); // try again
if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(null, error || changeId);
});
@@ -457,9 +492,9 @@ function unregisterSubdomain(app, location, callback) {
debugApp(app, 'Unregistering subdomain: %s', location);
subdomains.remove(record, function (error) {
if (error && error.reason === SubdomainError.STILL_BUSY) return retryCallback(error); // try again
if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR))return retryCallback(error); // try again
retryCallback(null);
retryCallback(error);
});
}, function (error) {
if (error) debugApp(app, 'Error unregistering subdomain: %s', error);
@@ -499,7 +534,7 @@ function waitForDnsPropagation(app, callback) {
// updates the app object and the database
function updateApp(app, values, callback) {
debugApp(app, 'installationState: %s progress: %s', app.installationState, app.installationProgress);
debugApp(app, 'updating app with values: %j', values);
appdb.update(app.id, values, function (error) {
if (error) return callback(error);
@@ -835,10 +870,7 @@ function uninstall(app, callback) {
function runApp(app, callback) {
startContainer(app, function (error) {
if (error) {
debugApp(app, 'Error starting container : %s', error);
return updateApp(app, { runState: appdb.RSTATE_ERROR }, callback);
}
if (error) return callback(error);
updateApp(app, { runState: appdb.RSTATE_RUNNING }, callback);
});
+22 -1
View File
@@ -8,7 +8,9 @@ exports = module.exports = {
addSubdomain: addSubdomain,
delSubdomain: delSubdomain,
getChangeStatus: getChangeStatus
getChangeStatus: getChangeStatus,
copyObject: copyObject
};
var assert = require('assert'),
@@ -259,3 +261,22 @@ function getChangeStatus(changeId, callback) {
});
});
}
function copyObject(from, to, callback) {
assert.strictEqual(typeof from, 'string');
assert.strictEqual(typeof to, 'string');
assert.strictEqual(typeof callback, 'function');
getAWSCredentials(function (error, credentials) {
if (error) return callback(error);
var params = {
Bucket: config.aws().backupBucket, // target bucket
Key: config.aws().backupPrefix + '/' + to, // target file
CopySource: config.aws().backupBucket + '/' + config.aws().backupPrefix + '/' + from, // source
};
var s3 = new AWS.S3(credentials);
s3.copyObject(params, callback);
});
}
+18 -3
View File
@@ -6,7 +6,9 @@ exports = module.exports = {
getAllPaged: getAllPaged,
getBackupUrl: getBackupUrl,
getRestoreUrl: getRestoreUrl
getRestoreUrl: getRestoreUrl,
copyLastBackup: copyLastBackup
};
var assert = require('assert'),
@@ -76,7 +78,7 @@ function getBackupUrl(app, callback) {
backupKey: config.backupKey()
};
debug('getBackupUrl: ', obj);
debug('getBackupUrl: id:%s url:%s sessionToken:%s backupKey:%s', obj.id, obj.url, obj.sessionToken, obj.backupKey);
callback(null, obj);
});
@@ -97,8 +99,21 @@ function getRestoreUrl(backupId, callback) {
backupKey: config.backupKey()
};
debug('getRestoreUrl: ', obj);
debug('getRestoreUrl: id:%s url:%s sessionToken:%s backupKey:%s', obj.id, obj.url, obj.sessionToken, obj.backupKey);
callback(null, obj);
});
}
function copyLastBackup(app, callback) {
assert(app && typeof app === 'object');
assert.strictEqual(typeof app.lastBackupId, 'string');
assert.strictEqual(typeof callback, 'function');
var toFilename = util.format('appbackup_%s_%s-v%s.tar.gz', app.id, (new Date()).toISOString(), app.manifest.version);
aws.copyObject(app.lastBackupId, toFilename, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
return callback(null, toFilename);
});
}
+7 -2
View File
@@ -22,7 +22,9 @@ function addSubdomain(zoneName, subdomain, type, value, callback) {
assert.strictEqual(typeof value, 'string');
assert.strictEqual(typeof callback, 'function');
debug('addSubdomain: ' + subdomain + ' for domain ' + zoneName + ' with value ' + value);
var fqdn = subdomain !== '' && type === 'TXT' ? subdomain + '.' + config.fqdn() : config.appFqdn(subdomain);
debug('addSubdomain: zoneName: %s subdomain: %s type: %s value: %s fqdn: %s', zoneName, subdomain, type, value, fqdn);
var data = {
type: type,
@@ -30,11 +32,12 @@ function addSubdomain(zoneName, subdomain, type, value, callback) {
};
superagent
.post(config.apiServerOrigin() + '/api/v1/domains/' + config.appFqdn(subdomain))
.post(config.apiServerOrigin() + '/api/v1/domains/' + fqdn)
.query({ token: config.token() })
.send(data)
.end(function (error, result) {
if (error) return callback(error);
if (result.status === 420) return callback(new SubdomainError(SubdomainError.STILL_BUSY));
if (result.status !== 201) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
return callback(null, result.body.changeId);
@@ -61,6 +64,8 @@ function delSubdomain(zoneName, subdomain, type, value, callback) {
.send(data)
.end(function (error, result) {
if (error) return callback(error);
if (result.status === 420) return callback(new SubdomainError(SubdomainError.STILL_BUSY));
if (result.status === 404) return callback(new SubdomainError(SubdomainError.NOT_FOUND));
if (result.status !== 204) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
return callback(null);
+24 -28
View File
@@ -19,7 +19,8 @@ exports = module.exports = {
reboot: reboot,
migrate: migrate,
backup: backup,
ensureBackup: ensureBackup};
ensureBackup: ensureBackup
};
var apps = require('./apps.js'),
AppsError = require('./apps.js').AppsError,
@@ -138,7 +139,7 @@ function setTimeZone(ip, callback) {
}
if (!result.body.timezone) {
debug('No timezone in geoip response');
debug('No timezone in geoip response : %j', result.body);
return callback(null);
}
@@ -158,7 +159,7 @@ function activate(username, password, email, name, ip, callback) {
debug('activating user:%s email:%s', username, email);
setTimeZone(ip, function () { });
setTimeZone(ip, function () { }); // TODO: get this from user. note that timezone is detected based on the browser location and not the cloudron region
if (!name) name = settings.getDefaultSync(settings.CLOUDRON_NAME_KEY);
@@ -323,31 +324,23 @@ function addDnsRecords() {
function checkIfInSync() {
debug('addDnsRecords: Check if admin DNS record is in sync.');
var allDone = true;
async.each(changeIds, function (changeId, callback) {
async.eachSeries(changeIds, function (changeId, callback) {
subdomains.status(changeId, function (error, result) {
if (error) return callback(new Error('Failed to check if admin DNS record is in sync.', error));
if (result !== 'done') allDone = false;
if (result !== 'done') return callback(new Error(changeId + ' is not in sync. result:' + result));
callback(null);
});
}, function (error) {
if (error) console.error(error);
// retry if needed
if (error || !allDone) {
if (error) {
console.error(error);
gAddDnsRecordsTimerId = setTimeout(checkIfInSync, 5000);
return;
}
config.set('dnsInSync', true);
// send heartbeat after the dns records are done
sendHeartbeat();
debug('addDnsRecords: done');
config.set('dnsInSync', true);
sendHeartbeat(); // send heartbeat after the dns records are done
});
}
@@ -463,7 +456,7 @@ function doUpgrade(boxUpdateInfo, callback) {
callback(e);
}
progress.set(progress.UPDATE, 5, 'Create app and box backup for upgrade');
progress.set(progress.UPDATE, 5, 'Backing up for upgrade');
backupBoxAndApps(function (error) {
if (error) return upgradeError(error);
@@ -473,7 +466,7 @@ function doUpgrade(boxUpdateInfo, callback) {
.send({ version: boxUpdateInfo.version })
.end(function (error, result) {
if (error) return upgradeError(new Error('Error making upgrade request: ' + error));
if (result.status !== 202) return upgradeError(new Error('Server not ready to upgrade: ' + result.body));
if (result.status !== 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');
@@ -492,9 +485,9 @@ function doUpdate(boxUpdateInfo, callback) {
callback(e);
}
progress.set(progress.UPDATE, 5, 'Create box backup for update');
progress.set(progress.UPDATE, 5, 'Backing up for update');
backupBox(function (error) {
backupBoxAndApps(function (error) {
if (error) return updateError(error);
// fetch a signed sourceTarballUrl
@@ -503,7 +496,7 @@ function doUpdate(boxUpdateInfo, callback) {
.end(function (error, result) {
if (error) return updateError(new Error('Error fetching sourceTarballUrl: ' + error));
if (result.status !== 200) return updateError(new Error('Error fetching sourceTarballUrl status: ' + result.status));
if (!safe.query(result, 'body.url')) return updateError(new Error('Error fetching sourceTarballUrl response: ' + result.body));
if (!safe.query(result, 'body.url')) return updateError(new Error('Error fetching sourceTarballUrl response: ' + JSON.stringify(result.body)));
// NOTE: the args here are tied to the installer revision, box code and appstore provisioning logic
var args = {
@@ -531,7 +524,7 @@ function doUpdate(boxUpdateInfo, callback) {
superagent.post(INSTALLER_UPDATE_URL).send(args).end(function (error, result) {
if (error) return updateError(error);
if (result.status !== 202) return updateError(new Error('Error initiating update: ' + result.body));
if (result.status !== 202) return updateError(new Error('Error initiating update: ' + JSON.stringify(result.body)));
progress.set(progress.UPDATE, 10, 'Updating cloudron software');
@@ -549,6 +542,9 @@ function backup(callback) {
var error = locker.lock(locker.OP_FULL_BACKUP);
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
// clearing backup ensures tools can 'wait' on progress
progress.clear(progress.BACKUP);
// start the backup operation in the background
backupBoxAndApps(function (error) {
if (error) console.error('backup failed.', error);
@@ -633,12 +629,12 @@ function backupBoxAndApps(callback) {
apps.backupApp(app, app.manifest.addons, function (error, backupId) {
progress.set(progress.BACKUP, step * processed, 'Backed up app at ' + app.location);
if (error && error.reason === AppsError.BAD_STATE) {
debugApp(app, 'Skipping backup (istate:%s health:%s). using lastBackupId:%s', app.installationState, app.health, app.lastBackupId);
backupId = app.lastBackupId;
if (error && error.reason !== AppsError.BAD_STATE) {
debugApp(app, 'Unable to backup', error);
return iteratorCallback(error);
}
return iteratorCallback(null, backupId);
iteratorCallback(null, backupId || null); // clear backupId if is in BAD_STATE and never backed up
});
}, function appsBackedUp(error, backupIds) {
if (error) {
@@ -646,7 +642,7 @@ function backupBoxAndApps(callback) {
return callback(error);
}
backupIds = backupIds.filter(function (id) { return id !== null; }); // remove apps that were never backed up
backupIds = backupIds.filter(function (id) { return id !== null; }); // remove apps in bad state that were never backed up
backupBoxWithAppBackupIds(backupIds, function (error, restoreKey) {
progress.set(progress.BACKUP, 100, error ? error.message : '');
+7
View File
@@ -25,6 +25,7 @@ exports = module.exports = {
// these values are derived
adminOrigin: adminOrigin,
internalAdminOrigin: internalAdminOrigin,
appFqdn: appFqdn,
zoneName: zoneName,
@@ -73,6 +74,8 @@ function initConfig() {
data.webServerOrigin = null;
data.internalPort = 3001;
data.ldapPort = 3002;
data.oauthProxyPort = 3003;
data.simpleAuthPort = 3004;
data.backupKey = 'backupKey';
data.aws = {
backupBucket: null,
@@ -162,6 +165,10 @@ function adminOrigin() {
return 'https://' + appFqdn(constants.ADMIN_LOCATION);
}
function internalAdminOrigin() {
return 'http://127.0.0.1:' + get('port');
}
function token() {
return get('token');
}
+3 -1
View File
@@ -48,6 +48,8 @@ function recreateJobs(unusedTimeZone, callback) {
if (typeof unusedTimeZone === 'function') callback = unusedTimeZone;
settings.getAll(function (error, allSettings) {
debug('Creating jobs with timezone %s', allSettings[settings.TIME_ZONE_KEY]);
if (gHeartbeatJob) gHeartbeatJob.stop();
gHeartbeatJob = new CronJob({
cronTime: '00 */1 * * * *', // every minute
@@ -110,7 +112,7 @@ function autoupdatePatternChanged(pattern) {
}
},
start: true,
timeZone: gBoxUpdateCheckerJob.cronTime.timeZone // hack
timeZone: gBoxUpdateCheckerJob.cronTime.zone // hack
});
}
+8 -4
View File
@@ -63,7 +63,6 @@ function start(callback) {
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && req.filter.matches(tmp.attributes)) {
res.send(tmp);
debug('ldap user send:', tmp);
}
});
@@ -100,7 +99,6 @@ function start(callback) {
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && req.filter.matches(tmp.attributes)) {
res.send(tmp);
debug('ldap group send:', tmp);
}
});
@@ -108,8 +106,14 @@ function start(callback) {
});
});
gServer.bind('dc=cloudron', function(req, res, next) {
debug('ldap bind: %s', req.dn.toString());
gServer.bind('ou=apps,dc=cloudron', function(req, res, next) {
// TODO: validate password
debug('ldap application bind: %s', req.dn.toString());
res.end();
});
gServer.bind('ou=users,dc=cloudron', function(req, res, next) {
debug('ldap user bind: %s', req.dn.toString());
if (!req.dn.rdns[0].cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
+2 -2
View File
@@ -2,7 +2,7 @@
Dear Cloudron Team,
unfortunately the <%= program %> on <%= fqdn %> crashed unexpectedly!
Unfortunately <%= program %> on <%= fqdn %> crashed unexpectedly!
Please see some excerpt of the logs below.
@@ -11,7 +11,7 @@ Your Cloudron
-------------------------------------
<%= context %>
<%- context %>
<% } else { %>
+2 -2
View File
@@ -87,13 +87,13 @@ function processQueue() {
var transport = nodemailer.createTransport(smtpTransport({
host: mailServerIp,
port: 25
port: 2500 // this value comes from mail container
}));
var mailQueueCopy = gMailQueue;
gMailQueue = [ ];
debug('Processing mail queue of size %d', mailQueueCopy.length);
debug('Processing mail queue of size %d (through %s:2500)', mailQueueCopy.length, mailServerIp);
async.mapSeries(mailQueueCopy, function iterator(mailOptions, callback) {
transport.sendMail(mailOptions, function (error) {
-38
View File
@@ -1,38 +0,0 @@
<% include header %>
<form action="/api/v1/oauth/dialog/authorize/decision" method="post">
<input name="transaction_id" type="hidden" value="<%= transactionID %>">
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
<div class="row">
<div class="col-md-3"></div>
<div class="col-md-6">
<div class="container-fluid">
<div class="row">
<div class="col-sm-12">
Hi <%= user.username %>!
</div>
</div>
<div class="row">
<div class="col-sm-12">
<b><%= client.name %></b> is requesting access to your account.
</div>
</div>
<div class="row">
<div class="col-sm-12">
Do you approve?
</div>
</div>
<div class="row">
<div class="col-sm-12">
<input class="btn btn-danger btn-outline" type="submit" value="Deny" name="cancel" id="deny"/>
<input class="btn btn-success btn-outline" type="submit" value="Allow"/>
</div>
</div>
</div>
</div>
<div class="col-md-3"></div>
</div>
</form>
<% include footer %>
+10
View File
@@ -26,3 +26,13 @@
</head>
<body class="oauth">
<!-- Navigation -->
<nav class="navbar navbar-default navbar-static-top shadow" role="navigation" style="margin-bottom: 0">
<div class="container-fluid">
<div class="navbar-header">
<span class="navbar-brand navbar-brand-icon"><img src="/api/v1/cloudron/avatar" width="40" height="40"/></span>
<span class="navbar-brand"><%= cloudronName %></span>
</div>
</div>
</nav>
+1 -1
View File
@@ -7,7 +7,7 @@
<div class="row">
<div class="col-md-12" style="text-align: center;">
<img width="128" height="128" src="<%= applicationLogo %>"/>
<h1>Login to <%= applicationName %> on <%= cloudronName %></h1>
<h1><small>Login to</small> <%= applicationName %></h1>
<br/>
</div>
</div>
+12 -10
View File
@@ -5,8 +5,6 @@ exports = module.exports = {
stop: stop
};
require('supererror')({ splatchError: true });
var appdb = require('./appdb.js'),
assert = require('assert'),
clientdb = require('./clientdb.js'),
@@ -20,9 +18,6 @@ var appdb = require('./appdb.js'),
url = require('url'),
uuid = require('node-uuid');
// Allow self signed certs!
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
var gSessions = {};
var gProxyMiddlewareCache = {};
var gHttpServer = null;
@@ -51,7 +46,11 @@ function verifySession(req, res, next) {
return next();
}
superagent.get(config.adminOrigin() + '/api/v1/profile').query({ access_token: req.sessionData.accessToken}).end(function (error, result) {
// use http admin origin so that it works with self-signed certs
superagent
.get(config.internalAdminOrigin() + '/api/v1/profile')
.query({ access_token: req.sessionData.accessToken})
.end(function (error, result) {
if (error) {
console.error(error);
req.authenticated = false;
@@ -85,7 +84,11 @@ function authenticate(req, res, next) {
client_secret: req.sessionData.clientSecret
};
superagent.post(config.adminOrigin() + '/api/v1/oauth/token').query(query).send(data).end(function (error, result) {
// use http admin origin so that it works with self-signed certs
superagent
.post(config.internalAdminOrigin() + '/api/v1/oauth/token')
.query(query).send(data)
.end(function (error, result) {
if (error) {
console.error(error);
return res.send(500, 'Unable to contact the oauth server.');
@@ -174,13 +177,12 @@ function initializeServer() {
return httpServer;
}
function start(port, callback) {
assert.strictEqual(typeof port, 'number');
function start(callback) {
assert.strictEqual(typeof callback, 'function');
gHttpServer = initializeServer();
gHttpServer.listen(port, callback);
gHttpServer.listen(config.get('oauthProxyPort'), callback);
}
function stop(callback) {
+3 -1
View File
@@ -24,5 +24,7 @@ exports = module.exports = {
CLOUDRON_AVATAR_FILE: path.join(config.baseDir(), 'data/box/avatar.png'),
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
FAVICON_FILE: path.join(__dirname + '/../assets/favicon.ico')
FAVICON_FILE: path.join(__dirname + '/../assets/favicon.ico'),
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'data/box/updatechecker.json')
};
+6 -3
View File
@@ -43,6 +43,7 @@ function removeInternalAppFields(app) {
health: app.health,
location: app.location,
accessRestriction: app.accessRestriction,
oauthProxy: app.oauthProxy,
lastBackupId: app.lastBackupId,
manifest: app.manifest,
portBindings: app.portBindings,
@@ -114,14 +115,15 @@ function installApp(req, res, next) {
if (typeof data.location !== 'string') return next(new HttpError(400, 'location is required'));
if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
if (typeof data.accessRestriction !== 'string') return next(new HttpError(400, 'accessRestriction is required'));
if (typeof data.oauthProxy !== 'boolean') return next(new HttpError(400, 'oauthProxy must be a boolean'));
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
// allow tests to provide an appId for testing
var appId = (process.env.BOX_ENV === 'test' && typeof data.appId === 'string') ? data.appId : uuid.v4();
debug('Installing app id:%s storeid:%s loc:%s port:%j restrict:%s manifest:%j', appId, data.appStoreId, data.location, data.portBindings, data.accessRestriction, data.manifest);
debug('Installing app id:%s storeid:%s loc:%s port:%j restrict:%s oauthproxy:%s manifest:%j', appId, data.appStoreId, data.location, data.portBindings, data.accessRestriction, data.oauthProxy, data.manifest);
apps.install(appId, data.appStoreId, data.manifest, data.location, data.portBindings || null, data.accessRestriction, data.icon || null, function (error) {
apps.install(appId, data.appStoreId, data.manifest, data.location, data.portBindings || null, data.accessRestriction, data.oauthProxy, data.icon || null, function (error) {
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
@@ -150,10 +152,11 @@ function configureApp(req, res, next) {
if (typeof data.location !== 'string') return next(new HttpError(400, 'location is required'));
if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
if (typeof data.accessRestriction !== 'string') return next(new HttpError(400, 'accessRestriction is required'));
if (typeof data.oauthProxy !== 'boolean') return next(new HttpError(400, 'oauthProxy must be a boolean'));
debug('Configuring app id:%s location:%s bindings:%j', req.params.id, data.location, data.portBindings);
apps.configure(req.params.id, data.location, data.portBindings || null, data.accessRestriction, function (error) {
apps.configure(req.params.id, data.location, data.portBindings || null, data.accessRestriction, data.oauthProxy, function (error) {
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
+7 -4
View File
@@ -7,6 +7,7 @@ exports = module.exports = {
};
var cloudron = require('../cloudron.js'),
CloudronError = require('../cloudron.js').CloudronError,
debug = require('debug')('box:routes/internal'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
@@ -14,10 +15,12 @@ var cloudron = require('../cloudron.js'),
function backup(req, res, next) {
debug('trigger backup');
// note that cloudron.backup only waits for backup initiation and not for backup to complete
// backup progress can be checked up ny polling the progress api call
cloudron.backup(function (error) {
if (error) debug('Internal route backup failed', error);
});
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
// we always succeed to trigger a backup
next(new HttpSuccess(202, {}));
next(new HttpSuccess(202, {}));
});
}
+65 -69
View File
@@ -148,6 +148,21 @@ session.ensureLoggedIn = function (redirectTo) {
};
};
function renderTemplate(res, template, data) {
assert.strictEqual(typeof res, 'object');
assert.strictEqual(typeof template, 'string');
assert.strictEqual(typeof data, 'object');
settings.getCloudronName(function (error, cloudronName) {
if (error) console.error(error);
// amend details which the header expects
data.cloudronName = cloudronName || 'Cloudron';
res.render(template, data);
});
}
function sendErrorPageOrRedirect(req, res, message) {
assert.strictEqual(typeof req, 'object');
assert.strictEqual(typeof res, 'object');
@@ -156,16 +171,19 @@ function sendErrorPageOrRedirect(req, res, message) {
debug('sendErrorPageOrRedirect: returnTo "%s".', req.query.returnTo, message);
if (typeof req.query.returnTo !== 'string') {
res.render('error', {
renderTemplate(res, 'error', {
adminOrigin: config.adminOrigin(),
message: message
});
} else {
var u = url.parse(req.query.returnTo);
if (!u.protocol || !u.host) return res.render('error', {
adminOrigin: config.adminOrigin(),
message: 'Invalid request. returnTo query is not a valid URI. ' + message
});
if (!u.protocol || !u.host) {
renderTemplate(res, 'error', {
adminOrigin: config.adminOrigin(),
message: 'Invalid request. returnTo query is not a valid URI. ' + message
});
return;
}
res.redirect(util.format('%s//%s', u.protocol, u.host));
}
@@ -178,7 +196,7 @@ function sendError(req, res, message) {
assert.strictEqual(typeof res, 'object');
assert.strictEqual(typeof message, 'string');
res.render('error', {
renderTemplate(res, 'error', {
adminOrigin: config.adminOrigin(),
message: message
});
@@ -191,47 +209,40 @@ function loginForm(req, res) {
var u = url.parse(req.session.returnTo, true);
if (!u.query.client_id) return sendErrorPageOrRedirect(req, res, 'Invalid login request. No client_id provided.');
var cloudronName = '';
function render(applicationName, applicationLogo) {
res.render('login', {
renderTemplate(res, 'login', {
adminOrigin: config.adminOrigin(),
csrf: req.csrfToken(),
cloudronName: cloudronName,
applicationName: applicationName,
applicationLogo: applicationLogo,
error: req.query.error || null
});
}
settings.getCloudronName(function (error, name) {
if (error) return sendError(req, res, 'Internal Error');
clientdb.get(u.query.client_id, function (error, result) {
if (error) return sendError(req, res, 'Unknown OAuth client');
cloudronName = name;
// Handle our different types of oauth clients
var appId = result.appId;
if (appId === constants.ADMIN_CLIENT_ID) {
return render(constants.ADMIN_NAME, '/api/v1/cloudron/avatar');
} else if (appId === constants.TEST_CLIENT_ID) {
return render(constants.TEST_NAME, '/api/v1/cloudron/avatar');
} else if (appId.indexOf('external-') === 0) {
return render('External Application', '/api/v1/cloudron/avatar');
} else if (appId.indexOf('addon-oauth-') === 0) {
appId = appId.slice('addon-oauth-'.length);
} else if (appId.indexOf('addon-simpleauth-') === 0) {
appId = appId.slice('addon-simpleauth-'.length);
} else if (appId.indexOf('proxy-') === 0) {
appId = appId.slice('proxy-'.length);
}
clientdb.get(u.query.client_id, function (error, result) {
if (error) return sendError(req, res, 'Unknown OAuth client');
appdb.get(appId, function (error, result) {
if (error) return sendErrorPageOrRedirect(req, res, 'Unknown Application for those OAuth credentials');
// Handle our different types of oauth clients
var appId = result.appId;
if (appId === constants.ADMIN_CLIENT_ID) {
return render(constants.ADMIN_NAME, '/api/v1/cloudron/avatar');
} else if (appId === constants.TEST_CLIENT_ID) {
return render(constants.TEST_NAME, '/api/v1/cloudron/avatar');
} else if (appId.indexOf('external-') === 0) {
return render('External Application', '/api/v1/cloudron/avatar');
} else if (appId.indexOf('addon-') === 0) {
appId = appId.slice('addon-'.length);
} else if (appId.indexOf('proxy-') === 0) {
appId = appId.slice('proxy-'.length);
}
appdb.get(appId, function (error, result) {
if (error) return sendErrorPageOrRedirect(req, res, 'Unknown Application for those OAuth credentials');
var applicationName = result.location || config.fqdn();
render(applicationName, '/api/v1/cloudron/avatar');
});
var applicationName = result.location || config.fqdn();
render(applicationName, '/api/v1/apps/' + result.id + '/icon');
});
});
}
@@ -259,7 +270,7 @@ function logout(req, res) {
// Form to enter email address to send a password reset request mail
// -> GET /api/v1/session/password/resetRequest.html
function passwordResetRequestSite(req, res) {
res.render('password_reset_request', { adminOrigin: config.adminOrigin(), csrf: req.csrfToken() });
renderTemplate(res, 'password_reset_request', { adminOrigin: config.adminOrigin(), csrf: req.csrfToken() });
}
// This route is used for above form submission
@@ -283,7 +294,7 @@ function passwordResetRequest(req, res, next) {
// -> GET /api/v1/session/password/sent.html
function passwordSentSite(req, res) {
res.render('password_reset_sent', { adminOrigin: config.adminOrigin() });
renderTemplate(res, 'password_reset_sent', { adminOrigin: config.adminOrigin() });
}
// -> GET /api/v1/session/password/setup.html
@@ -295,7 +306,12 @@ function passwordSetupSite(req, res, next) {
user.getByResetToken(req.query.reset_token, function (error, user) {
if (error) return next(new HttpError(401, 'Invalid reset_token'));
res.render('password_setup', { adminOrigin: config.adminOrigin(), user: user, csrf: req.csrfToken(), resetToken: req.query.reset_token });
renderTemplate(res, 'password_setup', {
adminOrigin: config.adminOrigin(),
user: user,
csrf: req.csrfToken(),
resetToken: req.query.reset_token
});
});
}
@@ -308,7 +324,12 @@ function passwordResetSite(req, res, next) {
user.getByResetToken(req.query.reset_token, function (error, user) {
if (error) return next(new HttpError(401, 'Invalid reset_token'));
res.render('password_reset', { adminOrigin: config.adminOrigin(), user: user, csrf: req.csrfToken(), resetToken: req.query.reset_token });
renderTemplate(res, 'password_reset', {
adminOrigin: config.adminOrigin(),
user: user,
csrf: req.csrfToken(),
resetToken: req.query.reset_token
});
});
}
@@ -343,7 +364,7 @@ var callback = [
session.ensureLoggedIn('/api/v1/session/login'),
function (req, res) {
debug('callback: with callback server ' + req.query.redirectURI);
res.render('callback', { adminOrigin: config.adminOrigin(), callbackServer: req.query.redirectURI });
renderTemplate(res, 'callback', { adminOrigin: config.adminOrigin(), callbackServer: req.query.redirectURI });
}
];
@@ -400,35 +421,11 @@ var authorization = [
callback(null, client, '/api/v1/session/callback?redirectURI=' + url.resolve(redirectOrigin, redirectPath));
});
}),
// Until we have OAuth scopes, skip decision dialog
// OAuth sopes skip START
function (req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.oauth2, 'object');
var scopes = req.oauth2.client.scope ? req.oauth2.client.scope.split(',') : ['profile','roleUser'];
if (scopes.indexOf('roleAdmin') !== -1 && !req.user.admin) {
return sendErrorPageOrRedirect(req, res, 'Admin capabilities required');
}
req.body.transaction_id = req.oauth2.transactionID;
next();
},
gServer.decision(function(req, done) {
debug('decision: with scope', req.oauth2.req.scope);
return done(null, { scope: req.oauth2.req.scope });
// we do not have a decision dialog, no need to load the transaction
gServer.decision({ loadTransaction: false }, function (req, done) {
debug('decision: with scope', req.oauth2.client.scope);
return done(null, { scope: req.oauth2.client.scope });
})
// OAuth sopes skip END
// function (req, res) {
// res.render('dialog', { transactionID: req.oauth2.transactionID, user: req.user, client: req.oauth2.client, csrf: req.csrfToken() });
// }
];
// this triggers the above grant middleware and handles the user's decision if he accepts the access
var decision = [
session.ensureLoggedIn('/api/v1/session/login'),
gServer.decision()
];
@@ -509,7 +506,6 @@ exports = module.exports = {
passwordSetupSite: passwordSetupSite,
passwordReset: passwordReset,
authorization: authorization,
decision: decision,
token: token,
scope: scope,
csrf: csrf
+70 -30
View File
@@ -40,12 +40,17 @@ var appdb = require('../../appdb.js'),
var SERVER_URL = 'http://localhost:' + config.get('port');
// Test image information
var TEST_IMAGE_REPO = 'cloudron/test';
var TEST_IMAGE_TAG = '6.0.0';
var TEST_IMAGE_ID = '7a53b21358cd7b014d29ee85f16ac535c37c11fb1f4c124197941236eb4d7c64';
var APP_STORE_ID = 'test', APP_ID;
var APP_LOCATION = 'appslocation';
var APP_LOCATION_2 = 'appslocationtwo';
var APP_LOCATION_NEW = 'appslocationnew';
var APP_MANIFEST = JSON.parse(fs.readFileSync(__dirname + '/../../../../test-app/CloudronManifest.json', 'utf8'));
APP_MANIFEST.dockerImage = 'girish/test:0.2.0';
APP_MANIFEST.dockerImage = TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG;
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='admin@me.com';
var USERNAME_1 = 'user', PASSWORD_1 = 'password', EMAIL_1 ='user@me.com';
var token = null; // authentication token
@@ -152,7 +157,7 @@ function cleanup(done) {
function (callback) { setTimeout(callback, 2000); }, // give taskmanager tasks couple of seconds to finish
child_process.exec.bind(null, 'docker rm -f mysql; docker rm -f postgresql; docker rm -f mongodb')
child_process.exec.bind(null, 'docker rm -f mysql; docker rm -f postgresql; docker rm -f mongodb; docker rm -f mail')
], done);
}
@@ -162,7 +167,7 @@ describe('App API', function () {
before(function (done) {
dockerProxy = startDockerProxy(function interceptor(req, res) {
if (req.method === 'DELETE' && req.url === '/images/c7ddfc8fb7cd8a14d4d70153a199ff0c6e9b709807aeec5a7b799d60618731d1?force=true&noprune=false') {
if (req.method === 'DELETE' && req.url === '/images/' + TEST_IMAGE_ID + '?force=true&noprune=false') {
res.writeHead(200);
res.end();
return true;
@@ -216,7 +221,7 @@ describe('App API', function () {
it('app install fails - invalid location', function (done) {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: '!awesome', accessRestriction: '' })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: '!awesome', accessRestriction: '', oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('Hostname can only contain alphanumerics and hyphen');
@@ -227,7 +232,7 @@ describe('App API', function () {
it('app install fails - invalid location type', function (done) {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: 42, accessRestriction: '' })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: 42, accessRestriction: '', oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('location is required');
@@ -238,7 +243,7 @@ describe('App API', function () {
it('app install fails - reserved admin location', function (done) {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: constants.ADMIN_LOCATION, accessRestriction: '' })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: constants.ADMIN_LOCATION, accessRestriction: '', oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql(constants.ADMIN_LOCATION + ' is reserved');
@@ -249,7 +254,7 @@ describe('App API', function () {
it('app install fails - reserved api location', function (done) {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: constants.API_LOCATION, accessRestriction: '' })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: constants.API_LOCATION, accessRestriction: '', oauthProxy: true })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql(constants.API_LOCATION + ' is reserved');
@@ -260,7 +265,7 @@ describe('App API', function () {
it('app install fails - portBindings must be object', function (done) {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: 23, accessRestriction: '' })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: 23, accessRestriction: '', oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('portBindings must be an object');
@@ -271,7 +276,7 @@ describe('App API', function () {
it('app install fails - accessRestriction is required', function (done) {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: {} })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: {}, oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('accessRestriction is required');
@@ -279,10 +284,21 @@ describe('App API', function () {
});
});
it('app install fails - oauthProxy is required', function (done) {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: {}, accessRestriction: '' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('oauthProxy must be a boolean');
done(err);
});
});
it('app install fails for non admin', function (done) {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token_1 })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: '' })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: '', oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done(err);
@@ -294,7 +310,7 @@ describe('App API', function () {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: '' })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: '', oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(402);
expect(fake.isDone()).to.be.ok();
@@ -307,7 +323,7 @@ describe('App API', function () {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: '' })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: '', oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(res.body.id).to.be.a('string');
@@ -322,7 +338,7 @@ describe('App API', function () {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: '' })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: '', oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
expect(fake.isDone()).to.be.ok();
@@ -446,7 +462,7 @@ describe('App API', function () {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION_2, portBindings: null, accessRestriction: '' })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION_2, portBindings: null, accessRestriction: '', oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(res.body.id).to.be.a('string');
@@ -475,7 +491,7 @@ describe('App API', function () {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, location: APP_LOCATION+APP_LOCATION, portBindings: null, accessRestriction: '' })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, location: APP_LOCATION+APP_LOCATION, portBindings: null, accessRestriction: '', oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(res.body.id).to.be.a('string');
@@ -510,12 +526,12 @@ describe('App installation', function () {
async.series([
function (callback) {
dockerProxy = startDockerProxy(function interceptor(req, res) {
if (req.method === 'POST' && req.url === '/images/create?fromImage=girish%2Ftest&tag=0.2.0') {
if (req.method === 'POST' && req.url === '/images/create?fromImage=' + encodeURIComponent(TEST_IMAGE_REPO) + '&tag=' + TEST_IMAGE_TAG) {
imageCreated = true;
res.writeHead(200);
res.end();
return true;
} else if (req.method === 'DELETE' && req.url === '/images/c7ddfc8fb7cd8a14d4d70153a199ff0c6e9b709807aeec5a7b799d60618731d1?force=true&noprune=false') {
} else if (req.method === 'DELETE' && req.url === '/images/' + TEST_IMAGE_ID + '?force=true&noprune=false') {
imageDeleted = true;
res.writeHead(200);
res.end();
@@ -586,7 +602,7 @@ describe('App installation', function () {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appId: APP_ID, appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: '' })
.send({ appId: APP_ID, appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: '', oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(fake.isDone()).to.be.ok();
@@ -618,9 +634,9 @@ describe('App installation', function () {
expect(data.Config.Env).to.contain('WEBADMIN_ORIGIN=' + config.adminOrigin());
expect(data.Config.Env).to.contain('API_ORIGIN=' + config.adminOrigin());
expect(data.Config.Env).to.contain('CLOUDRON=1');
clientdb.getByAppId('addon-' + appResult.id, function (error, client) {
clientdb.getByAppId('addon-oauth-' + appResult.id, function (error, client) {
expect(error).to.not.be.ok();
expect(client.id.length).to.be(46); // cid-addon- + 32 hex chars (128 bits) + 4 hyphens
expect(client.id.length).to.be(52); // cid-addon-oauth- + 32 hex chars (128 bits) + 4 hyphens
expect(client.clientSecret.length).to.be(64); // 32 hex chars (256 bits)
expect(data.Config.Env).to.contain('OAUTH_CLIENT_ID=' + client.id);
expect(data.Config.Env).to.contain('OAUTH_CLIENT_SECRET=' + client.clientSecret);
@@ -659,7 +675,14 @@ describe('App installation', function () {
it('installation - running container has volume mounted', function (done) {
docker.getContainer(appEntry.containerId).inspect(function (error, data) {
expect(error).to.not.be.ok();
expect(data.Volumes['/app/data']).to.eql(paths.DATA_DIR + '/' + APP_ID + '/data');
// support newer docker versions
if (data.Volumes) {
expect(data.Volumes['/app/data']).to.eql(paths.DATA_DIR + '/' + APP_ID + '/data');
} else {
expect(data.Mounts.filter(function (mount) { return mount.Destination === '/app/data'; })[0].Source).to.eql(paths.DATA_DIR + '/' + APP_ID + '/data');
}
done();
});
});
@@ -735,7 +758,7 @@ describe('App installation', function () {
child_process.exec('docker exec ' + appContainer.id + ' ' + cmd, { timeout: 5000 }, function (error, stdout, stderr) {
expect(!error).to.be.ok();
expect(stdout.length).to.be(0);
expect(stderr.length).to.be(0);
// expect(stderr.length).to.be(0); // "Warning: Using a password on the command line interface can be insecure."
done();
});
});
@@ -959,12 +982,12 @@ describe('App installation - port bindings', function () {
async.series([
function (callback) {
dockerProxy = startDockerProxy(function interceptor(req, res) {
if (req.method === 'POST' && req.url === '/images/create?fromImage=girish%2Ftest&tag=0.2.0') {
if (req.method === 'POST' && req.url === '/images/create?fromImage=' + encodeURIComponent(TEST_IMAGE_REPO) + '&tag=' + TEST_IMAGE_TAG) {
imageCreated = true;
res.writeHead(200);
res.end();
return true;
} else if (req.method === 'DELETE' && req.url === '/images/c7ddfc8fb7cd8a14d4d70153a199ff0c6e9b709807aeec5a7b799d60618731d1?force=true&noprune=false') {
} else if (req.method === 'DELETE' && req.url === '/images/' + TEST_IMAGE_ID + '?force=true&noprune=false') {
imageDeleted = true;
res.writeHead(200);
res.end();
@@ -1034,7 +1057,7 @@ describe('App installation - port bindings', function () {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appId: APP_ID, appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: { ECHO_SERVER_PORT: 7171 }, accessRestriction: '' })
.send({ appId: APP_ID, appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: { ECHO_SERVER_PORT: 7171 }, accessRestriction: '', oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(fake.isDone()).to.be.ok();
@@ -1114,7 +1137,14 @@ describe('App installation - port bindings', function () {
it('installation - running container has volume mounted', function (done) {
docker.getContainer(appEntry.containerId).inspect(function (error, data) {
expect(error).to.not.be.ok();
expect(data.Volumes['/app/data']).to.eql(paths.DATA_DIR + '/' + APP_ID + '/data');
// support newer docker versions
if (data.Volumes) {
expect(data.Volumes['/app/data']).to.eql(paths.DATA_DIR + '/' + APP_ID + '/data');
} else {
expect(data.Mounts.filter(function (mount) { return mount.Destination === '/app/data'; })[0].Source).to.eql(paths.DATA_DIR + '/' + APP_ID + '/data');
}
done();
});
});
@@ -1188,7 +1218,7 @@ describe('App installation - port bindings', function () {
it('cannot reconfigure app with missing location', function (done) {
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ appId: APP_ID, password: PASSWORD, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: 'roleAdmin' })
.send({ appId: APP_ID, password: PASSWORD, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: 'roleAdmin', oauthProxy: true })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
@@ -1198,7 +1228,17 @@ describe('App installation - port bindings', function () {
it('cannot reconfigure app with missing accessRestriction', function (done) {
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 } })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('cannot reconfigure app with missing oauthProxy', function (done) {
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: '' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
@@ -1208,7 +1248,7 @@ describe('App installation - port bindings', function () {
it('non admin cannot reconfigure app', function (done) {
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token_1 })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: 'roleAdmin' })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: 'roleAdmin', oauthProxy: true })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
@@ -1218,7 +1258,7 @@ describe('App installation - port bindings', function () {
it('can reconfigure app', function (done) {
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: 'roleAdmin' })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: '', oauthProxy: true })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
checkConfigureStatus(0, done);
+1 -1
View File
@@ -51,7 +51,7 @@ function setup(done) {
function addApp(callback) {
var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok', addons: { } };
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, '' /* accessRestriction */, callback);
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, '' /* accessRestriction */, false /* oauthProxy */, callback);
}
], done);
}
+2 -2
View File
@@ -591,8 +591,8 @@ describe('Clients', function () {
email: 'some@email.com',
admin: true,
salt: 'somesalt',
createdAt: (new Date()).toUTCString(),
modifiedAt: (new Date()).toUTCString(),
createdAt: (new Date()).toISOString(),
modifiedAt: (new Date()).toISOString(),
resetToken: hat(256)
};
+1 -1
View File
@@ -54,7 +54,7 @@ function setup(done) {
function addApp(callback) {
var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok' };
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, '' /* accessRestriction */, callback);
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, '' /* accessRestriction */, false /* oauthProxy */, callback);
}
], done);
}
+273
View File
@@ -0,0 +1,273 @@
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
var clientdb = require('../../clientdb.js'),
async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
request = require('superagent'),
server = require('../../server.js'),
simpleauth = require('../../simpleauth.js'),
nock = require('nock'),
userdb = require('../../userdb.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
var SIMPLE_AUTH_ORIGIN = 'http://localhost:' + config.get('simpleAuthPort');
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='silly@me.com';
var CLIENT = {
id: 'someclientid',
appId: 'someappid',
clientSecret: 'someclientsecret',
redirectURI: '',
scope: 'user,profile'
};
var server;
function setup(done) {
async.series([
server.start.bind(server),
simpleauth.start.bind(simpleauth),
userdb._clear,
function createAdmin(callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
request.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
callback();
});
},
function addClient(callback) {
clientdb.add(CLIENT.id, CLIENT.appId, CLIENT.clientSecret, CLIENT.redirectURI, CLIENT.scope, callback);
}
], done);
}
function cleanup(done) {
database._clear(function (error) {
expect(!error).to.be.ok();
server.stop(done);
});
}
describe('SimpleAuth API', function () {
before(setup);
after(cleanup);
describe('login', function () {
it('cannot login without clientId', function (done) {
var body = {};
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot login without username', function (done) {
var body = {
clientId: 'someclientid'
};
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot login without password', function (done) {
var body = {
clientId: 'someclientid',
username: USERNAME
};
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot login with unkown clientId', function (done) {
var body = {
clientId: CLIENT.id+CLIENT.id,
username: USERNAME,
password: PASSWORD
};
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(401);
done();
});
});
it('cannot login with unkown user', function (done) {
var body = {
clientId: CLIENT.id,
username: USERNAME+USERNAME,
password: PASSWORD
};
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(401);
done();
});
});
it('cannot login with empty password', function (done) {
var body = {
clientId: CLIENT.id,
username: USERNAME,
password: ''
};
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(401);
done();
});
});
it('cannot login with wrgon password', function (done) {
var body = {
clientId: CLIENT.id,
username: USERNAME,
password: PASSWORD+PASSWORD
};
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(401);
done();
});
});
it('succeeds', function (done) {
var body = {
clientId: CLIENT.id,
username: USERNAME,
password: PASSWORD
};
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(200);
expect(result.body.accessToken).to.be.a('string');
expect(result.body.user).to.be.an('object');
expect(result.body.user.id).to.be.a('string');
expect(result.body.user.username).to.be.a('string');
expect(result.body.user.email).to.be.a('string');
expect(result.body.user.admin).to.be.a('boolean');
request.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: result.body.accessToken })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.body).to.be.an('object');
expect(result.body.username).to.eql(USERNAME);
done();
});
});
});
});
describe('logout', function () {
var accessToken;
before(function (done) {
var body = {
clientId: CLIENT.id,
username: USERNAME,
password: PASSWORD
};
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(200);
accessToken = result.body.accessToken;
done();
});
});
it('fails without access_token', function (done) {
request.get(SIMPLE_AUTH_ORIGIN + '/api/v1/logout')
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with unkonwn access_token', function (done) {
request.get(SIMPLE_AUTH_ORIGIN + '/api/v1/logout')
.query({ access_token: accessToken+accessToken })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(401);
done();
});
});
it('succeeds', function (done) {
request.get(SIMPLE_AUTH_ORIGIN + '/api/v1/logout')
.query({ access_token: accessToken })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(200);
request.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: accessToken })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(401);
done();
});
});
});
});
});
+25 -8
View File
@@ -14,7 +14,7 @@ root_password=secret
start_postgresql() {
postgresql_vars="POSTGRESQL_ROOT_PASSWORD=${root_password}; POSTGRESQL_ROOT_HOST=172.17.0.0/255.255.0.0"
if which boot2docker >/dev/null; then
if which boot2docker >/dev/null 2>&1; then
boot2docker ssh "sudo rm -rf /tmp/postgresql_vars.sh"
boot2docker ssh "echo \"${postgresql_vars}\" > /tmp/postgresql_vars.sh"
else
@@ -24,13 +24,15 @@ start_postgresql() {
docker rm -f postgresql 2>/dev/null 1>&2 || true
docker run -dtP --name=postgresql -v "${postgresqldatadir}:/var/lib/postgresql" -v /tmp/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh "${POSTGRESQL_IMAGE}" >/dev/null
docker run -dtP --name=postgresql -v "${postgresqldatadir}:/var/lib/postgresql" \
--read-only -v /tmp -v /run -v /var/log \
-v /tmp/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh "${POSTGRESQL_IMAGE}" >/dev/null
}
start_mysql() {
local mysql_vars="MYSQL_ROOT_PASSWORD=${root_password}; MYSQL_ROOT_HOST=172.17.0.0/255.255.0.0"
if which boot2docker >/dev/null; then
if which boot2docker >/dev/null 2>&1; then
boot2docker ssh "sudo rm -rf /tmp/mysql_vars.sh"
boot2docker ssh "echo \"${mysql_vars}\" > /tmp/mysql_vars.sh"
else
@@ -40,13 +42,15 @@ start_mysql() {
docker rm -f mysql 2>/dev/null 1>&2 || true
docker run -dP --name=mysql -v "${mysqldatadir}:/var/lib/mysql" -v /tmp/mysql_vars.sh:/etc/mysql/mysql_vars.sh "${MYSQL_IMAGE}" >/dev/null
docker run -dP --name=mysql -v "${mysqldatadir}:/var/lib/mysql" \
--read-only -v /tmp -v /run -v /var/log \
-v /tmp/mysql_vars.sh:/etc/mysql/mysql_vars.sh "${MYSQL_IMAGE}" >/dev/null
}
start_mongodb() {
local mongodb_vars="MONGODB_ROOT_PASSWORD=${root_password}"
if which boot2docker >/dev/null; then
if which boot2docker >/dev/null 2>&1; then
boot2docker ssh "sudo rm -rf /tmp/mongodb_vars.sh"
boot2docker ssh "echo \"${mongodb_vars}\" > /tmp/mongodb_vars.sh"
else
@@ -56,16 +60,29 @@ start_mongodb() {
docker rm -f mongodb 2>/dev/null 1>&2 || true
docker run -dP --name=mongodb -v "${mongodbdatadir}:/var/lib/mongodb" -v /tmp/mongodb_vars.sh:/etc/mongodb_vars.sh "${MONGODB_IMAGE}" >/dev/null
docker run -dP --name=mongodb -v "${mongodbdatadir}:/var/lib/mongodb" \
--read-only -v /tmp -v /run -v /var/log \
-v /tmp/mongodb_vars.sh:/etc/mongodb_vars.sh "${MONGODB_IMAGE}" >/dev/null
}
start_mail() {
local mongodb_vars="MONGODB_ROOT_PASSWORD=${root_password}"
docker rm -f mail 2>/dev/null 1>&2 || true
docker run -dP --name=mail -e DOMAIN_NAME="localhost" \
--read-only -v /tmp -v /run -v /var/log \
-v /tmp/maildata:/app/data "${MAIL_IMAGE}" >/dev/null
}
start_mysql
start_postgresql
start_mongodb
start_mail
echo -n "Waiting for addons to start"
for i in {1..10}; do
echo -n "."
for i in {1..20}; do
echo -n "."
sleep 1
done
echo ""
+4 -8
View File
@@ -43,11 +43,7 @@ function initializeExpressSync() {
app.set('view options', { layout: true, debug: true });
app.set('view engine', 'ejs');
if (process.env.BOX_ENV === 'test') {
app.use(express.static(path.join(__dirname, '/../webadmin')));
} else {
app.use(middleware.morgan('dev', { immediate: false }));
}
if (process.env.BOX_ENV !== 'test') app.use(middleware.morgan('Box :method :url :status :response-time ms - :res[content-length]', { immediate: false }));
var router = new express.Router();
router.del = router.delete; // amend router.del for readability further on
@@ -130,7 +126,6 @@ function initializeExpressSync() {
// oauth2 routes
router.get ('/api/v1/oauth/dialog/authorize', routes.oauth2.authorization);
router.post('/api/v1/oauth/dialog/authorize/decision', csrf, routes.oauth2.decision);
router.post('/api/v1/oauth/token', routes.oauth2.token);
router.get ('/api/v1/oauth/clients', settingsScope, routes.clients.getAllByUserId);
router.post('/api/v1/oauth/clients', routes.developer.enabled, settingsScope, routes.clients.add);
@@ -144,7 +139,7 @@ function initializeExpressSync() {
// app routes
router.get ('/api/v1/apps', appsScope, routes.apps.getApps);
router.get ('/api/v1/apps/:id', appsScope, routes.apps.getApp);
router.get ('/api/v1/apps/:id/icon', appsScope, routes.apps.getAppIcon);
router.get ('/api/v1/apps/:id/icon', routes.apps.getAppIcon);
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);
@@ -199,6 +194,7 @@ function initializeExpressSync() {
return httpServer;
}
// provides hooks for the 'installer'
function initializeInternalExpressSync() {
var app = express();
var httpServer = http.createServer(app);
@@ -209,7 +205,7 @@ function initializeInternalExpressSync() {
var json = middleware.json({ strict: true, limit: QUERY_LIMIT }), // application/json
urlencoded = middleware.urlencoded({ extended: false, limit: QUERY_LIMIT }); // application/x-www-form-urlencoded
app.use(middleware.morgan('dev', { immediate: false }));
if (process.env.BOX_ENV !== 'test') app.use(middleware.morgan('Box Internal :method :url :status :response-time ms - :res[content-length]', { immediate: false }));
var router = new express.Router();
router.del = router.delete; // amend router.del for readability further on
+1 -4
View File
@@ -42,12 +42,9 @@ var assert = require('assert'),
_ = require('underscore');
var gDefaults = (function () {
var tz = safe.fs.readFileSync('/etc/timezone', 'utf8');
tz = tz ? tz.trim() : 'America/Los_Angeles';
var result = { };
result[exports.AUTOUPDATE_PATTERN_KEY] = '00 00 1,3,5,23 * * *';
result[exports.TIME_ZONE_KEY] = tz;
result[exports.TIME_ZONE_KEY] = 'America/Los_Angeles';
result[exports.CLOUDRON_NAME_KEY] = 'Cloudron';
result[exports.DEVELOPER_MODE_KEY] = false;
+140
View File
@@ -0,0 +1,140 @@
'use strict';
exports = module.exports = {
start: start,
stop: stop
};
var assert = require('assert'),
debug = require('debug')('box:simpleauth'),
user = require('./user.js'),
tokendb = require('./tokendb.js'),
clients = require('./clients.js'),
config = require('./config.js'),
debug = require('debug')('box:proxy'),
middleware = require('./middleware'),
express = require('express'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
DatabaseError = require('./databaseerror.js'),
UserError = require('./user.js').UserError,
http = require('http');
var gHttpServer = null;
function loginLogic(clientId, username, password, callback) {
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof username, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof callback, 'function');
debug('login: client %s and user %s', clientId, username);
clients.get(clientId, function (error, clientObject) {
if (error) return callback(error);
user.verify(username, password, function (error, userObject) {
if (error) return callback(error);
var accessToken = tokendb.generateToken();
var expires = Date.now() + 24 * 60 * 60 * 1000; // 1 day
tokendb.add(accessToken, tokendb.PREFIX_USER + userObject.id, clientId, expires, clientObject.scope, function (error) {
if (error) return callback(error);
debug('login: new access token for client %s and user %s: %s', clientId, username, accessToken);
callback(null, { accessToken: accessToken, user: userObject });
});
});
});
}
function logoutLogic(accessToken, callback) {
assert.strictEqual(typeof accessToken, 'string');
assert.strictEqual(typeof callback, 'function');
debug('logout: %s', accessToken);
tokendb.del(accessToken, function (error) {
if (error) return callback(error);
callback(null);
});
}
function login(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.clientId !== 'string') return next(new HttpError(400, 'clientId is required'));
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'username is required'));
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'password is required'));
loginLogic(req.body.clientId, req.body.username, req.body.password, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new HttpError(401, 'Unknown client'));
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(401, 'Forbidden'));
if (error && error.reason === UserError.WRONG_PASSWORD) return next(new HttpError(401, 'Forbidden'));
if (error) return next(new HttpError(500, error));
var tmp = {
accessToken: result.accessToken,
user: {
id: result.user.id,
username: result.user.username,
email: result.user.email,
admin: !!result.user.admin
}
};
next(new HttpSuccess(200, tmp));
});
}
function logout(req, res, next) {
assert.strictEqual(typeof req.query, 'object');
if (typeof req.query.access_token !== 'string') return next(new HttpError(400, 'access_token in query required'));
logoutLogic(req.query.access_token, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new HttpError(401, 'Forbidden'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, {}));
});
}
function initializeExpressSync() {
var app = express();
var httpServer = http.createServer(app);
httpServer.on('error', console.error);
var json = middleware.json({ strict: true, limit: '100kb' });
var router = new express.Router();
// basic auth
router.post('/api/v1/login', login);
router.get ('/api/v1/logout', logout);
if (process.env.BOX_ENV !== 'test') app.use(middleware.morgan('SimpleAuth :method :url :status :response-time ms - :res[content-length]', { immediate: false }));
app
.use(middleware.timeout(10000))
.use(json)
.use(router)
.use(middleware.lastMile());
return httpServer;
}
function start(callback) {
assert.strictEqual(typeof callback, 'function');
gHttpServer = initializeExpressSync();
gHttpServer.listen(config.get('simpleAuthPort'), '0.0.0.0', callback);
}
function stop(callback) {
assert.strictEqual(typeof callback, 'function');
gHttpServer.close(callback);
}
-2
View File
@@ -80,8 +80,6 @@ function status(changeId, callback) {
assert.strictEqual(typeof changeId, 'string');
assert.strictEqual(typeof callback, 'function');
debug('status: ', changeId);
api().getChangeStatus(changeId, function (error, status) {
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error));
callback(null, status === 'INSYNC' ? 'done' : 'pending');
+2
View File
@@ -48,6 +48,8 @@ function uninitialize(callback) {
stopAppTask(appId);
}
locker.removeListener('unlocked', startNextTask);
callback(null);
}
+3 -2
View File
@@ -36,14 +36,15 @@ describe('Apps', function () {
containerId: null,
portBindings: { PORT: 5678 },
healthy: null,
accessRestriction: ''
accessRestriction: '',
oauthProxy: false
};
before(function (done) {
async.series([
database.initialize,
database._clear,
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction)
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.oauthProxy)
], done);
});
+6 -5
View File
@@ -29,7 +29,7 @@ var MANIFEST = {
"contactEmail": "support@cloudron.io",
"version": "0.1.0",
"manifestVersion": 1,
"dockerImage": "girish/test:0.2.0",
"dockerImage": "cloudron/test:2.0.0",
"healthCheckPath": "/",
"httpPort": 7777,
"tcpPorts": {
@@ -58,6 +58,7 @@ var APP = {
httpPort: 4567,
portBindings: null,
accessRestriction: '',
oauthProxy: false,
dnsRecordId: 'someDnsRecordId'
};
@@ -81,7 +82,7 @@ describe('apptask', function () {
config.set('version', '0.5.0');
database.initialize(function (error) {
expect(error).to.be(null);
appdb.add(APP.id, APP.appStoreId, APP.manifest, APP.location, APP.portBindings, APP.accessRestriction, done);
appdb.add(APP.id, APP.appStoreId, APP.manifest, APP.location, APP.portBindings, APP.accessRestriction, APP.oauthProxy, done);
});
});
@@ -136,21 +137,21 @@ describe('apptask', function () {
});
it('allocate OAuth credentials', function (done) {
addons._allocateOAuthCredentials(APP, function (error) {
addons._setupOauth(APP, {}, function (error) {
expect(error).to.be(null);
done();
});
});
it('remove OAuth credentials', function (done) {
addons._removeOAuthCredentials(APP, function (error) {
addons._teardownOauth(APP, {}, function (error) {
expect(error).to.be(null);
done();
});
});
it('remove OAuth credentials twice succeeds', function (done) {
addons._removeOAuthCredentials(APP, function (error) {
addons._teardownOauth(APP, {}, function (error) {
expect(!error).to.be.ok();
done();
});
+2 -2
View File
@@ -34,8 +34,8 @@ for script in "${scripts[@]}"; do
fi
done
if ! docker inspect girish/test:0.2.0 >/dev/null 2>/dev/null; then
echo "docker pull girish/test:0.2.0 for tests to run"
if ! docker inspect cloudron/test:3.0.0 >/dev/null 2>/dev/null; then
echo "docker pull cloudron/test:3.0.0 for tests to run"
exit 1
fi
+17 -5
View File
@@ -479,6 +479,7 @@ describe('database', function () {
portBindings: { port: 5678 },
health: null,
accessRestriction: '',
oauthProxy: false,
lastBackupId: null,
lastBackupConfig: null,
oldConfig: null
@@ -497,6 +498,7 @@ describe('database', function () {
portBindings: { },
health: null,
accessRestriction: 'roleAdmin',
oauthProxy: true,
lastBackupId: null,
lastBackupConfig: null,
oldConfig: null
@@ -516,7 +518,7 @@ describe('database', function () {
});
it('add succeeds', function (done) {
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, function (error) {
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.oauthProxy, function (error) {
expect(error).to.be(null);
done();
});
@@ -540,7 +542,7 @@ describe('database', function () {
});
it('add of same app fails', function (done) {
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, [ ], APP_0.accessRestriction, function (error) {
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, [ ], APP_0.accessRestriction, APP_0.oauthProxy, function (error) {
expect(error).to.be.a(DatabaseError);
expect(error.reason).to.be(DatabaseError.ALREADY_EXISTS);
done();
@@ -569,10 +571,20 @@ describe('database', function () {
APP_0.installationState = 'some-other-status';
APP_0.location = 'some-other-location';
APP_0.manifest.version = '0.2';
APP_0.accessRestriction = true;
APP_0.accessRestriction = '';
APP_0.oauthProxy = true;
APP_0.httpPort = 1337;
appdb.update(APP_0.id, { installationState: APP_0.installationState, location: APP_0.location, manifest: APP_0.manifest, accessRestriction: APP_0.accessRestriction, httpPort: APP_0.httpPort }, function (error) {
var data = {
installationState: APP_0.installationState,
location: APP_0.location,
manifest: APP_0.manifest,
accessRestriction: APP_0.accessRestriction,
oauthProxy: APP_0.oauthProxy,
httpPort: APP_0.httpPort
};
appdb.update(APP_0.id, data, function (error) {
expect(error).to.be(null);
appdb.get(APP_0.id, function (error, result) {
@@ -602,7 +614,7 @@ describe('database', function () {
});
it('add second app succeeds', function (done) {
appdb.add(APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, [ ], APP_1.accessRestriction, function (error) {
appdb.add(APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, [ ], APP_1.accessRestriction, APP_0.oauthProxy, function (error) {
expect(error).to.be(null);
done();
});
-2
View File
@@ -6,8 +6,6 @@
'use strict';
require('supererror', { splatchError: true});
var database = require('../database.js'),
expect = require('expect.js'),
EventEmitter = require('events').EventEmitter,
+32 -6
View File
@@ -14,14 +14,23 @@ var apps = require('./apps.js'),
config = require('./config.js'),
debug = require('debug')('box:updatechecker'),
mailer = require('./mailer.js'),
paths = require('./paths.js'),
safe = require('safetydance'),
semver = require('semver'),
superagent = require('superagent'),
util = require('util');
var gAppUpdateInfo = { }, // id -> update info { creationDate, manifest }
gBoxUpdateInfo = null,
gMailedUser = { };
gBoxUpdateInfo = null;
function loadState() {
var state = safe.JSON.parse(safe.fs.readFileSync(paths.UPDATE_CHECKER_FILE, 'utf8'));
return state || { };
}
function saveState(mailedUser) {
safe.fs.writeFileSync(paths.UPDATE_CHECKER_FILE, JSON.stringify(mailedUser, null, 4), 'utf8');
}
function getUpdateInfo() {
return {
@@ -72,6 +81,9 @@ function getAppUpdates(callback) {
function getBoxUpdates(callback) {
var currentVersion = config.version();
// do not crash if boxVersionsUrl is not set
if (!config.get('boxVersionsUrl')) return callback(null, null);
superagent
.get(config.get('boxVersionsUrl'))
.timeout(10 * 1000)
@@ -116,13 +128,21 @@ function getBoxUpdates(callback) {
function checkAppUpdates() {
debug('Checking App Updates');
var oldState = loadState();
var newState = { box: oldState.box }; // creaee new state so that old app ids are removed
getAppUpdates(function (error, result) {
if (error) debug('Error checking app updates: ', error);
gAppUpdateInfo = error ? {} : result;
async.eachSeries(Object.keys(gAppUpdateInfo), function iterator(id, iteratorDone) {
if (gMailedUser[id]) return iteratorDone();
newState[id] = gAppUpdateInfo[id].manifest.version;
if (oldState[id] === gAppUpdateInfo[id].manifest.version) {
debug('Skipping notification of app update %s since user was already notified', id);
return iteratorDone();
}
apps.get(id, function (error, app) {
if (error) {
@@ -131,9 +151,10 @@ function checkAppUpdates() {
}
mailer.appUpdateAvailable(app, gAppUpdateInfo[id]);
gMailedUser[id] = true;
iteratorDone();
});
}, function () {
saveState(newState);
});
});
}
@@ -141,14 +162,19 @@ function checkAppUpdates() {
function checkBoxUpdates() {
debug('Checking Box Updates');
var state = loadState();
getBoxUpdates(function (error, result) {
if (error) debug('Error checking box updates: ', error);
gBoxUpdateInfo = error ? null : result;
if (gBoxUpdateInfo && !gMailedUser['box']) {
if (gBoxUpdateInfo && state.box !== gBoxUpdateInfo.version) {
mailer.boxUpdateAvailable(gBoxUpdateInfo.version, gBoxUpdateInfo.changelog);
gMailedUser['box'] = true;
state.box = gBoxUpdateInfo.version;
saveState(state);
} else {
debug('Skipping notification of box update as user was already notified');
}
});
}
+2 -2
View File
@@ -134,7 +134,7 @@ function createUser(username, password, email, admin, invitor, callback) {
crypto.pbkdf2(password, salt, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, function (error, derivedKey) {
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
var now = (new Date()).toUTCString();
var now = (new Date()).toISOString();
var user = {
id: username,
username: username,
@@ -331,7 +331,7 @@ function setPassword(userId, newPassword, callback) {
crypto.pbkdf2(newPassword, saltBuffer, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, function (error, derivedKey) {
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
user.modifiedAt = (new Date()).toUTCString();
user.modifiedAt = (new Date()).toISOString();
user.password = new Buffer(derivedKey, 'binary').toString('hex');
user.resetToken = '';
+1 -3
View File
@@ -75,9 +75,7 @@
</div>
<div ng-show="installedApps | readyToUpdate">
<b ng-show="config.update.box.upgrade" class="text-danger">
The update is a base system upgrade.<br/>
This will cause some application downtime!<br/>
<br/>
This update upgrades the base system and will cause some application downtime.<br/>
</b>
<p>New version: <b>{{config.update.box.version}}</b></p>
<p>Recent Changes:</p>
+6 -3
View File
@@ -215,7 +215,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
Client.prototype.installApp = function (id, manifest, title, config, callback) {
var that = this;
var data = { appStoreId: id, manifest: manifest, location: config.location, portBindings: config.portBindings, accessRestriction: config.accessRestriction };
var data = { appStoreId: id, manifest: manifest, location: config.location, portBindings: config.portBindings, accessRestriction: config.accessRestriction, oauthProxy: config.oauthProxy };
$http.post(client.apiOrigin + '/api/v1/apps/install', data).success(function (data, status) {
if (status !== 202 || typeof data !== 'object') return defaultErrorHandler(callback);
@@ -249,7 +249,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
};
Client.prototype.configureApp = function (id, password, config, callback) {
var data = { appId: id, password: password, location: config.location, portBindings: config.portBindings, accessRestriction: config.accessRestriction };
var data = { appId: id, password: password, location: config.location, portBindings: config.portBindings, accessRestriction: config.accessRestriction, oauthProxy: config.oauthProxy };
$http.post(client.apiOrigin + '/api/v1/apps/' + id + '/configure', data).success(function (data, status) {
if (status !== 202) return callback(new ClientError(status, data));
callback(null);
@@ -610,12 +610,15 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
this._userInfo = {};
var callbackURL = window.location.protocol + '//' + window.location.host + '/login_callback.html';
var scope = 'root,profile,apps,roleAdmin';
var scope = 'root,profile,apps,roleUser';
// generate a state id to protect agains csrf
var state = Math.floor((1 + Math.random()) * 0x1000000000000).toString(16).substring(1);
window.localStorage.oauth2State = state;
// stash for further use in login_callback
window.localStorage.returnTo = '/' + window.location.hash;
window.location.href = this.apiOrigin + '/api/v1/oauth/dialog/authorize?response_type=token&client_id=' + this._clientId + '&redirect_uri=' + callbackURL + '&scope=' + scope + '&state=' + state;
};
-10
View File
@@ -143,16 +143,6 @@ app.filter('applicationLink', function() {
};
});
app.filter('accessRestrictionLabel', function() {
return function (input) {
if (input === '') return 'public';
if (input === 'roleUser') return 'private';
if (input === 'roleAdmin') return 'private (Admins only)';
return input;
};
});
app.filter('prettyHref', function () {
return function (input) {
if (!input) return input;
+5 -1
View File
@@ -19,7 +19,11 @@
// clear oauth2 state
delete window.localStorage.oauth2State;
window.location.href = '/';
var returnTo = window.localStorage.returnTo;
delete window.localStorage.returnTo;
if (returnTo) window.location.href = returnTo;
else window.location.href = '/';
}
</script>
+6 -18
View File
@@ -120,7 +120,6 @@ html {
.grid-item {
padding: 10px;
min-width: 200px;
overflow: hidden;
}
.grid-item:hover .grid-item-bottom {
@@ -175,6 +174,12 @@ html {
}
}
.app-update-badge {
position: absolute;
right: 0;
top: 0;
}
// ----------------------------
// Appstore view
// ----------------------------
@@ -354,23 +359,6 @@ html {
max-width: 800px;
}
.app-update-badge {
font-size: $font-size-h4;
position: absolute;
left: 2px;
top: 2px;
width: $font-size-h4 + 6px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
background-color: transparent;
}
.app-update-badge:hover {
width: inherit;
background-color: #5CB85C;
}
.text-success {
color: #5CB85C;
}
+17 -15
View File
@@ -38,10 +38,16 @@
</div>
<div class="form-group">
<label class="control-label" for="accessRestriction">Website Visibility</label>
<div class="checkbox">
<label>
<input type="checkbox" ng-model="appConfigure.oauthProxy"> Cloudron users only
</label>
</div>
<!-- <label class="control-label" for="accessRestriction">Website Visibility</label>
<select class="form-control" id="accessRestriction" ng-model="appConfigure.accessRestriction">
<option value="">Visible to all</option>
<option value="roleUser">Visible only to Cloudron users</option>
</select>
</select> -->
</div>
<a ng-show="!!appConfigure.app.manifest.configurePath" ng-href="https://{{ appConfigure.app.location }}{{ !appConfigure.app.location ? '' : (config.isCustomDomain ? '.' : '-') }}{{ config.fqdn }}/{{ appConfigure.app.manifest.configurePath }}" target="_blank">Application Specific Settings</a>
<br/>
@@ -55,10 +61,6 @@
</fieldset>
</div>
<div class="modal-footer ">
<button type="button" class="btn btn-default" style="float: left;" ng-click="startApp(appConfigure.app)" ng-show="appConfigure.app.runState === 'stopped' && !appConfigure.runStateBusy && !(appConfigure.app | installationActive)"><i class="fa fa-play"></i> Start</button>
<button type="button" class="btn btn-default" style="float: left;" ng-show="appConfigure.app.runState !== 'stopped' && appConfigure.app.runState !== 'running' || appConfigure.runStateBusy && !(appConfigure.app | installationActive)" disabled ><i class="fa fa-refresh fa-spin"></i></button>
<button type="button" class="btn btn-default" style="float: left;" ng-click="stopApp(appConfigure.app)" ng-show="appConfigure.app.runState === 'running' && !appConfigure.runStateBusy && !(appConfigure.app | installationActive)"><i class="fa fa-pause"></i> Stop</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-success" ng-click="doConfigure()" ng-disabled="appConfigureForm.$invalid || appConfigure.busy"><i class="fa fa-spinner fa-pulse" ng-show="appConfigure.busy"></i> Save</button>
</div>
@@ -209,8 +211,7 @@
<div class="text-muted" style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden">
{{ app | installationStateLabel }}
</div>
<br ng-hide="app | installationActive"/>
<div ng-show="app | installationActive">
<div ng-style="{ 'visibility': (app | installationActive) ? 'visible' : 'hidden' }">
<div class="progress progress-striped active">
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ app.progress }}%"></div>
</div>
@@ -244,26 +245,27 @@
</div>
<div class="grid-item-bottom" ng-show="user.admin">
<br/>
<br/>
<div>
<a href="" ng-click="showUninstall(app)"><i class="fa fa-remove scale"></i></a>
<a href="" ng-click="showUninstall(app)" title="Uninstall App"><i class="fa fa-remove scale"></i></a>
</div>
<div ng-show="(app | installError) === true">
<a href="" ng-click="showRestore(app)"><i class="fa fa-undo scale"></i></a>
<a href="" ng-click="showRestore(app)" title="Restore App"><i class="fa fa-undo scale"></i></a>
</div>
<div ng-show="(app | installSuccess) == true">
<a href="" ng-click="showConfigure(app)"><i class="fa fa-wrench scale"></i></a>
</div>
<!-- we check the version here because the box updater does not know when an app gets updated -->
<div ng-show="config.update.apps[app.id].manifest.version && config.update.apps[app.id].manifest.version !== app.manifest.version && (app | installSuccess)">
<a href="" ng-click="showUpdate(app)"><i class="fa fa-arrow-up text-success scale"></i></a>
<a href="" ng-click="showConfigure(app)" title="Configure App"><i class="fa fa-wrench scale"></i></a>
</div>
<br/>
</div>
<!-- we check the version here because the box updater does not know when an app gets updated -->
<div class="app-update-badge" ng-show="config.update.apps[app.id].manifest.version && config.update.apps[app.id].manifest.version !== app.manifest.version && (app | installSuccess)">
<a href="" ng-click="showUpdate(app)" title="Update Available"><i class="fa fa-asterisk fa-2x text-success scale"></i></a>
</div>
</a>
</div>
</div>
+5 -18
View File
@@ -19,7 +19,8 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
portBindings: {},
portBindingsEnabled: {},
portBindingsInfo: {},
accessRestriction: ''
accessRestriction: '',
oauthProxy: false
};
$scope.appUninstall = {
@@ -53,6 +54,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
$scope.appConfigure.password = '';
$scope.appConfigure.portBindings = {};
$scope.appConfigure.accessRestriction = '';
$scope.appConfigure.oauthProxy = false;
$scope.appConfigureForm.$setPristine();
$scope.appConfigureForm.$setUntouched();
@@ -90,6 +92,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
$scope.appConfigure.app = app;
$scope.appConfigure.location = app.location;
$scope.appConfigure.accessRestriction = app.accessRestriction;
$scope.appConfigure.oauthProxy = app.oauthProxy;
$scope.appConfigure.portBindingsInfo = app.manifest.tcpPorts || {}; // Portbinding map only for information
$scope.appConfigure.portBindings = {}; // This is the actual model holding the env:port pair
$scope.appConfigure.portBindingsEnabled = {}; // This is the actual model holding the enabled/disabled flag
@@ -122,7 +125,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
}
}
Client.configureApp($scope.appConfigure.app.id, $scope.appConfigure.password, { location: $scope.appConfigure.location || '', portBindings: finalPortBindings, accessRestriction: $scope.appConfigure.accessRestriction }, function (error) {
Client.configureApp($scope.appConfigure.app.id, $scope.appConfigure.password, { location: $scope.appConfigure.location || '', portBindings: finalPortBindings, accessRestriction: $scope.appConfigure.accessRestriction, oauthProxy: $scope.appConfigure.oauthProxy }, function (error) {
if (error) {
if (error.statusCode === 409 && (error.message.indexOf('is reserved') !== -1 || error.message.indexOf('is already in use') !== -1)) {
$scope.appConfigure.error.port = error.message;
@@ -313,22 +316,6 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
});
};
$scope.startApp = function (app) {
$scope.appConfigure.runStateBusy = true;
app.runState = 'pending_start'; // we assume we will end up there
Client.startApp(app.id, function () {
$scope.appConfigure.runStateBusy = false;
});
};
$scope.stopApp = function (app) {
$scope.appConfigure.runStateBusy = true;
app.runState = 'pending_stop'; // we assume we will end up there
Client.stopApp(app.id, function () {
$scope.appConfigure.runStateBusy = false;
});
};
$scope.cancel = function () {
window.history.back();
};
+4 -1
View File
@@ -17,6 +17,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
location: '',
portBindings: {},
accessRestriction: '',
oauthProxy: false,
mediaLinks: []
};
@@ -136,6 +137,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
$scope.appInstall.location = '';
$scope.appInstall.portBindings = {};
$scope.appInstall.accessRestriction = '';
$scope.appInstall.oauthProxy = false;
$scope.appInstall.installFormVisible = false;
$scope.appInstall.mediaLinks = [];
$('#collapseInstallForm').collapse('hide');
@@ -165,6 +167,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
$scope.appInstall.portBindings = {}; // This is the actual model holding the env:port pair
$scope.appInstall.portBindingsEnabled = {}; // This is the actual model holding the enabled/disabled flag
$scope.appInstall.accessRestriction = app.accessRestriction || '';
$scope.appInstall.oauthProxy = app.oauthProxy || false;
// set default ports
for (var env in $scope.appInstall.app.manifest.tcpPorts) {
@@ -194,7 +197,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
}
}
Client.installApp($scope.appInstall.app.id, $scope.appInstall.app.manifest, $scope.appInstall.app.title, { location: $scope.appInstall.location || '', portBindings: finalPortBindings, accessRestriction: $scope.appInstall.accessRestriction }, function (error) {
Client.installApp($scope.appInstall.app.id, $scope.appInstall.app.manifest, $scope.appInstall.app.title, { location: $scope.appInstall.location || '', portBindings: finalPortBindings, accessRestriction: $scope.appInstall.accessRestriction, oauthProxy: $scope.appInstall.oauthProxy }, function (error) {
if (error) {
if (error.statusCode === 409 && (error.message.indexOf('is reserved') !== -1 || error.message.indexOf('is already in use') !== -1)) {
$scope.appInstall.error.port = error.message;