Compare commits

..

132 Commits

Author SHA1 Message Date
Girish Ramakrishnan 7339c37b98 Fix invite link in userAdded notification 2018-06-12 20:08:59 -07:00
Girish Ramakrishnan 3176938ea0 Add 2.4.1 changes 2018-06-12 19:25:24 -07:00
Girish Ramakrishnan c3c77c5a97 Fix wording 2018-06-12 18:18:41 -07:00
Girish Ramakrishnan 32e6b9024c Add email query param to reset code path
This reduces any attack surface
2018-06-12 17:56:41 -07:00
Girish Ramakrishnan 5a6ea33694 Display apptask logs 2018-06-12 14:55:58 -07:00
Girish Ramakrishnan 60bff95d9f Add journalctl fallback for box logs 2018-06-12 14:39:33 -07:00
Girish Ramakrishnan 0cc2838b8b lint 2018-06-12 13:49:52 -07:00
Girish Ramakrishnan 0fc4f4bbff Explicitly pass port and logdir 2018-06-11 22:53:12 -07:00
Girish Ramakrishnan 0b82146b3e Install cloudron-syslog service file from box repo 2018-06-11 22:42:49 -07:00
Girish Ramakrishnan 4369b3046e Make options non-optional 2018-06-11 15:14:59 -07:00
Girish Ramakrishnan ac75b60f47 Fix password validation text in setup and reset forms 2018-06-11 14:01:40 -07:00
Girish Ramakrishnan d752ef5fad Move password generation logic to model code 2018-06-11 13:06:08 -07:00
Girish Ramakrishnan c099d5d3fa Make password strength be 8 chars
Fixes #434
2018-06-11 12:55:24 -07:00
Girish Ramakrishnan 6534297a5d Remove hat module
It's not been updated for 6 years!
2018-06-11 12:38:29 -07:00
Girish Ramakrishnan 2aa6350c94 Change the function names
We had these because user and mailbox code used to be mixed
2018-06-11 11:43:41 -07:00
Girish Ramakrishnan 8b4a399b8f More changes 2018-06-11 11:43:41 -07:00
Johannes Zellner 177243b7f2 Support new platform/addon log style 2018-06-11 20:09:45 +02:00
Girish Ramakrishnan c2ca827458 Add 2.4.0 changes 2018-06-11 10:13:48 -07:00
Girish Ramakrishnan 90d7dc893c Fix test 2018-06-11 10:01:54 -07:00
Girish Ramakrishnan eeaaa95ca3 Put email on free plan 2018-06-09 18:26:00 -07:00
Girish Ramakrishnan 04be582573 make the mailbox name follow the apps new location, if the user did not set it explicitly 2018-06-09 11:05:54 -07:00
Girish Ramakrishnan 0953787559 Fix docker exec terminal resize issue
Fixes #549
2018-06-08 11:44:24 -07:00
Girish Ramakrishnan 3bd8a58ea5 Update docker to 18.03.1 2018-06-08 09:13:46 -07:00
Girish Ramakrishnan 275181824f Not sure why package lock keeps changing 2018-06-07 18:45:30 -07:00
Girish Ramakrishnan f814ffb14f Update node version 2018-06-07 18:43:52 -07:00
Girish Ramakrishnan 95ae948fce more package lock changes 2018-06-07 18:42:47 -07:00
Girish Ramakrishnan 9debf1f6c6 Update some packages 2018-06-07 18:36:36 -07:00
Girish Ramakrishnan 0e583b5afe Update node to 8.11.2 2018-06-07 17:06:47 -07:00
Girish Ramakrishnan fa47031a63 cloudron-activate: Set externalDisk to true 2018-06-07 11:38:07 -07:00
Girish Ramakrishnan 7fd1bb8597 backup: Add externalDisk option to fs backend
This merely confirms from the user understands that backups have to
stored on an external disk.
2018-06-07 11:14:13 -07:00
Johannes Zellner 8c5b550caa Explicitly use cloudron-syslog 1.0.0 instead of moving master 2018-06-07 16:38:44 +02:00
Johannes Zellner 3d57c32853 Explicitly send empty object for successful post to avoid angular warnings 2018-06-07 16:10:47 +02:00
Johannes Zellner 898d928dd6 logrotate files under platformdata must be owned by root 2018-06-06 18:45:54 +02:00
Johannes Zellner c578a048dd Ensure app logrotate file is owned by root 2018-06-06 18:39:52 +02:00
Johannes Zellner 2a475c1199 Add logrotate for app and addon logs 2018-06-06 17:36:48 +02:00
Johannes Zellner 57e195883c Use plain syslog tags to be compatible with the format 2018-06-06 14:09:50 +02:00
Johannes Zellner f2178d9b81 Setup addons to log to cloudron-syslog 2018-06-06 14:09:50 +02:00
Johannes Zellner df1ac43f40 Use subshells correctly 2018-06-06 14:09:50 +02:00
Johannes Zellner 39059c627b Add --unsafe-perm for cloudron-syslog installation 2018-06-06 14:09:50 +02:00
Johannes Zellner d942c77ceb Bump infra version to reconfigure the container 2018-06-06 14:09:50 +02:00
Johannes Zellner c39240c518 Install cloudron-syslog 2018-06-06 14:09:50 +02:00
Johannes Zellner fd0e2782d8 Deliver the correct utc timestamp instead of the ISO string 2018-06-06 14:09:50 +02:00
Johannes Zellner 36aaa0406e Fix comment about firewall rule setup 2018-06-06 14:09:50 +02:00
Johannes Zellner 17ecb366af Bring back json log format for now 2018-06-06 14:09:50 +02:00
Johannes Zellner 1a83281e16 use port 2514 for syslog 2018-06-06 14:09:50 +02:00
Johannes Zellner ec41e0eef5 Use tail instead of journalctl to deliver logs 2018-06-06 14:09:50 +02:00
Johannes Zellner d4097ed4e0 Move logs into platformdata/logs 2018-06-06 14:09:50 +02:00
Johannes Zellner 8fa99fae1a Put all apptask logs of an app in the same log file 2018-06-06 14:09:50 +02:00
Johannes Zellner e9400e5dce support test usecase to not put logs in /var/log 2018-06-06 14:09:50 +02:00
Johannes Zellner 372a17dc37 Cleanup logs on app uninstall 2018-06-06 14:09:50 +02:00
Johannes Zellner 5ca60b2d3c Since we use log files now, lets keep the apptask timestamp for debug() 2018-06-06 14:09:50 +02:00
Johannes Zellner 1dc649b7a2 Put apptask logs alongside the app logs 2018-06-06 14:09:50 +02:00
Johannes Zellner 74437db740 Use syslog logging backend for apps and addons 2018-06-06 14:09:50 +02:00
Girish Ramakrishnan 70128458b2 Fix crash when renewAll is called when cloudron is not setup yet 2018-06-05 21:27:32 -07:00
Girish Ramakrishnan 900225957e typo: code should return SetupError 2018-06-05 21:19:47 -07:00
Girish Ramakrishnan fd8f5e3c71 Return error for trailing dot instead 2018-06-05 21:09:07 -07:00
Girish Ramakrishnan 7382ea2b04 Handle my subdomain already existing 2018-06-05 20:53:28 -07:00
Girish Ramakrishnan 09163b8a2b strip any trailing dot in the domain and zoneName 2018-06-05 20:33:14 -07:00
Girish Ramakrishnan 953398c427 lint 2018-06-05 20:02:47 -07:00
Girish Ramakrishnan 9f7406c235 cloudron-activate: Add option to setup backup dir 2018-06-05 19:40:46 -07:00
Girish Ramakrishnan 2e427aa60e Add 2.3.2 changes 2018-06-05 09:51:56 -07:00
Girish Ramakrishnan ab80cc9ea1 Add username to the TOTP secret name
This works around issue in FreeOTP app which crashed when
the same name is used.

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

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

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

