Compare commits

..

3 Commits

Author SHA1 Message Date
Johannes Zellner f14a8b0ab0 add 1.8.3 changes 2017-11-23 02:48:16 +01:00
Girish Ramakrishnan 5f207716e5 1.8.2 changes 2017-11-23 02:48:16 +01:00
Johannes Zellner 010d48035b Setup domain record in settingsdb during dns setup 2017-11-23 02:48:12 +01:00
435 changed files with 67118 additions and 37703 deletions
-29
View File
@@ -1,29 +0,0 @@
{
"env": {
"es6": true,
"node": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 2017
},
"rules": {
"indent": [
"error",
4
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
],
"no-console": "off"
}
}
+1 -2
View File
@@ -1,7 +1,6 @@
# following files are skipped when exporting using git archive
test export-ignore
.jshintrc export-ignore
.gitlab export-ignore
docs export-ignore
.gitattributes export-ignore
.gitignore export-ignore
+2
View File
@@ -1,8 +1,10 @@
node_modules/
coverage/
webadmin/dist/
setup/splash/website/
installer/src/certs/server.key
# vim swap files
*.swp
-6
View File
@@ -1,6 +0,0 @@
Please do not use this issue tracker for support requests and bug reports.
This issue tracker is used by the Cloudron development team to track actual
bugs in the code.
Please use the forum at https://forum.cloudron.io to report bugs. For
confidential issues, please email us at support@cloudron.io.
-7
View File
@@ -1,7 +0,0 @@
Please do not use this issue tracker for support requests and feature reports.
This issue tracker is used by the Cloudron development team to track issues in
the code.
Please use the forum at https://forum.cloudron.io to report bugs. For
confidential issues, please email us at support@cloudron.io.
+8
View File
@@ -0,0 +1,8 @@
{
"node": true,
"browser": true,
"unused": true,
"globalstrict": true,
"predef": [ "angular", "$" ],
"esnext": true
}
-398
View File
@@ -1134,401 +1134,3 @@
[1.8.3]
* Ensure domain database record exists
[1.8.4]
* Fix issue where internal email was not delivered when email relay is enabled
* Fix display of DNS records when email relay is enabled
[1.8.5]
* Fix issues where unused addons were not cleaned on an app update causing uninstall to fail
* Change UI text from 'Waiting' to 'Pending'
[1.9.0]
* Prepare Cloudron for supporting multiple domains
* Add Cloudron restore UI
* Do not put app in errored state if backup fails
* Display backup progress in CaaS
* Add Google Cloud Storage backend for backups
* Update node to 8.9.3 LTS
* Set max email recepient limit (in outgoing emails) to 500
[1.9.1]
* Prepare Cloudron for supporting multiple domains
* Add Cloudron restore UI
* Do not put app in errored state if backup fails
* Display backup progress in CaaS
* Add Google Cloud Storage backend for backups
* Update node to 8.9.3 LTS
* Set max email recepient limit (in outgoing emails) to 500
* Put terminal and app logs viewer to separate window
[1.9.2]
* Prepare Cloudron for supporting multiple domains
* Add Cloudron restore UI
* Do not put app in errored state if backup fails
* Display backup progress in CaaS
* Add Google Cloud Storage backend for backups
* Update node to 8.9.3 LTS
* Set max email recepient limit (in outgoing emails) to 500
* Put terminal and app logs viewer to separate window
[1.9.3]
* Prepare Cloudron for supporting multiple domains
* Add Cloudron restore UI
* Do not put app in errored state if backup fails
* Display backup progress in CaaS
* Add Google Cloud Storage backend for backups
* Update node to 8.9.3 LTS
* Set max email recepient limit (in outgoing emails) to 500
* Put terminal and app logs viewer to separate window
[1.9.4]
* Fix typo causing LE cert renewals to fail
[1.10.0]
* Migrate mailboxes to support multiple domains
* Update addon containers to latest versions
* Add DigitalOcean Spaces region Singapore 1 (SGP1)
* Configure Exoscale SOS to use new SOS NG endpoint
* Fix S3 storage backend CopySource encoding rules
[1.10.1]
* Migrate mailboxes to support multiple domains
* Update addon containers to latest versions
* Add DigitalOcean Spaces region Singapore 1 (SGP1)
* Configure Exoscale SOS to use new SOS NG endpoint
* Fix S3 storage backend CopySource encoding rules
[1.10.2]
* Migrate mailboxes to support multiple domains
* Update addon containers to latest versions
* Add DigitalOcean Spaces region Singapore 1 (SGP1)
* Configure Exoscale SOS to use new SOS NG endpoint
* Fix S3 storage backend CopySource encoding rules
[1.11.0]
* Update Haraka to 2.8.17 to fix various crashes
* Report dependency error for clone if backup or domain was not found
* Enable auto-updates for major versions
[2.0.0]
* Multi-domain support
* Update Haraka to 2.8.18
* Split box and app autoupdate pattern settings
* Stop and disable any pre-installed postfix server
* Migrate altDomain as a manual DNS provider
* Use node's native dns resolve instead of dig
* DNS records can now be a A record or a CNAME record
* Fix generation of fallback certificates to include naked domain
* Merge multi-string DKIM records
* scheduler: do not start cron jobs all at once
* scheduler: give cron jobs a grace period of 30 minutes to complete
[2.0.1]
* Multi-domain support
* Update Haraka to 2.8.18
* Split box and app autoupdate pattern settings
* Stop and disable any pre-installed postfix server
* Migrate altDomain as a manual DNS provider
* Use node's native dns resolve instead of dig
* DNS records can now be a A record or a CNAME record
* Fix generation of fallback certificates to include naked domain
* Merge multi-string DKIM records
* scheduler: do not start cron jobs all at once
* scheduler: give cron jobs a grace period of 30 minutes to complete
* Rework the eventlog view
* App clone now clones the robotsTxt and backup settings
[2.1.0]
* Make S3 backend work reliably with slow internet connections
* Update docker to 18.03.0-ce
* Finalize the Email and Mailbox API
* Move mailbox settings from users to email view
* mail: fix issue where hosts with valid SPF for a Cloudron domain are unable to send mail to Cloudron
* mail: fix crash when bounce emails have a null sender
* Add CSP header for dashboard
* Add support for installing private docker images
[2.1.1]
* Make S3 backend work reliably with slow internet connections
* Update docker to 18.03.0-ce
* Finalize the Email and Mailbox API
* Move mailbox settings from users to email view
* mail: fix issue where hosts with valid SPF for a Cloudron domain are unable to send mail to Cloudron
* mail: fix crash when bounce emails have a null sender
* Add CSP header for dashboard
* Add support for installing private docker images
[2.2.0]
* Add 2FA support for the admin dashboard
* Cleanup scope management in REST API
* Enhance user creation API to take a password
* Relax restriction on mailbox names now that it is decoupled from user management
[2.2.1]
* Add 2FA support for the admin dashboard
* Add Gandi & GoDaddy DNS providers
* Fix zone detection logic on Route53 accounts with more than 100 zones
* Warn using when disabling email
* Cleanup scope management in REST API
* Enhance user creation API to take a password
* Relax restriction on mailbox names now that it is decoupled from user management
* Fix issue where mail container incorrectly advertised CRAM-MD5 support
[2.3.0]
* Add Name.com DNS provider
* Fix issue where account setup page was crashing
* Add advanced DNS configuration UI
* Preserve addon/database configuration across app updates and restores
* ManageSieve port now offers STARTTLS
[2.3.1]
* Add Name.com DNS provider
* Fix issue where account setup page was crashing
* Add advanced DNS configuration UI
* Preserve addon/database configuration across app updates and restores
* ManageSieve port now offers STARTTLS
* Allow mailbox name to be set for apps
* Rework the Email server UI
* Add the ability to manually trigger a backup of an application
* Enable/disable mail from validation within UI
* Allow setting app visibility for non-SSO apps
* Add Clone UI
[2.3.2]
* Fix issue where multi-db apps were not provisioned correctly
* Improve setup, restore views to have field labels
[2.4.0]
* Use custom logging backend to have more control over log rotation
* Make user explicitly confirm that fs backup dir is on external storage
* Update node to 8.11.2
* Update docker to 18.03.1
* Fix docker exec terminal resize issue
* Make the mailbox name follow the apps new location, if the user did not set it explicitly
* Add backups view
[2.4.1]
* Use custom logging backend to have more control over log rotation
* Mail logs and box logs UI
* Make user explicitly confirm that fs backup dir is on external storage
* Update node to 8.11.2
* Update docker to 18.03.1
* Fix docker exec terminal resize issue
* Make the mailbox name follow the apps new location, if the user did not set it explicitly
* Add backups view
[3.0.0]
* Support alternate app domains with redirects
* Allow hyphen in mailbox names
* Fix issue where the UI timesout when relay server is not reachable
* Add support for personal spaces
* Add UI to edit users in the groups dialog
* Add UI to set groups when creating a user
* Open logs and terminal in a new tab instead of a window
* Add button to view backup logs
* Add Mailjet mail relay support
* Encryption support for incremental backups
* Display restore errors in the UI
* Update Haraka to 2.8.19
* GPG verify releases
* Allow subdomains in location field
[3.0.1]
* Support alternate app domains with redirects
* Allow hyphen in mailbox names
* Fix issue where the UI timesout when relay server is not reachable
* Add support for personal spaces
* Add UI to edit users in the groups dialog
* Add UI to set groups when creating a user
* Open logs and terminal in a new tab instead of a window
* Add button to view backup logs
* Add Mailjet mail relay support
* Encryption support for incremental backups
* Display restore errors in the UI
* Update Haraka to 2.8.19
* GPG verify releases
* Allow subdomains in location field
[3.0.2]
* Fix issue where normal users are shown apps they don't have access to
* Re-configure email apps when email is enabled/disabled
[3.1.0]
* Add UDP support
* Clicking invite button does not send an invite immediately
* Implement docker addon
* Automatically login after password reset and account setup
* Make backup interval configurable
* Fix alternate domain certificate renewal
[3.1.1]
* Fix caas domain migration
[3.1.2]
* Add UDP support
* Clicking invite button does not send an invite immediately
* Implement docker addon
* Automatically login after password reset and account setup
* Make backup interval configurable
* Fix alternate domain certificate renewal
* API token can now have a name
[3.1.3]
* Prevent dashboard domain from being deleted
* Add alternateDomains to app install route
* cloudflare: Fix crash when access denied
[3.1.4]
* Fix issue where support tab was redirecting
[3.2.0]
* Add DO Spaces SFO2 region
* Wildcard DNS now validates the config
* Add ACMEv2 support
* Add Wildcard Let's Encrypt provider
[3.2.1]
* Add acme2 support. This provides DNS based validation removing inbound port 80 requirement
* Add support for wildcard certificates
* Allow mailbox name to be reset to the buit-in '.app' name
* Fix permission issue when restoring a Cloudron
* Fix a crash when restoring Cloudron
* Allow alternate domains to be set in app installation REST API
* Add SFO2 region for DigitalOcean Spaces
* Show the title in port bindings instead of the long description
[3.2.2]
* Update Haraka to 2.8.20
* (mail) Fix issue where LDAP connections where not cleaned up
[3.3.0]
* Use new addons with REST APIs
* Ubuntu 18.04 LTS support
* Custom env vars can be set per application
* Add a button to renew certs
* Add better support for private builds
* cloudflare: Fix crash when using bad email
* cloudflare: HTTP proxying works now
* add new exoscale-sos regions
* Add UI to toggle dynamic DNS
* Add support for hyphenated subdomains
[3.3.1]
* Use new addons with REST APIs
* Ubuntu 18.04 LTS support
* Custom env vars can be set per application
* Add a button to renew certs
* Add better support for private builds
* cloudflare: Fix crash when using bad email
* cloudflare: HTTP proxying works now
* add new exoscale-sos regions
* Add UI to toggle dynamic DNS
* Add support for hyphenated subdomains
[3.3.2]
* Use new addons with REST APIs
* Ubuntu 18.04 LTS support
* Custom env vars can be set per application
* Add a button to renew certs
* Add better support for private builds
* cloudflare: Fix crash when using bad email
* cloudflare: HTTP proxying works now
* add new exoscale-sos regions
* Add UI to toggle dynamic DNS
* Add support for hyphenated subdomains
* Add domain, mail events to eventlog
[3.3.3]
* Use new addons with REST APIs
* Ubuntu 18.04 LTS support
* Custom env vars can be set per application
* Add a button to renew certs
* Add better support for private builds
* cloudflare: Fix crash when using bad email
* cloudflare: HTTP proxying works now
* add new exoscale-sos regions
* Add UI to toggle dynamic DNS
* Add support for hyphenated subdomains
* Add domain, mail events to eventlog
[3.3.4]
* Use new addons with REST APIs
* Ubuntu 18.04 LTS support
* Custom env vars can be set per application
* Add a button to renew certs
* Add better support for private builds
* cloudflare: Fix crash when using bad email
* cloudflare: HTTP proxying works now
* add new exoscale-sos regions
* Add UI to toggle dynamic DNS
* Add support for hyphenated subdomains
* Add domain, mail events to eventlog
[3.4.0]
* Improve error page
* Add system view to manage addons and view their status
* Fix iconset regression for account and Cloudron name edits
* Add server reboot button and warn if reboot is required for security updates
* Backup and update tasks are now cancelable
* Move graphite away from port 3000 (reserved by ESXi)
* Flexible mailbox management
* Automatic updates can be toggled per app
[3.4.1]
* Improve error page
* Add system view to manage addons and view their status
* Fix iconset regression for account and Cloudron name edits
* Add server reboot button and warn if reboot is required for security updates
* Backup and update tasks are now cancelable
* Move graphite away from port 3000 (reserved by ESXi)
* Flexible mailbox management
* Automatic updates can be toggled per app
[3.4.2]
* Improve error page
* Add system view to manage addons and view their status
* Fix iconset regression for account and Cloudron name edits
* Add server reboot button and warn if reboot is required for security updates
* Backup and update tasks are now cancelable
* Move graphite away from port 3000 (reserved by ESXi)
* Flexible mailbox management
* Automatic updates can be toggled per app
[3.4.3]
* Improve error page
* Add system view to manage addons and view their status
* Fix iconset regression for account and Cloudron name edits
* Add server reboot button and warn if reboot is required for security updates
* Backup and update tasks are now cancelable
* Move graphite away from port 3000 (reserved by ESXi)
* Flexible mailbox management
* Automatic updates can be toggled per app
* Fix issue where OOM mails are sent out without a rate limit
[3.5.0]
* Add UI to switch dashboard domain
* Fix remote support button to not remove misparsed ssh keys
* cloudflare: preseve domain proxying status
* Fix issue where oom killer might kill the box code or the updater
* Add contabo and netcup as supported providers
* Allow full logs to be downloaded
* Update Haraka to 2.8.22
* Log events in the mail container
* Fix issue where SpamAssassin and SPF checks were run for outbound email
* Improve various eventlog messages
* Track dyndns change events
* Add new S3 regions - Paris/Stockholm/Osaka
* Retry errored downloads during restore
* Add user pagination UI
* Add namecheap as supported DNS provider
[3.5.1]
* Add dashboard domain change event
* Fix issue where notification email were sent from incorrect domain
* Alert about configuration issues in the notification UI
* Switching dashboard domain now updates MX, SPF records
* Mailbox and lists UI is now always visible (but disabled) when incoming email is disabled
* Fix issue where long passwords were not accepted
* DNS and backup credential secrets are not returned in API calls anymore
* Send notification when an app that went down, came back up
+1 -1
View File
@@ -630,7 +630,7 @@ state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
box
Copyright (C) 2016-2019 Cloudron UG
Copyright (C) 2016,2017 Cloudron UG
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
+23 -9
View File
@@ -9,6 +9,10 @@ a complex task.
We are building the ultimate platform for self-hosting web apps. The Cloudron allows
anyone to effortlessly host web applications on their server on their own terms.
Support us on
[![Flattr Cloudron](https://button.flattr.com/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=cloudron&url=https://cloudron.io&title=Cloudron&tags=opensource&category=software)
or [pay us a coffee](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=8982CKNM46D8U)
## Features
* Single click install for apps. Check out the [App Store](https://cloudron.io/appstore.html).
@@ -29,9 +33,9 @@ anyone to effortlessly host web applications on their server on their own terms.
* Trivially migrate to another server keeping your apps and data (for example, switch your
infrastructure provider or move to a bigger server).
* Comprehensive [REST API](https://cloudron.io/documentation/developer/api/).
* Comprehensive [REST API](https://cloudron.io/references/api.html).
* [CLI](https://cloudron.io/documentation/cli/) to configure apps.
* [CLI](https://git.cloudron.io/cloudron/cloudron-cli) to configure apps.
* Alerts, audit logs, graphs, dns management ... and much more
@@ -45,25 +49,35 @@ 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)
* [Selfhosting](https://cloudron.io/references/selfhosting.html) - [Pricing](https://cloudron.io/pricing.html)
* [Managed Hosting](https://cloudron.io/managed.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
on external services such as the App Store for apps to be installed. As such, don't
clone this repo and npm install and expect something to work.
The wiki has instructions on how you can install and update the Cloudron and the
apps from source.
## Documentation
* [Documentation](https://cloudron.io/documentation/)
* [User manual](https://cloudron.io/references/usermanual.html)
* [Developer docs](https://cloudron.io/documentation.html)
* [Architecture](https://cloudron.io/references/architecture.html)
## Related repos
The [base image repo](https://git.cloudron.io/cloudron/docker-base-image) is the parent image of all
the containers in the Cloudron.
The [graphite repo](https://git.cloudron.io/cloudron/docker-graphite) contains the graphite code
that collects metrics for graphs.
The addons are located in separate repositories
* [Redis](https://git.cloudron.io/cloudron/redis-addon)
* [Postgresql](https://git.cloudron.io/cloudron/postgresql-addon)
* [MySQL](https://git.cloudron.io/cloudron/mysql-addon)
* [Mongodb](https://git.cloudron.io/cloudron/mongodb-addon)
* [Mail](https://git.cloudron.io/cloudron/mail-addon)
## Community
* [Forum](https://forum.cloudron.io/)
* [Chat](https://chat.cloudron.io/)
* [Support](mailto:support@cloudron.io)
-1
View File
@@ -1 +0,0 @@
# release version. do not edit manually
+1 -1
View File
@@ -29,7 +29,7 @@ function create_droplet() {
local ssh_key_id="$1"
local box_name="$2"
local image_region="sfo2"
local image_region="sfo1"
local ubuntu_image_slug="ubuntu-16-04-x64"
local box_size="1gb"
+10 -39
View File
@@ -14,11 +14,8 @@ function die {
export DEBIAN_FRONTEND=noninteractive
# hold grub since updating it breaks on some VPS providers. also, dist-upgrade will trigger it
apt-mark hold grub* >/dev/null
apt-get -o Dpkg::Options::="--force-confdef" update -y
apt-get -o Dpkg::Options::="--force-confdef" upgrade -y
apt-mark unhold grub* >/dev/null
apt-get -o Dpkg::Options::="--force-confdef" dist-upgrade -y
echo "==> Installing required packages"
@@ -26,25 +23,20 @@ debconf-set-selections <<< 'mysql-server mysql-server/root_password password pas
debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password password'
# this enables automatic security upgrades (https://help.ubuntu.com/community/AutomaticSecurityUpdates)
# resolvconf is needed for unbound to work property after disabling systemd-resolved in 18.04
ubuntu_version=$(lsb_release -rs)
gpg_package=$([[ "${ubuntu_version}" == "16.04" ]] && echo "gnupg" || echo "gpg")
apt-get -y install \
acl \
awscli \
build-essential \
cron \
curl \
dmsetup \
$gpg_package \
iptables \
libpython2.7 \
logrotate \
mysql-server-5.7 \
nginx-full \
openssh-server \
pwgen \
resolvconf \
sudo \
rcconf \
swaks \
unattended-upgrades \
unbound \
@@ -55,10 +47,10 @@ apt-get -y install \
cp /usr/share/unattended-upgrades/20auto-upgrades /etc/apt/apt.conf.d/20auto-upgrades
echo "==> Installing node.js"
mkdir -p /usr/local/node-8.9.3
curl -sL https://nodejs.org/dist/v8.9.3/node-v8.9.3-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-8.9.3
ln -sf /usr/local/node-8.9.3/bin/node /usr/bin/node
ln -sf /usr/local/node-8.9.3/bin/npm /usr/bin/npm
mkdir -p /usr/local/node-6.11.5
curl -sL https://nodejs.org/dist/v6.11.5/node-v6.11.5-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-6.11.5
ln -sf /usr/local/node-6.11.5/bin/node /usr/bin/node
ln -sf /usr/local/node-6.11.5/bin/npm /usr/bin/npm
apt-get install -y python # Install python which is required for npm rebuild
[[ "$(python --version 2>&1)" == "Python 2.7."* ]] || die "Expecting python version to be 2.7.x"
@@ -69,7 +61,7 @@ echo "==> Installing Docker"
mkdir -p /etc/systemd/system/docker.service.d
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=overlay2" > /etc/systemd/system/docker.service.d/cloudron.conf
curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_18.03.1~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_17.09.0~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
# apt install with install deps (as opposed to dpkg -i)
apt install -y /tmp/docker.deb
rm /tmp/docker.deb
@@ -80,9 +72,8 @@ if [[ "${storage_driver}" != "overlay2" ]]; then
exit 1
fi
# do not upgrade grub because it might prompt user and break this script
echo "==> Enable memory accounting"
apt-get -y --no-upgrade install grub2-common
apt-get -y install grub2
sed -e 's/^GRUB_CMDLINE_LINUX="\(.*\)"$/GRUB_CMDLINE_LINUX="\1 cgroup_enable=memory swapaccount=1 panic_on_oops=1 panic=5"/' -i /etc/default/grub
update-grub
@@ -92,12 +83,11 @@ if [ ! -f "${arg_infraversionpath}/infra_version.js" ]; then
exit 1
fi
images=$(node -e "var i = require('${arg_infraversionpath}/infra_version.js'); console.log(i.baseImages.map(function (x) { return x.tag; }).join(' '), Object.keys(i.images).map(function (x) { return i.images[x].tag; }).join(' '));")
images=$(node -e "var i = require('${arg_infraversionpath}/infra_version.js'); console.log(i.baseImages.join(' '), Object.keys(i.images).map(function (x) { return i.images[x].tag; }).join(' '));")
echo -e "\tPulling docker images: ${images}"
for image in ${images}; do
docker pull "${image}"
docker pull "${image%@sha256:*}" # this will tag the image for readability
done
echo "==> Install collectd"
@@ -107,11 +97,6 @@ if ! apt-get install -y collectd collectd-utils; then
sed -e 's/^FQDNLookup true/FQDNLookup false/' -i /etc/collectd/collectd.conf
fi
echo "==> Configuring host"
sed -e 's/^#NTP=/NTP=0.ubuntu.pool.ntp.org 1.ubuntu.pool.ntp.org 2.ubuntu.pool.ntp.org 3.ubuntu.pool.ntp.org/' -i /etc/systemd/timesyncd.conf
timedatectl set-ntp 1
timedatectl set-timezone UTC
# Disable bind for good measure (on online.net, kimsufi servers these are pre-installed and conflicts with unbound)
systemctl stop bind9 || true
systemctl disable bind9 || true
@@ -120,17 +105,3 @@ systemctl disable bind9 || true
systemctl stop dnsmasq || true
systemctl disable dnsmasq || true
# on ssdnodes postfix seems to run by default
systemctl stop postfix || true
systemctl disable postfix || true
# on ubuntu 18.04, this is the default. this requires resolvconf for DNS to work further after the disable
systemctl stop systemd-resolved || true
systemctl disable systemd-resolved || true
# ubuntu's default config for unbound does not work if ipv6 is disabled. this config is overwritten in start.sh
# we need unbound to work as this is required for installer.sh to do any DNS requests
ip6=$([[ -s /proc/net/if_inet6 ]] && echo "yes" || echo "no")
echo -e "server:\n\tinterface: 127.0.0.1\n\tdo-ip6: ${ip6}" > /etc/unbound/unbound.conf.d/cloudron-network.conf
systemctl restart unbound
+8 -21
View File
@@ -2,21 +2,17 @@
'use strict';
// prefix all output with a timestamp
// debug() already prefixes and uses process.stderr NOT console.*
['log', 'info', 'warn', 'debug', 'error'].forEach(function (log) {
var orig = console[log];
console[log] = function () {
orig.apply(console, [new Date().toISOString()].concat(Array.prototype.slice.call(arguments)));
};
});
require('supererror')({ splatchError: true });
let async = require('async'),
// remove timestamp from debug() based output
require('debug').formatArgs = function formatArgs(args) {
args[0] = this.namespace + ' ' + args[0];
};
var appHealthMonitor = require('./src/apphealthmonitor.js'),
async = require('async'),
config = require('./src/config.js'),
ldap = require('./src/ldap.js'),
dockerProxy = require('./src/dockerproxy.js'),
server = require('./src/server.js');
console.log();
@@ -29,9 +25,6 @@ console.log(' Version: ', config.version());
console.log(' Admin Origin: ', config.adminOrigin());
console.log(' Appstore API server origin: ', config.apiServerOrigin());
console.log(' Appstore Web server origin: ', config.webServerOrigin());
console.log(' SysAdmin Port: ', config.get('sysadminPort'));
console.log(' LDAP Server Port: ', config.get('ldapPort'));
console.log(' Docker Proxy Port: ', config.get('dockerProxyPort'));
console.log();
console.log('==========================================');
console.log();
@@ -39,7 +32,7 @@ console.log();
async.series([
server.start,
ldap.start,
dockerProxy.start
appHealthMonitor.start,
], function (error) {
if (error) {
console.error('Error starting server', error);
@@ -51,19 +44,13 @@ async.series([
var NOOP_CALLBACK = function () { };
process.on('SIGINT', function () {
console.log('Received SIGINT. Shutting down.');
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
dockerProxy.stop(NOOP_CALLBACK);
setTimeout(process.exit.bind(process), 3000);
});
process.on('SIGTERM', function () {
console.log('Received SIGTERM. Shutting down.');
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
dockerProxy.stop(NOOP_CALLBACK);
setTimeout(process.exit.bind(process), 3000);
});
+4 -12
View File
@@ -2,23 +2,15 @@
'use strict';
var database = require('./src/database.js');
var sendFailureLogs = require('./src/logcollector').sendFailureLogs;
// This is triggered by systemd with the crashed unit name as argument
function main() {
if (process.argv.length !== 3) return console.error('Usage: crashnotifier.js <unitName>');
if (process.argv.length !== 3) return console.error('Usage: crashnotifier.js <processName>');
var unitName = process.argv[2];
console.log('Started crash notifier for', unitName);
var processName = process.argv[2];
console.log('Started crash notifier for', processName);
// eventlog api needs the db
database.initialize(function (error) {
if (error) return console.error('Cannot connect to database. Unable to send crash log.', error);
sendFailureLogs(unitName);
});
sendFailureLogs(processName, { unit: processName });
}
main();
+205
View File
@@ -0,0 +1,205 @@
/* jslint node:true */
'use strict';
var argv = require('yargs').argv,
autoprefixer = require('gulp-autoprefixer'),
concat = require('gulp-concat'),
cssnano = require('gulp-cssnano'),
ejs = require('gulp-ejs'),
gulp = require('gulp'),
rimraf = require('rimraf'),
sass = require('gulp-sass'),
serve = require('gulp-serve'),
sourcemaps = require('gulp-sourcemaps'),
uglify = require('gulp-uglify'),
url = require('url');
gulp.task('3rdparty', function () {
gulp.src([
'webadmin/src/3rdparty/**/*.js',
'webadmin/src/3rdparty/**/*.map',
'webadmin/src/3rdparty/**/*.css',
'webadmin/src/3rdparty/**/*.otf',
'webadmin/src/3rdparty/**/*.eot',
'webadmin/src/3rdparty/**/*.svg',
'webadmin/src/3rdparty/**/*.gif',
'webadmin/src/3rdparty/**/*.ttf',
'webadmin/src/3rdparty/**/*.woff',
'webadmin/src/3rdparty/**/*.woff2'
])
.pipe(gulp.dest('webadmin/dist/3rdparty/'))
.pipe(gulp.dest('setup/splash/website/3rdparty'));
gulp.src('node_modules/bootstrap-sass/assets/javascripts/bootstrap.min.js')
.pipe(gulp.dest('webadmin/dist/3rdparty/js'))
.pipe(gulp.dest('setup/splash/website/3rdparty/js'));
});
// --------------
// JavaScript
// --------------
if (argv.help || argv.h) {
console.log('Supported arguments for "gulp develop":');
console.log(' --client-id <clientId>');
console.log(' --client-secret <clientSecret>');
console.log(' --api-origin <cloudron api uri>');
process.exit(1);
}
gulp.task('js', ['js-index', 'js-setup', 'js-setupdns', 'js-update'], function () {});
var oauth = {
clientId: argv.clientId || 'cid-webadmin',
clientSecret: argv.clientSecret || 'unused',
apiOrigin: argv.apiOrigin || '',
apiOriginHostname: argv.apiOrigin ? url.parse(argv.apiOrigin).hostname : ''
};
console.log();
console.log('Using OAuth credentials:');
console.log(' ClientId: %s', oauth.clientId);
console.log(' ClientSecret: %s', oauth.clientSecret);
console.log(' Cloudron API: %s', oauth.apiOrigin || 'default');
console.log(' Cloudron Host: %s', oauth.apiOriginHostname);
console.log();
gulp.task('js-index', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src([
'webadmin/src/js/index.js',
'webadmin/src/js/client.js',
'webadmin/src/js/appstore.js',
'webadmin/src/js/main.js',
'webadmin/src/views/*.js'
])
.pipe(ejs({ oauth: oauth }, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('index.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist/js'));
});
gulp.task('js-setup', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['webadmin/src/js/setup.js', 'webadmin/src/js/client.js'])
.pipe(ejs({ oauth: oauth }, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('setup.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist/js'));
});
gulp.task('js-setupdns', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['webadmin/src/js/setupdns.js', 'webadmin/src/js/client.js'])
.pipe(ejs({ oauth: oauth }, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('setupdns.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist/js'));
});
gulp.task('js-update', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['webadmin/src/js/update.js'])
.pipe(sourcemaps.init())
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist/js'))
.pipe(gulp.dest('setup/splash/website/js'));
});
// --------------
// HTML
// --------------
gulp.task('html', ['html-views', 'html-update', 'html-templates'], function () {
return gulp.src('webadmin/src/*.html').pipe(ejs({ apiOriginHostname: oauth.apiOriginHostname }, { ext: '.html' })).pipe(gulp.dest('webadmin/dist'));
});
gulp.task('html-update', function () {
return gulp.src(['webadmin/src/update.html']).pipe(gulp.dest('setup/splash/website'));
});
gulp.task('html-views', function () {
return gulp.src('webadmin/src/views/**/*.html').pipe(gulp.dest('webadmin/dist/views'));
});
gulp.task('html-templates', function () {
return gulp.src('webadmin/src/templates/**/*.html').pipe(gulp.dest('webadmin/dist/templates'));
});
// --------------
// CSS
// --------------
gulp.task('css', function () {
return gulp.src('webadmin/src/*.scss')
.pipe(sourcemaps.init())
.pipe(sass({ includePaths: ['node_modules/bootstrap-sass/assets/stylesheets/'] }).on('error', sass.logError))
.pipe(autoprefixer())
.pipe(cssnano())
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist'))
.pipe(gulp.dest('setup/splash/website'));
});
gulp.task('images', function () {
return gulp.src('webadmin/src/img/**')
.pipe(gulp.dest('webadmin/dist/img'));
});
// --------------
// Utilities
// --------------
gulp.task('watch', ['default'], function () {
gulp.watch(['webadmin/src/*.scss'], ['css']);
gulp.watch(['webadmin/src/img/*'], ['images']);
gulp.watch(['webadmin/src/**/*.html'], ['html']);
gulp.watch(['webadmin/src/views/*.html'], ['html-views']);
gulp.watch(['webadmin/src/templates/*.html'], ['html-templates']);
gulp.watch(['webadmin/src/js/update.js'], ['js-update']);
gulp.watch(['webadmin/src/js/setup.js', 'webadmin/src/js/client.js'], ['js-setup']);
gulp.watch(['webadmin/src/js/setupdns.js', 'webadmin/src/js/client.js'], ['js-setupdns']);
gulp.watch(['webadmin/src/js/index.js', 'webadmin/src/js/client.js', 'webadmin/src/js/appstore.js', 'webadmin/src/js/main.js', 'webadmin/src/views/*.js'], ['js-index']);
gulp.watch(['webadmin/src/3rdparty/**/*'], ['3rdparty']);
});
gulp.task('clean', function () {
rimraf.sync('webadmin/dist');
rimraf.sync('setup/splash/website');
});
gulp.task('default', ['clean', 'html', 'js', '3rdparty', 'images', 'css'], function () {});
gulp.task('develop', ['watch'], serve({ root: 'webadmin/dist', port: 4000 }));
Executable
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env node
'use strict';
var tar = require('tar-fs'),
fs = require('fs'),
path = require('path'),
zlib = require('zlib');
if (process.argv.length < 4) {
console.error('Usage: tarjs <cwd> <dir>');
process.exit(1);
}
var dir = process.argv[3];
var cwd = process.argv[2];
console.error('Packing directory "'+ dir +'" from within "' + cwd + '" and stream to stdout');
process.chdir(cwd);
var stat = fs.statSync(dir);
if (!stat.isDirectory()) throw(dir + ' is not a directory');
var gzipStream = zlib.createGzip({});
tar.pack(path.resolve(dir), {
ignore: function (name) {
if (name === '.') return true;
return false;
}
}).pipe(gzipStream).pipe(process.stdout);
@@ -1,7 +1,7 @@
'use strict';
exports.up = function(db, callback) {
var cmd = "CREATE TABLE userGroups(" +
var cmd = "CREATE TABLE groups(" +
"id VARCHAR(128) NOT NULL UNIQUE," +
"name VARCHAR(128) NOT NULL UNIQUE," +
"PRIMARY KEY(id))";
@@ -13,7 +13,7 @@ exports.up = function(db, callback) {
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE userGroups', function (error) {
db.runSql('DROP TABLE groups', function (error) {
if (error) console.error(error);
callback(error);
});
@@ -4,7 +4,7 @@ exports.up = function(db, callback) {
var cmd = "CREATE TABLE IF NOT EXISTS groupMembers(" +
"groupId VARCHAR(128) NOT NULL," +
"userId VARCHAR(128) NOT NULL," +
"FOREIGN KEY(groupId) REFERENCES userGroups(id)," +
"FOREIGN KEY(groupId) REFERENCES groups(id)," +
"FOREIGN KEY(userId) REFERENCES users(id));";
db.runSql(cmd, function (error) {
@@ -2,12 +2,12 @@
var async = require('async');
var ADMIN_GROUP_ID = 'admin'; // see constants.js
var ADMIN_GROUP_ID = 'admin'; // see groups.js
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'INSERT INTO userGroups (id, name) VALUES (?, ?)', [ ADMIN_GROUP_ID, 'admin' ]),
db.runSql.bind(db, 'INSERT INTO groups (id, name) VALUES (?, ?)', [ ADMIN_GROUP_ID, 'admin' ]),
function migrateAdminFlag(done) {
db.all('SELECT * FROM users WHERE admin=1', function (error, results) {
if (error) return done(error);
@@ -10,7 +10,7 @@ exports.up = function(db, callback) {
function addGroupMailboxes(done) {
console.log('Importing group mailboxes');
db.all('SELECT id, name FROM userGroups', function (error, results) {
db.all('SELECT id, name FROM groups', function (error, results) {
if (error) return done(error);
async.eachSeries(results, function (g, next) {
@@ -1,31 +0,0 @@
'use strict';
// WARNING!!
// At this point the default db collation is utf8mb4_unicode_ci however we already have foreign key constraits
// already with tables on utf8_bin charset, so we cannot convert all tables here to utf8mb4 collation without
// a reimport from a sql dump, as foreign keys across different collations are not supported
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE appPortBindings CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE apps CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE authcodes CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE backups CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE clients CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE eventlog CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE groupMembers CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE userGroups CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE mailboxes CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE migrations CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE settings CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE tokens CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE users CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin')
], callback);
};
exports.down = function(db, callback) {
// nothing to be done here
callback();
};
@@ -1,70 +0,0 @@
'use strict';
var async = require('async'),
safe = require('safetydance');
exports.up = function(db, callback) {
// first check precondtion of domain entry in settings
db.all('SELECT * FROM settings WHERE name = ?', [ 'domain' ], function (error, result) {
if (error) return callback(error);
var domain = {};
if (result[0]) domain = safe.JSON.parse(result[0].value) || {};
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
function addAppsDomainColumn(done) {
db.runSql('ALTER TABLE apps ADD COLUMN domain VARCHAR(128)', [], done);
},
function setAppDomain(done) {
if (!domain.fqdn) return done(); // skip for new cloudrons without a domain
db.runSql('UPDATE apps SET domain = ?', [ domain.fqdn ], done);
},
function addAppsLocationDomainUniqueConstraint(done) {
db.runSql('ALTER TABLE apps ADD UNIQUE location_domain_unique_index (location, domain)', [], done);
},
function removePresetupAdminGroupIfNew(done) {
// do not delete on update, will update the record in setMailboxesDomain()
if (domain.fqdn) return done();
// this will be finally created once we have a domain when we create the owner in user.js
const ADMIN_GROUP_ID = 'admin'; // see constants.js
db.runSql('DELETE FROM userGroups WHERE id = ?', [ ADMIN_GROUP_ID ], function (error) {
if (error) return done(error);
db.runSql('DELETE FROM mailboxes WHERE ownerId = ?', [ ADMIN_GROUP_ID ], done);
});
},
function addMailboxesDomainColumn(done) {
db.runSql('ALTER TABLE mailboxes ADD COLUMN domain VARCHAR(128)', [], done);
},
function setMailboxesDomain(done) {
if (!domain.fqdn) return done(); // skip for new cloudrons without a domain
db.runSql('UPDATE mailboxes SET domain = ?', [ domain.fqdn ], done);
},
function dropAppsLocationUniqueConstraint(done) {
db.runSql('ALTER TABLE apps DROP INDEX location', [], done);
},
db.runSql.bind(db, 'COMMIT')
], callback);
});
};
exports.down = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
function dropMailboxesDomainColumn(done) {
db.runSql('ALTER TABLE mailboxes DROP COLUMN domain', [], done);
},
function dropLocationDomainUniqueConstraint(done) {
db.runSql('ALTER TABLE apps DROP INDEX location_domain_unique_index', [], done);
},
function dropAppsDomainColumn(done) {
db.runSql('ALTER TABLE apps DROP COLUMN domain', [], done);
},
function addAppsLocationUniqueConstraint(done) {
db.runSql('ALTER TABLE apps ADD UNIQUE location (location)', [], done);
},
db.runSql.bind(db, 'COMMIT')
], callback);
};
@@ -1,61 +0,0 @@
'use strict';
var async = require('async'),
safe = require('safetydance'),
tld = require('tldjs');
exports.up = function(db, callback) {
var fqdn, zoneName, configJson;
async.series([
function gatherDomain(done) {
db.all('SELECT * FROM settings WHERE name = ?', [ 'domain' ], function (error, result) {
if (error) return done(error);
var domain = {};
if (result[0]) domain = safe.JSON.parse(result[0].value) || {};
fqdn = domain.fqdn || ''; // will be null pre-setup
zoneName = domain.zoneName || tld.getDomain(fqdn) || fqdn;
done();
});
},
function gatherDNSConfig(done) {
db.all('SELECT * FROM settings WHERE name = ?', [ 'dns_config' ], function (error, result) {
if (error) return done(error);
configJson = (result[0] && result[0].value) ? result[0].value : JSON.stringify({ provider: 'manual'});
// caas dns config needs an fqdn
var config = JSON.parse(configJson);
if (config.provider === 'caas') config.fqdn = fqdn;
configJson = JSON.stringify(config);
done();
});
},
db.runSql.bind(db, 'START TRANSACTION;'),
function createDomainsTable(done) {
var cmd = `
CREATE TABLE domains(
domain VARCHAR(128) NOT NULL UNIQUE,
zoneName VARCHAR(128) NOT NULL,
configJson TEXT,
PRIMARY KEY (domain)) CHARACTER SET utf8 COLLATE utf8_bin
`;
db.runSql(cmd, [], done);
},
function addInitialDomain(done) {
if (!fqdn) return done();
db.runSql('INSERT INTO domains (domain, zoneName, configJson) VALUES (?, ?, ?)', [ fqdn, zoneName, configJson ], done);
},
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE domains', callback);
};
@@ -1,15 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD CONSTRAINT apps_domain_constraint FOREIGN KEY(domain) REFERENCES domains(domain)', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP FOREIGN KEY apps_domain_constraint', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,15 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE mailboxes ADD CONSTRAINT mailboxes_domain_constraint FOREIGN KEY(domain) REFERENCES domains(domain)', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE mailboxes DROP FOREIGN KEY mailboxes_domain_constraint', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,15 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE mailboxes DROP PRIMARY KEY', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE mailboxes ADD PRIMARY KEY(name)', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,15 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE mailboxes ADD UNIQUE mailboxes_name_domain_unique_index (name, domain)', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE mailboxes DROP INDEX mailboxes_name_domain_unique_index', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,15 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN updateTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN updateTime', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,15 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps CHANGE createdAt creationTime TIMESTAMP(2) NOT NULL', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps CHANGE creationTime createdAt TIMESTAMP(2) NOT NULL', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,28 +0,0 @@
'use strict';
var async = require('async');
// NOTE: This migration is incorrect because 'caas' domain is not guaranteed to be present in all Caas cloudrons
exports.up = function(db, callback) {
db.all('SELECT * FROM domains', function (error, domains) {
if (error) return callback(error);
var caasDomains = domains.filter(function (d) { return JSON.parse(d.configJson).provider === 'caas'; });
if (caasDomains.length === 0) return callback();
var caasDomain = caasDomains[0].domain;
db.all('SELECT * FROM settings WHERE name=?', [ 'backup_config' ], function (error, settings) {
if (error) return callback(error);
var setting = settings[0];
var config = JSON.parse(setting.value);
config.fqdn = caasDomain;
db.runSql('UPDATE settings SET value=? WHERE name=?', [ JSON.stringify(config), setting.name ], callback);
});
});
};
exports.down = function(db, callback) {
callback();
};
@@ -1,23 +0,0 @@
'use strict';
exports.up = function(db, callback) {
var backupConfig = {
"provider": "filesystem",
"backupFolder": "/var/backups",
"format": "tgz",
"retentionSecs": 172800
};
db.runSql('INSERT settings (name, value) VALUES(?, ?)', [ 'backup_config', JSON.stringify(backupConfig) ], function (error) {
if (!error || error.code === 'ER_DUP_ENTRY') return callback(); // dup entry is OK for existing cloudrons
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DELETE FROM settings WHERE name=?', ['backup_config'], function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,33 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
// first check precondtion of domain entry in settings
db.all('SELECT * FROM domains', [ ], function (error, domains) {
if (error) return callback(error);
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE domains ADD COLUMN provider VARCHAR(16) DEFAULT ""'),
function setProvider(done) {
async.eachSeries(domains, function (domain, iteratorCallback) {
var config = JSON.parse(domain.configJson);
var provider = config.provider;
delete config.provider;
db.runSql('UPDATE domains SET provider = ?, configJson = ? WHERE domain = ?', [ provider, JSON.stringify(config), domain.domain ], iteratorCallback);
}, done);
},
db.runSql.bind(db, 'ALTER TABLE domains MODIFY provider VARCHAR(16) NOT NULL'),
db.runSql.bind(db, 'COMMIT')
], callback);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE domains DROP COLUMN provider', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,24 +0,0 @@
'use strict';
exports.up = function(db, callback) {
var cmd = 'CREATE TABLE IF NOT EXISTS mail(' +
'domain VARCHAR(128) NOT NULL UNIQUE,' +
'enabled BOOLEAN DEFAULT 0,' +
'mailFromValidation BOOLEAN DEFAULT 1,' +
'catchAllJson TEXT,' +
'relayJson TEXT,' +
'FOREIGN KEY(domain) REFERENCES domains(domain),' +
'PRIMARY KEY(domain)) CHARACTER SET utf8 COLLATE utf8_bin';
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE mail', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,34 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.all('SELECT * FROM domains', function (error, domains) {
if (error) return callback(error);
if (domains.length === 0) return callback();
db.all('SELECT * FROM settings', function (error, allSettings) {
if (error) return callback(error);
// defaults
var mailFromValidation = true;
var catchAll = [ ];
var relay = { provider: 'cloudron-smtp' };
var mailEnabled = false;
allSettings.forEach(function (setting) {
switch (setting.name) {
case 'mail_from_validation': mailFromValidation = !!setting.value; break;
case 'catch_all_address': catchAll = JSON.parse(setting.value); break;
case 'mail_relay': relay = JSON.parse(setting.value); break;
case 'mail_config': mailEnabled = JSON.parse(setting.value).enabled; break;
}
});
db.runSql('INSERT INTO mail (domain, enabled, mailFromValidation, catchAllJson, relayJson) VALUES (?, ?, ?, ?, ?)',
[ domains[0].domain, mailEnabled, mailFromValidation, JSON.stringify(catchAll), JSON.stringify(relay) ], callback);
});
});
};
exports.down = function(db, callback) {
callback();
};
@@ -1,44 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.all('SELECT * FROM users', [ ], function (error, users) {
if (error) return callback(error);
db.all('SELECT * FROM mail WHERE enabled=1', [ ], function (error, mailDomains) {
if (error) return callback(error);
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE users DROP INDEX users_email'),
db.runSql.bind(db, 'ALTER TABLE users ADD COLUMN fallbackEmail VARCHAR(512) DEFAULT ""'),
function setDefaults(done) {
async.eachSeries(users, function (user, iteratorCallback) {
var defaultEmail = '';
var fallbackEmail = '';
if (mailDomains.length === 0) {
defaultEmail = user.email;
fallbackEmail = user.email;
} else {
defaultEmail = user.username ? (user.username + '@' + mailDomains[0].domain) : user.email;
fallbackEmail = user.email;
}
db.runSql('UPDATE users SET email = ?, fallbackEmail = ? WHERE id = ?', [ defaultEmail, fallbackEmail, user.id ], iteratorCallback);
}, done);
},
db.runSql.bind(db, 'ALTER TABLE users ADD UNIQUE users_email (email)'),
db.runSql.bind(db, 'COMMIT')
], callback);
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN fallbackEmail', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,26 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.all('SELECT * FROM settings WHERE name = ?', [ 'tls_config' ], function (error, result) {
if (error) return callback(error);
var tlsConfig = (result[0] && result[0].value) ? JSON.parse(result[0].value) : { provider: 'letsencrypt-prod'};
tlsConfig.provider = tlsConfig.provider.replace(/$le\-/, 'letsencrypt-'); // old cloudrons had le-prod/le-staging
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE domains ADD COLUMN tlsConfigJson TEXT'),
db.runSql.bind(db, 'UPDATE domains SET tlsConfigJson = ?', [ JSON.stringify(tlsConfig) ]),
db.runSql.bind(db, 'COMMIT')
], callback);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE domains DROP COLUMN tlsConfigJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,47 +0,0 @@
'use strict';
var async = require('async'),
fs = require('fs'),
superagent = require('superagent');
exports.up = function(db, callback) {
if (!fs.existsSync('/home/yellowtent/configs/cloudron.conf')) {
console.log('Unable to locate cloudron.conf');
return callback();
}
var config = JSON.parse(fs.readFileSync('/home/yellowtent/configs/cloudron.conf', 'utf8'));
if (config.provider !== 'caas' || !config.fqdn) {
console.log('Not caas (%s) or no fqdn', config.provider, config.fqdn);
return callback();
}
db.runSql('SELECT COUNT(*) AS total FROM users', function (error, result) {
if (error) return callback(error);
if (result[0].total === 0) {
console.log('This cloudron is not activated. It will automatically get appstore and caas configs from autoprovision logic');
return callback();
}
console.log('Downloading appstore and caas config');
superagent.get(config.apiServerOrigin + `/api/v1/boxes/${config.fqdn}/config`)
.query({ token: config.token })
.timeout(30 * 1000).end(function (error, result) {
if (error) return callback(error);
console.log('Adding %j config', result.body);
async.series([
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', [ 'appstore_config', JSON.stringify(result.body.appstoreConfig) ]),
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', [ 'caas_config', JSON.stringify(result.body.caasConfig) ])
], callback);
});
});
};
exports.down = function(db, callback) {
callback();
};
@@ -1,24 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.runSql('SELECT * FROM settings WHERE name=?', ['autoupdate_pattern'], function (error, results) {
if (error || results.length === 0) return callback(error); // will use defaults from box code
// migrate the 'daily' update pattern
var appUpdatePattern = results[0].value;
if (appUpdatePattern === '00 00 1,3,5,23 * * *') appUpdatePattern = '00 30 1,3,5,23 * * *';
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'DELETE FROM settings WHERE name=?', ['autoupdate_pattern']),
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', ['app_autoupdate_pattern', appUpdatePattern]),
db.runSql.bind(db, 'COMMIT')
], callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -1,121 +0,0 @@
'use strict';
var async = require('async'),
crypto = require('crypto'),
fs = require('fs'),
os = require('os'),
path = require('path'),
safe = require('safetydance'),
tldjs = require('tldjs');
exports.up = function(db, callback) {
db.all('SELECT * FROM apps', function (error, apps) {
if (error) return callback(error);
async.eachSeries(apps, function (app, callback) {
if (!app.altDomain) {
console.log('App %s does not use altDomain, skip', app.id);
return callback();
}
const domain = tldjs.getDomain(app.altDomain);
const subdomain = tldjs.getSubdomain(app.altDomain);
const mailboxName = (subdomain ? subdomain : JSON.parse(app.manifestJson).title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
console.log('App %s is on domain %s and subdomain %s with mailbox', app.id, domain, subdomain, mailboxName);
async.series([
// Add domain if not exists
function (callback) {
const query = 'INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson) VALUES (?, ?, ?, ?, ?)';
const args = [ domain, domain, 'manual', JSON.stringify({}), JSON.stringify({ provider: 'letsencrypt-prod' }) ];
db.runSql(query, args, function (error) {
if (error && error.code !== 'ER_DUP_ENTRY') return callback(error);
console.log('Added domain %s', domain);
// ensure we have a fallback cert for the newly added domain. This is the same as in reverseproxy.js
// WARNING this will only work on the cloudron itself not during local testing!
const certFilePath = `/home/yellowtent/boxdata/certs/${domain}.host.cert`;
const keyFilePath = `/home/yellowtent/boxdata/certs/${domain}.host.key`;
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) { // generate it
let opensslConf = safe.fs.readFileSync('/etc/ssl/openssl.cnf', 'utf8');
let opensslConfWithSan = `${opensslConf}\n[SAN]\nsubjectAltName=DNS:${domain}\n`;
let configFile = path.join(os.tmpdir(), 'openssl-' + crypto.randomBytes(4).readUInt32LE(0) + '.conf');
let certCommand = `openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=*.${domain} -extensions SAN -config ${configFile} -nodes`;
safe.fs.writeFileSync(configFile, opensslConfWithSan, 'utf8');
if (!safe.child_process.execSync(certCommand)) return callback(safe.error.message);
safe.fs.unlinkSync(configFile);
}
callback();
});
},
// Add domain to mail table if not exists
function (callback) {
const query = 'INSERT INTO mail (domain, enabled, mailFromValidation, catchAllJson, relayJson) VALUES (?, ?, ?, ?, ?)';
const args = [ domain, 0, 1, '[]', JSON.stringify({ provider: 'cloudron-smtp' }) ];
db.runSql(query, args, function (error) {
if (error && error.code !== 'ER_DUP_ENTRY') return callback(error);
console.log('Added domain %s to mail table', domain);
callback();
});
},
// Remove old mailbox record if any
function (callback) {
const query = 'DELETE FROM mailboxes WHERE ownerId=?';
const args = [ app.id ];
db.runSql(query, args, function (error) {
if (error) return callback(error);
console.log('Cleaned up mailbox record for app %s', app.id);
callback();
});
},
// Add new mailbox record
function (callback) {
const query = 'INSERT INTO mailboxes (name, domain, ownerId, ownerType) VALUES (?, ?, ?, ?)';
const args = [ mailboxName, domain, app.id, 'app' /* mailboxdb.TYPE_APP */ ];
db.runSql(query, args, function (error) {
if (error) return callback(error);
console.log('Added mailbox record for app %s', app.id);
callback();
});
},
// Update app record
function (callback) {
const query = 'UPDATE apps SET location=?, domain=?, altDomain=? WHERE id=?';
const args = [ subdomain, domain, '', app.id ];
db.runSql(query, args, function (error) {
if (error) return error;
console.log('Updated app %s with new domain', app.id);
callback();
});
}
], callback);
}, function (error) {
if (error) return callback(error);
// finally drop the altDomain db field
db.runSql('ALTER TABLE apps DROP COLUMN altDomain', [], callback);
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN altDomain VARCHAR(256)', [], callback);
};
@@ -1,19 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE mailboxes DROP FOREIGN KEY mailboxes_domain_constraint'),
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD CONSTRAINT mailboxes_domain_constraint FOREIGN KEY(domain) REFERENCES mail(domain)'),
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE mailboxes DROP FOREIGN KEY mailboxes_domain_constraint', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,51 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
var users = { }, groupMembers = { };
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD COLUMN membersJson TEXT'),
function getUsers(done) {
db.all('SELECT * from users', [ ], function (error, results) {
if (error) return done(error);
results.forEach(function (result) { users[result.id] = result; });
done();
});
},
function getGroups(done) {
db.all('SELECT id, name, GROUP_CONCAT(groupMembers.userId) AS userIds ' +
' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' +
' GROUP BY userGroups.id', [ ], function (error, results) {
if (error) return done(error);
results.forEach(function (result) {
var userIds = result.userIds ? result.userIds.split(',') : [];
var members = userIds.map(function (id) { return users[id].username; });
groupMembers[result.id] = members;
});
done();
});
},
function removeGroupIdAndSetMembers(done) {
async.eachSeries(Object.keys(groupMembers), function (gid, iteratorDone) {
console.log(`Migrating group id ${gid} to ${JSON.stringify(groupMembers[gid])}`);
db.runSql('UPDATE mailboxes SET membersJson = ?, ownerId = ? WHERE ownerId = ?', [ JSON.stringify(groupMembers[gid]), 'admin', gid ], iteratorDone);
}, done);
},
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE mailboxes DROP COLUMN membersJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,34 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD COLUMN type VARCHAR(16)'),
function addMailboxType(done) {
db.all('SELECT * from mailboxes', [ ], function (error, results) {
if (error) return done(error);
async.eachSeries(results, function (mailbox, iteratorCallback) {
let type = 'mailbox';
if (mailbox.aliasTarget) {
type = 'alias';
} else if (mailbox.membersJson) {
type = 'list';
}
db.runSql('UPDATE mailboxes SET type = ? WHERE name = ? AND domain = ?', [ type, mailbox.name, mailbox.domain ], iteratorCallback);
}, done);
});
},
db.runSql.bind(db, 'ALTER TABLE mailboxes MODIFY type VARCHAR(16) NOT NULL'),
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE mailboxes DROP COLUMN membersJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,15 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN twoFactorAuthenticationSecret VARCHAR(128) DEFAULT "", ADD COLUMN twoFactorAuthenticationEnabled BOOLEAN DEFAULT false', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP twoFactorAuthenticationSecret, DROP twoFactorAuthenticationEnabled', function (error) {
if (error) console.error(error);
callback(error);
});
};
-21
View File
@@ -1,21 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('UPDATE clients SET scope=? WHERE id=? OR id=? OR id=?', ['*', 'cid-webadmin', 'cid-sdk', 'cid-cli'], function (error) {
if (error) console.error(error);
db.runSql('UPDATE tokens SET scope=? WHERE scope LIKE ?', ['*', '%*%'], function (error) { // remove the roleSdk
if (error) console.error(error);
db.runSql('UPDATE tokens SET expires=? WHERE clientId=?', [ 1525636734905, 'cid-webadmin' ], function (error) { // force webadmin to get a new token
if (error) console.error(error);
callback(error);
});
});
});
};
exports.down = function(db, callback) {
callback();
};
@@ -1,28 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE apps ADD COLUMN ownerId VARCHAR(128)'),
function (next) {
db.all('SELECT id FROM users ORDER BY createdAt LIMIT 1', [ ], function (error, results) {
if (error || results.length === 0) return next(error);
var ownerId = results[0].id;
db.runSql('UPDATE apps SET ownerId=?', [ ownerId ], next);
});
},
db.runSql.bind(db, 'ALTER TABLE apps MODIFY ownerId VARCHAR(128) NOT NULL'),
db.runSql.bind(db, 'ALTER TABLE apps ADD CONSTRAINT apps_owner_constraint FOREIGN KEY(ownerId) REFERENCES users(id)'),
db.runSql.bind(db, 'COMMIT'),
], callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN ownerId', function (error) {
if (error) console.error(error);
callback(error);
});
};
-15
View File
@@ -1,15 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN ts TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN ts ', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,25 +0,0 @@
'use strict';
exports.up = function(db, callback) {
var cmd = 'CREATE TABLE IF NOT EXISTS subdomains(' +
'appId VARCHAR(128) NOT NULL,' +
'domain VARCHAR(128) NOT NULL,' +
'subdomain VARCHAR(128) NOT NULL,' +
'type VARCHAR(128) NOT NULL,' +
'dnsRecordId VARCHAR(512),' +
'FOREIGN KEY(domain) REFERENCES domains(domain),' +
'FOREIGN KEY(appId) REFERENCES apps(id),' +
'UNIQUE (subdomain, domain)) CHARACTER SET utf8 COLLATE utf8_bin';
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE subdomains', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,28 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.all('SELECT * from apps', [ ], function (error, results) {
if (error) return callback(error);
var queries = [
db.runSql.bind(db, 'START TRANSACTION;')
];
results.forEach(function (app) {
queries.push(db.runSql.bind(db, 'INSERT INTO subdomains (appId, domain, subdomain, type, dnsRecordId) VALUES (?, ?, ?, ?, ?)', [ app.id, app.domain, app.location, 'primary', app.dnsRecordId ]));
});
queries.push(db.runSql.bind(db, 'COMMIT'));
async.series(queries, callback);
});
};
exports.down = function(db, callback) {
db.runSql('DELETE FROM subdomains', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,41 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps DROP INDEX location_domain_unique_index, DROP FOREIGN KEY apps_domain_constraint, DROP COLUMN domain, DROP COLUMN location, DROP COLUMN dnsRecordId', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.all('SELECT * from subdomains WHERE type = ?', [ 'primary' ], function (error, results) {
if (error) return callback(error);
var cmd = 'ALTER TABLE apps'
+ ' ADD COLUMN location VARCHAR(128),'
+ ' ADD COLUMN domain VARCHAR(128),'
+ ' ADD COLUMN dnsRecordId VARCHAR(512)';
db.runSql(cmd, function (error) {
if (error) return callback(error);
var queries = [ db.runSql.bind(db, 'START TRANSACTION;') ];
results.forEach(function (d) {
queries.push(db.runSql.bind(db, 'UPDATE apps SET domain = ?, location = ?, dnsRecordId = ? WHERE id = ?', [ d.domain, d.subdomain, d.appId, d.dnsRecordId ]));
});
queries.push(db.runSql.bind(db, 'COMMIT'));
async.series(queries, function (error) {
if (error) return callback(error);
var cmd = 'ALTER TABLE apps'
+ ' ADD CONSTRAINT apps_domain_constraint FOREIGN KEY(domain) REFERENCES domains(domain),'
+ ' ADD UNIQUE location_domain_unique_index (location, domain)';
db.runSql(cmd, callback);
});
});
});
};
@@ -1,15 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE subdomains DROP COLUMN dnsRecordId', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE subdomains ADD COLUMN dnsRecordId VARCHAR(512)', function (error) {
if (error) return callback(error);
callback();
});
};
@@ -1,34 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN admin BOOLEAN DEFAULT 0', function (error) {
if (error) return callback(error);
db.all('SELECT userId FROM groupMembers WHERE groupId=?', [ 'admin' ], function (error, results) {
if (error) return callback(error);
if (results.length === 0) return callback();
async.eachSeries(results, function (result, iteratorDone) {
db.runSql('UPDATE users SET admin=1 WHERE id=?', [ result.userId ], iteratorDone);
}, function (error) {
if (error) return callback(error);
async.series([
db.runSql.bind(db, 'DELETE FROM groupMembers WHERE groupId=?', [ 'admin' ]),
db.runSql.bind(db, 'DELETE FROM userGroups WHERE id=?', [ 'admin' ])
], callback);
});
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN admin', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,13 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('UPDATE tokens SET expires=? WHERE clientId=?', [ 1525636734905, 'cid-webadmin' ], function (error) { // force webadmin to get a new token
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -1,18 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE appPortBindings ADD COLUMN type VARCHAR(8) NOT NULL DEFAULT "tcp"'),
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP INDEX hostPort'), // this drops the unique constraint
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP PRIMARY KEY, ADD PRIMARY KEY(hostPort, type)')
], callback);
};
exports.down = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP PRIMARY KEY, ADD PRIMARY KEY(hostPort)'),
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP COLUMN type')
], callback);
};
@@ -1,16 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
if (error || results.length === 0) return callback(error);
var backupConfig = JSON.parse(results[0].value);
backupConfig.intervalSecs = 24 * 60 * 60;
db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [ JSON.stringify(backupConfig) ], callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -1,23 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
// first check precondtion of domain entry in settings
db.all('SELECT * FROM domains', [ ], function (error, domains) {
if (error) return callback(error);
let caasDomains = domains.filter(function (d) { return d.provider === 'caas'; });
async.eachSeries(caasDomains, function (domain, iteratorCallback) {
let config = JSON.parse(domain.configJson);
config.hyphenatedSubdomains = true;
db.runSql('UPDATE domains SET configJson = ? WHERE domain = ?', [ JSON.stringify(config), domain.domain ], iteratorCallback);
}, callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -1,12 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE tokens ADD COLUMN name VARCHAR(64) DEFAULT ""', [], callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE tokens DROP COLUMN name', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,21 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.all('SELECT * from domains WHERE provider=?', [ 'manual' ], function (error, results) {
if (error) return callback(error);
async.eachSeries(results, function (result, iteratorDone) {
var config = JSON.parse(result.configJson || '{}');
if (!config.wildcard) return iteratorDone();
delete config.wildcard;
db.runSql('UPDATE domains SET provider=?, configJson=? WHERE domain=?', [ 'wildcard', JSON.stringify(config), result.domain ], iteratorDone);
}, callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -1,21 +0,0 @@
'use strict';
exports.up = function(db, callback) {
var cmd = 'CREATE TABLE IF NOT EXISTS appEnvVars(' +
'appId VARCHAR(128) NOT NULL,' +
'name TEXT NOT NULL,' +
'value TEXT NOT NULL,' +
'FOREIGN KEY(appId) REFERENCES apps(id)) CHARACTER SET utf8 COLLATE utf8_bin';
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE appEnvVars', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,27 +0,0 @@
'use strict';
exports.up = function(db, callback) {
var cmd = 'CREATE TABLE tasks(' +
'id int NOT NULL AUTO_INCREMENT,' +
'type VARCHAR(32) NOT NULL,' +
'argsJson TEXT,' +
'percent INTEGER DEFAULT 0,' +
'message TEXT,' +
'errorMessage TEXT,' +
'result TEXT,' +
'creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,' +
'ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,' +
'PRIMARY KEY (id))';
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE tasks', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,17 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('SELECT 1 FROM groups LIMIT 1', function (error) {
if (error) return callback(); // groups table does not exist
db.runSql('RENAME TABLE groups TO userGroups', function (error) {
if (error) console.error(error);
callback(error);
});
});
};
exports.down = function(db, callback) {
// this is a one way renaming since the previous migration steps have been already updated to match the new name
callback();
};
@@ -1,17 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE apps MODIFY creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'),
db.runSql.bind(db, 'ALTER TABLE apps MODIFY updateTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'),
db.runSql.bind(db, 'ALTER TABLE eventlog MODIFY creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'),
db.runSql.bind(db, 'ALTER TABLE backups MODIFY creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'),
db.runSql.bind(db, 'ALTER TABLE mailboxes MODIFY creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'),
], callback);
};
exports.down = function(db, callback) {
callback();
};
@@ -1,28 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE apps ADD COLUMN mailboxName VARCHAR(128)'),
db.runSql.bind(db, 'START TRANSACTION;'),
function migrateMailboxNames(done) {
db.all('SELECT * FROM mailboxes', function (error, mailboxes) {
if (error) return done(error);
async.eachSeries(mailboxes, function (mailbox, iteratorDone) {
if (mailbox.ownerType !== 'app') return iteratorDone();
db.runSql('UPDATE apps SET mailboxName = ? WHERE id = ?', [ mailbox.name, mailbox.ownerId ], iteratorDone);
}, done);
});
},
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
callback();
};
@@ -1,28 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
function migrateMailboxNames(done) {
db.all('SELECT * FROM mailboxes', function (error, mailboxes) {
if (error) return done(error);
async.eachSeries(mailboxes, function (mailbox, iteratorDone) {
if (mailbox.ownerType !== 'app') return iteratorDone();
db.runSql('DELETE FROM mailboxes WHERE name = ?', [ mailbox.name ], iteratorDone);
}, done);
});
},
db.runSql.bind(db, 'COMMIT'),
db.runSql.bind(db, 'ALTER TABLE mailboxes DROP COLUMN ownerType')
], callback);
};
exports.down = function(db, callback) {
callback();
};
@@ -1,16 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN enableAutomaticUpdate BOOLEAN DEFAULT 1', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN enableAutomaticUpdate', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,27 +0,0 @@
'use strict';
exports.up = function(db, callback) {
var cmd = 'CREATE TABLE notifications(' +
'id int NOT NULL AUTO_INCREMENT,' +
'userId VARCHAR(128) NOT NULL,' +
'eventId VARCHAR(128) NOT NULL,' +
'title VARCHAR(512) NOT NULL,' +
'message TEXT,' +
'action VARCHAR(512) NOT NULL,' +
'acknowledged BOOLEAN DEFAULT false,' +
'creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,' +
'FOREIGN KEY(eventId) REFERENCES eventlog(id),' +
'PRIMARY KEY (id)) CHARACTER SET utf8 COLLATE utf8_bin';
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE notifications', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,16 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE tasks CHANGE result resultJson TEXT', [], function (error) {
if (error) console.error(error);
db.runSql('DELETE FROM tasks', callback); // empty tasks table since we have bad results format
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE tasks CHANGE resultJson result TEXT', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,15 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN dataDir VARCHAR(256) UNIQUE', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN dataDir', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,17 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE domains ADD COLUMN locked BOOLEAN DEFAULT 0', function (error) {
if (error) return callback(error);
callback();
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE domains DROP COLUMN locked', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,22 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
// WARNING in the future always give constraints proper names to not rely on automatic ones
db.runSql.bind(db, 'ALTER TABLE notifications DROP FOREIGN KEY notifications_ibfk_1'),
db.runSql.bind(db, 'ALTER TABLE notifications MODIFY eventId VARCHAR(128)'),
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE notifications MODIFY eventId VARCHAR(128) NOT NULL'),
db.runSql.bind(db, 'ALTER TABLE notifications ADD FOREIGN KEY(eventId) REFERENCES eventlog(id)'),
db.runSql.bind(db, 'COMMIT')
], callback);
};
@@ -1,23 +0,0 @@
'use strict';
let async = require('async');
exports.up = function(db, callback) {
db.runSql('SELECT * FROM domains', function (error, domains) {
if (error) return callback(error);
async.eachSeries(domains, function (domain, iteratorCallback) {
if (domain.provider !== 'namecheap') return iteratorCallback();
let config = JSON.parse(domain.configJson);
config.token = config.apiKey;
delete config.apiKey;
db.runSql('UPDATE domains SET configJson = ? WHERE domain = ?', [ JSON.stringify(config), domain.domain ], iteratorCallback);
}, callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -1,17 +0,0 @@
'use strict';
exports.up = function(db, callback) {
var cmd = 'ALTER TABLE apps ADD COLUMN healthTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP';
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN healthTime', function (error) {
if (error) console.error(error);
callback(error);
});
};
+18 -108
View File
@@ -9,10 +9,6 @@
#### BLOB - stored offline from table row (use for binary data)
#### https://dev.mysql.com/doc/refman/5.0/en/storage-requirements.html
# The code uses zero dates. Make sure sql_mode does NOT have NO_ZERO_DATE
# http://johnemb.blogspot.com/2014/09/adding-or-removing-individual-sql-modes.html
# SET GLOBAL sql_mode=(SELECT REPLACE(@@sql_mode,'NO_ZERO_DATE',''));
CREATE TABLE IF NOT EXISTS users(
id VARCHAR(128) NOT NULL UNIQUE,
username VARCHAR(254) UNIQUE,
@@ -21,15 +17,11 @@ CREATE TABLE IF NOT EXISTS users(
salt VARCHAR(512) NOT NULL,
createdAt VARCHAR(512) NOT NULL,
modifiedAt VARCHAR(512) NOT NULL,
displayName VARCHAR(512) DEFAULT "",
fallbackEmail VARCHAR(512) DEFAULT "",
twoFactorAuthenticationSecret VARCHAR(128) DEFAULT "",
twoFactorAuthenticationEnabled BOOLEAN DEFAULT false,
admin BOOLEAN DEFAULT false,
admin INTEGER NOT NULL,
displayName VARCHAR(512) DEFAULT '',
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS userGroups(
CREATE TABLE IF NOT EXISTS groups(
id VARCHAR(128) NOT NULL UNIQUE,
name VARCHAR(254) NOT NULL UNIQUE,
PRIMARY KEY(id));
@@ -37,11 +29,10 @@ CREATE TABLE IF NOT EXISTS userGroups(
CREATE TABLE IF NOT EXISTS groupMembers(
groupId VARCHAR(128) NOT NULL,
userId VARCHAR(128) NOT NULL,
FOREIGN KEY(groupId) REFERENCES userGroups(id),
FOREIGN KEY(groupId) REFERENCES groups(id),
FOREIGN KEY(userId) REFERENCES users(id));
CREATE TABLE IF NOT EXISTS tokens(
name VARCHAR(64) DEFAULT "", // description
accessToken VARCHAR(128) NOT NULL UNIQUE,
identifier VARCHAR(128) NOT NULL,
clientId VARCHAR(128),
@@ -51,7 +42,7 @@ CREATE TABLE IF NOT EXISTS tokens(
CREATE TABLE IF NOT EXISTS clients(
id VARCHAR(128) NOT NULL UNIQUE, // prefixed with cid- to identify token easily in auth routes
appId VARCHAR(128) NOT NULL, // name of the client (for external apps) or id of app (for built-in apps)
appId VARCHAR(128) NOT NULL,
type VARCHAR(16) NOT NULL,
clientSecret VARCHAR(512) NOT NULL,
redirectURI VARCHAR(512) NOT NULL,
@@ -65,38 +56,30 @@ CREATE TABLE IF NOT EXISTS apps(
installationProgress TEXT,
runState VARCHAR(512),
health VARCHAR(128),
healthTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app last responded
containerId VARCHAR(128),
manifestJson TEXT,
httpPort INTEGER, // this is the nginx proxy port and not manifest.httpPort
location VARCHAR(128) NOT NULL,
domain VARCHAR(128) NOT NULL,
location VARCHAR(128) NOT NULL UNIQUE,
dnsRecordId VARCHAR(512), // tracks any id that we got back to track dns updates
accessRestrictionJson TEXT, // { users: [ ], groups: [ ] }
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app was installed
updateTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the last app update was done
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, // when this db record was updated (useful for UI caching)
createdAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
memoryLimit BIGINT DEFAULT 0,
altDomain VARCHAR(256),
xFrameOptions VARCHAR(512),
sso BOOLEAN DEFAULT 1, // whether user chose to enable SSO
debugModeJson TEXT, // options for development mode
robotsTxt TEXT,
enableBackup BOOLEAN DEFAULT 1, // misnomer: controls automatic daily backups
enableAutomaticUpdate BOOLEAN DEFAULT 1,
mailboxName VARCHAR(128), // mailbox of this app
enableBackup BOOLEAN DEFAULT 1,
// the following fields do not belong here, they can be removed when we use a queue for apptask
restoreConfigJson VARCHAR(256), // used to pass backupId to restore from to apptask
oldConfigJson TEXT, // used to pass old config to apptask (configure, restore)
updateConfigJson TEXT, // used to pass new config to apptask (update)
oldConfigJson TEXT, // used to pass old config for apptask (configure, restore)
updateConfigJson TEXT, // used to pass new config for apptask (update)
ownerId VARCHAR(128),
FOREIGN KEY(ownerId) REFERENCES users(id),
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS appPortBindings(
hostPort INTEGER NOT NULL UNIQUE,
type VARCHAR(8) NOT NULL DEFAULT "tcp",
environmentVariable VARCHAR(128) NOT NULL,
appId VARCHAR(128) NOT NULL,
FOREIGN KEY(appId) REFERENCES apps(id),
@@ -121,15 +104,9 @@ CREATE TABLE IF NOT EXISTS appAddonConfigs(
value VARCHAR(512) NOT NULL,
FOREIGN KEY(appId) REFERENCES apps(id));
CREATE TABLE IF NOT EXISTS appEnvVars(
appId VARCHAR(128) NOT NULL,
name TEXT NOT NULL,
value TEXT NOT NULL,
FOREIGN KEY(appId) REFERENCES apps(id));
CREATE TABLE IF NOT EXISTS backups(
id VARCHAR(128) NOT NULL,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
creationTime TIMESTAMP,
version VARCHAR(128) NOT NULL, /* app version or box version */
type VARCHAR(16) NOT NULL, /* 'box' or 'app' */
dependsOn TEXT, /* comma separate list of objects this backup depends on */
@@ -144,86 +121,19 @@ CREATE TABLE IF NOT EXISTS eventlog(
action VARCHAR(128) NOT NULL,
source TEXT, /* { userId, username, ip }. userId can be null for cron,sysadmin */
data TEXT, /* free flowing json based on action */
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
creationTime TIMESTAMP, /* FIXME: precision must be TIMESTAMP(2) */
PRIMARY KEY (id));
CREATE TABLE IF NOT EXISTS domains(
domain VARCHAR(128) NOT NULL UNIQUE, /* if this needs to be larger, InnoDB has a limit of 767 bytes for PRIMARY KEY values! */
zoneName VARCHAR(128) NOT NULL, /* this mostly contains the domain itself again */
provider VARCHAR(16) NOT NULL,
configJson TEXT, /* JSON containing the dns backend provider config */
tlsConfigJson TEXT, /* JSON containing the tls provider config */
PRIMARY KEY (domain))
/* the default db collation is utf8mb4_unicode_ci but for the app table domain constraint we have to use the old one */
CHARACTER SET utf8 COLLATE utf8_bin;
CREATE TABLE IF NOT EXISTS mail(
domain VARCHAR(128) NOT NULL UNIQUE,
enabled BOOLEAN DEFAULT 0, /* MDA enabled */
mailFromValidation BOOLEAN DEFAULT 1,
catchAllJson TEXT,
relayJson TEXT,
FOREIGN KEY(domain) REFERENCES domains(domain),
PRIMARY KEY(domain))
CHARACTER SET utf8 COLLATE utf8_bin;
/* Future fields:
* accessRestriction - to determine who can access it. So this has foreign keys
* quota - per mailbox quota
NOTE: this table exists only real mailboxes. And has unique constraint to handle
conflict with aliases and mailbox names
*/
CREATE TABLE IF NOT EXISTS mailboxes(
name VARCHAR(128) NOT NULL,
type VARCHAR(16) NOT NULL, /* 'mailbox', 'alias', 'list' */
ownerId VARCHAR(128) NOT NULL, /* user id */
ownerId VARCHAR(128) NOT NULL, /* app id or user id or group id */
ownerType VARCHAR(16) NOT NULL, /* 'app' or 'user' or 'group' */
aliasTarget VARCHAR(128), /* the target name type is an alias */
membersJson TEXT, /* members of a group */
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
domain VARCHAR(128),
creationTime TIMESTAMP,
FOREIGN KEY(domain) REFERENCES mail(domain),
UNIQUE (name, domain));
CREATE TABLE IF NOT EXISTS subdomains(
appId VARCHAR(128) NOT NULL,
domain VARCHAR(128) NOT NULL,
subdomain VARCHAR(128) NOT NULL,
type VARCHAR(128) NOT NULL,
FOREIGN KEY(domain) REFERENCES domains(domain),
FOREIGN KEY(appId) REFERENCES apps(id),
UNIQUE (subdomain, domain));
CREATE TABLE IF NOT EXISTS tasks(
id int NOT NULL AUTO_INCREMENT,
type VARCHAR(32) NOT NULL,
percent INTEGER DEFAULT 0,
message TEXT,
errorMessage TEXT,
result TEXT,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id));
CREATE TABLE IF NOT EXISTS notifications(
id int NOT NULL AUTO_INCREMENT,
userId VARCHAR(128) NOT NULL,
eventId VARCHAR(128),
title VARCHAR(512) NOT NULL,
message TEXT,
action VARCHAR(512) NOT NULL,
acknowledged BOOLEAN DEFAULT false,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
CHARACTER SET utf8 COLLATE utf8_bin;
PRIMARY KEY (name));
+5447
View File
File diff suppressed because it is too large Load Diff
-9419
View File
File diff suppressed because it is too large Load Diff
+53 -42
View File
@@ -14,39 +14,39 @@
"node": ">=4.0.0 <=4.1.1"
},
"dependencies": {
"@google-cloud/dns": "^0.7.2",
"@google-cloud/storage": "^1.7.0",
"@google-cloud/dns": "^0.7.0",
"@sindresorhus/df": "^2.1.0",
"async": "^2.6.1",
"aws-sdk": "^2.253.1",
"body-parser": "^1.18.3",
"cloudron-manifestformat": "^2.14.2",
"connect": "^3.6.6",
"async": "^2.6.0",
"aws-sdk": "^2.151.0",
"body-parser": "^1.18.2",
"cloudron-manifestformat": "^2.10.0",
"connect-ensure-login": "^0.1.1",
"connect-lastmile": "^1.0.2",
"connect-timeout": "^1.9.0",
"cookie-parser": "^1.3.5",
"cookie-session": "^1.3.2",
"cron": "^1.5.1",
"cron": "^1.3.0",
"csurf": "^1.6.6",
"db-migrate": "^0.11.1",
"db-migrate": "^0.10.0-beta.24",
"db-migrate-mysql": "^1.1.10",
"debug": "^3.1.0",
"dockerode": "^2.5.5",
"ejs": "^2.6.1",
"ejs-cli": "^2.0.1",
"express": "^4.16.3",
"dockerode": "^2.5.3",
"ejs": "^2.5.7",
"ejs-cli": "^2.0.0",
"express": "^4.16.2",
"express-session": "^1.15.6",
"gulp-sass": "^3.0.0",
"hat": "0.0.3",
"hock": "https://registry.npmjs.org/hock/-/hock-1.3.2.tgz",
"json": "^9.0.3",
"ldapjs": "^1.0.2",
"ldapjs": "^1.0.0",
"lodash.chunk": "^4.2.0",
"mime": "^2.3.1",
"moment-timezone": "^0.5.17",
"mime": "^2.0.3",
"moment-timezone": "^0.5.14",
"morgan": "^1.9.0",
"multiparty": "^4.1.4",
"multiparty": "^4.1.2",
"mysql": "^2.15.0",
"namecheap": "github:joshuakarjala/node-namecheap#464a952",
"nodemailer": "^4.6.5",
"nodemailer": "^4.4.0",
"nodemailer-smtp-transport": "^2.7.4",
"oauth2orize": "^1.11.0",
"once": "^1.3.2",
@@ -56,46 +56,57 @@
"passport-http-bearer": "^1.0.1",
"passport-local": "^1.0.0",
"passport-oauth2-client-password": "^0.1.2",
"password-generator": "^2.2.0",
"progress-stream": "^2.0.0",
"proxy-middleware": "^0.15.0",
"qrcode": "^1.2.0",
"readdirp": "^2.1.0",
"request": "^2.87.0",
"rimraf": "^2.6.2",
"s3-block-read-stream": "^0.5.0",
"request": "^2.83.0",
"s3-block-read-stream": "^0.2.0",
"safetydance": "^0.7.1",
"semver": "^5.5.0",
"showdown": "^1.8.6",
"speakeasy": "^2.0.0",
"semver": "^5.4.1",
"showdown": "^1.8.2",
"split": "^1.0.0",
"superagent": "^3.8.3",
"supererror": "^0.7.2",
"tar-fs": "^1.16.2",
"tar-stream": "^1.6.1",
"tldjs": "^2.3.1",
"underscore": "^1.9.1",
"uuid": "^3.2.1",
"superagent": "^3.8.1",
"supererror": "^0.7.1",
"tar-fs": "^1.16.0",
"tar-stream": "^1.5.5",
"tldjs": "^2.2.0",
"underscore": "^1.7.0",
"uuid": "^3.1.0",
"valid-url": "^1.0.9",
"validator": "^10.3.0",
"ws": "^5.2.0"
"validator": "^9.1.1",
"ws": "^3.3.1"
},
"devDependencies": {
"bootstrap-sass": "^3.3.3",
"expect.js": "*",
"hock": "^1.3.2",
"gulp": "^3.9.1",
"gulp-autoprefixer": "^4.0.0",
"gulp-concat": "^2.4.3",
"gulp-cssnano": "^2.1.0",
"gulp-ejs": "^3.1.0",
"gulp-sass": "^3.0.0",
"gulp-serve": "^1.0.0",
"gulp-sourcemaps": "^2.6.1",
"gulp-uglify": "^3.0.0",
"hock": "~1.2.0",
"istanbul": "*",
"js2xmlparser": "^3.0.0",
"mocha": "^5.2.0",
"mocha": "*",
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
"nock": "^9.0.14",
"node-sass": "^4.6.1",
"recursive-readdir": "^2.2.2",
"sinon": "^7.2.2"
"readdirp": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz",
"request": "^2.65.0",
"yargs": "^10.0.3"
},
"scripts": {
"test": "src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- --no-timeouts --exit -R spec ./src/test ./src/routes/test/[^a]*js",
"migrate_local": "DATABASE_URL=mysql://root:@localhost/box node_modules/.bin/db-migrate up",
"migrate_test": "BOX_ENV=test DATABASE_URL=mysql://root:@localhost/boxtest node_modules/.bin/db-migrate up",
"test": "npm run migrate_test && src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- -R spec ./src/test ./src/routes/test/[^a]*",
"test_all": "npm run migrate_test && src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- -R spec ./src/test ./src/routes/test",
"postmerge": "/bin/true",
"precommit": "/bin/true",
"prepush": "npm test",
"dashboard": "node_modules/.bin/gulp"
"webadmin": "node_modules/.bin/gulp"
}
}
-106
View File
@@ -1,106 +0,0 @@
#!/bin/bash
set -eu -o pipefail
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400 --http1.1"
ip=""
dns_config=""
tls_cert_file=""
tls_key_file=""
license_file=""
backup_config=""
args=$(getopt -o "" -l "ip:,backup-config:,license:,dns-config:,tls-cert:,tls-key:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--ip) ip="$2"; shift 2;;
--dns-config) dns_config="$2"; shift 2;;
--tls-cert) tls_cert_file="$2"; shift 2;;
--tls-key) tls_key_file="$2"; shift 2;;
--license) license_file="$2"; shift 2;;
--backup-config) backup_config="$2"; shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
# validate arguments in the absence of data
if [[ -z "${ip}" ]]; then
echo "--ip is required"
exit 1
fi
if [[ -z "${dns_config}" ]]; then
echo "--dns-config is required"
exit 1
fi
if [[ ! -f "${license_file}" ]]; then
echo "--license must be a valid license file"
exit 1
fi
function get_status() {
key="$1"
if status=$($curl -q -f -k "https://${ip}/api/v1/cloudron/status" 2>/dev/null); then
currentValue=$(echo "${status}" | python3 -c 'import sys, json; print(json.dumps(json.load(sys.stdin)[sys.argv[1]]))' "${key}")
echo "${currentValue}"
return 0
fi
return 1
}
function wait_for_status() {
key="$1"
expectedValue="$2"
echo "wait_for_status: $key to be $expectedValue"
while true; do
if currentValue=$(get_status "${key}"); then
echo "wait_for_status: $key is current: $currentValue expecting: $expectedValue"
if [[ "${currentValue}" == $expectedValue ]]; then
break
fi
fi
sleep 3
done
}
echo "=> Waiting for cloudron to be ready"
wait_for_status "version" '*'
domain=$(echo "${dns_config}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["domain"])')
echo "Provisioning Cloudron ${domain}"
if [[ -n "${tls_cert_file}" && -n "${tls_key_file}" ]]; then
tls_cert=$(cat "${tls_cert_file}" | awk '{printf "%s\\n", $0}')
tls_key=$(cat "${tls_key_file}" | awk '{printf "%s\\n", $0}')
fallback_cert=$(printf '{ "cert": "%s", "key": "%s", "provider": "fallback", "restricted": true }' "${tls_cert}" "${tls_key}")
else
fallback_cert=None
fi
tls_config='{ "provider": "fallback" }'
dns_config=$(echo "${dns_config}" | python3 -c "import json,sys;obj=json.load(sys.stdin);obj.update(tlsConfig=${tls_config});obj.update(fallbackCertficate=${fallback_cert});print(json.dumps(obj))")
license=$(cat "${license_file}")
if [[ -z "${backup_config:-}" ]]; then
backup_config='{ "provider": "filesystem", "backupFolder": "/var/backups", "format": "tgz" }'
fi
setupData=$(printf '{ "dnsConfig": %s, "autoconf": { "appstoreConfig": %s, "backupConfig": %s } }' "${dns_config}" "${license}" "${backup_config}")
if ! setupResult=$($curl -kq -X POST -H "Content-Type: application/json" -d "${setupData}" https://${ip}/api/v1/cloudron/setup); then
echo "Failed to setup with ${setupData} ${setupResult}"
exit 1
fi
wait_for_status "webadminStatus" '*"tls": true*'
echo "Cloudron is ready at https://my-${domain}"
+164 -133
View File
@@ -2,8 +2,19 @@
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
if [[ $(lsb_release -rs) != "16.04" ]]; then
echo "Cloudron requires Ubuntu 16.04" > /dev/stderr
exit 1
fi
# change this to a hash when we make a upgrade release
readonly LOG_FILE="/var/log/cloudron-setup.log"
readonly DATA_FILE="/root/cloudron-install-data.json"
readonly MINIMUM_DISK_SIZE_GB="18" # this is the size of "/" and required to fit in docker images 18 is a safe bet for different reporting on 20GB min
readonly MINIMUM_MEMORY="974" # this is mostly reported for 1GB main memory (DO 992, EC2 990, Linode 989, Serverdiscounter.com 974)
@@ -15,10 +26,6 @@ readonly physical_memory=$(LC_ALL=C free -m | awk '/Mem:/ { print $2 }')
readonly disk_size_bytes=$(LC_ALL=C df --output=size / | tail -n1)
readonly disk_size_gb=$((${disk_size_bytes}/1024/1024))
readonly RED='\033[31m'
readonly GREEN='\033[32m'
readonly DONE='\033[m'
# verify the system has minimum requirements met
if [[ "${rootfs_type}" != "ext4" ]]; then
echo "Error: Cloudron requires '/' to be ext4" # see #364
@@ -35,98 +42,110 @@ if [[ "${disk_size_gb}" -lt "${MINIMUM_DISK_SIZE_GB}" ]]; then
exit 1
fi
if systemctl -q is-active box; then
echo "Error: Cloudron is already installed. To reinstall, start afresh"
exit 1
fi
initBaseImage="true"
# provisioning data
domain=""
adminLocation="my"
zoneName=""
provider=""
edition=""
encryptionKey=""
restoreUrl=""
dnsProvider="manual"
tlsProvider="le-prod"
requestedVersion=""
apiServerOrigin="https://api.cloudron.io"
webServerOrigin="https://cloudron.io"
dataJson=""
prerelease="false"
sourceTarballUrl=""
rebootServer="true"
baseDataDir=""
args=$(getopt -o "" -l "help,skip-baseimage-init,provider:,version:,env:,edition:,skip-reboot" -n "$0" -- "$@")
# TODO this is still there for the restore case, see other occasions below
versionsUrl="https://s3.amazonaws.com/prod-cloudron-releases/versions.json"
args=$(getopt -o "" -l "domain:,help,skip-baseimage-init,data:,data-dir:,provider:,encryption-key:,restore-url:,tls-provider:,version:,dns-provider:,env:,admin-location:,prerelease,skip-reboot,source-url:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--domain) domain="$2"; shift 2;;
--admin-location) adminLocation="$2"; shift 2;;
--help) echo "See https://cloudron.io/documentation/installation/ on how to install Cloudron"; exit 0;;
--provider) provider="$2"; shift 2;;
--edition) edition="$2"; shift 2;;
--encryption-key) encryptionKey="$2"; shift 2;;
--restore-url) restoreUrl="$2"; shift 2;;
--tls-provider) tlsProvider="$2"; shift 2;;
--dns-provider) dnsProvider="$2"; shift 2;;
--version) requestedVersion="$2"; shift 2;;
--env)
if [[ "$2" == "dev" ]]; then
versionsUrl="https://s3.amazonaws.com/dev-cloudron-releases/versions.json"
apiServerOrigin="https://api.dev.cloudron.io"
webServerOrigin="https://dev.cloudron.io"
tlsProvider="le-staging"
prerelease="true"
elif [[ "$2" == "staging" ]]; then
versionsUrl="https://s3.amazonaws.com/staging-cloudron-releases/versions.json"
apiServerOrigin="https://api.staging.cloudron.io"
webServerOrigin="https://staging.cloudron.io"
tlsProvider="le-staging"
prerelease="true"
fi
shift 2;;
--skip-baseimage-init) initBaseImage="false"; shift;;
--skip-reboot) rebootServer="false"; shift;;
--data) dataJson="$2"; shift 2;;
--prerelease) prerelease="true"; shift;;
--source-url) sourceTarballUrl="$2"; version="0.0.1+custom"; shift 2;;
--data-dir) baseDataDir=$(realpath "$2"); shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
# Only --help works as non-root
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
# Only --help works with mismatched ubuntu
ubuntu_version=$(lsb_release -rs)
if [[ "${ubuntu_version}" != "16.04" && "${ubuntu_version}" != "18.04" ]]; then
echo "Cloudron requires Ubuntu 16.04 or 18.04" > /dev/stderr
exit 1
fi
# Can only write after we have confirmed script has root access
echo "Running cloudron-setup with args : $@" > "${LOG_FILE}"
# validate arguments in the absence of data
if [[ -z "${provider}" ]]; then
echo "--provider is required (azure, digitalocean, ec2, exoscale, gce, hetzner, lightsail, linode, netcup, ovh, rosehosting, scaleway, vultr or generic)"
exit 1
elif [[ \
"${provider}" != "ami" && \
"${provider}" != "azure" && \
"${provider}" != "caas" && \
"${provider}" != "cloudscale" && \
"${provider}" != "contabo" && \
"${provider}" != "digitalocean" && \
"${provider}" != "digitalocean-mp" && \
"${provider}" != "ec2" && \
"${provider}" != "exoscale" && \
"${provider}" != "galaxygate" && \
"${provider}" != "digitalocean" && \
"${provider}" != "gce" && \
"${provider}" != "hetzner" && \
"${provider}" != "lightsail" && \
"${provider}" != "linode" && \
"${provider}" != "netcup" && \
"${provider}" != "netcup-image" && \
"${provider}" != "ovh" && \
"${provider}" != "rosehosting" && \
"${provider}" != "scaleway" && \
"${provider}" != "vultr" && \
"${provider}" != "generic" \
]]; then
echo "--provider must be one of: azure, cloudscale.ch, contabo, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, lightsail, linode, netcup, ovh, rosehosting, scaleway, vultr or generic"
exit 1
fi
if [[ -z "${dataJson}" ]]; then
if [[ -z "${provider}" ]]; then
echo "--provider is required (azure, cloudscale.ch, digitalocean, ec2, exoscale, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
exit 1
elif [[ \
"${provider}" != "ami" && \
"${provider}" != "azure" && \
"${provider}" != "cloudscale.ch" && \
"${provider}" != "digitalocean" && \
"${provider}" != "ec2" && \
"${provider}" != "exoscale" && \
"${provider}" != "gce" && \
"${provider}" != "lightsail" && \
"${provider}" != "linode" && \
"${provider}" != "ovh" && \
"${provider}" != "rosehosting" && \
"${provider}" != "scaleway" && \
"${provider}" != "vultr" && \
"${provider}" != "generic" \
]]; then
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, gce, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
exit 1
fi
if [[ -n "${baseDataDir}" && ! -d "${baseDataDir}" ]]; then
echo "${baseDataDir} does not exist"
exit 1
if [[ "${tlsProvider}" != "fallback" && "${tlsProvider}" != "le-prod" && "${tlsProvider}" != "le-staging" ]]; then
echo "--tls-provider must be one of: le-prod, le-staging, fallback"
exit 1
fi
if [[ -z "${dnsProvider}" ]]; then
echo "--dns-provider is required (noop, manual)"
exit 1
elif [[ "${dnsProvider}" != "noop" && "${dnsProvider}" != "manual" ]]; then
echo "--dns-provider must be one of : manual, noop"
exit 1
fi
if [[ -n "${baseDataDir}" && ! -d "${baseDataDir}" ]]; then
echo "${baseDataDir} does not exist"
exit 1
fi
fi
echo ""
@@ -137,49 +156,92 @@ echo ""
echo " Follow setup logs in a second terminal with:"
echo " $ tail -f ${LOG_FILE}"
echo ""
echo " Join us at https://forum.cloudron.io for any questions."
echo " Join us at https://chat.cloudron.io for any questions."
echo ""
if [[ "${initBaseImage}" == "true" ]]; then
echo "=> Installing software-properties-common"
if ! apt-get install -y software-properties-common &>> "${LOG_FILE}"; then
echo "Could not install software-properties-common (for add-apt-repository below). See ${LOG_FILE}"
exit 1
fi
echo "=> Ensure required apt sources"
if ! add-apt-repository universe &>> "${LOG_FILE}"; then
echo "Could not add required apt sources (for nginx-full). See ${LOG_FILE}"
exit 1
fi
echo "=> Updating apt and installing script dependencies"
if ! apt-get update &>> "${LOG_FILE}"; then
echo "Could not update package repositories. See ${LOG_FILE}"
echo "Could not update package repositories"
exit 1
fi
if ! apt-get install curl python3 ubuntu-standard -y &>> "${LOG_FILE}"; then
echo "Could not install setup dependencies (curl). See ${LOG_FILE}"
echo "Could not install setup dependencies (curl)"
exit 1
fi
fi
echo "=> Checking version"
if ! releaseJson=$($curl -s "${apiServerOrigin}/api/v1/releases?boxVersion=${requestedVersion}"); then
echo "Failed to get release information"
exit 1
if [[ "${sourceTarballUrl}" == "" ]]; then
if ! releaseJson=$($curl -s "${apiServerOrigin}/api/v1/releases?prerelease=${prerelease}&boxVersion=${requestedVersion}"); then
echo "Failed to get release information"
exit 1
fi
if [[ "$requestedVersion" == "" ]]; then
version=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["version"])')
else
version="${requestedVersion}"
fi
if ! sourceTarballUrl=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["info"]["sourceTarballUrl"])'); then
echo "No source code for version '${requestedVersion:-latest}'"
exit 1
fi
fi
if [[ "$requestedVersion" == "" ]]; then
version=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["version"])')
# Build data
# TODO versionsUrl is still there for the cloudron restore case
if [[ -z "${dataJson}" ]]; then
if [[ -z "${restoreUrl}" ]]; then
data=$(cat <<EOF
{
"boxVersionsUrl": "${versionsUrl}",
"fqdn": "${domain}",
"adminLocation": "${adminLocation}",
"zoneName": "${zoneName}",
"provider": "${provider}",
"apiServerOrigin": "${apiServerOrigin}",
"webServerOrigin": "${webServerOrigin}",
"tlsConfig": {
"provider": "${tlsProvider}"
},
"dnsConfig": {
"provider": "${dnsProvider}"
},
"backupConfig" : {
"provider": "filesystem",
"backupFolder": "/var/backups",
"key": "${encryptionKey}",
"format": "tgz",
"retentionSecs": 172800
},
"version": "${version}"
}
EOF
)
else
data=$(cat <<EOF
{
"boxVersionsUrl": "${versionsUrl}",
"fqdn": "${domain}",
"adminLocation": "${adminLocation}",
"zoneName": "${zoneName}",
"provider": "${provider}",
"apiServerOrigin": "${apiServerOrigin}",
"webServerOrigin": "${webServerOrigin}",
"restore": {
"url": "${restoreUrl}",
"key": "${encryptionKey}"
},
"version": "${version}"
}
EOF
)
fi
else
version="${requestedVersion}"
fi
if ! sourceTarballUrl=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["info"]["sourceTarballUrl"])'); then
echo "No source code for version '${requestedVersion:-latest}'"
exit 1
data="${dataJson}"
fi
echo "=> Downloading version ${version} ..."
@@ -199,63 +261,32 @@ if [[ "${initBaseImage}" == "true" ]]; then
echo ""
fi
# NOTE: this install script only supports 3.x and above
echo "=> Installing version ${version} (this takes some time) ..."
if [[ "${version}" =~ 3\.[0-2]+\.[0-9]+ ]]; then
readonly DATA_FILE="/root/cloudron-install-data.json"
data=$(cat <<EOF
{
"provider": "${provider}",
"edition": "${edition}",
"apiServerOrigin": "${apiServerOrigin}",
"webServerOrigin": "${webServerOrigin}",
"version": "${version}"
}
EOF
)
echo "${data}" > "${DATA_FILE}"
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" &>> "${LOG_FILE}"; then
echo "Failed to install cloudron. See ${LOG_FILE} for details"
exit 1
fi
rm "${DATA_FILE}"
else
mkdir -p /etc/cloudron
cat > "/etc/cloudron/cloudron.conf" <<CONF_END
{
"apiServerOrigin": "${apiServerOrigin}",
"webServerOrigin": "${webServerOrigin}",
"provider": "${provider}",
"edition": "${edition}"
}
CONF_END
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" &>> "${LOG_FILE}"; then
echo "Failed to install cloudron. See ${LOG_FILE} for details"
exit 1
fi
echo "${data}" > "${DATA_FILE}"
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" --data-dir "${baseDataDir}" &>> "${LOG_FILE}"; then
echo "Failed to install cloudron. See ${LOG_FILE} for details"
exit 1
fi
rm "${DATA_FILE}"
echo -n "=> Waiting for cloudron to be ready (this takes some time) ..."
while true; do
echo -n "."
if status=$($curl -q -f "http://localhost:3000/api/v1/cloudron/status" 2>/dev/null); then
break # we are up and running
[[ -z "$domain" ]] && break # with no domain, we are up and running
[[ "$status" == *"\"tls\": true"* ]] && break # with a domain, wait for the cert
fi
sleep 10
done
echo -e "\n\n${GREEN}Visit https://<IP> and accept the self-signed certificate to finish setup.${DONE}\n"
if [[ -n "${domain}" ]]; then
echo -e "\n\nVisit https://my.${domain} to finish setup once the server has rebooted.\n"
else
echo -e "\n\nVisit https://<IP> to finish setup once the server has rebooted.\n"
fi
if [[ "${rebootServer}" == "true" ]]; then
systemctl stop box mysql # sometimes mysql ends up having corrupt privilege tables
read -p "The server has to be rebooted to apply all the settings. Reboot now ? [Y/n] " yn
yn=${yn:-y}
case $yn in
[Yy]* ) systemctl reboot;;
* ) exit;;
esac
echo -e "\n\nRebooting this server now to let bootloader changes take effect.\n"
systemctl stop mysql # sometimes mysql ends up having corrupt privilege tables
systemctl reboot
fi
-115
View File
@@ -1,115 +0,0 @@
#!/bin/bash
# This script collects diagnostic information to help debug server related issues
# It also enables SSH access for the cloudron support team
PASTEBIN="https://paste.cloudron.io"
OUT="/tmp/cloudron-support.log"
LINE="\n========================================================\n"
CLOUDRON_SUPPORT_PUBLIC_KEY="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQVilclYAIu+ioDp/sgzzFz6YU0hPcRYY7ze/LiF/lC7uQqK062O54BFXTvQ3ehtFZCx3bNckjlT2e6gB8Qq07OM66De4/S/g+HJW4TReY2ppSPMVNag0TNGxDzVH8pPHOysAm33LqT2b6L/wEXwC6zWFXhOhHjcMqXvi8Ejaj20H1HVVcf/j8qs5Thkp9nAaFTgQTPu8pgwD8wDeYX1hc9d0PYGesTADvo6HF4hLEoEnefLw7PaStEbzk2fD3j7/g5r5HcgQQXBe74xYZ/1gWOX2pFNuRYOBSEIrNfJEjFJsqk3NR1+ZoMGK7j+AZBR4k0xbrmncQLcQzl6MMDzkp support@cloudron.io"
HELP_MESSAGE="
This script collects diagnostic information to help debug server related issues
Options:
--enable-ssh Enable SSH access for the Cloudron support team
--help Show this message
"
# We require root
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
enableSSH="false"
args=$(getopt -o "" -l "help,enable-ssh" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--help) echo -e "${HELP_MESSAGE}"; exit 0;;
--enable-ssh) enableSSH="true"; shift;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
# check if at least 10mb root partition space is available
if [[ "`df --output="avail" / | sed -n 2p`" -lt "10240" ]]; then
echo "No more space left on /"
echo "This is likely the root case of the issue. Free up some space and also check other partitions below:"
echo ""
df -h
echo ""
echo "To recover from a full disk, follow the guide at https://cloudron.io/documentation/server/#recovery-after-disk-full"
exit 1
fi
# check for at least 5mb free /tmp space for the log file
if [[ "`df --output="avail" /tmp | sed -n 2p`" -lt "5120" ]]; then
echo "Not enough space left on /tmp"
echo "Free up some space first by deleting files from /tmp"
exit 1
fi
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
echo -e $LINE"Docker container"$LINE >> $OUT
if ! timeout --kill-after 10s 15s docker ps -a &>> $OUT 2>&1; then
echo -e "Docker is not responding" >> $OUT
fi
echo -e $LINE"Filesystem stats"$LINE >> $OUT
df -h &>> $OUT
echo -e $LINE"System daemon status"$LINE >> $OUT
systemctl status --lines=100 cloudron.target box mysql unbound cloudron-syslog nginx collectd docker &>> $OUT
echo -e $LINE"Box logs"$LINE >> $OUT
tail -n 100 /home/yellowtent/platformdata/logs/box.log &>> $OUT
echo -e $LINE"Firewall chains"$LINE >> $OUT
iptables -L &>> $OUT
echo "Done"
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 "${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 ""
echo "${PASTEBIN}/${paste_key}"
+42 -40
View File
@@ -7,78 +7,80 @@ set -eu
[[ $(uname -s) == "Darwin" ]] && GNU_GETOPT="/usr/local/opt/gnu-getopt/bin/getopt" || GNU_GETOPT="getopt"
readonly GNU_GETOPT
args=$(${GNU_GETOPT} -o "" -l "output:,version:" -n "$0" -- "$@")
args=$(${GNU_GETOPT} -o "" -l "revision:,output:" -n "$0" -- "$@")
eval set -- "${args}"
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
commitish="HEAD"
bundle_file=""
version=""
while true; do
case "$1" in
--revision) commitish="$2"; shift 2;;
--output) bundle_file="$2"; shift 2;;
--version) version="$2"; shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
if [[ -z "${version}" ]]; then
echo "--version is required"
exit 1
fi
readonly TMPDIR=${TMPDIR:-/tmp} # why is this not set on mint?
if ! $(cd "${SOURCE_DIR}" && git diff --exit-code >/dev/null); then
echo "You have local changes in box, stash or commit them to proceed"
echo "You have local changes, stash or commit them to proceed"
exit 1
fi
if ! $(cd "${SOURCE_DIR}/../dashboard" && git diff --exit-code >/dev/null); then
echo "You have local changes in dashboard, stash or commit them to proceed"
if [[ "$(node --version)" != "v6.11.5" ]]; then
echo "This script requires node 6.11.5"
exit 1
fi
if [[ "$(node --version)" != "v8.11.2" ]]; then
echo "This script requires node 8.11.2"
exit 1
fi
box_version=$(cd "${SOURCE_DIR}" && git rev-parse "HEAD")
branch=$(git rev-parse --abbrev-ref HEAD)
dashboard_version=$(cd "${SOURCE_DIR}/../dashboard" && git fetch && git rev-parse "${branch}")
version=$(cd "${SOURCE_DIR}" && git rev-parse "${commitish}")
bundle_dir=$(mktemp -d -t box 2>/dev/null || mktemp -d box-XXXXXXXXXX --tmpdir=$TMPDIR)
[[ -z "$bundle_file" ]] && bundle_file="${TMPDIR}/box-${box_version:0:10}-${dashboard_version:0:10}-${version}.tar.gz"
[[ -z "$bundle_file" ]] && bundle_file="${TMPDIR}/box-${version}.tar.gz"
chmod "o+rx,g+rx" "${bundle_dir}" # otherwise extracted tarball director won't be readable by others/group
echo "==> Checking out code box version [${box_version}] and dashboard version [${dashboard_version}] into ${bundle_dir}"
(cd "${SOURCE_DIR}" && git archive --format=tar ${box_version} | (cd "${bundle_dir}" && tar xf -))
(cd "${SOURCE_DIR}/../dashboard" && git archive --format=tar ${dashboard_version} | (mkdir -p "${bundle_dir}/dashboard.build" && cd "${bundle_dir}/dashboard.build" && tar xf -))
(cp "${SOURCE_DIR}/../dashboard/LICENSE" "${bundle_dir}")
echo "${version}" > "${bundle_dir}/VERSION"
echo "Checking out code [${version}] into ${bundle_dir}"
(cd "${SOURCE_DIR}" && git archive --format=tar ${version} | (cd "${bundle_dir}" && tar xf -))
echo "==> Installing modules for dashboard asset generation"
(cd "${bundle_dir}/dashboard.build" && npm install --production)
if diff "${TMPDIR}/boxtarball.cache/npm-shrinkwrap.json.all" "${bundle_dir}/npm-shrinkwrap.json" >/dev/null 2>&1; then
echo "Reusing dev modules from cache"
cp -r "${TMPDIR}/boxtarball.cache/node_modules-all/." "${bundle_dir}/node_modules"
else
echo "Installing modules with dev dependencies"
(cd "${bundle_dir}" && npm install)
echo "==> Building dashboard assets"
(cd "${bundle_dir}/dashboard.build" && ./node_modules/.bin/gulp --revision ${dashboard_version})
echo "Caching dev dependencies"
mkdir -p "${TMPDIR}/boxtarball.cache/node_modules-all"
rsync -a --delete "${bundle_dir}/node_modules/" "${TMPDIR}/boxtarball.cache/node_modules-all/"
cp "${bundle_dir}/npm-shrinkwrap.json" "${TMPDIR}/boxtarball.cache/npm-shrinkwrap.json.all"
fi
echo "==> Move built dashboard assets into destination"
mkdir -p "${bundle_dir}/dashboard"
mv "${bundle_dir}/dashboard.build/dist" "${bundle_dir}/dashboard/"
echo "Building webadmin assets"
(cd "${bundle_dir}" && ./node_modules/.bin/gulp)
echo "==> Cleanup dashboard build artifacts"
rm -rf "${bundle_dir}/dashboard.build"
echo "Remove intermediate files required at build-time only"
rm -rf "${bundle_dir}/node_modules/"
rm -rf "${bundle_dir}/webadmin/src"
rm -rf "${bundle_dir}/gulpfile.js"
echo "==> Installing toplevel node modules"
(cd "${bundle_dir}" && npm install --production --no-optional)
if diff "${TMPDIR}/boxtarball.cache/npm-shrinkwrap.json.prod" "${bundle_dir}/npm-shrinkwrap.json" >/dev/null 2>&1; then
echo "Reusing prod modules from cache"
cp -r "${TMPDIR}/boxtarball.cache/node_modules-prod/." "${bundle_dir}/node_modules"
else
echo "Installing modules for production"
(cd "${bundle_dir}" && npm install --production --no-optional)
echo "==> Create final tarball"
echo "Caching prod dependencies"
mkdir -p "${TMPDIR}/boxtarball.cache/node_modules-prod"
rsync -a --delete "${bundle_dir}/node_modules/" "${TMPDIR}/boxtarball.cache/node_modules-prod/"
cp "${bundle_dir}/npm-shrinkwrap.json" "${TMPDIR}/boxtarball.cache/npm-shrinkwrap.json.prod"
fi
echo "Create final tarball"
(cd "${bundle_dir}" && tar czf "${bundle_file}" .)
echo "==> Cleaning up ${bundle_dir}"
echo "Cleaning up ${bundle_dir}"
rm -rf "${bundle_dir}"
echo "==> Tarball saved at ${bundle_file}"
echo "Tarball saved at ${bundle_file}"
+46 -57
View File
@@ -1,9 +1,5 @@
#!/bin/bash
# This script is run before the box code is switched. This means that we can
# put network related/curl downloads here. If the script fails, the old code
# will continue to run
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
@@ -14,41 +10,52 @@ fi
readonly USER=yellowtent
readonly BOX_SRC_DIR=/home/${USER}/box
readonly BASE_DATA_DIR=/home/${USER}
readonly CLOUDRON_CONF=/home/yellowtent/configs/cloudron.conf
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly box_src_tmp_dir="$(realpath ${script_dir}/..)"
readonly is_update=$(systemctl is-active box && echo "yes" || echo "no")
readonly is_update=$([[ -f "${CLOUDRON_CONF}" ]] && echo "yes" || echo "no")
arg_data=""
arg_data_dir=""
args=$(getopt -o "" -l "data:,data-file:,data-dir:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--data) arg_data="$2"; shift 2;;
--data-file) arg_data=$(cat $2); shift 2;;
--data-dir) arg_data_dir="$2"; shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
echo "==> installer: updating docker"
if [[ $(docker version --format {{.Client.Version}}) != "18.03.1-ce" ]]; then
$curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_18.03.1~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
if [[ $(docker version --format {{.Client.Version}}) != "17.09.0-ce" ]]; then
$curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_17.09.0~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
# https://download.docker.com/linux/ubuntu/dists/xenial/stable/binary-amd64/Packages
if [[ $(sha256sum /tmp/docker.deb | cut -d' ' -f1) != "54f4c9268492a4fd2ec2e6bcc95553855b025f35dcc8b9f60ac34e0aa307279b" ]]; then
echo "==> installer: docker binary download is corrupt"
if [[ $(sha256sum /tmp/docker.deb | cut -d' ' -f1) != "d33f6eb134f0ab0876148bd96de95ea47d583d7f2cddfdc6757979453f9bd9bf" ]]; then
echo "docker binary download is corrupt"
exit 5
fi
echo "==> installer: Waiting for all dpkg tasks to finish..."
echo "Waiting for all dpkg tasks to finish..."
while fuser /var/lib/dpkg/lock; do
sleep 1
done
while ! dpkg --force-confold --configure -a; do
echo "==> installer: Failed to fix packages. Retry"
sleep 1
done
# the latest docker might need newer packages
while ! apt update -y; do
echo "==> installer: Failed to update packages. Retry"
echo "Failed to fix packages. Retry"
sleep 1
done
while ! apt install -y /tmp/docker.deb; do
echo "==> installer: Failed to install docker. Retry"
echo "Failed to install docker. Retry"
sleep 1
done
@@ -56,15 +63,14 @@ if [[ $(docker version --format {{.Client.Version}}) != "18.03.1-ce" ]]; then
fi
echo "==> installer: updating node"
if [[ "$(node --version)" != "v8.11.2" ]]; then
mkdir -p /usr/local/node-8.11.2
$curl -sL https://nodejs.org/dist/v8.11.2/node-v8.11.2-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-8.11.2
ln -sf /usr/local/node-8.11.2/bin/node /usr/bin/node
ln -sf /usr/local/node-8.11.2/bin/npm /usr/bin/npm
rm -rf /usr/local/node-6.11.5
if [[ "$(node --version)" != "v6.11.5" ]]; then
mkdir -p /usr/local/node-6.11.5
$curl -sL https://nodejs.org/dist/v6.11.5/node-v6.11.5-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-6.11.5
ln -sf /usr/local/node-6.11.5/bin/node /usr/bin/node
ln -sf /usr/local/node-6.11.5/bin/npm /usr/bin/npm
rm -rf /usr/local/node-6.11.3
fi
# this is here (and not in updater.js) because rebuild requires the above node
for try in `seq 1 10`; do
# for reasons unknown, the dtrace package will fail. but rebuilding second time will work
@@ -72,49 +78,32 @@ for try in `seq 1 10`; do
# however by default npm drops privileges for npm rebuild
# https://docs.npmjs.com/misc/config#unsafe-perm
if cd "${box_src_tmp_dir}" && npm rebuild --unsafe-perm; then break; fi
echo "==> installer: Failed to rebuild, trying again"
echo "Failed to rebuild, trying again"
sleep 5
done
if [[ ${try} -eq 10 ]]; then
echo "==> installer: npm rebuild failed, giving up"
echo "npm rebuild failed"
exit 4
fi
echo "==> installer: downloading new addon images"
images=$(node -e "var i = require('${box_src_tmp_dir}/src/infra_version.js'); console.log(i.baseImages.map(function (x) { return x.tag; }).join(' '), Object.keys(i.images).map(function (x) { return i.images[x].tag; }).join(' '));")
echo -e "\tPulling docker images: ${images}"
for image in ${images}; do
if ! docker pull "${image}"; then # this pulls the image using the sha256
echo "==> installer: Could not pull ${image}"
exit 5
fi
if ! docker pull "${image%@sha256:*}"; then # this will tag the image for readability
echo "==> installer: Could not pull ${image%@sha256:*}"
exit 6
fi
done
echo "==> installer: update cloudron-syslog"
CLOUDRON_SYSLOG_DIR=/usr/local/cloudron-syslog
CLOUDRON_SYSLOG="${CLOUDRON_SYSLOG_DIR}/bin/cloudron-syslog"
CLOUDRON_SYSLOG_VERSION="1.0.3"
while [[ ! -f "${CLOUDRON_SYSLOG}" || "$(${CLOUDRON_SYSLOG} --version)" != ${CLOUDRON_SYSLOG_VERSION} ]]; do
rm -rf "${CLOUDRON_SYSLOG_DIR}"
mkdir -p "${CLOUDRON_SYSLOG_DIR}"
if npm install --unsafe-perm -g --prefix "${CLOUDRON_SYSLOG_DIR}" cloudron-syslog@${CLOUDRON_SYSLOG_VERSION}; then break; fi
echo "===> installer: Failed to install cloudron-syslog, trying again"
sleep 5
done
if ! id "${USER}" 2>/dev/null; then
useradd "${USER}" -m
fi
if [[ "${is_update}" == "yes" ]]; then
echo "==> installer: stop cloudron.target service for update"
${BOX_SRC_DIR}/setup/stop.sh
echo "Setting up update splash screen"
"${box_src_tmp_dir}/setup/splashpage.sh" --data "${arg_data}" || true # show splash from new code
${BOX_SRC_DIR}/setup/stop.sh # stop the old code
fi
# setup links to data directory
if [[ -n "${arg_data_dir}" ]]; then
echo "==> installer: setting up links to data directory"
mkdir "${arg_data_dir}/appsdata"
ln -s "${arg_data_dir}/appsdata" "${BASE_DATA_DIR}/appsdata"
mkdir "${arg_data_dir}/platformdata"
ln -s "${arg_data_dir}/platformdata" "${BASE_DATA_DIR}/platformdata"
fi
# ensure we are not inside the source directory, which we will remove now
@@ -126,4 +115,4 @@ mv "${box_src_tmp_dir}" "${BOX_SRC_DIR}"
chown -R "${USER}:${USER}" "${BOX_SRC_DIR}"
echo "==> installer: calling box setup script"
"${BOX_SRC_DIR}/setup/start.sh"
"${BOX_SRC_DIR}/setup/start.sh" --data "${arg_data}"
+108
View File
@@ -0,0 +1,108 @@
#!/bin/bash
source_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
json="${source_dir}/../node_modules/.bin/json"
# IMPORTANT: Fix cloudron.js:doUpdate if you add/remove any arg. keep these sorted for readability
arg_api_server_origin=""
arg_fqdn=""
arg_admin_location=""
arg_zone_name=""
arg_is_custom_domain="false"
arg_restore_key=""
arg_restore_url=""
arg_retire_reason=""
arg_retire_info=""
arg_tls_config=""
arg_tls_cert=""
arg_tls_key=""
arg_token=""
arg_version=""
arg_web_server_origin=""
arg_backup_config=""
arg_dns_config=""
arg_provider=""
arg_is_demo="false"
args=$(getopt -o "" -l "data:,retire-reason:,retire-info:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--retire-reason)
arg_retire_reason="$2"
shift 2
;;
--retire-info)
arg_retire_info="$2"
shift 2
;;
--data)
# these params must be valid in all cases
arg_fqdn=$(echo "$2" | $json fqdn)
arg_zone_name=$(echo "$2" | $json zoneName)
arg_is_custom_domain=$(echo "$2" | $json isCustomDomain)
[[ "${arg_is_custom_domain}" == "" ]] && arg_is_custom_domain="true"
arg_admin_location=$(echo "$2" | $json adminLocation)
[[ "${arg_admin_location}" == "" ]] && arg_admin_location="my"
# only update/restore have this valid (but not migrate)
arg_api_server_origin=$(echo "$2" | $json apiServerOrigin)
[[ "${arg_api_server_origin}" == "" ]] && arg_api_server_origin="https://api.cloudron.io"
arg_web_server_origin=$(echo "$2" | $json webServerOrigin)
[[ "${arg_web_server_origin}" == "" ]] && arg_web_server_origin="https://cloudron.io"
# TODO check if and where this is used
arg_version=$(echo "$2" | $json version)
# read possibly empty parameters here
arg_is_demo=$(echo "$2" | $json isDemo)
[[ "${arg_is_demo}" == "" ]] && arg_is_demo="false"
arg_tls_cert=$(echo "$2" | $json tlsCert)
[[ "${arg_tls_cert}" == "null" ]] && arg_tls_cert=""
arg_tls_key=$(echo "$2" | $json tlsKey)
[[ "${arg_tls_key}" == "null" ]] && arg_tls_key=""
arg_token=$(echo "$2" | $json token)
arg_provider=$(echo "$2" | $json provider)
[[ "${arg_provider}" == "" ]] && arg_provider="generic"
arg_tls_config=$(echo "$2" | $json tlsConfig)
[[ "${arg_tls_config}" == "null" ]] && arg_tls_config=""
arg_restore_url=$(echo "$2" | $json restore.url)
[[ "${arg_restore_url}" == "null" ]] && arg_restore_url=""
arg_restore_key=$(echo "$2" | $json restore.key)
[[ "${arg_restore_key}" == "null" ]] && arg_restore_key=""
arg_backup_config=$(echo "$2" | $json backupConfig)
[[ "${arg_backup_config}" == "null" ]] && arg_backup_config=""
arg_dns_config=$(echo "$2" | $json dnsConfig)
[[ "${arg_dns_config}" == "null" ]] && arg_dns_config=""
shift 2
;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
echo "Parsed arguments:"
echo "api server: ${arg_api_server_origin}"
echo "fqdn: ${arg_fqdn}"
echo "custom domain: ${arg_is_custom_domain}"
echo "restore url: ${arg_restore_url}"
echo "tls cert: ${arg_tls_cert}"
# do not dump these as they might become available via logs API
#echo "restore key: ${arg_restore_key}"
#echo "tls key: ${arg_tls_key}"
#echo "token: ${arg_token}"
echo "tlsConfig: ${arg_tls_config}"
echo "version: ${arg_version}"
echo "web server: ${arg_web_server_origin}"
echo "provider: ${arg_provider}"
+49
View File
@@ -0,0 +1,49 @@
#!/bin/bash
set -eu -o pipefail
readonly SETUP_WEBSITE_DIR="/home/yellowtent/setup/website"
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly box_src_dir="$(realpath ${script_dir}/..)"
readonly PLATFORM_DATA_DIR="/home/yellowtent/platformdata"
echo "Setting up nginx update page"
if [[ ! -f "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf" ]]; then
echo "No admin.conf found. This Cloudron has no domain yet. Skip splash setup"
exit
fi
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
# keep this is sync with config.js appFqdn()
admin_fqdn=$([[ "${arg_is_custom_domain}" == "true" ]] && echo "${arg_admin_location}.${arg_fqdn}" || echo "${arg_admin_location}-${arg_fqdn}")
admin_origin="https://${admin_fqdn}"
# copy the website
rm -rf "${SETUP_WEBSITE_DIR}" && mkdir -p "${SETUP_WEBSITE_DIR}"
cp -r "${script_dir}/splash/website/"* "${SETUP_WEBSITE_DIR}"
# create nginx config
readonly current_infra=$(node -e "console.log(require('${script_dir}/../src/infra_version.js').version);")
existing_infra="none"
[[ -f "${PLATFORM_DATA_DIR}/INFRA_VERSION" ]] && existing_infra=$(node -e "console.log(JSON.parse(require('fs').readFileSync('${PLATFORM_DATA_DIR}/INFRA_VERSION', 'utf8')).version);")
if [[ "${arg_retire_reason}" != "" || "${existing_infra}" != "${current_infra}" ]]; then
echo "Showing progress bar on all subdomains in retired mode or infra update. retire: ${arg_retire_reason} existing: ${existing_infra} current: ${current_infra}"
rm -f ${PLATFORM_DATA_DIR}/nginx/applications/*
${box_src_dir}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
-O "{ \"vhost\": \"~^(.+)\$\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\", \"robotsTxtQuoted\": null, \"hasIPv6\": false }" > "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf"
else
echo "Show progress bar only on admin domain for normal update"
${box_src_dir}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\", \"robotsTxtQuoted\": null, \"hasIPv6\": false }" > "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf"
fi
if [[ "${arg_retire_reason}" == "migrate" ]]; then
echo "{ \"migrate\": { \"percent\": \"10\", \"message\": \"Migrating cloudron. This could take up to 15 minutes.\", \"info\": ${arg_retire_info} }, \"backup\": null, \"apiServerOrigin\": \"${arg_api_server_origin}\" }" > "${SETUP_WEBSITE_DIR}/progress.json"
else
echo '{ "update": { "percent": "10", "message": "Updating cloudron software" }, "backup": null }' > "${SETUP_WEBSITE_DIR}/progress.json"
fi
nginx -s reload
+132 -31
View File
@@ -2,9 +2,6 @@
set -eu -o pipefail
# This script is run after the box code is switched. This means that this script
# should pretty much always succeed. No network logic/download code here.
echo "==> Cloudron Start"
readonly USER="yellowtent"
@@ -13,12 +10,28 @@ readonly BOX_SRC_DIR="${HOME_DIR}/box"
readonly PLATFORM_DATA_DIR="${HOME_DIR}/platformdata" # platform data
readonly APPS_DATA_DIR="${HOME_DIR}/appsdata" # app data
readonly BOX_DATA_DIR="${HOME_DIR}/boxdata" # box data
readonly CONFIG_DIR="${HOME_DIR}/configs"
readonly SETUP_PROGRESS_JSON="${HOME_DIR}/setup/website/progress.json"
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly json="$(realpath ${script_dir}/../node_modules/.bin/json)"
readonly ubuntu_version=$(lsb_release -rs)
cp -f "${script_dir}/../scripts/cloudron-support" /usr/bin/cloudron-support
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
set_progress() {
local percent="$1"
local message="$2"
echo "==> ${percent} - ${message}"
(echo "{ \"update\": { \"percent\": \"${percent}\", \"message\": \"${message}\" }, \"backup\": {} }" > "${SETUP_PROGRESS_JSON}") 2> /dev/null || true # as this will fail in non-update mode
}
set_progress "20" "Configuring host"
sed -e 's/^#NTP=/NTP=0.ubuntu.pool.ntp.org 1.ubuntu.pool.ntp.org 2.ubuntu.pool.ntp.org 3.ubuntu.pool.ntp.org/' -i /etc/systemd/timesyncd.conf
timedatectl set-ntp 1
timedatectl set-timezone UTC
hostnamectl set-hostname "${arg_fqdn}"
echo "==> Configuring docker"
cp "${script_dir}/start/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
@@ -44,6 +57,19 @@ if [[ ! -f /etc/systemd/system/docker.service.d/cloudron.conf ]] || ! diff -q /e
fi
docker network create --subnet=172.18.0.0/16 cloudron || true
# caas has ssh on port 202 and we disable password login
if [[ "${arg_provider}" == "caas" ]]; then
# https://stackoverflow.com/questions/4348166/using-with-sed on why ? must be escaped
sed -e 's/^#\?PermitRootLogin .*/PermitRootLogin without-password/g' \
-e 's/^#\?PermitEmptyPasswords .*/PermitEmptyPasswords no/g' \
-e 's/^#\?PasswordAuthentication .*/PasswordAuthentication no/g' \
-e 's/^#\?Port .*/Port 202/g' \
-i /etc/ssh/sshd_config
# required so we can connect to this machine since port 22 is blocked by iptables by now
systemctl reload sshd
fi
mkdir -p "${BOX_DATA_DIR}"
mkdir -p "${APPS_DATA_DIR}"
@@ -54,14 +80,11 @@ mkdir -p "${PLATFORM_DATA_DIR}/graphite"
mkdir -p "${PLATFORM_DATA_DIR}/mysql"
mkdir -p "${PLATFORM_DATA_DIR}/postgresql"
mkdir -p "${PLATFORM_DATA_DIR}/mongodb"
mkdir -p "${PLATFORM_DATA_DIR}/redis"
mkdir -p "${PLATFORM_DATA_DIR}/addons/mail"
mkdir -p "${PLATFORM_DATA_DIR}/collectd/collectd.conf.d"
mkdir -p "${PLATFORM_DATA_DIR}/logrotate.d"
mkdir -p "${PLATFORM_DATA_DIR}/acme"
mkdir -p "${PLATFORM_DATA_DIR}/backup"
mkdir -p "${PLATFORM_DATA_DIR}/logs/backup" "${PLATFORM_DATA_DIR}/logs/updater" "${PLATFORM_DATA_DIR}/logs/tasks"
mkdir -p "${PLATFORM_DATA_DIR}/update"
mkdir -p "${BOX_DATA_DIR}/appicons"
mkdir -p "${BOX_DATA_DIR}/certs"
@@ -90,22 +113,22 @@ systemctl daemon-reload
systemctl restart systemd-journald
setfacl -n -m u:${USER}:r /var/log/journal/*/system.journal
echo "==> Creating config directory"
rm -rf "${CONFIG_DIR}" && mkdir "${CONFIG_DIR}"
echo "==> Setting up unbound"
# DO uses Google nameservers by default. This causes RBL queries to fail (host 2.0.0.127.zen.spamhaus.org)
# We do not use dnsmasq because it is not a recursive resolver and defaults to the value in the interfaces file (which is Google DNS!)
# We listen on 0.0.0.0 because there is no way control ordering of docker (which creates the 172.18.0.0/16) and unbound
# If IP6 is not enabled, dns queries seem to fail on some hosts. -s returns false if file missing or 0 size
ip6=$([[ -s /proc/net/if_inet6 ]] && echo "yes" || echo "no")
echo -e "server:\n\tinterface: 0.0.0.0\n\tdo-ip6: ${ip6}\n\taccess-control: 127.0.0.1 allow\n\taccess-control: 172.18.0.1/16 allow\n\tcache-max-negative-ttl: 30\n\tcache-max-ttl: 300\n\t#logfile: /var/log/unbound.log\n\t#verbosity: 10" > /etc/unbound/unbound.conf.d/cloudron-network.conf
# If IP6 is not enabled, dns queries seem to fail on some hosts
echo -e "server:\n\tinterface: 0.0.0.0\n\tdo-ip6: yes\n\taccess-control: 127.0.0.1 allow\n\taccess-control: 172.18.0.1/16 allow\n\tcache-max-negative-ttl: 30\n\tcache-max-ttl: 300\n\t#logfile: /var/log/unbound.log\n\t#verbosity: 10" > /etc/unbound/unbound.conf.d/cloudron-network.conf
# update the root anchor after a out-of-disk-space situation (see #269)
unbound-anchor -a /var/lib/unbound/root.key
echo "==> Adding systemd services"
cp -r "${script_dir}/start/systemd/." /etc/systemd/system/
[[ "${ubuntu_version}" == "16.04" ]] && sed -e 's/MemoryMax/MemoryLimit/g' -i /etc/systemd/system/box.service
systemctl daemon-reload
systemctl enable unbound
systemctl enable cloudron-syslog
systemctl enable cloudron.target
systemctl enable cloudron-firewall
@@ -118,9 +141,6 @@ systemctl enable --now cron
# ensure unbound runs
systemctl restart unbound
# ensure cloudron-syslog runs
systemctl restart cloudron-syslog
echo "==> Configuring sudoers"
rm -f /etc/sudoers.d/${USER}
cp "${script_dir}/start/sudoers" /etc/sudoers.d/${USER}
@@ -135,8 +155,6 @@ echo "==> Configuring logrotate"
if ! grep -q "^include ${PLATFORM_DATA_DIR}/logrotate.d" /etc/logrotate.conf; then
echo -e "\ninclude ${PLATFORM_DATA_DIR}/logrotate.d\n" >> /etc/logrotate.conf
fi
cp "${script_dir}/start/box-logrotate" "${script_dir}/start/app-logrotate" "${PLATFORM_DATA_DIR}/logrotate.d/"
chown root:root "${PLATFORM_DATA_DIR}/logrotate.d/box-logrotate" "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate"
echo "==> Adding motd message for admins"
cp "${script_dir}/start/cloudron-motd" /etc/update-motd.d/92-cloudron
@@ -156,6 +174,9 @@ if ! grep -q "^Restart=" /etc/systemd/system/multi-user.target.wants/nginx.servi
fi
systemctl start nginx
# bookkeep the version as part of data
echo "{ \"version\": \"${arg_version}\", \"apiServerOrigin\": \"${arg_api_server_origin}\" }" > "${BOX_DATA_DIR}/version"
# restart mysql to make sure it has latest config
if [[ ! -f /etc/mysql/mysql.cnf ]] || ! diff -q "${script_dir}/start/mysql.cnf" /etc/mysql/mysql.cnf >/dev/null; then
# wait for all running mysql jobs
@@ -178,8 +199,94 @@ readonly mysql_root_password="password"
mysqladmin -u root -ppassword password password # reset default root password
mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
echo "==> Migrating data"
(cd "${BOX_SRC_DIR}" && BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up)
if [[ -n "${arg_restore_url}" ]]; then
set_progress "30" "Downloading restore data"
readonly restore_dir="${arg_restore_url#file://}"
if [[ -d "${restore_dir}" ]]; then # rsync backup
echo "==> Copying backup: ${restore_dir}"
if [[ $(stat -c "%d" "${BOX_DATA_DIR}") == $(stat -c "%d" "${restore_dir}") ]]; then
cp -rfl "${restore_dir}/." "${BOX_DATA_DIR}"
else
cp -rf "${restore_dir}/." "${BOX_DATA_DIR}"
fi
else # tgz backup
decrypt=""
if [[ "${arg_restore_url}" == *.tar.gz.enc || -n "${arg_restore_key}" ]]; then
echo "==> Downloading encrypted backup: ${arg_restore_url} and key: ${arg_restore_key}"
decrypt=(openssl aes-256-cbc -d -nosalt -pass "pass:${arg_restore_key}")
elif [[ "${arg_restore_url}" == *.tar.gz ]]; then
echo "==> Downloading backup: ${arg_restore_url}"
decrypt=(cat -)
fi
while true; do
if $curl -L "${arg_restore_url}" | "${decrypt[@]}" \
| tar -zxf - --overwrite -C "${BOX_DATA_DIR}"; then break; fi
echo "Failed to download data, trying again"
done
fi
set_progress "35" "Setting up MySQL"
if [[ -f "${BOX_DATA_DIR}/box.mysqldump" ]]; then
echo "==> Importing existing database into MySQL"
mysql -u root -p${mysql_root_password} box < "${BOX_DATA_DIR}/box.mysqldump"
fi
fi
set_progress "40" "Migrating data"
sudo -u "${USER}" -H bash <<EOF
set -eu
cd "${BOX_SRC_DIR}"
BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up
EOF
echo "==> Adding automated configs"
mysql -u root -p${mysql_root_password} -e "REPLACE INTO settings (name, value) VALUES (\"domain\", '{ \"fqdn\": \"$arg_fqdn\", \"zoneName\": \"$arg_zone_name\", \"adminLocation\": \"$arg_admin_location\" }')" box
if [[ ! -z "${arg_backup_config}" ]]; then
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO settings (name, value) VALUES (\"backup_config\", '$arg_backup_config')" box
fi
if [[ ! -z "${arg_dns_config}" ]]; then
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO settings (name, value) VALUES (\"dns_config\", '$arg_dns_config')" box
fi
if [[ ! -z "${arg_tls_config}" ]]; then
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO settings (name, value) VALUES (\"tls_config\", '$arg_tls_config')" box
fi
echo "==> Creating cloudron.conf"
cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
{
"version": "${arg_version}",
"token": "${arg_token}",
"apiServerOrigin": "${arg_api_server_origin}",
"webServerOrigin": "${arg_web_server_origin}",
"fqdn": "${arg_fqdn}",
"adminLocation": "${arg_admin_location}",
"zoneName": "${arg_zone_name}",
"isCustomDomain": ${arg_is_custom_domain},
"provider": "${arg_provider}",
"isDemo": ${arg_is_demo}
}
CONF_END
# pass these out-of-band because they have new lines which interfere with json
if [[ -n "${arg_tls_cert}" && -n "${arg_tls_key}" ]]; then
echo "${arg_tls_cert}" > "${CONFIG_DIR}/host.cert"
echo "${arg_tls_key}" > "${CONFIG_DIR}/host.key"
fi
echo "==> Creating config.json for webadmin"
cat > "${BOX_SRC_DIR}/webadmin/dist/config.json" <<CONF_END
{
"webServerOrigin": "${arg_web_server_origin}"
}
CONF_END
if [[ ! -f "${BOX_DATA_DIR}/dhparams.pem" ]]; then
echo "==> Generating dhparams (takes forever)"
@@ -190,25 +297,19 @@ else
fi
echo "==> Changing ownership"
# be careful of what is chown'ed here. subdirs like mysql,redis etc are owned by the containers and will stop working if perms change
chown -R "${USER}" /etc/cloudron
chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/nginx" "${PLATFORM_DATA_DIR}/collectd" "${PLATFORM_DATA_DIR}/addons" "${PLATFORM_DATA_DIR}/acme" "${PLATFORM_DATA_DIR}/backup" "${PLATFORM_DATA_DIR}/logs" "${PLATFORM_DATA_DIR}/update"
chown "${USER}:${USER}" -R "${CONFIG_DIR}"
chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/nginx" "${PLATFORM_DATA_DIR}/collectd" "${PLATFORM_DATA_DIR}/logrotate.d" "${PLATFORM_DATA_DIR}/addons" "${PLATFORM_DATA_DIR}/acme" "${PLATFORM_DATA_DIR}/backup"
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}/INFRA_VERSION" 2>/dev/null || true
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}"
chown "${USER}:${USER}" "${APPS_DATA_DIR}"
# logrotate files have to be owned by root, this is here to fixup existing installations where we were resetting the owner to yellowtent
chown root:root -R "${PLATFORM_DATA_DIR}/logrotate.d"
# do not chown the boxdata/mail directory; dovecot gets upset
chown "${USER}:${USER}" "${BOX_DATA_DIR}"
find "${BOX_DATA_DIR}" -mindepth 1 -maxdepth 1 -not -path "${BOX_DATA_DIR}/mail" -exec chown -R "${USER}:${USER}" {} \;
chown "${USER}:${USER}" "${BOX_DATA_DIR}/mail"
chown "${USER}:${USER}" -R "${BOX_DATA_DIR}/mail/dkim" # this is owned by box currently since it generates the keys
echo "==> Starting Cloudron"
set_progress "60" "Starting Cloudron"
systemctl start cloudron.target
sleep 2 # give systemd sometime to start the processes
echo "==> Almost done"
set_progress "90" "Almost done"
-10
View File
@@ -1,10 +0,0 @@
# logrotate config for app logs
/home/yellowtent/platformdata/logs/*/*.log {
# only keep one rotated file, we currently do not send that over the api
rotate 1
size 10M
# we never compress so we can simply tail the files
nocompress
copytruncate
}
-9
View File
@@ -1,9 +0,0 @@
# logrotate config for box logs
/home/yellowtent/platformdata/logs/box.log {
rotate 10
size 10M
# we never compress so we can simply tail the files
nocompress
copytruncate
}
+12 -26
View File
@@ -1,29 +1,15 @@
#!/bin/bash
#!/bin/sh
# motd hook to remind admins about updates
printf "\t\t\tNOTE TO CLOUDRON ADMINS\n"
printf "\t\t\t-----------------------\n"
printf "Please do not run apt upgrade manually as it will update packages that\n"
printf "Cloudron relies on and may break your installation. Ubuntu security updates\n"
printf "are automatically installed on this server every night.\n"
printf "\n"
printf "Read more at https://cloudron.io/documentation/security/#os-updates\n"
printf "**********************************************************************\n\n"
if [[ -z "$(ls -A /home/yellowtent/boxdata/mail/dkim)" ]]; then
printf "\t\t\tWELCOME TO CLOUDRON\n"
printf "\t\t\t-------------------\n"
printf '\n\e[1;32m%-6s\e[m\n\n' "Visit https://<IP> on your browser and accept the self-signed certificate to finish setup."
printf "Cloudron overview - https://cloudron.io/documentation/ \n"
printf "Cloudron setup - https://cloudron.io/documentation/installation/#setup \n"
else
printf "\t\t\tNOTE TO CLOUDRON ADMINS\n"
printf "\t\t\t-----------------------\n"
printf "Please do not run apt upgrade manually as it will update packages that\n"
printf "Cloudron relies on and may break your installation. Ubuntu security updates\n"
printf "are automatically installed on this server every night.\n"
printf "\n"
printf "Read more at https://cloudron.io/documentation/security/#os-updates\n"
if grep -q "^PasswordAuthentication yes" /etc/ssh/sshd_config; then
printf "\nPlease disable password based SSH access to secure your server. Read more at\n"
printf "https://cloudron.io/documentation/security/#securing-ssh-access\n"
fi
if grep -q "^PasswordAuthentication yes" /etc/ssh/sshd_config; then
printf "\nPlease disable password based SSH access to secure your server. Read more at\n"
printf "https://cloudron.io/documentation/security/#securing-ssh-access\n"
fi
printf "\nFor help and more information, visit https://forum.cloudron.io\n\n"
printf "**********************************************************************\n"
@@ -66,9 +66,8 @@ server {
# https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
ssl_prefer_server_ciphers on;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # don't use SSLv3 ref: POODLE
# ciphers according to https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=nginx-1.10.3&openssl=1.0.2g&hsts=yes&profile=modern
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
# ciphers according to https://weakdh.org/sysadmin.html
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
ssl_dhparam /home/yellowtent/boxdata/dhparams.pem;
add_header Strict-Transport-Security "max-age=15768000";
@@ -90,11 +89,6 @@ server {
add_header Referrer-Policy "no-referrer-when-downgrade";
proxy_hide_header Referrer-Policy;
<% if ( endpoint === 'admin' ) { -%>
# CSP headers for the admin/dashboard resources
add_header Content-Security-Policy "default-src 'none'; connect-src wss: https: 'self' *.cloudron.io; script-src https: 'self' 'unsafe-inline' 'unsafe-eval'; img-src * data:; style-src https: 'unsafe-inline'; object-src 'none'; font-src https: 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self';";
<% } -%>
proxy_http_version 1.1;
proxy_intercept_errors on;
proxy_read_timeout 3500;
@@ -112,7 +106,7 @@ server {
proxy_set_header Connection $connection_upgrade;
# only serve up the status page if we get proxy gateway errors
root <%= sourceDir %>/dashboard/dist;
root <%= sourceDir %>/webadmin/dist;
error_page 502 503 504 /appstatus.html;
location /appstatus.html {
internal;
@@ -160,22 +154,51 @@ server {
}
# graphite paths (uncomment block below and visit /graphite/index.html)
# remember to comment out the CSP policy as well to access the graphite dashboard
# location ~ ^/(graphite|content|metrics|dashboard|render|browser|composer)/ {
# proxy_pass http://127.0.0.1:8417;
# proxy_pass http://127.0.0.1:8000;
# client_max_body_size 1m;
# }
location / {
root <%= sourceDir %>/dashboard/dist;
root <%= sourceDir %>/webadmin/dist;
index index.html index.htm;
}
<% } else if ( endpoint === 'app' ) { %>
proxy_pass http://127.0.0.1:<%= port %>;
<% } else if ( endpoint === 'redirect' ) { %>
# redirect everything to the app. this is temporary because there is no way
# to clear a permanent redirect on the browser
return 302 https://<%= redirectTo %>$request_uri;
<% } else if ( endpoint === 'splash' ) { %>
root <%= sourceDir %>;
error_page 503 /update.html;
location /update.html {
add_header Cache-Control no-cache;
}
location /theme.css {
add_header Cache-Control no-cache;
}
location /3rdparty/ {
add_header Cache-Control no-cache;
}
location /js/ {
add_header Cache-Control no-cache;
}
location /progress.json {
add_header Cache-Control no-cache;
}
location /api/v1/cloudron/progress {
add_header Cache-Control no-cache;
default_type application/json;
alias <%= sourceDir %>/progress.json;
}
location / {
return 503;
}
<% } %>
}
}
+8 -23
View File
@@ -1,17 +1,11 @@
# sudo logging breaks journalctl output with very long urls (systemd bug)
Defaults !syslog
Defaults!/home/yellowtent/box/src/scripts/clearvolume.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/clearvolume.sh
Defaults!/home/yellowtent/box/src/scripts/createappdir.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/createappdir.sh
Defaults!/home/yellowtent/box/src/scripts/mvvolume.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/mvvolume.sh
Defaults!/home/yellowtent/box/src/scripts/mkdirvolume.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/mkdirvolume.sh
Defaults!/home/yellowtent/box/src/scripts/rmaddondir.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmaddondir.sh
Defaults!/home/yellowtent/box/src/scripts/rmappdir.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmappdir.sh
Defaults!/home/yellowtent/box/src/scripts/reloadnginx.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reloadnginx.sh
@@ -31,21 +25,12 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/retire.sh
Defaults!/home/yellowtent/box/src/scripts/update.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/update.sh
Defaults!/home/yellowtent/box/src/scripts/remotesupport.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/remotesupport.sh
Defaults!/home/yellowtent/box/src/scripts/authorized_keys.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/authorized_keys.sh
Defaults!/home/yellowtent/box/src/scripts/configurelogrotate.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/configurelogrotate.sh
Defaults!/home/yellowtent/box/src/scripts/backupupload.js env_keep="HOME BOX_ENV"
Defaults!/home/yellowtent/box/src/scripts/backupupload.js closefrom_override
yellowtent ALL=(root) NOPASSWD:SETENV: /home/yellowtent/box/src/scripts/backupupload.js
Defaults!/home/yellowtent/box/src/backuptask.js env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD:SETENV: /home/yellowtent/box/src/backuptask.js
Defaults!/home/yellowtent/box/src/scripts/restart.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restart.sh
Defaults!/home/yellowtent/box/src/scripts/restartdocker.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartdocker.sh
Defaults!/home/yellowtent/box/src/scripts/restartunbound.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartunbound.sh
+2 -6
View File
@@ -12,17 +12,13 @@ Wants=cloudron-resize-fs.service
Type=idle
WorkingDirectory=/home/yellowtent/box
Restart=always
; Systemd does not append logs when logging to files, we spawn a shell first and exec to replace it after setting up the pipes
ExecStart=/bin/sh -c 'echo "Logging to /home/yellowtent/platformdata/logs/box.log"; exec /usr/bin/node --max_old_space_size=150 /home/yellowtent/box/box.js >> /home/yellowtent/platformdata/logs/box.log 2>&1'
ExecStart=/usr/bin/node --max_old_space_size=150 /home/yellowtent/box/box.js
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
; kill apptask processes as well
KillMode=control-group
; Do not kill this process on OOM. Children inherit this score. Do not set it to -1000 so that MemoryMax can keep working
OOMScoreAdjust=-999
User=yellowtent
Group=yellowtent
; OOM killer is invoked in this unit beyond this. The start script replaces this with MemoryLimit for Ubuntu 16
MemoryMax=400M
MemoryLimit=200M
TimeoutStopSec=5s
StartLimitInterval=1
StartLimitBurst=60
@@ -1,14 +0,0 @@
[Unit]
Description=Cloudron Syslog
After=network.target
[Service]
ExecStart=/usr/local/cloudron-syslog/bin/cloudron-syslog --port 2514 --logdir /home/yellowtent/platformdata/logs
WorkingDirectory=/usr/local/cloudron-syslog
Environment="NODE_ENV=production"
Restart=always
User=yellowtent
Group=yellowtent
[Install]
WantedBy=multi-user.target
-143
View File
@@ -1,143 +0,0 @@
'use strict';
exports = module.exports = {
SCOPE_APPS_READ: 'apps:read',
SCOPE_APPS_MANAGE: 'apps:manage',
SCOPE_CLIENTS: 'clients',
SCOPE_CLOUDRON: 'cloudron',
SCOPE_DOMAINS_READ: 'domains:read',
SCOPE_DOMAINS_MANAGE: 'domains:manage',
SCOPE_MAIL: 'mail',
SCOPE_PROFILE: 'profile',
SCOPE_SETTINGS: 'settings',
SCOPE_USERS_READ: 'users:read',
SCOPE_USERS_MANAGE: 'users:manage',
SCOPE_APPSTORE: 'appstore',
VALID_SCOPES: [ 'apps', 'appstore', 'clients', 'cloudron', 'domains', 'mail', 'profile', 'settings', 'users' ], // keep this sorted
SCOPE_ANY: '*',
validateScopeString: validateScopeString,
hasScopes: hasScopes,
canonicalScopeString: canonicalScopeString,
intersectScopes: intersectScopes,
validateToken: validateToken,
scopesForUser: scopesForUser
};
var assert = require('assert'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:accesscontrol'),
tokendb = require('./tokendb.js'),
users = require('./users.js'),
UsersError = users.UsersError,
_ = require('underscore');
// returns scopes that does not have wildcards and is sorted
function canonicalScopeString(scope) {
if (scope === exports.SCOPE_ANY) return exports.VALID_SCOPES.join(',');
return scope.split(',').sort().join(',');
}
function intersectScopes(allowedScopes, wantedScopes) {
assert(Array.isArray(allowedScopes), 'Expecting sorted array');
assert(Array.isArray(wantedScopes), 'Expecting sorted array');
if (_.isEqual(allowedScopes, wantedScopes)) return allowedScopes; // quick path
let wantedScopesMap = new Map();
let results = [];
// make a map of scope -> [ subscopes ]
for (let w of wantedScopes) {
let parts = w.split(':');
let subscopes = wantedScopesMap.get(parts[0]) || new Set();
subscopes.add(parts[1] || '*');
wantedScopesMap.set(parts[0], subscopes);
}
for (let a of allowedScopes) {
let parts = a.split(':');
let as = parts[1] || '*';
let subscopes = wantedScopesMap.get(parts[0]);
if (!subscopes) continue;
if (subscopes.has('*') || subscopes.has(as)) {
results.push(a);
} else if (as === '*') {
results = results.concat(Array.from(subscopes).map(function (ss) { return `${a}:${ss}`; }));
}
}
return results;
}
function validateScopeString(scope) {
assert.strictEqual(typeof scope, 'string');
if (scope === '') return new Error('Empty scope not allowed');
// NOTE: this function intentionally does not allow '*'. This is only allowed in the db to allow
// us not write a migration script every time we add a new scope
var allValid = scope.split(',').every(function (s) { return exports.VALID_SCOPES.indexOf(s.split(':')[0]) !== -1; });
if (!allValid) return new Error('Invalid scope. Available scopes are ' + exports.VALID_SCOPES.join(', '));
return null;
}
// tests if all requiredScopes are attached to the request
function hasScopes(authorizedScopes, requiredScopes) {
assert(Array.isArray(authorizedScopes), 'Expecting array');
assert(Array.isArray(requiredScopes), 'Expecting array');
if (authorizedScopes.indexOf(exports.SCOPE_ANY) !== -1) return null;
for (var i = 0; i < requiredScopes.length; ++i) {
const scopeParts = requiredScopes[i].split(':');
// this allows apps:write if the token has a higher apps scope
if (authorizedScopes.indexOf(requiredScopes[i]) === -1 && authorizedScopes.indexOf(scopeParts[0]) === -1) {
debug('scope: missing scope "%s".', requiredScopes[i]);
return new Error('Missing required scope "' + requiredScopes[i] + '"');
}
}
return null;
}
function scopesForUser(user, callback) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof callback, 'function');
if (user.admin) return callback(null, exports.VALID_SCOPES);
callback(null, config.isSpacesEnabled() ? [ 'profile', 'apps', 'domains:read', 'users:read' ] : [ 'profile', 'apps:read' ]);
}
function validateToken(accessToken, callback) {
assert.strictEqual(typeof accessToken, 'string');
assert.strictEqual(typeof callback, 'function');
tokendb.get(accessToken, function (error, token) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
if (error) return callback(error); // this triggers 'internal error' in passport
users.get(token.identifier, function (error, user) {
if (error && error.reason === UsersError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
if (error) return callback(error);
scopesForUser(user, function (error, userScopes) {
if (error) return callback(error);
var authorizedScopes = intersectScopes(userScopes, token.scope.split(','));
const skipPasswordVerification = token.clientId === 'cid-sdk' || token.clientId === 'cid-cli'; // these clients do not require password checks unlike UI
var info = { authorizedScopes: authorizedScopes, skipPasswordVerification: skipPasswordVerification }; // ends up in req.authInfo
callback(null, user, info);
});
});
});
}
+191 -1197
View File
File diff suppressed because it is too large Load Diff
+47 -216
View File
@@ -18,16 +18,12 @@ exports = module.exports = {
getAddonConfigByName: getAddonConfigByName,
unsetAddonConfig: unsetAddonConfig,
unsetAddonConfigByAppId: unsetAddonConfigByAppId,
getAppIdByAddonConfigValue: getAppIdByAddonConfigValue,
setHealth: setHealth,
setInstallationCommand: setInstallationCommand,
setRunCommand: setRunCommand,
getAppStoreIds: getAppStoreIds,
setOwner: setOwner,
transferOwnership: transferOwnership,
// installation codes (keep in sync in UI)
ISTATE_PENDING_INSTALL: 'pending_install', // installs and fresh reinstalls
ISTATE_PENDING_CLONE: 'pending_clone', // clone
@@ -43,7 +39,7 @@ exports = module.exports = {
RSTATE_RUNNING: 'running',
RSTATE_PENDING_START: 'pending_start',
RSTATE_PENDING_STOP: 'pending_stop',
RSTATE_STOPPED: 'stopped', // app stopped by us
RSTATE_STOPPED: 'stopped', // app stopped by use
// run codes (keep in sync in UI)
HEALTH_HEALTHY: 'healthy',
@@ -51,10 +47,6 @@ exports = module.exports = {
HEALTH_ERROR: 'error',
HEALTH_DEAD: 'dead',
// subdomain table types
SUBDOMAIN_TYPE_PRIMARY: 'primary',
SUBDOMAIN_TYPE_REDIRECT: 'redirect',
_clear: clear
};
@@ -62,19 +54,16 @@ var assert = require('assert'),
async = require('async'),
database = require('./database.js'),
DatabaseError = require('./databaseerror'),
mailboxdb = require('./mailboxdb.js'),
safe = require('safetydance'),
util = require('util');
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.installationProgress', 'apps.runState',
'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.xFrameOptions', '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(',');
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'apps.location', 'apps.dnsRecordId',
'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit', 'apps.altDomain',
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(',');
const SUBDOMAIN_FIELDS = [ 'appId', 'domain', 'subdomain', 'type' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
function postProcess(result) {
assert.strictEqual(typeof result, 'object');
@@ -99,16 +88,14 @@ function postProcess(result) {
assert(result.environmentVariables === null || typeof result.environmentVariables === 'string');
result.portBindings = { };
let hostPorts = result.hostPorts === null ? [ ] : result.hostPorts.split(',');
let environmentVariables = result.environmentVariables === null ? [ ] : result.environmentVariables.split(',');
let portTypes = result.portTypes === null ? [ ] : result.portTypes.split(',');
var hostPorts = result.hostPorts === null ? [ ] : result.hostPorts.split(',');
var environmentVariables = result.environmentVariables === null ? [ ] : result.environmentVariables.split(',');
delete result.hostPorts;
delete result.environmentVariables;
delete result.portTypes;
for (let i = 0; i < environmentVariables.length; i++) {
result.portBindings[environmentVariables[i]] = { hostPort: parseInt(hostPorts[i], 10), type: portTypes[i] };
for (var i = 0; i < environmentVariables.length; i++) {
result.portBindings[environmentVariables[i]] = parseInt(hostPorts[i], 10);
}
assert(result.accessRestrictionJson === null || typeof result.accessRestrictionJson === 'string');
@@ -121,28 +108,10 @@ function postProcess(result) {
result.sso = !!result.sso; // make it bool
result.enableBackup = !!result.enableBackup; // make it bool
result.enableAutomaticUpdate = !!result.enableAutomaticUpdate; // make it bool
assert(result.debugModeJson === null || typeof result.debugModeJson === 'string');
result.debugMode = safe.JSON.parse(result.debugModeJson);
delete result.debugModeJson;
result.alternateDomains = result.alternateDomains || [];
result.alternateDomains.forEach(function (d) {
delete d.appId;
delete d.type;
});
let envNames = JSON.parse(result.envNames), envValues = JSON.parse(result.envValues);
delete result.envNames;
delete result.envValues;
result.env = {};
for (let i = 0; i < envNames.length; i++) { // NOTE: envNames is [ null ] when env of an app is empty
if (envNames[i]) result.env[envNames[i]] = envValues[i];
}
// in the db, we store dataDir as unique/nullable
result.dataDir = result.dataDir || '';
}
function get(id, callback) {
@@ -150,25 +119,14 @@ function get(id, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes, '
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' WHERE apps.id = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, id ], function (error, result) {
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId WHERE apps.id = ? GROUP BY apps.id', [ id ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
postProcess(result[0]);
result[0].alternateDomains = alternateDomains;
postProcess(result[0]);
callback(null, result[0]);
});
callback(null, result[0]);
});
}
@@ -177,24 +135,14 @@ function getByHttpPort(httpPort, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' WHERE httpPort = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, httpPort ], function (error, result) {
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId WHERE httpPort = ? GROUP BY apps.id', [ httpPort ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
postProcess(result[0]);
result[0].alternateDomains = alternateDomains;
postProcess(result[0]);
callback(null, result[0]);
});
callback(null, result[0]);
});
}
@@ -203,24 +151,14 @@ function getByContainerId(containerId, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' WHERE containerId = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, containerId ], function (error, result) {
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId WHERE containerId = ? GROUP BY apps.id', [ containerId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
postProcess(result[0]);
result[0].alternateDomains = alternateDomains;
postProcess(result[0]);
callback(null, result[0]);
});
callback(null, result[0]);
});
}
@@ -228,41 +166,23 @@ function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' GROUP BY apps.id ORDER BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY ], function (error, results) {
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' GROUP BY apps.id ORDER BY apps.id', function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE type = ?', [ exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(postProcess);
alternateDomains.forEach(function (d) {
var domain = results.find(function (a) { return d.appId === a.id; });
if (!domain) return;
domain.alternateDomains = domain.alternateDomains || [];
domain.alternateDomains.push(d);
});
results.forEach(postProcess);
callback(null, results);
});
callback(null, results);
});
}
function add(id, appStoreId, manifest, location, domain, ownerId, portBindings, data, callback) {
function add(id, appStoreId, manifest, location, portBindings, data, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof appStoreId, 'string');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof manifest.version, 'string');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof portBindings, 'object');
assert(data && typeof data === 'object');
assert.strictEqual(typeof callback, 'function');
@@ -274,54 +194,36 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
var accessRestriction = data.accessRestriction || null;
var accessRestrictionJson = JSON.stringify(accessRestriction);
var memoryLimit = data.memoryLimit || 0;
var altDomain = data.altDomain || null;
var xFrameOptions = data.xFrameOptions || '';
var installationState = data.installationState || exports.ISTATE_PENDING_INSTALL;
var restoreConfigJson = data.restoreConfig ? JSON.stringify(data.restoreConfig) : null; // used when cloning
var sso = 'sso' in data ? data.sso : null;
var robotsTxt = 'robotsTxt' in data ? data.robotsTxt : null;
var debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null;
var env = data.env || {};
const mailboxName = data.mailboxName || null;
var queries = [];
queries.push({
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId, mailboxName) ' +
' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId, mailboxName ]
});
queries.push({
query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)',
args: [ id, domain, location, exports.SUBDOMAIN_TYPE_PRIMARY ]
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, restoreConfigJson, sso, debugModeJson) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, restoreConfigJson, sso, debugModeJson ]
});
Object.keys(portBindings).forEach(function (env) {
queries.push({
query: 'INSERT INTO appPortBindings (environmentVariable, hostPort, type, appId) VALUES (?, ?, ?, ?)',
args: [ env, portBindings[env].hostPort, portBindings[env].type, id ]
query: 'INSERT INTO appPortBindings (environmentVariable, hostPort, appId) VALUES (?, ?, ?)',
args: [ env, portBindings[env], id ]
});
});
Object.keys(env).forEach(function (name) {
// only allocate a mailbox if mailboxName is set
if (data.mailboxName) {
queries.push({
query: 'INSERT INTO appEnvVars (appId, name, value) VALUES (?, ?, ?)',
args: [ id, name, env[name] ]
});
});
if (data.alternateDomains) {
data.alternateDomains.forEach(function (d) {
queries.push({
query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)',
args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_REDIRECT ]
});
query: 'INSERT INTO mailboxes (name, ownerId, ownerType) VALUES (?, ?, ?)',
args: [ data.mailboxName, id, mailboxdb.TYPE_APP ]
});
}
database.transaction(queries, function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error.message));
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));
callback(null);
@@ -348,19 +250,18 @@ function getPortBindings(id, callback) {
var portBindings = { };
for (var i = 0; i < results.length; i++) {
portBindings[results[i].environmentVariable] = { hostPort: results[i].hostPort, type: results[i].type };
portBindings[results[i].environmentVariable] = results[i].hostPort;
}
callback(null, portBindings);
});
}
function delPortBinding(hostPort, type, callback) {
function delPortBinding(hostPort, callback) {
assert.strictEqual(typeof hostPort, 'number');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM appPortBindings WHERE hostPort=? AND type=?', [ hostPort, type ], function (error, result) {
database.query('DELETE FROM appPortBindings WHERE hostPort=?', [ hostPort ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
@@ -373,15 +274,14 @@ function del(id, callback) {
assert.strictEqual(typeof callback, 'function');
var queries = [
{ query: 'DELETE FROM subdomains WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM mailboxes WHERE ownerId=?', args: [ id ] },
{ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM appEnvVars WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM apps WHERE id = ?', args: [ id ] }
];
database.transaction(queries, function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results[3].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (results[2].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null);
});
@@ -391,10 +291,8 @@ function clear(callback) {
assert.strictEqual(typeof callback, 'function');
async.series([
database.query.bind(null, 'DELETE FROM subdomains'),
database.query.bind(null, 'DELETE FROM appPortBindings'),
database.query.bind(null, 'DELETE FROM appAddonConfigs'),
database.query.bind(null, 'DELETE FROM appEnvVars'),
database.query.bind(null, 'DELETE FROM apps')
], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -413,8 +311,6 @@ function updateWithConstraints(id, app, constraints, callback) {
assert.strictEqual(typeof callback, 'function');
assert(!('portBindings' in app) || typeof app.portBindings === 'object');
assert(!('accessRestriction' in app) || typeof app.accessRestriction === 'object' || app.accessRestriction === '');
assert(!('alternateDomains' in app) || Array.isArray(app.alternateDomains));
assert(!('env' in app) || typeof app.env === 'object');
var queries = [ ];
@@ -423,30 +319,8 @@ function updateWithConstraints(id, app, constraints, callback) {
// replace entries by app id
queries.push({ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] });
Object.keys(portBindings).forEach(function (env) {
var values = [ portBindings[env].hostPort, portBindings[env].type, env, id ];
queries.push({ query: 'INSERT INTO appPortBindings (hostPort, type, environmentVariable, appId) VALUES(?, ?, ?, ?)', args: values });
});
}
if ('env' in app) {
queries.push({ query: 'DELETE FROM appEnvVars WHERE appId = ?', args: [ id ] });
Object.keys(app.env).forEach(function (name) {
queries.push({
query: 'INSERT INTO appEnvVars (appId, name, value) VALUES (?, ?, ?)',
args: [ id, name, app.env[name] ]
});
});
}
if ('location' in app && 'domain' in app) { // must be updated together as they are unique together
queries.push({ query: 'UPDATE subdomains SET subdomain = ?, domain = ? WHERE appId = ? AND type = ?', args: [ app.location, app.domain, id, exports.SUBDOMAIN_TYPE_PRIMARY ]});
}
if ('alternateDomains' in app) {
queries.push({ query: 'DELETE FROM subdomains WHERE appId = ? AND type = ?', args: [ id, exports.SUBDOMAIN_TYPE_REDIRECT ]});
app.alternateDomains.forEach(function (d) {
queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_REDIRECT ]});
var values = [ portBindings[env], env, id ];
queries.push({ query: 'INSERT INTO appPortBindings (hostPort, environmentVariable, appId) VALUES(?, ?, ?)', args: values });
});
}
@@ -455,7 +329,7 @@ function updateWithConstraints(id, app, constraints, callback) {
if (p === 'manifest' || p === 'oldConfig' || p === 'updateConfig' || p === 'restoreConfig' || p === 'accessRestriction' || p === 'debugMode') {
fields.push(`${p}Json = ?`);
values.push(JSON.stringify(app[p]));
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'env') {
} else if (p !== 'portBindings') {
fields.push(p + ' = ?');
values.push(app[p]);
}
@@ -476,13 +350,12 @@ function updateWithConstraints(id, app, constraints, callback) {
}
// not sure if health should influence runState
function setHealth(appId, health, healthTime, callback) {
function setHealth(appId, health, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof health, 'string');
assert(util.isDate(healthTime));
assert.strictEqual(typeof callback, 'function');
var values = { health, healthTime };
var values = { health: health };
var constraints = 'AND runState NOT LIKE "pending_%" AND installationState = "installed"';
@@ -615,20 +488,6 @@ function getAddonConfigByAppId(appId, callback) {
});
}
function getAppIdByAddonConfigValue(addonId, name, value, callback) {
assert.strictEqual(typeof addonId, 'string');
assert.strictEqual(typeof name, '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) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null, results[0].appId);
});
}
function getAddonConfigByName(appId, addonId, name, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof addonId, 'string');
@@ -642,31 +501,3 @@ function getAddonConfigByName(appId, addonId, name, callback) {
callback(null, results[0].value);
});
}
function setOwner(appId, ownerId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('UPDATE apps SET ownerId=? WHERE appId=?', [ ownerId, appId ], function (error, results) {
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such user'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such app'));
callback(null);
});
}
function transferOwnership(oldOwnerId, newOwnerId, callback) {
assert.strictEqual(typeof oldOwnerId, 'string');
assert.strictEqual(typeof newOwnerId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('UPDATE apps SET ownerId=? WHERE ownerId=?', [ newOwnerId, oldOwnerId ], function (error) {
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such user'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
+101 -96
View File
@@ -7,26 +7,29 @@ var appdb = require('./appdb.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:apphealthmonitor'),
docker = require('./docker.js').connection,
eventlog = require('./eventlog.js'),
mailer = require('./mailer.js'),
superagent = require('superagent'),
util = require('util');
exports = module.exports = {
run: run
start: start,
stop: stop
};
const HEALTHCHECK_INTERVAL = 10 * 1000; // every 10 seconds. this needs to be small since the UI makes only healthy apps clickable
const UNHEALTHY_THRESHOLD = 10 * 60 * 1000; // 10 minutes
const OOM_MAIL_LIMIT = 60 * 60 * 1000; // 60 minutes
let gLastOomMailTime = Date.now() - (5 * 60 * 1000); // pretend we sent email 5 minutes ago
const AUDIT_SOURCE = { userId: null, username: 'healthmonitor' };
var HEALTHCHECK_INTERVAL = 10 * 1000; // every 10 seconds. this needs to be small since the UI makes only healthy apps clickable
var UNHEALTHY_THRESHOLD = 10 * 60 * 1000; // 10 minutes
var gHealthInfo = { }; // { time, emailSent }
var gRunTimeout = null;
var gDockerEventStream = null;
function debugApp(app) {
assert(typeof app === 'object');
assert(!app || typeof app === 'object');
debug(app.fqdn + ' ' + app.manifest.id + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)) + ' - ' + app.id);
var prefix = app ? (app.location || 'naked_domain') : '(no app)';
var manifestAppId = app ? app.manifest.id : '';
var id = app ? app.id : '';
debug(prefix + ' ' + manifestAppId + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)) + ' - ' + id);
}
function setHealth(app, health, callback) {
@@ -34,29 +37,27 @@ function setHealth(app, health, callback) {
assert.strictEqual(typeof health, 'string');
assert.strictEqual(typeof callback, 'function');
let now = new Date(), healthTime = app.healthTime, curHealth = app.health;
var now = new Date();
if (!(app.id in gHealthInfo)) { // add new apps to list
gHealthInfo[app.id] = { time: now, emailSent: false };
}
if (health === appdb.HEALTH_HEALTHY) {
healthTime = now;
if (curHealth && curHealth !== appdb.HEALTH_HEALTHY) { // app starts out with null health
debugApp(app, 'app switched from %s to healthy', curHealth);
gHealthInfo[app.id].time = now;
} else if (Math.abs(now - gHealthInfo[app.id].time) > UNHEALTHY_THRESHOLD) {
if (gHealthInfo[app.id].emailSent) return callback(null);
// do not send mails for dev apps
if (!app.debugMode) eventlog.add(eventlog.ACTION_APP_UP, AUDIT_SOURCE, { app: app });
}
} else if (Math.abs(now - healthTime) > UNHEALTHY_THRESHOLD) {
if (curHealth === appdb.HEALTH_HEALTHY) {
debugApp(app, 'marking as unhealthy since not seen for more than %s minutes', UNHEALTHY_THRESHOLD/(60 * 1000));
debugApp(app, 'marking as unhealthy since not seen for more than %s minutes', UNHEALTHY_THRESHOLD/(60 * 1000));
// do not send mails for dev apps
if (!app.debugMode) eventlog.add(eventlog.ACTION_APP_DOWN, AUDIT_SOURCE, { app: app });
}
if (!app.debugMode) mailer.appDied(app); // do not send mails for dev apps
gHealthInfo[app.id].emailSent = true;
} else {
debugApp(app, 'waiting for %s seconds to update the app health', (UNHEALTHY_THRESHOLD - Math.abs(now - healthTime))/1000);
debugApp(app, 'waiting for sometime to update the app health');
return callback(null);
}
appdb.setHealth(app.id, health, healthTime, function (error) {
appdb.setHealth(app.id, health, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null); // app uninstalled?
if (error) return callback(error);
@@ -69,9 +70,6 @@ function setHealth(app, health, callback) {
// callback is called with error for fatal errors and not if health check failed
function checkAppHealth(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
debugApp(app, 'skipped. istate:%s rstate:%s', app.installationState, app.runState);
return callback(null);
@@ -91,9 +89,6 @@ function checkAppHealth(app, callback) {
return setHealth(app, appdb.HEALTH_DEAD, callback);
}
// non-appstore apps may not have healthCheckPath
if (!manifest.healthCheckPath) return setHealth(app, appdb.HEALTH_HEALTHY, callback);
// poll through docker network instead of nginx to bypass any potential oauth proxy
var healthCheckUrl = 'http://127.0.0.1:' + app.httpPort + manifest.healthCheckPath;
superagent
@@ -116,60 +111,7 @@ function checkAppHealth(app, callback) {
});
}
/*
OOM can be tested using stress tool like so:
docker run -ti -m 100M cloudron/base:0.10.0 /bin/bash
apt-get update && apt-get install stress
stress --vm 1 --vm-bytes 200M --vm-hang 0
*/
function processDockerEvents(intervalSecs, callback) {
assert.strictEqual(typeof intervalSecs, 'number');
assert.strictEqual(typeof callback, 'function');
const since = ((new Date().getTime() / 1000) - intervalSecs).toFixed(0);
const until = ((new Date().getTime() / 1000) - 1).toFixed(0);
docker.getEvents({ since: since, until: until, filters: JSON.stringify({ event: [ 'oom' ] }) }, function (error, stream) {
if (error) return callback(error);
stream.setEncoding('utf8');
stream.on('data', function (data) {
var ev = JSON.parse(data);
var containerId = ev.id;
appdb.getByContainerId(containerId, function (error, app) { // this can error for addons
var program = error || !app.id ? containerId : `app-${app.id}`;
var now = Date.now();
const notifyUser = (!app || !app.debugMode) && (now - gLastOomMailTime > OOM_MAIL_LIMIT);
debug('OOM %s notifyUser: %s. lastOomTime: %s (now: %s)', program, notifyUser, gLastOomMailTime, now, ev);
// do not send mails for dev apps
if (notifyUser) {
// app can be null for addon containers
eventlog.add(eventlog.ACTION_APP_OOM, AUDIT_SOURCE, { ev: ev, containerId: containerId, app: app || null });
gLastOomMailTime = now;
}
});
});
stream.on('error', function (error) {
debug('Error reading docker events', error);
callback();
});
stream.on('end', callback);
// safety hatch if 'until' doesn't work (there are cases where docker is working with a different time)
setTimeout(stream.destroy.bind(stream), 3000); // https://github.com/apocas/dockerode/issues/179
});
}
function processApp(callback) {
assert.strictEqual(typeof callback, 'function');
function processApps(callback) {
apps.getAll(function (error, result) {
if (error) return callback(error);
@@ -187,16 +129,79 @@ function processApp(callback) {
});
}
function run(intervalSecs, callback) {
assert.strictEqual(typeof intervalSecs, 'number');
assert.strictEqual(typeof callback, 'function');
function run() {
processApps(function (error) {
if (error) console.error(error);
async.series([
processApp, // this is first because docker.getEvents seems to get 'stuck' sometimes
processDockerEvents.bind(null, intervalSecs)
], function (error) {
if (error) debug(error);
callback();
gRunTimeout = setTimeout(run, HEALTHCHECK_INTERVAL);
});
}
/*
OOM can be tested using stress tool like so:
docker run -ti -m 100M cloudron/base:0.10.0 /bin/bash
apt-get update && apt-get install stress
stress --vm 1 --vm-bytes 200M --vm-hang 0
*/
function processDockerEvents() {
// note that for some reason, the callback is called only on the first event
debug('Listening for docker events');
const OOM_MAIL_LIMIT = 60 * 60 * 1000; // 60 minutes
var lastOomMailTime = new Date(new Date() - OOM_MAIL_LIMIT);
docker.getEvents({ filters: JSON.stringify({ event: [ 'oom' ] }) }, function (error, stream) {
if (error) return console.error(error);
gDockerEventStream = stream;
stream.setEncoding('utf8');
stream.on('data', function (data) {
var ev = JSON.parse(data);
appdb.getByContainerId(ev.id, function (error, app) { // this can error for addons
var program = error || !app.appStoreId ? ev.id : app.appStoreId;
var context = JSON.stringify(ev);
var now = new Date();
if (app) context = context + '\n\n' + JSON.stringify(app, null, 4) + '\n';
debug('OOM Context: %s', context);
// do not send mails for dev apps
if ((!app || !app.debugMode) && (now - lastOomMailTime > OOM_MAIL_LIMIT)) {
mailer.oomEvent(program, context); // app can be null if it's an addon crash
lastOomMailTime = now;
}
});
});
stream.on('error', function (error) {
console.error('Error reading docker events', error);
gDockerEventStream = null; // TODO: reconnect?
});
stream.on('end', function () {
console.error('Docker event stream ended');
gDockerEventStream = null; // TODO: reconnect?
});
});
}
function start(callback) {
assert.strictEqual(typeof callback, 'function');
debug('Starting apphealthmonitor');
processDockerEvents();
run();
callback();
}
function stop(callback) {
assert.strictEqual(typeof callback, 'function');
clearTimeout(gRunTimeout);
if (gDockerEventStream) gDockerEventStream.end();
callback();
}
+238 -511
View File
File diff suppressed because it is too large Load Diff
+91 -137
View File
@@ -5,7 +5,6 @@ exports = module.exports = {
unpurchase: unpurchase,
getSubscription: getSubscription,
isFreePlan: isFreePlan,
sendAliveStatus: sendAliveStatus,
@@ -19,17 +18,11 @@ exports = module.exports = {
AppstoreError: AppstoreError
};
var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
var assert = require('assert'),
config = require('./config.js'),
debug = require('debug')('box:appstore'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
mail = require('./mail.js'),
os = require('os'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
superagent = require('superagent'),
util = require('util');
@@ -63,12 +56,23 @@ var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function getAppstoreConfig(callback) {
assert.strictEqual(typeof callback, 'function');
settings.getAppstoreConfig(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
if (!result.token) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
// Caas Cloudrons do not store appstore credentials in their local database
if (config.provider() === 'caas') {
var url = config.apiServerOrigin() + '/api/v1/exchangeBoxTokenWithUserToken';
superagent.post(url).query({ token: config.token() }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
callback(null, result);
});
callback(null, result.body);
});
} else {
settings.getAppstoreConfig(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
if (!result.token) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
callback(null, result);
});
}
}
function getSubscription(callback) {
@@ -90,21 +94,18 @@ function getSubscription(callback) {
});
}
function isFreePlan(subscription) {
return !subscription || subscription.plan.id === 'free';
}
// See app.js install it will create a db record first but remove it again if appstore purchase fails
function purchase(appId, data, callback) {
function purchase(appId, appstoreId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof data, 'object');
assert(data.appstoreId || data.manifestId);
assert.strictEqual(typeof appstoreId, 'string');
assert.strictEqual(typeof callback, 'function');
if (appstoreId === '') return callback(null);
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/apps/' + appId;
var data = { appstoreId: appstoreId };
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
@@ -118,12 +119,13 @@ function purchase(appId, data, callback) {
});
}
function unpurchase(appId, data, callback) {
function unpurchase(appId, appstoreId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof data, 'object');
assert(data.appstoreId || data.manifestId);
assert.strictEqual(typeof appstoreId, 'string');
assert.strictEqual(typeof callback, 'function');
if (appstoreId === '') return callback(null);
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
@@ -133,9 +135,9 @@ function unpurchase(appId, data, callback) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
if (result.statusCode === 404) return callback(null); // was never purchased
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
superagent.del(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
superagent.del(url).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
if (result.statusCode !== 204) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
@@ -146,87 +148,66 @@ function unpurchase(appId, data, callback) {
});
}
function sendAliveStatus(callback) {
function sendAliveStatus(data, callback) {
callback = callback || NOOP_CALLBACK;
var allSettings, allDomains, mailDomains, loginEvents;
settings.getAll(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
async.series([
function (callback) {
settings.getAll(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
allSettings = result;
callback();
});
},
function (callback) {
domains.getAll(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
allDomains = result;
callback();
});
},
function (callback) {
mail.getDomains(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
mailDomains = result;
callback();
});
},
function (callback) {
eventlog.getAllPaged([ eventlog.ACTION_USER_LOGIN ], null, 1, 1, function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
loginEvents = result;
callback();
});
}
], function (error) {
if (error) return callback(error);
eventlog.getAllPaged(eventlog.ACTION_USER_LOGIN, null, 1, 1, function (error, loginEvents) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
var backendSettings = {
backupConfig: {
provider: allSettings[settings.BACKUP_CONFIG_KEY].provider,
hardlinks: !allSettings[settings.BACKUP_CONFIG_KEY].noHardlinks
},
domainConfig: {
count: allDomains.length,
domains: Array.from(new Set(allDomains.map(function (d) { return { domain: d.domain, provider: d.provider }; })))
},
mailConfig: {
outboundCount: mailDomains.length,
inboundCount: mailDomains.filter(function (d) { return d.enabled; }).length,
catchAllCount: mailDomains.filter(function (d) { return d.catchAll.length !== 0; }).length,
relayProviders: Array.from(new Set(mailDomains.map(function (d) { return d.relay.provider; })))
},
appAutoupdatePattern: allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY],
boxAutoupdatePattern: allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY],
timeZone: allSettings[settings.TIME_ZONE_KEY],
};
var backendSettings = {
dnsConfig: {
provider: result[settings.DNS_CONFIG_KEY].provider,
wildcard: result[settings.DNS_CONFIG_KEY].provider === 'manual' ? result[settings.DNS_CONFIG_KEY].wildcard : undefined
},
tlsConfig: {
provider: result[settings.TLS_CONFIG_KEY].provider
},
backupConfig: {
provider: result[settings.BACKUP_CONFIG_KEY].provider,
hardlinks: !result[settings.BACKUP_CONFIG_KEY].noHardlinks
},
mailConfig: {
enabled: result[settings.MAIL_CONFIG_KEY].enabled
},
mailRelay: {
provider: result[settings.MAIL_RELAY_KEY].provider
},
mailCatchAll: {
count: result[settings.CATCH_ALL_ADDRESS_KEY].length
},
autoupdatePattern: result[settings.AUTOUPDATE_PATTERN_KEY],
timeZone: result[settings.TIME_ZONE_KEY],
};
var data = {
version: config.version(),
adminFqdn: config.adminFqdn(),
provider: config.provider(),
backendSettings: backendSettings,
machine: {
cpus: os.cpus(),
totalmem: os.totalmem()
},
events: {
lastLogin: loginEvents[0] ? (new Date(loginEvents[0].creationTime).getTime()) : 0
}
};
var data = {
domain: config.fqdn(),
version: config.version(),
adminFqdn: config.adminFqdn(),
provider: config.provider(),
backendSettings: backendSettings,
machine: {
cpus: os.cpus(),
totalmem: os.totalmem()
},
events: {
lastLogin: loginEvents[0] ? (new Date(loginEvents[0].creationTime).getTime()) : 0
}
};
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/alive';
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Sending alive status failed. %s %j', result.status, result.body)));
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/alive';
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Sending alive status failed. %s %j', result.status, result.body)));
callback(null);
callback(null);
});
});
});
});
@@ -241,18 +222,12 @@ function getBoxUpdate(callback) {
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/boxupdate';
superagent.get(url).query({ accessToken: appstoreConfig.token, boxVersion: config.version() }).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 204) return callback(null); // no update
if (result.statusCode !== 200 || !result.body) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
var updateInfo = result.body;
if (!semver.valid(updateInfo.version) || semver.gt(config.version(), updateInfo.version)) {
return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Invalid update version: %s %s', result.statusCode, result.text)));
}
// updateInfo: { version, changelog, upgrade, sourceTarballUrl, sourceTarballSigUrl, boxVersionsUrl, boxVersionsSigUrl }
callback(null, updateInfo);
// { version, changelog, upgrade, sourceTarballUrl}
callback(null, result.body);
});
});
}
@@ -269,21 +244,10 @@ function getAppUpdate(app, callback) {
superagent.get(url).query({ accessToken: appstoreConfig.token, boxVersion: config.version(), appId: app.appStoreId, appVersion: app.manifest.version }).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 204) return callback(null); // no update
if (result.statusCode !== 200 || !result.body) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
const updateInfo = result.body;
// for the appstore, x.y.z is the same as x.y.z-0 but in semver, x.y.z > x.y.z-0
const curAppVersion = semver.prerelease(app.manifest.version) ? app.manifest.version : `${app.manifest.version}-0`;
// do some sanity checks
if (!safe.query(updateInfo, 'manifest.version') || semver.gt(curAppVersion, safe.query(updateInfo, 'manifest.version'))) {
debug('Skipping malformed update of app %s version: %s. got %j', app.id, curAppVersion, updateInfo);
return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Malformed update: %s %s', result.statusCode, result.text)));
}
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
// { id, creationDate, manifest }
callback(null, updateInfo);
callback(null, result.body);
});
});
}
@@ -315,26 +279,16 @@ function sendFeedback(info, callback) {
assert.strictEqual(typeof info.description, 'string');
assert.strictEqual(typeof callback, 'function');
function collectAppInfoIfNeeded(callback) {
if (!info.appId) return callback();
apps.get(info.appId, callback);
}
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
collectAppInfoIfNeeded(function (error, result) {
if (error) console.error('Unable to get app info', error);
if (result) info.app = result;
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/feedback';
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/feedback';
superagent.post(url).query({ accessToken: appstoreConfig.token }).send(info).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
superagent.post(url).query({ accessToken: appstoreConfig.token }).send(info).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
callback(null);
});
callback(null);
});
});
}
+145 -271
View File
@@ -8,55 +8,60 @@ exports = module.exports = {
// exported for testing
_reserveHttpPort: reserveHttpPort,
_configureReverseProxy: configureReverseProxy,
_unconfigureReverseProxy: unconfigureReverseProxy,
_createAppDir: createAppDir,
_deleteAppDir: deleteAppDir,
_configureNginx: configureNginx,
_unconfigureNginx: unconfigureNginx,
_createVolume: createVolume,
_deleteVolume: deleteVolume,
_verifyManifest: verifyManifest,
_registerSubdomain: registerSubdomain,
_unregisterSubdomain: unregisterSubdomain,
_waitForDnsPropagation: waitForDnsPropagation
_waitForDnsPropagation: waitForDnsPropagation,
_waitForAltDomainDnsPropagation: waitForAltDomainDnsPropagation
};
require('supererror')({ splatchError: true });
// remove timestamp from debug() based output
require('debug').formatArgs = function formatArgs(args) {
args[0] = this.namespace + ' ' + args[0];
};
var addons = require('./addons.js'),
appdb = require('./appdb.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
certificates = require('./certificates.js'),
config = require('./config.js'),
database = require('./database.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:apptask'),
docker = require('./docker.js'),
domains = require('./domains.js'),
DomainsError = domains.DomainsError,
ejs = require('ejs'),
fs = require('fs'),
manifestFormat = require('cloudron-manifestformat'),
mkdirp = require('mkdirp'),
net = require('net'),
nginx = require('./nginx.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
rimraf = require('rimraf'),
safe = require('safetydance'),
shell = require('./shell.js'),
SubdomainError = require('./subdomains.js').SubdomainError,
subdomains = require('./subdomains.js'),
superagent = require('superagent'),
sysinfo = require('./sysinfo.js'),
tld = require('tldjs'),
util = require('util'),
_ = require('underscore');
var COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd.config.ejs', { encoding: 'utf8' }),
CONFIGURE_COLLECTD_CMD = path.join(__dirname, 'scripts/configurecollectd.sh'),
MV_VOLUME_CMD = path.join(__dirname, 'scripts/mvvolume.sh'),
LOGROTATE_CONFIG_EJS = fs.readFileSync(__dirname + '/logrotate.ejs', { encoding: 'utf8' }),
CONFIGURE_LOGROTATE_CMD = path.join(__dirname, 'scripts/configurelogrotate.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
CONFIGURE_LOGROTATE_CMD = path.join(__dirname, 'scripts/configurelogrotate.sh'),
RMAPPDIR_CMD = path.join(__dirname, 'scripts/rmappdir.sh'),
CREATEAPPDIR_CMD = path.join(__dirname, 'scripts/createappdir.sh');
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -67,26 +72,8 @@ function initialize(callback) {
function debugApp(app) {
assert.strictEqual(typeof app, 'object');
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
// updates the app object and the database
function updateApp(app, values, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof values, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'updating app with values: %j', values);
appdb.update(app.id, values, function (error) {
if (error) return callback(error);
for (var value in values) {
app[value] = values[value];
}
return callback(null);
});
var prefix = app ? (app.location || '(bare)') : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function reserveHttpPort(app, callback) {
@@ -107,19 +94,23 @@ function reserveHttpPort(app, callback) {
});
}
function configureReverseProxy(app, callback) {
function configureNginx(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
reverseProxy.configureApp(app, { userId: null, username: 'apptask' }, callback);
certificates.ensureCertificate(app, function (error, certFilePath, keyFilePath) {
if (error) return callback(error);
nginx.configureApp(app, certFilePath, keyFilePath, callback);
});
}
function unconfigureReverseProxy(app, callback) {
function unconfigureNginx(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
// TODO: maybe revoke the cert
reverseProxy.unconfigureApp(app, callback);
nginx.unconfigureApp(app, callback);
}
function createContainer(app, callback) {
@@ -136,62 +127,32 @@ function createContainer(app, callback) {
});
}
function deleteContainers(app, options, callback) {
function deleteContainers(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'deleting app containers (app, scheduler)');
docker.deleteContainers(app.id, options, function (error) {
docker.deleteContainers(app.id, function (error) {
if (error) return callback(new Error('Error deleting container: ' + error));
updateApp(app, { containerId: null }, callback);
});
}
function createAppDir(app, callback) {
function createVolume(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
mkdirp(path.join(paths.APPS_DATA_DIR, app.id), callback);
shell.sudo('createVolume', [ CREATEAPPDIR_CMD, app.id ], callback);
}
function deleteAppDir(app, options, callback) {
function deleteVolume(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
const appDataDir = path.join(paths.APPS_DATA_DIR, app.id);
// resolve any symlinked data dir
const stat = safe.fs.lstatSync(appDataDir);
if (!stat) return callback(null);
const resolvedAppDataDir = stat.isSymbolicLink() ? safe.fs.readlinkSync(appDataDir) : appDataDir;
if (safe.fs.existsSync(resolvedAppDataDir)) {
const entries = safe.fs.readdirSync(resolvedAppDataDir);
if (!entries) return callback(`Error listing ${resolvedAppDataDir}: ${safe.error.message}`);
// remove only files. directories inside app dir are currently volumes managed by the addons
// we cannot delete those dirs anyway because of perms
entries.forEach(function (entry) {
let stat = safe.fs.statSync(path.join(resolvedAppDataDir, entry));
if (stat && !stat.isDirectory()) safe.fs.unlinkSync(path.join(resolvedAppDataDir, entry));
});
}
// if this fails, it's probably because the localstorage/redis addons have not cleaned up properly
if (options.removeDirectory) {
if (stat.isSymbolicLink()) {
if (!safe.fs.unlinkSync(appDataDir)) return callback(safe.error.code === 'ENOENT' ? null : safe.error);
} else {
if (!safe.fs.rmdirSync(appDataDir)) return callback(safe.error.code === 'ENOENT' ? null : safe.error);
}
}
callback(null);
shell.sudo('deleteVolume', [ RMAPPDIR_CMD, app.id, !!options.removeDirectory ], callback);
}
function addCollectdProfile(app, callback) {
@@ -201,7 +162,7 @@ function addCollectdProfile(app, callback) {
var collectdConf = ejs.render(COLLECTD_CONFIG_EJS, { appId: app.id, containerId: app.containerId });
fs.writeFile(path.join(paths.COLLECTD_APPCONFIG_DIR, app.id + '.conf'), collectdConf, function (error) {
if (error) return callback(error);
shell.sudo('addCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'add', app.id ], {}, callback);
shell.sudo('addCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'add', app.id ], callback);
});
}
@@ -211,7 +172,7 @@ function removeCollectdProfile(app, callback) {
fs.unlink(path.join(paths.COLLECTD_APPCONFIG_DIR, app.id + '.conf'), function (error) {
if (error && error.code !== 'ENOENT') debugApp(app, 'Error removing collectd profile', error);
shell.sudo('removeCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'remove', app.id ], {}, callback);
shell.sudo('removeCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'remove', app.id ], callback);
});
}
@@ -230,7 +191,7 @@ function addLogrotateConfig(app, callback) {
var tmpFilePath = path.join(os.tmpdir(), app.id + '.logrotate');
fs.writeFile(tmpFilePath, logrotateConf, function (error) {
if (error) return callback(error);
shell.sudo('addLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'add', app.id, tmpFilePath ], {}, callback);
shell.sudo('addLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'add', app.id, tmpFilePath ], callback);
});
});
}
@@ -239,7 +200,7 @@ function removeLogrotateConfig(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
shell.sudo('removeLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'remove', app.id ], {}, callback);
shell.sudo('removeLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'remove', app.id ], callback);
}
function verifyManifest(manifest, callback) {
@@ -291,128 +252,66 @@ function registerSubdomain(app, overwrite, callback) {
if (error) return callback(error);
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Registering subdomain location [%s] overwrite: %s', app.fqdn, overwrite);
debugApp(app, 'Registering subdomain location [%s] overwrite: %s', app.location, overwrite);
// get the current record before updating it
domains.getDnsRecords(app.location, app.domain, 'A', function (error, values) {
subdomains.get(app.location, 'A', function (error, values) {
if (error) return retryCallback(error);
// refuse to update any existing DNS record for custom domains that we did not create
if (values.length !== 0 && !overwrite) return retryCallback(null, new Error('DNS Record already exists'));
// note that the appstore sets up the naked domain for non-custom domains
if (config.isCustomDomain() && values.length !== 0 && !overwrite) return retryCallback(null, new Error('DNS Record already exists'));
domains.upsertDnsRecords(app.location, app.domain, 'A', [ ip ], function (error) {
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) {
debug('Upsert error. Will retry.', error.message);
return retryCallback(error); // try again
}
subdomains.upsert(app.location, 'A', [ ip ], function (error, changeId) {
if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(null, error);
retryCallback(null, error || changeId);
});
});
}, function (error, result) {
if (error || result instanceof Error) return callback(error || result);
callback(null);
// dnsRecordId tracks whether we created this DNS record so that we can unregister later
updateApp(app, { dnsRecordId: result }, callback);
});
});
}
function unregisterSubdomain(app, location, domain, callback) {
function unregisterSubdomain(app, location, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
// do not unregister bare domain because we show a error/cloudron info page there
if (!config.isCustomDomain() && location === '') {
debugApp(app, 'Skip unregister of empty subdomain');
return callback(null);
}
if (!app.dnsRecordId) {
debugApp(app, 'Skip unregister of record not created by cloudron');
return callback(null);
}
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Unregistering subdomain: %s', app.fqdn);
debugApp(app, 'Unregistering subdomain: %s', location);
domains.removeDnsRecords(location, domain, 'A', [ ip ], function (error) {
if (error && error.reason === DomainsError.NOT_FOUND) return retryCallback(null, null); // domain can be not found if oldConfig.domain or restoreConfig.domain was removed
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) return retryCallback(error); // try again
subdomains.remove(location, 'A', [ ip ], function (error) {
if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(null, error);
});
}, function (error, result) {
if (error || result instanceof Error) return callback(error || result);
callback(null);
updateApp(app, { dnsRecordId: null }, callback);
});
});
}
function registerAlternateDomains(app, overwrite, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof overwrite, 'boolean');
assert.strictEqual(typeof callback, 'function');
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
async.eachSeries(app.alternateDomains, function (domain, callback) {
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Registering alternate subdomain [%s] overwrite: %s', (domain.subdomain ? (domain.subdomain + '.') : '') + domain.domain, overwrite);
// get the current record before updating it
domains.getDnsRecords(domain.subdomain, domain.domain, 'A', function (error, values) {
if (error) return retryCallback(error);
// refuse to update any existing DNS record for custom domains that we did not create
if (values.length !== 0 && !overwrite) return retryCallback(null, new Error('DNS Record already exists'));
domains.upsertDnsRecords(domain.subdomain, domain.domain, 'A', [ ip ], function (error) {
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) {
debug('Upsert error. Will retry.', error.message);
return retryCallback(error); // try again
}
retryCallback(null, error);
});
});
}, function (error, result) {
if (error || result instanceof Error) return callback(error || result);
callback();
});
}, callback);
});
}
function unregisterAlternateDomains(app, all, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof all, 'boolean');
assert.strictEqual(typeof callback, 'function');
let obsoleteDomains = [];
if (all) {
obsoleteDomains = app.alternateDomains;
} else if (app.oldConfig) { // oldConfig can be null during an infra update
obsoleteDomains = app.oldConfig.alternateDomains.filter(function (o) {
return !app.alternateDomains.some(function (n) { return n.subdomain === o.subdomain && n.domain === o.domain; });
});
}
if (obsoleteDomains.length === 0) return callback();
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
async.eachSeries(obsoleteDomains, function (domain, callback) {
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Unregistering subdomain: %s%s', domain.subdomain ? (domain.subdomain + '.') : '', domain.domain);
domains.removeDnsRecords(domain.subdomain, domain.domain, 'A', [ ip ], function (error) {
if (error && error.reason === DomainsError.NOT_FOUND) return retryCallback(null, null);
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(null, error);
});
}, function (error, result) {
if (error || result instanceof Error) return callback(error || result);
callback();
});
}, callback);
});
}
function removeIcon(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -423,18 +322,6 @@ function removeIcon(app, callback) {
});
}
function cleanupLogs(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
// note that redis container logs are cleaned up by the addon
rimraf(path.join(paths.LOG_DIR, app.id), function (error) {
if (error) debugApp(app, 'cannot cleanup logs: %s', error);
callback(null);
});
}
function waitForDnsPropagation(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -447,28 +334,44 @@ function waitForDnsPropagation(app, callback) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
domains.waitForDnsRecord(app.location, app.domain, 'A', ip, { interval: 5000, times: 240 }, function (error) {
if (error) return callback(error);
// now wait for alternateDomains, if any
async.eachSeries(app.alternateDomains, function (domain, iteratorCallback) {
domains.waitForDnsRecord(domain.subdomain, domain.domain, 'A', ip, { interval: 5000, times: 240 }, iteratorCallback);
}, callback);
});
subdomains.waitForDns(config.appFqdn(app.location), ip, 'A', { interval: 5000, times: 120 }, callback);
});
}
function migrateDataDir(app, sourceDir, callback) {
function waitForAltDomainDnsPropagation(app, callback) {
if (!app.altDomain) return callback(null);
// try for 10 minutes before giving up. this allows the user to "reconfigure" the app in the case where
// an app has an external domain and cloudron is migrated to custom domain.
var isNakedDomain = tld.getDomain(app.altDomain) === app.altDomain;
if (isNakedDomain) { // check naked domains with A record since CNAME records don't work there
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
subdomains.waitForDns(app.altDomain, ip, 'A', { interval: 10000, times: 60 }, callback);
});
} else {
subdomains.waitForDns(app.altDomain, config.appFqdn(app.location) + '.', 'CNAME', { interval: 10000, times: 60 }, callback);
}
}
// updates the app object and the database
function updateApp(app, values, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof sourceDir, 'string');
assert.strictEqual(typeof values, 'object');
assert.strictEqual(typeof callback, 'function');
let resolvedSourceDir = apps.getDataDir(app, sourceDir);
let resolvedTargetDir = apps.getDataDir(app, app.dataDir);
debugApp(app, 'updating app with values: %j', values);
debug(`migrateDataDir: migrating data from ${resolvedSourceDir} to ${resolvedTargetDir}`);
appdb.update(app.id, values, function (error) {
if (error) return callback(error);
shell.sudo('migrateDataDir', [ MV_VOLUME_CMD, resolvedSourceDir, resolvedTargetDir ], {}, callback);
for (var value in values) {
app[value] = values[value];
}
return callback(null);
});
}
// Ordering is based on the following rationale:
@@ -495,18 +398,14 @@ function install(app, callback) {
// teardown for re-installs
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
unconfigureReverseProxy.bind(null, app),
unconfigureNginx.bind(null, app),
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
deleteContainers.bind(null, app, { managedOnly: true }),
function teardownAddons(next) {
// when restoring, app does not require these addons anymore. remove carefully to preserve the db passwords
var addonsToRemove = !isRestoring ? app.manifest.addons : _.omit(app.oldConfig.manifest.addons, Object.keys(app.manifest.addons));
addons.teardownAddons(app, addonsToRemove, next);
},
deleteAppDir.bind(null, app, { removeDirectory: false }), // do not remove any symlinked appdata dir
deleteContainers.bind(null, app),
// oldConfig can be null during upgrades
addons.teardownAddons.bind(null, app, app.oldConfig ? app.oldConfig.manifest.addons : app.manifest.addons),
deleteVolume.bind(null, app, { removeDirectory: false }), // do not remove any symlinked volume
// for restore case
function deleteImageIfChanged(done) {
@@ -523,14 +422,11 @@ function install(app, callback) {
updateApp.bind(null, app, { installationProgress: '30, Registering subdomain' }),
registerSubdomain.bind(null, app, isRestoring /* overwrite */),
updateApp.bind(null, app, { installationProgress: '35, Registering alternate domains'}),
registerAlternateDomains.bind(null, app, isRestoring /* overwrite */),
updateApp.bind(null, app, { installationProgress: '40, Downloading image' }),
docker.downloadImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '50, Creating app data directory' }),
createAppDir.bind(null, app),
updateApp.bind(null, app, { installationProgress: '50, Creating volume' }),
createVolume.bind(null, app),
function restoreFromBackup(next) {
if (!restoreConfig) {
@@ -540,10 +436,8 @@ function install(app, callback) {
], next);
} else {
async.series([
updateApp.bind(null, app, { installationProgress: '65, Download backup and restoring addons' }),
addons.setupAddons.bind(null, app, app.manifest.addons),
addons.clearAddons.bind(null, app, app.manifest.addons),
backups.restoreApp.bind(null, app, app.manifest.addons, restoreConfig, (progress) => updateApp(app, { installationProgress: `65, Restore - ${progress.message}` }, NOOP_CALLBACK))
updateApp.bind(null, app, { installationProgress: '60, Download backup and restoring addons' }),
backups.restoreApp.bind(null, app, app.manifest.addons, restoreConfig),
], next);
}
},
@@ -562,8 +456,11 @@ function install(app, callback) {
updateApp.bind(null, app, { installationProgress: '85, Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '95, Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Waiting for External Domain setup' }),
exports._waitForAltDomainDnsPropagation.bind(null, app), // required when restoring and !restoreConfig
updateApp.bind(null, app, { installationProgress: '95, Configure nginx' }),
configureNginx.bind(null, app),
// done!
function (callback) {
@@ -585,7 +482,7 @@ function backup(app, callback) {
async.series([
updateApp.bind(null, app, { installationProgress: '10, Backing up' }),
backups.backupApp.bind(null, app, (progress) => updateApp(app, { installationProgress: `30, ${progress.message}` }, NOOP_CALLBACK)),
backups.backupApp.bind(null, app, app.manifest),
// done!
function (callback) {
@@ -607,21 +504,18 @@ function configure(app, callback) {
assert.strictEqual(typeof callback, 'function');
// oldConfig can be null during an infra update
const locationChanged = app.oldConfig && (app.oldConfig.fqdn !== app.fqdn);
const dataDirChanged = app.oldConfig && (app.oldConfig.dataDir !== app.dataDir);
var locationChanged = app.oldConfig && app.oldConfig.location !== app.location;
async.series([
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
unconfigureReverseProxy.bind(null, app),
unconfigureNginx.bind(null, app),
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
deleteContainers.bind(null, app, { managedOnly: true }),
unregisterAlternateDomains.bind(null, app, false /* all */),
deleteContainers.bind(null, app),
function (next) {
if (!locationChanged) return next();
unregisterSubdomain(app, app.oldConfig.location, app.oldConfig.domain, next);
unregisterSubdomain(app, app.oldConfig.location, next);
},
reserveHttpPort.bind(null, app),
@@ -629,29 +523,19 @@ function configure(app, callback) {
updateApp.bind(null, app, { installationProgress: '20, Downloading icon' }),
downloadIcon.bind(null, app),
updateApp.bind(null, app, { installationProgress: '30, Registering subdomain' }),
updateApp.bind(null, app, { installationProgress: '35, Registering subdomain' }),
registerSubdomain.bind(null, app, !locationChanged /* overwrite */), // if location changed, do not overwrite to detect conflicts
updateApp.bind(null, app, { installationProgress: '35, Registering alternate domains'}),
registerAlternateDomains.bind(null, app, true /* overwrite */), // figure out when to overwrite
updateApp.bind(null, app, { installationProgress: '40, Downloading image' }),
docker.downloadImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '45, Ensuring app data directory' }),
createAppDir.bind(null, app),
updateApp.bind(null, app, { installationProgress: '45, Ensuring volume' }),
createVolume.bind(null, app),
// re-setup addons since they rely on the app's fqdn (e.g oauth)
updateApp.bind(null, app, { installationProgress: '50, Setting up addons' }),
addons.setupAddons.bind(null, app, app.manifest.addons),
// migrate dataDir
function (next) {
if (!dataDirChanged) return next();
migrateDataDir(app, app.oldConfig.dataDir, next);
},
updateApp.bind(null, app, { installationProgress: '60, Creating container' }),
createContainer.bind(null, app),
@@ -666,8 +550,11 @@ function configure(app, callback) {
updateApp.bind(null, app, { installationProgress: '80, Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
updateApp.bind(null, app, { installationProgress: '85, Waiting for External Domain setup' }),
exports._waitForAltDomainDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Configuring Nginx' }),
configureNginx.bind(null, app),
// done!
function (callback) {
@@ -705,11 +592,8 @@ function update(app, callback) {
async.series([
updateApp.bind(null, app, { installationProgress: '15, Backing up app' }),
backups.backupApp.bind(null, app, (progress) => updateApp(app, { installationProgress: `15, Backup - ${progress.message}` }, NOOP_CALLBACK))
], function (error) {
if (error) error.backupError = true;
next(error);
});
backups.backupApp.bind(null, app, app.manifest)
], next);
},
// download new image before app is stopped. this is so we can reduce downtime
@@ -723,7 +607,7 @@ function update(app, callback) {
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
deleteContainers.bind(null, app, { managedOnly: true }),
deleteContainers.bind(null, app),
function deleteImageIfChanged(done) {
if (app.manifest.dockerImage === app.updateConfig.manifest.dockerImage) return done();
@@ -735,14 +619,14 @@ function update(app, callback) {
// free unused ports
function (next) {
const currentPorts = app.portBindings || {};
const newTcpPorts = app.updateConfig.manifest.tcpPorts || {};
const newUdpPorts = app.updateConfig.manifest.udpPorts || {};
// make sure we always have objects
var currentPorts = app.portBindings || {};
var newPorts = app.updateConfig.manifest.tcpPorts || {};
async.each(Object.keys(currentPorts), function (portName, callback) {
if (newTcpPorts[portName] || newUdpPorts[portName]) return callback(); // port still in use
if (newPorts[portName]) return callback(); // port still in use
appdb.delPortBinding(currentPorts[portName], apps.PORT_TYPE_TCP, function (error) {
appdb.delPortBinding(currentPorts[portName], function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) console.error('Portbinding does not exist in database.');
else if (error) return next(error);
@@ -761,7 +645,7 @@ function update(app, callback) {
downloadIcon.bind(null, app),
updateApp.bind(null, app, { installationProgress: '70, Updating addons' }),
addons.setupAddons.bind(null, app, app.updateConfig.manifest.addons),
addons.setupAddons.bind(null, app, app.manifest.addons),
updateApp.bind(null, app, { installationProgress: '80, Creating container' }),
createContainer.bind(null, app),
@@ -777,18 +661,14 @@ function update(app, callback) {
// done!
function (callback) {
debugApp(app, 'updated');
updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '', health: null, updateConfig: null, updateTime: new Date() }, callback);
updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '', health: null, updateConfig: null }, callback);
}
], function seriesDone(error) {
if (error && error.backupError) {
debugApp(app, 'update aborted because backup failed', error);
updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '', health: null, updateConfig: null }, callback.bind(null, error));
} else if (error) {
if (error) {
debugApp(app, 'Error updating app: %s', error);
updateApp(app, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message, updateTime: new Date() }, callback.bind(null, error));
} else {
callback(null);
return updateApp(app, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message }, callback.bind(null, error));
}
callback(null);
});
}
@@ -809,29 +689,25 @@ function uninstall(app, callback) {
stopApp.bind(null, app),
updateApp.bind(null, app, { installationProgress: '20, Deleting container' }),
deleteContainers.bind(null, app, {}),
deleteContainers.bind(null, app),
updateApp.bind(null, app, { installationProgress: '30, Teardown addons' }),
addons.teardownAddons.bind(null, app, app.manifest.addons),
updateApp.bind(null, app, { installationProgress: '40, Deleting app data directory' }),
deleteAppDir.bind(null, app, { removeDirectory: true }),
updateApp.bind(null, app, { installationProgress: '40, Deleting volume' }),
deleteVolume.bind(null, app, { removeDirectory: true }),
updateApp.bind(null, app, { installationProgress: '50, Deleting image' }),
docker.deleteImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '60, Unregistering domains' }),
unregisterAlternateDomains.bind(null, app, true /* all */),
unregisterSubdomain.bind(null, app, app.location, app.domain),
updateApp.bind(null, app, { installationProgress: '60, Unregistering subdomain' }),
unregisterSubdomain.bind(null, app, app.location),
updateApp.bind(null, app, { installationProgress: '70, Cleanup icon' }),
updateApp.bind(null, app, { installationProgress: '80, Cleanup icon' }),
removeIcon.bind(null, app),
updateApp.bind(null, app, { installationProgress: '80, Unconfiguring reverse proxy' }),
unconfigureReverseProxy.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Cleanup logs' }),
cleanupLogs.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Unconfiguring Nginx' }),
unconfigureNginx.bind(null, app),
updateApp.bind(null, app, { installationProgress: '95, Remove app from database' }),
appdb.del.bind(null, app.id)
@@ -889,7 +765,7 @@ function startTask(appId, callback) {
assert.strictEqual(typeof callback, 'function');
// determine what to do
apps.get(appId, function (error, app) {
appdb.get(appId, function (error, app) {
if (error) return callback(error);
debugApp(app, 'startTask installationState: %s runState: %s', app.installationState, app.runState);
@@ -921,8 +797,6 @@ function startTask(appId, callback) {
if (require.main === module) {
assert.strictEqual(process.argv.length, 3, 'Pass the appid as argument');
// add a separator for the log file
debug('------------------------------------------------------------');
debug('Apptask for %s', process.argv[2]);
process.on('SIGTERM', function () {
+125
View File
@@ -0,0 +1,125 @@
'use strict';
exports = module.exports = {
initialize: initialize,
uninitialize: uninitialize,
accessTokenAuth: accessTokenAuth
};
var assert = require('assert'),
BasicStrategy = require('passport-http').BasicStrategy,
BearerStrategy = require('passport-http-bearer').Strategy,
clients = require('./clients'),
ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy,
ClientsError = clients.ClientsError,
DatabaseError = require('./databaseerror'),
debug = require('debug')('box:auth'),
LocalStrategy = require('passport-local').Strategy,
crypto = require('crypto'),
passport = require('passport'),
tokendb = require('./tokendb'),
user = require('./user'),
UserError = user.UserError,
_ = require('underscore');
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
passport.serializeUser(function (user, callback) {
callback(null, user.id);
});
passport.deserializeUser(function(userId, callback) {
user.get(userId, function (error, result) {
if (error) return callback(error);
var md5 = crypto.createHash('md5').update(result.alternateEmail || result.email).digest('hex');
result.gravatar = 'https://www.gravatar.com/avatar/' + md5 + '.jpg?s=24&d=mm';
callback(null, result);
});
});
passport.use(new LocalStrategy(function (username, password, callback) {
if (username.indexOf('@') === -1) {
user.verifyWithUsername(username, password, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
if (error && error.reason === UserError.WRONG_PASSWORD) return callback(null, false);
if (error) return callback(error);
if (!result) return callback(null, false);
callback(null, _.pick(result, 'id', 'username', 'email', 'admin'));
});
} else {
user.verifyWithEmail(username, password, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
if (error && error.reason === UserError.WRONG_PASSWORD) return callback(null, false);
if (error) return callback(error);
if (!result) return callback(null, false);
callback(null, _.pick(result, 'id', 'username', 'email', 'admin'));
});
}
}));
passport.use(new BasicStrategy(function (username, password, callback) {
if (username.indexOf('cid-') === 0) {
debug('BasicStrategy: detected client id %s instead of username:password', username);
// username is actually client id here
// password is client secret
clients.get(username, function (error, client) {
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
if (client.clientSecret != password) return callback(null, false);
return callback(null, client);
});
} else {
user.verifyWithUsername(username, password, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
if (error && error.reason === UserError.WRONG_PASSWORD) return callback(null, false);
if (error) return callback(error);
if (!result) return callback(null, false);
callback(null, result);
});
}
}));
passport.use(new ClientPasswordStrategy(function (clientId, clientSecret, callback) {
clients.get(clientId, function(error, client) {
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
if (error) { return callback(error); }
if (client.clientSecret != clientSecret) { return callback(null, false); }
return callback(null, client);
});
}));
passport.use(new BearerStrategy(accessTokenAuth));
callback(null);
}
function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
callback(null);
}
function accessTokenAuth(accessToken, callback) {
assert.strictEqual(typeof accessToken, 'string');
assert.strictEqual(typeof callback, 'function');
tokendb.get(accessToken, function (error, token) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
// scopes here can define what capabilities that token carries
// passport put the 'info' object into req.authInfo, where we can further validate the scopes
var info = { scope: token.scope };
user.get(token.identifier, function (error, user) {
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
callback(null, user, info);
});
});
}
+279 -534
View File
File diff suppressed because it is too large Load Diff

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