Compare commits

..

83 Commits

Author SHA1 Message Date
Girish Ramakrishnan
11353e9e3a DO DNS API break means this value must atleast be 30
(cherry picked from commit c0c5561aac)
2019-06-17 20:13:32 -07:00
Girish Ramakrishnan
8cd5c15c2b Fix mail auth with manifest v2 2019-06-17 11:13:59 -07:00
Girish Ramakrishnan
b86b8b8ee1 4.1.4 changes
(cherry picked from commit 73a44d1fb2)
2019-06-16 17:59:18 -07:00
Girish Ramakrishnan
c5f6e6b028 Prefix mysql url/database variables 2019-06-15 10:06:51 -07:00
Girish Ramakrishnan
592d8abc58 Roll back async package
something is broken, not sure what
2019-06-14 16:24:41 -07:00
Girish Ramakrishnan
d93068fc62 Update package lock 2019-06-14 15:32:11 -07:00
Girish Ramakrishnan
a864af52df Update packages 2019-06-14 15:31:13 -07:00
Johannes Zellner
1eedd4b185 Send changelog for updated app notifications 2019-06-12 17:15:02 +02:00
Johannes Zellner
9d38edfe95 Update the emaildevelop test template data 2019-06-12 17:14:58 +02:00
Johannes Zellner
f895ebba73 Add some changes 2019-06-12 10:13:36 +02:00
Girish Ramakrishnan
511287b16e linter likes this better 2019-06-11 12:32:15 -07:00
Johannes Zellner
530e06ec66 Add changes 2019-06-11 20:33:56 +02:00
Johannes Zellner
9cab383b43 Namecom does not support @ for naked domain anymore 2019-06-11 20:33:56 +02:00
Girish Ramakrishnan
9785ab82ed Use cloudron as prefix instead of suffix 2019-06-11 09:39:45 -07:00
Johannes Zellner
9d237e7bd6 Fix sudo installation on scaleway 2019-06-11 13:30:15 +02:00
Girish Ramakrishnan
7e9885012d vary dkim selector per mail domain
this is required for the case where the domain is added on multiple
cloudrons. initially, the plan was to just vary this as a derivation
of the dashboard domain. but this will break existing installation (wildcard
and manual domain setups cannot be re-programmed automatically).
2019-06-10 18:35:38 -07:00
Girish Ramakrishnan
1de785d97c cloudron-support: add ip addr output
with cloudflare dns, we don't have ip to login
2019-06-10 09:31:34 -07:00
Girish Ramakrishnan
2bd6566537 clear timeout when get services status 2019-06-09 22:20:35 -07:00
Girish Ramakrishnan
88fa4cf188 remove reserved 'api' location
this is unused and we have no plans to use it.
2019-06-09 18:16:31 -07:00
Girish Ramakrishnan
b26167481e Make the dkim private keys readable
https://forum.cloudron.io/topic/1675/dkim-and-dmarc-for-built-in-outgoing-mail/25
2019-06-06 14:45:42 -07:00
Girish Ramakrishnan
1b6af9bd12 scaleway: add tzdata
the bionic image is missing this and only has UTC in the output
of timedatectl list-timezones
2019-06-06 12:42:07 -07:00
Girish Ramakrishnan
0159963cb0 More changes 2019-06-06 11:43:24 -07:00
Girish Ramakrishnan
996041cabc add mechanism to whitelist and blacklist apps 2019-06-06 11:42:42 -07:00
Girish Ramakrishnan
cb0352e33c Do not remove existing custom.yml 2019-06-06 11:24:19 -07:00
Johannes Zellner
3169f032c8 Return correct conflicting domain 2019-06-05 19:54:15 +02:00
Girish Ramakrishnan
5ff8ee1a8f Check manifest version when installing an app
This should have been done for manifest v1 already. For now, apps
will have to put in a minBoxVersion.
2019-06-03 14:02:47 -07:00
Girish Ramakrishnan
d3f31a3ace Ensure all env vars have the CLOUDRON_ prefix
this is currently injected based on the manifest version (i.e v2)
2019-06-03 13:45:35 -07:00
Girish Ramakrishnan
ac7e7f0db9 Set name as the network alias for app containers
this allows scheduler containers to reach app containers by http

https://forum.cloudron.io/topic/1082/bitwarden-self-hosted-password-manager
2019-06-01 10:48:51 -07:00
Girish Ramakrishnan
4c1e967dad give containers a hostname
this only affects the hostname and not the network name/alias
2019-06-01 10:02:26 -07:00
Girish Ramakrishnan
f3ccd5c074 More changes 2019-06-01 09:05:48 -07:00
Girish Ramakrishnan
8369c0e2c0 4.1.2 changes 2019-05-31 12:52:57 -07:00
Girish Ramakrishnan
122a966e72 No exclamation 2019-05-30 12:17:47 -07:00
Girish Ramakrishnan
9c2ff2f862 fix image prune logic 2019-05-29 12:15:13 -07:00
Girish Ramakrishnan
0ba45e746b Accept incoming mail from a private relay 2019-05-29 11:33:37 -07:00
Girish Ramakrishnan
54c06cdabb support: send a result message 2019-05-28 10:04:04 -07:00
Girish Ramakrishnan
5a2e10317c remove autoconf
this was mostly for caas
2019-05-24 15:20:25 -07:00
Girish Ramakrishnan
8292d52acf Add changes 2019-05-24 11:19:22 -07:00
Girish Ramakrishnan
7d21470fc7 remove cloudron-provision
will move to separate repo
2019-05-22 22:33:41 -07:00
Girish Ramakrishnan
eb0530bcba add note 2019-05-22 18:33:02 -07:00
Girish Ramakrishnan
8855092faa update changes 2019-05-22 14:39:47 -07:00
Girish Ramakrishnan
2e02a3c71e Revert "only admins have sftp access"
This reverts commit ecc9415679.

We want to support the workflow where normal users can have SFTP
access without being cloudron admins. The reason it is admin only
is because it is possible to upload/modify app code via SFTP to
then get cloudron admin credentials.

For this reason, we will fixup the apps as follows:
* Unmanaged WP - remove LDAP integration
* LAMP - remove LDAP. We will make a new major version that informs
  the user NOT to update the app if they use LDAP. In 4.1, we will
  expose the LDAP server, so they can use the public LDAP server for
  any integration.
* Managed WP - Remove SFTP. This is contential but if people want to
  really build/develop plugins then they can use Unmanaged WP for the dev
  environment.
* Surfer - no change. Can have SFTP and LDAP since code is not modifiable

In general, should also be careful then about adding SFTP access to random
apps (like say nextcloud), since this would allow normal user to access
other people's data.
2019-05-22 14:32:45 -07:00
Girish Ramakrishnan
5b5303ba7f Always return object in response 2019-05-22 10:41:34 -07:00
Girish Ramakrishnan
022a54278e Add missing error code 2019-05-22 10:41:25 -07:00
Girish Ramakrishnan
19b50dc428 do not dump values in debug
it ends up dumping the icon in logs
2019-05-22 09:38:27 -07:00
Girish Ramakrishnan
e7eac003a9 cloudron-support: add ssh keys like support.js 2019-05-21 09:50:38 -07:00
Girish Ramakrishnan
cc17c6b2cd cloudron-support: add set 2019-05-21 09:21:22 -07:00
Girish Ramakrishnan
23d16b07aa Add API to get original icon 2019-05-21 00:14:54 -07:00
Girish Ramakrishnan
7ecb3dd771 Fix resolution of cloudflare MX record
cf might rewrite the MX record if it deems that there is a conflict

