Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11353e9e3a | ||
|
|
8cd5c15c2b | ||
|
|
b86b8b8ee1 | ||
|
|
c5f6e6b028 | ||
|
|
592d8abc58 | ||
|
|
d93068fc62 | ||
|
|
a864af52df | ||
|
|
1eedd4b185 | ||
|
|
9d38edfe95 | ||
|
|
f895ebba73 | ||
|
|
511287b16e | ||
|
|
530e06ec66 | ||
|
|
9cab383b43 | ||
|
|
9785ab82ed | ||
|
|
9d237e7bd6 | ||
|
|
7e9885012d | ||
|
|
1de785d97c | ||
|
|
2bd6566537 | ||
|
|
88fa4cf188 | ||
|
|
b26167481e | ||
|
|
1b6af9bd12 | ||
|
|
0159963cb0 | ||
|
|
996041cabc | ||
|
|
cb0352e33c | ||
|
|
3169f032c8 | ||
|
|
5ff8ee1a8f | ||
|
|
d3f31a3ace | ||
|
|
ac7e7f0db9 | ||
|
|
4c1e967dad | ||
|
|
f3ccd5c074 | ||
|
|
8369c0e2c0 | ||
|
|
122a966e72 | ||
|
|
9c2ff2f862 | ||
|
|
0ba45e746b | ||
|
|
54c06cdabb | ||
|
|
5a2e10317c | ||
|
|
8292d52acf | ||
|
|
7d21470fc7 | ||
|
|
eb0530bcba | ||
|
|
8855092faa | ||
|
|
2e02a3c71e | ||
|
|
5b5303ba7f | ||
|
|
022a54278e | ||
|
|
19b50dc428 | ||
|
|
e7eac003a9 | ||
|
|
cc17c6b2cd | ||
|
|
23d16b07aa | ||
|
|
7ecb3dd771 | ||
|
|
e43f974d34 | ||
|
|
e16cd38722 | ||
|
|
9d2f81d6b9 | ||
|
|
3fe539436b | ||
|
|
76f94eb559 | ||
|
|
7630ef921d | ||
|
|
625127d298 | ||
|
|
f24c4d2805 | ||
|
|
194340afa0 | ||
|
|
fdc9639aba | ||
|
|
f95ec53a85 | ||
|
|
3d425b7030 | ||
|
|
37c6c24e0e | ||
|
|
50bdd7ec7b | ||
|
|
769cb3e251 | ||
|
|
9447c45406 | ||
|
|
66a3962cfe | ||
|
|
d145eacbaf | ||
|
|
ed03ed7bad | ||
|
|
953b463799 | ||
|
|
6d28bb0489 | ||
|
|
c2f464ea75 | ||
|
|
4c56ffc767 | ||
|
|
885aa8833c | ||
|
|
63310c44c0 | ||
|
|
05dd65718f | ||
|
|
05d3f8a667 | ||
|
|
3fa45ea728 | ||
|
|
a7d2098f09 | ||
|
|
e1ecb49d59 | ||
|
|
6facfac4c5 | ||
|
|
97d2494fe3 | ||
|
|
a54be69c96 | ||
|
|
800e25a7a7 | ||
|
|
c1ce2977fa |
34
CHANGES
34
CHANGES
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
15
migrations/20190610195323-mail-add-dkimSelector.js
Normal file
15
migrations/20190610195323-mail-add-dkimSelector.js
Normal 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);
|
||||
});
|
||||
};
|
||||
@@ -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
1070
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
44
package.json
44
package.json
@@ -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",
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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: '© 2019 [Cloudron](https://cloudron.io) [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)'
|
||||
|
||||
132
src/addons.js
132
src/addons.js
@@ -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([
|
||||
|
||||
@@ -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
|
||||
|
||||
18
src/appdb.js
18
src/appdb.js
@@ -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));
|
||||
|
||||
|
||||
88
src/apps.js
88
src/apps.js
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
|
||||
@@ -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: '© 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;
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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)}`);
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
];
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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' }
|
||||
}
|
||||
|
||||
39
src/ldap.js
39
src/ldap.js
@@ -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 });
|
||||
|
||||
71
src/mail.js
71
src/mail.js
@@ -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));
|
||||
|
||||
@@ -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>
|
||||
<% } %>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>"
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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, {}));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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'));
|
||||
|
||||
|
||||
@@ -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'));
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user