Fixes #509
2018-05-05 09:56:21 -07:00
Girish Ramakrishnan da5fd71aaa Bump mail container for CRAM-MD5 login fix 2018-05-04 21:57:26 -07:00
74 changed files with 7992 additions and 2332 deletions
+53
View File
@@ -1264,3 +1264,56 @@
* Enhance user creation API to take a password
* Relax restriction on mailbox names now that it is decoupled from user management
[2.2.1]
* Add 2FA support for the admin dashboard
* Add Gandi & GoDaddy DNS providers
* Fix zone detection logic on Route53 accounts with more than 100 zones
* Warn using when disabling email
* Cleanup scope management in REST API
* Enhance user creation API to take a password
* Relax restriction on mailbox names now that it is decoupled from user management
* Fix issue where mail container incorrectly advertised CRAM-MD5 support
[2.3.0]
* Add Name.com DNS provider
* Fix issue where account setup page was crashing
* Add advanced DNS configuration UI
* Preserve addon/database configuration across app updates and restores
* ManageSieve port now offers STARTTLS
[2.3.1]
* Add Name.com DNS provider
* Fix issue where account setup page was crashing
* Add advanced DNS configuration UI
* Preserve addon/database configuration across app updates and restores
* ManageSieve port now offers STARTTLS
* Allow mailbox name to be set for apps
* Rework the Email server UI
* Add the ability to manually trigger a backup of an application
* Enable/disable mail from validation within UI
* Allow setting app visibility for non-SSO apps
* Add Clone UI
[2.3.2]
* Fix issue where multi-db apps were not provisioned correctly
* Improve setup, restore views to have field labels
[2.4.0]
* Use custom logging backend to have more control over log rotation
* Make user explicitly confirm that fs backup dir is on external storage
* Update node to 8.11.2
* Update docker to 18.03.1
* Fix docker exec terminal resize issue
* Make the mailbox name follow the apps new location, if the user did not set it explicitly
* Add backups view
[2.4.1]
* Use custom logging backend to have more control over log rotation
* Mail logs and box logs UI
* Make user explicitly confirm that fs backup dir is on external storage
* Update node to 8.11.2
* Update docker to 18.03.1
* Fix docker exec terminal resize issue
* Make the mailbox name follow the apps new location, if the user did not set it explicitly
* Add backups view
+1 -1
View File
@@ -630,7 +630,7 @@ state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
box
Copyright (C) 2016,2017 Cloudron UG
Copyright (C) 2016,2017,2018 Cloudron UG
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
+1 -1
View File
@@ -61,7 +61,7 @@ echo "==> Installing Docker"
mkdir -p /etc/systemd/system/docker.service.d
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=overlay2" > /etc/systemd/system/docker.service.d/cloudron.conf
curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_17.09.0~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_18.03.1~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
# apt install with install deps (as opposed to dpkg -i)
apt install -y /tmp/docker.deb
rm /tmp/docker.deb
+6 -2
View File
@@ -4,10 +4,14 @@ exports.up = function(db, callback) {
db.runSql('UPDATE clients SET scope=? WHERE id=? OR id=? OR id=?', ['*', 'cid-webadmin', 'cid-sdk', 'cid-cli'], function (error) {
if (error) console.error(error);
db.runSql('UPDATE tokens SET scope=? WHERE scope LIKE ?', ['*', '%*%'], function (error) {
db.runSql('UPDATE tokens SET scope=? WHERE scope LIKE ?', ['*', '%*%'], function (error) { // remove the roleSdk
if (error) console.error(error);
callback(error);
db.runSql('UPDATE tokens SET expires=? WHERE clientId=?', [ 1525636734905, 'cid-webadmin' ], function (error) { // force webadmin to get a new token
if (error) console.error(error);
callback(error);
});
});
});
};
+6065 -1748
View File
File diff suppressed because it is too large Load Diff
+28 -30
View File
@@ -14,12 +14,12 @@
"node": ">=4.0.0 <=4.1.1"
},
"dependencies": {
"@google-cloud/dns": "^0.7.1",
"@google-cloud/storage": "^1.6.0",
"@google-cloud/dns": "^0.7.2",
"@google-cloud/storage": "^1.7.0",
"@sindresorhus/df": "^2.1.0",
"async": "^2.6.0",
"aws-sdk": "^2.201.0",
"body-parser": "^1.18.2",
"async": "^2.6.1",
"aws-sdk": "^2.253.1",
"body-parser": "^1.18.3",
"cloudron-manifestformat": "^2.11.0",
"connect-ensure-login": "^0.1.1",
"connect-lastmile": "^1.0.2",
@@ -28,24 +28,23 @@
"cookie-session": "^1.3.2",
"cron": "^1.3.0",
"csurf": "^1.6.6",
"db-migrate": "^0.10.5",
"db-migrate": "^0.11.1",
"db-migrate-mysql": "^1.1.10",
"debug": "^3.1.0",
"dockerode": "^2.5.4",
"ejs": "^2.5.7",
"ejs-cli": "^2.0.0",
"express": "^4.16.2",
"dockerode": "^2.5.5",
"ejs": "^2.6.1",
"ejs-cli": "^2.0.1",
"express": "^4.16.3",
"express-session": "^1.15.6",
"hat": "0.0.3",
"json": "^9.0.3",
"ldapjs": "^1.0.2",
"lodash.chunk": "^4.2.0",
"mime": "^2.2.0",
"moment-timezone": "^0.5.14",
"mime": "^2.3.1",
"moment-timezone": "^0.5.17",
"morgan": "^1.9.0",
"multiparty": "^4.1.2",
"multiparty": "^4.1.4",
"mysql": "^2.15.0",
"nodemailer": "^4.6.0",
"nodemailer": "^4.6.5",
"nodemailer-smtp-transport": "^2.7.4",
"oauth2orize": "^1.11.0",
"once": "^1.3.2",
@@ -55,40 +54,39 @@
"passport-http-bearer": "^1.0.1",
"passport-local": "^1.0.0",
"passport-oauth2-client-password": "^0.1.2",
"password-generator": "^2.2.0",
"progress-stream": "^2.0.0",
"proxy-middleware": "^0.15.0",
"qrcode": "^1.2.0",
"recursive-readdir": "^2.2.1",
"request": "^2.83.0",
"s3-block-read-stream": "^0.2.0",
"recursive-readdir": "^2.2.2",
"request": "^2.87.0",
"rimraf": "^2.6.2",
"s3-block-read-stream": "^0.5.0",
"safetydance": "^0.7.1",
"semver": "^5.5.0",
"showdown": "^1.8.2",
"showdown": "^1.8.6",
"speakeasy": "^2.0.0",
"split": "^1.0.0",
"superagent": "^3.8.1",
"supererror": "^0.7.1",
"tar-fs": "^1.16.0",
"tar-stream": "^1.5.5",
"superagent": "^3.8.3",
"supererror": "^0.7.2",
"tar-fs": "^1.16.2",
"tar-stream": "^1.6.1",
"tldjs": "^2.3.1",
"underscore": "^1.7.0",
"underscore": "^1.9.1",
"uuid": "^3.2.1",
"valid-url": "^1.0.9",
"validator": "^9.4.1",
"ws": "^3.3.3"
"validator": "^10.3.0",
"ws": "^5.2.0"
},
"devDependencies": {
"expect.js": "*",
"hock": "^1.3.2",
"istanbul": "*",
"js2xmlparser": "^3.0.0",
"mocha": "^5.0.1",
"mocha": "^5.2.0",
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
"nock": "^9.0.14",
"node-sass": "^4.6.1",
"readdirp": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz",
"rimraf": "^2.6.2"
"readdirp": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz"
},
"scripts": {
"migrate_local": "DATABASE_URL=mysql://root:@localhost/box node_modules/.bin/db-migrate up",
+122
View File
@@ -0,0 +1,122 @@
#!/bin/bash
set -eu -o pipefail
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
function get_status() {
key="$1"
if status=$($curl -q -f "http://localhost:3000/api/v1/cloudron/status" 2>/dev/null); then
currentValue=$(echo "${status}" | python3 -c 'import sys, json; print(json.dumps(json.load(sys.stdin)[sys.argv[1]]))' "${key}")
echo "${currentValue}"
return 0
fi
return 1
}
function wait_for_status() {
key="$1"
expectedValue="$2"
echo "wait_for_status: $key to be $expectedValue"
while true; do
if currentValue=$(get_status "${key}"); then
echo "wait_for_status: $key is current: $currentValue expecting: $expectedValue"
if [[ "${currentValue}" == $expectedValue ]]; then
break
fi
fi
sleep 3
done
}
domain=""
domainProvider=""
domainConfigJson="{}"
domainTlsProvider="letsencrypt-prod"
adminUsername="superadmin"
adminPassword="Secret123#"
adminEmail="admin@server.local"
appstoreUserId=""
appstoreToken=""
backupDir="/var/backups"
args=$(getopt -o "" -l "domain:,domain-provider:,domain-tls-provider:,admin-username:,admin-password:,admin-email:,appstore-user:,appstore-token:,backup-dir:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--domain) domain="$2"; shift 2;;
--domain-provider) domainProvider="$2"; shift 2;;
--domain-tls-provider) domainTlsProvider="$2"; shift 2;;
--admin-username) adminUsername="$2"; shift 2;;
--admin-password) adminPassword="$2"; shift 2;;
--admin-email) adminEmail="$2"; shift 2;;
--appstore-user) appstoreUser="$2"; shift 2;;
--appstore-token) appstoreToken="$2"; shift 2;;
--backup-dir) backupDir="$2"; shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
echo "=> Waiting for cloudron to be ready"
wait_for_status "version" '*'
if [[ $(get_status "webadminStatus") != *'"tls": true'* ]]; then
echo "=> Domain setup"
dnsSetupData=$(printf '{ "domain": "%s", "adminFqdn": "%s", "provider": "%s", "config": %s, "tlsConfig": { "provider": "%s" } }' "${domain}" "my.${domain}" "${domainProvider}" "$domainConfigJson" "${domainTlsProvider}")
if ! $curl -X POST -H "Content-Type: application/json" -d "${dnsSetupData}" http://localhost:3000/api/v1/cloudron/dns_setup; then
echo "DNS Setup Failed"
exit 1
fi
wait_for_status "webadminStatus" '*"tls": true*'
else
echo "=> Skipping Domain setup"
fi
activationData=$(printf '{"username": "%s", "password":"%s", "email": "%s" }' "${adminUsername}" "${adminPassword}" "${adminEmail}")
if [[ $(get_status "activated") == "false" ]]; then
echo "=> Activating"
if ! activationResult=$($curl -X POST -H "Content-Type: application/json" -d "${activationData}" http://localhost:3000/api/v1/cloudron/activate); then
echo "Failed to activate with ${activationData}: ${activationResult}"
exit 1
fi
wait_for_status "activated" "true"
else
echo "=> Skipping Activation"
fi
echo "=> Getting token"
if ! activationResult=$($curl -X POST -H "Content-Type: application/json" -d "${activationData}" http://localhost:3000/api/v1/developer/login); then
echo "Failed to login with ${activationData}: ${activationResult}"
exit 1
fi
accessToken=$(echo "${activationResult}" | python3 -c 'import sys, json; print(json.load(sys.stdin)[sys.argv[1]])' "accessToken")
echo "=> Setting up App Store account with accessToken ${accessToken}"
appstoreData=$(printf '{"userId":"%s", "token":"%s" }' "${appstoreUser}" "${appstoreToken}")
if ! appstoreResult=$($curl -X POST -H "Content-Type: application/json" -d "${appstoreData}" "http://localhost:3000/api/v1/settings/appstore_config?access_token=${accessToken}"); then
echo "Failed to setup Appstore account with ${appstoreData}: ${appstoreResult}"
exit 1
fi
echo "=> Setting up Backup Directory with accessToken ${accessToken}"
backupData=$(printf '{"provider":"filesystem", "key":"", "backupFolder":"%s", "retentionSecs": 864000, "format": "tgz", "externalDisk": true}' "${backupDir}")
chown -R yellowtent:yellowtent "${backupDir}"
if ! backupResult=$($curl -X POST -H "Content-Type: application/json" -d "${backupData}" "http://localhost:3000/api/v1/settings/backup_config?access_token=${accessToken}"); then
echo "Failed to setup backup configuration with ${backupDir}: ${backupResult}"
exit 1
fi
echo "=> Done!"
+2 -2
View File
@@ -34,8 +34,8 @@ if ! $(cd "${SOURCE_DIR}/../dashboard" && git diff --exit-code >/dev/null); then
exit 1
fi
if [[ "$(node --version)" != "v8.9.3" ]]; then
echo "This script requires node 8.9.3"
if [[ "$(node --version)" != "v8.11.2" ]]; then
echo "This script requires node 8.11.2"
exit 1
fi
+18 -8
View File
@@ -35,11 +35,11 @@ while true; do
done
echo "==> installer: updating docker"
if [[ $(docker version --format {{.Client.Version}}) != "18.03.0-ce" ]]; then
$curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_18.03.0~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
if [[ $(docker version --format {{.Client.Version}}) != "18.03.1-ce" ]]; then
$curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_18.03.1~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
# https://download.docker.com/linux/ubuntu/dists/xenial/stable/binary-amd64/Packages
if [[ $(sha256sum /tmp/docker.deb | cut -d' ' -f1) != "1f7315b5723b849fe542fe973b0edb4164a0200e926d386ac14363a968f9e4fc" ]]; then
if [[ $(sha256sum /tmp/docker.deb | cut -d' ' -f1) != "54f4c9268492a4fd2ec2e6bcc95553855b025f35dcc8b9f60ac34e0aa307279b" ]]; then
echo "==> installer: docker binary download is corrupt"
exit 5
fi
@@ -69,11 +69,11 @@ if [[ $(docker version --format {{.Client.Version}}) != "18.03.0-ce" ]]; then
fi
echo "==> installer: updating node"
if [[ "$(node --version)" != "v8.9.3" ]]; then
mkdir -p /usr/local/node-8.9.3
$curl -sL https://nodejs.org/dist/v8.9.3/node-v8.9.3-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-8.9.3
ln -sf /usr/local/node-8.9.3/bin/node /usr/bin/node
ln -sf /usr/local/node-8.9.3/bin/npm /usr/bin/npm
if [[ "$(node --version)" != "v8.11.2" ]]; then
mkdir -p /usr/local/node-8.11.2
$curl -sL https://nodejs.org/dist/v8.11.2/node-v8.11.2-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-8.11.2
ln -sf /usr/local/node-8.11.2/bin/node /usr/bin/node
ln -sf /usr/local/node-8.11.2/bin/npm /usr/bin/npm
rm -rf /usr/local/node-6.11.5
fi
@@ -93,6 +93,16 @@ if [[ ${try} -eq 10 ]]; then
exit 4
fi
echo "==> installer: update cloudron-syslog"
CLOUDRON_SYSLOG_DIR=/usr/local/cloudron-syslog
if [[ "$($CLOUDRON_SYSLOG_DIR/bin/cloudron-syslog --version)" != "1.0.1" ]]; then
rm -rf "${CLOUDRON_SYSLOG_DIR}"
mkdir -p "${CLOUDRON_SYSLOG_DIR}"
if npm install --unsafe-perm -g --prefix "${CLOUDRON_SYSLOG_DIR}" cloudron-syslog@1.0.1; then break; fi
echo "===> installer: Failed to install cloudron-syslog, trying again"
sleep 5
fi
if ! id "${USER}" 2>/dev/null; then
useradd "${USER}" -m
fi
+10 -1
View File
@@ -120,6 +120,7 @@ echo "==> Adding systemd services"
cp -r "${script_dir}/start/systemd/." /etc/systemd/system/
systemctl daemon-reload
systemctl enable unbound
systemctl enable cloudron-syslog
systemctl enable cloudron.target
systemctl enable cloudron-firewall
@@ -132,6 +133,9 @@ systemctl enable --now cron
# ensure unbound runs
systemctl restart unbound
# ensure cloudron-syslog runs
systemctl restart cloudron-syslog
echo "==> Configuring sudoers"
rm -f /etc/sudoers.d/${USER}
cp "${script_dir}/start/sudoers" /etc/sudoers.d/${USER}
@@ -146,6 +150,8 @@ echo "==> Configuring logrotate"
if ! grep -q "^include ${PLATFORM_DATA_DIR}/logrotate.d" /etc/logrotate.conf; then
echo -e "\ninclude ${PLATFORM_DATA_DIR}/logrotate.d\n" >> /etc/logrotate.conf
fi
cp "${script_dir}/start/app-logrotate" "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate"
chown root:root "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate"
echo "==> Adding motd message for admins"
cp "${script_dir}/start/cloudron-motd" /etc/update-motd.d/92-cloudron
@@ -231,10 +237,13 @@ fi
echo "==> Changing ownership"
chown "${USER}:${USER}" -R "${CONFIG_DIR}"
chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/nginx" "${PLATFORM_DATA_DIR}/collectd" "${PLATFORM_DATA_DIR}/logrotate.d" "${PLATFORM_DATA_DIR}/addons" "${PLATFORM_DATA_DIR}/acme" "${PLATFORM_DATA_DIR}/backup"
chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/nginx" "${PLATFORM_DATA_DIR}/collectd" "${PLATFORM_DATA_DIR}/addons" "${PLATFORM_DATA_DIR}/acme" "${PLATFORM_DATA_DIR}/backup"
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}/INFRA_VERSION" 2>/dev/null || true
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}"
# logrotate files have to be owned by root, this is here to fixup existing installations where we were resetting the owner to yellowtent
chown root:root -R "${PLATFORM_DATA_DIR}/logrotate.d"
# do not chown the boxdata/mail directory; dovecot gets upset
chown "${USER}:${USER}" "${BOX_DATA_DIR}"
find "${BOX_DATA_DIR}" -mindepth 1 -maxdepth 1 -not -path "${BOX_DATA_DIR}/mail" -exec chown -R "${USER}:${USER}" {} \;
+10
View File
@@ -0,0 +1,10 @@
# logrotate config for app logs
/home/yellowtent/platformdata/logs/*/*.log {
# only keep one rotated file, we currently do not send that over the api
rotate 1
size 10M
# we never compress so we can simply tail the files
nocompress
copytruncate
}
@@ -0,0 +1,14 @@
[Unit]
Description=Cloudron Syslog
After=network.target
[Service]
ExecStart=/usr/local/cloudron-syslog/bin/cloudron-syslog --port 2514 --logdir /home/yellowtent/platformdata/logs
WorkingDirectory=/usr/local/cloudron-syslog
Environment="NODE_ENV=production"
Restart=always
User=yellowtent
Group=yellowtent
[Install]
WantedBy=multi-user.target
+192 -106
View File
@@ -22,12 +22,13 @@ var accesscontrol = require('./accesscontrol.js'),
clients = require('./clients.js'),
config = require('./config.js'),
ClientsError = clients.ClientsError,
crypto = require('crypto'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:addons'),
docker = require('./docker.js'),
dockerConnection = docker.connection,
fs = require('fs'),
generatePassword = require('password-generator'),
hat = require('hat'),
hat = require('./hat.js'),
infra = require('./infra_version.js'),
mail = require('./mail.js'),
mailboxdb = require('./mailboxdb.js'),
@@ -364,23 +365,28 @@ function setupSendMail(app, options, callback) {
debugApp(app, 'Setting up SendMail');
mailboxdb.getByOwnerId(app.id, function (error, results) {
if (error) return callback(error);
appdb.getAddonConfigByName(app.id, 'sendmail', 'MAIL_SMTP_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
var password = generatePassword(128, false /* memorable */, /[\w\d_]/);
var password = error ? hat(4 * 128) : existingPassword;
var env = [
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
{ name: 'MAIL_SMTP_PORT', value: '2525' },
{ name: 'MAIL_SMTPS_PORT', value: '2465' },
{ name: 'MAIL_SMTP_USERNAME', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_SMTP_PASSWORD', value: password },
{ name: 'MAIL_FROM', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_DOMAIN', value: app.domain }
];
debugApp(app, 'Setting sendmail addon config to %j', env);
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
mailboxdb.getByOwnerId(app.id, function (error, results) {
if (error) return callback(error);
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
var env = [
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
{ name: 'MAIL_SMTP_PORT', value: '2525' },
{ name: 'MAIL_SMTPS_PORT', value: '2465' },
{ name: 'MAIL_SMTP_USERNAME', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_SMTP_PASSWORD', value: password },
{ name: 'MAIL_FROM', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_DOMAIN', value: app.domain }
];
debugApp(app, 'Setting sendmail addon config to %j', env);
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
});
});
}
@@ -401,23 +407,28 @@ function setupRecvMail(app, options, callback) {
debugApp(app, 'Setting up recvmail');
mailboxdb.getByOwnerId(app.id, function (error, results) {
if (error) return callback(error);
appdb.getAddonConfigByName(app.id, 'recvmail', 'MAIL_IMAP_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
var password = generatePassword(128, false /* memorable */, /[\w\d_]/);
var password = error ? hat(4 * 128) : existingPassword;
var env = [
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
{ name: 'MAIL_IMAP_PORT', value: '9993' },
{ name: 'MAIL_IMAP_USERNAME', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_IMAP_PASSWORD', value: password },
{ name: 'MAIL_TO', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_DOMAIN', value: app.domain }
];
mailboxdb.getByOwnerId(app.id, function (error, results) {
if (error) return callback(error);
debugApp(app, 'Setting sendmail addon config to %j', env);
appdb.setAddonConfig(app.id, 'recvmail', env, callback);
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
var env = [
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
{ name: 'MAIL_IMAP_PORT', value: '9993' },
{ name: 'MAIL_IMAP_USERNAME', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_IMAP_PASSWORD', value: password },
{ name: 'MAIL_TO', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_DOMAIN', value: app.domain }
];
debugApp(app, 'Setting sendmail addon config to %j', env);
appdb.setAddonConfig(app.id, 'recvmail', env, callback);
});
});
}
@@ -431,6 +442,14 @@ function teardownRecvMail(app, options, callback) {
appdb.unsetAddonConfig(app.id, 'recvmail', callback);
}
function mysqlDatabaseName(appId) {
assert.strictEqual(typeof appId, 'string');
var md5sum = crypto.createHash('md5'); // get rid of "-"
md5sum.update(appId);
return md5sum.digest('hex').substring(0, 16); // max length of mysql usernames is 16
}
function setupMySql(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
@@ -438,16 +457,36 @@ function setupMySql(app, options, callback) {
debugApp(app, 'Setting up mysql');
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'add-prefix' : 'add', app.id ];
appdb.getAddonConfigByName(app.id, 'mysql', 'MYSQL_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
docker.execContainer('mysql', cmd, { bufferStdout: true }, function (error, stdout) {
if (error) return callback(error);
const dbname = mysqlDatabaseName(app.id);
const password = error ? hat(4 * 48) : existingPassword; // see box#362 for password length
var result = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
var env = result.map(function (r) { var idx = r.indexOf('='); return { name: r.substr(0, idx), value: r.substr(idx + 1) }; });
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'add-prefix' : 'add', dbname, password ];
debugApp(app, 'Setting mysql addon config to %j', env);
appdb.setAddonConfig(app.id, 'mysql', env, callback);
docker.execContainer('mysql', cmd, { bufferStdout: true }, function (error) {
if (error) return callback(error);
var env = [
{ name: 'MYSQL_USERNAME', value: dbname },
{ name: 'MYSQL_PASSWORD', value: password },
{ name: 'MYSQL_HOST', value: 'mysql' },
{ name: 'MYSQL_PORT', value: '3306' }
];
if (options.multipleDatabases) {
env = env.concat({ name: 'MYSQL_DATABASE_PREFIX', value: `${dbname}_` });
} else {
env = env.concat(
{ name: 'MYSQL_URL', value: `mysql://${dbname}:${password}@mysql/${dbname}` },
{ name: 'MYSQL_DATABASE', value: dbname }
);
}
debugApp(app, 'Setting mysql addon config to %j', env);
appdb.setAddonConfig(app.id, 'mysql', env, callback);
});
});
}
@@ -456,7 +495,8 @@ function teardownMySql(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'remove-prefix' : 'remove', app.id ];
const dbname = mysqlDatabaseName(app.id);
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'remove-prefix' : 'remove', dbname ];
debugApp(app, 'Tearing down mysql');
@@ -479,7 +519,8 @@ function backupMySql(app, options, callback) {
var output = fs.createWriteStream(path.join(paths.APPS_DATA_DIR, app.id, 'mysqldump'));
output.on('error', callback);
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'backup-prefix' : 'backup', app.id ];
const dbname = mysqlDatabaseName(app.id);
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'backup-prefix' : 'backup', dbname ];
docker.execContainer('mysql', cmd, { stdout: output }, callback);
}
@@ -499,7 +540,8 @@ function restoreMySql(app, options, callback) {
var input = fs.createReadStream(path.join(paths.APPS_DATA_DIR, app.id, 'mysqldump'));
input.on('error', callback);
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'restore-prefix' : 'restore', app.id ];
const dbname = mysqlDatabaseName(app.id);
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'restore-prefix' : 'restore', dbname ];
docker.execContainer('mysql', cmd, { stdin: input }, callback);
});
}
@@ -511,16 +553,29 @@ function setupPostgreSql(app, options, callback) {
debugApp(app, 'Setting up postgresql');
var cmd = [ '/addons/postgresql/service.sh', 'add', app.id ];
appdb.getAddonConfigByName(app.id, 'postgresql', 'POSTGRESQL_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
docker.execContainer('postgresql', cmd, { bufferStdout: true }, function (error, stdout) {
if (error) return callback(error);
const password = error ? hat(4 * 128) : existingPassword;
const appId = app.id.replace(/-/g, '');
var result = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
var env = result.map(function (r) { var idx = r.indexOf('='); return { name: r.substr(0, idx), value: r.substr(idx + 1) }; });
var cmd = [ '/addons/postgresql/service.sh', 'add', appId, password ];
debugApp(app, 'Setting postgresql addon config to %j', env);
appdb.setAddonConfig(app.id, 'postgresql', env, callback);
docker.execContainer('postgresql', cmd, { bufferStdout: true }, function (error) {
if (error) return callback(error);
var env = [
{ name: 'POSTGRESQL_URL', value: `postgres://user${appId}:${password}@postgresql/db${appId}` },
{ name: 'POSTGRESQL_USERNAME', value: `user${appId}` },
{ name: 'POSTGRESQL_PASSWORD', value: password },
{ name: 'POSTGRESQL_HOST', value: 'postgresql' },
{ name: 'POSTGRESQL_PORT', value: '5432' },
{ name: 'POSTGRESQL_DATABASE', value: `db${appId}` }
];
debugApp(app, 'Setting postgresql addon config to %j', env);
appdb.setAddonConfig(app.id, 'postgresql', env, callback);
});
});
}
@@ -529,7 +584,9 @@ function teardownPostgreSql(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var cmd = [ '/addons/postgresql/service.sh', 'remove', app.id ];
const appId = app.id.replace(/-/g, '');
var cmd = [ '/addons/postgresql/service.sh', 'remove', appId ];
debugApp(app, 'Tearing down postgresql');
@@ -552,7 +609,8 @@ function backupPostgreSql(app, options, callback) {
var output = fs.createWriteStream(path.join(paths.APPS_DATA_DIR, app.id, 'postgresqldump'));
output.on('error', callback);
var cmd = [ '/addons/postgresql/service.sh', 'backup', app.id ];
const appId = app.id.replace(/-/g, '');
var cmd = [ '/addons/postgresql/service.sh', 'backup', appId ];
docker.execContainer('postgresql', cmd, { stdout: output }, callback);
}
@@ -572,7 +630,8 @@ function restorePostgreSql(app, options, callback) {
var input = fs.createReadStream(path.join(paths.APPS_DATA_DIR, app.id, 'postgresqldump'));
input.on('error', callback);
var cmd = [ '/addons/postgresql/service.sh', 'restore', app.id ];
const appId = app.id.replace(/-/g, '');
var cmd = [ '/addons/postgresql/service.sh', 'restore', appId ];
docker.execContainer('postgresql', cmd, { stdin: input }, callback);
});
@@ -585,16 +644,30 @@ function setupMongoDb(app, options, callback) {
debugApp(app, 'Setting up mongodb');
var cmd = [ '/addons/mongodb/service.sh', 'add', app.id ];
appdb.getAddonConfigByName(app.id, 'mongodb', 'MONGODB_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
docker.execContainer('mongodb', cmd, { bufferStdout: true }, function (error, stdout) {
if (error) return callback(error);
const password = error ? hat(4 * 128) : existingPassword;
var result = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
var env = result.map(function (r) { var idx = r.indexOf('='); return { name: r.substr(0, idx), value: r.substr(idx + 1) }; });
const dbname = app.id;
debugApp(app, 'Setting mongodb addon config to %j', env);
appdb.setAddonConfig(app.id, 'mongodb', env, callback);
var cmd = [ '/addons/mongodb/service.sh', 'add', dbname, password ];
docker.execContainer('mongodb', cmd, { bufferStdout: true }, function (error) {
if (error) return callback(error);
var env = [
{ name: 'MONGODB_URL', value : `mongodb://${dbname}:${password}@mongodb/${dbname}` },
{ name: 'MONGODB_USERNAME', value : dbname },
{ name: 'MONGODB_PASSWORD', value: password },
{ name: 'MONGODB_HOST', value : 'mongodb' },
{ name: 'MONGODB_PORT', value : '27017' },
{ name: 'MONGODB_DATABASE', value : dbname }
];
debugApp(app, 'Setting mongodb addon config to %j', env);
appdb.setAddonConfig(app.id, 'mongodb', env, callback);
});
});
}
@@ -603,7 +676,8 @@ function teardownMongoDb(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var cmd = [ '/addons/mongodb/service.sh', 'remove', app.id ];
const dbname = app.id;
var cmd = [ '/addons/mongodb/service.sh', 'remove', dbname ];
debugApp(app, 'Tearing down mongodb');
@@ -626,7 +700,8 @@ function backupMongoDb(app, options, callback) {
var output = fs.createWriteStream(path.join(paths.APPS_DATA_DIR, app.id, 'mongodbdump'));
output.on('error', callback);
var cmd = [ '/addons/mongodb/service.sh', 'backup', app.id ];
const dbname = app.id;
var cmd = [ '/addons/mongodb/service.sh', 'backup', dbname ];
docker.execContainer('mongodb', cmd, { stdout: output }, callback);
}
@@ -646,7 +721,9 @@ function restoreMongoDb(app, options, callback) {
var input = fs.createReadStream(path.join(paths.APPS_DATA_DIR, app.id, 'mongodbdump'));
input.on('error', callback);
var cmd = [ '/addons/mongodb/service.sh', 'restore', app.id ];
const dbname = app.id;
var cmd = [ '/addons/mongodb/service.sh', 'restore', dbname ];
docker.execContainer('mongodb', cmd, { stdin: input }, callback);
});
}
@@ -657,58 +734,67 @@ function setupRedis(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var redisPassword = generatePassword(128, false /* memorable */, /[\w\d_]/); // ensure no / in password for being sed friendly (and be uri friendly)
var redisVarsFile = path.join(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
var redisDataDir = path.join(paths.APPS_DATA_DIR, app.id + '/redis');
appdb.getAddonConfigByName(app.id, 'redis', 'REDIS_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
if (!safe.fs.writeFileSync(redisVarsFile, 'REDIS_PASSWORD=' + redisPassword)) {
return callback(new Error('Error writing redis config'));
}
const redisPassword = error ? hat(4 * 48) : existingPassword; // see box#362 for password length
if (!safe.fs.mkdirSync(redisDataDir) && safe.error.code !== 'EEXIST') return callback(new Error('Error creating redis data dir:' + safe.error));
var redisVarsFile = path.join(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
var redisDataDir = path.join(paths.APPS_DATA_DIR, app.id + '/redis');
// Compute redis memory limit based on app's memory limit (this is arbitrary)
var memoryLimit = app.memoryLimit || app.manifest.memoryLimit || 0;
if (!safe.fs.writeFileSync(redisVarsFile, 'REDIS_PASSWORD=' + redisPassword)) {
return callback(new Error('Error writing redis config'));
}
if (memoryLimit === -1) { // unrestricted (debug mode)
memoryLimit = 0;
} else if (memoryLimit === 0 || memoryLimit <= (2 * 1024 * 1024 * 1024)) { // less than 2G (ram+swap)
memoryLimit = 150 * 1024 * 1024; // 150m
} else {
memoryLimit = 600 * 1024 * 1024; // 600m
}
if (!safe.fs.mkdirSync(redisDataDir) && safe.error.code !== 'EEXIST') return callback(new Error('Error creating redis data dir:' + safe.error));
const tag = infra.images.redis.tag, redisName = 'redis-' + app.id;
const label = app.fqdn;
// note that we do not add appId label because this interferes with the stop/start app logic
const cmd = `docker run --restart=always -d --name=${redisName} \
--label=location=${label} \
--net cloudron \
--net-alias ${redisName} \
-m ${memoryLimit/2} \
--memory-swap ${memoryLimit} \
--dns 172.18.0.1 \
--dns-search=. \
-v ${redisVarsFile}:/etc/redis/redis_vars.sh:ro \
-v ${redisDataDir}:/var/lib/redis:rw \
--read-only -v /tmp -v /run ${tag}`;
// Compute redis memory limit based on app's memory limit (this is arbitrary)
var memoryLimit = app.memoryLimit || app.manifest.memoryLimit || 0;
var env = [
{ name: 'REDIS_URL', value: 'redis://redisuser:' + redisPassword + '@redis-' + app.id },
{ name: 'REDIS_PASSWORD', value: redisPassword },
{ name: 'REDIS_HOST', value: redisName },
{ name: 'REDIS_PORT', value: '6379' }
];
if (memoryLimit === -1) { // unrestricted (debug mode)
memoryLimit = 0;
} else if (memoryLimit === 0 || memoryLimit <= (2 * 1024 * 1024 * 1024)) { // less than 2G (ram+swap)
memoryLimit = 150 * 1024 * 1024; // 150m
} else {
memoryLimit = 600 * 1024 * 1024; // 600m
}
async.series([
// stop so that redis can flush itself with SIGTERM
shell.execSync.bind(null, 'stopRedis', `docker stop --time=10 ${redisName} 2>/dev/null || true`),
shell.execSync.bind(null, 'stopRedis', `docker rm --volumes ${redisName} 2>/dev/null || true`),
shell.execSync.bind(null, 'startRedis', cmd),
appdb.setAddonConfig.bind(null, app.id, 'redis', env)
], function (error) {
if (error) debug('Error setting up redis: ', error);
callback(error);
const tag = infra.images.redis.tag, redisName = 'redis-' + app.id;
const label = app.fqdn;
// note that we do not add appId label because this interferes with the stop/start app logic
const cmd = `docker run --restart=always -d --name=${redisName} \
--label=location=${label} \
--net cloudron \
--net-alias ${redisName} \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag="${redisName}" \
-m ${memoryLimit/2} \
--memory-swap ${memoryLimit} \
--dns 172.18.0.1 \
--dns-search=. \
-v ${redisVarsFile}:/etc/redis/redis_vars.sh:ro \
-v ${redisDataDir}:/var/lib/redis:rw \
--read-only -v /tmp -v /run ${tag}`;
var env = [
{ name: 'REDIS_URL', value: 'redis://redisuser:' + redisPassword + '@redis-' + app.id },
{ name: 'REDIS_PASSWORD', value: redisPassword },
{ name: 'REDIS_HOST', value: redisName },
{ name: 'REDIS_PORT', value: '6379' }
];
async.series([
// stop so that redis can flush itself with SIGTERM
shell.execSync.bind(null, 'stopRedis', `docker stop --time=10 ${redisName} 2>/dev/null || true`),
shell.execSync.bind(null, 'stopRedis', `docker rm --volumes ${redisName} 2>/dev/null || true`),
shell.execSync.bind(null, 'startRedis', cmd),
appdb.setAddonConfig.bind(null, app.id, 'redis', env)
], function (error) {
if (error) debug('Error setting up redis: ', error);
callback(error);
});
});
}
+64 -35
View File
@@ -47,8 +47,7 @@ exports = module.exports = {
_validateAccessRestriction: validateAccessRestriction
};
var addons = require('./addons.js'),
appdb = require('./appdb.js'),
var appdb = require('./appdb.js'),
appstore = require('./appstore.js'),
AppstoreError = require('./appstore.js').AppstoreError,
assert = require('assert'),
@@ -66,6 +65,7 @@ var addons = require('./addons.js'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
groups = require('./groups.js'),
mail = require('./mail.js'),
mailboxdb = require('./mailboxdb.js'),
manifestFormat = require('cloudron-manifestformat'),
os = require('os'),
@@ -158,7 +158,7 @@ function validateHostname(location, domain, hostname) {
function validatePortBindings(portBindings, tcpPorts) {
assert.strictEqual(typeof portBindings, 'object');
// keep the public ports in sync with firewall rules in scripts/initializeBaseUbuntuImage.sh
// keep the public ports in sync with firewall rules in setup/start/cloudron-firewall.sh
// these ports are reserved even if we listen only on 127.0.0.1 because we setup HostIp to be 127.0.0.1
// for custom tcp ports
var RESERVED_PORTS = [
@@ -175,6 +175,7 @@ function validatePortBindings(portBindings, tcpPorts) {
2003, /* graphite (lo) */
2004, /* graphite (lo) */
2020, /* mail server */
2514, /* cloudron-syslog (lo) */
config.get('port'), /* app server (lo) */
config.get('sysadminPort'), /* sysadmin app server (lo) */
config.get('smtpPort'), /* internal smtp port (lo) */
@@ -314,7 +315,6 @@ function getAppConfig(app) {
manifest: app.manifest,
location: app.location,
domain: app.domain,
fqdn: app.fqdn,
accessRestriction: app.accessRestriction,
portBindings: app.portBindings,
memoryLimit: app.memoryLimit,
@@ -325,8 +325,9 @@ function getAppConfig(app) {
}
function removeInternalAppFields(app) {
return _.pick(app, 'id', 'appStoreId', 'installationState', 'installationProgress', 'runState', 'health',
'location', 'domain', 'fqdn',
return _.pick(app,
'id', 'appStoreId', 'installationState', 'installationProgress', 'runState', 'health',
'location', 'domain', 'fqdn', 'mailboxName',
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'xFrameOptions',
'sso', 'debugMode', 'robotsTxt', 'enableBackup', 'creationTime', 'updateTime');
}
@@ -376,7 +377,13 @@ function get(appId, callback) {
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
callback(null, app);
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!error) app.mailboxName = mailboxes[0].name;
callback(null, app);
});
});
});
}
@@ -398,7 +405,13 @@ function getByIpAddress(ip, callback) {
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
callback(null, app);
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!error) app.mailboxName = mailboxes[0].name;
callback(null, app);
});
});
});
});
@@ -417,7 +430,13 @@ function getAll(callback) {
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
iteratorDone();
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!error) app.mailboxName = mailboxes[0].name;
iteratorDone(null, app);
});
});
}, function (error) {
if (error) return callback(error);
@@ -460,6 +479,10 @@ function downloadManifest(appStoreId, manifest, callback) {
});
}
function mailboxNameForLocation(location, manifest) {
return (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
}
function install(data, auditSource, callback) {
assert(data && typeof data === 'object');
assert.strictEqual(typeof auditSource, 'object');
@@ -549,7 +572,7 @@ function install(data, auditSource, callback) {
xFrameOptions: xFrameOptions,
sso: sso,
debugMode: debugMode,
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app',
mailboxName: mailboxNameForLocation(location, manifest),
restoreConfig: backupId ? { backupId: backupId, backupFormat: backupFormat } : null,
enableBackup: enableBackup,
robotsTxt: robotsTxt
@@ -652,6 +675,11 @@ function configure(appId, data, auditSource, callback) {
if (error) return callback(error);
}
if ('mailboxName' in data) {
error = mail.validateName(data.mailboxName);
if (error) return callback(error);
}
domains.get(domain, function (error, domainObject) {
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
@@ -681,8 +709,9 @@ function configure(appId, data, auditSource, callback) {
debug('Will configure app with id:%s values:%j', appId, values);
var oldName = (app.location ? app.location : app.manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
var newName = (location ? location : app.manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
// make the mailbox name follow the apps new location, if the user did not set it explicitly
var oldName = app.mailboxName;
var newName = data.mailboxName || (app.mailboxName.endsWith('.app') ? mailboxNameForLocation(location, app.manifest) : app.mailboxName);
mailboxdb.updateName(oldName, values.oldConfig.domain, newName, domain, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new AppsError(AppsError.ALREADY_EXISTS, 'This mailbox is already taken'));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
@@ -779,12 +808,6 @@ function update(appId, data, auditSource, callback) {
});
}
function appLogFilter(app) {
var names = [ app.id ].concat(addons.getContainerNamesSync(app, app.manifest.addons));
return names.map(function (name) { return 'CONTAINER_NAME=' + name; });
}
function getLogs(appId, options, callback) {
assert.strictEqual(typeof appId, 'string');
assert(options && typeof options === 'object');
@@ -792,32 +815,34 @@ function getLogs(appId, options, callback) {
debug('Getting logs for %s', appId);
get(appId, function (error, app) {
get(appId, function (error /*, app */) {
if (error) return callback(error);
var lines = options.lines || 100,
follow = !!options.follow,
format = options.format || 'json';
format = options.format || 'json',
follow = !!options.follow;
var args = [ '--no-pager', '--lines=' + lines ];
if (follow) args.push('--follow');
if (format == 'short') args.push('--output=short', '-a'); else args.push('--output=json');
args = args.concat(appLogFilter(app));
assert.strictEqual(typeof lines, 'number');
assert.strictEqual(typeof format, 'string');
var cp = spawn('/bin/journalctl', args);
var args = [ '--lines=' + lines ];
if (follow) args.push('--follow', '--retry'); // same as -F. to make it work if file doesn't exist
args.push(path.join(paths.LOG_DIR, appId, 'app.log'));
args.push(path.join(paths.LOG_DIR, appId, 'apptask.log'));
var cp = spawn('/usr/bin/tail', args);
var transformStream = split(function mapper(line) {
if (format !== 'json') return line + '\n';
var obj = safe.JSON.parse(line);
if (!obj) return undefined;
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
var timestamp = (new Date(data[0])).getTime();
if (isNaN(timestamp)) timestamp = 0;
var source = obj.CONTAINER_NAME.slice(app.id.length + 1);
return JSON.stringify({
realtimeTimestamp: obj.__REALTIME_TIMESTAMP,
monotonicTimestamp: obj.__MONOTONIC_TIMESTAMP,
message: obj.MESSAGE,
source: source || 'main'
realtimeTimestamp: timestamp * 1000,
message: line.slice(data[0].length+1),
source: appId
}) + '\n';
});
@@ -1095,7 +1120,11 @@ function exec(appId, options, callback) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (options.rows && options.columns) {
exec.resize({ h: options.rows, w: options.columns }, function (error) { if (error) debug('Error resizing console', error); });
// there is a race where resizing too early results in a 404 "no such exec"
// https://git.cloudron.io/cloudron/box/issues/549
setTimeout(function () {
exec.resize({ h: options.rows, w: options.columns }, function (error) { if (error) debug('Error resizing console', error); });
}, 2000);
}
return callback(null, stream);
@@ -1203,7 +1232,7 @@ function restoreInstalledApps(callback) {
debug(`marking ${app.fqdn} for restore using restore config ${JSON.stringify(restoreConfig)}`);
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_RESTORE, { restoreConfig: restoreConfig, oldConfig: null }, function (error) {
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_RESTORE, { restoreConfig: restoreConfig, oldConfig: getAppConfig(app) }, function (error) {
if (error) debug(`Error marking ${app.fqdn} for restore: ${JSON.stringify(error)}`);
iteratorDone(); // always succeed
+38 -11
View File
@@ -5,6 +5,7 @@ exports = module.exports = {
unpurchase: unpurchase,
getSubscription: getSubscription,
isFreePlan: isFreePlan,
sendAliveStatus: sendAliveStatus,
@@ -18,7 +19,8 @@ exports = module.exports = {
AppstoreError: AppstoreError
};
var apps = require('./apps.js'),
var appdb = require('./appdb.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
config = require('./config.js'),
@@ -89,6 +91,10 @@ function getSubscription(callback) {
});
}
function isFreePlan(subscription) {
return !subscription || subscription.plan.id === 'free';
}
function purchase(appId, appstoreId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof appstoreId, 'string');
@@ -96,20 +102,41 @@ function purchase(appId, appstoreId, callback) {
if (appstoreId === '') return callback(null);
getAppstoreConfig(function (error, appstoreConfig) {
function doThePurchase() {
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/apps/' + appId;
var data = { appstoreId: appstoreId };
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
if (result.statusCode === 402) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED, result.body.message));
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
callback(null);
});
});
}
getSubscription(function (error, result) {
if (error) return callback(error);
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/apps/' + appId;
var data = { appstoreId: appstoreId };
// only check for app install count if on the free plan
if (result.id !== 'free') return doThePurchase();
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
if (result.statusCode === 402) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED, result.body.message));
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
appdb.getAppStoreIds(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
callback(null);
var count = result.filter(function (a) { return !!a.appStoreId; }).length;
// we only allow max of 2 app installations without a subscription
// WARNING install and clone in apps.js will first add the db record and then call purchase() so we test for more than 2 here
if (count > 2) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED, 'Too many apps installed'));
doThePurchase();
});
});
}
+27 -10
View File
@@ -20,11 +20,6 @@ exports = module.exports = {
require('supererror')({ splatchError: true });
// remove timestamp from debug() based output
require('debug').formatArgs = function formatArgs(args) {
args[0] = this.namespace + ' ' + args[0];
};
var addons = require('./addons.js'),
appdb = require('./appdb.js'),
apps = require('./apps.js'),
@@ -46,6 +41,7 @@ var addons = require('./addons.js'),
path = require('path'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
rimraf = require('rimraf'),
safe = require('safetydance'),
shell = require('./shell.js'),
superagent = require('superagent'),
@@ -329,6 +325,16 @@ function removeIcon(app, callback) {
});
}
function cleanupLogs(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
rimraf(path.join(paths.LOG_DIR, app.id), function (error) {
if (error) debugApp(app, 'cannot cleanup logs: %s', error);
callback(null);
});
}
function waitForDnsPropagation(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -341,7 +347,7 @@ function waitForDnsPropagation(app, callback) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
domains.waitForDnsRecord(app.fqdn, app.domain, ip, { interval: 5000, times: 120 }, callback);
domains.waitForDnsRecord(app.fqdn, app.domain, ip, { interval: 5000, times: 240 }, callback);
});
}
@@ -374,8 +380,14 @@ function install(app, callback) {
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
deleteContainers.bind(null, app),
// oldConfig can be null during upgrades
addons.teardownAddons.bind(null, app, app.oldConfig ? app.oldConfig.manifest.addons : app.manifest.addons),
function teardownAddons(next) {
// when restoring, app does not require these addons anymore. remove carefully to preserve the db passwords
var addonsToRemove = !isRestoring
? app.manifest.addons
: _.omit(app.oldConfig.manifest.addons, Object.keys(app.manifest.addons));
addons.teardownAddons(app, addonsToRemove, next);
},
deleteVolume.bind(null, app, { removeDirectory: false }), // do not remove any symlinked volume
// for restore case
@@ -676,12 +688,15 @@ function uninstall(app, callback) {
updateApp.bind(null, app, { installationProgress: '60, Unregistering subdomain' }),
unregisterSubdomain.bind(null, app, app.location, app.domain),
updateApp.bind(null, app, { installationProgress: '80, Cleanup icon' }),
updateApp.bind(null, app, { installationProgress: '70, Cleanup icon' }),
removeIcon.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Unconfiguring reverse proxy' }),
updateApp.bind(null, app, { installationProgress: '80, Unconfiguring reverse proxy' }),
unconfigureReverseProxy.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Cleanup logs' }),
cleanupLogs.bind(null, app),
updateApp.bind(null, app, { installationProgress: '95, Remove app from database' }),
appdb.del.bind(null, app.id)
], function seriesDone(error) {
@@ -770,6 +785,8 @@ function startTask(appId, callback) {
if (require.main === module) {
assert.strictEqual(process.argv.length, 3, 'Pass the appid as argument');
// add a separator for the log file
debug('------------------------------------------------------------');
debug('Apptask for %s', process.argv[2]);
process.on('SIGTERM', function () {
+1 -1
View File
@@ -33,7 +33,7 @@ var apps = require('./apps.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:clients'),
eventlog = require('./eventlog.js'),
hat = require('hat'),
hat = require('./hat.js'),
accesscontrol = require('./accesscontrol.js'),
tokendb = require('./tokendb.js'),
users = require('./users.js'),
+46 -24
View File
@@ -327,44 +327,66 @@ function checkDiskSpace(callback) {
});
}
function getLogs(options, callback) {
function getLogs(unit, options, callback) {
assert.strictEqual(typeof unit, 'string');
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
var units = options.units || [],
lines = options.lines || 100,
var lines = options.lines || 100,
format = options.format || 'json',
follow = !!options.follow;
assert(Array.isArray(units));
assert.strictEqual(typeof lines, 'number');
assert.strictEqual(typeof format, 'string');
debug('Getting logs for %j', units);
assert.strictEqual(typeof lines, 'number');
assert.strictEqual(typeof format, 'string');
var args = [ '--no-pager', '--lines=' + lines ];
units.forEach(function (u) {
if (u === 'box') args.push('--unit=box');
else if (u === 'mail') args.push('CONTAINER_NAME=mail');
});
if (format === 'short') args.push('--output=short', '-a'); else args.push('--output=json');
if (follow) args.push('--follow');
debug('Getting logs for %s as %s', unit, format);
var cp = spawn('/bin/journalctl', args);
var cp, transformStream;
if (unit === 'box') {
let args = [ '--no-pager', `--lines=${lines}` ];
if (format === 'short') args.push('--output=short', '-a'); else args.push('--output=json');
if (follow) args.push('--follow');
args.push('--unit=box');
args.push('--unit=cloudron-updater');
cp = spawn('/bin/journalctl', args);
var transformStream = split(function mapper(line) {
if (format !== 'json') return line + '\n';
transformStream = split(function mapper(line) {
if (format !== 'json') return line + '\n';
var obj = safe.JSON.parse(line);
if (!obj) return undefined;
var obj = safe.JSON.parse(line);
if (!obj) return undefined;
return JSON.stringify({
realtimeTimestamp: obj.__REALTIME_TIMESTAMP,
monotonicTimestamp: obj.__MONOTONIC_TIMESTAMP,
message: obj.MESSAGE,
source: obj.SYSLOG_IDENTIFIER || ''
}) + '\n';
});
return JSON.stringify({
realtimeTimestamp: obj.__REALTIME_TIMESTAMP,
monotonicTimestamp: obj.__MONOTONIC_TIMESTAMP,
message: obj.MESSAGE,
source: obj.SYSLOG_IDENTIFIER || ''
}) + '\n';
});
} else { // mail, mongodb, mysql, postgresql
let args = [ '--lines=' + lines ];
if (follow) args.push('--follow');
args.push(path.join(paths.LOG_DIR, unit, 'app.log'));
cp = spawn('/usr/bin/tail', args);
transformStream = split(function mapper(line) {
if (format !== 'json') return line + '\n';
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
var timestamp = (new Date(data[0])).getTime();
if (isNaN(timestamp)) timestamp = 0;
return JSON.stringify({
realtimeTimestamp: timestamp * 1000,
message: line.slice(data[0].length+1),
source: unit
}) + '\n';
});
}
transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process
+2 -4
View File
@@ -28,11 +28,9 @@ function maybeSend(callback) {
var pendingAppUpdates = updateInfo.apps || {};
pendingAppUpdates = Object.keys(pendingAppUpdates).map(function (key) { return pendingAppUpdates[key]; });
appstore.getSubscription(function (error, result) {
appstore.getSubscription(function (error, subscription) {
if (error) debug('Error getting subscription:', error);
var hasSubscription = result && result.plan.id !== 'free' && result.plan.id !== 'undecided';
eventlog.getByCreationTime(new Date(new Date() - 7*86400000), function (error, events) {
if (error) return callback(error);
@@ -46,7 +44,7 @@ function maybeSend(callback) {
if (error) return callback(error);
var info = {
hasSubscription: hasSubscription,
hasSubscription: appstore.isFreePlan(subscription),
pendingAppUpdates: pendingAppUpdates,
pendingBoxUpdate: updateInfo.box || null,
+146
View File
@@ -0,0 +1,146 @@
'use strict';
exports = module.exports = {
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
debug = require('debug')('box:dns/gandi'),
dns = require('../native-dns.js'),
DomainsError = require('../domains.js').DomainsError,
superagent = require('superagent'),
util = require('util');
var GANDI_API = 'https://dns.api.gandi.net/api/v5';
function formatError(response) {
return util.format(`Gandi DNS error [${response.statusCode}] ${response.body.message}`);
}
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
debug(`upsert: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
var data = {
'rrset_ttl': 300, // this is the minimum allowed
'rrset_values': values // for mx records, value is already of the '<priority> <server>' format
};
superagent.put(`${GANDI_API}/domains/${zoneName}/records/${subdomain}/${type}`)
.set('X-Api-Key', dnsConfig.token)
.timeout(30 * 1000)
.send(data)
.end(function (error, result) {
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
if (result.statusCode === 400) return callback(new DomainsError(DomainsError.BAD_FIELD, formatError(result)));
if (result.statusCode !== 201) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
return callback(null, 'unused-id');
});
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
debug(`get: ${subdomain} in zone ${zoneName} of type ${type}`);
superagent.get(`${GANDI_API}/domains/${zoneName}/records/${subdomain}/${type}`)
.set('X-Api-Key', dnsConfig.token)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
if (result.statusCode === 404) return callback(null, [ ]);
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
debug('get: %j', result.body);
return callback(null, result.body.rrset_values);
});
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
debug(`del: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
superagent.del(`${GANDI_API}/domains/${zoneName}/records/${subdomain}/${type}`)
.set('X-Api-Key', dnsConfig.token)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 404) return callback(null);
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 204) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
debug('del: done');
return callback(null);
});
}
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
var credentials = {
token: dnsConfig.token
};
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
if (!nameservers.every(function (n) { return n.toLowerCase().indexOf('.gandi.net') !== -1; })) {
debug('verifyDnsConfig: %j does not contain Gandi NS', nameservers);
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Gandi'));
}
const testSubdomain = 'cloudrontestdns';
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
callback(null, credentials);
});
});
});
}
+3 -3
View File
@@ -174,14 +174,14 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !resolvedNS) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
getZoneByName(credentials, zoneName, function (error, zone) {
if (error) return callback(error);
var definedNS = zone.metadata.nameServers.sort().map(function(r) { return r.replace(/\.$/, ''); });
if (!_.isEqual(definedNS, resolvedNS.sort())) {
debug('verifyDnsConfig: %j and %j do not match', resolvedNS, definedNS);
if (!_.isEqual(definedNS, nameservers.sort())) {
debug('verifyDnsConfig: %j and %j do not match', nameservers, definedNS);
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Google Cloud DNS'));
}
+181
View File
@@ -0,0 +1,181 @@
'use strict';
exports = module.exports = {
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
debug = require('debug')('box:dns/godaddy'),
dns = require('../native-dns.js'),
DomainsError = require('../domains.js').DomainsError,
superagent = require('superagent'),
util = require('util');
// const GODADDY_API_OTE = 'https://api.ote-godaddy.com/v1/domains';
const GODADDY_API = 'https://api.godaddy.com/v1/domains';
// this is a workaround for godaddy not having a delete API
// https://stackoverflow.com/questions/39347464/delete-record-libcloud-godaddy-api
const GODADDY_INVALID_IP = '0.0.0.0';
function formatError(response) {
return util.format(`GoDaddy DNS error [${response.statusCode}] ${response.body.message}`);
}
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
debug(`upsert: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
var records = [ ];
values.forEach(function (value) {
var record = { ttl: 600 }; // 600 is the min ttl
if (type === 'MX') {
record.priority = parseInt(value.split(' ')[0], 10);
record.data = value.split(' ')[1];
} else {
record.data = value;
}
records.push(record);
});
superagent.put(`${GODADDY_API}/${zoneName}/records/${type}/${subdomain}`)
.set('Authorization', `sso-key ${dnsConfig.apiKey}:${dnsConfig.apiSecret}`)
.timeout(30 * 1000)
.send(records)
.end(function (error, result) {
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
if (result.statusCode === 400) return callback(new DomainsError(DomainsError.BAD_FIELD, formatError(result))); // no such zone
if (result.statusCode === 422) return callback(new DomainsError(DomainsError.BAD_FIELD, formatError(result))); // conflict
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
return callback(null, 'unused-id');
});
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
debug(`get: ${subdomain} in zone ${zoneName} of type ${type}`);
superagent.get(`${GODADDY_API}/${zoneName}/records/${type}/${subdomain}`)
.set('Authorization', `sso-key ${dnsConfig.apiKey}:${dnsConfig.apiSecret}`)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
if (result.statusCode === 404) return callback(null, [ ]);
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
debug('get: %j', result.body);
var values = result.body.map(function (record) { return record.data; });
if (values.length === 1 && values[0] === GODADDY_INVALID_IP) return callback(null, [ ]); // pretend this record doesn't exist
return callback(null, values);
});
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
debug(`get: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
if (type !== 'A') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, new Error('Not supported by GoDaddy API'))); // can never happen
// check if the record exists at all so that we don't insert the "Dead" record for no reason
get(dnsConfig, zoneName, subdomain, type, function (error, values) {
if (error) return callback(error);
if (values.length === 0) return callback();
// godaddy does not have a delete API. so fill it up with an invalid IP that we can ignore in future get()
var records = [{
ttl: 600,
data: GODADDY_INVALID_IP
}];
superagent.put(`${GODADDY_API}/${zoneName}/records/${type}/${subdomain}`)
.set('Authorization', `sso-key ${dnsConfig.apiKey}:${dnsConfig.apiSecret}`)
.send(records)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 404) return callback(null);
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
debug('del: done');
return callback(null);
});
});
}
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
var credentials = {
apiKey: dnsConfig.apiKey,
apiSecret: dnsConfig.apiSecret
};
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
if (!nameservers.every(function (n) { return n.toLowerCase().indexOf('.domaincontrol.com') !== -1; })) {
debug('verifyDnsConfig: %j does not contain GoDaddy NS', nameservers);
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to GoDaddy'));
}
const testSubdomain = 'cloudrontestdns';
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
callback(null, credentials);
});
});
});
}
+2 -1
View File
@@ -57,7 +57,8 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
// Very basic check if the nameservers can be fetched
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to get nameservers'));
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
callback(null, { wildcard: !!dnsConfig.wildcard });
});
+243
View File
@@ -0,0 +1,243 @@
'use strict';
exports = module.exports = {
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
debug = require('debug')('box:dns/namecom'),
dns = require('../native-dns.js'),
safe = require('safetydance'),
DomainsError = require('../domains.js').DomainsError,
superagent = require('superagent');
const NAMECOM_API = 'https://api.name.com/v4';
function formatError(response) {
return `Name.com DNS error [${response.statusCode}] ${response.text}`;
}
function addRecord(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug(`add: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
var data = {
host: subdomain,
type: type,
ttl: 300 // 300 is the lowest
};
if (type === 'MX') {
data.priority = parseInt(values[0].split(' ')[0], 10);
data.answer = values[0].split(' ')[1];
} else {
data.answer = values[0];
}
superagent.post(`${NAMECOM_API}/domains/${zoneName}/records`)
.auth(dnsConfig.username, dnsConfig.token)
.timeout(30 * 1000)
.send(data)
.end(function (error, result) {
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, `Network error ${error.message}`));
if (result.statusCode === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
return callback(null, 'unused-id');
});
}
function updateRecord(dnsConfig, zoneName, recordId, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof recordId, 'number');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug(`update:${recordId} on ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
var data = {
host: subdomain,
type: type,
ttl: 300 // 300 is the lowest
};
if (type === 'MX') {
data.priority = parseInt(values[0].split(' ')[0], 10);
data.answer = values[0].split(' ')[1];
} else {
data.answer = values[0];
}
superagent.put(`${NAMECOM_API}/domains/${zoneName}/records/${recordId}`)
.auth(dnsConfig.username, dnsConfig.token)
.timeout(30 * 1000)
.send(data)
.end(function (error, result) {
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, `Network error ${error.message}`));
if (result.statusCode === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
return callback(null, 'unused-id');
});
}
function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
debug(`getInternal: ${subdomain} in zone ${zoneName} of type ${type}`);
superagent.get(`${NAMECOM_API}/domains/${zoneName}/records`)
.auth(dnsConfig.username, dnsConfig.token)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, `Network error ${error.message}`));
if (result.statusCode === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
// name.com does not return the correct content-type
result.body = safe.JSON.parse(result.text);
if (!result.body.records) result.body.records = [];
result.body.records.forEach(function (r) {
// name.com api simply strips empty properties
r.host = r.host || '@';
});
var results = result.body.records.filter(function (r) {
return (r.host === subdomain && r.type === type);
});
debug('getInternal: %j', results);
return callback(null, results);
});
}
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
debug(`upsert: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
if (result.length === 0) return addRecord(dnsConfig, zoneName, subdomain, type, values, callback);
return updateRecord(dnsConfig, zoneName, result[0].id, subdomain, type, values, callback);
});
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
var tmp = result.map(function (record) { return record.answer; });
debug('get: %j', tmp);
return callback(null, tmp);
});
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
debug(`del: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
if (result.length === 0) return callback();
superagent.del(`${NAMECOM_API}/domains/${zoneName}/records/${result[0].id}`)
.auth(dnsConfig.username, dnsConfig.token)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, `Network error ${error.message}`));
if (result.statusCode === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
return callback(null);
});
});
}
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
var credentials = {
username: dnsConfig.username,
token: dnsConfig.token
};
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
if (!nameservers.every(function (n) { return n.toLowerCase().indexOf('.name.com') !== -1; })) {
debug('verifyDnsConfig: %j does not contain Name.com NS', nameservers);
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Name.com'));
}
const testSubdomain = 'cloudrontestdns';
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
callback(null, credentials);
});
});
});
}
+14 -4
View File
@@ -39,7 +39,16 @@ function getZoneByName(dnsConfig, zoneName, callback) {
assert.strictEqual(typeof callback, 'function');
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
route53.listHostedZones({}, function (error, result) {
// backward compat for 2.2, where we only required access to "listHostedZones"
let listHostedZones;
if (dnsConfig.listHostedZonesByName) {
listHostedZones = route53.listHostedZonesByName.bind(route53, { MaxItems: '1', DNSName: zoneName + '.' });
} else {
listHostedZones = route53.listHostedZones.bind(route53, {}); // currently, this route does not support > 100 zones
}
listHostedZones(function (error, result) {
if (error && error.code === 'AccessDenied') return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error && error.code === 'InvalidClientTokenId') return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
@@ -87,7 +96,7 @@ function add(dnsConfig, zoneName, subdomain, type, values, callback) {
if (error) return callback(error);
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName;
var records = values.map(function (v) { return { Value: v }; });
var records = values.map(function (v) { return { Value: v }; }); // for mx records, value is already of the '<priority> <server>' format
var params = {
ChangeBatch: {
@@ -228,7 +237,8 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
accessKeyId: dnsConfig.accessKeyId,
secretAccessKey: dnsConfig.secretAccessKey,
region: dnsConfig.region || 'us-east-1',
endpoint: dnsConfig.endpoint || null
endpoint: dnsConfig.endpoint || null,
listHostedZonesByName: true // new/updated creds require this perm
};
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
@@ -252,7 +262,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
del(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
+8
View File
@@ -179,6 +179,14 @@ function createSubcontainer(app, name, cmd, options, callback) {
},
HostConfig: {
Binds: addons.getBindsSync(app, app.manifest.addons),
LogConfig: {
Type: 'syslog',
Config: {
'tag': app.id,
'syslog-address': 'udp://127.0.0.1:2514', // see apps.js:validatePortBindings()
'syslog-format': 'rfc5424'
}
},
Memory: memoryLimit / 2,
MemorySwap: memoryLimit, // Memory + Swap
PortBindings: isAppContainer ? dockerPortBindings : { },
+17 -5
View File
@@ -68,7 +68,7 @@ DomainsError.STILL_BUSY = 'Still busy';
DomainsError.IN_USE = 'In Use';
DomainsError.INTERNAL_ERROR = 'Internal error';
DomainsError.ACCESS_DENIED = 'Access denied';
DomainsError.INVALID_PROVIDER = 'provider must be route53, gcdns, digitalocean, cloudflare, noop, manual or caas';
DomainsError.INVALID_PROVIDER = 'provider must be route53, gcdns, digitalocean, gandi, cloudflare, namecom, noop, manual or caas';
// choose which subdomain backend we use for test purpose we use route53
function api(provider) {
@@ -80,6 +80,9 @@ function api(provider) {
case 'route53': return require('./dns/route53.js');
case 'gcdns': return require('./dns/gcdns.js');
case 'digitalocean': return require('./dns/digitalocean.js');
case 'gandi': return require('./dns/gandi.js');
case 'godaddy': return require('./dns/godaddy.js');
case 'namecom': return require('./dns/namecom.js');
case 'noop': return require('./dns/noop.js');
case 'manual': return require('./dns/manual.js');
default: return null;
@@ -111,9 +114,11 @@ function add(domain, zoneName, provider, config, fallbackCertificate, tlsConfig,
assert.strictEqual(typeof callback, 'function');
if (!tld.isValid(domain)) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid domain'));
if (domain.endsWith('.')) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid domain'));
if (zoneName) {
if (!tld.isValid(zoneName)) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid zoneName'));
if (zoneName.endsWith('.')) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid zoneName'));
} else {
zoneName = tld.getDomain(domain) || domain;
}
@@ -133,7 +138,7 @@ function add(domain, zoneName, provider, config, fallbackCertificate, tlsConfig,
verifyDnsConfig(config, domain, zoneName, provider, ip, function (error, result) {
if (error && error.reason === DomainsError.ACCESS_DENIED) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record. Access denied'));
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Zone not found'));
if (error && error.reason === DomainsError.EXTERNAL_ERROR) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record:' + error.message));
if (error && error.reason === DomainsError.EXTERNAL_ERROR) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record: ' + error.message));
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
if (error && error.reason === DomainsError.INVALID_PROVIDER) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
@@ -186,8 +191,9 @@ function getAll(callback) {
});
}
function update(domain, provider, config, fallbackCertificate, tlsConfig, callback) {
function update(domain, zoneName, provider, config, fallbackCertificate, tlsConfig, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof fallbackCertificate, 'object');
@@ -198,6 +204,12 @@ function update(domain, provider, config, fallbackCertificate, tlsConfig, callba
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
if (zoneName) {
if (!tld.isValid(zoneName)) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid zoneName'));
} else {
zoneName = result.zoneName;
}
if (fallbackCertificate) {
let error = reverseProxy.validateCertificate(`test.${domain}`, fallbackCertificate.cert, fallbackCertificate.key);
if (error) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
@@ -210,7 +222,7 @@ function update(domain, provider, config, fallbackCertificate, tlsConfig, callba
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
verifyDnsConfig(config, domain, result.zoneName, provider, ip, function (error, result) {
verifyDnsConfig(config, domain, zoneName, provider, ip, function (error, result) {
if (error && error.reason === DomainsError.ACCESS_DENIED) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record. Access denied'));
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Zone not found'));
if (error && error.reason === DomainsError.EXTERNAL_ERROR) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record:' + error.message));
@@ -218,7 +230,7 @@ function update(domain, provider, config, fallbackCertificate, tlsConfig, callba
if (error && error.reason === DomainsError.INVALID_PROVIDER) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
domaindb.update(domain, { provider: provider, config: result, tlsConfig: tlsConfig }, function (error) {
domaindb.update(domain, { zoneName: zoneName, provider: provider, config: result, tlsConfig: tlsConfig }, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
+9
View File
@@ -0,0 +1,9 @@
'use strict';
exports = module.exports = hat;
var crypto = require('crypto');
function hat (bits) {
return crypto.randomBytes(bits / 8).toString('hex');
}
+5 -5
View File
@@ -7,18 +7,18 @@
exports = module.exports = {
// a major version makes all apps restore from backup. #451 must be fixed before we do this.
// a minor version makes all apps re-configure themselves
'version': '48.9.0',
'version': '48.11.0',
'baseImages': [ 'cloudron/base:0.10.0' ],
// Note that if any of the databases include an upgrade, bump the infra version above
// This is because we upgrade using dumps instead of mysql_upgrade, pg_upgrade etc
'images': {
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:1.0.0' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:1.0.0' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:1.0.1' },
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:1.1.0' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:1.1.0' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:1.1.0' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:1.0.0' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:1.2.2' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:1.3.0' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:1.0.0' }
}
};
+9 -7
View File
@@ -12,6 +12,8 @@ exports = module.exports = {
addDnsRecords: addDnsRecords,
validateName: validateName,
setMailFromValidation: setMailFromValidation,
setCatchAllAddress: setCatchAllAddress,
setMailRelay: setMailRelay,
@@ -46,7 +48,6 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:mail'),
dns = require('./native-dns.js'),
@@ -92,10 +93,12 @@ function MailError(reason, errorOrMessage) {
}
util.inherits(MailError, Error);
MailError.INTERNAL_ERROR = 'Internal Error';
MailError.EXTERNAL_ERROR = 'External Error';
MailError.BAD_FIELD = 'Bad Field';
MailError.ALREADY_EXISTS = 'Already Exists';
MailError.NOT_FOUND = 'Not Found';
MailError.IN_USE = 'In Use';
MailError.BILLING_REQUIRED = 'Billing Required';
function validateName(name) {
assert.strictEqual(typeof name, 'string');
@@ -357,11 +360,6 @@ const RBL_LIST = [
'dns': 'spam.dnsbl.sorbs.net',
'site': 'http://sorbs.net'
},
{
'name': 'Spam Cannibal',
'dns': 'bl.spamcannibal.org',
'site': 'http://www.spamcannibal.org/cannibal.cgi'
},
{
'name': 'SpamCop',
'dns': 'bl.spamcop.net',
@@ -562,6 +560,10 @@ function restartMail(callback) {
const cmd = `docker run --restart=always -d --name="mail" \
--net cloudron \
--net-alias mail \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=mail \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
@@ -989,7 +991,7 @@ function setAliases(name, domain, aliases, callback) {
mailboxdb.setAliasesForName(name, domain, aliases, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS && error.message.indexOf('mailboxes_name_domain_unique_index') !== -1) {
var aliasMatch = error.message.match(new RegExp(`^ER_DUP_ENTRY: Duplicate entry '(.*)-${domain}' for key 'mailboxes_name_domain_unique_index'$`))
var aliasMatch = error.message.match(new RegExp(`^ER_DUP_ENTRY: Duplicate entry '(.*)-${domain}' for key 'mailboxes_name_domain_unique_index'$`));
if (!aliasMatch) return callback(new MailError(MailError.ALREADY_EXISTS, error.message));
return callback(new MailError(MailError.ALREADY_EXISTS, `Mailbox, mailinglist or alias for ${aliasMatch[1]} already exists`));
}
+16 -17
View File
@@ -60,6 +60,19 @@ function splatchError(error) {
return util.inspect(result, { depth: null, showHidden: true });
}
function getAdminEmails(callback) {
users.getAllAdmins(function (error, admins) {
if (error) return callback(error);
if (admins.length === 0) return callback(new Error('No admins on this cloudron')); // box not activated yet
var adminEmails = [ ];
admins.forEach(function (admin) { adminEmails.push(admin.email); });
callback(null, adminEmails);
});
}
// This will collect the most common details required for notification emails
function getMailConfig(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -157,20 +170,6 @@ function render(templateFile, params) {
return content;
}
function getAdminEmails(callback) {
users.getAllAdmins(function (error, admins) {
if (error) return callback(error);
if (admins.length === 0) return callback(new Error('No admins on this cloudron')); // box not activated yet
var adminEmails = [ ];
adminEmails.push(admins[0].fallbackEmail);
admins.forEach(function (admin) { adminEmails.push(admin.email); });
callback(null, adminEmails);
});
}
function mailUserEventToAdmins(user, event) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof event, 'string');
@@ -203,7 +202,7 @@ function sendInvite(user, invitor) {
var templateData = {
user: user,
webadminUrl: config.adminOrigin(),
setupLink: config.adminOrigin() + '/api/v1/session/account/setup.html?reset_token=' + user.resetToken,
setupLink: `${config.adminOrigin()}/api/v1/session/account/setup.html?reset_token=${user.resetToken}&email=${user.email}`,
invitor: invitor,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
@@ -240,7 +239,7 @@ function userAdded(user, inviteSent) {
var templateData = {
user: user,
inviteLink: inviteSent ? null : config.adminOrigin() + '/api/v1/session/account/setup.html?reset_token=' + user.resetToken,
inviteLink: inviteSent ? null : `${config.adminOrigin()}/api/v1/session/account/setup.html?reset_token=${user.resetToken}&email=${user.email}`,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
@@ -290,7 +289,7 @@ function passwordReset(user) {
var templateData = {
user: user,
resetLink: config.adminOrigin() + '/api/v1/session/password/reset.html?reset_token=' + user.resetToken,
resetLink: `${config.adminOrigin()}/api/v1/session/password/reset.html?reset_token=${user.resetToken}&email=${user.email}`,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
+5 -4
View File
@@ -19,8 +19,8 @@ app.controller('Controller', ['$scope', function ($scope) {
<center>
<br/>
<h4>Hello <%= (user && user.email) ? user.email : '' %>, welcome to <%= cloudronName %>.</h4>
<h2>Setup your account and password.</h2>
<h4>Hello <%= (user && user.email) ? user.email : '' %>, welcome to <%= cloudronName %>!</h4>
<h2>Setup your account and password</h2>
</center>
<div class="container" ng-app="Application" ng-controller="Controller">
@@ -29,6 +29,7 @@ app.controller('Controller', ['$scope', function ($scope) {
<form action="/api/v1/session/account/setup" method="post" name="setupForm" autocomplete="off" role="form" novalidate>
<input type="password" style="display: none;">
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
<input type="hidden" name="email" value="<%= email %>"/>
<input type="hidden" name="resetToken" value="<%= resetToken %>"/>
<center><p class="has-error"><%= error %></p></center>
@@ -58,9 +59,9 @@ app.controller('Controller', ['$scope', function ($scope) {
<div class="form-group" ng-class="{ 'has-error': (setupForm.password.$dirty && setupForm.password.$invalid) }">
<label class="control-label">New Password</label>
<div class="control-label" ng-show="setupForm.password.$dirty && setupForm.password.$invalid">
<small ng-show="setupForm.password.$dirty && setupForm.password.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
<small ng-show="setupForm.password.$dirty && setupForm.password.$invalid">Password must be atleast 8 characters</small>
</div>
<input type="password" class="form-control" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" required>
<input type="password" class="form-control" ng-model="password" name="password" ng-pattern="/^.{8,30}$/" required>
</div>
<div class="form-group" ng-class="{ 'has-error': (setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)) }">
+1 -1
View File
@@ -1,6 +1,6 @@
<footer class="text-center">
<span class="text-muted">&copy; 2017 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
<span class="text-muted">&copy; 2016-18 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fa fa-twitter"></i></a></span>
<span class="text-muted"><a href="https://chat.cloudron.io" target="_blank">Chat <i class="fa fa-comments"></i></a></span>
</footer>
+3 -2
View File
@@ -26,14 +26,15 @@ app.controller('Controller', [function () {}]);
<form action="/api/v1/session/password/reset" method="post" name="resetForm" autocomplete="off" role="form" novalidate>
<input type="password" style="display: none;">
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
<input type="hidden" name="email" value="<%= email %>"/>
<input type="hidden" name="resetToken" value="<%= resetToken %>"/>
<div class="form-group" ng-class="{ 'has-error': resetForm.password.$dirty && resetForm.password.$invalid }">
<label class="control-label" for="inputPassword">New Password</label>
<div class="control-label" ng-show="resetForm.password.$dirty && resetForm.password.$invalid">
<small ng-show="resetForm.password.$dirty && resetForm.password.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
<small ng-show="resetForm.password.$dirty && resetForm.password.$invalid">Password must be atleast 8 characters</small>
</div>
<input type="password" class="form-control" id="inputPassword" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" autofocus required>
<input type="password" class="form-control" id="inputPassword" ng-model="password" name="password" ng-pattern="/^.{8,30}$/" autofocus required>
</div>
<div class="form-group" ng-class="{ 'has-error': resetForm.passwordRepeat.$dirty && (password !== passwordRepeat) }">
<label class="control-label" for="inputPasswordRepeat">Repeat Password</label>
-47
View File
@@ -1,47 +0,0 @@
/* jslint node:true */
'use strict';
// From https://www.npmjs.com/package/password-generator
exports = module.exports = {
generate: generate,
validate: validate
};
var assert = require('assert'),
generatePassword = require('password-generator');
// http://www.w3resource.com/javascript/form/example4-javascript-form-validation-password.html
// WARNING!!! if this is changed, the UI parts in the setup and account view have to be adjusted!
var gPasswordTestRegExp = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/;
var UPPERCASE_RE = /([A-Z])/g;
var LOWERCASE_RE = /([a-z])/g;
var NUMBER_RE = /([\d])/g;
var SPECIAL_CHAR_RE = /([\?\-])/g;
function isStrongEnough(password) {
var uc = password.match(UPPERCASE_RE);
var lc = password.match(LOWERCASE_RE);
var n = password.match(NUMBER_RE);
var sc = password.match(SPECIAL_CHAR_RE);
return uc && lc && n && sc;
}
function generate() {
var password = '';
while (!isStrongEnough(password)) password = generatePassword(8, false, /[\w\d\?\-]/);
return password;
}
function validate(password) {
assert.strictEqual(typeof password, 'string');
if (!password.match(gPasswordTestRegExp)) return new Error('Password must be 8-30 character with at least one uppercase, one numeric and one special character');
return null;
}
+3 -1
View File
@@ -33,5 +33,7 @@ exports = module.exports = {
CLOUDRON_AVATAR_FILE: path.join(config.baseDir(), 'boxdata/avatar.png'),
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'boxdata/updatechecker.json'),
AUTO_PROVISION_FILE: path.join(config.baseDir(), 'configs/autoprovision.json')
AUTO_PROVISION_FILE: path.join(config.baseDir(), 'configs/autoprovision.json'),
LOG_DIR: path.join(config.baseDir(), 'platformdata/logs')
};
+47 -4
View File
@@ -13,7 +13,7 @@ var apps = require('./apps.js'),
config = require('./config.js'),
debug = require('debug')('box:platform'),
fs = require('fs'),
hat = require('hat'),
hat = require('./hat.js'),
infra = require('./infra_version.js'),
locker = require('./locker.js'),
mail = require('./mail.js'),
@@ -22,6 +22,7 @@ var apps = require('./apps.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
shell = require('./shell.js'),
taskmanager = require('./taskmanager.js'),
util = require('util'),
@@ -47,8 +48,15 @@ function start(callback) {
// short-circuit for the restart case
if (_.isEqual(infra, existingInfra)) {
debug('platform is uptodate at version %s', infra.version);
emitPlatformReady();
return callback();
updateAddons(function (error) {
if (error) return callback(error);
emitPlatformReady();
callback();
});
return;
}
debug('Updating infrastructure from %s to %s', existingInfra.version, infra.version);
@@ -61,7 +69,8 @@ function start(callback) {
startAddons.bind(null, existingInfra),
removeOldImages,
startApps.bind(null, existingInfra),
fs.writeFile.bind(fs, paths.INFRA_VERSION_FILE, JSON.stringify(infra))
fs.writeFile.bind(fs, paths.INFRA_VERSION_FILE, JSON.stringify(infra, null, 4)),
updateAddons
], function (error) {
if (error) return callback(error);
@@ -80,6 +89,24 @@ function stop(callback) {
taskmanager.pauseTasks(callback);
}
function updateAddons(callback) {
settings.getPlatformConfig(function (error, platformConfig) {
if (error) return callback(error);
for (var containerName of [ 'mysql', 'postgresql', 'mail', 'mongodb' ]) {
const containerConfig = platformConfig[containerName];
if (!containerConfig) continue;
if (!containerConfig.memory || !containerConfig.memorySwap) continue;
const cmd = `docker update --memory ${containerConfig.memory} --memory-swap ${containerConfig.memorySwap} ${containerName}`;
shell.execSync(`update${containerName}`, cmd);
}
callback();
});
}
function emitPlatformReady() {
// give some time for the platform to "settle". For example, mysql might still be initing the
// database dir and we cannot call service scripts until that's done.
@@ -135,6 +162,10 @@ function startGraphite(callback) {
const cmd = `docker run --restart=always -d --name="graphite" \
--net cloudron \
--net-alias graphite \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=graphite \
-m 75m \
--memory-swap 150m \
--dns 172.18.0.1 \
@@ -164,6 +195,10 @@ function startMysql(callback) {
const cmd = `docker run --restart=always -d --name="mysql" \
--net cloudron \
--net-alias mysql \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=mysql \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
@@ -190,6 +225,10 @@ function startPostgresql(callback) {
const cmd = `docker run --restart=always -d --name="postgresql" \
--net cloudron \
--net-alias postgresql \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=postgresql \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
@@ -216,6 +255,10 @@ function startMongodb(callback) {
const cmd = `docker run --restart=always -d --name="mongodb" \
--net cloudron \
--net-alias mongodb \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=mongodb \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
+1
View File
@@ -386,6 +386,7 @@ function renewAll(auditSource, callback) {
async.eachSeries(allApps, function (app, iteratorCallback) {
ensureCertificate(app, auditSource, function (error, bundle) {
if (error) return iteratorCallback(error); // this can happen if cloudron is not setup yet
if (bundle.reason !== 'new-le' && bundle.reason !== 'fallback') return iteratorCallback();
// reconfigure for the case where we got a renewed cert after fallback
+2
View File
@@ -170,6 +170,8 @@ function configureApp(req, res, next) {
if (data.robotsTxt && typeof data.robotsTxt !== 'string') return next(new HttpError(400, 'robotsTxt must be a string'));
if ('mailboxName' in data && typeof data.mailboxName !== 'string') return next(new HttpError(400, 'mailboxName must be a string'));
debug('Configuring app id:%s data:%j', req.params.id, data);
apps.configure(req.params.id, data, auditSource(req), function (error) {
+6 -8
View File
@@ -97,19 +97,18 @@ function feedback(req, res, next) {
}
function getLogs(req, res, next) {
assert.strictEqual(typeof req.params.unit, 'string');
var lines = req.query.lines ? parseInt(req.query.lines, 10) : 100;
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number'));
var units = req.query.units || 'all';
var options = {
lines: lines,
follow: false,
units: units.split(','),
format: req.query.format
};
cloudron.getLogs(options, function (error, logStream) {
cloudron.getLogs(req.params.unit, options, function (error, logStream) {
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(404, 'Invalid type'));
if (error) return next(new HttpError(500, error));
@@ -124,11 +123,11 @@ function getLogs(req, res, next) {
}
function getLogStream(req, res, next) {
assert.strictEqual(typeof req.params.unit, 'string');
var lines = req.query.lines ? parseInt(req.query.lines, 10) : -10; // we ignore last-event-id
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
var units = req.query.units || 'all';
function sse(id, data) { return 'id: ' + id + '\ndata: ' + data + '\n\n'; }
if (req.headers.accept !== 'text/event-stream') return next(new HttpError(400, 'This API call requires EventStream'));
@@ -136,11 +135,10 @@ function getLogStream(req, res, next) {
var options = {
lines: lines,
follow: true,
units: units.split(','),
format: req.query.format
};
cloudron.getLogs(options, function (error, logStream) {
cloudron.getLogs(req.params.unit, options, function (error, logStream) {
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(404, 'Invalid type'));
if (error) return next(new HttpError(500, error));
+1 -1
View File
@@ -17,7 +17,7 @@ function login(req, res, next) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
if (user.twoFactorAuthenticationEnabled) {
if (!user.ghost && user.twoFactorAuthenticationEnabled) {
if (!req.body.totpToken) return next(new HttpError(401, 'A totpToken must be provided'));
let verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken });
+2 -1
View File
@@ -67,6 +67,7 @@ function update(req, res, next) {
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider must be an object'));
if (typeof req.body.config !== 'object') return next(new HttpError(400, 'config must be an object'));
if ('zoneName' in req.body && typeof req.body.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be a string'));
if ('fallbackCertificate' in req.body && typeof req.body.fallbackCertificate !== 'object') return next(new HttpError(400, 'fallbackCertificate must be a object with cert and key strings'));
if (req.body.fallbackCertificate && (!req.body.fallbackCertificate.cert || typeof req.body.fallbackCertificate.cert !== 'string')) return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
if (req.body.fallbackCertificate && (!req.body.fallbackCertificate.key || typeof req.body.fallbackCertificate.key !== 'string')) return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
@@ -76,7 +77,7 @@ function update(req, res, next) {
// some DNS providers like DigitalOcean take a really long time to verify credentials (https://github.com/expressjs/timeout/issues/26)
req.clearTimeout();
domains.update(req.params.domain, req.body.provider, req.body.config, req.body.fallbackCertificate || null, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
domains.update(req.params.domain, req.body.zoneName || '', req.body.provider, req.body.config, req.body.fallbackCertificate || null, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
if (error && error.reason === DomainsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === DomainsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === DomainsError.INVALID_PROVIDER) return next(new HttpError(400, error.message));
+1
View File
@@ -182,6 +182,7 @@ function setMailEnabled(req, res, next) {
mail.setMailEnabled(req.params.domain, !!req.body.enabled, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === MailError.BILLING_REQUIRED) return next(new HttpError(402, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
+19 -13
View File
@@ -30,7 +30,7 @@ var accesscontrol = require('../accesscontrol.js'),
DatabaseError = require('../databaseerror.js'),
debug = require('debug')('box:routes/oauth2'),
eventlog = require('../eventlog.js'),
hat = require('hat'),
hat = require('../hat.js'),
HttpError = require('connect-lastmile').HttpError,
middleware = require('../middleware/index.js'),
oauth2orize = require('oauth2orize'),
@@ -286,7 +286,7 @@ function login(req, res) {
passport.authenticate('local', {
failureRedirect: '/api/v1/session/login?' + failureQuery
})(req, res, function () {
if (req.user.twoFactorAuthenticationEnabled) {
if (!req.user.ghost && req.user.twoFactorAuthenticationEnabled) {
if (!req.body.totpToken) {
let failureQuery = querystring.stringify({ error: 'A 2FA token is required', returnTo: returnTo });
return res.redirect('/api/v1/session/login?' + failureQuery);
@@ -327,7 +327,7 @@ function passwordResetRequestSite(req, res) {
function passwordResetRequest(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.identifier !== 'string') return next(new HttpError(400, 'Missing identifier'));
if (typeof req.body.identifier !== 'string') return next(new HttpError(400, 'Missing identifier')); // email or username
debug('passwordResetRequest: email or username %s.', req.body.identifier);
@@ -352,6 +352,7 @@ function renderAccountSetupSite(res, req, userObject, error) {
error: error,
csrf: req.csrfToken(),
resetToken: req.query.reset_token || req.body.resetToken,
email: req.query.email || req.body.email,
title: 'Password Setup'
});
}
@@ -359,9 +360,10 @@ function renderAccountSetupSite(res, req, userObject, error) {
// -> GET /api/v1/session/account/setup.html
function accountSetupSite(req, res) {
if (!req.query.reset_token) return sendError(req, res, 'Missing Reset Token');
if (!req.query.email) return sendError(req, res, 'Missing Email');
users.getByResetToken(req.query.reset_token, function (error, userObject) {
if (error) return sendError(req, res, 'Invalid Reset Token');
users.getByResetToken(req.query.email, req.query.reset_token, function (error, userObject) {
if (error) return sendError(req, res, 'Invalid Email or Reset Token');
renderAccountSetupSite(res, req, userObject, '');
});
@@ -371,14 +373,15 @@ function accountSetupSite(req, res) {
function accountSetup(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'Missing email'));
if (typeof req.body.resetToken !== 'string') return next(new HttpError(400, 'Missing resetToken'));
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'Missing password'));
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'Missing username'));
if (typeof req.body.displayName !== 'string') return next(new HttpError(400, 'Missing displayName'));
debug('acountSetup: with token %s.', req.body.resetToken);
debug(`acountSetup: for email ${req.body.email} with token ${req.body.resetToken}`);
users.getByResetToken(req.body.resetToken, function (error, userObject) {
users.getByResetToken(req.body.email, req.body.resetToken, function (error, userObject) {
if (error) return sendError(req, res, 'Invalid Reset Token');
var data = _.pick(req.body, 'username', 'displayName');
@@ -397,7 +400,7 @@ function accountSetup(req, res, next) {
if (error) return next(new HttpError(500, error));
res.redirect(util.format('%s?accessToken=%s&expiresAt=%s', config.adminOrigin(), result.token, result.expiresAt));
res.redirect(config.adminOrigin());
});
});
});
@@ -405,15 +408,17 @@ function accountSetup(req, res, next) {
// -> GET /api/v1/session/password/reset.html
function passwordResetSite(req, res, next) {
if (!req.query.email) return next(new HttpError(400, 'Missing email'));
if (!req.query.reset_token) return next(new HttpError(400, 'Missing reset_token'));
users.getByResetToken(req.query.reset_token, function (error, user) {
if (error) return next(new HttpError(401, 'Invalid reset_token'));
users.getByResetToken(req.query.email, req.query.reset_token, function (error, user) {
if (error) return next(new HttpError(401, 'Invalid email or reset token'));
renderTemplate(res, 'password_reset', {
user: user,
csrf: req.csrfToken(),
resetToken: req.query.reset_token,
email: req.query.email,
title: 'Password Reset'
});
});
@@ -423,13 +428,14 @@ function passwordResetSite(req, res, next) {
function passwordReset(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'Missing email'));
if (typeof req.body.resetToken !== 'string') return next(new HttpError(400, 'Missing resetToken'));
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'Missing password'));
debug('passwordReset: with token %s.', req.body.resetToken);
debug(`passwordReset: for ${req.body.email} with token ${req.body.resetToken}`);
users.getByResetToken(req.body.resetToken, function (error, userObject) {
if (error) return next(new HttpError(401, 'Invalid resetToken'));
users.getByResetToken(req.body.email, req.body.resetToken, function (error, userObject) {
if (error) return next(new HttpError(401, 'Invalid email or resetToken'));
if (!userObject.username) return next(new HttpError(401, 'No username set'));
+6 -6
View File
@@ -47,7 +47,7 @@ function setAppAutoupdatePattern(req, res, next) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
next(new HttpSuccess(200, {}));
});
}
@@ -68,7 +68,7 @@ function setBoxAutoupdatePattern(req, res, next) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
next(new HttpSuccess(200, {}));
});
}
@@ -81,7 +81,7 @@ function setCloudronName(req, res, next) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
next(new HttpSuccess(202, {}));
});
}
@@ -110,7 +110,7 @@ function setTimeZone(req, res, next) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
next(new HttpSuccess(200, {}));
});
}
@@ -123,7 +123,7 @@ function setCloudronAvatar(req, res, next) {
settings.setCloudronAvatar(avatar, function (error) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
next(new HttpSuccess(202, {}));
});
}
@@ -165,7 +165,7 @@ function setBackupConfig(req, res, next) {
if (error && error.reason === SettingsError.EXTERNAL_ERROR) return next(new HttpError(402, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
next(new HttpSuccess(200, {}));
});
}
+1
View File
@@ -78,6 +78,7 @@ function dnsSetup(req, res, next) {
setup.dnsSetup(req.body.adminFqdn.toLowerCase(), req.body.domain.toLowerCase(), req.body.zoneName || '', req.body.provider, req.body.config, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
if (error && error.reason === SetupError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === SetupError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === SetupError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
+3 -3
View File
@@ -13,7 +13,7 @@ var accesscontrol = require('../../accesscontrol.js'),
oauth2 = require('../oauth2.js'),
expect = require('expect.js'),
uuid = require('uuid'),
hat = require('hat'),
hat = require('../../hat.js'),
superagent = require('superagent'),
server = require('../../server.js');
@@ -372,7 +372,7 @@ describe('Clients', function () {
setup,
function (callback) {
superagent.get(SERVER_URL + '/api/v1/user/profile')
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: token })
.end(function (error, result) {
expect(result).to.be.ok();
@@ -536,7 +536,7 @@ describe('Clients', function () {
expect(result.statusCode).to.equal(204);
// further calls with this token should not work
superagent.get(SERVER_URL + '/api/v1/user/profile')
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
+5 -5
View File
@@ -192,7 +192,7 @@ describe('Developer API', function () {
});
},
function (callback) {
superagent.post(`${SERVER_URL}/api/v1/user/profile/twofactorauthentication`).query({ access_token: accessToken }).end(function (error, result) {
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication`).query({ access_token: accessToken }).end(function (error, result) {
secret = result.body.secret;
callback(error);
});
@@ -203,7 +203,7 @@ describe('Developer API', function () {
encoding: 'base32'
});
superagent.post(`${SERVER_URL}/api/v1/user/profile/twofactorauthentication/enable`).query({ access_token: accessToken }).send({ totpToken: totpToken }).end(function (error, result) {
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication/enable`).query({ access_token: accessToken }).send({ totpToken: totpToken }).end(function (error, result) {
callback(error);
});
}
@@ -213,7 +213,7 @@ describe('Developer API', function () {
after(function (done) {
async.series([
function (callback) {
superagent.post(`${SERVER_URL}/api/v1/user/profile/twofactorauthentication/disable`).query({ access_token: accessToken }).send({ password: PASSWORD }).end(function (error, result) {
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication/disable`).query({ access_token: accessToken }).send({ password: PASSWORD }).end(function (error, result) {
callback(error);
});
},
@@ -285,14 +285,14 @@ describe('Developer API', function () {
after(cleanup);
it('fails with non sdk token', function (done) {
superagent.post(SERVER_URL + '/api/v1/user/profile/password').query({ access_token: token_normal }).send({ newPassword: 'Some?$123' }).end(function (error, result) {
superagent.post(SERVER_URL + '/api/v1/profile/password').query({ access_token: token_normal }).send({ newPassword: 'Some?$123' }).end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/user/profile/password').query({ access_token: token_sdk }).send({ newPassword: 'Some?$123' }).end(function (error, result) {
superagent.post(SERVER_URL + '/api/v1/profile/password').query({ access_token: token_sdk }).send({ newPassword: 'Some?$123' }).end(function (error, result) {
expect(result.statusCode).to.equal(204);
done();
});
+1 -1
View File
@@ -46,7 +46,7 @@ function setup(done) {
// stash token for further use
token = result.body.token;
superagent.get(SERVER_URL + '/api/v1/user/profile')
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: token })
.end(function (error, result) {
expect(result).to.be.ok();
+32 -13
View File
@@ -15,7 +15,7 @@ var accesscontrol = require('../../accesscontrol.js'),
database = require('../../database.js'),
domains = require('../../domains.js'),
expect = require('expect.js'),
hat = require('hat'),
hat = require('../../hat.js'),
nock = require('nock'),
oauth2 = require('../oauth2.js'),
querystring = require('querystring'),
@@ -219,7 +219,7 @@ describe('OAuth2', function () {
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.domain, APP_2.portBindings, APP_2),
appdb.add.bind(null, APP_3.id, APP_3.appStoreId, APP_3.manifest, APP_3.location, APP_3.domain, APP_3.portBindings, APP_3),
function (callback) {
users.create(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, null /* source */, function (error, userObject) {
users.create(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, { }, null /* source */, function (error, userObject) {
expect(error).to.not.be.ok();
// update the global objects to reflect the new user id
@@ -601,7 +601,7 @@ describe('OAuth2', function () {
});
},
function (callback) {
superagent.post(`${SERVER_URL}/api/v1/user/profile/twofactorauthentication`).query({ access_token: accessToken }).end(function (error, result) {
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication`).query({ access_token: accessToken }).end(function (error, result) {
secret = result.body.secret;
callback(error);
});
@@ -612,7 +612,7 @@ describe('OAuth2', function () {
encoding: 'base32'
});
superagent.post(`${SERVER_URL}/api/v1/user/profile/twofactorauthentication/enable`).query({ access_token: accessToken }).send({ totpToken: totpToken }).end(function (error, result) {
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication/enable`).query({ access_token: accessToken }).send({ totpToken: totpToken }).end(function (error, result) {
callback(error);
});
}
@@ -865,7 +865,7 @@ describe('OAuth2', function () {
expect(foo.token_type).to.eql('Bearer');
// Ensure the token is also usable
superagent.get(SERVER_URL + '/api/v1/user/profile?access_token=' + foo.access_token, function (error, result) {
superagent.get(SERVER_URL + '/api/v1/profile?access_token=' + foo.access_token, function (error, result) {
expect(error).to.not.be.ok();
expect(result.status).to.eql(200);
expect(result.body.username).to.equal(USER_0.username.toLowerCase());
@@ -1252,7 +1252,7 @@ describe('OAuth2', function () {
expect(body.token_type).to.eql('Bearer');
// Ensure the token is also usable
superagent.get(SERVER_URL + '/api/v1/user/profile?access_token=' + body.access_token, function (error, result) {
superagent.get(SERVER_URL + '/api/v1/profile?access_token=' + body.access_token, function (error, result) {
expect(error).to.not.be.ok();
expect(result.status).to.eql(200);
expect(result.body.username).to.equal(USER_0.username.toLowerCase());
@@ -1341,9 +1341,19 @@ describe('Password', function () {
});
});
it('setup succeeds', function (done) {
it('setup fails without email', function (done) {
superagent.get(SERVER_URL + '/api/v1/session/account/setup.html')
.query({ reset_token: USER_0.resetToken })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.text.indexOf('<!-- error tester -->')).to.not.equal(-1);
done();
});
});
it('setup succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/session/account/setup.html')
.query({ email: USER_0.email, reset_token: USER_0.resetToken })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.text.indexOf('<!-- tester -->')).to.not.equal(-1);
@@ -1361,7 +1371,16 @@ describe('Password', function () {
it('reset fails due to invalid reset_token', function (done) {
superagent.get(SERVER_URL + '/api/v1/session/password/reset.html')
.query({ reset_token: hat(256) })
.query({ email: USER_0.email, reset_token: hat(256) })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('reset fails due to invalid email', function (done) {
superagent.get(SERVER_URL + '/api/v1/session/password/reset.html')
.query({ email: USER_0.email + 'x', reset_token: hat(256) })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
@@ -1370,7 +1389,7 @@ describe('Password', function () {
it('reset succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/session/password/reset.html')
.query({ reset_token: USER_0.resetToken })
.query({ email: USER_0.email, reset_token: USER_0.resetToken })
.end(function (error, result) {
expect(result.text.indexOf('<!-- tester -->')).to.not.equal(-1);
expect(result.statusCode).to.equal(200);
@@ -1427,7 +1446,7 @@ describe('Password', function () {
it('fails due to empty password', function (done) {
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
.send({ password: '', resetToken: hat(256) })
.send({ password: '', email: USER_0.email, resetToken: hat(256) })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
@@ -1436,7 +1455,7 @@ describe('Password', function () {
it('fails due to empty resetToken', function (done) {
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
.send({ password: '', resetToken: '' })
.send({ password: '', email: USER_0.email, resetToken: '' })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
@@ -1445,7 +1464,7 @@ describe('Password', function () {
it('fails due to weak password', function (done) {
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
.send({ password: 'foobar', resetToken: USER_0.resetToken })
.send({ password: 'foobar', email: USER_0.email, resetToken: USER_0.resetToken })
.end(function (error, result) {
expect(result.statusCode).to.equal(406);
done();
@@ -1460,7 +1479,7 @@ describe('Password', function () {
.get('/').reply(200, {});
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
.send({ password: 'ASF23$%somepassword', resetToken: USER_0.resetToken })
.send({ password: '12345678', email: USER_0.email, resetToken: USER_0.resetToken })
.end(function (error, result) {
expect(scope.isDone()).to.be.ok();
expect(result.statusCode).to.equal(200);
+19 -19
View File
@@ -73,7 +73,7 @@ describe('Profile API', function () {
after(cleanup);
it('fails without token', function (done) {
superagent.get(SERVER_URL + '/api/v1/user/profile/').end(function (error, result) {
superagent.get(SERVER_URL + '/api/v1/profile/').end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
@@ -81,7 +81,7 @@ describe('Profile API', function () {
});
it('fails with empty token', function (done) {
superagent.get(SERVER_URL + '/api/v1/user/profile/').query({ access_token: '' }).end(function (error, result) {
superagent.get(SERVER_URL + '/api/v1/profile/').query({ access_token: '' }).end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
@@ -89,7 +89,7 @@ describe('Profile API', function () {
});
it('fails with invalid token', function (done) {
superagent.get(SERVER_URL + '/api/v1/user/profile/').query({ access_token: 'some token' }).end(function (error, result) {
superagent.get(SERVER_URL + '/api/v1/profile/').query({ access_token: 'some token' }).end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
@@ -97,7 +97,7 @@ describe('Profile API', function () {
});
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/user/profile/').query({ access_token: token_0 }).end(function (error, result) {
superagent.get(SERVER_URL + '/api/v1/profile/').query({ access_token: token_0 }).end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.username).to.equal(USERNAME_0.toLowerCase());
expect(result.body.email).to.equal(EMAIL_0.toLowerCase());
@@ -120,7 +120,7 @@ describe('Profile API', function () {
tokendb.add(token, user_0.id, null, expires, accesscontrol.SCOPE_ANY, function (error) {
expect(error).to.not.be.ok();
superagent.get(SERVER_URL + '/api/v1/user/profile').query({ access_token: token }).end(function (error, result) {
superagent.get(SERVER_URL + '/api/v1/profile').query({ access_token: token }).end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
@@ -129,14 +129,14 @@ describe('Profile API', function () {
});
it('fails with invalid token in auth header', function (done) {
superagent.get(SERVER_URL + '/api/v1/user/profile').set('Authorization', 'Bearer ' + 'x' + token_0).end(function (error, result) {
superagent.get(SERVER_URL + '/api/v1/profile').set('Authorization', 'Bearer ' + 'x' + token_0).end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('succeeds with token in auth header', function (done) {
superagent.get(SERVER_URL + '/api/v1/user/profile').set('Authorization', 'Bearer ' + token_0).end(function (error, result) {
superagent.get(SERVER_URL + '/api/v1/profile').set('Authorization', 'Bearer ' + token_0).end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.username).to.equal(USERNAME_0.toLowerCase());
expect(result.body.email).to.equal(EMAIL_0.toLowerCase());
@@ -154,7 +154,7 @@ describe('Profile API', function () {
after(cleanup);
it('change email fails due to missing token', function (done) {
superagent.post(SERVER_URL + '/api/v1/user/profile')
superagent.post(SERVER_URL + '/api/v1/profile')
.send({ email: EMAIL_0_NEW })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
@@ -163,7 +163,7 @@ describe('Profile API', function () {
});
it('change email fails due to invalid email', function (done) {
superagent.post(SERVER_URL + '/api/v1/user/profile')
superagent.post(SERVER_URL + '/api/v1/profile')
.query({ access_token: token_0 })
.send({ email: 'foo@bar' })
.end(function (error, result) {
@@ -173,7 +173,7 @@ describe('Profile API', function () {
});
it('change user succeeds without email nor displayName', function (done) {
superagent.post(SERVER_URL + '/api/v1/user/profile')
superagent.post(SERVER_URL + '/api/v1/profile')
.query({ access_token: token_0 })
.send({})
.end(function (error, result) {
@@ -183,13 +183,13 @@ describe('Profile API', function () {
});
it('change email succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/user/profile')
superagent.post(SERVER_URL + '/api/v1/profile')
.query({ access_token: token_0 })
.send({ email: EMAIL_0_NEW, fallbackEmail: EMAIL_0_NEW_FALLBACK })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
superagent.get(SERVER_URL + '/api/v1/user/profile')
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: token_0 })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
@@ -205,13 +205,13 @@ describe('Profile API', function () {
});
it('change displayName succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/user/profile')
superagent.post(SERVER_URL + '/api/v1/profile')
.query({ access_token: token_0 })
.send({ displayName: DISPLAY_NAME_0_NEW })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
superagent.get(SERVER_URL + '/api/v1/user/profile')
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: token_0 })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
@@ -231,7 +231,7 @@ describe('Profile API', function () {
after(cleanup);
it('fails due to missing current password', function (done) {
superagent.post(SERVER_URL + '/api/v1/user/profile/password')
superagent.post(SERVER_URL + '/api/v1/profile/password')
.query({ access_token: token_0 })
.send({ newPassword: 'some wrong password' })
.end(function (err, res) {
@@ -241,7 +241,7 @@ describe('Profile API', function () {
});
it('fails due to missing new password', function (done) {
superagent.post(SERVER_URL + '/api/v1/user/profile/password')
superagent.post(SERVER_URL + '/api/v1/profile/password')
.query({ access_token: token_0 })
.send({ password: PASSWORD })
.end(function (err, res) {
@@ -251,7 +251,7 @@ describe('Profile API', function () {
});
it('fails due to wrong password', function (done) {
superagent.post(SERVER_URL + '/api/v1/user/profile/password')
superagent.post(SERVER_URL + '/api/v1/profile/password')
.query({ access_token: token_0 })
.send({ password: 'some wrong password', newPassword: 'MOre#$%34' })
.end(function (err, res) {
@@ -261,7 +261,7 @@ describe('Profile API', function () {
});
it('fails due to invalid password', function (done) {
superagent.post(SERVER_URL + '/api/v1/user/profile/password')
superagent.post(SERVER_URL + '/api/v1/profile/password')
.query({ access_token: token_0 })
.send({ password: PASSWORD, newPassword: 'five' })
.end(function (err, res) {
@@ -271,7 +271,7 @@ describe('Profile API', function () {
});
it('succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/user/profile/password')
superagent.post(SERVER_URL + '/api/v1/profile/password')
.query({ access_token: token_0 })
.send({ password: PASSWORD, newPassword: 'MOre#$%34' })
.end(function (err, res) {
+2 -2
View File
@@ -127,7 +127,7 @@ describe('Users API', function () {
// stash for later use
token = res.body.token;
superagent.get(SERVER_URL + '/api/v1/user/profile').query({ access_token: token }).end(function (error, result) {
superagent.get(SERVER_URL + '/api/v1/profile').query({ access_token: token }).end(function (error, result) {
expect(error).to.eql(null);
expect(result.status).to.equal(200);
@@ -703,7 +703,7 @@ describe('Users API', function () {
});
it('can get profile of user with pre-set password', function (done) {
superagent.get(SERVER_URL + '/api/v1/user/profile')
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
+2 -3
View File
@@ -13,7 +13,6 @@ exports = module.exports = {
var assert = require('assert'),
constants = require('../constants.js'),
generatePassword = require('../password.js').generate,
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
users = require('../users.js'),
@@ -33,13 +32,13 @@ function create(req, res, next) {
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be string'));
var password = req.body.password || generatePassword();
var password = req.body.password || null;
var email = req.body.email;
var sendInvite = req.body.invite;
var username = 'username' in req.body ? req.body.username : null;
var displayName = req.body.displayName || '';
users.create(username, password, email, displayName, auditSource(req), { invitor: req.user, sendInvite: sendInvite }, function (error, user) {
users.create(username, password, email, displayName, { invitor: req.user, sendInvite: sendInvite }, auditSource(req), function (error, user) {
if (error && error.reason === UsersError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === UsersError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
+9 -10
View File
@@ -13,7 +13,7 @@ var accesscontrol = require('./accesscontrol.js'),
database = require('./database.js'),
eventlog = require('./eventlog.js'),
express = require('express'),
hat = require('hat'),
hat = require('./hat.js'),
http = require('http'),
middleware = require('./middleware'),
passport = require('passport'),
@@ -120,8 +120,8 @@ function initializeExpressSync() {
router.post('/api/v1/cloudron/reboot', cloudronScope, routes.cloudron.reboot);
router.get ('/api/v1/cloudron/graphs', cloudronScope, routes.graphs.getGraphs);
router.get ('/api/v1/cloudron/disks', cloudronScope, routes.cloudron.getDisks);
router.get ('/api/v1/cloudron/logs', cloudronScope, routes.cloudron.getLogs);
router.get ('/api/v1/cloudron/logstream', cloudronScope, routes.cloudron.getLogStream);
router.get ('/api/v1/cloudron/logs/:unit', cloudronScope, routes.cloudron.getLogs);
router.get ('/api/v1/cloudron/logstream/:unit', cloudronScope, routes.cloudron.getLogStream);
router.get ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, routes.ssh.getAuthorizedKeys);
router.put ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, routes.ssh.addAuthorizedKey);
router.get ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, routes.ssh.getAuthorizedKey);
@@ -131,13 +131,12 @@ function initializeExpressSync() {
// working off the user behind the provided token
router.get ('/api/v1/user/apps', profileScope, routes.apps.getAllByUser);
router.get ('/api/v1/user/cloudron_config', profileScope, routes.user.getCloudronConfig);
router.get ('/api/v1/profile', profileScope, routes.profile.get); // duplicate route for compatibility
router.get ('/api/v1/user/profile', profileScope, routes.profile.get);
router.post('/api/v1/user/profile', profileScope, routes.profile.update);
router.post('/api/v1/user/profile/password', profileScope, routes.users.verifyPassword, routes.profile.changePassword);
router.post('/api/v1/user/profile/twofactorauthentication', profileScope, routes.profile.setTwoFactorAuthenticationSecret);
router.post('/api/v1/user/profile/twofactorauthentication/enable', profileScope, routes.profile.enableTwoFactorAuthentication);
router.post('/api/v1/user/profile/twofactorauthentication/disable', profileScope, routes.users.verifyPassword, routes.profile.disableTwoFactorAuthentication);
router.get ('/api/v1/profile', profileScope, routes.profile.get);
router.post('/api/v1/profile', profileScope, routes.profile.update);
router.post('/api/v1/profile/password', profileScope, routes.users.verifyPassword, routes.profile.changePassword);
router.post('/api/v1/profile/twofactorauthentication', profileScope, routes.profile.setTwoFactorAuthenticationSecret);
router.post('/api/v1/profile/twofactorauthentication/enable', profileScope, routes.profile.enableTwoFactorAuthentication);
router.post('/api/v1/profile/twofactorauthentication/disable', profileScope, routes.users.verifyPassword, routes.profile.disableTwoFactorAuthentication);
// user routes
router.get ('/api/v1/users', usersScope, routes.users.list);
+29 -1
View File
@@ -35,6 +35,9 @@ exports = module.exports = {
getEmailDigest: getEmailDigest,
setEmailDigest: setEmailDigest,
getPlatformConfig: getPlatformConfig,
setPlatformConfig: setPlatformConfig,
getAll: getAll,
// booleans. if you add an entry here, be sure to fix getAll
@@ -46,6 +49,7 @@ exports = module.exports = {
UPDATE_CONFIG_KEY: 'update_config',
APPSTORE_CONFIG_KEY: 'appstore_config',
CAAS_CONFIG_KEY: 'caas_config',
PLATFORM_CONFIG_KEY: 'platform_config',
// strings
APP_AUTOUPDATE_PATTERN_KEY: 'app_autoupdate_pattern',
@@ -88,6 +92,7 @@ var gDefaults = (function () {
result[exports.APPSTORE_CONFIG_KEY] = {};
result[exports.CAAS_CONFIG_KEY] = {};
result[exports.EMAIL_DIGEST] = true;
result[exports.PLATFORM_CONFIG_KEY] = {};
return result;
})();
@@ -371,6 +376,29 @@ function getAppstoreConfig(callback) {
});
}
function getPlatformConfig(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.PLATFORM_CONFIG_KEY, function (error, value) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.PLATFORM_CONFIG_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
callback(null, JSON.parse(value));
});
}
function setPlatformConfig(platformConfig, callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.set(exports.PLATFORM_CONFIG_KEY, JSON.stringify(platformConfig), function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
exports.events.emit(exports.PLATFORM_CONFIG_KEY, platformConfig);
callback(null);
});
}
function setAppstoreConfig(appstoreConfig, callback) {
assert.strictEqual(typeof appstoreConfig, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -443,7 +471,7 @@ function getAll(callback) {
result[exports.DYNAMIC_DNS_KEY] = !!result[exports.DYNAMIC_DNS_KEY];
// convert JSON objects
[exports.BACKUP_CONFIG_KEY, exports.UPDATE_CONFIG_KEY, exports.APPSTORE_CONFIG_KEY ].forEach(function (key) {
[exports.BACKUP_CONFIG_KEY, exports.UPDATE_CONFIG_KEY, exports.APPSTORE_CONFIG_KEY, exports.PLATFORM_CONFIG_KEY ].forEach(function (key) {
result[key] = typeof result[key] === 'object' ? result[key] : safe.JSON.parse(result[key]);
});
+4 -5
View File
@@ -11,8 +11,7 @@ exports = module.exports = {
SetupError: SetupError
};
var accesscontrol = require('./accesscontrol.js'),
assert = require('assert'),
var assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
BackupsError = require('./backups.js').BackupsError,
@@ -33,7 +32,6 @@ var accesscontrol = require('./accesscontrol.js'),
semver = require('semver'),
settingsdb = require('./settingsdb.js'),
settings = require('./settings.js'),
SettingsError = settings.SettingsError,
shell = require('./shell.js'),
superagent = require('superagent'),
sysinfo = require('./sysinfo.js'),
@@ -176,6 +174,7 @@ function dnsSetup(adminFqdn, domain, zoneName, provider, dnsConfig, tlsConfig, c
function done(error) {
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
if (error && error.reason === DomainsError.ALREADY_EXISTS) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
config.setAdminDomain(domain); // set fqdn only after dns config is valid, otherwise cannot re-setup if we failed
@@ -192,9 +191,9 @@ function dnsSetup(adminFqdn, domain, zoneName, provider, dnsConfig, tlsConfig, c
}
domains.get(domain, function (error, result) {
if (error && error.reason !== DomainsError.NOT_FOUND) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (error && error.reason !== DomainsError.NOT_FOUND) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
if (result) return callback(new SettingsError(SettingsError.ALREADY_EXISTS, 'domain already exists'));
if (result) return callback(new SetupError(SetupError.BAD_STATE, 'Domain already exists'));
async.series([
domains.add.bind(null, domain, zoneName, provider, dnsConfig, null /* cert */, tlsConfig),
-1
View File
@@ -10,7 +10,6 @@ exports = module.exports = {
var assert = require('assert'),
child_process = require('child_process'),
debug = require('debug')('box:shell'),
fs = require('fs'),
once = require('once'),
util = require('util');
+2
View File
@@ -162,6 +162,8 @@ function testConfig(apiConfig, callback) {
if ('noHardlinks' in apiConfig && typeof apiConfig.noHardlinks !== 'boolean') return callback(new BackupsError(BackupsError.BAD_FIELD, 'noHardlinks must be boolean'));
if ('externalDisk' in apiConfig && typeof apiConfig.externalDisk !== 'boolean') return callback(new BackupsError(BackupsError.BAD_FIELD, 'externalDisk must be boolean'));
fs.stat(apiConfig.backupFolder, function (error, result) {
if (error) return callback(new BackupsError(BackupsError.BAD_FIELD, 'Directory does not exist or cannot be accessed: ' + error.message));
if (!result.isDirectory()) return callback(new BackupsError(BackupsError.BAD_FIELD, 'Backup location is not a directory'));
+34 -17
View File
@@ -17,7 +17,11 @@ var appdb = require('./appdb.js'),
async = require('async'),
child_process = require('child_process'),
debug = require('debug')('box:taskmanager'),
fs = require('fs'),
locker = require('./locker.js'),
mkdirp = require('mkdirp'),
path = require('path'),
paths = require('./paths.js'),
sendFailureLogs = require('./logcollector.js').sendFailureLogs,
util = require('util'),
_ = require('underscore');
@@ -122,26 +126,39 @@ function startAppTask(appId, callback) {
return callback();
}
// when parent process dies, apptask processes are killed because KillMode=control-group in systemd unit file
gActiveTasks[appId] = child_process.fork(__dirname + '/apptask.js', [ appId ]);
// ensure log folder
mkdirp.sync(path.join(paths.LOG_DIR, appId));
var logFilePath = path.join(paths.LOG_DIR, appId, 'apptask.log');
var pid = gActiveTasks[appId].pid;
debug('Started task of %s pid: %s', appId, pid);
gActiveTasks[appId].once('exit', function (code, signal) {
debug('Task for %s pid %s completed with status %s', appId, pid, code);
if (code === null /* signal */ || (code !== 0 && code !== 50)) { // apptask crashed
debug('Apptask crashed with code %s and signal %s', code, signal);
sendFailureLogs('apptask', { unit: 'box' });
appdb.update(appId, { installationState: appdb.ISTATE_ERROR, installationProgress: 'Apptask crashed with code ' + code + ' and signal ' + signal }, NOOP_CALLBACK);
} else if (code === 50) {
sendFailureLogs('apptask', { unit: 'box' });
// will autoclose
fs.open(logFilePath, 'a', function (error, fd) {
if (error) {
debug('Unable to open log file, queueing task for %s', appId, error);
gPendingTasks.push(appId);
return callback();
}
delete gActiveTasks[appId];
locker.unlock(locker.OP_APPTASK); // unlock event will trigger next task
});
callback();
// when parent process dies, apptask processes are killed because KillMode=control-group in systemd unit file
gActiveTasks[appId] = child_process.fork(__dirname + '/apptask.js', [ appId ], { stdio: [ 'pipe', fd, fd, 'ipc' ]});
var pid = gActiveTasks[appId].pid;
debug('Started task of %s pid: %s. See logs at %s', appId, pid, logFilePath);
gActiveTasks[appId].once('exit', function (code, signal) {
debug('Task for %s pid %s completed with status %s', appId, pid, code);
if (code === null /* signal */ || (code !== 0 && code !== 50)) { // apptask crashed
debug('Apptask crashed with code %s and signal %s', code, signal);
sendFailureLogs('apptask', { unit: 'box' });
appdb.update(appId, { installationState: appdb.ISTATE_ERROR, installationProgress: 'Apptask crashed with code ' + code + ' and signal ' + signal }, NOOP_CALLBACK);
} else if (code === 50) {
sendFailureLogs('apptask', { unit: 'box' });
}
delete gActiveTasks[appId];
locker.unlock(locker.OP_APPTASK); // unlock event will trigger next task
});
callback();
});
}
function stopAppTask(appId, callback) {
+15 -11
View File
@@ -16,7 +16,7 @@ var appdb = require('../appdb.js'),
expect = require('expect.js'),
groupdb = require('../groupdb.js'),
groups = require('../groups.js'),
hat = require('hat'),
hat = require('../hat.js'),
settings = require('../settings.js'),
settingsdb = require('../settingsdb.js'),
userdb = require('../userdb.js');
@@ -102,7 +102,9 @@ describe('Apps', function () {
},
portBindings: { PORT: 5678 },
accessRestriction: null,
memoryLimit: 0
memoryLimit: 0,
robotsTxt: null,
sso: false
};
var APP_1 = {
@@ -114,7 +116,7 @@ describe('Apps', function () {
version: '0.1', dockerImage: 'docker/app1', healthCheckPath: '/', httpPort: 80, title: 'app1',
tcpPorts: {}
},
portBindings: null,
portBindings: {},
accessRestriction: { users: [ 'someuser' ], groups: [ GROUP_0.id ] },
memoryLimit: 0
};
@@ -128,9 +130,11 @@ describe('Apps', function () {
version: '0.1', dockerImage: 'docker/app2', healthCheckPath: '/', httpPort: 80, title: 'app2',
tcpPorts: {}
},
portBindings: null,
portBindings: {},
accessRestriction: { users: [ 'someuser', USER_0.id ], groups: [ GROUP_1.id ] },
memoryLimit: 0
memoryLimit: 0,
robotsTxt: null,
sso: false
};
before(function (done) {
@@ -409,12 +413,12 @@ describe('Apps', function () {
apps.restoreInstalledApps(function (error) {
expect(error).to.be(null);
apps.getAll(function (error, apps) {
expect(apps[0].installationState).to.be(appdb.ISTATE_PENDING_RESTORE);
expect(apps[0].oldConfig).to.be(null);
expect(apps[1].installationState).to.be(appdb.ISTATE_PENDING_RESTORE);
expect(apps[2].installationState).to.be(appdb.ISTATE_PENDING_RESTORE);
expect(apps[2].oldConfig).to.be(null);
apps.getAll(function (error, result) {
expect(result[0].installationState).to.be(appdb.ISTATE_PENDING_RESTORE);
expect(result[0].oldConfig).to.eql(apps.getAppConfig(APP_0));
expect(result[1].installationState).to.be(appdb.ISTATE_PENDING_RESTORE);
expect(result[2].installationState).to.be(appdb.ISTATE_PENDING_RESTORE);
expect(result[2].oldConfig).to.eql(apps.getAppConfig(APP_2));
done();
});
+7 -2
View File
@@ -125,13 +125,18 @@ describe('Appstore', function () {
});
it('can purchase an app', function (done) {
var scope = nock('http://localhost:6060')
var scope1 = nock('http://localhost:6060')
.post(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons/${CLOUDRON_ID}/apps/${APP_ID}?accessToken=${APPSTORE_TOKEN}`, function () { return true; })
.reply(201, {});
var scope2 = nock('http://localhost:6060')
.get(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons/${CLOUDRON_ID}/subscription?accessToken=${APPSTORE_TOKEN}`, function () { return true; })
.reply(200, { subscription: { id: 'basic' }});
appstore.purchase(APP_ID, APPSTORE_APP_ID, function (error) {
expect(error).to.not.be.ok();
expect(scope.isDone()).to.be.ok();
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
done();
});
+2 -2
View File
@@ -239,7 +239,7 @@ describe('apptask', function () {
nock.cleanAll();
var awsScope = nock('http://localhost:5353')
.get('/2013-04-01/hostedzone')
.get('/2013-04-01/hostedzonesbyname?dnsname=example.com.&maxitems=1')
.times(2)
.reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { wrapHandlers: { HostedZones: () => 'HostedZone'} }))
.get('/2013-04-01/hostedzone/ZONEID/rrset?maxitems=1&name=applocation.' + DOMAIN_0.domain + '.&type=A')
@@ -258,7 +258,7 @@ describe('apptask', function () {
nock.cleanAll();
var awsScope = nock('http://localhost:5353')
.get('/2013-04-01/hostedzone')
.get('/2013-04-01/hostedzonesbyname?dnsname=example.com.&maxitems=1')
.reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { wrapHandlers: { HostedZones: () => 'HostedZone'} }))
.post('/2013-04-01/hostedzone/ZONEID/rrset/')
.reply(200, js2xml('ChangeResourceRecordSetsResponse', { ChangeInfo: { Id: 'RRID', Status: 'INSYNC' } }));
+15 -4
View File
@@ -17,7 +17,7 @@ var appdb = require('../appdb.js'),
eventlogdb = require('../eventlogdb.js'),
expect = require('expect.js'),
groupdb = require('../groupdb.js'),
hat = require('hat'),
hat = require('../hat.js'),
mailboxdb = require('../mailboxdb.js'),
maildb = require('../maildb.js'),
settingsdb = require('../settingsdb.js'),
@@ -95,6 +95,8 @@ const TEST_DOMAIN = {
};
describe('database', function () {
this.timeout(5000);
before(function (done) {
config._reset();
config.setFqdn(TEST_DOMAIN.domain);
@@ -313,8 +315,8 @@ describe('database', function () {
});
});
it('can get by resetToken fails for empty resetToken', function (done) {
userdb.getByResetToken('', function (error, user) {
it('getByResetToken fails for empty resetToken', function (done) {
userdb.getByResetToken(USER_0.email, '', function (error, user) {
expect(error).to.be.ok();
expect(error.reason).to.be(DatabaseError.INTERNAL_ERROR);
expect(user).to.not.be.ok();
@@ -322,8 +324,17 @@ describe('database', function () {
});
});
it('getByResetToken fails for bad email', function (done) {
userdb.getByResetToken(USER_0.email + 'x', USER_0.resetToken, function (error, user) {
expect(error).to.be.ok();
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
expect(user).to.not.be.ok();
done();
});
});
it('can get by resetToken', function (done) {
userdb.getByResetToken(USER_0.resetToken, function (error, user) {
userdb.getByResetToken(USER_0.email, USER_0.resetToken, function (error, user) {
expect(error).to.not.be.ok();
expect(user).to.eql(USER_0);
done();
+3 -3
View File
@@ -123,7 +123,7 @@ describe('digest', function () {
digest.maybeSend(function (error) {
if (error) return done(error);
checkMails(1, `${USER_0.fallbackEmail}, ${USER_0.email}`, done);
checkMails(1, `${USER_0.email}`, done);
});
});
@@ -133,7 +133,7 @@ describe('digest', function () {
digest.maybeSend(function (error) {
if (error) return done(error);
checkMails(1, `${USER_0.fallbackEmail}, ${USER_0.email}`, done);
checkMails(1, `${USER_0.email}`, done);
});
});
@@ -146,7 +146,7 @@ describe('digest', function () {
digest.maybeSend(function (error) {
if (error) return done(error);
checkMails(1, `${USER_0.fallbackEmail}, ${USER_0.email}`, done);
checkMails(1, `${USER_0.email}`, done);
});
});
});
+261 -5
View File
@@ -51,7 +51,7 @@ describe('dns provider', function () {
DOMAIN_0.provider = 'noop';
DOMAIN_0.config = {};
domains.update(DOMAIN_0.domain, DOMAIN_0.provider, DOMAIN_0.config, null, DOMAIN_0.tlsConfig, done);
domains.update(DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, null, DOMAIN_0.tlsConfig, done);
});
it('upsert succeeds', function (done) {
@@ -92,7 +92,7 @@ describe('dns provider', function () {
token: TOKEN
};
domains.update(DOMAIN_0.domain, DOMAIN_0.provider, DOMAIN_0.config, null, DOMAIN_0.tlsConfig, done);
domains.update(DOMAIN_0.domain, '', DOMAIN_0.provider, DOMAIN_0.config, null, DOMAIN_0.tlsConfig, done);
});
it('upsert non-existing record succeeds', function (done) {
@@ -341,6 +341,262 @@ describe('dns provider', function () {
});
});
describe('godaddy', function () {
var KEY = 'somekey', SECRET = 'somesecret';
var GODADDY_API = 'https://api.godaddy.com/v1/domains';
before(function (done) {
DOMAIN_0.provider = 'godaddy';
DOMAIN_0.config = {
apiKey: KEY,
apiSecret: SECRET
};
domains.update(DOMAIN_0.domain, '', DOMAIN_0.provider, DOMAIN_0.config, null, DOMAIN_0.tlsConfig, done);
});
it('upsert record succeeds', function (done) {
nock.cleanAll();
var DOMAIN_RECORD_0 = [{
ttl: 600,
data: '1.2.3.4'
}];
var req1 = nock(GODADDY_API)
.put('/' + DOMAIN_0.zoneName + '/records/A/test', DOMAIN_RECORD_0)
.reply(200, { });
domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error) {
expect(error).to.eql(null);
expect(req1.isDone()).to.be.ok();
done();
});
});
it('get succeeds', function (done) {
nock.cleanAll();
var DOMAIN_RECORD_0 = [{
ttl: 600,
data: '1.2.3.4'
}];
var req1 = nock(GODADDY_API)
.get('/' + DOMAIN_0.zoneName + '/records/A/test')
.reply(200, DOMAIN_RECORD_0);
domains.getDnsRecords('test', DOMAIN_0.domain, 'A', function (error, result) {
expect(error).to.eql(null);
expect(result).to.be.an(Array);
expect(result.length).to.eql(1);
expect(result[0]).to.eql(DOMAIN_RECORD_0[0].data);
expect(req1.isDone()).to.be.ok();
done();
});
});
it('del succeeds', function (done) {
nock.cleanAll();
var DOMAIN_RECORD_0 = [{ // existing
ttl: 600,
data: '1.2.3.4'
}];
var DOMAIN_RECORD_1 = [{ // replaced
ttl: 600,
data: '0.0.0.0'
}];
var req1 = nock(GODADDY_API)
.get('/' + DOMAIN_0.zoneName + '/records/A/test')
.reply(200, DOMAIN_RECORD_0);
var req2 = nock(GODADDY_API)
.put('/' + DOMAIN_0.zoneName + '/records/A/test', DOMAIN_RECORD_1)
.reply(200, { });
domains.removeDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) {
expect(error).to.eql(null);
expect(req1.isDone()).to.be.ok();
expect(req2.isDone()).to.be.ok();
done();
});
});
});
describe('gandi', function () {
var TOKEN = 'sometoken';
var GANDI_API = 'https://dns.api.gandi.net/api/v5';
before(function (done) {
DOMAIN_0.provider = 'gandi';
DOMAIN_0.config = {
token: TOKEN
};
domains.update(DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, null, DOMAIN_0.tlsConfig, done);
});
it('upsert record succeeds', function (done) {
nock.cleanAll();
var DOMAIN_RECORD_0 = {
'rrset_ttl': 300,
'rrset_values': [ '1.2.3.4' ]
};
var req1 = nock(GANDI_API)
.put('/domains/' + DOMAIN_0.zoneName + '/records/test/A', DOMAIN_RECORD_0)
.reply(201, { message: 'Zone Record Created' });
domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error) {
expect(error).to.eql(null);
expect(req1.isDone()).to.be.ok();
done();
});
});
it('get succeeds', function (done) {
nock.cleanAll();
var DOMAIN_RECORD_0 = {
'rrset_type': 'A',
'rrset_ttl': 600,
'rrset_name': 'test',
'rrset_values': [ '1.2.3.4' ]
};
var req1 = nock(GANDI_API)
.get('/domains/' + DOMAIN_0.zoneName + '/records/test/A')
.reply(200, DOMAIN_RECORD_0);
domains.getDnsRecords('test', DOMAIN_0.domain, 'A', function (error, result) {
expect(error).to.eql(null);
expect(result).to.be.an(Array);
expect(result.length).to.eql(1);
expect(result[0]).to.eql(DOMAIN_RECORD_0.rrset_values[0]);
expect(req1.isDone()).to.be.ok();
done();
});
});
it('del succeeds', function (done) {
nock.cleanAll();
var req2 = nock(GANDI_API)
.delete('/domains/' + DOMAIN_0.zoneName + '/records/test/A')
.reply(204, { });
domains.removeDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) {
expect(error).to.eql(null);
expect(req2.isDone()).to.be.ok();
done();
});
});
});
describe('name.com', function () {
const TOKEN = 'sometoken';
const NAMECOM_API = 'https://api.name.com/v4';
before(function (done) {
DOMAIN_0.provider = 'namecom';
DOMAIN_0.config = {
token: TOKEN
};
domains.update(DOMAIN_0.domain, '', DOMAIN_0.provider, DOMAIN_0.config, null, DOMAIN_0.tlsConfig, done);
});
it('upsert record succeeds', function (done) {
nock.cleanAll();
var DOMAIN_RECORD_0 = {
host: 'test',
type: 'A',
answer: '1.2.3.4',
ttl: 300
};
var req1 = nock(NAMECOM_API)
.get(`/domains/${DOMAIN_0.zoneName}/records`)
.reply(200, { records: [] });
var req2 = nock(NAMECOM_API)
.post(`/domains/${DOMAIN_0.zoneName}/records`, DOMAIN_RECORD_0)
.reply(200, {});
domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error) {
expect(error).to.eql(null);
expect(req1.isDone()).to.be.ok();
expect(req2.isDone()).to.be.ok();
done();
});
});
it('get succeeds', function (done) {
nock.cleanAll();
var DOMAIN_RECORD_0 = {
host: 'test',
type: 'A',
answer: '1.2.3.4',
ttl: 300
};
var req1 = nock(NAMECOM_API)
.get(`/domains/${DOMAIN_0.zoneName}/records`)
.reply(200, { records: [ DOMAIN_RECORD_0 ] });
domains.getDnsRecords('test', DOMAIN_0.domain, 'A', function (error, result) {
expect(error).to.eql(null);
expect(result).to.be.an(Array);
expect(result.length).to.eql(1);
expect(result[0]).to.eql(DOMAIN_RECORD_0.answer);
expect(req1.isDone()).to.be.ok();
done();
});
});
it('del succeeds', function (done) {
nock.cleanAll();
var DOMAIN_RECORD_0 = {
id: 'someid',
host: 'test',
type: 'A',
answer: '1.2.3.4',
ttl: 300
};
var req1 = nock(NAMECOM_API)
.get(`/domains/${DOMAIN_0.zoneName}/records`)
.reply(200, { records: [ DOMAIN_RECORD_0 ] });
var req2 = nock(NAMECOM_API)
.delete(`/domains/${DOMAIN_0.zoneName}/records/${DOMAIN_RECORD_0.id}`)
.reply(200, {});
domains.removeDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) {
expect(error).to.eql(null);
expect(req1.isDone()).to.be.ok();
expect(req2.isDone()).to.be.ok();
done();
});
});
});
describe('route53', function () {
// do not clear this with [] but .length = 0 so we don't loose the reference in mockery
var awsAnswerQueue = [];
@@ -412,14 +668,14 @@ describe('dns provider', function () {
Route53Mock.prototype.getChange = mockery(awsAnswerQueue);
Route53Mock.prototype.changeResourceRecordSets = mockery(awsAnswerQueue);
Route53Mock.prototype.listResourceRecordSets = mockery(awsAnswerQueue);
Route53Mock.prototype.listHostedZones = mockery(awsAnswerQueue);
Route53Mock.prototype.listHostedZonesByName = mockery(awsAnswerQueue);
// override route53 in AWS
// Comment this out and replace the config with real tokens to test against AWS proper
AWS._originalRoute53 = AWS.Route53;
AWS.Route53 = Route53Mock;
domains.update(DOMAIN_0.domain, DOMAIN_0.provider, DOMAIN_0.config, null, DOMAIN_0.tlsConfig, done);
domains.update(DOMAIN_0.domain, '', DOMAIN_0.provider, DOMAIN_0.config, null, DOMAIN_0.tlsConfig, done);
});
after(function () {
@@ -575,7 +831,7 @@ describe('dns provider', function () {
_OriginalGCDNS = GCDNS.prototype.getZones;
GCDNS.prototype.getZones = mockery(zoneQueue);
domains.update(DOMAIN_0.domain, DOMAIN_0.provider, DOMAIN_0.config, null, DOMAIN_0.tlsConfig, done);
domains.update(DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, null, DOMAIN_0.tlsConfig, done);
});
after(function () {
+1 -1
View File
@@ -14,7 +14,7 @@ var async = require('async'),
expect = require('expect.js'),
groups = require('../groups.js'),
GroupsError = groups.GroupsError,
hat = require('hat'),
hat = require('../hat.js'),
mailboxdb = require('../mailboxdb.js'),
userdb = require('../userdb.js');
+2 -2
View File
@@ -115,7 +115,7 @@ function setup(done) {
});
},
function (callback) {
users.create(USER_1.username, USER_1.password, USER_1.email, USER_0.displayName, AUDIT_SOURCE, { invitor: USER_0 }, function (error, result) {
users.create(USER_1.username, USER_1.password, USER_1.email, USER_0.displayName, { invitor: USER_0 }, AUDIT_SOURCE, function (error, result) {
if (error) return callback(error);
USER_1.id = result.id;
@@ -124,7 +124,7 @@ function setup(done) {
});
},
function (callback) {
users.create(USER_2.username, USER_2.password, USER_2.email, USER_0.displayName, AUDIT_SOURCE, { invitor: USER_0 }, function (error, result) {
users.create(USER_2.username, USER_2.password, USER_2.email, USER_0.displayName, { invitor: USER_0 }, AUDIT_SOURCE, function (error, result) {
if (error) return callback(error);
USER_2.id = result.id;
+26 -2
View File
@@ -2,6 +2,7 @@
/* global describe:false */
/* global before:false */
/* global after:false */
/* global beforeEach:false */
'use strict';
@@ -11,7 +12,10 @@ var async = require('async'),
domains = require('../domains.js'),
expect = require('expect.js'),
mail = require('../mail.js'),
maildb = require('../maildb.js');
MailError = mail.MailError,
maildb = require('../maildb.js'),
nock = require('nock'),
settings = require('../settings.js');
const DOMAIN_0 = {
domain: 'example.com',
@@ -22,6 +26,10 @@ const DOMAIN_0 = {
tlsConfig: { provider: 'fallback' }
};
const APPSTORE_USER_ID = 'appstoreuserid';
const APPSTORE_TOKEN = 'appstoretoken';
const CLOUDRON_ID = 'cloudronid';
function setup(done) {
config._reset();
config.set('fqdn', 'example.com');
@@ -30,13 +38,27 @@ function setup(done) {
async.series([
database.initialize,
database._clear,
settings.initialize,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig),
mail.addDomain.bind(null, DOMAIN_0.domain)
mail.addDomain.bind(null, DOMAIN_0.domain),
function (callback) {
var scope = nock('http://localhost:6060')
.post(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons?accessToken=${APPSTORE_TOKEN}`, function () { return true; })
.reply(201, { cloudron: { id: CLOUDRON_ID }});
settings.setAppstoreConfig({ userId: APPSTORE_USER_ID, token: APPSTORE_TOKEN }, function (error) {
expect(error).to.not.be.ok();
expect(scope.isDone()).to.be.ok();
callback();
});
}
], done);
}
function cleanup(done) {
async.series([
settings.uninitialize,
database._clear,
database.uninitialize
], done);
@@ -46,6 +68,8 @@ describe('Mail', function () {
before(setup);
after(cleanup);
beforeEach(nock.cleanAll);
describe('values', function () {
it('can get default', function (done) {
mail.getDomain(DOMAIN_0.domain, function (error, mailConfig) {
+3 -3
View File
@@ -121,7 +121,7 @@ describe('Certificates', function () {
async.series([
setup,
domains.update.bind(null, DOMAIN_0.domain, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig)
domains.update.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig)
], done);
});
@@ -152,7 +152,7 @@ describe('Certificates', function () {
async.series([
setup,
domains.update.bind(null, DOMAIN_0.domain, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig)
domains.update.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig)
], done);
});
@@ -183,7 +183,7 @@ describe('Certificates', function () {
async.series([
setup,
domains.update.bind(null, DOMAIN_0.domain, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig)
domains.update.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig)
], done);
});
+12 -65
View File
@@ -119,37 +119,7 @@ describe('User', function () {
after(cleanupUsers);
it('fails due to short password', function (done) {
users.create(USERNAME, 'Fo$%23', EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
expect(error).to.be.ok();
expect(result).to.not.be.ok();
expect(error.reason).to.equal(UsersError.BAD_FIELD);
done();
});
});
it('fails due to missing upper case password', function (done) {
users.create(USERNAME, 'thisiseightch%$234arslong', EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
expect(error).to.be.ok();
expect(result).to.not.be.ok();
expect(error.reason).to.equal(UsersError.BAD_FIELD);
done();
});
});
it('fails due to missing numerics in password', function (done) {
users.create(USERNAME, 'foobaRASDF%', EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
expect(error).to.be.ok();
expect(result).to.not.be.ok();
expect(error.reason).to.equal(UsersError.BAD_FIELD);
done();
});
});
it('fails due to missing special chars in password', function (done) {
users.create(USERNAME, 'foobaRASDF23423', EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
users.create(USERNAME, 'Fo$%23', EMAIL, DISPLAY_NAME, { }, AUDIT_SOURCE, function (error, result) {
expect(error).to.be.ok();
expect(result).to.not.be.ok();
expect(error.reason).to.equal(UsersError.BAD_FIELD);
@@ -159,7 +129,7 @@ describe('User', function () {
});
it('fails due to reserved username', function (done) {
users.create('admin', PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
users.create('admin', PASSWORD, EMAIL, DISPLAY_NAME, { }, AUDIT_SOURCE, function (error, result) {
expect(error).to.be.ok();
expect(result).to.not.be.ok();
expect(error.reason).to.equal(UsersError.BAD_FIELD);
@@ -169,7 +139,7 @@ describe('User', function () {
});
it('fails due to invalid username', function (done) {
users.create('moo-daemon', PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
users.create('moo+daemon', PASSWORD, EMAIL, DISPLAY_NAME, { }, AUDIT_SOURCE, function (error, result) {
expect(error).to.be.ok();
expect(result).to.not.be.ok();
expect(error.reason).to.equal(UsersError.BAD_FIELD);
@@ -179,7 +149,7 @@ describe('User', function () {
});
it('fails due to short username', function (done) {
users.create('', PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
users.create('', PASSWORD, EMAIL, DISPLAY_NAME, { }, AUDIT_SOURCE, function (error, result) {
expect(error).to.be.ok();
expect(result).to.not.be.ok();
expect(error.reason).to.equal(UsersError.BAD_FIELD);
@@ -189,7 +159,7 @@ describe('User', function () {
});
it('fails due to long username', function (done) {
users.create(new Array(257).fill('Z').join(''), PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
users.create(new Array(257).fill('Z').join(''), PASSWORD, EMAIL, DISPLAY_NAME, { }, AUDIT_SOURCE, function (error, result) {
expect(error).to.be.ok();
expect(result).to.not.be.ok();
expect(error.reason).to.equal(UsersError.BAD_FIELD);
@@ -198,8 +168,8 @@ describe('User', function () {
});
});
it('fails due to reserved pattern', function (done) {
users.create('maybe-app', PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
it('fails due to reserved app pattern', function (done) {
users.create('maybe.app', PASSWORD, EMAIL, DISPLAY_NAME, { }, AUDIT_SOURCE, function (error, result) {
expect(error).to.be.ok();
expect(result).to.not.be.ok();
expect(error.reason).to.equal(UsersError.BAD_FIELD);
@@ -221,31 +191,8 @@ describe('User', function () {
});
});
it('fails because of invalid BAD_FIELD', function (done) {
expect(function () {
users.create(EMAIL, {}, function () {});
}).to.throwException();
expect(function () {
users.create(12345, PASSWORD, EMAIL, function () {});
}).to.throwException();
expect(function () {
users.create(USERNAME, PASSWORD, EMAIL, {});
}).to.throwException();
expect(function () {
users.create(USERNAME, PASSWORD, EMAIL, {}, function () {});
}).to.throwException();
expect(function () {
users.create(USERNAME, PASSWORD, EMAIL, {});
}).to.throwException();
expect(function () {
users.create(USERNAME, PASSWORD, EMAIL, false, null, 'foobar');
}).to.throwException();
done();
});
it('fails because user exists', function (done) {
users.create(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
users.create(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, { }, AUDIT_SOURCE, function (error, result) {
expect(error).to.be.ok();
expect(result).not.to.be.ok();
expect(error.reason).to.equal(UsersError.ALREADY_EXISTS);
@@ -255,7 +202,7 @@ describe('User', function () {
});
it('fails because password is empty', function (done) {
users.create(USERNAME, '', EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
users.create(USERNAME, '', EMAIL, DISPLAY_NAME, { }, AUDIT_SOURCE, function (error, result) {
expect(error).to.be.ok();
expect(result).not.to.be.ok();
expect(error.reason).to.equal(UsersError.BAD_FIELD);
@@ -269,7 +216,7 @@ describe('User', function () {
maildb.update(DOMAIN_0.domain, { enabled: true }, function (error) {
expect(error).not.to.be.ok();
users.create(USERNAME_1, PASSWORD_1, EMAIL_1, DISPLAY_NAME_1, AUDIT_SOURCE, { sendInvite: true }, function (error, result) {
users.create(USERNAME_1, PASSWORD_1, EMAIL_1, DISPLAY_NAME_1, { sendInvite: true }, AUDIT_SOURCE, function (error, result) {
expect(error).not.to.be.ok();
expect(result).to.be.ok();
expect(result.username).to.equal(USERNAME_1.toLowerCase());
@@ -720,7 +667,7 @@ describe('User', function () {
it('make second user admin succeeds', function (done) {
var invitor = { username: USERNAME, email: EMAIL };
users.create(user1.username, user1.password, user1.email, DISPLAY_NAME, AUDIT_SOURCE, { invitor: invitor }, function (error, result) {
users.create(user1.username, user1.password, user1.email, DISPLAY_NAME, { invitor: invitor }, AUDIT_SOURCE, function (error, result) {
expect(error).to.not.be.ok();
expect(result).to.be.ok();
@@ -773,7 +720,7 @@ describe('User', function () {
};
var invitor = { username: USERNAME, email: EMAIL };
users.create(user1.username, user1.password, user1.email, DISPLAY_NAME, AUDIT_SOURCE, { invitor: invitor }, function (error, result) {
users.create(user1.username, user1.password, user1.email, DISPLAY_NAME, { invitor: invitor }, AUDIT_SOURCE, function (error, result) {
expect(error).to.eql(null);
expect(result).to.be.ok();
+1 -2
View File
@@ -20,8 +20,7 @@ exports = module.exports = {
var assert = require('assert'),
database = require('./database.js'),
DatabaseError = require('./databaseerror'),
hat = require('hat');
hat = require('./hat.js');
var TOKENS_FIELDS = [ 'accessToken', 'identifier', 'clientId', 'scope', 'expires' ].join(',');
+2 -2
View File
@@ -105,7 +105,7 @@ function checkAppUpdates(callback) {
}
// always send notifications if user is on the free plan
if (result.plan.id === 'free' || result.plan.id === 'undecided') {
if (appstore.isFreePlan(result)) {
debug('Notifying user of app update for %s from %s to %s', app.id, app.manifest.version, updateInfo.manifest.version);
mailer.appUpdateAvailable(app, false /* subscription */, updateInfo);
return iteratorDone();
@@ -162,7 +162,7 @@ function checkBoxUpdates(callback) {
}
// always send notifications if user is on the free plan
if (result.plan.id === 'free' || result.plan.id === 'undecided') {
if (appstore.isFreePlan(result)) {
mailer.boxUpdateAvailable(false /* hasSubscription */, updateInfo.version, updateInfo.changelog);
return done();
}
+3 -2
View File
@@ -82,13 +82,14 @@ function getOwner(callback) {
});
}
function getByResetToken(resetToken, callback) {
function getByResetToken(email, resetToken, callback) {
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof resetToken, 'string');
assert.strictEqual(typeof callback, 'function');
if (resetToken.length === 0) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, 'Empty resetToken not allowed'));
database.query('SELECT ' + USERS_FIELDS + ' FROM users WHERE resetToken=?', [ resetToken ], function (error, result) {
database.query('SELECT ' + USERS_FIELDS + ' FROM users WHERE email=? AND resetToken=?', [ email, resetToken ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
+46 -32
View File
@@ -5,14 +5,14 @@ exports = module.exports = {
removePrivateFields: removePrivateFields,
list: listUsers,
create: createUser,
list: list,
create: create,
count: count,
verify: verify,
verifyWithUsername: verifyWithUsername,
verifyWithEmail: verifyWithEmail,
remove: removeUser,
get: getUser,
get: get,
getByResetToken: getByResetToken,
getAllAdmins: getAllAdmins,
resetPasswordByIdentifier: resetPasswordByIdentifier,
@@ -37,7 +37,7 @@ var assert = require('assert'),
groupdb = require('./groupdb.js'),
groups = require('./groups.js'),
GroupsError = groups.GroupsError,
hat = require('hat'),
hat = require('./hat.js'),
mailer = require('./mailer.js'),
qrcode = require('qrcode'),
safe = require('safetydance'),
@@ -45,7 +45,6 @@ var assert = require('assert'),
userdb = require('./userdb.js'),
util = require('util'),
uuid = require('uuid'),
validatePassword = require('./password.js').validate,
validator = require('validator'),
_ = require('underscore');
@@ -91,8 +90,8 @@ function validateUsername(username) {
if (constants.RESERVED_NAMES.indexOf(username) !== -1) return new UsersError(UsersError.BAD_FIELD, 'Username is reserved');
// +/- can be tricky in emails. also need to consider valid LDAP characters here (e.g '+' is reserved)
if (/[^a-zA-Z0-9.]/.test(username)) return new UsersError(UsersError.BAD_FIELD, 'Username can only contain alphanumerals and dot');
// also need to consider valid LDAP characters here (e.g '+' is reserved)
if (/[^a-zA-Z0-9.-]/.test(username)) return new UsersError(UsersError.BAD_FIELD, 'Username can only contain alphanumerals, dot and -');
// app emails are sent using the .app suffix
if (username.indexOf('.app') !== -1) return new UsersError(UsersError.BAD_FIELD, 'Username pattern is reserved for apps');
@@ -122,25 +121,29 @@ function validateDisplayName(name) {
return null;
}
function validatePassword(password) {
assert.strictEqual(typeof password, 'string');
if (password.length < 8) return new UsersError(UsersError.BAD_FIELD, 'Password must be atleast 8 characters');
return null;
}
function removePrivateFields(user) {
return _.pick(user, 'id', 'username', 'email', 'fallbackEmail', 'displayName', 'groupIds', 'admin');
}
function createUser(username, password, email, displayName, auditSource, options, callback) {
function create(username, password, email, displayName, options, auditSource, callback) {
assert(username === null || typeof username === 'string');
assert.strictEqual(typeof password, 'string');
assert(password === null || typeof password === 'string');
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof displayName, 'string');
assert(options && typeof options === 'object');
assert.strictEqual(typeof auditSource, 'object');
if (typeof options === 'function') {
callback = options;
options = null;
}
var invitor = options && options.invitor ? options.invitor : null,
sendInvite = options && options.sendInvite ? true : false,
owner = options && options.owner ? true : false;
var invitor = options.invitor || null,
sendInvite = !!options.sendInvite,
owner = !!options.owner;
var error;
@@ -150,8 +153,12 @@ function createUser(username, password, email, displayName, auditSource, options
if (error) return callback(error);
}
error = validatePassword(password);
if (error) return callback(new UsersError(UsersError.BAD_FIELD, error.message));
if (password !== null) {
error = validatePassword(password);
if (error) return callback(new UsersError(UsersError.BAD_FIELD, error.message));
} else {
password = hat(8 * 8);
}
email = email.toLowerCase();
error = validateEmail(email);
@@ -216,11 +223,14 @@ function verify(userId, password, callback) {
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof callback, 'function');
getUser(userId, function (error, user) {
get(userId, function (error, user) {
if (error) return callback(error);
// for just invited users the username may be still null
if (user.username && verifyGhost(user.username, password)) return callback(null, user);
if (user.username && verifyGhost(user.username, password)) {
user.ghost = true;
return callback(null, user);
}
var saltBinary = new Buffer(user.salt, 'hex');
crypto.pbkdf2(password, saltBinary, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, CRYPTO_DIGEST, function (error, derivedKey) {
@@ -265,7 +275,7 @@ function removeUser(userId, auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
getUser(userId, function (error, user) {
get(userId, function (error, user) {
if (error) return callback(error);
if (config.isDemo() && user.username === constants.DEMO_USERNAME) return callback(new UsersError(UsersError.BAD_FIELD, 'Not allowed in demo mode'));
@@ -283,7 +293,7 @@ function removeUser(userId, auditSource, callback) {
});
}
function listUsers(callback) {
function list(callback) {
assert.strictEqual(typeof callback, 'function');
userdb.getAllWithGroupIds(function (error, results) {
@@ -307,7 +317,7 @@ function count(callback) {
});
}
function getUser(userId, callback) {
function get(userId, callback) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -327,18 +337,22 @@ function getUser(userId, callback) {
});
}
function getByResetToken(resetToken, callback) {
function getByResetToken(email, resetToken, callback) {
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof resetToken, 'string');
assert.strictEqual(typeof callback, 'function');
var error = validateToken(resetToken);
var error = validateEmail(email);
if (error) return callback(error);
userdb.getByResetToken(resetToken, function (error, result) {
error = validateToken(resetToken);
if (error) return callback(error);
userdb.getByResetToken(email, resetToken, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UsersError(UsersError.NOT_FOUND));
if (error) return callback(new UsersError(UsersError.INTERNAL_ERROR, error));
getUser(result.id, callback);
get(result.id, callback);
});
}
@@ -382,7 +396,7 @@ function updateUser(userId, data, auditSource, callback) {
callback();
getUser(userId, function (error, result) {
get(userId, function (error, result) {
if (error) return console.error(error);
eventlog.add(eventlog.ACTION_USER_UPDATE, auditSource, { userId: userId, user: removePrivateFields(result) });
@@ -409,7 +423,7 @@ function setGroups(userId, groupIds, callback) {
var wasAdmin = oldGroupIds.some(function (g) { return g === constants.ADMIN_GROUP_ID; });
if ((isAdmin && !wasAdmin) || (!isAdmin && wasAdmin)) {
getUser(userId, function (error, result) {
get(userId, function (error, result) {
if (error) return debug('Failed to send admin change mail.', error);
mailer.adminChanged(result, isAdmin);
@@ -508,7 +522,7 @@ function createOwner(username, password, email, displayName, auditSource, callba
// we proceed if it already exists so we can re-create the owner if need be
if (error && error.reason !== DatabaseError.ALREADY_EXISTS) return callback(new UsersError(UsersError.INTERNAL_ERROR, error));
createUser(username, password, email, displayName, auditSource, { owner: true }, function (error, user) {
create(username, password, email, displayName, { owner: true }, auditSource, function (error, user) {
if (error) return callback(error);
groups.addMember(constants.ADMIN_GROUP_ID, user.id, function (error) {
@@ -562,7 +576,7 @@ function setTwoFactorAuthenticationSecret(userId, callback) {
if (result.twoFactorAuthenticationEnabled) return callback(new UsersError(UsersError.ALREADY_EXISTS));
var secret = speakeasy.generateSecret({ name: `Cloudron (${config.adminFqdn()})` });
var secret = speakeasy.generateSecret({ name: `Cloudron ${config.adminFqdn()} (${result.username})` });
userdb.update(userId, { twoFactorAuthenticationSecret: secret.base32, twoFactorAuthenticationEnabled: false }, function (error) {
if (error) return callback(new UsersError(UsersError.INTERNAL_ERROR, error));