https://support.cloudflare.com/hc/en-us/articles/360020296512-DNS-Troubleshooting-FAQ
2019-05-20 18:20:04 -07:00
Johannes Zellner
e43f974d34 Rework namecheap tests 2019-05-20 22:21:20 +02:00
Girish Ramakrishnan
e16cd38722 Update changes 2019-05-20 10:34:54 -07:00
Girish Ramakrishnan
9d2f81d6b9 Remove X-Frame-Options
This option is now obsolete in the standards and browsers are complaining.
This needs to move to be a CSP header but this is hard to do from outside
the app (since it has to be 'merged' with the app's existing CSP).

fixes #596
2019-05-20 10:11:52 -07:00
Johannes Zellner
3fe539436b Sinon was only used in old namecheap tests 2019-05-20 16:35:23 +02:00
Girish Ramakrishnan
76f94eb559 namecheap module is not used 2019-05-18 09:41:05 -07:00
Girish Ramakrishnan
7630ef921d Add changes 2019-05-17 14:40:33 -07:00
Girish Ramakrishnan
625127d298 add icon to configure route 2019-05-17 12:50:08 -07:00
Girish Ramakrishnan
f24c4d2805 Look for a user set app icon 2019-05-17 10:14:02 -07:00
Girish Ramakrishnan
194340afa0 protect app icon route 2019-05-17 09:54:45 -07:00
Johannes Zellner
fdc9639aba Deal with bad namecheap API naming convention 2019-05-16 18:03:09 +02:00
Johannes Zellner
f95ec53a85 Check for namecheap response status 2019-05-16 18:03:09 +02:00
Johannes Zellner
3d425b7030 Rewrite namecheap backend to not rely on unmaintained node module 2019-05-16 18:03:09 +02:00
Girish Ramakrishnan
37c6c24e0e caas is dead 2019-05-16 08:49:08 -07:00
Girish Ramakrishnan
50bdd7ec7b mail: Remove authType when username is empty 2019-05-15 16:23:56 -07:00
Girish Ramakrishnan
769cb3e251 Update mail container 2019-05-15 15:54:51 -07:00
Girish Ramakrishnan
9447c45406 enable the gcdns test 2019-05-15 10:18:30 -07:00
Johannes Zellner
66a3962cfe Do not create notifications when apps are updated through the cli 2019-05-15 19:15:57 +02:00
Girish Ramakrishnan
d145eacbaf send domain in auto-register
previously, this was done during startup and we didn't have a domain
in hand
2019-05-15 09:58:59 -07:00
Girish Ramakrishnan
ed03ed7bad make changeDashboardDomain customizable 2019-05-14 19:20:45 -07:00
Girish Ramakrishnan
953b463799 4.1.0 changes 2019-05-14 18:00:34 -07:00
Johannes Zellner
6d28bb0489 4.0.3 changes
(cherry picked from commit 8686832bd1)
2019-05-14 16:17:00 -07:00
Johannes Zellner
c2f464ea75 password change api now returns 400 instead of 403 2019-05-13 23:46:38 +02:00
Johannes Zellner
4c56ffc767 Add default footer content to custom.yml 2019-05-13 22:50:28 +02:00
Johannes Zellner
885aa8833c Remove password requirement for destructive rest routes 2019-05-13 22:48:33 +02:00
Johannes Zellner
63310c44c0 Ensure notifications are sorted by time descending 2019-05-13 22:05:58 +02:00
Johannes Zellner
05dd65718f Remove unused CLOUDRON_ID 2019-05-13 16:28:46 +02:00
Girish Ramakrishnan
05d3f8a667 gcs: fix crash 2019-05-12 18:05:48 -07:00
Girish Ramakrishnan
3fa45ea728 4.0.2 changes 2019-05-12 13:59:57 -07:00
Girish Ramakrishnan
a7d2098f09 Add option to skip backup before update 2019-05-12 13:28:53 -07:00
Girish Ramakrishnan
e1ecb49d59 gcdns: fix crash 2019-05-11 19:18:11 -07:00
Johannes Zellner
6facfac4c5 Add footer customization option 2019-05-11 13:37:43 +02:00
Girish Ramakrishnan
97d2494fe3 Make ticket body customizable 2019-05-10 17:35:47 -07:00
Girish Ramakrishnan
a54be69c96 rework custom configuration 2019-05-10 16:18:43 -07:00
Girish Ramakrishnan
800e25a7a7 Fix crash because params was undefined 2019-05-10 13:07:29 -07:00
Girish Ramakrishnan
c1ce2977fa custom: refactor code for defaults 2019-05-10 11:31:16 -07:00
60 changed files with 1515 additions and 1788 deletions

34
CHANGES
View File

@@ -1597,3 +1597,37 @@
* Fix GCDNS crash
* Add option to update without backing up
[4.0.3]
* Fix dashboard issue for non-admins
[4.1.0]
* Remove password requirement for uninstalling apps and users
* Hosting provider edition
* Enforce limits in mail container
* Fix crash when using unauthenticated relay
* Fix domain and tag filtering
* Customizable app icons
* Remove obsolete X-Frame-Options from nginx configs
* Give SFTP access based on access restriction
[4.1.1]
* Add UI hint about SFTP access restriction
[4.1.2]
* Accept incoming mail from a private relay
* Fix issue where unused addon images were not pruned
* Add UI for redirect from multiple domains
* Allow apps to be relocated to custom data directory
* Make all cloudron env vars have CLOUDRON_ prefix
* Update manifest version to 2
* Fix issue where DKIM keys were inaccessible
* Fix DKIM selector conflict when adding same domain across multiple cloudrons
* Fix name.com DNS backend issue for naked domains
* Add DigitalOcean Frankfurt (fra1) region for backup storage
[4.1.3]
* Update manifest format package
[4.1.4]
* Add CLOUDRON_ prefix to MySQL addon variables

View File

@@ -41,12 +41,7 @@ Try our demo at https://my.demo.cloudron.io (username: cloudron password: cloudr
## Installing
You can install the Cloudron platform on your own server or get a managed server
from cloudron.io. In either case, the Cloudron platform will keep your server and
apps up-to-date and secure.
* [Selfhosting](https://cloudron.io/documentation/installation/) - [Pricing](https://cloudron.io/pricing.html)
* [Managed Hosting](https://cloudron.io/managed.html)
[Install script](https://cloudron.io/documentation/installation/) - [Pricing](https://cloudron.io/pricing.html)
**Note:** This repo is a small part of what gets installed on your server - there is
the dashboard, database addons, graph container, base image etc. Cloudron also relies

View File

@@ -46,12 +46,15 @@ apt-get -y install \
openssh-server \
pwgen \
resolvconf \
sudo \
swaks \
tzdata \
unattended-upgrades \
unbound \
xfsprogs
# on some providers like scaleway the sudo file is changed and we want to keep the old one
apt-get -o Dpkg::Options::="--force-confold" install -y sudo
# this ensures that unattended upgades are enabled, if it was disabled during ubuntu install time (see #346)
# debconf-set-selection of unattended-upgrades/enable_auto_updates + dpkg-reconfigure does not work
cp /usr/share/unattended-upgrades/20auto-upgrades /etc/apt/apt.conf.d/20auto-upgrades

View File

@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE mail ADD COLUMN dkimSelector VARCHAR(128) NOT NULL DEFAULT "cloudron"', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE mail DROP COLUMN dkimSelector', function (error) {
if (error) console.error(error);
callback(error);
});
};

View File

@@ -174,6 +174,8 @@ CREATE TABLE IF NOT EXISTS mail(
catchAllJson TEXT,
relayJson TEXT,
dkimSelector VARCHAR(128) NOT NULL DEFAULT "cloudron",
FOREIGN KEY(domain) REFERENCES domains(domain),
PRIMARY KEY(domain))

1070
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,40 +14,40 @@
"node": ">=4.0.0 <=4.1.1"
},
"dependencies": {
"@google-cloud/dns": "^0.9.2",
"@google-cloud/dns": "^1.1.0",
"@google-cloud/storage": "^2.5.0",
"@sindresorhus/df": "^3.1.0",
"async": "^2.6.2",
"aws-sdk": "^2.441.0",
"body-parser": "^1.18.3",
"cloudron-manifestformat": "^2.14.2",
"connect": "^3.6.6",
"aws-sdk": "^2.476.0",
"body-parser": "^1.19.0",
"cloudron-manifestformat": "^2.15.0",
"connect": "^3.7.0",
"connect-ensure-login": "^0.1.1",
"connect-lastmile": "^1.0.2",
"connect-timeout": "^1.9.0",
"cookie-parser": "^1.4.4",
"cookie-session": "^1.3.3",
"cron": "^1.7.0",
"csurf": "^1.9.0",
"db-migrate": "^0.11.5",
"cron": "^1.7.1",
"csurf": "^1.10.0",
"db-migrate": "^0.11.6",
"db-migrate-mysql": "^1.1.10",
"debug": "^4.1.1",
"dockerode": "^2.5.8",
"ejs": "^2.6.1",
"ejs-cli": "^2.0.1",
"express": "^4.16.4",
"express-session": "^1.16.1",
"express": "^4.17.1",
"express-session": "^1.16.2",
"js-yaml": "^3.13.1",
"json": "^9.0.6",
"ldapjs": "^1.0.2",
"lodash": "^4.17.11",
"lodash.chunk": "^4.2.0",
"mime": "^2.4.2",
"mime": "^2.4.4",
"moment-timezone": "^0.5.25",
"morgan": "^1.9.1",
"multiparty": "^4.2.1",
"mysql": "^2.17.1",
"namecheap": "github:joshuakarjala/node-namecheap#464a952",
"nodemailer": "^6.1.1",
"nodemailer": "^6.2.1",
"nodemailer-smtp-transport": "^2.7.4",
"oauth2orize": "^1.11.0",
"once": "^1.4.0",
@@ -60,25 +60,26 @@
"progress-stream": "^2.0.0",
"proxy-middleware": "^0.15.0",
"qrcode": "^1.3.3",
"readdirp": "^3.0.0",
"readdirp": "^3.0.2",
"request": "^2.88.0",
"rimraf": "^2.6.3",
"s3-block-read-stream": "^0.5.0",
"safetydance": "^0.7.1",
"semver": "^6.0.0",
"semver": "^6.1.1",
"showdown": "^1.9.0",
"speakeasy": "^2.0.0",
"split": "^1.0.1",
"superagent": "^5.0.2",
"superagent": "^5.0.9",
"supererror": "^0.7.2",
"tar-fs": "github:cloudron-io/tar-fs#ignore_stat_error",
"tar-stream": "^2.0.1",
"tar-stream": "^2.1.0",
"tldjs": "^2.3.1",
"underscore": "^1.9.1",
"uuid": "^3.3.2",
"valid-url": "^1.0.9",
"validator": "^10.11.0",
"ws": "^6.2.1"
"validator": "^11.0.0",
"ws": "^7.0.0",
"xml2js": "^0.4.19"
},
"devDependencies": {
"expect.js": "*",
@@ -87,9 +88,8 @@
"mocha": "^6.1.4",
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
"nock": "^10.0.6",
"node-sass": "^4.11.0",
"recursive-readdir": "^2.2.2",
"sinon": "^7.3.2"
"node-sass": "^4.12.0",
"recursive-readdir": "^2.2.2"
},
"scripts": {
"test": "./runTests",

View File

@@ -1,106 +0,0 @@
#!/bin/bash
set -eu -o pipefail
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400 --http1.1"
ip=""
dns_config=""
tls_cert_file=""
tls_key_file=""
license_file=""
backup_config=""
args=$(getopt -o "" -l "ip:,backup-config:,license:,dns-config:,tls-cert:,tls-key:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--ip) ip="$2"; shift 2;;
--dns-config) dns_config="$2"; shift 2;;
--tls-cert) tls_cert_file="$2"; shift 2;;
--tls-key) tls_key_file="$2"; shift 2;;
--license) license_file="$2"; shift 2;;
--backup-config) backup_config="$2"; shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
# validate arguments in the absence of data
if [[ -z "${ip}" ]]; then
echo "--ip is required"
exit 1
fi
if [[ -z "${dns_config}" ]]; then
echo "--dns-config is required"
exit 1
fi
if [[ ! -f "${license_file}" ]]; then
echo "--license must be a valid license file"
exit 1
fi
function get_status() {
key="$1"
if status=$($curl -q -f -k "https://${ip}/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
}
echo "=> Waiting for cloudron to be ready"
wait_for_status "version" '*'
domain=$(echo "${dns_config}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["domain"])')
echo "Provisioning Cloudron ${domain}"
if [[ -n "${tls_cert_file}" && -n "${tls_key_file}" ]]; then
tls_cert=$(cat "${tls_cert_file}" | awk '{printf "%s\\n", $0}')
tls_key=$(cat "${tls_key_file}" | awk '{printf "%s\\n", $0}')
fallback_cert=$(printf '{ "cert": "%s", "key": "%s", "provider": "fallback", "restricted": true }' "${tls_cert}" "${tls_key}")
else
fallback_cert=None
fi
tls_config='{ "provider": "fallback" }'
dns_config=$(echo "${dns_config}" | python3 -c "import json,sys;obj=json.load(sys.stdin);obj.update(tlsConfig=${tls_config});obj.update(fallbackCertficate=${fallback_cert});print(json.dumps(obj))")
license=$(cat "${license_file}")
if [[ -z "${backup_config:-}" ]]; then
backup_config='{ "provider": "filesystem", "backupFolder": "/var/backups", "format": "tgz" }'
fi
setupData=$(printf '{ "dnsConfig": %s, "autoconf": { "appstoreConfig": %s, "backupConfig": %s } }' "${dns_config}" "${license}" "${backup_config}")
if ! setupResult=$($curl -kq -X POST -H "Content-Type: application/json" -d "${setupData}" https://${ip}/api/v1/cloudron/setup); then
echo "Failed to setup with ${setupData} ${setupResult}"
exit 1
fi
wait_for_status "webadminStatus" '*"tls": true*'
echo "Cloudron is ready at https://my-${domain}"

View File

@@ -1,5 +1,7 @@
#!/bin/bash
set -eu -o pipefail
# This script collects diagnostic information to help debug server related issues
# It also enables SSH access for the cloudron support team
@@ -17,7 +19,7 @@ This script collects diagnostic information to help debug server related issues
# We require root
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
echo "This script should be run as root. Run with sudo"
exit 1
fi
@@ -58,21 +60,6 @@ echo -n "Generating Cloudron Support stats..."
# clear file
rm -rf $OUT
ssh_port=$(cat /etc/ssh/sshd_config | grep "Port " | sed -e "s/.*Port //")
if [[ $SUDO_USER == "" ]]; then
ssh_user="root"
ssh_folder="/root/.ssh/"
authorized_key_file="${ssh_folder}/authorized_keys"
else
ssh_user="$SUDO_USER"
ssh_folder="/home/$SUDO_USER/.ssh/"
authorized_key_file="${ssh_folder}/authorized_keys"
fi
echo -e $LINE"SSH"$LINE >> $OUT
echo "Username: ${ssh_user}" >> $OUT
echo "Port: ${ssh_port}" >> $OUT
echo -e $LINE"cloudron.conf"$LINE >> $OUT
cat /etc/cloudron/cloudron.conf &>> $OUT
@@ -99,25 +86,50 @@ systemctl status --lines=100 cloudron.target box mysql unbound cloudron-syslog n
echo -e $LINE"Box logs"$LINE >> $OUT
tail -n 100 /home/yellowtent/platformdata/logs/box.log &>> $OUT
echo -e $LINE"Firewall chains"$LINE >> $OUT
ip addr &>> $OUT
echo -e $LINE"Firewall chains"$LINE >> $OUT
iptables -L &>> $OUT
echo "Done"
if [[ "${enableSSH}" == "true" ]]; then
ssh_port=$(cat /etc/ssh/sshd_config | grep "Port " | sed -e "s/.*Port //")
permit_root_login=$(grep -q ^PermitRootLogin.*yes /etc/ssh/sshd_config && echo "yes" || echo "no")
# support.js uses similar logic
if $(grep -q "ec2\|lightsail\|ami" /etc/cloudron/cloudron.conf); then
ssh_user="ubuntu"
keys_file="/home/ubuntu/.ssh/authorized_keys"
else
ssh_user="root"
keys_file="/root/.ssh/authorized_keys"
fi
echo -e $LINE"SSH"$LINE >> $OUT
echo "Username: ${ssh_user}" >> $OUT
echo "Port: ${ssh_port}" >> $OUT
echo "PermitRootLogin: ${permit_root_login}" >> $OUT
echo "Key file: ${keys_file}" >> $OUT
echo -n "Enabling ssh access for the Cloudron support team..."
mkdir -p $(dirname "${keys_file}") # .ssh does not exist sometimes
touch "${keys_file}" # required for concat to work
if ! grep -q "${CLOUDRON_SUPPORT_PUBLIC_KEY}" "${keys_file}"; then
echo -e "\n${CLOUDRON_SUPPORT_PUBLIC_KEY}" >> "${keys_file}"
chmod 600 "${keys_file}"
chown "${ssh_user}" "${keys_file}"
fi
echo "Done"
fi
echo -n "Uploading information..."
# for some reason not using $(cat $OUT) will not contain newlines!?
paste_key=$(curl -X POST ${PASTEBIN}/documents --silent -d "$(cat $OUT)" | python3 -c "import sys, json; print(json.load(sys.stdin)['key'])")
echo "Done"
if [[ "${enableSSH}" == "true" ]]; then
echo -n "Enabling ssh access for the Cloudron support team..."
mkdir -p "${ssh_folder}"
echo -e "\n${CLOUDRON_SUPPORT_PUBLIC_KEY}" >> ${authorized_key_file}
chown -R ${ssh_user} "${ssh_folder}"
chmod 600 "${authorized_key_file}"
echo "Done"
fi
echo ""
echo "Please email the following link to support@cloudron.io"
echo ""

View File

@@ -110,9 +110,6 @@ systemctl restart cloudron-syslog
$json -f /etc/cloudron/cloudron.conf -I -e "delete this.edition" # can be removed after 4.0
echo "==> Clearing custom.yml"
rm -f /etc/cloudron/custom.yml
echo "==> Configuring sudoers"
rm -f /etc/sudoers.d/${USER}
cp "${script_dir}/start/sudoers" /etc/sudoers.d/${USER}

View File

@@ -1,16 +1,40 @@
# add customizations here
# after making changes run "sudo systemctl restart box"
# features:
# configureBackup: true
# appstore:
# blacklist:
# - io.wekan.cloudronapp
# - io.cloudron.openvpn
# whitelist:
# org.wordpress.cloudronapp: {}
# chat.rocket.cloudronapp: {}
# com.nextcloud.cloudronapp: {}
#
# backups:
# configurable: true
#
# domains:
# dynamicDns: true
# subscription: true
# remoteSupport: true
#
# changeDashboardDomain: true
#
# subscription:
# configurable: true
#
# support:
# email: support@cloudron.io
#
# remoteSupport: true
#
# ticketFormBody: |
# Use this form to open support tickets. You can also write directly to [support@cloudron.io](mailto:support@cloudron.io).
# * [Knowledge Base & App Docs](https://cloudron.io/documentation/apps/?support_view)
# * [Custom App Packaging & API](https://cloudron.io/developer/packaging/?support_view)
# * [Forum](https://forum.cloudron.io/)
#
# submitTickets: true
#
# alerts:
# email: support@cloudron.io
# notifyCloudronAdmins: false
#
# footer:
# body: '&copy; 2019 [Cloudron](https://cloudron.io) [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)'

View File

@@ -795,10 +795,12 @@ function setupOauth(app, options, callback) {
clients.add(appId, clients.TYPE_OAUTH, redirectURI, scope, function (error, result) {
if (error) return callback(error);
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
var env = [
{ name: 'OAUTH_CLIENT_ID', value: result.id },
{ name: 'OAUTH_CLIENT_SECRET', value: result.clientSecret },
{ name: 'OAUTH_ORIGIN', value: config.adminOrigin() }
{ name: `${envPrefix}OAUTH_CLIENT_ID`, value: result.id },
{ name: `${envPrefix}OAUTH_CLIENT_SECRET`, value: result.clientSecret },
{ name: `${envPrefix}OAUTH_ORIGIN`, value: config.adminOrigin() }
];
debugApp(app, 'Setting oauth addon config to %j', env);
@@ -832,17 +834,19 @@ function setupEmail(app, options, callback) {
const mailInDomains = mailDomains.filter(function (d) { return d.enabled; }).map(function (d) { return d.domain; }).join(',');
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
// note that "external" access info can be derived from MAIL_DOMAIN (since it's part of user documentation)
var env = [
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
{ name: 'MAIL_SMTP_PORT', value: '2525' },
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
{ name: 'MAIL_IMAP_PORT', value: '9993' },
{ name: 'MAIL_SIEVE_SERVER', value: 'mail' },
{ name: 'MAIL_SIEVE_PORT', value: '4190' },
{ name: 'MAIL_DOMAIN', value: app.domain },
{ name: 'MAIL_DOMAINS', value: mailInDomains },
{ name: 'LDAP_MAILBOXES_BASE_DN', value: 'ou=mailboxes,dc=cloudron' }
{ name: `${envPrefix}MAIL_SMTP_SERVER`, value: 'mail' },
{ name: `${envPrefix}MAIL_SMTP_PORT`, value: '2525' },
{ name: `${envPrefix}MAIL_IMAP_SERVER`, value: 'mail' },
{ name: `${envPrefix}MAIL_IMAP_PORT`, value: '9993' },
{ name: `${envPrefix}MAIL_SIEVE_SERVER`, value: 'mail' },
{ name: `${envPrefix}MAIL_SIEVE_PORT`, value: '4190' },
{ name: `${envPrefix}MAIL_DOMAIN`, value: app.domain },
{ name: `${envPrefix}MAIL_DOMAINS`, value: mailInDomains },
{ name: `${envPrefix}LDAP_MAILBOXES_BASE_DN`, value: 'ou=mailboxes,dc=cloudron' }
];
debugApp(app, 'Setting up Email');
@@ -868,14 +872,16 @@ function setupLdap(app, options, callback) {
if (!app.sso) return callback(null);
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
var env = [
{ name: 'LDAP_SERVER', value: '172.18.0.1' },
{ name: 'LDAP_PORT', value: '' + config.get('ldapPort') },
{ name: 'LDAP_URL', value: 'ldap://172.18.0.1:' + config.get('ldapPort') },
{ name: 'LDAP_USERS_BASE_DN', value: 'ou=users,dc=cloudron' },
{ name: 'LDAP_GROUPS_BASE_DN', value: 'ou=groups,dc=cloudron' },
{ name: 'LDAP_BIND_DN', value: 'cn='+ app.id + ',ou=apps,dc=cloudron' },
{ name: 'LDAP_BIND_PASSWORD', value: hat(4 * 128) } // this is ignored
{ name: `${envPrefix}LDAP_SERVER`, value: '172.18.0.1' },
{ name: `${envPrefix}LDAP_PORT`, value: '' + config.get('ldapPort') },
{ name: `${envPrefix}LDAP_URL`, value: 'ldap://172.18.0.1:' + config.get('ldapPort') },
{ name: `${envPrefix}LDAP_USERS_BASE_DN`, value: 'ou=users,dc=cloudron' },
{ name: `${envPrefix}LDAP_GROUPS_BASE_DN`, value: 'ou=groups,dc=cloudron' },
{ name: `${envPrefix}LDAP_BIND_DN`, value: 'cn='+ app.id + ',ou=apps,dc=cloudron' },
{ name: `${envPrefix}LDAP_BIND_PASSWORD`, value: hat(4 * 128) } // this is ignored
];
debugApp(app, 'Setting up LDAP');
@@ -905,14 +911,16 @@ function setupSendMail(app, options, callback) {
var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
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: app.mailboxName + '@' + app.domain },
{ name: 'MAIL_SMTP_PASSWORD', value: password },
{ name: 'MAIL_FROM', value: app.mailboxName + '@' + app.domain },
{ name: 'MAIL_DOMAIN', value: app.domain }
{ name: `${envPrefix}MAIL_SMTP_SERVER`, value: 'mail' },
{ name: `${envPrefix}MAIL_SMTP_PORT`, value: '2525' },
{ name: `${envPrefix}MAIL_SMTPS_PORT`, value: '2465' },
{ name: `${envPrefix}MAIL_SMTP_USERNAME`, value: app.mailboxName + '@' + app.domain },
{ name: `${envPrefix}MAIL_SMTP_PASSWORD`, value: password },
{ name: `${envPrefix}MAIL_FROM`, value: app.mailboxName + '@' + app.domain },
{ name: `${envPrefix}MAIL_DOMAIN`, value: app.domain }
];
debugApp(app, 'Setting sendmail addon config to %j', env);
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
@@ -941,13 +949,15 @@ function setupRecvMail(app, options, callback) {
var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
var env = [
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
{ name: 'MAIL_IMAP_PORT', value: '9993' },
{ name: 'MAIL_IMAP_USERNAME', value: app.mailboxName + '@' + app.domain },
{ name: 'MAIL_IMAP_PASSWORD', value: password },
{ name: 'MAIL_TO', value: app.mailboxName + '@' + app.domain },
{ name: 'MAIL_DOMAIN', value: app.domain }
{ name: `${envPrefix}MAIL_IMAP_SERVER`, value: 'mail' },
{ name: `${envPrefix}MAIL_IMAP_PORT`, value: '9993' },
{ name: `${envPrefix}MAIL_IMAP_USERNAME`, value: app.mailboxName + '@' + app.domain },
{ name: `${envPrefix}MAIL_IMAP_PASSWORD`, value: password },
{ name: `${envPrefix}MAIL_TO`, value: app.mailboxName + '@' + app.domain },
{ name: `${envPrefix}MAIL_DOMAIN`, value: app.domain }
];
debugApp(app, 'Setting sendmail addon config to %j', env);
@@ -992,6 +1002,7 @@ function startMysql(existingInfra, callback) {
if (error) return callback(error);
const cmd = `docker run --restart=always -d --name="mysql" \
--hostname mysql \
--net cloudron \
--net-alias mysql \
--log-driver syslog \
@@ -1048,19 +1059,21 @@ function setupMySql(app, options, callback) {
if (error) return callback(new Error('Error setting up mysql: ' + error));
if (response.statusCode !== 201) return callback(new Error(`Error setting up mysql. Status code: ${response.statusCode} message: ${response.body.message}`));
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
var env = [
{ name: 'MYSQL_USERNAME', value: data.username },
{ name: 'MYSQL_PASSWORD', value: data.password },
{ name: 'MYSQL_HOST', value: 'mysql' },
{ name: 'MYSQL_PORT', value: '3306' }
{ name: `${envPrefix}MYSQL_USERNAME`, value: data.username },
{ name: `${envPrefix}MYSQL_PASSWORD`, value: data.password },
{ name: `${envPrefix}MYSQL_HOST`, value: 'mysql' },
{ name: `${envPrefix}MYSQL_PORT`, value: '3306' }
];
if (options.multipleDatabases) {
env = env.concat({ name: 'MYSQL_DATABASE_PREFIX', value: `${data.prefix}_` });
env = env.concat({ name: `${envPrefix}MYSQL_DATABASE_PREFIX`, value: `${data.prefix}_` });
} else {
env = env.concat(
{ name: 'MYSQL_URL', value: `mysql://${data.username}:${data.password}@mysql/${data.database}` },
{ name: 'MYSQL_DATABASE', value: data.database }
{ name: `${envPrefix}MYSQL_URL`, value: `mysql://${data.username}:${data.password}@mysql/${data.database}` },
{ name: `${envPrefix}MYSQL_DATABASE`, value: data.database }
);
}
@@ -1204,6 +1217,7 @@ function startPostgresql(existingInfra, callback) {
if (error) return callback(error);
const cmd = `docker run --restart=always -d --name="postgresql" \
--hostname postgresql \
--net cloudron \
--net-alias postgresql \
--log-driver syslog \
@@ -1258,13 +1272,15 @@ function setupPostgreSql(app, options, callback) {
if (error) return callback(new Error('Error setting up postgresql: ' + error));
if (response.statusCode !== 201) return callback(new Error(`Error setting up postgresql. Status code: ${response.statusCode} message: ${response.body.message}`));
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
var env = [
{ name: 'POSTGRESQL_URL', value: `postgres://${data.username}:${data.password}@postgresql/${data.database}` },
{ name: 'POSTGRESQL_USERNAME', value: data.username },
{ name: 'POSTGRESQL_PASSWORD', value: data.password },
{ name: 'POSTGRESQL_HOST', value: 'postgresql' },
{ name: 'POSTGRESQL_PORT', value: '5432' },
{ name: 'POSTGRESQL_DATABASE', value: data.database }
{ name: `${envPrefix}POSTGRESQL_URL`, value: `postgres://${data.username}:${data.password}@postgresql/${data.database}` },
{ name: `${envPrefix}POSTGRESQL_USERNAME`, value: data.username },
{ name: `${envPrefix}POSTGRESQL_PASSWORD`, value: data.password },
{ name: `${envPrefix}POSTGRESQL_HOST`, value: 'postgresql' },
{ name: `${envPrefix}POSTGRESQL_PORT`, value: '5432' },
{ name: `${envPrefix}POSTGRESQL_DATABASE`, value: data.database }
];
debugApp(app, 'Setting postgresql addon config to %j', env);
@@ -1378,6 +1394,7 @@ function startMongodb(existingInfra, callback) {
if (error) return callback(error);
const cmd = `docker run --restart=always -d --name="mongodb" \
--hostname mongodb \
--net cloudron \
--net-alias mongodb \
--log-driver syslog \
@@ -1430,13 +1447,15 @@ function setupMongoDb(app, options, callback) {
if (error) return callback(new Error('Error setting up mongodb: ' + error));
if (response.statusCode !== 201) return callback(new Error(`Error setting up mongodb. Status code: ${response.statusCode}`));
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
var env = [
{ name: 'MONGODB_URL', value : `mongodb://${data.username}:${data.password}@mongodb/${data.database}` },
{ name: 'MONGODB_USERNAME', value : data.username },
{ name: 'MONGODB_PASSWORD', value: data.password },
{ name: 'MONGODB_HOST', value : 'mongodb' },
{ name: 'MONGODB_PORT', value : '27017' },
{ name: 'MONGODB_DATABASE', value : data.database }
{ name: `${envPrefix}MONGODB_URL`, value : `mongodb://${data.username}:${data.password}@mongodb/${data.database}` },
{ name: `${envPrefix}MONGODB_USERNAME`, value : data.username },
{ name: `${envPrefix}MONGODB_PASSWORD`, value: data.password },
{ name: `${envPrefix}MONGODB_HOST`, value : 'mongodb' },
{ name: `${envPrefix}MONGODB_PORT`, value : '27017' },
{ name: `${envPrefix}MONGODB_DATABASE`, value : data.database }
];
debugApp(app, 'Setting mongodb addon config to %j', env);
@@ -1572,6 +1591,7 @@ function setupRedis(app, options, callback) {
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} \
--hostname ${redisName} \
--label=location=${label} \
--net cloudron \
--net-alias ${redisName} \
@@ -1589,11 +1609,13 @@ function setupRedis(app, options, callback) {
--label isCloudronManaged=true \
--read-only -v /tmp -v /run ${tag}`;
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
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' }
{ name: `${envPrefix}REDIS_URL`, value: 'redis://redisuser:' + redisPassword + '@redis-' + app.id },
{ name: `${envPrefix}REDIS_PASSWORD`, value: redisPassword },
{ name: `${envPrefix}REDIS_HOST`, value: redisName },
{ name: `${envPrefix}REDIS_PORT`, value: '6379' }
];
async.series([

View File

@@ -72,10 +72,6 @@ server {
ssl_dhparam /home/yellowtent/boxdata/dhparams.pem;
add_header Strict-Transport-Security "max-age=15768000";
# https://developer.mozilla.org/en-US/docs/Web/HTTP/X-Frame-Options
add_header X-Frame-Options "<%= xFrameOptions %>";
proxy_hide_header X-Frame-Options;
# https://github.com/twitter/secureheaders
# https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#tab=Compatibility_Matrix
# https://wiki.mozilla.org/Security/Guidelines/Web_Security

View File

@@ -69,7 +69,7 @@ var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationSta
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'subdomains.subdomain AS location', 'subdomains.domain',
'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit',
'apps.label', 'apps.tagsJson',
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.mailboxName', 'apps.enableAutomaticUpdate',
'apps.dataDir', 'apps.ts', 'apps.healthTime' ].join(',');
@@ -121,9 +121,6 @@ function postProcess(result) {
if (result.accessRestriction && !result.accessRestriction.users) result.accessRestriction.users = [];
delete result.accessRestrictionJson;
// TODO remove later once all apps have this attribute
result.xFrameOptions = result.xFrameOptions || 'SAMEORIGIN';
result.sso = !!result.sso; // make it bool
result.enableBackup = !!result.enableBackup; // make it bool
result.enableAutomaticUpdate = !!result.enableAutomaticUpdate; // make it bool
@@ -279,7 +276,6 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
const accessRestriction = data.accessRestriction || null;
const accessRestrictionJson = JSON.stringify(accessRestriction);
const memoryLimit = data.memoryLimit || 0;
const xFrameOptions = data.xFrameOptions || '';
const installationState = data.installationState || exports.ISTATE_PENDING_INSTALL;
const restoreConfigJson = data.restoreConfig ? JSON.stringify(data.restoreConfig) : null; // used when cloning
const sso = 'sso' in data ? data.sso : null;
@@ -293,10 +289,10 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
var queries = [];
queries.push({
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions,'
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, '
+ 'restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId, mailboxName, label, tagsJson) '
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson,
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, restoreConfigJson,
sso, debugModeJson, robotsTxt, ownerId, mailboxName, label, tagsJson ]
});
@@ -625,13 +621,13 @@ function getAddonConfigByAppId(appId, callback) {
});
}
function getAppIdByAddonConfigValue(addonId, name, value, callback) {
function getAppIdByAddonConfigValue(addonId, namePattern, value, callback) {
assert.strictEqual(typeof addonId, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof namePattern, 'string');
assert.strictEqual(typeof value, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT appId FROM appAddonConfigs WHERE addonId = ? AND name = ? AND value = ?', [ addonId, name, value ], function (error, results) {
database.query('SELECT appId FROM appAddonConfigs WHERE addonId = ? AND name LIKE ? AND value = ?', [ addonId, namePattern, value ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));

View File

@@ -90,7 +90,6 @@ var appdb = require('./appdb.js'),
taskmanager = require('./taskmanager.js'),
TransformStream = require('stream').Transform,
updateChecker = require('./updatechecker.js'),
url = require('url'),
util = require('util'),
uuid = require('uuid'),
validator = require('validator'),
@@ -241,20 +240,6 @@ function validateMemoryLimit(manifest, memoryLimit) {
return null;
}
// https://tools.ietf.org/html/rfc7034
function validateXFrameOptions(xFrameOptions) {
assert.strictEqual(typeof xFrameOptions, 'string');
if (xFrameOptions === 'DENY') return null;
if (xFrameOptions === 'SAMEORIGIN') return null;
var parts = xFrameOptions.split(' ');
if (parts.length !== 2 || parts[0] !== 'ALLOW-FROM') return new AppsError(AppsError.BAD_FIELD, 'xFrameOptions must be "DENY", "SAMEORIGIN" or "ALLOW-FROM uri"' );
var uri = url.parse(parts[1]);
return (uri.protocol === 'http:' || uri.protocol === 'https:') ? null : new AppsError(AppsError.BAD_FIELD, 'xFrameOptions ALLOW-FROM uri must be a valid http[s] uri' );
}
function validateDebugMode(debugMode) {
assert.strictEqual(typeof debugMode, 'object');
@@ -336,11 +321,12 @@ function validateDataDir(dataDir) {
return null;
}
function getDuplicateErrorDetails(error, location, domainObject, portBindings) {
function getDuplicateErrorDetails(error, location, domainObject, portBindings, alternateDomains) {
assert.strictEqual(error.reason, DatabaseError.ALREADY_EXISTS);
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof portBindings, 'object');
assert(Array.isArray(alternateDomains));
var match = error.message.match(/ER_DUP_ENTRY: Duplicate entry '(.*)' for key '(.*)'/);
if (!match) {
@@ -348,10 +334,17 @@ function getDuplicateErrorDetails(error, location, domainObject, portBindings) {
return new AppsError(AppsError.INTERNAL_ERROR, error);
}
// check if the location conflicts
// check if the location or alternateDomains conflicts
if (match[2] === 'subdomain') {
const fqdn = domains.fqdn(location, domainObject);
return new AppsError(AppsError.ALREADY_EXISTS, `subdomain '${fqdn}' is in use`);
// mysql reports a unique conflict with a dash: eg. domain:example.com subdomain:test => test-example.com
if (match[1] === `${location}-${domainObject.domain}`) return new AppsError(AppsError.ALREADY_EXISTS, `Domain '${domains.fqdn(location, domainObject)}' is in use`);
// check alternateDomains
let tmp = alternateDomains.filter(function (d) {
return match[1] === `${d.subdomain}-${d.domain}`;
});
if (tmp.length > 0) return new AppsError(AppsError.ALREADY_EXISTS, `Alternate domain '${tmp[0].subdomain}.${tmp[0].domain}' is in use`);
}
// check if any of the port bindings conflict
@@ -372,7 +365,7 @@ function getAppConfig(app) {
accessRestriction: app.accessRestriction,
portBindings: app.portBindings,
memoryLimit: app.memoryLimit,
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN',
robotsTxt: app.robotsTxt,
sso: app.sso,
alternateDomains: app.alternateDomains || [],
@@ -389,7 +382,7 @@ function removeInternalFields(app) {
return _.pick(app,
'id', 'appStoreId', 'installationState', 'installationProgress', 'runState', 'health',
'location', 'domain', 'fqdn', 'mailboxName',
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'xFrameOptions',
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit',
'sso', 'debugMode', 'robotsTxt', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags',
'label', 'alternateDomains', 'ownerId', 'env', 'enableAutomaticUpdate', 'dataDir');
}
@@ -402,8 +395,15 @@ function removeRestrictedFields(app) {
}
function getIconUrlSync(app) {
var iconPath = paths.APP_ICONS_DIR + '/' + app.id + '.png';
return fs.existsSync(iconPath) ? '/api/v1/apps/' + app.id + '/icon' : null;
const iconUrl = '/api/v1/apps/' + app.id + '/icon';
const userIconPath = `${paths.APP_ICONS_DIR}/${app.id}.user.png`;
if (safe.fs.existsSync(userIconPath)) return iconUrl;
const appstoreIconPath = `${paths.APP_ICONS_DIR}/${app.id}.png`;
if (safe.fs.existsSync(appstoreIconPath)) return iconUrl;
return null;
}
function postProcess(app, domainObjectMap) {
@@ -578,7 +578,6 @@ function install(data, user, auditSource, callback) {
cert = data.cert || null,
key = data.key || null,
memoryLimit = data.memoryLimit || 0,
xFrameOptions = data.xFrameOptions || 'SAMEORIGIN',
sso = 'sso' in data ? data.sso : null,
debugMode = data.debugMode || null,
robotsTxt = data.robotsTxt || null,
@@ -613,9 +612,6 @@ function install(data, user, auditSource, callback) {
error = validateMemoryLimit(manifest, memoryLimit);
if (error) return callback(error);
error = validateXFrameOptions(xFrameOptions);
if (error) return callback(error);
error = validateDebugMode(debugMode);
if (error) return callback(error);
@@ -672,7 +668,6 @@ function install(data, user, auditSource, callback) {
var data = {
accessRestriction: accessRestriction,
memoryLimit: memoryLimit,
xFrameOptions: xFrameOptions,
sso: sso,
debugMode: debugMode,
mailboxName: mailboxName,
@@ -685,7 +680,7 @@ function install(data, user, auditSource, callback) {
};
appdb.add(appId, appStoreId, manifest, location, domain, ownerId, translatePortBindings(portBindings, manifest), data, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, location, domainObject, portBindings));
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, location, domainObject, portBindings, data.alternateDomains));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, error.message));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -754,12 +749,6 @@ function configure(appId, data, user, auditSource, callback) {
if (error) return callback(error);
}
if ('xFrameOptions' in data) {
values.xFrameOptions = data.xFrameOptions;
error = validateXFrameOptions(values.xFrameOptions);
if (error) return callback(error);
}
if ('debugMode' in data) {
values.debugMode = data.debugMode;
error = validateDebugMode(values.debugMode);
@@ -813,6 +802,18 @@ function configure(appId, data, user, auditSource, callback) {
values.tags = data.tags;
}
if ('icon' in data) {
if (data.icon) {
if (!validator.isBase64(data.icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png'), Buffer.from(data.icon, 'base64'))) {
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
}
} else {
safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png'));
}
}
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));
@@ -836,10 +837,10 @@ function configure(appId, data, user, auditSource, callback) {
values.oldConfig = getAppConfig(app);
debug('Will configure app with id:%s values:%j', appId, values);
debug(`configure: id:${appId}`);
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_CONFIGURE, values, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, location, domainObject, portBindings));
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, location, domainObject, portBindings, data.alternateDomains));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -864,7 +865,7 @@ function update(appId, data, auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('Will update app with id:%s', appId);
debug(`update: id:${appId}`);
get(appId, function (error, app) {
if (error) return callback(error);
@@ -901,11 +902,11 @@ function update(appId, data, auditSource, callback) {
if (data.icon) {
if (!validator.isBase64(data.icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.png'), Buffer.from(data.icon, 'base64'))) {
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png'), Buffer.from(data.icon, 'base64'))) {
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
}
} else {
safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, appId + '.png'));
safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png'));
}
}
@@ -1119,7 +1120,6 @@ function clone(appId, data, user, auditSource, callback) {
installationState: appdb.ISTATE_PENDING_CLONE,
memoryLimit: app.memoryLimit,
accessRestriction: app.accessRestriction,
xFrameOptions: app.xFrameOptions,
restoreConfig: { backupId: backupId, backupFormat: backupInfo.format },
sso: !!app.sso,
mailboxName: mailboxName,
@@ -1129,7 +1129,7 @@ function clone(appId, data, user, auditSource, callback) {
};
appdb.add(newAppId, app.appStoreId, manifest, location, domain, ownerId, translatePortBindings(portBindings, manifest), data, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, location, domainObject, portBindings));
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, location, domainObject, portBindings, []));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
purchaseApp({ appId: newAppId, appstoreId: app.appStoreId, manifestId: manifest.id }, function (error) {
@@ -1218,6 +1218,8 @@ function stop(appId, callback) {
function checkManifestConstraints(manifest) {
assert(manifest && typeof manifest === 'object');
if (manifest.manifestVersion > 2) return new AppsError(AppsError.BAD_FIELD, 'Manifest version must be <= 2');
if (!manifest.dockerImage) return new AppsError(AppsError.BAD_FIELD, 'Missing dockerImage'); // dockerImage is optional in manifest
if (semver.valid(manifest.maxBoxVersion) && semver.gt(config.version(), manifest.maxBoxVersion)) {
@@ -1462,7 +1464,7 @@ function downloadFile(appId, filePath, callback) {
transform: function (chunk, ignoredEncoding, callback) {
this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
while (true) {
for (;;) {
if (this._buffer.length < 8) break; // header is 8 bytes
var type = this._buffer.readUInt8(0);

View File

@@ -387,14 +387,15 @@ function registerCloudron(data, callback) {
});
}
function registerWithLicense(license, callback) {
function registerWithLicense(license, domain, callback) {
assert.strictEqual(typeof license, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
getCloudronToken(function (error, token) {
if (token) return callback(new AppstoreError(AppstoreError.ALREADY_REGISTERED));
registerCloudron({ license }, callback);
registerCloudron({ license, domain }, callback);
});
}
@@ -446,7 +447,7 @@ function createTicket(info, callback) {
let url = config.apiServerOrigin() + '/api/v1/ticket';
info.supportEmail = custom.supportEmail(); // destination address for tickets
info.supportEmail = custom.spec().support.email; // destination address for tickets
superagent.post(url).query({ accessToken: token }).send(info).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));

View File

@@ -420,10 +420,15 @@ function removeIcon(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
fs.unlink(path.join(paths.APP_ICONS_DIR, app.id + '.png'), function (error) {
if (error && error.code !== 'ENOENT') debugApp(app, 'cannot remove icon : %s', error);
callback(null);
});
if (!safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, app.id + '.png'))) {
if (safe.error.code !== 'ENOENT') debugApp(app, 'cannot remove icon : %s', safe.error);
}
if (!safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, app.id + '.user.png'))) {
if (safe.error.code !== 'ENOENT') debugApp(app, 'cannot remove user icon : %s', safe.error);
}
callback(null);
}
function cleanupLogs(app, callback) {
@@ -697,6 +702,8 @@ function update(app, callback) {
// FIXME: this does not handle option changes (like multipleDatabases)
var unusedAddons = _.omit(app.manifest.addons, Object.keys(app.updateConfig.manifest.addons));
const FORCED_UPDATE = (app.installationState === appdb.ISTATE_PENDING_FORCE_UPDATE);
async.series([
// this protects against the theoretical possibility of an app being marked for update from
// a previous version of box code
@@ -704,7 +711,7 @@ function update(app, callback) {
verifyManifest.bind(null, app.updateConfig.manifest),
function (next) {
if (app.installationState === appdb.ISTATE_PENDING_FORCE_UPDATE) return next(null);
if (FORCED_UPDATE) return next(null);
async.series([
updateApp.bind(null, app, { installationProgress: '15, Backing up app' }),
@@ -791,6 +798,9 @@ function update(app, callback) {
debugApp(app, 'Error updating app: %s', error);
updateApp(app, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message, updateTime: new Date() }, callback.bind(null, error));
} else {
// do not spam the notifcation view
if (FORCED_UPDATE) return callback();
eventlog.add(eventlog.ACTION_APP_UPDATE_FINISH, auditsource.APP_TASK, { app: app, success: true }, callback);
}
});

View File

@@ -174,8 +174,7 @@ function getConfig(callback) {
memory: os.totalmem(),
provider: config.provider(),
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
features: custom.features(),
supportEmail: custom.supportEmail()
uiSpec: custom.uiSpec()
});
});
}

View File

@@ -1,7 +1,6 @@
'use strict';
exports = module.exports = {
API_LOCATION: 'api', // this is unused but reserved for future use (#403)
SMTP_LOCATION: 'smtp',
IMAP_LOCATION: 'imap',
@@ -18,7 +17,6 @@ exports = module.exports = {
],
ADMIN_LOCATION: 'my',
DKIM_SELECTOR: 'cloudron',
NGINX_DEFAULT_CONFIG_FILE_NAME: 'default.conf',

View File

@@ -1,44 +1,67 @@
'use strict';
let debug = require('debug')('box:features'),
let config = require('./config.js'),
debug = require('debug')('box:features'),
lodash = require('lodash'),
paths = require('./paths.js'),
safe = require('safetydance'),
yaml = require('js-yaml');
exports = module.exports = {
features: features,
supportEmail: supportEmail,
alertsEmail: alertsEmail,
sendAlertsToCloudronAdmins: sendAlertsToCloudronAdmins
uiSpec: uiSpec,
spec: spec
};
const gCustom = (function () {
const DEFAULT_SPEC = {
appstore: {
blacklist: [],
whitelist: null // null imples, not set. this is an object and not an array
},
backups: {
configurable: true
},
domains: {
dynamicDns: true,
changeDashboardDomain: true
},
subscription: {
configurable: true
},
support: {
email: 'support@cloudron.io',
remoteSupport: true,
ticketFormBody:
'Use this form to open support tickets. You can also write directly to [support@cloudron.io](mailto:support@cloudron.io).\n\n'
+ `* [Knowledge Base & App Docs](${config.webServerOrigin()}/documentation/apps/?support_view)\n`
+ `* [Custom App Packaging & API](${config.webServerOrigin()}/developer/packaging/?support_view)\n`
+ '* [Forum](https://forum.cloudron.io/)\n\n',
submitTickets: true
},
alerts: {
email: '',
notifyCloudronAdmins: false
},
footer: {
body: '&copy; 2019 [Cloudron](https://cloudron.io) [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)'
}
};
const gSpec = (function () {
try {
if (!safe.fs.existsSync(paths.CUSTOM_FILE)) return {};
return yaml.safeLoad(safe.fs.readFileSync(paths.CUSTOM_FILE, 'utf8'));
if (!safe.fs.existsSync(paths.CUSTOM_FILE)) return DEFAULT_SPEC;
const c = yaml.safeLoad(safe.fs.readFileSync(paths.CUSTOM_FILE, 'utf8'));
return lodash.merge({}, DEFAULT_SPEC, c);
} catch (e) {
debug(`Error loading features file from ${paths.CUSTOM_FILE} : ${e.message}`);
return {};
return DEFAULT_SPEC;
}
})();
function features() {
return {
dynamicDns: safe.query(gCustom, 'features.dynamicDns', true),
remoteSupport: safe.query(gCustom, 'features.remoteSupport', true),
subscription: safe.query(gCustom, 'features.subscription', true),
configureBackup: safe.query(gCustom, 'features.configureBackup', true)
};
// flags sent to the UI. this is separate because we have values that are secret to the backend
function uiSpec() {
return gSpec;
}
function supportEmail() {
return safe.query(gCustom, 'support.email', 'support@cloudron.io');
}
function alertsEmail() {
return safe.query(gCustom, 'alerts.email', '');
}
function sendAlertsToCloudronAdmins() {
return safe.query(gCustom, 'alerts.notifyCloudronAdmins', true);
}
function spec() {
return gSpec;
}

View File

@@ -111,7 +111,7 @@ function upsert(domainObject, location, type, values, callback) {
name: name,
data: value,
priority: priority,
ttl: 1
ttl: 30 // Recent DO DNS API break means this value must atleast be 30
};
if (i >= result.length) {

View File

@@ -15,14 +15,14 @@ var assert = require('assert'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
Namecheap = require('namecheap'),
safe = require('safetydance'),
superagent = require('superagent'),
sysinfo = require('../sysinfo.js'),
util = require('util'),
waitForDns = require('./waitfordns.js');
waitForDns = require('./waitfordns.js'),
xml2js = require('xml2js');
function formatError(response) {
return util.format('NameCheap DNS error [%s] %j', response.code, response.message);
}
const ENDPOINT = 'https://api.namecheap.com/xml.response';
function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER;
@@ -33,37 +33,19 @@ function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
// Only send required fields - https://www.namecheap.com/support/api/methods/domains-dns/set-hosts.aspx
function mapHosts(hosts) {
return hosts.map(function (host) {
let tmp = {};
tmp.TTL = '300';
tmp.RecordType = host.RecordType || host.Type;
tmp.HostName = host.HostName || host.Name;
tmp.Address = host.Address;
if (tmp.RecordType === 'MX') {
tmp.EmailType = 'MX';
if (host.MXPref) tmp.MXPref = host.MXPref;
}
return tmp;
});
}
function getApi(dnsConfig, callback) {
function getQuery(dnsConfig, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
// Note that for all NameCheap calls to go through properly, the public IP returned by the getPublicIp method below must be whitelisted on NameCheap's API dashboard
let namecheap = new Namecheap(dnsConfig.username, dnsConfig.token, ip);
namecheap.setUsername(dnsConfig.username);
callback(null, namecheap);
callback(null, {
ApiUser: dnsConfig.username,
ApiKey: dnsConfig.token,
UserName: dnsConfig.username,
ClientIp: ip
});
});
}
@@ -74,15 +56,31 @@ function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
getApi(dnsConfig, function (error, namecheap) {
getQuery(dnsConfig, function (error, query) {
if (error) return callback(error);
namecheap.domains.dns.getHosts(zoneName, function (error, result) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(error)));
query.Command = 'namecheap.domains.dns.getHosts';
query.SLD = zoneName.split('.')[0];
query.TLD = zoneName.split('.')[1];
debug('entire getInternal response: %j', result);
superagent.get(ENDPOINT).query(query).end(function (error, result) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error));
return callback(null, result['DomainDNSGetHostsResult']['host']);
var parser = new xml2js.Parser();
parser.parseString(result.text, function (error, result) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error));
var tmp = result.ApiResponse;
if (tmp['$'].Status !== 'OK') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, safe.query(tmp, 'Errors[0].Error[0]._', 'Invalid response')));
if (!tmp.CommandResponse[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
if (!tmp.CommandResponse[0].DomainDNSGetHostsResult[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
var hosts = result.ApiResponse.CommandResponse[0].DomainDNSGetHostsResult[0].host.map(function (h) {
return h['$'];
});
callback(null, hosts);
});
});
});
}
@@ -93,15 +91,42 @@ function setInternal(dnsConfig, zoneName, hosts, callback) {
assert(Array.isArray(hosts));
assert.strictEqual(typeof callback, 'function');
let mappedHosts = mapHosts(hosts);
getApi(dnsConfig, function (error, namecheap) {
getQuery(dnsConfig, function (error, query) {
if (error) return callback(error);
namecheap.domains.dns.setHosts(zoneName, mappedHosts, function (error, result) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(error)));
query.Command = 'namecheap.domains.dns.setHosts';
query.SLD = zoneName.split('.')[0];
query.TLD = zoneName.split('.')[1];
return callback(null, result);
// Map to query params https://www.namecheap.com/support/api/methods/domains-dns/set-hosts.aspx
hosts.forEach(function (host, i) {
var n = i+1; // api starts with 1 not 0
query['TTL' + n] = '300'; // keep it low
query['HostName' + n] = host.HostName || host.Name;
query['RecordType' + n] = host.RecordType || host.Type;
query['Address' + n] = host.Address;
if (host.Type === 'MX') {
query['EmailType' + n] = 'MX';
if (host.MXPref) query['MXPref' + n] = host.MXPref;
}
});
superagent.post(ENDPOINT).query(query).end(function (error, result) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error));
var parser = new xml2js.Parser();
parser.parseString(result.text, function (error, result) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error));
var tmp = result.ApiResponse;
if (tmp['$'].Status !== 'OK') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, safe.query(tmp, 'Errors[0].Error[0]._', 'Invalid response')));
if (!tmp.CommandResponse[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
if (!tmp.CommandResponse[0].DomainDNSSetHostsResult[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
if (tmp.CommandResponse[0].DomainDNSSetHostsResult[0]['$'].IsSuccess !== 'true') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
callback(null);
});
});
});
}

View File

@@ -131,7 +131,7 @@ function getInternal(dnsConfig, zoneName, name, type, callback) {
result.body.records.forEach(function (r) {
// name.com api simply strips empty properties
r.host = r.host || '@';
r.host = r.host || '';
});
var results = result.body.records.filter(function (r) {
@@ -153,7 +153,7 @@ function upsert(domainObject, location, type, values, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
name = domains.getName(domainObject, location, type) || '';
debug(`upsert: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
@@ -174,7 +174,7 @@ function get(domainObject, location, type, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
name = domains.getName(domainObject, location, type) || '';
getInternal(dnsConfig, zoneName, name, type, function (error, result) {
if (error) return callback(error);
@@ -196,7 +196,7 @@ function del(domainObject, location, type, values, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
name = domains.getName(domainObject, location, type) || '';
debug(`del: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);

View File

@@ -181,14 +181,17 @@ function createSubcontainer(app, name, cmd, options, callback) {
var manifest = app.manifest;
var exposedPorts = {}, dockerPortBindings = { };
var domain = app.fqdn;
// TODO: these should all have the CLOUDRON_ prefix
var stdEnv = [
const envPrefix = manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
let stdEnv = [
'CLOUDRON=1',
'CLOUDRON_PROXY_IP=172.18.0.1',
'WEBADMIN_ORIGIN=' + config.adminOrigin(),
'API_ORIGIN=' + config.adminOrigin(),
'APP_ORIGIN=https://' + domain,
'APP_DOMAIN=' + domain
`CLOUDRON_APP_HOSTNAME=${name}`,
`${envPrefix}WEBADMIN_ORIGIN=${config.adminOrigin()}`,
`${envPrefix}API_ORIGIN=${config.adminOrigin()}`,
`${envPrefix}APP_ORIGIN=https://${domain}`,
`${envPrefix}APP_DOMAIN=${domain}`
];
// docker portBindings requires ports to be exposed
@@ -235,9 +238,9 @@ function createSubcontainer(app, name, cmd, options, callback) {
// Note that Hostname has no effect on DNS. We have to use the --net-alias for dns.
// Hostname cannot be set with container NetworkMode
var containerOptions = {
name: name, // used for filtering logs
name: name, // for referencing containers
Tty: isAppContainer,
Hostname: app.id, // set to something 'constant' so app containers can use this to communicate (across app updates)
Hostname: name,
Image: app.manifest.dockerImage,
Cmd: (isAppContainer && app.debugMode && app.debugMode.cmd) ? app.debugMode.cmd : cmd,
Env: stdEnv.concat(addonEnv).concat(portEnv).concat(appEnv),
@@ -273,10 +276,17 @@ function createSubcontainer(app, name, cmd, options, callback) {
},
CpuShares: 512, // relative to 1024 for system processes
VolumesFrom: isAppContainer ? null : [ app.containerId + ':rw' ],
NetworkMode: 'cloudron',
NetworkMode: 'cloudron', // user defined bridge network
Dns: ['172.18.0.1'], // use internal dns
DnsSearch: ['.'], // use internal dns
SecurityOpt: [ 'apparmor=docker-cloudron-app' ]
},
NetworkingConfig: {
EndpointsConfig: {
cloudron: {
Aliases: [ name ] // this allows sub-containers reach app containers by name
}
}
}
};

View File

@@ -145,7 +145,6 @@ function validateHostname(location, domainObject) {
const hostname = fqdn(location, domainObject);
const RESERVED_LOCATIONS = [
constants.API_LOCATION,
constants.SMTP_LOCATION,
constants.IMAP_LOCATION
];

View File

@@ -19,6 +19,7 @@ function startGraphite(existingInfra, callback) {
if (existingInfra.version === infra.version && infra.images.graphite.tag === existingInfra.images.graphite.tag) return callback();
const cmd = `docker run --restart=always -d --name="graphite" \
--hostname graphite \
--net cloudron \
--net-alias graphite \
--log-driver syslog \

View File

@@ -6,7 +6,7 @@
exports = module.exports = {
// a version change recreates all containers with latest docker config
'version': '48.14.0',
'version': '48.15.0',
'baseImages': [
{ repo: 'cloudron/base', tag: 'cloudron/base:1.0.0@sha256:147a648a068a2e746644746bbfb42eb7a50d682437cead3c67c933c546357617' }
@@ -19,7 +19,7 @@ exports = module.exports = {
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.0.2@sha256:6dcee0731dfb9b013ed94d56205eee219040ee806c7e251db3b3886eaa4947ff' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.0.2@sha256:95e006390ddce7db637e1672eb6f3c257d3c2652747424f529b1dee3cbe6728c' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.0.0@sha256:8a88dd334b62b578530a014ca1a2425a54cb9df1e475f5d3a36806e5cfa22121' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.2.0@sha256:20e4d2508dcf712eb56481067993ae39bf541d793d44f99f6a41d630ad941d9e' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.3.1@sha256:9693e3ae42a12a7ac8cf5df94d828d46f5b22b4e2e1c7d1bc614d6ee2a22c365' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.2@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' },
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:0.1.0@sha256:e177c5bf5f38c84ce1dea35649c22a1b05f96eec67a54a812c5a35e585670f0f' }
}

View File

@@ -520,22 +520,25 @@ function userSearchSftp(req, res, next) {
users.getByUsername(username, function (error, user) {
if (error) return next(new ldap.OperationsError(error.toString()));
if (!user.admin) return next(new ldap.InsufficientAccessRightsError('Not authorized'));
apps.hasAccessTo(app, user, function (error, hasAccess) {
if (error) return next(new ldap.OperationsError(error.toString()));
if (!hasAccess) return next(new ldap.InsufficientAccessRightsError('Not authorized'));
var obj = {
dn: ldap.parseDN(`cn=${username}@${appFqdn},ou=sftp,dc=cloudron`).toString(),
attributes: {
homeDirectory: path.join('/app/data', app.id, 'data'),
objectclass: ['user'],
objectcategory: 'person',
cn: user.id,
uid: `${username}@${appFqdn}`, // for bind after search
uidNumber: uidNumber, // unix uid for ftp access
gidNumber: uidNumber // unix gid for ftp access
}
};
var obj = {
dn: ldap.parseDN(`cn=${username}@${appFqdn},ou=sftp,dc=cloudron`).toString(),
attributes: {
homeDirectory: path.join('/app/data', app.id, 'data'),
objectclass: ['user'],
objectcategory: 'person',
cn: user.id,
uid: `${username}@${appFqdn}`, // for bind after search
uidNumber: uidNumber, // unix uid for ftp access
gidNumber: uidNumber // unix gid for ftp access
}
};
finalSend([ obj ], req, res, next);
finalSend([ obj ], req, res, next);
});
});
});
}
@@ -557,13 +560,13 @@ function authenticateMailAddon(req, res, next) {
if (addonId === 'recvmail' && !domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
let name;
if (addonId === 'sendmail') name = 'MAIL_SMTP_PASSWORD';
else if (addonId === 'recvmail') name = 'MAIL_IMAP_PASSWORD';
let namePattern; // manifest v2 has a CLOUDRON_ prefix for names
if (addonId === 'sendmail') namePattern = '%MAIL_SMTP_PASSWORD';
else if (addonId === 'recvmail') namePattern = '%MAIL_IMAP_PASSWORD';
else return next(new ldap.OperationsError('Invalid DN'));
// note: with sendmail addon, apps can send mail without a mailbox (unlike users)
appdb.getAppIdByAddonConfigValue(addonId, name, req.credentials || '', function (error, appId) {
appdb.getAppIdByAddonConfigValue(addonId, namePattern, req.credentials || '', function (error, appId) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return next(new ldap.OperationsError(error.message));
if (appId) { // matched app password
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: email }, { appId: appId, addonId: addonId });

View File

@@ -210,10 +210,14 @@ function verifyRelay(relay, callback) {
});
}
function checkDkim(domain, callback) {
var dkim = {
domain: `${constants.DKIM_SELECTOR}._domainkey.${domain}`,
name: `${constants.DKIM_SELECTOR}._domainkey`,
function checkDkim(mailDomain, callback) {
assert.strictEqual(typeof mailDomain, 'object');
assert.strictEqual(typeof callback, 'function');
const domain = mailDomain.domain;
let dkim = {
domain: `${mailDomain.dkimSelector}._domainkey.${domain}`,
name: `${mailDomain.dkimSelector}._domainkey`,
type: 'TXT',
expected: null,
value: null,
@@ -289,13 +293,25 @@ function checkMx(domain, mailFqdn, callback) {
dns.resolve(mx.domain, mx.type, DNS_OPTIONS, function (error, mxRecords) {
if (error) return callback(error, mx);
if (mxRecords.length === 0) return callback(null, mx);
if (mxRecords.length !== 0) {
mx.status = mxRecords.length == 1 && mxRecords[0].exchange === mailFqdn;
mx.value = mxRecords.map(function (r) { return r.priority + ' ' + r.exchange + '.'; }).join(' ');
}
mx.status = mxRecords.length == 1 && mxRecords[0].exchange === mailFqdn;
mx.value = mxRecords.map(function (r) { return r.priority + ' ' + r.exchange + '.'; }).join(' ');
callback(null, mx);
if (mx.status) return callback(null, mx); // MX record is "my."
// cloudflare might create a conflict subdomain (https://support.cloudflare.com/hc/en-us/articles/360020296512-DNS-Troubleshooting-FAQ)
dns.resolve(mxRecords[0].exchange, 'A', DNS_OPTIONS, function (error, mxIps) {
if (error || mxIps.length !== 1) return callback(null, mx);
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(null, mx);
mx.status = mxIps[0] === ip;
callback(null, mx);
});
});
});
}
@@ -483,28 +499,28 @@ function getStatus(domain, callback) {
const mailFqdn = config.mailFqdn();
getDomain(domain, function (error, result) {
getDomain(domain, function (error, mailDomain) {
if (error) return callback(error);
let checks = [];
if (result.enabled) {
if (mailDomain.enabled) {
checks.push(
recordResult('dns.mx', checkMx.bind(null, domain, mailFqdn)),
recordResult('dns.dmarc', checkDmarc.bind(null, domain))
);
}
if (result.relay.provider === 'cloudron-smtp') {
if (mailDomain.relay.provider === 'cloudron-smtp') {
// these tests currently only make sense when using Cloudron's SMTP server at this point
checks.push(
recordResult('dns.spf', checkSpf.bind(null, domain, mailFqdn)),
recordResult('dns.dkim', checkDkim.bind(null, domain)),
recordResult('dns.dkim', checkDkim.bind(null, mailDomain)),
recordResult('dns.ptr', checkPtr.bind(null, mailFqdn)),
recordResult('relay', checkOutboundPort25),
recordResult('rbl', checkRblStatus.bind(null, domain))
);
} else if (result.relay.provider !== 'noop') {
checks.push(recordResult('relay', checkSmtpRelay.bind(null, result.relay)));
} else if (mailDomain.relay.provider !== 'noop') {
checks.push(recordResult('relay', checkSmtpRelay.bind(null, mailDomain.relay)));
}
async.parallel(checks, function () {
@@ -596,13 +612,14 @@ function createMailConfig(mailFqdn, callback) {
const enableRelay = relay.provider !== 'cloudron-smtp' && relay.provider !== 'noop',
host = relay.host || '',
port = relay.port || 25,
authType = relay.username ? 'plain' : '',
username = relay.username || '',
password = relay.password || '';
if (!enableRelay) return;
if (!safe.fs.appendFileSync(paths.ADDON_CONFIG_DIR + '/mail/smtp_forward.ini',
`[${domain.domain}]\nenable_outbound=true\nhost=${host}\nport=${port}\nenable_tls=true\nauth_type=plain\nauth_user=${username}\nauth_pass=${password}\n\n`, 'utf8')) {
`[${domain.domain}]\nenable_outbound=true\nhost=${host}\nport=${port}\nenable_tls=true\nauth_type=${authType}\nauth_user=${username}\nauth_pass=${password}\n\n`, 'utf8')) {
return callback(new Error('Could not create mail var file:' + safe.error.message));
}
});
@@ -757,9 +774,10 @@ function txtRecordsWithSpf(domain, mailFqdn, callback) {
});
}
function ensureDkimKeySync(domain) {
assert.strictEqual(typeof domain, 'string');
function ensureDkimKeySync(mailDomain) {
assert.strictEqual(typeof mailDomain, 'object');
const domain = mailDomain.domain;
const dkimPath = path.join(paths.MAIL_DATA_DIR, `dkim/${domain}`);
const dkimPrivateKeyFile = path.join(dkimPath, 'private');
const dkimPublicKeyFile = path.join(dkimPath, 'public');
@@ -782,7 +800,10 @@ function ensureDkimKeySync(domain) {
if (!safe.child_process.execSync('openssl genrsa -out ' + dkimPrivateKeyFile + ' 1024')) return new MailError(MailError.INTERNAL_ERROR, safe.error);
if (!safe.child_process.execSync('openssl rsa -in ' + dkimPrivateKeyFile + ' -out ' + dkimPublicKeyFile + ' -pubout -outform PEM')) return new MailError(MailError.INTERNAL_ERROR, safe.error);
if (!safe.fs.writeFileSync(dkimSelectorFile, constants.DKIM_SELECTOR, 'utf8')) return new MailError(MailError.INTERNAL_ERROR, safe.error);
if (!safe.fs.writeFileSync(dkimSelectorFile, mailDomain.dkimSelector, 'utf8')) return new MailError(MailError.INTERNAL_ERROR, safe.error);
// if the 'yellowtent' user of OS and the 'cloudron' user of mail container don't match, the keys become inaccessible by mail code
if (!safe.fs.chmodSync(dkimPrivateKeyFile, 0o644)) return new MailError(MailError.INTERNAL_ERROR, safe.error);
return null;
}
@@ -813,11 +834,11 @@ function upsertDnsRecords(domain, mailFqdn, callback) {
debug(`upsertDnsRecords: updating mail dns records of domain ${domain} and mail fqdn ${mailFqdn}`);
maildb.get(domain, function (error, result) {
maildb.get(domain, function (error, mailDomain) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
error = ensureDkimKeySync(domain);
error = ensureDkimKeySync(mailDomain);
if (error) return callback(error);
if (process.env.BOX_ENV === 'test') return callback();
@@ -826,11 +847,11 @@ function upsertDnsRecords(domain, mailFqdn, callback) {
if (!dkimKey) return callback(new MailError(MailError.INTERNAL_ERROR, new Error('Failed to read dkim public key')));
// t=s limits the domainkey to this domain and not it's subdomains
var dkimRecord = { subdomain: `${constants.DKIM_SELECTOR}._domainkey`, domain: domain, type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
var dkimRecord = { subdomain: `${mailDomain.dkimSelector}._domainkey`, domain: domain, type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
var records = [ ];
records.push(dkimRecord);
if (result.enabled) {
if (mailDomain.enabled) {
records.push({ subdomain: '_dmarc', domain: domain, type: 'TXT', values: [ '"v=DMARC1; p=reject; pct=100"' ] });
records.push({ subdomain: '', domain: domain, type: 'MX', values: [ '10 ' + mailFqdn + '.' ] });
}
@@ -888,7 +909,9 @@ function addDomain(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
maildb.add(domain, function (error) {
const dkimSelector = domain === config.adminDomain() ? 'cloudron' : ('cloudron-' + config.adminDomain().replace(/\./g, ''));
maildb.add(domain, { dkimSelector }, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, 'Domain already exists'));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'No such domain'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));

View File

@@ -4,10 +4,37 @@ Dear Cloudron Admin,
The application '<%= title %>' installed at <%= appFqdn %> was updated to app package version <%= version %>.
Changes:
<%= changelog %>
Powered by https://cloudron.io
Sent at: <%= new Date().toUTCString() %>
<% } else { %>
<center>
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
<h3>Dear <%= cloudronName %> Admin,</h3>
<br/>
<div style="width: 650px; text-align: left;">
The application '<%= title %>' installed at <%= appFqdn %> was updated to app package version <%= version %>.
<h5>Changelog:</h5>
<%- changelogHTML %>
</div>
<br/>
<br/>
<div style="font-size: 10px; color: #333333; background: #ffffff;">
Powered by <a href="https://cloudron.io">Cloudron</a>.<br/>
Sent at: <%= new Date().toUTCString() %>
</div>
</center>
<% } %>

View File

@@ -3,7 +3,7 @@
Dear Cloudron Admin,
<% for (var i = 0; i < apps.length; i++) { -%>
A new version <%= apps[i].updateInfo.manifest.version %> of the app '<%= apps[i].app.manifest.title %>' installed at <%= apps[i].app.fqdn %> is available!
A new version <%= apps[i].updateInfo.manifest.version %> of the app '<%= apps[i].app.manifest.title %>' installed at <%= apps[i].app.fqdn %> is available.
Changes:
<%= apps[i].updateInfo.manifest.changelog %>
@@ -28,10 +28,12 @@ Sent at: <%= new Date().toUTCString() %>
<h3>Dear <%= cloudronName %> Admin,</h3>
<br/>
<div style="width: 650px; text-align: left;">
<% for (var i = 0; i < apps.length; i++) { -%>
<p>
A new version <%= apps[i].updateInfo.manifest.version %> of the app '<%= apps[i].app.manifest.title %>' installed at <a href="https://<%= apps[i].app.fqdn %>"><%= apps[i].app.fqdn %></a> is available!
A new version <%= apps[i].updateInfo.manifest.version %> of the app '<%= apps[i].app.manifest.title %>' installed at <a href="https://<%= apps[i].app.fqdn %>"><%= apps[i].app.fqdn %></a> is available.
</p>
<h5>Changelog:</h5>
@@ -40,21 +42,20 @@ Sent at: <%= new Date().toUTCString() %>
<br/>
<% } -%>
<% if (!hasSubscription) { %>
<% if (!hasSubscription) { -%>
<p>Keep your Cloudron automatically up-to-date and secure by upgrading to a <a href="<%= webadminUrl %>/#/settings">paid plan</a>.</p>
<% } else { -%>
<% } else { -%>
<p>
<br/>
<center><a href="<%= webadminUrl %>">Update now</a></center>
<br/>
</p>
<% } -%>
<br/>
<% } -%>
</div>
<div style="font-size: 10px; color: #333333; background: #ffffff;">
Powered by <a href="https://cloudron.io">Cloudron</a>.
Powered by <a href="https://cloudron.io">Cloudron</a>.<br/>
Sent at: <%= new Date().toUTCString() %>
</div>
</center>

View File

@@ -1,58 +1,48 @@
{
"app_updated.ejs": {
"format": "html",
"title": "WordPress",
"appFqdn": "updated.smartserver.io",
"version": "1.3.4",
"changelog": "* This has changed\n * and that as well",
"changelogHTML": "<ul><li>This has changed</li><li>and that as well</li></ul>",
"cloudronName": "Smartserver",
"cloudronAvatarUrl": "https://cloudron.io/img/logo.png"
},
"app_updates_available.ejs": {
"format": "html",
"webadminUrl": "https://my.cloudron.io",
"cloudronName": "Smartserver",
"cloudronAvatarUrl": "https://cloudron.io/img/logo.png",
"info": {
"pendingBoxUpdate": {
"version": "1.3.7",
"changelog": [
"Feature one",
"Feature two"
]
},
"pendingAppUpdates": [{
"manifest": {
"title": "Wordpress",
"version": "1.2.3",
"changelog": "* This has changed\n * and that as well"
}
}],
"finishedBoxUpdates": [{
"boxUpdateInfo": {
"version": "1.0.1",
"changelog": [
"Feature one",
"Feature two"
]
}
}, {
"boxUpdateInfo": {
"version": "1.0.2",
"changelog": [
"Feature one",
"Feature two",
"Feature three"
]
}
}],
"finishedAppUpdates": [{
"toManifest": {
"title": "Rocket.Chat",
"version": "0.2.1",
"changelog": "* This has changed\n * and that as well\n * some more"
}
}, {
"toManifest": {
"title": "Redmine",
"version": "1.2.1",
"changelog": "* This has changed\n * and that as well\n * some more"
}
}],
"certRenewals": [],
"finishedBackups": [],
"usersAdded": [],
"usersRemoved": [],
"hasSubscription": false
}
"hasSubscription": true,
"apps": [{
"updateInfo": {
"manifest": {
"version": "1.4.3"
}
},
"app": {
"fqdn": "site.smartserver.io",
"manifest": {
"title": "WordPress"
}
},
"changelog": "* This has changed\n * and that as well",
"changelogHTML": "<ul><li>This has changed</li><li>and that as well</li></ul>"
}, {
"updateInfo": {
"manifest": {
"version": "0.1.3"
}
},
"app": {
"fqdn": "another.smartserver.io",
"manifest": {
"title": "RocketChat"
}
},
"changelog": "* This has changed\n * and that as well",
"changelogHTML": "<ul><li>This has changed</li><li>and that as well</li></ul>"
}]
}
}

View File

@@ -19,7 +19,7 @@ var assert = require('assert'),
DatabaseError = require('./databaseerror.js'),
safe = require('safetydance');
var MAILDB_FIELDS = [ 'domain', 'enabled', 'mailFromValidation', 'catchAllJson', 'relayJson' ].join(',');
var MAILDB_FIELDS = [ 'domain', 'enabled', 'mailFromValidation', 'catchAllJson', 'relayJson', 'dkimSelector' ].join(',');
function postProcess(data) {
data.enabled = !!data.enabled; // int to boolean
@@ -34,10 +34,12 @@ function postProcess(data) {
return data;
}
function add(domain, callback) {
function add(domain, data, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO mail (domain) VALUES (?)', [ domain ], function (error) {
database.query('INSERT INTO mail (domain, dkimSelector) VALUES (?, ?)', [ domain, data.dkimSelector || 'cloudron' ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'mail domain already exists'));
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND), 'no such domain');
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));

View File

@@ -289,7 +289,7 @@ function appDied(mailTo, app) {
from: mailConfig.notificationFrom,
to: mailTo,
subject: util.format('[%s] App %s is down', mailConfig.cloudronName, app.fqdn),
text: render('app_down.ejs', { title: app.manifest.title, appFqdn: app.fqdn, supportEmail: custom.supportEmail(), format: 'text' })
text: render('app_down.ejs', { title: app.manifest.title, appFqdn: app.fqdn, supportEmail: custom.spec().support.email, format: 'text' })
};
sendMail(mailOptions);
@@ -306,11 +306,29 @@ function appUpdated(mailTo, app, callback) {
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
var converter = new showdown.Converter();
var templateData = {
title: app.manifest.title,
appFqdn: app.fqdn,
version: app.manifest.version,
changelog: app.manifest.changelog,
changelogHTML: converter.makeHtml(app.manifest.changelog),
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
var mailOptions = {
from: mailConfig.notificationFrom,
to: mailTo,
subject: util.format('[%s] App %s was updated', mailConfig.cloudronName, app.fqdn),
text: render('app_updated.ejs', { title: app.manifest.title, appFqdn: app.fqdn, version: app.manifest.version, format: 'text' })
subject: `[${mailConfig.cloudronName}] App ${app.fqdn} was updated`,
text: render('app_updated.ejs', templateDataText),
html: render('app_updated.ejs', templateDataHTML)
};
sendMail(mailOptions, callback);

View File

@@ -112,7 +112,7 @@ function listByUserIdPaged(userId, page, perPage, callback) {
var data = [ userId ];
var query = 'SELECT ' + NOTIFICATION_FIELDS + ' FROM notifications WHERE userId=?';
query += ' ORDER BY creationTime, title DESC LIMIT ?,?';
query += ' ORDER BY creationTime DESC LIMIT ?,?';
data.push((page-1)*perPage);
data.push(perPage);

View File

@@ -193,8 +193,8 @@ function oomEvent(eventId, app, addon, containerId, event, callback) {
message = 'The container has been restarted automatically. Consider increasing the [memory limit](https://docs.docker.com/v17.09/edge/engine/reference/commandline/update/#update-a-containers-kernel-memory-constraints)';
}
if (custom.alertsEmail()) mailer.oomEvent(custom.alertsEmail(), program, event);
if (!custom.sendAlertsToCloudronAdmins()) return callback();
if (custom.spec().alerts.email) mailer.oomEvent(custom.spec().alerts.email, program, event);
if (!custom.spec().alerts.notifyCloudronAdmins) return callback();
actionForAllAdmins([], function (admin, done) {
mailer.oomEvent(admin.email, program, event);
@@ -208,8 +208,8 @@ function appUp(eventId, app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (custom.alertsEmail()) mailer.appUp(custom.alertsEmail(), app);
if (!custom.sendAlertsToCloudronAdmins()) return callback();
if (custom.spec().alerts.email) mailer.appUp(custom.spec().alerts.email, app);
if (!custom.spec().alerts.notifyCloudronAdmins) return callback();
actionForAllAdmins([], function (admin, done) {
mailer.appUp(admin.email, app);
@@ -222,8 +222,8 @@ function appDied(eventId, app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (custom.alertsEmail()) mailer.appDied(custom.alertsEmail(), app);
if (!custom.sendAlertsToCloudronAdmins()) return callback();
if (custom.spec().alerts.email) mailer.appDied(custom.spec().alerts.email, app);
if (!custom.spec().alerts.notifyCloudronAdmins) return callback();
actionForAllAdmins([], function (admin, callback) {
mailer.appDied(admin.email, app);
@@ -254,8 +254,8 @@ function certificateRenewalError(eventId, vhost, errorMessage, callback) {
assert.strictEqual(typeof errorMessage, 'string');
assert.strictEqual(typeof callback, 'function');
if (custom.alertsEmail()) mailer.certificateRenewalError(custom.alertsEmail(), vhost, errorMessage);
if (!custom.sendAlertsToCloudronAdmins()) return callback();
if (custom.spec().alerts.email) mailer.certificateRenewalError(custom.spec().alerts.email, vhost, errorMessage);
if (!custom.spec().alerts.notifyCloudronAdmins) return callback();
actionForAllAdmins([], function (admin, callback) {
mailer.certificateRenewalError(admin.email, vhost, errorMessage);
@@ -269,8 +269,8 @@ function backupFailed(eventId, taskId, errorMessage, callback) {
assert.strictEqual(typeof errorMessage, 'string');
assert.strictEqual(typeof callback, 'function');
if (custom.alertsEmail()) mailer.backupFailed(custom.alertsEmail(), errorMessage, `${config.adminOrigin()}/logs.html?taskId=${taskId}`);
if (!custom.sendAlertsToCloudronAdmins()) return callback();
if (custom.spec().alerts.email) mailer.backupFailed(custom.spec().alerts.email, errorMessage, `${config.adminOrigin()}/logs.html?taskId=${taskId}`);
if (!custom.spec().alerts.notifyCloudronAdmins) return callback();
actionForAllAdmins([], function (admin, callback) {
mailer.backupFailed(admin.email, errorMessage, `${config.adminOrigin()}/logs.html?taskId=${taskId}`);

View File

@@ -119,10 +119,13 @@ function pruneInfraImages(callback) {
if (!line) continue;
let parts = line.split(' '); // [ ID, Repo:Tag@Digest ]
if (image.tag === parts[1]) continue; // keep
debug(`pruneInfraImages: removing unused image of ${image.repo}: ${line}`);
debug(`pruneInfraImages: removing unused image of ${image.repo}: tag: ${parts[1]} id: ${parts[0]}`);
shell.exec('pruneInfraImages', `docker rmi ${parts[0]}`, iteratorCallback);
let result = safe.child_process.execSync(`docker rmi ${parts[0]}`, { encoding: 'utf8' });
if (result === null) debug(`Erroring removing image ${parts[0]}: ${safe.error.mesage}`);
}
iteratorCallback();
}, callback);
}

View File

@@ -78,6 +78,7 @@ ProvisionError.BAD_STATE = 'Bad State';
ProvisionError.ALREADY_SETUP = 'Already Setup';
ProvisionError.INTERNAL_ERROR = 'Internal Error';
ProvisionError.EXTERNAL_ERROR = 'External Error';
ProvisionError.LICENSE_ERROR = 'License Error';
ProvisionError.ALREADY_PROVISIONED = 'Already Provisioned';
function setProgress(task, message, callback) {
@@ -86,29 +87,8 @@ function setProgress(task, message, callback) {
callback();
}
function autoprovision(autoconf, callback) {
assert.strictEqual(typeof autoconf, 'object');
assert.strictEqual(typeof callback, 'function');
async.eachSeries(Object.keys(autoconf), function (key, iteratorDone) {
debug(`autoprovision: ${key}`);
switch (key) {
case 'backupConfig':
settings.setBackupConfig(autoconf[key], iteratorDone);
break;
default:
debug(`autoprovision: ${key} ignored`);
return iteratorDone();
}
}, function (error) {
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
callback(null);
});
}
function autoRegister(callback) {
function autoRegister(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
if (!fs.existsSync(paths.LICENSE_FILE)) return callback();
@@ -118,7 +98,7 @@ function autoRegister(callback) {
debug('Auto-registering cloudron');
appstore.registerWithLicense(license.trim(), function (error) {
appstore.registerWithLicense(license.trim(), domain, function (error) {
if (error && error.reason !== AppstoreError.ALREADY_REGISTERED) {
debug('Failed to auto-register cloudron', error);
return callback(new ProvisionError(ProvisionError.LICENSE_ERROR, 'Failed to auto-register Cloudron with license. Please contact support@cloudron.io'));
@@ -144,9 +124,9 @@ function unprovision(callback) {
}
function setup(dnsConfig, autoconf, auditSource, callback) {
function setup(dnsConfig, backupConfig, auditSource, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof autoconf, 'object');
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -188,12 +168,12 @@ function setup(dnsConfig, autoconf, auditSource, callback) {
callback(); // now that args are validated run the task in the background
async.series([
autoRegister,
autoRegister.bind(null, domain),
domains.prepareDashboardDomain.bind(null, domain, auditSource, (progress) => setProgress('setup', progress.message, NOOP_CALLBACK)),
cloudron.setDashboardDomain.bind(null, domain, auditSource), // this sets up the config.fqdn()
mail.addDomain.bind(null, domain), // this relies on config.mailFqdn()
mail.addDomain.bind(null, domain), // this relies on config.mailFqdn() and config.adminDomain()
setProgress.bind(null, 'setup', 'Applying auto-configuration'),
autoprovision.bind(null, autoconf),
(next) => { if (!backupConfig) return next(); settings.setBackupConfig(backupConfig, next); },
setProgress.bind(null, 'setup', 'Done'),
eventlog.add.bind(null, eventlog.ACTION_PROVISION, auditSource, { })
], function (error) {
@@ -264,11 +244,10 @@ function activate(username, password, email, displayName, ip, auditSource, callb
});
}
function restore(backupConfig, backupId, version, autoconf, auditSource, callback) {
function restore(backupConfig, backupId, version, auditSource, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof version, 'string');
assert.strictEqual(typeof autoconf, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -302,7 +281,6 @@ function restore(backupConfig, backupId, version, autoconf, auditSource, callbac
setProgress.bind(null, 'restore', 'Downloading backup'),
backups.restore.bind(null, backupConfig, backupId, (progress) => setProgress('restore', progress.message, NOOP_CALLBACK)),
setProgress.bind(null, 'restore', 'Applying auto-configuration'),
autoprovision.bind(null, autoconf),
// currently, our suggested restore flow is after a dnsSetup. The dnSetup creates DKIM keys and updates the DNS
// for this reason, we have to re-setup DNS after a restore so it has DKIm from the backup
// Once we have a 100% IP based restore, we can skip this

View File

@@ -392,7 +392,6 @@ function writeAdminNginxConfig(bundle, configFileName, vhost, callback) {
endpoint: 'admin',
certFilePath: bundle.certFilePath,
keyFilePath: bundle.keyFilePath,
xFrameOptions: 'SAMEORIGIN',
robotsTxtQuoted: JSON.stringify('User-agent: *\nDisallow: /\n')
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
@@ -457,8 +456,7 @@ function writeAppNginxConfig(app, bundle, callback) {
endpoint: endpoint,
certFilePath: bundle.certFilePath,
keyFilePath: bundle.keyFilePath,
robotsTxtQuoted: app.robotsTxt ? JSON.stringify(app.robotsTxt) : null,
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN' // once all apps have been updated/
robotsTxtQuoted: app.robotsTxt ? JSON.stringify(app.robotsTxt) : null
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
@@ -487,8 +485,7 @@ function writeAppRedirectNginxConfig(app, fqdn, bundle, callback) {
endpoint: 'redirect',
certFilePath: bundle.certFilePath,
keyFilePath: bundle.keyFilePath,
robotsTxtQuoted: null,
xFrameOptions: 'SAMEORIGIN'
robotsTxtQuoted: null
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);

View File

@@ -31,9 +31,7 @@ var apps = require('../apps.js'),
AppsError = apps.AppsError,
assert = require('assert'),
auditSource = require('../auditsource.js'),
config = require('../config.js'),
debug = require('debug')('box:routes/apps'),
fs = require('fs'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
paths = require('../paths.js'),
@@ -67,11 +65,15 @@ function getApps(req, res, next) {
function getAppIcon(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
var iconPath = paths.APP_ICONS_DIR + '/' + req.params.id + '.png';
fs.exists(iconPath, function (exists) {
if (!exists) return next(new HttpError(404, 'No such icon'));
res.sendFile(iconPath);
});
if (!req.query.original) {
const userIconPath = `${paths.APP_ICONS_DIR}/${req.params.id}.user.png`;
if (safe.fs.existsSync(userIconPath)) return res.sendFile(userIconPath);
}
const appstoreIconPath = `${paths.APP_ICONS_DIR}/${req.params.id}.png`;
if (safe.fs.existsSync(appstoreIconPath)) return res.sendFile(appstoreIconPath);
return next(new HttpError(404, 'No such icon'));
}
function installApp(req, res, next) {
@@ -107,8 +109,6 @@ function installApp(req, res, next) {
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
if ('sso' in data && typeof data.sso !== 'boolean') return next(new HttpError(400, 'sso must be a boolean'));
if ('enableBackup' in data && typeof data.enableBackup !== 'boolean') return next(new HttpError(400, 'enableBackup must be a boolean'));
if ('enableAutomaticUpdate' in data && typeof data.enableAutomaticUpdate !== 'boolean') return next(new HttpError(400, 'enableAutomaticUpdate must be a boolean'));
@@ -166,7 +166,6 @@ function configureApp(req, res, next) {
if (!data.cert && data.key) return next(new HttpError(400, 'cert must be provided'));
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
if ('enableBackup' in data && typeof data.enableBackup !== 'boolean') return next(new HttpError(400, 'enableBackup must be a boolean'));
if ('enableAutomaticUpdate' in data && typeof data.enableAutomaticUpdate !== 'boolean') return next(new HttpError(400, 'enableAutomaticUpdate must be a boolean'));
@@ -189,6 +188,7 @@ function configureApp(req, res, next) {
if ('label' in data && typeof data.label !== 'string') return next(new HttpError(400, 'label must be a string'));
if ('dataDir' in data && typeof data.dataDir !== 'string') return next(new HttpError(400, 'dataDir must be a string'));
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
debug('Configuring app id:%s data:%j', req.params.id, data);
@@ -326,7 +326,6 @@ function updateApp(req, res, next) {
if ('appStoreId' in data && typeof data.appStoreId !== 'string') return next(new HttpError(400, 'appStoreId must be a string'));
if (!data.manifest && !data.appStoreId) return next(new HttpError(400, 'appStoreId or manifest is required'));
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
if ('force' in data && typeof data.force !== 'boolean') return next(new HttpError(400, 'force must be a boolean'));
debug('Update app id:%s to manifest:%j', req.params.id, data.manifest);

View File

@@ -12,9 +12,19 @@ exports = module.exports = {
var appstore = require('../appstore.js'),
AppstoreError = appstore.AppstoreError,
assert = require('assert'),
custom = require('../custom.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
function isAppAllowed(appstoreId) {
if (custom.spec().appstore.blacklist.includes(appstoreId)) return false;
if (!custom.spec().appstore.whitelist) return true;
if (!custom.spec().appstore.whitelist[appstoreId]) return false;
return true;
}
function getApps(req, res, next) {
appstore.getApps(function (error, apps) {
if (error && error.reason === AppstoreError.INVALID_TOKEN) return next(new HttpError(402, error.message));
@@ -22,13 +32,18 @@ function getApps(req, res, next) {
if (error && error.reason === AppstoreError.NOT_REGISTERED) return next(new HttpError(412, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { apps: apps }));
let filteredApps = apps.filter((app) => !custom.spec().appstore.blacklist.includes(app.id));
if (custom.spec().appstore.whitelist) filteredApps = filteredApps.filter((app) => app.id in custom.spec().appstore.whitelist);
next(new HttpSuccess(200, { apps: filteredApps }));
});
}
function getApp(req, res, next) {
assert.strictEqual(typeof req.params.appstoreId, 'string');
if (!isAppAllowed(req.params.appstoreId)) return next(new HttpError(405, 'feature disabled by admin'));
appstore.getApp(req.params.appstoreId, function (error, app) {
if (error && error.reason === AppstoreError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppstoreError.INVALID_TOKEN) return next(new HttpError(402, error.message));
@@ -44,6 +59,8 @@ function getAppVersion(req, res, next) {
assert.strictEqual(typeof req.params.appstoreId, 'string');
assert.strictEqual(typeof req.params.versionId, 'string');
if (!isAppAllowed(req.params.appstoreId)) return next(new HttpError(405, 'feature disabled by admin'));
appstore.getAppVersion(req.params.appstoreId, req.params.versionId, function (error, manifest) {
if (error && error.reason === AppstoreError.NOT_FOUND) return next(new HttpError(404, 'No such app or version'));
if (error && error.reason === AppstoreError.INVALID_TOKEN) return next(new HttpError(402, error.message));

View File

@@ -20,6 +20,7 @@ let assert = require('assert'),
auditSource = require('../auditsource.js'),
cloudron = require('../cloudron.js'),
CloudronError = cloudron.CloudronError,
custom = require('../custom.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
updater = require('../updater.js'),
@@ -152,6 +153,8 @@ function getLogStream(req, res, next) {
function setDashboardAndMailDomain(req, res, next) {
if (!req.body.domain || typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
if (!custom.spec().domains.changeDashboardDomain) return next(new HttpError(405, 'feature disabled by admin'));
cloudron.setDashboardAndMailDomain(req.body.domain, auditSource.fromRequest(req), function (error) {
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
@@ -163,6 +166,8 @@ function setDashboardAndMailDomain(req, res, next) {
function prepareDashboardDomain(req, res, next) {
if (!req.body.domain || typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
if (!custom.spec().domains.changeDashboardDomain) return next(new HttpError(405, 'feature disabled by admin'));
cloudron.prepareDashboardDomain(req.body.domain, auditSource.fromRequest(req), function (error, taskId) {
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(404, error.message));
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));

View File

@@ -53,19 +53,18 @@ function setup(req, res, next) {
if ('tlsConfig' in dnsConfig && typeof dnsConfig.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be an object'));
if (dnsConfig.tlsConfig && (!dnsConfig.tlsConfig.provider || typeof dnsConfig.tlsConfig.provider !== 'string')) return next(new HttpError(400, 'tlsConfig.provider must be a string'));
// TODO: validate subfields of these objects
if (req.body.autoconf && typeof req.body.autoconf !== 'object') return next(new HttpError(400, 'autoconf must be an object'));
if ('backupConfig' in req.body && typeof req.body.backupConfig !== 'object') return next(new HttpError(400, 'backupConfig must be an object'));
// it can take sometime to setup DNS, register cloudron
req.clearTimeout();
provision.setup(dnsConfig, req.body.autoconf || {}, auditSource.fromRequest(req), function (error) {
provision.setup(dnsConfig, req.body.backupConfig || null, auditSource.fromRequest(req), function (error) {
if (error && error.reason === ProvisionError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === ProvisionError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === ProvisionError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
next(new HttpSuccess(200, {}));
});
}
@@ -108,17 +107,14 @@ function restore(req, res, next) {
if (typeof req.body.backupId !== 'string') return next(new HttpError(400, 'backupId must be a string or null'));
if (typeof req.body.version !== 'string') return next(new HttpError(400, 'version must be a string'));
// TODO: validate subfields of these objects
if (req.body.autoconf && typeof req.body.autoconf !== 'object') return next(new HttpError(400, 'autoconf must be an object'));
provision.restore(backupConfig, req.body.backupId, req.body.version, req.body.autoconf || {}, auditSource.fromRequest(req), function (error) {
provision.restore(backupConfig, req.body.backupId, req.body.version, auditSource.fromRequest(req), function (error) {
if (error && error.reason === ProvisionError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === ProvisionError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === ProvisionError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === ProvisionError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
next(new HttpSuccess(200, {}));
});
}

View File

@@ -17,6 +17,8 @@ var addons = require('../addons.js'),
HttpSuccess = require('connect-lastmile').HttpSuccess;
function getAll(req, res, next) {
req.clearTimeout(); // can take a while to get status of all services
addons.getServices(function (error, result) {
if (error) return next(new HttpError(500, error));

View File

@@ -131,8 +131,8 @@ function getBackupConfig(req, res, next) {
settings.getBackupConfig(function (error, config) {
if (error) return next(new HttpError(500, error));
// used by the UI to figure if backups are disabled
if (!custom.features().configureBackup) {
// always send provider as it is used by the UI to figure if backups are disabled ('noop' backend)
if (!custom.spec().backups.configurable) {
return next(new HttpSuccess(200, { provider: config.provider }));
}
@@ -143,7 +143,7 @@ function getBackupConfig(req, res, next) {
function setBackupConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!custom.features().configureBackup) return next(new HttpError(405, 'feature disabled by admin'));
if (!custom.spec().backups.configurable) return next(new HttpError(405, 'feature disabled by admin'));
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if (typeof req.body.retentionSecs !== 'number') return next(new HttpError(400, 'retentionSecs is required'));
@@ -207,7 +207,7 @@ function getDynamicDnsConfig(req, res, next) {
function setDynamicDnsConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!custom.features().dynamicDns) return next(new HttpError(405, 'feature disabled by admin'));
if (!custom.spec().domains.dynamicDns) return next(new HttpError(405, 'feature disabled by admin'));
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled boolean is required'));

View File

@@ -18,6 +18,8 @@ var appstore = require('../appstore.js'),
function createTicket(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
if (!custom.spec().support.submitTickets) return next(new HttpError(405, 'feature disabled by admin'));
const VALID_TYPES = [ 'feedback', 'ticket', 'app_missing', 'app_error', 'upgrade_request' ];
if (typeof req.body.type !== 'string' || !req.body.type) return next(new HttpError(400, 'type must be string'));
@@ -27,16 +29,16 @@ function createTicket(req, res, next) {
if (req.body.appId && typeof req.body.appId !== 'string') return next(new HttpError(400, 'appId must be string'));
appstore.createTicket(_.extend({ }, req.body, { email: req.user.email, displayName: req.user.displayName }), function (error) {
if (error) return next(new HttpError(503, `Error contacting cloudron.io: ${error.message}. Please email ${custom.supportEmail()}`));
if (error) return next(new HttpError(503, `Error contacting cloudron.io: ${error.message}. Please email ${custom.spec().support.email}`));
next(new HttpSuccess(201, {}));
next(new HttpSuccess(201, { message: `An email for sent to ${custom.spec().support.email}. We will get back shortly!` }));
});
}
function enableRemoteSupport(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!custom.features().remoteSupport) return next(new HttpError(405, 'feature disabled by admin'));
if (!custom.spec().support.remoteSupport) return next(new HttpError(405, 'feature disabled by admin'));
if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enabled is required'));

View File

@@ -56,8 +56,6 @@ const DOMAIN_0 = {
tlsConfig: { provider: 'fallback' }
};
const CLOUDRON_ID = 'somecloudronid';
var APP_STORE_ID = 'test', APP_ID;
var APP_LOCATION = 'appslocation';
var APP_LOCATION_2 = 'appslocationtwo';
@@ -383,13 +381,13 @@ describe('App API', function () {
});
});
it('app install fails - reserved api location', function (done) {
it('app install fails - reserved smtp location', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: constants.API_LOCATION, accessRestriction: null, domain: DOMAIN_0.domain })
.send({ manifest: APP_MANIFEST, location: constants.SMTP_LOCATION, accessRestriction: null, domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.contain(constants.API_LOCATION + ' is reserved');
expect(res.body.message).to.contain(constants.SMTP_LOCATION + ' is reserved');
done();
});
});
@@ -539,7 +537,6 @@ describe('App API', function () {
it('cannot uninstall invalid app', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/whatever/uninstall')
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
@@ -547,28 +544,8 @@ describe('App API', function () {
});
});
it('cannot uninstall app without password', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('cannot uninstall app with wrong password', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
.send({ password: PASSWORD+PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
});
});
it('non admin cannot uninstall app', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
.send({ password: PASSWORD })
.query({ access_token: token_1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
@@ -581,7 +558,6 @@ describe('App API', function () {
var fake2 = nock(config.apiServerOrigin()).delete(function (uri) { return uri.indexOf('/api/v1/cloudronapps/') >= 0; }).reply(204, { });
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
@@ -1148,7 +1124,6 @@ describe('App installation', function () {
}
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);

View File

@@ -253,7 +253,6 @@ describe('Domains API', function () {
it('cannot delete locked domain', function (done) {
superagent.delete(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain)
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(423);
done();
@@ -262,31 +261,9 @@ describe('Domains API', function () {
});
describe('delete', function () {
it('fails without password', function (done) {
superagent.delete(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with wrong password', function (done) {
superagent.delete(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain)
.query({ access_token: token })
.send({ password: PASSWORD + PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(403);
done();
});
});
it('fails for non-existing domain', function (done) {
superagent.delete(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain + DOMAIN_0.domain)
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
@@ -297,7 +274,6 @@ describe('Domains API', function () {
it('succeeds', function (done) {
superagent.delete(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain)
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);

View File

@@ -250,7 +250,6 @@ describe('Groups API', function () {
it('can remove empty group', function (done) {
superagent.del(SERVER_URL + '/api/v1/groups/' + group1Object.id)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
@@ -260,7 +259,6 @@ describe('Groups API', function () {
it('can remove non-empty group', function (done) {
superagent.del(SERVER_URL + '/api/v1/groups/' + groupObject.id)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);

View File

@@ -190,29 +190,9 @@ describe('Mail API', function () {
});
});
it('cannot delete domain without password', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/doesnotexist.com')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('cannot delete domain with wrong password', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/doesnotexist.com')
.send({ password: PASSWORD+PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
});
});
it('cannot delete non-existing domain', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/doesnotexist.com')
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
@@ -221,7 +201,6 @@ describe('Mail API', function () {
it('cannot delete admin mail domain', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + ADMIN_DOMAIN.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
@@ -231,7 +210,6 @@ describe('Mail API', function () {
it('can delete admin mail domain', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
@@ -261,7 +239,7 @@ describe('Mail API', function () {
callback(null, dnsAnswerQueue[hostname][type]);
};
dkimDomain = 'cloudron._domainkey.' + DOMAIN_0.domain;
dkimDomain = 'cloudron-admincom._domainkey.' + DOMAIN_0.domain;
spfDomain = DOMAIN_0.domain;
mxDomain = DOMAIN_0.domain;
dmarcDomain = '_dmarc.' + DOMAIN_0.domain;
@@ -289,7 +267,6 @@ describe('Mail API', function () {
dns.resolve = resolve;
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
@@ -539,7 +516,6 @@ describe('Mail API', function () {
after(function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
@@ -591,7 +567,6 @@ describe('Mail API', function () {
after(function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
@@ -662,7 +637,6 @@ describe('Mail API', function () {
after(function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
@@ -740,7 +714,6 @@ describe('Mail API', function () {
after(function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
@@ -846,7 +819,6 @@ describe('Mail API', function () {
if (error) return done(error);
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
@@ -972,7 +944,6 @@ describe('Mail API', function () {
if (error) return done(error);
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);

View File

@@ -248,7 +248,7 @@ describe('Profile API', function () {
.query({ access_token: token_0 })
.send({ password: 'some wrong password', newPassword: 'MOre#$%34' })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
expect(res.statusCode).to.equal(400);
done();
});
});

View File

@@ -529,7 +529,6 @@ describe('Users API', function () {
it('remove random user fails', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/randomid')
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
@@ -539,46 +538,15 @@ describe('Users API', function () {
it('user removes himself is not allowed', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + user_0.id)
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
done();
});
});
it('admin cannot remove normal user without giving a password', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + user_1.id)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('admin cannot remove normal user with empty password', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + user_1.id)
.query({ access_token: token })
.send({ password: '' })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
});
});
it('admin cannot remove normal user with giving wrong password', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + user_1.id)
.query({ access_token: token })
.send({ password: PASSWORD + PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
});
});
it('admin removes normal user', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + user_1.id)
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
@@ -588,7 +556,6 @@ describe('Users API', function () {
it('admin removes himself should not be allowed', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + user_0.id)
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
done();

View File

@@ -132,7 +132,7 @@ function verifyPassword(req, res, next) {
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'API call requires user password'));
users.verifyWithUsername(req.user.username, req.body.password, function (error) {
if (error && error.reason === UsersError.WRONG_PASSWORD) return next(new HttpError(403, 'Password incorrect')); // not 401 intentionally since the UI redirects for 401
if (error && error.reason === UsersError.WRONG_PASSWORD) return next(new HttpError(400, 'Password incorrect'));
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
if (error) return next(new HttpError(500, error));

View File

@@ -18,7 +18,6 @@ var accesscontrol = require('./accesscontrol.js'),
middleware = require('./middleware'),
passport = require('passport'),
path = require('path'),
provision = require('./provision.js'),
routes = require('./routes/index.js'),
ws = require('ws');
@@ -168,7 +167,7 @@ function initializeExpressSync() {
router.get ('/api/v1/users', usersReadScope, routes.users.list);
router.post('/api/v1/users', usersManageScope, routes.users.create);
router.get ('/api/v1/users/:userId', usersManageScope, routes.users.get); // this is manage scope because it returns non-restricted fields
router.del ('/api/v1/users/:userId', usersManageScope, routes.users.verifyPassword, routes.users.remove);
router.del ('/api/v1/users/:userId', usersManageScope, routes.users.remove);
router.post('/api/v1/users/:userId', usersManageScope, routes.users.update);
router.post('/api/v1/users/:userId/password', usersManageScope, routes.users.changePassword);
router.put ('/api/v1/users/:userId/groups', usersManageScope, routes.users.setGroups);
@@ -182,7 +181,7 @@ function initializeExpressSync() {
router.get ('/api/v1/groups/:groupId', usersManageScope, routes.groups.get);
router.put ('/api/v1/groups/:groupId/members', usersManageScope, routes.groups.updateMembers);
router.post('/api/v1/groups/:groupId', usersManageScope, routes.groups.update);
router.del ('/api/v1/groups/:groupId', usersManageScope, routes.users.verifyPassword, routes.groups.remove);
router.del ('/api/v1/groups/:groupId', usersManageScope, routes.groups.remove);
// form based login routes used by oauth2 frame
router.get ('/api/v1/session/login', csrf, routes.oauth2.loginForm);
@@ -222,13 +221,13 @@ function initializeExpressSync() {
// app routes
router.get ('/api/v1/apps', appsReadScope, routes.apps.getApps);
router.get ('/api/v1/apps/:id', appsManageScope, routes.apps.getApp);
router.get ('/api/v1/apps/:id/icon', routes.apps.getAppIcon);
router.get ('/api/v1/apps/:id/icon', appsReadScope, routes.apps.getAppIcon);
router.post('/api/v1/apps/install', appsManageScope, routes.apps.installApp);
router.post('/api/v1/apps/:id/uninstall', appsManageScope, routes.users.verifyPassword, routes.apps.uninstallApp);
router.post('/api/v1/apps/:id/uninstall', appsManageScope, routes.apps.uninstallApp);
router.post('/api/v1/apps/:id/configure', appsManageScope, routes.apps.configureApp);
router.post('/api/v1/apps/:id/update', appsManageScope, routes.apps.updateApp);
router.post('/api/v1/apps/:id/restore', appsManageScope, routes.users.verifyPassword, routes.apps.restoreApp);
router.post('/api/v1/apps/:id/restore', appsManageScope, routes.apps.restoreApp);
router.post('/api/v1/apps/:id/backup', appsManageScope, routes.apps.backupApp);
router.get ('/api/v1/apps/:id/backups', appsManageScope, routes.apps.listBackups);
router.post('/api/v1/apps/:id/stop', appsManageScope, routes.apps.stopApp);
@@ -252,8 +251,8 @@ function initializeExpressSync() {
// email routes
router.get ('/api/v1/mail/:domain', mailScope, routes.mail.getDomain);
router.post('/api/v1/mail', mailScope, routes.mail.addDomain);
router.get ('/api/v1/mail/:domain/stats', mailScope, routes.users.verifyPassword, routes.mail.getDomainStats);
router.del ('/api/v1/mail/:domain', mailScope, routes.users.verifyPassword, routes.mail.removeDomain);
router.get ('/api/v1/mail/:domain/stats', mailScope, routes.mail.getDomainStats);
router.del ('/api/v1/mail/:domain', mailScope, routes.mail.removeDomain);
router.get ('/api/v1/mail/:domain/status', mailScope, routes.mail.getStatus);
router.post('/api/v1/mail/:domain/mail_from_validation', mailScope, routes.mail.setMailFromValidation);
router.post('/api/v1/mail/:domain/catch_all', mailScope, routes.mail.setCatchAllAddress);
@@ -285,7 +284,7 @@ function initializeExpressSync() {
router.get ('/api/v1/domains', domainsReadScope, routes.domains.getAll);
router.get ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.domains.get); // this is manage scope because it returns non-restricted fields
router.put ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.domains.update);
router.del ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.users.verifyPassword, routes.domains.del);
router.del ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.domains.del);
// addon routes
router.get ('/api/v1/services', cloudronScope, routes.services.getAll);
@@ -370,7 +369,6 @@ function start(callback) {
async.series([
routes.accesscontrol.initialize, // hooks up authentication strategies into passport
database.initialize,
provision.autoRegister,
cloudron.initialize,
gHttpServer.listen.bind(gHttpServer, config.get('port'), '127.0.0.1'),
gSysadminHttpServer.listen.bind(gSysadminHttpServer, config.get('sysadminPort'), '127.0.0.1'),

View File

@@ -19,6 +19,7 @@ function startSftp(existingInfra, callback) {
if (existingInfra.version === infra.version && infra.images.graphite.tag === existingInfra.images.graphite.tag) return callback();
const cmd = `docker run --restart=always -d --name="sftp" \
--hostname sftp \
--net cloudron \
--net-alias sftp \
--log-driver syslog \

View File

@@ -14,6 +14,7 @@ let assert = require('assert'),
path = require('path'),
util = require('util');
// the logic here is also used in the cloudron-support tool
var AUTHORIZED_KEYS_FILEPATH = config.TEST ? path.join(config.baseDir(), 'authorized_keys') : ((config.provider() === 'ec2' || config.provider() === 'lightsail' || config.provider() === 'ami') ? '/home/ubuntu/.ssh/authorized_keys' : '/root/.ssh/authorized_keys'),
AUTHORIZED_KEYS_USER = config.TEST ? process.getuid() : ((config.provider() === 'ec2' || config.provider() === 'lightsail' || config.provider() === 'ami') ? 'ubuntu' : 'root'),
AUTHORIZED_KEYS_CMD = path.join(__dirname, 'scripts/remotesupport.sh');

View File

@@ -412,7 +412,6 @@ describe('database', function () {
oldConfig: null,
newConfig: null,
memoryLimit: 4294967296,
xFrameOptions: 'DENY',
sso: true,
debugMode: null,
robotsTxt: null,
@@ -993,7 +992,6 @@ describe('database', function () {
oldConfig: null,
updateConfig: null,
memoryLimit: 4294967296,
xFrameOptions: 'DENY',
sso: true,
debugMode: null,
robotsTxt: null,
@@ -1028,7 +1026,6 @@ describe('database', function () {
oldConfig: null,
updateConfig: null,
memoryLimit: 0,
xFrameOptions: 'SAMEORIGIN',
sso: true,
debugMode: null,
robotsTxt: null,
@@ -2047,7 +2044,7 @@ describe('database', function () {
before(function (done) {
async.series([
domaindb.add.bind(null, DOMAIN_0.domain, { zoneName: DOMAIN_0.zoneName, provider: DOMAIN_0.provider, config: DOMAIN_0.config, tlsConfig: DOMAIN_0.tlsConfig }),
maildb.add.bind(null, DOMAIN_0.domain)
maildb.add.bind(null, DOMAIN_0.domain, {})
], done);
});
@@ -2209,7 +2206,8 @@ describe('database', function () {
enabled: false,
relay: { provider: 'cloudron-smtp' },
catchAll: [ ],
mailFromValidation: true
mailFromValidation: true,
dkimSelector: 'cloudron'
};
before(function (done) {
@@ -2221,7 +2219,7 @@ describe('database', function () {
});
it('cannot add non-existing domain', function (done) {
maildb.add(MAIL_DOMAIN_0.domain + 'nope', function (error) {
maildb.add(MAIL_DOMAIN_0.domain + 'nope', {}, function (error) {
expect(error).to.be.ok();
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
@@ -2230,7 +2228,7 @@ describe('database', function () {
});
it('can add domain', function (done) {
maildb.add(MAIL_DOMAIN_0.domain, function (error) {
maildb.add(MAIL_DOMAIN_0.domain, {}, function (error) {
expect(error).to.equal(null);
done();

View File

@@ -2,20 +2,19 @@
/* global it:false */
/* global describe:false */
/* global before:false */
/* global beforeEach:false */
/* global after:false */
'use strict';
var async = require('async'),
AWS = require('aws-sdk'),
GCDNS = require('@google-cloud/dns'),
GCDNS = require('@google-cloud/dns').DNS,
config = require('../config.js'),
database = require('../database.js'),
domains = require('../domains.js'),
expect = require('expect.js'),
namecheap = require('namecheap'),
nock = require('nock'),
sinon = require('sinon'),
util = require('util');
var DOMAIN_0 = {
@@ -596,465 +595,282 @@ describe('dns provider', function () {
});
});
xdescribe('namecheap', function () {
let sandbox = require('sinon').createSandbox();
describe('namecheap', function () {
const NAMECHEAP_ENDPOINT = 'https://api.namecheap.com';
const username = 'namecheapuser';
const token = 'namecheaptoken';
let username = 'namecheapuser';
let apiKey = 'API_KEY';
// the success answer is always the same
const SET_HOSTS_RETURN = `<?xml version="1.0" encoding="utf-8"?>
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
<Errors />
<Warnings />
<RequestedCommand>namecheap.domains.dns.sethosts</RequestedCommand>
<CommandResponse Type="namecheap.domains.dns.setHosts">
<DomainDNSSetHostsResult Domain="cloudron.space" IsSuccess="true">
<Warnings />
</DomainDNSSetHostsResult>
</CommandResponse>
<Server>PHX01APIEXT03</Server>
<GMTTimeDifference>--4:00</GMTTimeDifference>
<ExecutionTime>0.408</ExecutionTime>
</ApiResponse>`;
before(function (done) {
DOMAIN_0.provider = 'namecheap';
DOMAIN_0.config = {
username,
apiKey
username: username,
token: token
};
domains.update(DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE, done);
});
after(function() {
sandbox.restore();
beforeEach(function () {
nock.cleanAll();
});
it('upsert non-existing record succeeds', function (done) {
const GET_HOSTS_RETURN = `<?xml version="1.0" encoding="utf-8"?>
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
<Errors />
<Warnings />
<RequestedCommand>namecheap.domains.dns.gethosts</RequestedCommand>
<CommandResponse Type="namecheap.domains.dns.getHosts">
<DomainDNSGetHostsResult Domain="cloudron.space" EmailType="MX" IsUsingOurDNS="true">
<host HostId="160859434" Name="@" Type="MX" Address="my.nebulon.space." MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
<host HostId="173400612" Name="@" Type="TXT" Address="v=spf1 a:my.nebulon.space ~all" MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
</DomainDNSGetHostsResult>
</CommandResponse>
<Server>PHX01APIEXT04</Server>
<GMTTimeDifference>--4:00</GMTTimeDifference>
<ExecutionTime>0.16</ExecutionTime>
</ApiResponse>`;
let getHostsReturn = {
'Type': 'namecheap.domains.dns.getHosts',
'DomainDNSGetHostsResult': {
'Domain': 'example-dns-test.com',
'EmailType': 'FWD',
'IsUsingOurDNS': 'true',
'host': [
{
'HostId': '614433',
'Name': 'www',
'Type': 'CNAME',
'Address': 'parkingpage.namecheap.com.',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'Name': '@',
'Type': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
}
]
}
};
var req1 = nock(NAMECHEAP_ENDPOINT).get('/xml.response')
.query({
ApiUser: username,
ApiKey: token,
UserName: username,
ClientIp: '127.0.0.1',
Command: 'namecheap.domains.dns.getHosts',
SLD: DOMAIN_0.zoneName.split('.')[0],
TLD: DOMAIN_0.zoneName.split('.')[1]
})
.reply(200, GET_HOSTS_RETURN);
let setInternalExpect = [
{
'HostId': '614433',
'HostName': 'www',
'RecordType': 'CNAME',
'Address': 'parkingpage.namecheap.com.',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'HostName': '@',
'RecordType': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
RecordType: 'A',
HostName: 'test',
Address: '1.2.3.4'
}
];
var req2 = nock(NAMECHEAP_ENDPOINT).post('/xml.response')
.query({
ApiUser: username,
ApiKey: token,
UserName: username,
ClientIp: '127.0.0.1',
Command: 'namecheap.domains.dns.setHosts',
SLD: DOMAIN_0.zoneName.split('.')[0],
TLD: DOMAIN_0.zoneName.split('.')[1],
let getHostsFake = sinon.fake.yields(null, getHostsReturn);
let setHostsFake = sinon.fake.yields(null, true);
let mockObj = {
dns: {
getHosts: getHostsFake,
setHosts: setHostsFake
}
};
TTL1: '300',
HostName1: '@',
RecordType1: 'MX',
Address1: 'my.nebulon.space.',
EmailType1: 'MX',
MXPref1: '10',
sandbox.stub(namecheap.prototype, 'domains').value(mockObj);
TTL2: '300',
HostName2: '@',
RecordType2: 'TXT',
Address2: 'v=spf1 a:my.nebulon.space ~all',
TTL3: '300',
HostName3: 'test',
RecordType3: 'A',
Address3: '1.2.3.4',
})
.reply(200, SET_HOSTS_RETURN);
domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) {
expect(error).to.eql(null);
expect(setHostsFake.calledOnce).to.eql(true);
expect(setHostsFake.calledWith(DOMAIN_0.domain, setInternalExpect)).to.eql(true);
expect(req1.isDone()).to.be.ok();
expect(req2.isDone()).to.be.ok();
done();
});
});
it('upsert multiple non-existing records succeeds', function (done) {
const GET_HOSTS_RETURN = `<?xml version="1.0" encoding="utf-8"?>
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
<Errors />
<Warnings />
<RequestedCommand>namecheap.domains.dns.gethosts</RequestedCommand>
<CommandResponse Type="namecheap.domains.dns.getHosts">
<DomainDNSGetHostsResult Domain="cloudron.space" EmailType="MX" IsUsingOurDNS="true">
<host HostId="160859434" Name="@" Type="MX" Address="my.nebulon.space." MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
<host HostId="173400612" Name="@" Type="TXT" Address="v=spf1 a:my.nebulon.space ~all" MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
</DomainDNSGetHostsResult>
</CommandResponse>
<Server>PHX01APIEXT04</Server>
<GMTTimeDifference>--4:00</GMTTimeDifference>
<ExecutionTime>0.16</ExecutionTime>
</ApiResponse>`;
let getHostsReturn = {
'Type': 'namecheap.domains.dns.getHosts',
'DomainDNSGetHostsResult': {
'Domain': 'example-dns-test.com',
'EmailType': 'FWD',
'IsUsingOurDNS': 'true',
'host': [
{
'HostId': '614433',
'Name': 'www',
'Type': 'CNAME',
'Address': 'parkingpage.namecheap.com.',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'Name': '@',
'Type': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
}
]
}
};
var req1 = nock(NAMECHEAP_ENDPOINT).get('/xml.response')
.query({
ApiUser: username,
ApiKey: token,
UserName: username,
ClientIp: '127.0.0.1',
Command: 'namecheap.domains.dns.getHosts',
SLD: DOMAIN_0.zoneName.split('.')[0],
TLD: DOMAIN_0.zoneName.split('.')[1]
})
.reply(200, GET_HOSTS_RETURN);
let setInternalExpect = [
{
'HostId': '614433',
'HostName': 'www',
'RecordType': 'CNAME',
'Address': 'parkingpage.namecheap.com.',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'HostName': '@',
'RecordType': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
RecordType: 'TXT',
HostName: 'test',
Address: '1.2.3.4'
},
{
RecordType: 'TXT',
HostName: 'test',
Address: '2.3.4.5'
},
{
RecordType: 'TXT',
HostName: 'test',
Address: '3.4.5.6'
}
];
var req2 = nock(NAMECHEAP_ENDPOINT).post('/xml.response')
.query({
ApiUser: username,
ApiKey: token,
UserName: username,
ClientIp: '127.0.0.1',
Command: 'namecheap.domains.dns.setHosts',
SLD: DOMAIN_0.zoneName.split('.')[0],
TLD: DOMAIN_0.zoneName.split('.')[1],
let getHostsFake = sinon.fake.yields(null, getHostsReturn);
let setHostsFake = sinon.fake.yields(null, true);
let mockObj = {
dns: {
getHosts: getHostsFake,
setHosts: setHostsFake
}
};
TTL1: '300',
HostName1: '@',
RecordType1: 'MX',
Address1: 'my.nebulon.space.',
EmailType1: 'MX',
MXPref1: '10',
sandbox.stub(namecheap.prototype, 'domains').value(mockObj);
TTL2: '300',
HostName2: '@',
RecordType2: 'TXT',
Address2: 'v=spf1 a:my.nebulon.space ~all',
TTL3: '300',
HostName3: 'test',
RecordType3: 'TXT',
Address3: '1.2.3.4',
TTL4: '300',
HostName4: 'test',
RecordType4: 'TXT',
Address4: '2.3.4.5',
TTL5: '300',
HostName5: 'test',
RecordType5: 'TXT',
Address5: '3.4.5.6',
})
.reply(200, SET_HOSTS_RETURN);
domains.upsertDnsRecords('test', DOMAIN_0.domain, 'TXT', ['1.2.3.4', '2.3.4.5', '3.4.5.6'], function (error) {
expect(error).to.eql(null);
expect(setHostsFake.calledOnce).to.eql(true);
expect(setHostsFake.calledWith(DOMAIN_0.domain, setInternalExpect)).to.eql(true);
done();
});
});
it('upsert multiple non-existing MX records succeeds', function (done) {
let getHostsReturn = {
'Type': 'namecheap.domains.dns.getHosts',
'DomainDNSGetHostsResult': {
'Domain': 'example-dns-test.com',
'EmailType': 'FWD',
'IsUsingOurDNS': 'true',
'host': [
{
'HostId': '614433',
'Name': 'www',
'Type': 'CNAME',
'Address': 'parkingpage.namecheap.com.',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'Name': '@',
'Type': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
}
]
}
};
let setInternalExpect = [
{
'HostId': '614433',
'HostName': 'www',
'RecordType': 'CNAME',
'Address': 'parkingpage.namecheap.com.',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'HostName': '@',
'RecordType': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
RecordType: 'MX',
HostName: 'test',
Address: '1.2.3.4',
MXPref: '10'
},
{
RecordType: 'MX',
HostName: 'test',
Address: '2.3.4.5',
MXPref: '20'
},
{
RecordType: 'MX',
HostName: 'test',
Address: '3.4.5.6',
MXPref: '30'
}
];
let getHostsFake = sinon.fake.yields(null, getHostsReturn);
let setHostsFake = sinon.fake.yields(null, true);
let mockObj = {
dns: {
getHosts: getHostsFake,
setHosts: setHostsFake
}
};
sandbox.stub(namecheap.prototype, 'domains').value(mockObj);
domains.upsertDnsRecords('test', DOMAIN_0.domain, 'MX', ['10 1.2.3.4', '20 2.3.4.5', '30 3.4.5.6'], function (error) {
expect(error).to.eql(null);
expect(setHostsFake.calledOnce).to.eql(true);
expect(setHostsFake.calledWith(DOMAIN_0.domain, setInternalExpect)).to.eql(true);
expect(req1.isDone()).to.be.ok();
expect(req2.isDone()).to.be.ok();
done();
});
});
it('upsert existing record succeeds', function (done) {
const GET_HOSTS_RETURN = `<?xml version="1.0" encoding="utf-8"?>
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
<Errors />
<Warnings />
<RequestedCommand>namecheap.domains.dns.gethosts</RequestedCommand>
<CommandResponse Type="namecheap.domains.dns.getHosts">
<DomainDNSGetHostsResult Domain="cloudron.space" EmailType="MX" IsUsingOurDNS="true">
<host HostId="160859434" Name="@" Type="MX" Address="my.nebulon.space." MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
<host HostId="173400612" Name="www" Type="CNAME" Address="1.2.3.4" MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
</DomainDNSGetHostsResult>
</CommandResponse>
<Server>PHX01APIEXT04</Server>
<GMTTimeDifference>--4:00</GMTTimeDifference>
<ExecutionTime>0.16</ExecutionTime>
</ApiResponse>`;
let getHostsReturn = {
'Type': 'namecheap.domains.dns.getHosts',
'DomainDNSGetHostsResult': {
'Domain': 'example-dns-test.com',
'EmailType': 'FWD',
'IsUsingOurDNS': 'true',
'host': [
{
'HostId': '614433',
'Name': 'www',
'Type': 'CNAME',
'Address': DOMAIN_0.domain,
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'Name': '@',
'Type': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
}
]
}
};
var req1 = nock(NAMECHEAP_ENDPOINT).get('/xml.response')
.query({
ApiUser: username,
ApiKey: token,
UserName: username,
ClientIp: '127.0.0.1',
Command: 'namecheap.domains.dns.getHosts',
SLD: DOMAIN_0.zoneName.split('.')[0],
TLD: DOMAIN_0.zoneName.split('.')[1]
})
.reply(200, GET_HOSTS_RETURN);
let setInternalExpect = [
{
'HostId': '614433',
'HostName': 'www',
'RecordType': 'CNAME',
'Address': '1.2.3.4',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'HostName': '@',
'RecordType': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
}
];
var req2 = nock(NAMECHEAP_ENDPOINT).post('/xml.response')
.query({
ApiUser: username,
ApiKey: token,
UserName: username,
ClientIp: '127.0.0.1',
Command: 'namecheap.domains.dns.setHosts',
SLD: DOMAIN_0.zoneName.split('.')[0],
TLD: DOMAIN_0.zoneName.split('.')[1],
let getHostsFake = sinon.fake.yields(null, getHostsReturn);
let setHostsFake = sinon.fake.yields(null, true);
let mockObj = {
dns: {
getHosts: getHostsFake,
setHosts: setHostsFake
}
};
TTL1: '300',
HostName1: '@',
RecordType1: 'MX',
Address1: 'my.nebulon.space.',
EmailType1: 'MX',
MXPref1: '10',
sandbox.stub(namecheap.prototype, 'domains').value(mockObj);
TTL2: '300',
HostName2: 'www',
RecordType2: 'CNAME',
Address2: '1.2.3.4'
})
.reply(200, SET_HOSTS_RETURN);
domains.upsertDnsRecords('www', DOMAIN_0.domain, 'CNAME', ['1.2.3.4'], function (error) {
expect(error).to.eql(null);
expect(setHostsFake.calledOnce).to.eql(true);
expect(setHostsFake.calledWith(DOMAIN_0.domain, setInternalExpect)).to.eql(true);
expect(req1.isDone()).to.be.ok();
expect(req2.isDone()).to.be.ok();
done();
});
});
it('get succeeds', function(done) {
let getHostsReturn = {
'Type': 'namecheap.domains.dns.getHosts',
'DomainDNSGetHostsResult': {
'Domain': 'example-dns-test.com',
'EmailType': 'FWD',
'IsUsingOurDNS': 'true',
'host': [
{
'HostId': '614433',
'Name': 'www',
'Type': 'CNAME',
'Address': '1.2.3.4',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'Name': 'test',
'Type': 'A',
'Address': '1.2.3.4',
'MXPref': '10',
'TTL': '1800',
'FriendlyName': 'A Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614431',
'Name': 'test',
'Type': 'A',
'Address': '2.3.4.5',
'MXPref': '10',
'TTL': '1800',
'FriendlyName': 'A Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
}
]
}
};
const GET_HOSTS_RETURN = `<?xml version="1.0" encoding="utf-8"?>
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
<Errors />
<Warnings />
<RequestedCommand>namecheap.domains.dns.gethosts</RequestedCommand>
<CommandResponse Type="namecheap.domains.dns.getHosts">
<DomainDNSGetHostsResult Domain="cloudron.space" EmailType="MX" IsUsingOurDNS="true">
<host HostId="160859434" Name="@" Type="MX" Address="my.nebulon.space." MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
<host HostId="173400612" Name="test" Type="A" Address="1.2.3.4" MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
<host HostId="173400613" Name="test" Type="A" Address="2.3.4.5" MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
</DomainDNSGetHostsResult>
</CommandResponse>
<Server>PHX01APIEXT04</Server>
<GMTTimeDifference>--4:00</GMTTimeDifference>
<ExecutionTime>0.16</ExecutionTime>
</ApiResponse>`;
let getHostsFake = sinon.fake.yields(null, getHostsReturn);
let mockObj = {
dns: {
getHosts: getHostsFake
}
};
sandbox.stub(namecheap.prototype, 'domains').value(mockObj);
var req1 = nock(NAMECHEAP_ENDPOINT).get('/xml.response')
.query({
ApiUser: username,
ApiKey: token,
UserName: username,
ClientIp: '127.0.0.1',
Command: 'namecheap.domains.dns.getHosts',
SLD: DOMAIN_0.zoneName.split('.')[0],
TLD: DOMAIN_0.zoneName.split('.')[1]
})
.reply(200, GET_HOSTS_RETURN);
domains.getDnsRecords('test', DOMAIN_0.domain, 'A', function (error, result) {
expect(error).to.eql(null);
expect(req1.isDone()).to.be.ok();
expect(result).to.be.an(Array);
expect(result.length).to.eql(2);
expect(getHostsFake.calledOnce).to.eql(true);
expect(result).to.eql(['1.2.3.4', '2.3.4.5']);
done();
@@ -1062,130 +878,74 @@ describe('dns provider', function () {
});
it('del succeeds', function (done) {
const GET_HOSTS_RETURN = `<?xml version="1.0" encoding="utf-8"?>
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
<Errors />
<Warnings />
<RequestedCommand>namecheap.domains.dns.gethosts</RequestedCommand>
<CommandResponse Type="namecheap.domains.dns.getHosts">
<DomainDNSGetHostsResult Domain="cloudron.space" EmailType="MX" IsUsingOurDNS="true">
<host HostId="160859434" Name="@" Type="MX" Address="my.nebulon.space." MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
<host HostId="173400612" Name="www" Type="CNAME" Address="1.2.3.4" MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
</DomainDNSGetHostsResult>
</CommandResponse>
<Server>PHX01APIEXT04</Server>
<GMTTimeDifference>--4:00</GMTTimeDifference>
<ExecutionTime>0.16</ExecutionTime>
</ApiResponse>`;
let getHostsReturn = {
'Type': 'namecheap.domains.dns.getHosts',
'DomainDNSGetHostsResult': {
'Domain': 'example-dns-test.com',
'EmailType': 'FWD',
'IsUsingOurDNS': 'true',
'host': [
{
'HostId': '614433',
'Name': 'www',
'Type': 'CNAME',
'Address': '1.2.3.4',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'Name': '@',
'Type': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
}
]
}
};
let setInternalExpect = [
{
'HostId': '614432',
'HostName': '@',
'RecordType': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
}
];
let getHostsFake = sinon.fake.yields(null, getHostsReturn);
let setHostsFake = sinon.fake.yields(null, true);
let mockObj = {
dns: {
getHosts: getHostsFake,
setHosts: setHostsFake
}
};
sandbox.stub(namecheap.prototype, 'domains').value(mockObj);
var req1 = nock(NAMECHEAP_ENDPOINT).get('/xml.response')
.query({
ApiUser: username,
ApiKey: token,
UserName: username,
ClientIp: '127.0.0.1',
Command: 'namecheap.domains.dns.getHosts',
SLD: DOMAIN_0.zoneName.split('.')[0],
TLD: DOMAIN_0.zoneName.split('.')[1]
})
.reply(200, GET_HOSTS_RETURN);
domains.removeDnsRecords('www', DOMAIN_0.domain, 'CNAME', ['1.2.3.4'], function (error) {
expect(error).to.eql(null);
expect(setHostsFake.calledOnce).to.eql(true);
expect(setHostsFake.calledWith(DOMAIN_0.domain, setInternalExpect)).to.eql(true);
expect(req1.isDone()).to.be.ok();
done();
});
});
it('del succeeds w/ non-present host', function (done) {
it('del succeeds with non-existing domain', function (done) {
const GET_HOSTS_RETURN = `<?xml version="1.0" encoding="utf-8"?>
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
<Errors />
<Warnings />
<RequestedCommand>namecheap.domains.dns.gethosts</RequestedCommand>
<CommandResponse Type="namecheap.domains.dns.getHosts">
<DomainDNSGetHostsResult Domain="cloudron.space" EmailType="MX" IsUsingOurDNS="true">
<host HostId="160859434" Name="@" Type="MX" Address="my.nebulon.space." MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
<host HostId="173400612" Name="www" Type="CNAME" Address="1.2.3.4" MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
</DomainDNSGetHostsResult>
</CommandResponse>
<Server>PHX01APIEXT04</Server>
<GMTTimeDifference>--4:00</GMTTimeDifference>
<ExecutionTime>0.16</ExecutionTime>
</ApiResponse>`;
let getHostsReturn = {
'Type': 'namecheap.domains.dns.getHosts',
'DomainDNSGetHostsResult': {
'Domain': 'example-dns-test.com',
'EmailType': 'FWD',
'IsUsingOurDNS': 'true',
'host': [
{
'HostId': '614433',
'Name': 'www',
'Type': 'CNAME',
'Address': 'parkingpage.namecheap.com.',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'Name': '@',
'Type': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
}
]
}
};
let getHostsFake = sinon.fake.yields(null, getHostsReturn);
let setHostsFake = sinon.fake.yields(null, true);
let mockObj = {
dns: {
getHosts: getHostsFake,
setHosts: setHostsFake
}
};
sandbox.stub(namecheap.prototype, 'domains').value(mockObj);
var req1 = nock(NAMECHEAP_ENDPOINT).get('/xml.response')
.query({
ApiUser: username,
ApiKey: token,
UserName: username,
ClientIp: '127.0.0.1',
Command: 'namecheap.domains.dns.getHosts',
SLD: DOMAIN_0.zoneName.split('.')[0],
TLD: DOMAIN_0.zoneName.split('.')[1]
})
.reply(200, GET_HOSTS_RETURN);
domains.removeDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) {
expect(error).to.eql(null);
expect(setHostsFake.notCalled).to.eql(true);
expect(req1.isDone()).to.be.ok();
done();
});
@@ -1375,7 +1135,7 @@ describe('dns provider', function () {
});
});
xdescribe('gcdns', function () {
describe('gcdns', function () {
var HOSTED_ZONES = [];
var zoneQueue = [];
var _OriginalGCDNS;
@@ -1413,7 +1173,7 @@ describe('dns provider', function () {
}
function fakeZone(name, ns, recordQueue) {
var zone = GCDNS().zone(name.replace('.', '-'));
var zone = new GCDNS().zone(name.replace('.', '-'));
zone.metadata.dnsName = name + '.';
zone.metadata.nameServers = ns || ['8.8.8.8', '8.8.4.4'];
zone.getRecords = mockery(recordQueue || zoneQueue);
@@ -1450,7 +1210,7 @@ describe('dns provider', function () {
it('upsert existing record succeeds', function (done) {
zoneQueue.push([null, HOSTED_ZONES]);
zoneQueue.push([null, [GCDNS().zone('test').record('A', { 'name': 'test', data: ['5.6.7.8'], ttl: 1 })]]);
zoneQueue.push([null, [new GCDNS().zone('test').record('A', { 'name': 'test', data: ['5.6.7.8'], ttl: 1 })]]);
zoneQueue.push([null, { id: '2' }]);
domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) {
@@ -1476,7 +1236,7 @@ describe('dns provider', function () {
it('get succeeds', function (done) {
zoneQueue.push([null, HOSTED_ZONES]);
zoneQueue.push([null, [GCDNS().zone('test').record('A', { 'name': 'test', data: ['1.2.3.4', '5.6.7.8'], ttl: 1 })]]);
zoneQueue.push([null, [new GCDNS().zone('test').record('A', { 'name': 'test', data: ['1.2.3.4', '5.6.7.8'], ttl: 1 })]]);
domains.getDnsRecords('test', DOMAIN_0.domain, 'A', function (error, result) {
expect(error).to.eql(null);
@@ -1491,7 +1251,7 @@ describe('dns provider', function () {
it('del succeeds', function (done) {
zoneQueue.push([null, HOSTED_ZONES]);
zoneQueue.push([null, [GCDNS().zone('test').record('A', { 'name': 'test', data: ['5.6.7.8'], ttl: 1 })]]);
zoneQueue.push([null, [new GCDNS().zone('test').record('A', { 'name': 'test', data: ['5.6.7.8'], ttl: 1 })]]);
zoneQueue.push([null, { id: '5' }]);
domains.removeDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) {

View File

@@ -100,7 +100,7 @@ function setup(done) {
database._clear.bind(null),
ldapServer.start.bind(null),
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
maildb.add.bind(null, DOMAIN_0.domain),
maildb.add.bind(null, DOMAIN_0.domain, {}),
function (callback) {
users.createOwner(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE, function (error, result) {
if (error) return callback(error);

View File

@@ -11,7 +11,6 @@ var async = require('async'),
users = require('../users.js'),
userdb = require('../userdb.js'),
eventlogdb = require('../eventlogdb.js'),
notificationdb = require('../notificationdb.js'),
notifications = require('../notifications.js'),
NotificationsError = notifications.NotificationsError,
expect = require('expect.js');
@@ -152,20 +151,24 @@ describe('Notifications', function () {
});
});
it('getAllPaged succeeds for second page', function (done) {
async.timesSeries(20, function (n, callback) {
notifications._add(USER_0.id, EVENT_0.id, 'title' + n, 'some message', callback);
it('getAllPaged succeeds for second page (takes 5 seconds to add)', function (done) {
async.timesSeries(5, function (n, callback) {
// timeout is for database TIMESTAMP resolution
setTimeout(function () {
notifications._add(USER_0.id, EVENT_0.id, 'title' + n, 'some message', callback);
}, 1000);
}, function (error) {
expect(error).to.eql(null);
notifications.getAllPaged(USER_0.id, null /* ack */, 2, 10, function (error, results) {
notifications.getAllPaged(USER_0.id, null /* ack */, 2, 3, function (error, results) {
expect(error).to.eql(null);
expect(results).to.be.an(Array);
expect(results.length).to.be(10);
expect(results.length).to.be(3);
// we cannot compare the title because ordering is by time which is stored in mysql with seconds
// precision. making the ordering random...
// expect(results[0].title).to.equal('title9');
expect(results[0].title).to.equal('title1');
expect(results[1].title).to.equal('title0');
// the previous tests already add one notification with 'title'
expect(results[2].title).to.equal('title');
done();
});