Compare commits

...

166 Commits
3.3 ... v3.4.2

Author SHA1 Message Date
Girish Ramakrishnan
2d7f0c3ebe 3.4.2 changes 2018-12-15 09:28:18 -08:00
Girish Ramakrishnan
a66bc7192d 3.4.1 changes 2018-12-14 17:35:54 -08:00
Girish Ramakrishnan
ff550e897a caas: remove box plan change routes 2018-12-13 09:41:52 -08:00
Johannes Zellner
10034fcbba Function is called tasks.removePrivateFields 2018-12-13 13:50:53 +01:00
Girish Ramakrishnan
36f8ce453f No need to reserve .app suffix 2018-12-12 15:07:49 -08:00
Girish Ramakrishnan
c2e40acb2c Fixup configuration and validation of mailboxName 2018-12-12 14:40:26 -08:00
Girish Ramakrishnan
82b1bb668d Fix tests 2018-12-11 16:26:19 -08:00
Girish Ramakrishnan
935a8258a6 task API: remove arg from response
it can contain sensitive information
2018-12-11 16:22:53 -08:00
Girish Ramakrishnan
fa483e5806 rename to listByTypePaged 2018-12-11 16:14:07 -08:00
Girish Ramakrishnan
e0c9658cb9 setup -> provision 2018-12-11 15:29:47 -08:00
Girish Ramakrishnan
0266a46b32 Ensure that location and domain are provided together
in our db, {location,domain} is unique. If we replace them one
by one in the database, it will cause conflicts.
2018-12-11 12:10:22 -08:00
Girish Ramakrishnan
e7294f2950 Make handleCertChanged take a callback 2018-12-11 11:02:32 -08:00
Girish Ramakrishnan
c9f325e75d renewCerts does not call callback 2018-12-11 10:49:04 -08:00
Girish Ramakrishnan
0fa353c2e2 return taskId for renewCerts 2018-12-11 10:16:38 -08:00
Girish Ramakrishnan
c7da090882 tasks: remove auditSource from start/stop 2018-12-11 09:22:13 -08:00
Johannes Zellner
ee609c8ef0 renew certs task also needs an auditSource 2018-12-11 13:10:32 +01:00
Johannes Zellner
6891ce2bc8 Fix typos 2018-12-11 12:33:35 +01:00
Johannes Zellner
94f5adba04 Renew certs is a POST route so lets use the body 2018-12-11 11:55:59 +01:00
Johannes Zellner
b8f843993a Fix typo 2018-12-11 11:26:45 +01:00
Girish Ramakrishnan
f9add21899 Add task route tests 2018-12-10 21:42:03 -08:00
Girish Ramakrishnan
1277da8bfe Add tasks test 2018-12-10 21:17:30 -08:00
Girish Ramakrishnan
55650fb734 Fix crash when db got cleared 2018-12-10 20:56:38 -08:00
Girish Ramakrishnan
d2f4b68c9f Make certificate renewal a task 2018-12-10 20:48:10 -08:00
Girish Ramakrishnan
a76731a991 remove old cloudron.conf and version field 2018-12-10 15:36:00 -08:00
Girish Ramakrishnan
536b8166ce typo 2018-12-09 12:04:51 -08:00
Girish Ramakrishnan
d43106b0af Add taskworker that runs funcs out of process 2018-12-09 10:06:22 -08:00
Girish Ramakrishnan
3688371ce8 Make tasks log to their own log file 2018-12-08 21:59:20 -08:00
Girish Ramakrishnan
6d66eb7759 Various fixes 2018-12-08 21:12:49 -08:00
Girish Ramakrishnan
8502bf4bfa tasks: add route to list tasks 2018-12-08 20:18:20 -08:00
Girish Ramakrishnan
d8225ad653 Make tasks indexed by id instead of type
The caas migrate logic is broken at this point until it uses new
task framework
2018-12-08 19:41:53 -08:00
Girish Ramakrishnan
cfb68a0511 setAdmin -> setDashboardDomain 2018-12-08 18:19:49 -08:00
Girish Ramakrishnan
76677e0aea Fix response 2018-12-07 16:39:22 -08:00
Girish Ramakrishnan
515ee891d3 refactor code to allow admin domain to be set 2018-12-07 16:15:21 -08:00
Girish Ramakrishnan
3aea1f3c9d Fix error message 2018-12-07 15:41:02 -08:00
Girish Ramakrishnan
8d944f9a4a Allow setup to be run again as long as not-activated
this is useful if the activation fails somewhere mid-way and we don't
need to re-setup the cloudron all over
2018-12-07 15:11:25 -08:00
Girish Ramakrishnan
331c8ae247 Add a new line 2018-12-07 11:28:23 -08:00
Girish Ramakrishnan
c71a429f61 Ask user before reboot
this is useful when installing on bare metal, don't want to reboot
server since the instructions get lost
2018-12-07 11:12:45 -08:00
Girish Ramakrishnan
3bad9e523c Add option to toggle app automatic updates 2018-12-07 10:02:11 -08:00
Girish Ramakrishnan
dfa61f1b2d rework how app mailboxes are allocated
Our current setup had a mailbox allocated for an app during app
install (into the mailboxes table). This has many issues:

* When set to a custom mailbox location, there was no way to access
  this mailbox even via IMAP. Even when using app credentials, we
  cannot use IMAP since the ldap logic was testing on the addon type
  (most of our apps only use sendmail addon and thus cannot recvmail).

* The mailboxes table was being used to add hidden 'app' type entries.
  This made it very hard for the user to understand why a mailbox conflicts.
  For example, if you set an app to use custom mailbox 'blog', this is
  hidden from all views.

The solution is to let an app send email as whatever mailbox name is
allocated to it (which we now track in the apps table. the default is in the
db already so that REST response contains it). When not using
Cloudron email, it will just send mail as that mailbox and the auth
checks the "app password" in the addons table. Any replies to that
mailbox will end up in the domain's mail server (not our problem).

When using cloudron email, the app can send mail like above. Any responses
will not end anywhere and bounce since there is no 'mailbox'. This is the
expected behavior. If user wants to access this mailbox name, he can
create a concrete mailbox and set himself as owner OR set this as
an alias.

For apps using the recvmail addon, the workflow is to actually create
a mailbox at some point. Currently, we have no UI for this 'flow'.
It's fine because we have only meemo using it.

Intuitive much!
2018-12-06 22:13:32 -08:00
Girish Ramakrishnan
6331fa5ced Update mail addon (does not use ownerType) 2018-12-06 20:34:18 -08:00
Girish Ramakrishnan
707b03b8c8 mailbox: ownerType is now purely internal 2018-12-06 20:25:24 -08:00
Girish Ramakrishnan
f2f93ed141 ldap: nobody binds to mailboxes 2018-12-06 19:34:25 -08:00
Girish Ramakrishnan
37e16c7a4c Force owner type to be user 2018-12-06 13:00:57 -08:00
Girish Ramakrishnan
41b0c3242e Make function name clearer 2018-12-06 12:47:50 -08:00
Girish Ramakrishnan
48ed051edf Use list verb 2018-12-06 10:23:10 -08:00
Girish Ramakrishnan
502642fd25 Add note on why we need universe 2018-12-06 09:32:02 -08:00
Johannes Zellner
4abe6a7a00 Ensure we have universe apt sources enabled 2018-12-06 13:23:30 +01:00
Girish Ramakrishnan
3f8fa64b98 wildcard must be outside the quotes 2018-12-05 16:39:29 -08:00
Johannes Zellner
527ff1b1fb Support docker and unbound service logs through journalctl 2018-12-05 16:19:10 +01:00
Girish Ramakrishnan
804467dce2 StandardOutput=file does not work in ubuntu 16
ubuntu 16 has systemd 229 which does not support this. ubuntu 18
has systemd 239 which works ok
2018-12-04 15:02:48 -08:00
Girish Ramakrishnan
4d7f308821 translate TaskError to UpdateError 2018-12-04 14:04:43 -08:00
Girish Ramakrishnan
a5b8418845 Remove spurious success return 2018-12-04 09:34:00 -08:00
Johannes Zellner
93d428b8c5 Update cron node module 2018-12-04 14:53:08 +01:00
Johannes Zellner
7c424ad60c Add netcup as provider 2018-12-04 09:51:40 +01:00
Girish Ramakrishnan
5b29a8680d Add missing callback 2018-12-03 15:23:26 -08:00
Girish Ramakrishnan
8f57c44837 provision: force http1.1
for unknown reasons, we get a http2 framing error from curl at times.
2018-12-03 13:29:43 -08:00
Johannes Zellner
b23939127b Ensure we always set the correct default value for TIMESTAMP types
So far it we rely on 0 default values in some case like the eventlog
This will not work in mysql strict mode with https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_no_zero_date
2018-12-03 14:09:57 +01:00
Johannes Zellner
3196322063 Add migration script to rename groups table to userGroups 2018-12-03 12:24:04 +01:00
Johannes Zellner
54c96d98d1 Remove all individual test timeouts and ignore timeouts toplevel 2018-12-03 12:24:04 +01:00
Johannes Zellner
f5f92fbb03 Rename groups table to userGroups 2018-12-03 12:24:04 +01:00
Girish Ramakrishnan
be0876603c ADDON_STATUS_* -> SERVICE_STATUS_* 2018-12-02 19:40:27 -08:00
Girish Ramakrishnan
7c1ef143f9 Add unbound service 2018-12-02 19:38:34 -08:00
Girish Ramakrishnan
6d128595e7 all container related functionality is 'service' 2018-12-02 19:12:43 -08:00
Girish Ramakrishnan
2f55abfc60 Fixup logs routes as well 2018-12-02 19:07:12 -08:00
Girish Ramakrishnan
f93044ac3b move routes to services.js 2018-12-02 18:46:34 -08:00
Girish Ramakrishnan
7ed422a3c1 Split out service related functions 2018-12-02 18:45:39 -08:00
Girish Ramakrishnan
823b3b8aa8 Rename addon route to service
service are the containers and other things like unbound, nginx.
addons are app development hooks.
2018-12-02 17:56:11 -08:00
Girish Ramakrishnan
9a701560f4 Fix email status 2018-12-01 21:50:28 -08:00
Girish Ramakrishnan
9800154d01 Use latest mail container (healthcheck route) 2018-12-01 21:50:24 -08:00
Girish Ramakrishnan
4b3f18ccdb Fix messages in update.sh 2018-11-30 21:18:36 -08:00
Girish Ramakrishnan
840d78b2f4 run update as a task, so it is cancelable 2018-11-30 21:04:24 -08:00
Girish Ramakrishnan
b409fd775d Revert "Fix crash"
This reverts commit 12a5965740.

Really fix crash
2018-11-30 19:10:44 -08:00
Girish Ramakrishnan
dbcfb20fab Remove shebang hack, this does not apply anymore 2018-11-30 17:10:28 -08:00
Girish Ramakrishnan
12a5965740 Fix crash 2018-11-30 17:10:28 -08:00
Girish Ramakrishnan
006ab75433 More changes 2018-11-30 17:10:28 -08:00
Girish Ramakrishnan
c72ea91743 make task args an object and put it in eventlog 2018-11-30 14:57:24 -08:00
Girish Ramakrishnan
f39ce20580 better name for task functions 2018-11-30 14:41:11 -08:00
Girish Ramakrishnan
b5c59e6b7d Add args to tasks table 2018-11-30 14:12:24 -08:00
Girish Ramakrishnan
b0ecdcc8b6 Fix another crash 2018-11-30 10:03:16 -08:00
Girish Ramakrishnan
8e1560f412 Fix crash in apptask 2018-11-30 09:48:26 -08:00
Girish Ramakrishnan
df927eae74 move backupupload.js to scripts
it is just a sudo helper
2018-11-29 23:30:56 -08:00
Girish Ramakrishnan
30aea047e3 startTask now takes args 2018-11-29 23:28:26 -08:00
Girish Ramakrishnan
cbcadaa449 Return an active field to indicate if task is actually running 2018-11-29 23:12:03 -08:00
Girish Ramakrishnan
9f4226093b Handle bad state if task is already stopped 2018-11-29 23:10:15 -08:00
Girish Ramakrishnan
fca0e897b2 Add tasks.startTask 2018-11-29 16:21:04 -08:00
Girish Ramakrishnan
2f729b56fa Use result in db instead of ipc 2018-11-29 15:34:05 -08:00
Girish Ramakrishnan
d9f3f64c76 Store result and error message separately 2018-11-29 14:45:28 -08:00
Girish Ramakrishnan
e8fa909c2f Bump graphite since the container port changed 2018-11-29 09:33:46 -08:00
Johannes Zellner
44f6636653 Add more changes 2018-11-29 16:44:44 +01:00
Johannes Zellner
148a0d0fc6 Add memory usage for addon status api 2018-11-28 12:33:24 +01:00
Girish Ramakrishnan
632ba69663 Make restore/download logic have progress callbacks 2018-11-27 12:13:16 -08:00
Girish Ramakrishnan
b2465dd2ee Set the backup task progress in the task only 2018-11-27 12:12:27 -08:00
Girish Ramakrishnan
e56b87766b backupApp should not set backup task progress 2018-11-27 11:06:03 -08:00
Girish Ramakrishnan
f7ca2e416a Add progress callback for snapshotting 2018-11-27 11:03:58 -08:00
Girish Ramakrishnan
002f68b0a1 Add progress info during backup rotation 2018-11-27 10:51:35 -08:00
Girish Ramakrishnan
aa31be5c5a backup: make upload progress available with callback+ipc 2018-11-27 10:42:34 -08:00
Johannes Zellner
6c0b7017bd Add some 3.4 changes 2018-11-27 14:41:03 +01:00
Girish Ramakrishnan
581774e001 Use ipc to pass back the result 2018-11-26 20:39:52 -08:00
Girish Ramakrishnan
3847a6616e No need to track backup time since it is killable now 2018-11-26 20:39:20 -08:00
Girish Ramakrishnan
48fbe28355 Make upload task abort when parent dies 2018-11-26 19:11:30 -08:00
Girish Ramakrishnan
e3ee5bc1d5 lint 2018-11-26 15:55:00 -08:00
Girish Ramakrishnan
a2da9bea58 backup: use ipc for communicating with upload process 2018-11-26 15:21:48 -08:00
Johannes Zellner
e4512e12c5 Add --help for cloudron-support and make ssh key enabling optional 2018-11-26 15:28:11 +01:00
Johannes Zellner
114f48fb17 Remove leftover console.log() 2018-11-26 14:50:10 +01:00
Johannes Zellner
289e018160 Fix comment text 2018-11-26 08:19:24 +01:00
Johannes Zellner
cb6699eeed Add api to check if the server needs a reboot 2018-11-26 08:19:24 +01:00
Girish Ramakrishnan
802011bb7e Fix args to shell.sudo 2018-11-25 14:57:51 -08:00
Girish Ramakrishnan
6cd8e769be remove all uses of sudoSync 2018-11-25 14:43:29 -08:00
Girish Ramakrishnan
9f6f67d331 ssh: remove use of shell.sudoSync 2018-11-25 14:27:06 -08:00
Johannes Zellner
161a8fe2bf Make docker restartable 2018-11-24 22:12:28 +01:00
Girish Ramakrishnan
b9c9839bb7 apparmor is always enabled on all ubuntu 2018-11-23 13:11:31 -08:00
Girish Ramakrishnan
76edbee48c Better error message 2018-11-23 13:11:15 -08:00
Girish Ramakrishnan
4142d7a050 Fix error handling of all the execSync usage 2018-11-23 13:11:15 -08:00
Girish Ramakrishnan
a0306c69e1 remove unused acme1.js
it got merged to acme2.js
2018-11-23 13:11:15 -08:00
Girish Ramakrishnan
31823f6282 Check if result is not null 2018-11-23 13:11:15 -08:00
Girish Ramakrishnan
9b4fffde29 Use shell.exec instead of shell.execSync 2018-11-23 11:18:45 -08:00
Girish Ramakrishnan
cce03e250d these can just be debugs 2018-11-22 18:05:41 -08:00
Girish Ramakrishnan
9b32cad946 typoe 2018-11-22 17:26:06 -08:00
Girish Ramakrishnan
2877a1057e Add CLOUDRON_PROXY_IP 2018-11-22 16:50:02 -08:00
Johannes Zellner
e2debe3c39 Add starting addon state when we wait for healthcheck 2018-11-22 21:54:31 +01:00
Johannes Zellner
f54ab11f18 Handle mail related addon logs 2018-11-22 12:38:50 +01:00
Johannes Zellner
b560e281d0 share container addon status code 2018-11-22 12:38:50 +01:00
Girish Ramakrishnan
3bb4ef5727 Some servers can be very slow starting databases 2018-11-21 09:49:08 -08:00
Johannes Zellner
900c008d20 Only allow addon restarts, no start+stop 2018-11-21 16:06:58 +01:00
Johannes Zellner
c1183a09a8 Add addon configure route 2018-11-21 15:47:41 +01:00
Girish Ramakrishnan
e04b7b55b0 Remove upgrade flag
This is not used since ages since we changed our update methodology
2018-11-20 10:29:54 -08:00
Johannes Zellner
329cc80933 Send memory settings on addon status 2018-11-20 16:53:27 +01:00
Johannes Zellner
a13f0706b4 Report addon state detail for further use 2018-11-20 13:11:11 +01:00
Johannes Zellner
55811de4b8 Check for docker ping result 2018-11-20 13:07:41 +01:00
Johannes Zellner
ab456f179e Check for healthcheck in addon status code 2018-11-20 12:26:33 +01:00
Johannes Zellner
f9d5bcd352 Make getAddonDetails() return AddonsError 2018-11-20 12:12:25 +01:00
Johannes Zellner
6a337884b5 Add start/stop addon code 2018-11-20 11:09:09 +01:00
Johannes Zellner
f953d115da Fix missing require 2018-11-20 11:09:09 +01:00
Johannes Zellner
88e8fc840f Fetch basic addon status from docker 2018-11-20 11:09:09 +01:00
Johannes Zellner
d1818e31b0 Add addon status hooks 2018-11-20 11:09:09 +01:00
Johannes Zellner
3f4bf647e8 Allow console.* in the linter config 2018-11-20 11:09:09 +01:00
Johannes Zellner
725a7e6dec Add missing requires 2018-11-20 11:09:09 +01:00
Johannes Zellner
e08b210001 Add initial addons rest apis 2018-11-20 11:09:09 +01:00
Girish Ramakrishnan
ec08ccb996 move tasks to separate directory
these are just wrappers that we use to launch as processes
2018-11-19 21:17:04 -08:00
Girish Ramakrishnan
b47a146c2b unlock on log failure 2018-11-19 21:12:58 -08:00
Girish Ramakrishnan
14dff27d45 boxUpdateInfo cannot be null here 2018-11-19 21:09:33 -08:00
Girish Ramakrishnan
305a3c94d0 Rework the progress API to use the task progress 2018-11-19 21:06:44 -08:00
Girish Ramakrishnan
218739a6b5 Add tasks table and API
progress will be tracked with this table instead of being in-process
like progress.js
2018-11-19 17:37:42 -08:00
Girish Ramakrishnan
390e69c01c Fix backup API routes 2018-11-19 14:41:16 -08:00
Johannes Zellner
4ef274acf0 Box logs are now in a log file 2018-11-19 19:47:19 +01:00
Girish Ramakrishnan
8267279779 backup can now be stopped
track the backup process and killable
2018-11-17 20:31:07 -08:00
Girish Ramakrishnan
6d971b9235 backupupload also logs to stdout now 2018-11-17 19:38:02 -08:00
Girish Ramakrishnan
98dc160886 remove the progress and watchdog timers
not needed since we will track these processes now and can be killed
by the user.
2018-11-17 19:38:02 -08:00
Girish Ramakrishnan
a869c88b43 Remove setBackupProgress
the upload task can just output to stdout. for progress status, we
can just use a tailstream on the ui side
2018-11-17 19:38:02 -08:00
Girish Ramakrishnan
0b86070fe9 run backuptask as separate process 2018-11-17 19:38:02 -08:00
Girish Ramakrishnan
5c9b6736f0 Remove pruneInfraImages out of platform startup
If it fails, it's really OK. Maybe we will have a button to remove
images.

This goes further into step of webadmin always staying up and not
crashing/erroring for cosmetic issues.
2018-11-17 19:35:39 -08:00
Girish Ramakrishnan
fd4057df94 shell.exec -> shell.spawn 2018-11-17 19:26:19 -08:00
Girish Ramakrishnan
1b1945e1f5 Move out graphite from port 8000
Port 8000 is used by esxi management service (!)
2018-11-17 19:14:21 -08:00
Girish Ramakrishnan
ebb053b900 add back timestamps to debug output 2018-11-16 14:16:11 -08:00
Girish Ramakrishnan
3381d9b595 Add format to the default backup config 2018-11-16 14:15:20 -08:00
Girish Ramakrishnan
d7a11ef394 runBackupTask -> runBackupUpload 2018-11-16 09:21:07 -08:00
Johannes Zellner
9d40cffabe Add logrotate config for box logs 2018-11-16 13:14:59 +01:00
Johannes Zellner
de44c63557 box now also logs to a a file instead of journald 2018-11-16 13:05:11 +01:00
Johannes Zellner
ac25477cd7 Ensure we prefix all output with a timestamp
note that debug() already does this now on its own in the same format
it does not use console.log but process.stderr
2018-11-16 13:02:37 +01:00
Johannes Zellner
59b86aa090 Stop logging box to journald 2018-11-16 12:15:38 +01:00
Girish Ramakrishnan
6abd48d480 rename backuptask to backupupload 2018-11-15 12:00:51 -08:00
Girish Ramakrishnan
72fc6b8c5a Fix tests 2018-11-15 12:00:51 -08:00
Girish Ramakrishnan
fcce4a6853 Add note on StandardError 2018-11-15 10:52:31 -08:00
Girish Ramakrishnan
a3b1a2c781 Get the domain correctly from subject 2018-11-15 10:47:20 -08:00
Johannes Zellner
a838a1706f Fix indentation 2018-11-15 19:26:50 +01:00
Johannes Zellner
a24c9fbafb Put cloudron-updater (installer.sh) logs persistently to /var/log/ 2018-11-15 14:54:12 +01:00
107 changed files with 4312 additions and 3656 deletions

View File

@@ -23,6 +23,7 @@
"semi": [
"error",
"always"
]
],
"no-console": "off"
}
}

30
CHANGES
View File

@@ -1464,3 +1464,33 @@
* 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

14
box.js
View File

@@ -2,12 +2,16 @@
'use strict';
require('supererror')({ splatchError: true });
// 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)));
};
});
// remove timestamp from debug() based output
require('debug').formatArgs = function formatArgs(args) {
args[0] = this.namespace + ' ' + args[0];
};
require('supererror')({ splatchError: true });
let async = require('async'),
config = require('./src/config.js'),

View File

@@ -1,7 +1,7 @@
'use strict';
exports.up = function(db, callback) {
var cmd = "CREATE TABLE groups(" +
var cmd = "CREATE TABLE userGroups(" +
"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 groups', function (error) {
db.runSql('DROP TABLE userGroups', function (error) {
if (error) console.error(error);
callback(error);
});

View File

@@ -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 groups(id)," +
"FOREIGN KEY(groupId) REFERENCES userGroups(id)," +
"FOREIGN KEY(userId) REFERENCES users(id));";
db.runSql(cmd, function (error) {

View File

@@ -7,7 +7,7 @@ var ADMIN_GROUP_ID = 'admin'; // see constants.js
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'INSERT INTO groups (id, name) VALUES (?, ?)', [ ADMIN_GROUP_ID, 'admin' ]),
db.runSql.bind(db, 'INSERT INTO userGroups (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);

View File

@@ -10,7 +10,7 @@ exports.up = function(db, callback) {
function addGroupMailboxes(done) {
console.log('Importing group mailboxes');
db.all('SELECT id, name FROM groups', function (error, results) {
db.all('SELECT id, name FROM userGroups', function (error, results) {
if (error) return done(error);
async.eachSeries(results, function (g, next) {

View File

@@ -16,7 +16,7 @@ exports.up = function(db, callback) {
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 groups 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'),

View File

@@ -29,7 +29,7 @@ exports.up = function(db, callback) {
// 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 groups WHERE id = ?', [ ADMIN_GROUP_ID ], function (error) {
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);

View File

@@ -19,8 +19,8 @@ exports.up = function(db, callback) {
},
function getGroups(done) {
db.all('SELECT id, name, GROUP_CONCAT(groupMembers.userId) AS userIds ' +
' FROM groups LEFT OUTER JOIN groupMembers ON groups.id = groupMembers.groupId ' +
' GROUP BY groups.id', [ ], function (error, results) {
' 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) {

View File

@@ -18,7 +18,7 @@ exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'DELETE FROM groupMembers WHERE groupId=?', [ 'admin' ]),
db.runSql.bind(db, 'DELETE FROM groups WHERE id=?', [ 'admin' ])
db.runSql.bind(db, 'DELETE FROM userGroups WHERE id=?', [ 'admin' ])
], callback);
});
});

View File

@@ -0,0 +1,27 @@
'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);
});
};

View File

@@ -0,0 +1,17 @@
'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();
};

View File

@@ -0,0 +1,17 @@
'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();
};

View File

@@ -0,0 +1,28 @@
'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();
};

View File

@@ -0,0 +1,28 @@
'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();
};

View File

@@ -0,0 +1,16 @@
'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);
});
};

View File

@@ -29,7 +29,7 @@ CREATE TABLE IF NOT EXISTS users(
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS groups(
CREATE TABLE IF NOT EXISTS userGroups(
id VARCHAR(128) NOT NULL UNIQUE,
name VARCHAR(254) NOT NULL UNIQUE,
PRIMARY KEY(id));
@@ -37,7 +37,7 @@ CREATE TABLE IF NOT EXISTS groups(
CREATE TABLE IF NOT EXISTS groupMembers(
groupId VARCHAR(128) NOT NULL,
userId VARCHAR(128) NOT NULL,
FOREIGN KEY(groupId) REFERENCES groups(id),
FOREIGN KEY(groupId) REFERENCES userGroups(id),
FOREIGN KEY(userId) REFERENCES users(id));
CREATE TABLE IF NOT EXISTS tokens(
@@ -71,8 +71,8 @@ CREATE TABLE IF NOT EXISTS apps(
location VARCHAR(128) NOT NULL,
domain VARCHAR(128) NOT NULL,
accessRestrictionJson TEXT, // { users: [ ], groups: [ ] }
creationTime TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app was installed
updateTime TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the last app update was done
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)
memoryLimit BIGINT DEFAULT 0,
xFrameOptions VARCHAR(512),
@@ -80,6 +80,8 @@ CREATE TABLE IF NOT EXISTS apps(
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
// 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
@@ -126,7 +128,7 @@ CREATE TABLE IF NOT EXISTS appEnvVars(
CREATE TABLE IF NOT EXISTS backups(
id VARCHAR(128) NOT NULL,
creationTime TIMESTAMP,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_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 */
@@ -141,7 +143,7 @@ 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 */
createdAt TIMESTAMP(2) NOT NULL,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id));
@@ -173,15 +175,17 @@ CREATE TABLE IF NOT EXISTS mail(
/* 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, /* app id or user id or group id */
ownerType VARCHAR(16) NOT NULL, /* 'app' or 'user' or 'group' */
ownerId VARCHAR(128) NOT NULL, /* user id */
aliasTarget VARCHAR(128), /* the target name type is an alias */
membersJson TEXT, /* members of a group */
creationTime TIMESTAMP,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
domain VARCHAR(128),
FOREIGN KEY(domain) REFERENCES mail(domain),
@@ -195,6 +199,18 @@ CREATE TABLE IF NOT EXISTS subdomains(
FOREIGN KEY(domain) REFERENCES domains(domain),
FOREIGN KEY(appId) REFERENCES apps(id),
UNIQUE (subdomain, domain))
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));
CHARACTER SET utf8 COLLATE utf8_bin;

3113
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,7 @@
"connect-timeout": "^1.9.0",
"cookie-parser": "^1.3.5",
"cookie-session": "^1.3.2",
"cron": "^1.3.0",
"cron": "^1.5.1",
"csurf": "^1.6.6",
"db-migrate": "^0.11.1",
"db-migrate-mysql": "^1.1.10",
@@ -92,7 +92,7 @@
"scripts": {
"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 -- --exit -R spec ./src/test ./src/routes/test",
"test": "npm run migrate_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",
"postmerge": "/bin/true",
"precommit": "/bin/true",
"prepush": "npm test",

View File

@@ -2,7 +2,7 @@
set -eu -o pipefail
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400 --http1.1"
ip=""
dns_config=""

View File

@@ -94,7 +94,7 @@ 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, ovh, rosehosting, scaleway, vultr or generic)"
echo "--provider is required (azure, digitalocean, ec2, exoscale, gce, hetzner, lightsail, linode, netcup, ovh, rosehosting, scaleway, vultr or generic)"
exit 1
elif [[ \
"${provider}" != "ami" && \
@@ -110,13 +110,14 @@ elif [[ \
"${provider}" != "hetzner" && \
"${provider}" != "lightsail" && \
"${provider}" != "linode" && \
"${provider}" != "netcup" && \
"${provider}" != "ovh" && \
"${provider}" != "rosehosting" && \
"${provider}" != "scaleway" && \
"${provider}" != "vultr" && \
"${provider}" != "generic" \
]]; then
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, lightsail, linode, netcup, ovh, rosehosting, scaleway, vultr or generic"
exit 1
fi
@@ -137,6 +138,12 @@ echo " Join us at https://forum.cloudron.io for any questions."
echo ""
if [[ "${initBaseImage}" == "true" ]]; then
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}"
@@ -231,10 +238,15 @@ while true; do
sleep 10
done
echo -e "\n\n${GREEN}Visit https://<IP> and accept the self-signed certificate to finish setup.${DONE}"
echo -e "\n\n${GREEN}Visit https://<IP> and accept the self-signed certificate to finish setup.${DONE}\n"
if [[ "${rebootServer}" == "true" ]]; then
echo -e "\n${RED}Rebooting this server now to let changes take effect.${DONE}\n"
systemctl stop mysql # sometimes mysql ends up having corrupt privilege tables
systemctl reboot
systemctl stop box mysql # sometimes mysql ends up having corrupt privilege tables
read -p "This server has to rebooted to apply all the settings. Reboot now ? [Y/n] " yn
yn=${yn:-y}
case $yn in
[Yy]* ) systemctl reboot;;
* ) exit;;
esac
fi

View File

@@ -7,6 +7,13 @@ 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
@@ -14,6 +21,20 @@ if [[ ${EUID} -ne 0 ]]; then
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 /"
@@ -66,6 +87,9 @@ 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
@@ -76,12 +100,14 @@ echo -n "Uploading information..."
paste_key=$(curl -X POST ${PASTEBIN}/documents --silent -d "$(cat $OUT)" | python3 -c "import sys, json; print(json.load(sys.stdin)['key'])")
echo "Done"
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"
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"

View File

@@ -86,8 +86,14 @@ images=$(node -e "var i = require('${box_src_tmp_dir}/src/infra_version.js'); co
echo -e "\tPulling docker images: ${images}"
for image in ${images}; do
docker pull "${image}" # this pulls the image using the sha256
docker pull "${image%@sha256:*}" # this will tag the image for readability
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"

View File

@@ -16,7 +16,7 @@ readonly BOX_DATA_DIR="${HOME_DIR}/boxdata" # box data
readonly CONFIG_DIR="${HOME_DIR}/configs"
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly get_config="$(realpath ${script_dir}/../node_modules/.bin/json) -f /etc/cloudron/cloudron.conf"
readonly json="$(realpath ${script_dir}/../node_modules/.bin/json)"
echo "==> Configuring docker"
cp "${script_dir}/start/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
@@ -58,7 +58,7 @@ 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"
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"
@@ -91,12 +91,9 @@ setfacl -n -m u:${USER}:r /var/log/journal/*/system.journal
echo "==> Creating config directory"
mkdir -p "${CONFIG_DIR}"
# migration for cloudron.conf file. Can be removed after 3.3
if [[ ! -d /etc/cloudron ]]; then
echo "==> Migrating existing cloudron.conf to new location"
mkdir -p /etc/cloudron
cp "${CONFIG_DIR}/cloudron.conf" /etc/cloudron/cloudron.conf
fi
# remove old cloudron.conf. Can be removed after 3.4
rm -f "${CONFIG_DIR}/cloudron.conf"
$json -f /etc/cloudron/cloudron.conf -I -e "delete this.version" # remove the version field
chown -R "${USER}" /etc/cloudron
echo "==> Setting up unbound"
@@ -142,8 +139,8 @@ echo "==> Configuring logrotate"
if ! grep -q "^include ${PLATFORM_DATA_DIR}/logrotate.d" /etc/logrotate.conf; then
echo -e "\ninclude ${PLATFORM_DATA_DIR}/logrotate.d\n" >> /etc/logrotate.conf
fi
cp "${script_dir}/start/app-logrotate" "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate"
chown root:root "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate"
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

View File

@@ -0,0 +1,9 @@
# 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
}

View File

@@ -162,7 +162,7 @@ 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:8000;
# proxy_pass http://127.0.0.1:8417;
# client_max_body_size 1m;
# }

View File

@@ -4,8 +4,8 @@ Defaults !syslog
Defaults!/home/yellowtent/box/src/scripts/rmvolume.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmvolume.sh
Defaults!/home/yellowtent/box/src/scripts/rmaddon.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmaddon.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/reloadnginx.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reloadnginx.sh
@@ -31,9 +31,15 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/authorized_keys
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/backuptask.js env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD:SETENV: /home/yellowtent/box/src/backuptask.js
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/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

View File

@@ -12,7 +12,8 @@ Wants=cloudron-resize-fs.service
Type=idle
WorkingDirectory=/home/yellowtent/box
Restart=always
ExecStart=/usr/bin/node --max_old_space_size=150 /home/yellowtent/box/box.js
; 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'
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
; kill apptask processes as well
KillMode=control-group

View File

@@ -1,8 +1,16 @@
'use strict';
exports = module.exports = {
startAddons: startAddons,
updateAddonConfig: updateAddonConfig,
AddonsError: AddonsError,
getServices: getServices,
getService: getService,
configureService: configureService,
getServiceLogs: getServiceLogs,
restartService: restartService,
startServices: startServices,
updateServiceConfig: updateServiceConfig,
setupAddons: setupAddons,
teardownAddons: teardownAddons,
@@ -16,7 +24,11 @@ exports = module.exports = {
// exported for testing
_setupOauth: setupOauth,
_teardownOauth: teardownOauth
_teardownOauth: teardownOauth,
SERVICE_STATUS_STARTING: 'starting', // container up, waiting for healthcheck
SERVICE_STATUS_ACTIVE: 'active',
SERVICE_STATUS_STOPPED: 'stopped'
};
var accesscontrol = require('./accesscontrol.js'),
@@ -31,11 +43,11 @@ var accesscontrol = require('./accesscontrol.js'),
debug = require('debug')('box:addons'),
docker = require('./docker.js'),
dockerConnection = docker.connection,
DockerError = docker.DockerError,
fs = require('fs'),
hat = require('./hat.js'),
infra = require('./infra_version.js'),
mail = require('./mail.js'),
mailboxdb = require('./mailboxdb.js'),
once = require('once'),
os = require('os'),
path = require('path'),
@@ -43,13 +55,41 @@ var accesscontrol = require('./accesscontrol.js'),
rimraf = require('rimraf'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
shell = require('./shell.js'),
spawn = require('child_process').spawn,
split = require('split'),
request = require('request'),
util = require('util');
// http://dustinsenos.com/articles/customErrorsInNode
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
function AddonsError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(AddonsError, Error);
AddonsError.INTERNAL_ERROR = 'Internal Error';
AddonsError.NOT_FOUND = 'Not Found';
AddonsError.NOT_ACTIVE = 'Not Active';
const NOOP = function (app, options, callback) { return callback(); };
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
const RMADDON_CMD = path.join(__dirname, 'scripts/rmaddon.sh');
const RMADDONDIR_CMD = path.join(__dirname, 'scripts/rmaddondir.sh');
// setup can be called multiple times for the same app (configure crash restart) and existing data must not be lost
// teardown is destructive. app data stored with the addon is lost
@@ -59,84 +99,117 @@ var KNOWN_ADDONS = {
teardown: teardownEmail,
backup: NOOP,
restore: setupEmail,
clear: NOOP
clear: NOOP,
},
ldap: {
setup: setupLdap,
teardown: teardownLdap,
backup: NOOP,
restore: setupLdap,
clear: NOOP
clear: NOOP,
},
localstorage: {
setup: setupLocalStorage, // docker creates the directory for us
teardown: teardownLocalStorage,
backup: NOOP, // no backup because it's already inside app data
restore: NOOP,
clear: clearLocalStorage
clear: clearLocalStorage,
},
mongodb: {
setup: setupMongoDb,
teardown: teardownMongoDb,
backup: backupMongoDb,
restore: restoreMongoDb,
clear: clearMongodb
clear: clearMongodb,
},
mysql: {
setup: setupMySql,
teardown: teardownMySql,
backup: backupMySql,
restore: restoreMySql,
clear: clearMySql
clear: clearMySql,
},
oauth: {
setup: setupOauth,
teardown: teardownOauth,
backup: NOOP,
restore: setupOauth,
clear: NOOP
clear: NOOP,
},
postgresql: {
setup: setupPostgreSql,
teardown: teardownPostgreSql,
backup: backupPostgreSql,
restore: restorePostgreSql,
clear: clearPostgreSql
clear: clearPostgreSql,
},
recvmail: {
setup: setupRecvMail,
teardown: teardownRecvMail,
backup: NOOP,
restore: setupRecvMail,
clear: NOOP
clear: NOOP,
},
redis: {
setup: setupRedis,
teardown: teardownRedis,
backup: backupRedis,
restore: restoreRedis,
clear: clearRedis
clear: clearRedis,
},
sendmail: {
setup: setupSendMail,
teardown: teardownSendMail,
backup: NOOP,
restore: setupSendMail,
clear: NOOP
clear: NOOP,
},
scheduler: {
setup: NOOP,
teardown: NOOP,
backup: NOOP,
restore: NOOP,
clear: NOOP
clear: NOOP,
},
docker: {
setup: NOOP,
teardown: NOOP,
backup: NOOP,
restore: NOOP,
clear: NOOP
clear: NOOP,
}
};
const KNOWN_SERVICES = {
mail: {
status: containerStatus.bind(null, 'mail', 'CLOUDRON_MAIL_TOKEN'),
restart: restartContainer.bind(null, 'mail'),
defaultMemoryLimit: Math.max((1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 128, 256) * 1024 * 1024
},
mongodb: {
status: containerStatus.bind(null, 'mongodb', 'CLOUDRON_MONGODB_TOKEN'),
restart: restartContainer.bind(null, 'mongodb'),
defaultMemoryLimit: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 200 * 1024 * 1024
},
mysql: {
status: containerStatus.bind(null, 'mysql', 'CLOUDRON_MYSQL_TOKEN'),
restart: restartContainer.bind(null, 'mysql'),
defaultMemoryLimit: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256 * 1024 * 1024
},
postgresql: {
status: containerStatus.bind(null, 'postgresql', 'CLOUDRON_POSTGRESQL_TOKEN'),
restart: restartContainer.bind(null, 'postgresql'),
defaultMemoryLimit: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256 * 1024 * 1024
},
docker: {
status: statusDocker,
restart: restartDocker,
defaultMemoryLimit: 0
},
unbound: {
status: statusUnbound,
restart: restartUnbound,
defaultMemoryLimit: 0
}
};
@@ -170,41 +243,233 @@ function dumpPath(addon, appId) {
}
}
function getAddonDetails(containerName, tokenEnvName, callback) {
function restartContainer(serviceName, callback) {
assert.strictEqual(typeof serviceName, 'string');
assert.strictEqual(typeof callback, 'function');
assert(KNOWN_SERVICES[serviceName], `Unknown service ${serviceName}`);
docker.stopContainer(serviceName, function (error) {
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
docker.startContainer(serviceName, function (error) {
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
callback(null);
});
});
}
function containerStatus(addonName, addonTokenName, callback) {
assert.strictEqual(typeof addonName, 'string');
assert.strictEqual(typeof addonTokenName, 'string');
assert.strictEqual(typeof callback, 'function');
getServiceDetails(addonName, addonTokenName, function (error, addonDetails) {
if (error && error.reason === AddonsError.NOT_ACTIVE) return callback(null, { status: exports.SERVICE_STATUS_STOPPED });
if (error) return callback(error);
request.get(`https://${addonDetails.ip}:3000/healthcheck?access_token=${addonDetails.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
if (error) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for ${addonName}: ${error.message}` });
if (response.statusCode !== 200 || !response.body.status) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for ${addonName}. Status code: ${response.statusCode} message: ${response.body.message}` });
docker.memoryUsage(addonName, function (error, result) {
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
var tmp = {
status: addonDetails.state.Running ? exports.SERVICE_STATUS_ACTIVE : exports.SERVICE_STATUS_STOPPED,
memoryUsed: result.memory_stats.usage,
memoryPercent: parseInt(100 * result.memory_stats.usage / result.memory_stats.limit)
};
callback(null, tmp);
});
});
});
}
function getServices(callback) {
assert.strictEqual(typeof callback, 'function');
let services = Object.keys(KNOWN_SERVICES);
callback(null, services);
}
function getService(serviceName, callback) {
assert.strictEqual(typeof serviceName, 'string');
assert.strictEqual(typeof callback, 'function');
if (!KNOWN_SERVICES[serviceName]) return callback(new AddonsError(AddonsError.NOT_FOUND));
var tmp = {
name: serviceName,
status: null,
config: {
// If a property is not set then we cannot change it through the api, see below
// memory: 0,
// memorySwap: 0
}
};
settings.getPlatformConfig(function (error, platformConfig) {
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
if (platformConfig[serviceName] && platformConfig[serviceName].memory && platformConfig[serviceName].memorySwap) {
tmp.config.memory = platformConfig[serviceName].memory;
tmp.config.memorySwap = platformConfig[serviceName].memorySwap;
} else if (KNOWN_SERVICES[serviceName].defaultMemoryLimit) {
tmp.config.memory = KNOWN_SERVICES[serviceName].defaultMemoryLimit;
tmp.config.memorySwap = tmp.config.memory * 2;
}
KNOWN_SERVICES[serviceName].status(function (error, result) {
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
tmp.status = result.status;
tmp.memoryUsed = result.memoryUsed;
tmp.memoryPercent = result.memoryPercent;
tmp.error = result.error || null;
callback(null, tmp);
});
});
}
function configureService(serviceName, data, callback) {
assert.strictEqual(typeof serviceName, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
if (!KNOWN_SERVICES[serviceName]) return callback(new AddonsError(AddonsError.NOT_FOUND));
settings.getPlatformConfig(function (error, platformConfig) {
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
if (!platformConfig[serviceName]) platformConfig[serviceName] = {};
// if not specified we clear the entry and use defaults
if (!data.memory || !data.memorySwap) {
delete platformConfig[serviceName];
} else {
platformConfig[serviceName].memory = data.memory;
platformConfig[serviceName].memorySwap = data.memorySwap;
}
settings.setPlatformConfig(platformConfig, function (error) {
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
callback(null);
});
});
}
function getServiceLogs(serviceName, options, callback) {
assert.strictEqual(typeof serviceName, 'string');
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
if (!KNOWN_SERVICES[serviceName]) return callback(new AddonsError(AddonsError.NOT_FOUND));
debug(`Getting logs for ${serviceName}`);
var lines = options.lines || 100,
format = options.format || 'json',
follow = !!options.follow;
assert.strictEqual(typeof lines, 'number');
assert.strictEqual(typeof format, 'string');
var cmd;
var args = [ '--lines=' + lines ];
// docker and unbound use journald
if (serviceName === 'docker' || serviceName === 'unbound') {
cmd = 'journalctl';
args.push(`--unit=${serviceName}`);
args.push('--no-pager');
args.push('--output=short-iso');
if (follow) args.push('--follow');
} else {
cmd = '/usr/bin/tail';
if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs
args.push(path.join(paths.LOG_DIR, serviceName, 'app.log'));
}
var cp = spawn(cmd, args);
var transformStream = split(function mapper(line) {
if (format !== 'json') return line + '\n';
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
var timestamp = (new Date(data[0])).getTime();
if (isNaN(timestamp)) timestamp = 0;
var message = line.slice(data[0].length+1);
// ignore faulty empty logs
if (!timestamp && !message) return;
return JSON.stringify({
realtimeTimestamp: timestamp * 1000,
message: message,
source: serviceName
}) + '\n';
});
transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process
cp.stdout.pipe(transformStream);
callback(null, transformStream);
}
function restartService(serviceName, callback) {
assert.strictEqual(typeof serviceName, 'string');
assert.strictEqual(typeof callback, 'function');
if (!KNOWN_SERVICES[serviceName]) return callback(new AddonsError(AddonsError.NOT_FOUND));
KNOWN_SERVICES[serviceName].restart(callback);
}
function getServiceDetails(containerName, tokenEnvName, callback) {
assert.strictEqual(typeof containerName, 'string');
assert.strictEqual(typeof tokenEnvName, 'string');
assert.strictEqual(typeof callback, 'function');
var container = dockerConnection.getContainer(containerName);
container.inspect(function (error, result) {
if (error) return callback(new Error(`Error inspecting ${containerName} container: ` + error));
docker.inspect(containerName, function (error, result) {
if (error && error.reason === DockerError.NOT_FOUND) return callback(new AddonsError(AddonsError.NOT_ACTIVE, error));
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
const ip = safe.query(result, 'NetworkSettings.Networks.cloudron.IPAddress', null);
if (!ip) return callback(new Error(`Error getting ${containerName} container ip`));
if (!ip) return callback(new AddonsError(AddonsError.NOT_ACTIVE, `Error getting ${containerName} container ip`));
// extract the cloudron token for auth
const env = safe.query(result, 'Config.Env', null);
if (!env) return callback(new Error(`Error getting ${containerName} env`));
if (!env) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, `Error getting ${containerName} env`));
const tmp = env.find(function (e) { return e.indexOf(tokenEnvName) === 0; });
if (!tmp) return callback(new Error(`Error getting ${containerName} cloudron token env var`));
if (!tmp) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, `Error getting ${containerName} cloudron token env var`));
const token = tmp.slice(tokenEnvName.length + 1); // +1 for the = sign
if (!token) return callback(new Error(`Error getting ${containerName} cloudron token`));
if (!token) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, `Error getting ${containerName} cloudron token`));
callback(null, { ip: ip, token: token });
callback(null, { ip: ip, token: token, state: result.State });
});
}
function waitForAddon(containerName, tokenEnvName, callback) {
function waitForService(containerName, tokenEnvName, callback) {
assert.strictEqual(typeof containerName, 'string');
assert.strictEqual(typeof tokenEnvName, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`Waiting for ${containerName}`);
getAddonDetails(containerName, tokenEnvName, function (error, result) {
getServiceDetails(containerName, tokenEnvName, function (error, result) {
if (error) return callback(error);
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
async.retry({ times: 10, interval: 15000 }, function (retryCallback) {
request.get(`https://${result.ip}:3000/healthcheck?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
if (error) return retryCallback(new Error(`Error waiting for ${containerName}: ${error.message}`));
if (response.statusCode !== 200 || !response.body.status) return retryCallback(new Error(`Error waiting for ${containerName}. Status code: ${response.statusCode} message: ${response.body.message}`));
@@ -343,37 +608,29 @@ function importDatabase(addon, callback) {
});
}
function updateAddonConfig(platformConfig, callback) {
function updateServiceConfig(platformConfig, callback) {
callback = callback || NOOP_CALLBACK;
// TODO: maybe derive these defaults based on how many apps are using them
const defaultMemoryLimits = {
mysql: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256 * 1024 * 1024,
mongodb: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 200 * 1024 * 1024,
postgresql: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256 * 1024 * 1024,
mail: Math.max((1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 128, 256) * 1024 * 1024
};
debug('updateAddonConfig: %j', platformConfig);
debug('updateServiceConfig: %j', platformConfig);
// TODO: this should possibly also rollback memory to default
async.eachSeries([ 'mysql', 'postgresql', 'mail', 'mongodb' ], function iterator(containerName, iteratorCallback) {
const containerConfig = platformConfig[containerName];
async.eachSeries([ 'mysql', 'postgresql', 'mail', 'mongodb' ], function iterator(serviceName, iteratorCallback) {
const containerConfig = platformConfig[serviceName];
let memory, memorySwap;
if (containerConfig && containerConfig.memory && containerConfig.memorySwap) {
memory = containerConfig.memory;
memorySwap = containerConfig.memorySwap;
} else {
memory = defaultMemoryLimits[containerName];
memory = KNOWN_SERVICES[serviceName].defaultMemoryLimit;
memorySwap = memory * 2;
}
const args = `update --memory ${memory} --memory-swap ${memorySwap} ${containerName}`.split(' ');
shell.exec(`update${containerName}`, '/usr/bin/docker', args, { }, iteratorCallback);
const args = `update --memory ${memory} --memory-swap ${memorySwap} ${serviceName}`.split(' ');
shell.spawn(`update${serviceName}`, '/usr/bin/docker', args, { }, iteratorCallback);
}, callback);
}
function startAddons(existingInfra, callback) {
function startServices(existingInfra, callback) {
assert.strictEqual(typeof existingInfra, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -381,7 +638,7 @@ function startAddons(existingInfra, callback) {
// always start addons on any infra change, regardless of minor or major update
if (existingInfra.version !== infra.version) {
debug(`startAddons: ${existingInfra.version} -> ${infra.version}. starting all addons`);
debug(`startServices: ${existingInfra.version} -> ${infra.version}. starting all services`);
startFuncs.push(
startMysql.bind(null, existingInfra),
startPostgresql.bind(null, existingInfra),
@@ -397,7 +654,7 @@ function startAddons(existingInfra, callback) {
if (infra.images.mail.tag !== existingInfra.images.mail.tag) startFuncs.push(mail.startMail);
if (infra.images.redis.tag !== existingInfra.images.redis.tag) startFuncs.push(startRedis.bind(null, existingInfra));
debug('startAddons: existing infra. incremental addon create %j', startFuncs.map(function (f) { return f.name; }));
debug('startServices: existing infra. incremental service create %j', startFuncs.map(function (f) { return f.name; }));
}
async.series(startFuncs, callback);
@@ -621,23 +878,17 @@ function setupSendMail(app, options, callback) {
var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length
mailboxdb.getByOwnerId(app.id, function (error, results) {
if (error) return callback(error);
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
var env = [
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
{ name: 'MAIL_SMTP_PORT', value: '2525' },
{ name: 'MAIL_SMTPS_PORT', value: '2465' },
{ name: 'MAIL_SMTP_USERNAME', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_SMTP_PASSWORD', value: password },
{ name: 'MAIL_FROM', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_DOMAIN', value: app.domain }
];
debugApp(app, 'Setting sendmail addon config to %j', env);
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
});
var env = [
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
{ name: 'MAIL_SMTP_PORT', value: '2525' },
{ name: 'MAIL_SMTPS_PORT', value: '2465' },
{ name: 'MAIL_SMTP_USERNAME', value: app.mailboxName + '@' + app.domain },
{ name: 'MAIL_SMTP_PASSWORD', value: password },
{ name: 'MAIL_FROM', value: app.mailboxName + '@' + app.domain },
{ name: 'MAIL_DOMAIN', value: app.domain }
];
debugApp(app, 'Setting sendmail addon config to %j', env);
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
});
}
@@ -663,23 +914,17 @@ function setupRecvMail(app, options, callback) {
var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length
mailboxdb.getByOwnerId(app.id, function (error, results) {
if (error) return callback(error);
var env = [
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
{ name: 'MAIL_IMAP_PORT', value: '9993' },
{ name: 'MAIL_IMAP_USERNAME', value: app.mailboxName + '@' + app.domain },
{ name: 'MAIL_IMAP_PASSWORD', value: password },
{ name: 'MAIL_TO', value: app.mailboxName + '@' + app.domain },
{ name: 'MAIL_DOMAIN', value: app.domain }
];
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
var env = [
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
{ name: 'MAIL_IMAP_PORT', value: '9993' },
{ name: 'MAIL_IMAP_USERNAME', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_IMAP_PASSWORD', value: password },
{ name: 'MAIL_TO', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_DOMAIN', value: app.domain }
];
debugApp(app, 'Setting sendmail addon config to %j', env);
appdb.setAddonConfig(app.id, 'recvmail', env, callback);
});
debugApp(app, 'Setting sendmail addon config to %j', env);
appdb.setAddonConfig(app.id, 'recvmail', env, callback);
});
}
@@ -713,36 +958,40 @@ function startMysql(existingInfra, callback) {
const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.mysql.tag, tag);
if (upgrading) {
debug('startMysql: mysql will be upgraded');
shell.sudoSync('startMysql', `${RMADDON_CMD} mysql`);
}
if (upgrading) debug('startMysql: mysql will be upgraded');
const upgradeFunc = upgrading ? shell.sudo.bind(null, 'startMysql', [ RMADDONDIR_CMD, 'mysql' ], {}) : (next) => next();
const cmd = `docker run --restart=always -d --name="mysql" \
--net cloudron \
--net-alias mysql \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=mysql \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_MYSQL_TOKEN=${cloudronToken} \
-e CLOUDRON_MYSQL_ROOT_HOST=172.18.0.1 \
-e CLOUDRON_MYSQL_ROOT_PASSWORD=${rootPassword} \
-v "${dataDir}/mysql:/var/lib/mysql" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
shell.execSync('startMysql', cmd);
waitForAddon('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error) {
upgradeFunc(function (error) {
if (error) return callback(error);
if (!upgrading) return callback(null);
importDatabase('mysql', callback);
const cmd = `docker run --restart=always -d --name="mysql" \
--net cloudron \
--net-alias mysql \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=mysql \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_MYSQL_TOKEN=${cloudronToken} \
-e CLOUDRON_MYSQL_ROOT_HOST=172.18.0.1 \
-e CLOUDRON_MYSQL_ROOT_PASSWORD=${rootPassword} \
-v "${dataDir}/mysql:/var/lib/mysql" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
shell.exec('startMysql', cmd, function (error) {
if (error) return callback(error);
waitForService('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error) {
if (error) return callback(error);
if (!upgrading) return callback(null);
importDatabase('mysql', callback);
});
});
});
}
@@ -765,7 +1014,7 @@ function setupMySql(app, options, callback) {
password: error ? hat(4 * 48) : existingPassword // see box#362 for password length
};
getAddonDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) {
@@ -802,7 +1051,7 @@ function clearMySql(app, options, callback) {
const database = mysqlDatabaseName(app.id);
getAddonDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}/clear?access_token=${result.token}`, { rejectUnauthorized: false }, function (error, response) {
@@ -821,7 +1070,7 @@ function teardownMySql(app, options, callback) {
const database = mysqlDatabaseName(app.id);
const username = database;
getAddonDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
if (error) return callback(error);
request.delete(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}?access_token=${result.token}&username=${username}`, { rejectUnauthorized: false }, function (error, response) {
@@ -868,7 +1117,7 @@ function backupMySql(app, options, callback) {
debugApp(app, 'Backing up mysql');
getAddonDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
if (error) return callback(error);
const url = `https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}/backup?access_token=${result.token}`;
@@ -887,7 +1136,7 @@ function restoreMySql(app, options, callback) {
callback = once(callback); // protect from multiple returns with streams
getAddonDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
if (error) return callback(error);
var input = fs.createReadStream(dumpPath('mysql', app.id));
@@ -921,35 +1170,39 @@ function startPostgresql(existingInfra, callback) {
const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.postgresql.tag, tag);
if (upgrading) {
debug('startPostgresql: postgresql will be upgraded');
shell.sudoSync('startPostgresql', `${RMADDON_CMD} postgresql`);
}
if (upgrading) debug('startPostgresql: postgresql will be upgraded');
const upgradeFunc = upgrading ? shell.sudo.bind(null, 'startPostgresql', [ RMADDONDIR_CMD, 'postgresql' ], {}) : (next) => next();
const cmd = `docker run --restart=always -d --name="postgresql" \
--net cloudron \
--net-alias postgresql \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=postgresql \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_POSTGRESQL_ROOT_PASSWORD="${rootPassword}" \
-e CLOUDRON_POSTGRESQL_TOKEN="${cloudronToken}" \
-v "${dataDir}/postgresql:/var/lib/postgresql" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
shell.execSync('startPostgresql', cmd);
waitForAddon('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error) {
upgradeFunc(function (error) {
if (error) return callback(error);
if (!upgrading) return callback(null);
importDatabase('postgresql', callback);
const cmd = `docker run --restart=always -d --name="postgresql" \
--net cloudron \
--net-alias postgresql \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=postgresql \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_POSTGRESQL_ROOT_PASSWORD="${rootPassword}" \
-e CLOUDRON_POSTGRESQL_TOKEN="${cloudronToken}" \
-v "${dataDir}/postgresql:/var/lib/postgresql" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
shell.exec('startPostgresql', cmd, function (error) {
if (error) return callback(error);
waitForService('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error) {
if (error) return callback(error);
if (!upgrading) return callback(null);
importDatabase('postgresql', callback);
});
});
});
}
@@ -971,7 +1224,7 @@ function setupPostgreSql(app, options, callback) {
password: error ? hat(4 * 128) : existingPassword
};
getAddonDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/databases?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) {
@@ -1003,7 +1256,7 @@ function clearPostgreSql(app, options, callback) {
debugApp(app, 'Clearing postgresql');
getAddonDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/databases/${database}/clear?access_token=${result.token}&username=${username}`, { rejectUnauthorized: false }, function (error, response) {
@@ -1022,7 +1275,7 @@ function teardownPostgreSql(app, options, callback) {
const { database, username } = postgreSqlNames(app.id);
getAddonDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
if (error) return callback(error);
request.delete(`https://${result.ip}:3000/databases/${database}?access_token=${result.token}&username=${username}`, { rejectUnauthorized: false }, function (error, response) {
@@ -1043,7 +1296,7 @@ function backupPostgreSql(app, options, callback) {
const { database } = postgreSqlNames(app.id);
getAddonDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
if (error) return callback(error);
const url = `https://${result.ip}:3000/databases/${database}/backup?access_token=${result.token}`;
@@ -1062,7 +1315,7 @@ function restorePostgreSql(app, options, callback) {
callback = once(callback); // protect from multiple returns with streams
getAddonDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
if (error) return callback(error);
var input = fs.createReadStream(dumpPath('postgresql', app.id));
@@ -1091,35 +1344,39 @@ function startMongodb(existingInfra, callback) {
const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.mongodb.tag, tag);
if (upgrading) {
debug('startMongodb: mongodb will be upgraded');
shell.sudoSync('startMongodb', `${RMADDON_CMD} mongodb`);
}
if (upgrading) debug('startMongodb: mongodb will be upgraded');
const upgradeFunc = upgrading ? shell.sudo.bind(null, 'startMongodb', [ RMADDONDIR_CMD, 'mongodb' ], {}) : (next) => next();
const cmd = `docker run --restart=always -d --name="mongodb" \
--net cloudron \
--net-alias mongodb \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=mongodb \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_MONGODB_ROOT_PASSWORD="${rootPassword}" \
-e CLOUDRON_MONGODB_TOKEN="${cloudronToken}" \
-v "${dataDir}/mongodb:/var/lib/mongodb" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
shell.execSync('startMongodb', cmd);
waitForAddon('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error) {
upgradeFunc(function (error) {
if (error) return callback(error);
if (!upgrading) return callback(null);
importDatabase('mongodb', callback);
const cmd = `docker run --restart=always -d --name="mongodb" \
--net cloudron \
--net-alias mongodb \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=mongodb \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_MONGODB_ROOT_PASSWORD="${rootPassword}" \
-e CLOUDRON_MONGODB_TOKEN="${cloudronToken}" \
-v "${dataDir}/mongodb:/var/lib/mongodb" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
shell.exec('startMongodb', cmd, function (error) {
if (error) return callback(error);
waitForService('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error) {
if (error) return callback(error);
if (!upgrading) return callback(null);
importDatabase('mongodb', callback);
});
});
});
}
@@ -1139,7 +1396,7 @@ function setupMongoDb(app, options, callback) {
password: error ? hat(4 * 128) : existingPassword
};
getAddonDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/databases?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) {
@@ -1169,7 +1426,7 @@ function clearMongodb(app, options, callback) {
debugApp(app, 'Clearing mongodb');
getAddonDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/databases/${app.id}/clear?access_token=${result.token}`, { rejectUnauthorized: false }, function (error, response) {
@@ -1188,7 +1445,7 @@ function teardownMongoDb(app, options, callback) {
debugApp(app, 'Tearing down mongodb');
getAddonDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
if (error) return callback(error);
request.delete(`https://${result.ip}:3000/databases/${app.id}?access_token=${result.token}`, { rejectUnauthorized: false }, function (error, response) {
@@ -1207,7 +1464,7 @@ function backupMongoDb(app, options, callback) {
debugApp(app, 'Backing up mongodb');
getAddonDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
if (error) return callback(error);
const url = `https://${result.ip}:3000/databases/${app.id}/backup?access_token=${result.token}`;
@@ -1224,7 +1481,7 @@ function restoreMongoDb(app, options, callback) {
debugApp(app, 'restoreMongoDb');
getAddonDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
if (error) return callback(error);
const readStream = fs.createReadStream(dumpPath('mongodb', app.id));
@@ -1313,9 +1570,9 @@ function setupRedis(app, options, callback) {
];
async.series([
shell.execSync.bind(null, 'startRedis', cmd),
shell.exec.bind(null, 'startRedis', cmd),
appdb.setAddonConfig.bind(null, app.id, 'redis', env),
waitForAddon.bind(null, 'redis-' + app.id, 'CLOUDRON_REDIS_TOKEN')
waitForService.bind(null, 'redis-' + app.id, 'CLOUDRON_REDIS_TOKEN')
], function (error) {
if (error) debug('Error setting up redis: ', error);
callback(error);
@@ -1331,7 +1588,7 @@ function clearRedis(app, options, callback) {
debugApp(app, 'Clearing redis');
getAddonDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
getServiceDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/clear?access_token=${result.token}`, { rejectUnauthorized: false }, function (error, response) {
@@ -1358,7 +1615,7 @@ function teardownRedis(app, options, callback) {
container.remove(removeOptions, function (error) {
if (error && error.statusCode !== 404) return callback(new Error('Error removing container:' + error));
shell.sudo('removeVolume', [ RMADDON_CMD, 'redis', app.id ], function (error) {
shell.sudo('removeVolume', [ RMADDONDIR_CMD, 'redis', app.id ], {}, function (error) {
if (error) return callback(new Error('Error removing redis data:' + error));
rimraf(path.join(paths.LOG_DIR, `redis-${app.id}`), function (error) {
@@ -1377,7 +1634,7 @@ function backupRedis(app, options, callback) {
debugApp(app, 'Backing up redis');
getAddonDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
getServiceDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
if (error) return callback(error);
const url = `https://${result.ip}:3000/backup?access_token=${result.token}`;
@@ -1392,7 +1649,7 @@ function restoreRedis(app, options, callback) {
debugApp(app, 'Restoring redis');
getAddonDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
getServiceDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
if (error) return callback(error);
let input;
@@ -1414,3 +1671,35 @@ function restoreRedis(app, options, callback) {
input.pipe(restoreReq);
});
}
function statusDocker(callback) {
assert.strictEqual(typeof callback, 'function');
docker.ping(function (error) {
callback(null, { status: error ? exports.SERVICE_STATUS_STOPPED: exports.SERVICE_STATUS_ACTIVE });
});
}
function restartDocker(callback) {
assert.strictEqual(typeof callback, 'function');
shell.sudo('restartdocker', [ path.join(__dirname, 'scripts/restartdocker.sh') ], {}, NOOP_CALLBACK);
callback(null);
}
function statusUnbound(callback) {
assert.strictEqual(typeof callback, 'function');
shell.exec('statusUnbound', 'systemctl is-active unbound', function (error) {
callback(null, { status: error ? exports.SERVICE_STATUS_STOPPED : exports.SERVICE_STATUS_ACTIVE });
});
}
function restartUnbound(callback) {
assert.strictEqual(typeof callback, 'function');
shell.sudo('restartunbound', [ path.join(__dirname, 'scripts/restartunbound.sh') ], {}, NOOP_CALLBACK);
callback(null);
}

View File

@@ -18,6 +18,7 @@ exports = module.exports = {
getAddonConfigByName: getAddonConfigByName,
unsetAddonConfig: unsetAddonConfig,
unsetAddonConfigByAppId: unsetAddonConfigByAppId,
getAppIdByAddonConfigValue: getAppIdByAddonConfigValue,
setHealth: setHealth,
setInstallationCommand: setInstallationCommand,
@@ -61,7 +62,6 @@ var assert = require('assert'),
async = require('async'),
database = require('./database.js'),
DatabaseError = require('./databaseerror'),
mailboxdb = require('./mailboxdb.js'),
safe = require('safetydance'),
util = require('util');
@@ -69,7 +69,7 @@ var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationSta
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'subdomains.subdomain AS location', 'subdomains.domain',
'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit',
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.ts' ].join(',');
'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.mailboxName', 'apps.enableAutomaticUpdate', 'apps.ts' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(',');
@@ -120,6 +120,7 @@ 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);
@@ -276,13 +277,14 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
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) ' +
' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId ]
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({
@@ -304,14 +306,6 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
});
});
// only allocate a mailbox if mailboxName is set
if (data.mailboxName) {
queries.push({
query: 'INSERT INTO mailboxes (name, type, domain, ownerId, ownerType) VALUES (?, ?, ?, ?, ?)',
args: [ data.mailboxName, mailboxdb.TYPE_MAILBOX, domain, id, mailboxdb.OWNER_TYPE_APP ]
});
}
if (data.alternateDomains) {
data.alternateDomains.forEach(function (d) {
queries.push({
@@ -376,7 +370,6 @@ function del(id, callback) {
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 ] }
@@ -384,7 +377,7 @@ function del(id, callback) {
database.transaction(queries, function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results[4].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (results[3].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null);
});
@@ -442,12 +435,8 @@ function updateWithConstraints(id, app, constraints, callback) {
});
}
if ('location' in app) {
queries.push({ query: 'UPDATE subdomains SET subdomain = ? WHERE appId = ? AND type = ?', args: [ app.location, id, exports.SUBDOMAIN_TYPE_PRIMARY ]});
}
if ('domain' in app) {
queries.push({ query: 'UPDATE subdomains SET domain = ? WHERE appId = ? AND type = ?', args: [ app.domain, id, exports.SUBDOMAIN_TYPE_PRIMARY ]});
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) {
@@ -621,6 +610,20 @@ 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');

View File

@@ -72,7 +72,6 @@ var appdb = require('./appdb.js'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
mail = require('./mail.js'),
mailboxdb = require('./mailboxdb.js'),
manifestFormat = require('cloudron-manifestformat'),
os = require('os'),
path = require('path'),
@@ -154,7 +153,8 @@ function validatePortBindings(portBindings, manifest) {
config.get('ldapPort'), /* ldap server (lo) */
3306, /* mysql (lo) */
4190, /* managesieve */
8000, /* graphite (lo) */
8000, /* ESXi monitoring */
8417, /* graphite (lo) */
];
if (!portBindings) return null;
@@ -346,7 +346,7 @@ function removeInternalFields(app) {
'location', 'domain', 'fqdn', 'mailboxName',
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'xFrameOptions',
'sso', 'debugMode', 'robotsTxt', 'enableBackup', 'creationTime', 'updateTime', 'ts',
'alternateDomains', 'ownerId', 'env');
'alternateDomains', 'ownerId', 'env', 'enableAutomaticUpdate');
}
function removeRestrictedFields(app) {
@@ -399,13 +399,7 @@ function get(appId, callback) {
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!error) app.mailboxName = mailboxes[0].name;
callback(null, app);
});
callback(null, app);
});
});
}
@@ -439,13 +433,7 @@ function getByIpAddress(ip, callback) {
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!error) app.mailboxName = mailboxes[0].name;
callback(null, app);
});
callback(null, app);
});
});
});
@@ -471,13 +459,7 @@ function getAll(callback) {
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return iteratorDone(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!error) app.mailboxName = mailboxes[0].name;
iteratorDone(null, app);
});
iteratorDone(null, app);
}, function (error) {
if (error) return callback(error);
@@ -543,11 +525,13 @@ function install(data, user, auditSource, callback) {
debugMode = data.debugMode || null,
robotsTxt = data.robotsTxt || null,
enableBackup = 'enableBackup' in data ? data.enableBackup : true,
enableAutomaticUpdate = 'enableAutomaticUpdate' in data ? data.enableAutomaticUpdate : true,
backupId = data.backupId || null,
backupFormat = data.backupFormat || 'tgz',
ownerId = data.ownerId,
alternateDomains = data.alternateDomains || [],
env = data.env || {};
env = data.env || {},
mailboxName = data.mailboxName || '';
assert(data.appStoreId || data.manifest); // atleast one of them is required
@@ -588,6 +572,13 @@ function install(data, user, auditSource, callback) {
error = validateEnv(env);
if (error) return callback(error);
if (mailboxName) {
error = mail.validateName(mailboxName);
if (error) return callback(error);
} else {
mailboxName = mailboxNameForLocation(location, manifest);
}
var appId = uuid.v4();
if (icon) {
@@ -621,9 +612,10 @@ function install(data, user, auditSource, callback) {
xFrameOptions: xFrameOptions,
sso: sso,
debugMode: debugMode,
mailboxName: mailboxNameForLocation(location, manifest),
mailboxName: mailboxName,
restoreConfig: backupId ? { backupId: backupId, backupFormat: backupFormat } : null,
enableBackup: enableBackup,
enableAutomaticUpdate: enableAutomaticUpdate,
robotsTxt: robotsTxt,
alternateDomains: alternateDomains,
env: env
@@ -682,12 +674,14 @@ function configure(appId, data, user, auditSource, callback) {
get(appId, function (error, app) {
if (error) return callback(error);
let domain, location, portBindings, values = { }, mailboxName;
if ('location' in data) location = values.location = data.location.toLowerCase();
else location = app.location;
if ('domain' in data) domain = values.domain = data.domain.toLowerCase();
else domain = app.domain;
let domain, location, portBindings, values = { };
if ('location' in data && 'domain' in data) {
location = values.location = data.location.toLowerCase();
domain = values.domain = data.domain.toLowerCase();
} else {
location = app.location;
domain = app.domain;
}
if ('accessRestriction' in data) {
values.accessRestriction = data.accessRestriction;
@@ -729,15 +723,15 @@ function configure(appId, data, user, auditSource, callback) {
}
if ('mailboxName' in data) {
if (data.mailboxName === '') { // special case to reset back to .app
mailboxName = mailboxNameForLocation(location, app.manifest);
} else {
if (data.mailboxName) {
error = mail.validateName(data.mailboxName);
if (error) return callback(error);
mailboxName = data.mailboxName;
values.mailboxName = data.mailboxName;
} else {
values.mailboxName = mailboxNameForLocation(location, app.manifest);
}
} else { // keep existing name or follow the new location
mailboxName = app.mailboxName.endsWith('.app') ? mailboxNameForLocation(location, app.manifest) : app.mailboxName;
values.mailboxName = app.mailboxName.endsWith('.app') ? mailboxNameForLocation(location, app.manifest) : app.mailboxName;
}
if ('alternateDomains' in data) {
@@ -773,34 +767,26 @@ function configure(appId, data, user, auditSource, callback) {
}
if ('enableBackup' in data) values.enableBackup = data.enableBackup;
if ('enableAutomaticUpdate' in data) values.enableAutomaticUpdate = data.enableAutomaticUpdate;
values.oldConfig = getAppConfig(app);
debug('Will configure app with id:%s values:%j', appId, values);
// make the mailbox name follow the apps new location, if the user did not set it explicitly
mailboxdb.updateName(app.mailboxName /* old */, values.oldConfig.domain, mailboxName, domain, function (error) {
if (mailboxName.endsWith('.app')) error = null; // ignore internal mailbox conflict errors since we want to show location conflict errors in the UI
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new AppsError(AppsError.ALREADY_EXISTS, 'This mailbox is already taken'));
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_CONFIGURE, values, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_CONFIGURE, values, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
taskmanager.restartAppTask(appId);
// fetch fresh app object for eventlog
get(appId, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: result });
// fetch fresh app object for eventlog
get(appId, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: result });
callback(null);
});
callback(null);
});
});
});
@@ -987,7 +973,8 @@ function clone(appId, data, user, auditSource, callback) {
domain = data.domain.toLowerCase(),
portBindings = data.portBindings || null,
backupId = data.backupId,
ownerId = data.ownerId;
ownerId = data.ownerId,
mailboxName = data.mailboxName || '';
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof location, 'string');
@@ -1005,13 +992,22 @@ function clone(appId, data, user, auditSource, callback) {
if (!backupInfo.manifest) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore config'));
const manifest = backupInfo.manifest;
// re-validate because this new box version may not accept old configs
error = checkManifestConstraints(backupInfo.manifest);
error = checkManifestConstraints(manifest);
if (error) return callback(error);
error = validatePortBindings(portBindings, backupInfo.manifest);
error = validatePortBindings(portBindings, manifest);
if (error) return callback(error);
if (mailboxName) {
error = mail.validateName(mailboxName);
if (error) return callback(error);
} else {
mailboxName = mailboxNameForLocation(location, manifest);
}
domains.get(domain, function (error, domainObject) {
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
@@ -1020,7 +1016,7 @@ function clone(appId, data, user, auditSource, callback) {
error = domains.validateHostname(location, domainObject);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message));
var newAppId = uuid.v4(), manifest = backupInfo.manifest;
var newAppId = uuid.v4();
var data = {
installationState: appdb.ISTATE_PENDING_CLONE,
@@ -1029,7 +1025,7 @@ function clone(appId, data, user, auditSource, callback) {
xFrameOptions: app.xFrameOptions,
restoreConfig: { backupId: backupId, backupFormat: backupInfo.format },
sso: !!app.sso,
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app',
mailboxName: mailboxName,
enableBackup: app.enableBackup,
robotsTxt: app.robotsTxt,
env: app.env
@@ -1220,6 +1216,7 @@ function autoupdateApps(updateInfo, auditSource, callback) { // updateInfo is {
assert.strictEqual(typeof callback, 'function');
function canAutoupdateApp(app, newManifest) {
if (!app.enableAutomaticUpdate) return new Error('Automatic update disabled');
if ((semver.major(app.manifest.version) !== 0) && (semver.major(app.manifest.version) !== semver.major(newManifest.version))) return new Error('Major version change'); // major changes are blocking
const newTcpPorts = newManifest.tcpPorts || { };

View File

@@ -55,6 +55,8 @@ var COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd.config.ejs', {
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); };
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -210,7 +212,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);
});
}
@@ -220,7 +222,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);
});
}
@@ -239,7 +241,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);
});
});
}
@@ -248,7 +250,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) {
@@ -539,7 +541,7 @@ function install(app, callback) {
updateApp.bind(null, app, { installationProgress: '60, 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),
backups.restoreApp.bind(null, app, app.manifest.addons, restoreConfig, (progress) => updateApp(app, { installationProgress: progress.message }, NOOP_CALLBACK))
], next);
}
},
@@ -581,7 +583,7 @@ function backup(app, callback) {
async.series([
updateApp.bind(null, app, { installationProgress: '10, Backing up' }),
backups.backupApp.bind(null, app),
backups.backupApp.bind(null, app, (progress) => updateApp(app, { installationProgress: progress.message }, NOOP_CALLBACK)),
// done!
function (callback) {
@@ -694,7 +696,7 @@ function update(app, callback) {
async.series([
updateApp.bind(null, app, { installationProgress: '15, Backing up app' }),
backups.backupApp.bind(null, app)
backups.backupApp.bind(null, app, (progress) => updateApp(app, { installationProgress: progress.message }, NOOP_CALLBACK))
], function (error) {
if (error) error.backupError = true;
next(error);

View File

@@ -10,9 +10,9 @@ exports = module.exports = {
get: get,
startBackupTask: startBackupTask,
ensureBackup: ensureBackup,
backup: backup,
restore: restore,
backupApp: backupApp,
@@ -53,7 +53,6 @@ var addons = require('./addons.js'),
once = require('once'),
path = require('path'),
paths = require('./paths.js'),
progress = require('./progress.js'),
progressStream = require('progress-stream'),
safe = require('safetydance'),
shell = require('./shell.js'),
@@ -61,12 +60,12 @@ var addons = require('./addons.js'),
superagent = require('superagent'),
syncer = require('./syncer.js'),
tar = require('tar-fs'),
tasks = require('./tasks.js'),
util = require('util'),
zlib = require('zlib');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
var BACKUPTASK_CMD = path.join(__dirname, 'backuptask.js');
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
const BACKUP_UPLOAD_CMD = path.join(__dirname, 'scripts/backupupload.js');
function debugApp(app) {
assert(typeof app === 'object');
@@ -181,11 +180,6 @@ function getBackupFilePath(backupConfig, backupId, format) {
}
}
function log(detail) {
safe.fs.appendFileSync(paths.BACKUP_LOG_FILE, detail + '\n', 'utf8');
progress.setDetail(progress.BACKUP, detail);
}
function encryptFilePath(filePath, key) {
assert.strictEqual(typeof filePath, 'string');
assert.strictEqual(typeof key, 'string');
@@ -238,10 +232,6 @@ function createReadStream(sourceFile, key) {
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
ps.on('progress', function(progress) {
debug('createReadStream: %s@%s (%s)', Math.round(progress.transferred/1024/1024) + 'M', Math.round(progress.speed/1024/1024) + 'Mbps', sourceFile);
});
if (key !== null) {
var encrypt = crypto.createCipher('aes-256-cbc', key);
encrypt.on('error', function (error) {
@@ -287,7 +277,7 @@ function createTarPackStream(sourceDir, key) {
});
var gzip = zlib.createGzip({});
var ps = progressStream({ time: 10000 }); // display a progress every 10 seconds
var ps = progressStream({ time: 10000 }); // emit 'pgoress' every 10 seconds
pack.on('error', function (error) {
debug('createTarPackStream: tar stream error.', error);
@@ -299,10 +289,6 @@ function createTarPackStream(sourceDir, key) {
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
ps.on('progress', function(progress) {
debug('createTarPackStream: %s@%s', Math.round(progress.transferred/1024/1024) + 'M', Math.round(progress.speed/1024/1024) + 'Mbps');
});
if (key !== null) {
var encrypt = crypto.createCipher('aes-256-cbc', key);
encrypt.on('error', function (error) {
@@ -315,17 +301,13 @@ function createTarPackStream(sourceDir, key) {
}
}
function sync(backupConfig, backupId, dataDir, callback) {
function sync(backupConfig, backupId, dataDir, progressCallback, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof dataDir, 'string');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
function setBackupProgress(message) {
debug('%s', message);
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, message);
}
syncer.sync(dataDir, function processTask(task, iteratorCallback) {
debug('sync: processing task: %j', task);
// the empty task.path is special to signify the directory
@@ -333,12 +315,12 @@ function sync(backupConfig, backupId, dataDir, callback) {
const backupFilePath = path.join(getBackupFilePath(backupConfig, backupId, backupConfig.format), destPath);
if (task.operation === 'removedir') {
setBackupProgress(`Removing directory ${backupFilePath}`);
debug(`Removing directory ${backupFilePath}`);
return api(backupConfig.provider).removeDir(backupConfig, backupFilePath)
.on('progress', setBackupProgress)
.on('progress', (message) => progressCallback({ message }))
.on('done', iteratorCallback);
} else if (task.operation === 'remove') {
setBackupProgress(`Removing ${backupFilePath}`);
debug(`Removing ${backupFilePath}`);
return api(backupConfig.provider).remove(backupConfig, backupFilePath, iteratorCallback);
}
@@ -347,16 +329,19 @@ function sync(backupConfig, backupId, dataDir, callback) {
retryCallback = once(retryCallback); // protect again upload() erroring much later after read stream error
++retryCount;
debug(`${task.operation} ${task.path} try ${retryCount}`);
progressCallback({ message: `${task.operation} ${task.path} try ${retryCount}` });
if (task.operation === 'add') {
setBackupProgress(`Adding ${task.path} position ${task.position} try ${retryCount}`);
debug(`Adding ${task.path} position ${task.position} try ${retryCount}`);
var stream = createReadStream(path.join(dataDir, task.path), backupConfig.key || null);
stream.on('error', function (error) {
setBackupProgress(`read stream error for ${task.path}: ${error.message}`);
debug(`read stream error for ${task.path}: ${error.message}`);
retryCallback();
}); // ignore error if file disappears
stream.on('progress', function(progress) {
progressCallback({ message: `Uploading ${task.path}: ${Math.round(progress.transferred/1024/1024)}M@${Math.round(progress.speed/1024/1024)}` });
});
api(backupConfig.provider).upload(backupConfig, backupFilePath, stream, function (error) {
setBackupProgress(error ? `Error uploading ${task.path} try ${retryCount}: ${error.message}` : `Uploaded ${task.path}`);
debug(error ? `Error uploading ${task.path} try ${retryCount}: ${error.message}` : `Uploaded ${task.path}`);
retryCallback(error);
});
}
@@ -388,14 +373,15 @@ function saveFsMetadata(appDataDir, callback) {
callback();
}
// this function is called via backuptask (since it needs root to traverse app's directory)
function upload(backupId, format, dataDir, callback) {
// this function is called via backupupload (since it needs root to traverse app's directory)
function upload(backupId, format, dataDir, progressCallback, callback) {
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof format, 'string');
assert.strictEqual(typeof dataDir, 'string');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
debug('upload: id %s format %s dataDir %s', backupId, format, dataDir);
debug(`upload: id ${backupId} format ${format} dataDir ${dataDir}`);
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
@@ -405,6 +391,9 @@ function upload(backupId, format, dataDir, callback) {
retryCallback = once(retryCallback); // protect again upload() erroring much later after tar stream error
var tarStream = createTarPackStream(dataDir, backupConfig.key || null);
tarStream.on('progress', function(progress) {
progressCallback({ message: `Uploading ${Math.round(progress.transferred/1024/1024)}M@${Math.round(progress.speed/1024/1024)}Mbps` });
});
tarStream.on('error', retryCallback); // already returns BackupsError
api(backupConfig.provider).upload(backupConfig, getBackupFilePath(backupConfig, backupId, format), tarStream, retryCallback);
@@ -412,7 +401,7 @@ function upload(backupId, format, dataDir, callback) {
} else {
async.series([
saveFsMetadata.bind(null, dataDir),
sync.bind(null, backupConfig, backupId, dataDir)
sync.bind(null, backupConfig, backupId, dataDir, progressCallback)
], callback);
}
});
@@ -435,10 +424,6 @@ function tarExtract(inStream, destination, key, callback) {
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
ps.on('progress', function(progress) {
debug('tarExtract: %s@%s', Math.round(progress.transferred/1024/1024) + 'M', Math.round(progress.speed/1024/1024) + 'Mbps');
});
gunzip.on('error', function (error) {
debug('tarExtract: gunzip stream error.', error);
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
@@ -464,13 +449,15 @@ function tarExtract(inStream, destination, key, callback) {
} else {
inStream.pipe(ps).pipe(gunzip).pipe(extract);
}
return ps;
}
function restoreFsMetadata(appDataDir, callback) {
assert.strictEqual(typeof appDataDir, 'string');
assert.strictEqual(typeof callback, 'function');
log('Recreating empty directories');
debug(`Recreating empty directories in ${appDataDir}`);
var metadataJson = safe.fs.readFileSync(path.join(appDataDir, 'fsmetadata.json'), 'utf8');
if (metadataJson === null) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error loading fsmetadata.txt:' + safe.error.message));
@@ -492,10 +479,11 @@ function restoreFsMetadata(appDataDir, callback) {
});
}
function downloadDir(backupConfig, backupFilePath, destDir, callback) {
function downloadDir(backupConfig, backupFilePath, destDir, progressCallback, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupFilePath, 'string');
assert.strictEqual(typeof destDir, 'string');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
debug(`downloadDir: ${backupFilePath} to ${destDir}`);
@@ -519,7 +507,7 @@ function downloadDir(backupConfig, backupFilePath, destDir, callback) {
let destStream = createWriteStream(destFilePath, backupConfig.key || null);
destStream.on('error', callback);
debug(`downloadDir: Copying ${entry.fullPath} to ${destFilePath}`);
progressCallback({ message: `Downloading ${entry.fullPath} to ${destFilePath}` });
sourceStream.pipe(destStream, { end: true }).on('finish', callback);
});
@@ -531,25 +519,27 @@ function downloadDir(backupConfig, backupFilePath, destDir, callback) {
}, callback);
}
function download(backupConfig, backupId, format, dataDir, callback) {
function download(backupConfig, backupId, format, dataDir, progressCallback, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof format, 'string');
assert.strictEqual(typeof dataDir, 'string');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
safe.fs.unlinkSync(paths.BACKUP_LOG_FILE); // start fresh log file
log(`Downloading ${backupId} of format ${format} to ${dataDir}`);
debug(`download - Downloading ${backupId} of format ${format} to ${dataDir}`);
if (format === 'tgz') {
api(backupConfig.provider).download(backupConfig, getBackupFilePath(backupConfig, backupId, format), function (error, sourceStream) {
if (error) return callback(error);
tarExtract(sourceStream, dataDir, backupConfig.key || null, callback);
let ps = tarExtract(sourceStream, dataDir, backupConfig.key || null, callback);
ps.on('progress', function (progress) {
progressCallback({ message: `Downloading ${Math.round(progress.transferred/1024/1024)}M@${Math.round(progress.speed/1024/1024)}Mbps` });
});
});
} else {
downloadDir(backupConfig, getBackupFilePath(backupConfig, backupId, format), dataDir, function (error) {
downloadDir(backupConfig, getBackupFilePath(backupConfig, backupId, format), dataDir, progressCallback, function (error) {
if (error) return callback(error);
restoreFsMetadata(dataDir, callback);
@@ -557,12 +547,13 @@ function download(backupConfig, backupId, format, dataDir, callback) {
}
}
function restore(backupConfig, backupId, callback) {
function restore(backupConfig, backupId, progressCallback, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
download(backupConfig, backupId, backupConfig.format, paths.BOX_DATA_DIR, function (error) {
download(backupConfig, backupId, backupConfig.format, paths.BOX_DATA_DIR, progressCallback, function (error) {
if (error) return callback(error);
debug('restore: download completed, importing database');
@@ -577,10 +568,11 @@ function restore(backupConfig, backupId, callback) {
});
}
function restoreApp(app, addonsToRestore, restoreConfig, callback) {
function restoreApp(app, addonsToRestore, restoreConfig, progressCallback, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof addonsToRestore, 'object');
assert.strictEqual(typeof restoreConfig, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
var appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id));
@@ -591,7 +583,7 @@ function restoreApp(app, addonsToRestore, restoreConfig, callback) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
async.series([
download.bind(null, backupConfig, restoreConfig.backupId, restoreConfig.backupFormat, appDataDir),
download.bind(null, backupConfig, restoreConfig.backupId, restoreConfig.backupFormat, appDataDir, progressCallback),
addons.restoreAddons.bind(null, app, addonsToRestore)
], function (error) {
debug('restoreApp: time: %s', (new Date() - startTime)/1000);
@@ -601,44 +593,27 @@ function restoreApp(app, addonsToRestore, restoreConfig, callback) {
});
}
function runBackupTask(backupId, format, dataDir, callback) {
function runBackupUpload(backupId, format, dataDir, progressCallback, callback) {
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof format, 'string');
assert.strictEqual(typeof dataDir, 'string');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
var killTimerId = null, progressTimerId = null;
var logStream = fs.createWriteStream(paths.BACKUP_LOG_FILE, { flags: 'a' });
var cp = shell.sudo(`backup-${backupId}`, [ BACKUPTASK_CMD, backupId, format, dataDir ], { env: process.env, logStream: logStream }, function (error) {
clearTimeout(killTimerId);
clearInterval(progressTimerId);
cp = null;
let result = '';
shell.sudo(`backup-${backupId}`, [ BACKUP_UPLOAD_CMD, backupId, format, dataDir ], { preserveEnv: true, ipc: true }, function (error) {
if (error && (error.code === null /* signal */ || (error.code !== 0 && error.code !== 50))) { // backuptask crashed
return callback(new BackupsError(BackupsError.INTERNAL_ERROR, 'Backuptask crashed'));
} else if (error && error.code === 50) { // exited with error
var result = safe.fs.readFileSync(paths.BACKUP_RESULT_FILE, 'utf8') || safe.error.message;
return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, result));
}
callback();
});
progressTimerId = setInterval(function () {
var result = safe.fs.readFileSync(paths.BACKUP_RESULT_FILE, 'utf8');
if (result) progress.setDetail(progress.BACKUP, result);
}, 1000); // every second
killTimerId = setTimeout(function () {
debug('runBackupTask: backup task taking too long. killing');
cp.kill();
}, 4 * 60 * 60 * 1000); // 4 hours
logStream.on('error', function (error) {
debug('runBackupTask: error in logging stream', error);
cp.kill();
}).on('message', function (message) {
if (!message.result) return progressCallback(message);
debug(`runBackupUpload: result - ${message}`);
result = message.result;
});
}
@@ -664,10 +639,11 @@ function setSnapshotInfo(id, info, callback) {
callback();
}
function snapshotBox(callback) {
function snapshotBox(progressCallback, callback) {
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
log('Snapshotting box');
progressCallback({ message: 'Snapshotting box' });
database.exportToFile(`${paths.BOX_DATA_DIR}/box.mysqldump`, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
@@ -676,16 +652,17 @@ function snapshotBox(callback) {
});
}
function uploadBoxSnapshot(backupConfig, callback) {
function uploadBoxSnapshot(backupConfig, progressCallback, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
var startTime = new Date();
snapshotBox(function (error) {
snapshotBox(progressCallback, function (error) {
if (error) return callback(error);
runBackupTask('snapshot/box', backupConfig.format, paths.BOX_DATA_DIR, function (error) {
runBackupUpload('snapshot/box', backupConfig.format, paths.BOX_DATA_DIR, progressCallback, function (error) {
if (error) return callback(error);
debug('uploadBoxSnapshot: time: %s secs', (new Date() - startTime)/1000);
@@ -723,10 +700,11 @@ function backupDone(apiConfig, backupId, appBackupIds, callback) {
});
}
function rotateBoxBackup(backupConfig, timestamp, appBackupIds, callback) {
function rotateBoxBackup(backupConfig, timestamp, appBackupIds, progressCallback, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof timestamp, 'string');
assert(Array.isArray(appBackupIds));
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
var snapshotInfo = getSnapshotInfo('box');
@@ -736,13 +714,13 @@ function rotateBoxBackup(backupConfig, timestamp, appBackupIds, callback) {
var backupId = util.format('%s/box_%s_v%s', timestamp, snapshotTime, config.version());
const format = backupConfig.format;
log(`Rotating box backup to id ${backupId}`);
debug(`Rotating box backup to id ${backupId}`);
backupdb.add({ id: backupId, version: config.version(), type: backupdb.BACKUP_TYPE_BOX, dependsOn: appBackupIds, manifest: null, format: format }, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
var copy = api(backupConfig.provider).copy(backupConfig, getBackupFilePath(backupConfig, 'snapshot/box', format), getBackupFilePath(backupConfig, backupId, format));
copy.on('progress', log);
copy.on('progress', (message) => progressCallback({ message }));
copy.on('done', function (copyBackupError) {
const state = copyBackupError ? backupdb.BACKUP_STATE_ERROR : backupdb.BACKUP_STATE_NORMAL;
@@ -750,7 +728,7 @@ function rotateBoxBackup(backupConfig, timestamp, appBackupIds, callback) {
if (copyBackupError) return callback(copyBackupError);
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
log(`Rotated box backup successfully as id ${backupId}`);
debug(`Rotated box backup successfully as id ${backupId}`);
backupDone(backupConfig, backupId, appBackupIds, function (error) {
if (error) return callback(error);
@@ -762,18 +740,19 @@ function rotateBoxBackup(backupConfig, timestamp, appBackupIds, callback) {
});
}
function backupBoxWithAppBackupIds(appBackupIds, timestamp, callback) {
function backupBoxWithAppBackupIds(appBackupIds, timestamp, progressCallback, callback) {
assert(Array.isArray(appBackupIds));
assert.strictEqual(typeof timestamp, 'string');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
uploadBoxSnapshot(backupConfig, function (error) {
uploadBoxSnapshot(backupConfig, progressCallback, function (error) {
if (error) return callback(error);
rotateBoxBackup(backupConfig, timestamp, appBackupIds, callback);
rotateBoxBackup(backupConfig, timestamp, appBackupIds, progressCallback, callback);
});
});
}
@@ -787,11 +766,12 @@ function canBackupApp(app) {
app.installationState === appdb.ISTATE_PENDING_UPDATE; // called from apptask
}
function snapshotApp(app, callback) {
function snapshotApp(app, progressCallback, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
log(`Snapshotting app ${app.id}`);
progressCallback({ message: `Snapshotting app ${app.id}` });
if (!safe.fs.writeFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/config.json'), JSON.stringify(apps.getAppConfig(app)))) {
return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error creating config.json: ' + safe.error.message));
@@ -804,10 +784,11 @@ function snapshotApp(app, callback) {
});
}
function rotateAppBackup(backupConfig, app, timestamp, callback) {
function rotateAppBackup(backupConfig, app, timestamp, progressCallback, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof timestamp, 'string');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
var snapshotInfo = getSnapshotInfo(app.id);
@@ -818,13 +799,13 @@ function rotateAppBackup(backupConfig, app, timestamp, callback) {
var backupId = util.format('%s/app_%s_%s_v%s', timestamp, app.id, snapshotTime, manifest.version);
const format = backupConfig.format;
log(`Rotating app backup of ${app.id} to id ${backupId}`);
debug(`Rotating app backup of ${app.id} to id ${backupId}`);
backupdb.add({ id: backupId, version: manifest.version, type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ], manifest: manifest, format: format }, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
var copy = api(backupConfig.provider).copy(backupConfig, getBackupFilePath(backupConfig, `snapshot/app_${app.id}`, format), getBackupFilePath(backupConfig, backupId, format));
copy.on('progress', log);
copy.on('progress', (message) => progressCallback({ message }));
copy.on('done', function (copyBackupError) {
const state = copyBackupError ? backupdb.BACKUP_STATE_ERROR : backupdb.BACKUP_STATE_NORMAL;
@@ -832,7 +813,7 @@ function rotateAppBackup(backupConfig, app, timestamp, callback) {
if (copyBackupError) return callback(copyBackupError);
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
log(`Rotated app backup of ${app.id} successfully to id ${backupId}`);
debug(`Rotated app backup of ${app.id} successfully to id ${backupId}`);
callback(null, backupId);
});
@@ -840,21 +821,22 @@ function rotateAppBackup(backupConfig, app, timestamp, callback) {
});
}
function uploadAppSnapshot(backupConfig, app, callback) {
function uploadAppSnapshot(backupConfig, app, progressCallback, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
if (!canBackupApp(app)) return callback(); // nothing to do
var startTime = new Date();
snapshotApp(app, function (error) {
snapshotApp(app, progressCallback, function (error) {
if (error) return callback(error);
var backupId = util.format('snapshot/app_%s', app.id);
var appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id));
runBackupTask(backupId, backupConfig.format, appDataDir, function (error) {
runBackupUpload(backupId, backupConfig.format, appDataDir, progressCallback, function (error) {
if (error) return callback(error);
debugApp(app, 'uploadAppSnapshot: %s done time: %s secs', backupId, (new Date() - startTime)/1000);
@@ -864,9 +846,10 @@ function uploadAppSnapshot(backupConfig, app, callback) {
});
}
function backupAppWithTimestamp(app, timestamp, callback) {
function backupAppWithTimestamp(app, timestamp, progressCallback, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof timestamp, 'string');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
if (!canBackupApp(app)) return callback(); // nothing to do
@@ -874,110 +857,88 @@ function backupAppWithTimestamp(app, timestamp, callback) {
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
uploadAppSnapshot(backupConfig, app, function (error) {
uploadAppSnapshot(backupConfig, app, progressCallback, function (error) {
if (error) return callback(error);
rotateAppBackup(backupConfig, app, timestamp, callback);
rotateAppBackup(backupConfig, app, timestamp, progressCallback, callback);
});
});
}
function backupApp(app, callback) {
function backupApp(app, progressCallback, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
const timestamp = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
safe.fs.unlinkSync(paths.BACKUP_LOG_FILE); // start fresh log file
progress.set(progress.BACKUP, 10, 'Backing up ' + app.fqdn);
debug(`backupApp - Backing up ${app.fqdn} with timestamp ${timestamp}`);
backupAppWithTimestamp(app, timestamp, function (error) {
progress.set(progress.BACKUP, 100, error ? error.message : '');
callback(error);
});
backupAppWithTimestamp(app, timestamp, progressCallback, callback);
}
// this function expects you to have a lock
function backupBoxAndApps(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
callback = callback || NOOP_CALLBACK;
// this function expects you to have a lock. Unlike other progressCallback this also has a progress field
function backupBoxAndApps(progressCallback, callback) {
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
var timestamp = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
safe.fs.unlinkSync(paths.BACKUP_LOG_FILE); // start fresh log file
eventlog.add(eventlog.ACTION_BACKUP_START, auditSource, { });
apps.getAll(function (error, allApps) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
var processed = 1;
var step = 100/(allApps.length+2);
let percent = 1;
let step = 100/(allApps.length+2);
async.mapSeries(allApps, function iterator(app, iteratorCallback) {
progress.set(progress.BACKUP, step * processed, 'Backing up ' + app.fqdn);
++processed;
progressCallback({ percent: percent, message: `Backing up ${app.fqdn}` });
percent += step;
if (!app.enableBackup) {
progress.set(progress.BACKUP, step * processed, 'Skipped backup ' + app.fqdn);
debug(`Skipped backup ${app.fqdn}`);
return iteratorCallback(null, null); // nothing to backup
}
backupAppWithTimestamp(app, timestamp, function (error, backupId) {
backupAppWithTimestamp(app, timestamp, (progress) => progressCallback({ percent: percent, message: progress.message }), function (error, backupId) {
if (error && error.reason !== BackupsError.BAD_STATE) {
debugApp(app, 'Unable to backup', error);
return iteratorCallback(error);
}
progress.set(progress.BACKUP, step * processed, 'Backed up ' + app.fqdn);
debugApp(app, 'Backed up');
iteratorCallback(null, backupId || null); // clear backupId if is in BAD_STATE and never backed up
});
}, function appsBackedUp(error, backupIds) {
if (error) {
progress.set(progress.BACKUP, 100, error.message);
return callback(error);
}
if (error) return callback(error);
backupIds = backupIds.filter(function (id) { return id !== null; }); // remove apps in bad state that were never backed up
progress.set(progress.BACKUP, step * processed, 'Backing up system data');
progressCallback({ percent: percent, message: 'Backing up system data' });
percent += step;
backupBoxWithAppBackupIds(backupIds, timestamp, function (error, backupId) {
progress.set(progress.BACKUP, 100, error ? error.message : '');
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { errorMessage: error ? error.message : null, backupId: backupId, timestamp: timestamp });
callback(error, backupId);
});
backupBoxWithAppBackupIds(backupIds, timestamp, (progress) => progressCallback({ percent: percent, message: progress.message }), callback);
});
});
}
function backup(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var error = locker.lock(locker.OP_FULL_BACKUP);
if (error) return callback(new BackupsError(BackupsError.BAD_STATE, error.message));
var startTime = new Date();
progress.set(progress.BACKUP, 0, 'Starting'); // ensure tools can 'wait' on progress
backupBoxAndApps(auditSource, function (error) { // start the backup operation in the background
if (error) {
debug('backup failed.', error);
mailer.backupFailed(error);
}
function startBackupTask(auditSource, callback) {
let error = locker.lock(locker.OP_FULL_BACKUP);
if (error) return callback(error);
let task = tasks.startTask(tasks.TASK_BACKUP, [], auditSource);
task.on('error', (error) => callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)));
task.on('start', (taskId) => {
eventlog.add(eventlog.ACTION_BACKUP_START, auditSource, { taskId });
callback(null, taskId);
});
task.on('finish', (error, result) => {
locker.unlock(locker.OP_FULL_BACKUP);
debug('backup took %s seconds', (new Date() - startTime)/1000);
});
if (error) mailer.backupFailed(error);
callback(null);
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { errorMessage: error ? error.message : null, backupId: result });
});
}
function ensureBackup(auditSource, callback) {
@@ -999,7 +960,7 @@ function ensureBackup(auditSource, callback) {
return callback(null);
}
backup(auditSource, callback);
startBackupTask(auditSource, callback);
});
});
}

View File

@@ -4,29 +4,18 @@ exports = module.exports = {
verifySetupToken: verifySetupToken,
setupDone: setupDone,
changePlan: changePlan,
upgrade: upgrade,
sendHeartbeat: sendHeartbeat,
getBoxAndUserDetails: getBoxAndUserDetails,
setPtrRecord: setPtrRecord,
CaasError: CaasError
};
var assert = require('assert'),
backups = require('./backups.js'),
config = require('./config.js'),
debug = require('debug')('box:caas'),
locker = require('./locker.js'),
path = require('path'),
progress = require('./progress.js'),
settings = require('./settings.js'),
shell = require('./shell.js'),
superagent = require('superagent'),
util = require('util'),
_ = require('underscore');
const RETIRE_CMD = path.join(__dirname, 'scripts/retire.sh');
util = require('util');
function CaasError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
@@ -53,20 +42,6 @@ CaasError.INVALID_TOKEN = 'Invalid Token';
CaasError.INTERNAL_ERROR = 'Internal Error';
CaasError.EXTERNAL_ERROR = 'External Error';
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function retire(reason, info, callback) {
assert(reason === 'migrate' || reason === 'upgrade');
info = info || { };
callback = callback || NOOP_CALLBACK;
var data = {
apiServerOrigin: config.apiServerOrigin(),
adminFqdn: config.adminFqdn()
};
shell.sudo('retire', [ RETIRE_CMD, reason, JSON.stringify(info), JSON.stringify(data) ], callback);
}
function getCaasConfig(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -117,96 +92,6 @@ function setupDone(setupToken, callback) {
});
});
}
function doMigrate(options, caasConfig, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof caasConfig, 'object');
assert.strictEqual(typeof callback, 'function');
var error = locker.lock(locker.OP_MIGRATE);
if (error) return callback(new CaasError(CaasError.BAD_STATE, error.message));
function unlock(error) {
debug('Failed to migrate', error);
locker.unlock(locker.OP_MIGRATE);
progress.set(progress.MIGRATE, -1, 'Backup failed: ' + error.message);
}
progress.set(progress.MIGRATE, 10, 'Backing up for migration');
// initiate the migration in the background
backups.backupBoxAndApps({ userId: null, username: 'migrator' }, function (error) {
if (error) return unlock(error);
debug('migrate: domain: %s size %s region %s', options.domain, options.size, options.region);
superagent
.post(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId + '/migrate')
.query({ token: caasConfig.token })
.send(options)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return unlock(error); // network error
if (result.statusCode === 409) return unlock(new CaasError(CaasError.BAD_STATE));
if (result.statusCode === 404) return unlock(new CaasError(CaasError.NOT_FOUND));
if (result.statusCode !== 202) return unlock(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
progress.set(progress.MIGRATE, 10, 'Migrating');
retire('migrate', _.pick(options, 'domain', 'size', 'region'));
});
});
callback(null);
}
function changePlan(options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
if (config.isDemo()) return callback(new CaasError(CaasError.BAD_FIELD, 'Not allowed in demo mode'));
getCaasConfig(function (error, result) {
if (error) return callback(error);
doMigrate(options, result, callback);
});
}
// this function expects a lock
function upgrade(boxUpdateInfo, callback) {
assert(boxUpdateInfo !== null && typeof boxUpdateInfo === 'object');
function upgradeError(e) {
progress.set(progress.UPDATE, -1, e.message);
callback(e);
}
progress.set(progress.UPDATE, 5, 'Backing up for upgrade');
backups.backupBoxAndApps({ userId: null, username: 'upgrader' }, function (error) {
if (error) return upgradeError(error);
getCaasConfig(function (error, result) {
if (error) return upgradeError(error);
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + result.boxId + '/upgrade')
.query({ token: result.token })
.send({ version: boxUpdateInfo.version })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return upgradeError(new Error('Network error making upgrade request: ' + error));
if (result.statusCode !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
progress.set(progress.UPDATE, 10, 'Updating base system');
// no need to unlock since this is the last thing we ever do on this box
callback();
retire('upgrade');
});
});
});
}
function sendHeartbeat() {
assert(config.provider() === 'caas', 'Heartbeat is only sent for managed cloudrons');
@@ -223,27 +108,6 @@ function sendHeartbeat() {
});
}
function getBoxAndUserDetails(callback) {
assert.strictEqual(typeof callback, 'function');
if (config.provider() !== 'caas') return callback(null, {});
getCaasConfig(function (error, caasConfig) {
if (error) return callback(error);
superagent
.get(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId)
.query({ token: caasConfig.token })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, 'Cannot reach appstore'));
if (result.statusCode !== 200) return callback(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
return callback(null, result.body);
});
});
}
function setPtrRecord(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');

View File

@@ -1,481 +0,0 @@
'use strict';
var assert = require('assert'),
async = require('async'),
crypto = require('crypto'),
debug = require('debug')('box:cert/acme1'),
execSync = require('safetydance').child_process.execSync,
fs = require('fs'),
parseLinks = require('parse-links'),
path = require('path'),
paths = require('../paths.js'),
safe = require('safetydance'),
superagent = require('superagent'),
util = require('util'),
_ = require('underscore');
var CA_PROD = 'https://acme-v01.api.letsencrypt.org',
CA_STAGING = 'https://acme-staging.api.letsencrypt.org',
LE_AGREEMENT = 'https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf';
exports = module.exports = {
getCertificate: getCertificate,
// testing
_name: 'acme'
};
function Acme1Error(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(Acme1Error, Error);
Acme1Error.INTERNAL_ERROR = 'Internal Error';
Acme1Error.EXTERNAL_ERROR = 'External Error';
Acme1Error.ALREADY_EXISTS = 'Already Exists';
Acme1Error.NOT_COMPLETED = 'Not Completed';
Acme1Error.FORBIDDEN = 'Forbidden';
// http://jose.readthedocs.org/en/latest/
// https://www.ietf.org/proceedings/92/slides/slides-92-acme-1.pdf
// https://community.letsencrypt.org/t/list-of-client-implementations/2103
function Acme1(options) {
assert.strictEqual(typeof options, 'object');
this.caOrigin = options.prod ? CA_PROD : CA_STAGING;
this.accountKeyPem = null; // Buffer
this.email = options.email;
}
Acme1.prototype.getNonce = function (callback) {
superagent.get(this.caOrigin + '/directory').timeout(30 * 1000).end(function (error, response) {
if (error && !error.response) return callback(error);
if (response.statusCode !== 200) return callback(new Error('Invalid response code when fetching nonce : ' + response.statusCode));
return callback(null, response.headers['Replay-Nonce'.toLowerCase()]);
});
};
// urlsafe base64 encoding (jose)
function urlBase64Encode(string) {
return string.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
function b64(str) {
var buf = util.isBuffer(str) ? str : new Buffer(str);
return urlBase64Encode(buf.toString('base64'));
}
function getModulus(pem) {
assert(util.isBuffer(pem));
var stdout = execSync('openssl rsa -modulus -noout', { input: pem, encoding: 'utf8' });
if (!stdout) return null;
var match = stdout.match(/Modulus=([0-9a-fA-F]+)$/m);
if (!match) return null;
return Buffer.from(match[1], 'hex');
}
Acme1.prototype.sendSignedRequest = function (url, payload, callback) {
assert.strictEqual(typeof url, 'string');
assert.strictEqual(typeof payload, 'string');
assert.strictEqual(typeof callback, 'function');
assert(util.isBuffer(this.accountKeyPem));
var that = this;
var header = {
alg: 'RS256',
jwk: {
e: b64(Buffer.from([0x01, 0x00, 0x01])), // exponent - 65537
kty: 'RSA',
n: b64(getModulus(this.accountKeyPem))
}
};
var payload64 = b64(payload);
this.getNonce(function (error, nonce) {
if (error) return callback(error);
debug('sendSignedRequest: using nonce %s for url %s', nonce, url);
var protected64 = b64(JSON.stringify(_.extend({ }, header, { nonce: nonce })));
var signer = crypto.createSign('RSA-SHA256');
signer.update(protected64 + '.' + payload64, 'utf8');
var signature64 = urlBase64Encode(signer.sign(that.accountKeyPem, 'base64'));
var data = {
header: header,
protected: protected64,
payload: payload64,
signature: signature64
};
superagent.post(url).set('Content-Type', 'application/x-www-form-urlencoded').send(JSON.stringify(data)).timeout(30 * 1000).end(function (error, res) {
if (error && !error.response) return callback(error); // network errors
callback(null, res);
});
});
};
Acme1.prototype.updateContact = function (registrationUri, callback) {
assert.strictEqual(typeof registrationUri, 'string');
assert.strictEqual(typeof callback, 'function');
debug('updateContact: %s %s', registrationUri, this.email);
// https://github.com/ietf-wg-acme/acme/issues/30
var payload = {
resource: 'reg',
contact: [ 'mailto:' + this.email ],
agreement: LE_AGREEMENT
};
var that = this;
this.sendSignedRequest(registrationUri, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
if (result.statusCode !== 202) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to update contact. Expecting 202, got %s %s', result.statusCode, result.text)));
debug('updateContact: contact of user updated to %s', that.email);
callback();
});
};
Acme1.prototype.registerUser = function (callback) {
assert.strictEqual(typeof callback, 'function');
var payload = {
resource: 'new-reg',
contact: [ 'mailto:' + this.email ],
agreement: LE_AGREEMENT
};
debug('registerUser: %s', this.email);
var that = this;
this.sendSignedRequest(this.caOrigin + '/acme/new-reg', JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
if (result.statusCode === 409) return that.updateContact(result.headers.location, callback); // already exists
if (result.statusCode !== 201) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
debug('registerUser: registered user %s', that.email);
callback(null);
});
};
Acme1.prototype.registerDomain = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
var payload = {
resource: 'new-authz',
identifier: {
type: 'dns',
value: domain
}
};
debug('registerDomain: %s', domain);
this.sendSignedRequest(this.caOrigin + '/acme/new-authz', JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when registering domain: ' + error.message));
if (result.statusCode === 403) return callback(new Acme1Error(Acme1Error.FORBIDDEN, result.body.detail));
if (result.statusCode !== 201) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
debug('registerDomain: registered %s', domain);
callback(null, result.body);
});
};
Acme1.prototype.prepareHttpChallenge = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
debug('prepareHttpChallenge: preparing for challenge %j', challenge);
var token = challenge.token;
assert(util.isBuffer(this.accountKeyPem));
var jwk = {
e: b64(Buffer.from([0x01, 0x00, 0x01])), // Exponent - 65537
kty: 'RSA',
n: b64(getModulus(this.accountKeyPem))
};
var shasum = crypto.createHash('sha256');
shasum.update(JSON.stringify(jwk));
var thumbprint = urlBase64Encode(shasum.digest('base64'));
var keyAuthorization = token + '.' + thumbprint;
debug('prepareHttpChallenge: writing %s to %s', keyAuthorization, path.join(paths.ACME_CHALLENGES_DIR, token));
fs.writeFile(path.join(paths.ACME_CHALLENGES_DIR, token), token + '.' + thumbprint, function (error) {
if (error) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, error));
callback();
});
};
Acme1.prototype.notifyChallengeReady = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
debug('notifyChallengeReady: %s was met', challenge.uri);
var keyAuthorization = fs.readFileSync(path.join(paths.ACME_CHALLENGES_DIR, challenge.token), 'utf8');
var payload = {
resource: 'challenge',
keyAuthorization: keyAuthorization
};
this.sendSignedRequest(challenge.uri, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when notifying challenge: ' + error.message));
if (result.statusCode !== 202) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 202, got %s %s', result.statusCode, result.text)));
callback();
});
};
Acme1.prototype.waitForChallenge = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
debug('waitingForChallenge: %j', challenge);
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
debug('waitingForChallenge: getting status');
superagent.get(challenge.uri).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) {
debug('waitForChallenge: network error getting uri %s', challenge.uri);
return retryCallback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, error.message)); // network error
}
if (result.statusCode !== 202) {
debug('waitForChallenge: invalid response code getting uri %s', result.statusCode);
return retryCallback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
}
debug('waitForChallenge: status is "%s %j', result.body.status, result.body);
if (result.body.status === 'pending') return retryCallback(new Acme1Error(Acme1Error.NOT_COMPLETED));
else if (result.body.status === 'valid') return retryCallback();
else return retryCallback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Unexpected status: ' + result.body.status));
});
}, function retryFinished(error) {
// async.retry will pass 'undefined' as second arg making it unusable with async.waterfall()
callback(error);
});
};
// https://community.letsencrypt.org/t/public-beta-rate-limits/4772 for rate limits
Acme1.prototype.signCertificate = function (domain, csrDer, callback) {
assert.strictEqual(typeof domain, 'string');
assert(util.isBuffer(csrDer));
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
var payload = {
resource: 'new-cert',
csr: b64(csrDer)
};
debug('signCertificate: sending new-cert request');
this.sendSignedRequest(this.caOrigin + '/acme/new-cert', JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when signing certificate: ' + error.message));
// 429 means we reached the cert limit for this domain
if (result.statusCode !== 201) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 201, got %s %s', result.statusCode, result.text)));
var certUrl = result.headers.location;
if (!certUrl) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Missing location in downloadCertificate'));
safe.fs.writeFileSync(path.join(outdir, domain + '.url'), certUrl, 'utf8'); // maybe use for renewal
return callback(null, result.headers.location);
});
};
Acme1.prototype.createKeyAndCsr = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
var csrFile = path.join(outdir, domain + '.csr');
var privateKeyFile = path.join(outdir, domain + '.key');
if (safe.fs.existsSync(privateKeyFile)) {
// in some old releases, csr file was corrupt. so always regenerate it
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
} else {
var key = execSync('openssl genrsa 4096');
if (!key) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
}
var csrDer = execSync(util.format('openssl req -new -key %s -outform DER -subj /CN=%s', privateKeyFile, domain));
if (!csrDer) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error)); // bookkeeping
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFile);
callback(null, csrDer);
};
// TODO: download the chain in a loop following 'up' header
Acme1.prototype.downloadChain = function (linkHeader, callback) {
if (!linkHeader) return new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Empty link header when downloading certificate chain');
debug('downloadChain: linkHeader %s', linkHeader);
var linkInfo = parseLinks(linkHeader);
if (!linkInfo || !linkInfo.up) return new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Failed to parse link header when downloading certificate chain');
var intermediateCertUrl = linkInfo.up.startsWith('https://') ? linkInfo.up : (this.caOrigin + linkInfo.up);
debug('downloadChain: downloading from %s', intermediateCertUrl);
superagent.get(intermediateCertUrl).buffer().parse(function (res, done) {
var data = [ ];
res.on('data', function(chunk) { data.push(chunk); });
res.on('end', function () { res.text = Buffer.concat(data); done(); });
}).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when downloading certificate'));
if (result.statusCode !== 200) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
var chainDer = result.text;
var chainPem = execSync('openssl x509 -inform DER -outform PEM', { input: chainDer }); // this is really just base64 encoding with header
if (!chainPem) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
callback(null, chainPem);
});
};
Acme1.prototype.downloadCertificate = function (domain, certUrl, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof certUrl, 'string');
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
var that = this;
superagent.get(certUrl).buffer().parse(function (res, done) {
var data = [ ];
res.on('data', function(chunk) { data.push(chunk); });
res.on('end', function () { res.text = Buffer.concat(data); done(); });
}).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when downloading certificate'));
if (result.statusCode === 202) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, 'Retry not implemented yet'));
if (result.statusCode !== 200) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
var certificateDer = result.text;
safe.fs.writeFileSync(path.join(outdir, domain + '.der'), certificateDer);
debug('downloadCertificate: cert der file for %s saved', domain);
var certificatePem = execSync('openssl x509 -inform DER -outform PEM', { input: certificateDer }); // this is really just base64 encoding with header
if (!certificatePem) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
that.downloadChain(result.header['link'], function (error, chainPem) {
if (error) return callback(error);
var certificateFile = path.join(outdir, domain + '.cert');
var fullChainPem = Buffer.concat([certificatePem, chainPem]);
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
debug('downloadCertificate: cert file for %s saved at %s', domain, certificateFile);
callback();
});
});
};
Acme1.prototype.acmeFlow = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
if (!fs.existsSync(paths.ACME_ACCOUNT_KEY_FILE)) {
debug('getCertificate: generating acme account key on first run');
this.accountKeyPem = safe.child_process.execSync('openssl genrsa 4096');
if (!this.accountKeyPem) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
safe.fs.writeFileSync(paths.ACME_ACCOUNT_KEY_FILE, this.accountKeyPem);
} else {
debug('getCertificate: using existing acme account key');
this.accountKeyPem = fs.readFileSync(paths.ACME_ACCOUNT_KEY_FILE);
}
var that = this;
this.registerUser(function (error) {
if (error) return callback(error);
that.registerDomain(domain, function (error, result) {
if (error) return callback(error);
debug('acmeFlow: challenges: %j', result);
var httpChallenges = result.challenges.filter(function(x) { return x.type === 'http-01'; });
if (httpChallenges.length === 0) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'no http challenges'));
var challenge = httpChallenges[0];
async.waterfall([
that.prepareHttpChallenge.bind(that, challenge),
that.notifyChallengeReady.bind(that, challenge),
that.waitForChallenge.bind(that, challenge),
that.createKeyAndCsr.bind(that, domain),
that.signCertificate.bind(that, domain),
that.downloadCertificate.bind(that, domain)
], callback);
});
});
};
Acme1.prototype.getCertificate = function (hostname, domain, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
debug('getCertificate: start acme flow for %s from %s', hostname, this.caOrigin);
this.acmeFlow(hostname, function (error) {
if (error) return callback(error);
var outdir = paths.APP_CERTS_DIR;
callback(null, path.join(outdir, hostname + '.cert'), path.join(outdir, hostname + '.key'));
});
};
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var acme = new Acme1(options || { });
acme.getCertificate(hostname, domain, callback);
}

View File

@@ -5,7 +5,6 @@ var assert = require('assert'),
crypto = require('crypto'),
debug = require('debug')('box:cert/acme2'),
domains = require('../domains.js'),
execSync = require('safetydance').child_process.execSync,
fs = require('fs'),
path = require('path'),
paths = require('../paths.js'),
@@ -88,7 +87,7 @@ function b64(str) {
function getModulus(pem) {
assert(util.isBuffer(pem));
var stdout = execSync('openssl rsa -modulus -noout', { input: pem, encoding: 'utf8' });
var stdout = safe.child_process.execSync('openssl rsa -modulus -noout', { input: pem, encoding: 'utf8' });
if (!stdout) return null;
var match = stdout.match(/Modulus=([0-9a-fA-F]+)$/m);
if (!match) return null;
@@ -351,14 +350,14 @@ Acme2.prototype.createKeyAndCsr = function (hostname, callback) {
// in some old releases, csr file was corrupt. so always regenerate it
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
} else {
var key = execSync('openssl genrsa 4096');
var key = safe.child_process.execSync('openssl genrsa 4096');
if (!key) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
}
var csrDer = execSync(`openssl req -new -key ${privateKeyFile} -outform DER -subj /CN=${hostname}`);
var csrDer = safe.child_process.execSync(`openssl req -new -key ${privateKeyFile} -outform DER -subj /CN=${hostname}`);
if (!csrDer) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error)); // bookkeeping

View File

@@ -11,9 +11,12 @@ exports = module.exports = {
getStatus: getStatus,
reboot: reboot,
isRebootRequired: isRebootRequired,
onActivated: onActivated,
setDashboardDomain: setDashboardDomain,
renewCerts: renewCerts,
checkDiskSpace: checkDiskSpace,
@@ -23,24 +26,26 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
clients = require('./clients.js'),
config = require('./config.js'),
cron = require('./cron.js'),
debug = require('debug')('box:cloudron'),
domains = require('./domains.js'),
DomainsError = require('./domains.js').DomainsError,
df = require('@sindresorhus/df'),
fs = require('fs'),
mailer = require('./mailer.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
platform = require('./platform.js'),
progress = require('./progress.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
shell = require('./shell.js'),
spawn = require('child_process').spawn,
split = require('split'),
sysinfo = require('./sysinfo.js'),
tasks = require('./tasks.js'),
users = require('./users.js'),
util = require('util');
@@ -82,8 +87,6 @@ CloudronError.INTERNAL_ERROR = 'Internal Error';
CloudronError.EXTERNAL_ERROR = 'External Error';
CloudronError.BAD_STATE = 'Bad state';
CloudronError.ALREADY_UPTODATE = 'No Update Available';
CloudronError.NOT_FOUND = 'Not found';
CloudronError.SELF_UPGRADE_NOT_SUPPORTED = 'Self upgrade not supported';
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -171,7 +174,6 @@ function getConfig(callback) {
adminFqdn: config.adminFqdn(),
mailFqdn: config.mailFqdn(),
version: config.version(),
progress: progress.getAll(),
isDemo: config.isDemo(),
edition: config.edition(),
memory: os.totalmem(),
@@ -182,7 +184,14 @@ function getConfig(callback) {
}
function reboot(callback) {
shell.sudo('reboot', [ REBOOT_CMD ], callback);
shell.sudo('reboot', [ REBOOT_CMD ], {}, callback);
}
function isRebootRequired(callback) {
assert.strictEqual(typeof callback, 'function');
// https://serverfault.com/questions/92932/how-does-ubuntu-keep-track-of-the-system-restart-required-flag-in-motd
callback(null, fs.existsSync('/var/run/reboot-required'));
}
function checkDiskSpace(callback) {
@@ -244,49 +253,28 @@ function getLogs(unit, options, callback) {
debug('Getting logs for %s as %s', unit, format);
var cp, transformStream;
if (unit === 'box') {
let args = [ '--no-pager', `--lines=${lines}` ];
if (format === 'short') args.push('--output=short', '-a'); else args.push('--output=json');
if (follow) args.push('--follow');
args.push('--unit=box');
args.push('--unit=cloudron-updater');
cp = spawn('/bin/journalctl', args);
let args = [ '--lines=' + lines ];
if (follow) args.push('--follow');
transformStream = split(function mapper(line) {
if (format !== 'json') return line + '\n';
// need to handle box.log without subdir
if (unit === 'box') args.push(path.join(paths.LOG_DIR, 'box.log'));
else args.push(path.join(paths.LOG_DIR, unit, 'app.log'));
var obj = safe.JSON.parse(line);
if (!obj) return undefined;
var cp = spawn('/usr/bin/tail', args);
return JSON.stringify({
realtimeTimestamp: obj.__REALTIME_TIMESTAMP,
monotonicTimestamp: obj.__MONOTONIC_TIMESTAMP,
message: obj.MESSAGE,
source: obj.SYSLOG_IDENTIFIER || ''
}) + '\n';
});
} else { // mail, mongodb, mysql, postgresql, backup
let args = [ '--lines=' + lines ];
if (follow) args.push('--follow');
args.push(path.join(paths.LOG_DIR, unit, 'app.log'));
var transformStream = split(function mapper(line) {
if (format !== 'json') return line + '\n';
cp = spawn('/usr/bin/tail', args);
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
var timestamp = (new Date(data[0])).getTime();
if (isNaN(timestamp)) timestamp = 0;
transformStream = split(function mapper(line) {
if (format !== 'json') return line + '\n';
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
var timestamp = (new Date(data[0])).getTime();
if (isNaN(timestamp)) timestamp = 0;
return JSON.stringify({
realtimeTimestamp: timestamp * 1000,
message: line.slice(data[0].length+1),
source: unit
}) + '\n';
});
}
return JSON.stringify({
realtimeTimestamp: timestamp * 1000,
message: line.slice(data[0].length+1),
source: unit
}) + '\n';
});
transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process
@@ -365,3 +353,37 @@ function getStatus(callback) {
});
});
}
function setDashboardDomain(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`setDashboardDomain: ${domain}`);
domains.get(domain, function (error, result) {
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new CloudronError(CloudronError.BAD_FIELD, 'No such domain'));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
config.setAdminDomain(result.domain);
config.setAdminLocation('my');
config.setAdminFqdn('my' + (result.config.hyphenatedSubdomains ? '-' : '.') + result.domain);
clients.addDefaultClients(config.adminOrigin(), function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
callback(null);
configureWebadmin(NOOP_CALLBACK); // ## trigger as task
});
});
}
function renewCerts(options, auditSource, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
let task = tasks.startTask(tasks.TASK_RENEW_CERTS, [ options, auditSource ]);
task.on('error', (error) => callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)));
task.on('start', (taskId) => callback(null, taskId));
}

View File

@@ -186,7 +186,7 @@ function recreateJobs(tz) {
if (gJobs.certificateRenew) gJobs.certificateRenew.stop();
gJobs.certificateRenew = new CronJob({
cronTime: '00 00 */12 * * *', // every 12 hours
onTick: reverseProxy.renewAll.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
onTick: cloudron.renewCerts.bind(null, {}, AUDIT_SOURCE, NOOP_CALLBACK),
start: true,
timeZone: tz
});

View File

@@ -1,9 +1,13 @@
'use strict';
exports = module.exports = {
DockerError: DockerError,
connection: connectionInstance(),
setRegistryConfig: setRegistryConfig,
ping: ping,
downloadImage: downloadImage,
createContainer: createContainer,
startContainer: startContainer,
@@ -18,24 +22,26 @@ exports = module.exports = {
getContainerIdByIp: getContainerIdByIp,
inspect: inspect,
inspectByName: inspect,
memoryUsage: memoryUsage,
execContainer: execContainer,
createVolume: createVolume,
removeVolume: removeVolume,
clearVolume: clearVolume
};
function connectionInstance() {
// timeout is optional
function connectionInstance(timeout) {
var Docker = require('dockerode');
var docker;
if (process.env.BOX_ENV === 'test') {
// test code runs a docker proxy on this port
docker = new Docker({ host: 'http://localhost', port: 5687 });
docker = new Docker({ host: 'http://localhost', port: 5687, timeout: timeout });
// proxy code uses this to route to the real docker
docker.options = { socketPath: '/var/run/docker.sock' };
} else {
docker = new Docker({ socketPath: '/var/run/docker.sock' });
docker = new Docker({ socketPath: '/var/run/docker.sock', timeout: timeout });
}
return docker;
@@ -80,6 +86,7 @@ function DockerError(reason, errorOrMessage) {
}
util.inherits(DockerError, Error);
DockerError.INTERNAL_ERROR = 'Internal Error';
DockerError.NOT_FOUND = 'Not found';
DockerError.BAD_FIELD = 'Bad field';
function debugApp(app, args) {
@@ -104,12 +111,26 @@ function setRegistryConfig(auth, callback) {
});
}
function ping(callback) {
assert.strictEqual(typeof callback, 'function');
// do not let the request linger
var docker = connectionInstance(1000);
docker.ping(function (error, result) {
if (error) return callback(new DockerError(DockerError.INTERNAL_ERROR, error));
if (result !== 'OK') return callback(new DockerError(DockerError.INTERNAL_ERROR, 'Unable to ping the docker daemon'));
callback(null);
});
}
function pullImage(manifest, callback) {
var docker = exports.connection;
// Use docker CLI here to support downloading of private repos. for dockerode, we have to use
// https://github.com/apocas/dockerode#pull-from-private-repos
shell.exec('pullImage', '/usr/bin/docker', [ 'pull', manifest.dockerImage ], { }, function (error) {
shell.spawn('pullImage', '/usr/bin/docker', [ 'pull', manifest.dockerImage ], {}, function (error) {
if (error) {
debug(`pullImage: Error pulling image ${manifest.dockerImage} of ${manifest.id}: ${error.message}`);
return callback(new Error('Failed to pull image'));
@@ -161,8 +182,10 @@ function createSubcontainer(app, name, cmd, options, callback) {
var manifest = app.manifest;
var exposedPorts = {}, dockerPortBindings = { };
var domain = app.fqdn;
// TODO: these should all have the CLOUDRON_ prefix
var stdEnv = [
'CLOUDRON=1',
'CLOUDRON_PROXY_IP=172.18.0.1',
'WEBADMIN_ORIGIN=' + config.adminOrigin(),
'API_ORIGIN=' + config.adminOrigin(),
'APP_ORIGIN=https://' + domain,
@@ -204,9 +227,6 @@ function createSubcontainer(app, name, cmd, options, callback) {
// if required, we can make this a manifest and runtime argument later
if (!isAppContainer) memoryLimit *= 2;
// apparmor is disabled on few servers
var enableSecurityOpt = config.CLOUDRON && safe(function () { return child_process.spawnSync('aa-enabled').status === 0; }, false);
addons.getEnvironment(app, function (error, addonEnv) {
if (error) return callback(new Error('Error getting addon environment : ' + error));
@@ -257,7 +277,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
NetworkMode: 'cloudron',
Dns: ['172.18.0.1'], // use internal dns
DnsSearch: ['.'], // use internal dns
SecurityOpt: enableSecurityOpt ? [ 'apparmor=docker-cloudron-app' ] : null // profile available only on cloudron
SecurityOpt: [ 'apparmor=docker-cloudron-app' ]
}
};
@@ -445,7 +465,23 @@ function inspect(containerId, callback) {
var container = exports.connection.getContainer(containerId);
container.inspect(function (error, result) {
if (error) return callback(error);
if (error && error.statusCode === 404) return callback(new DockerError(DockerError.NOT_FOUND));
if (error) return callback(new DockerError(DockerError.INTERNAL_ERROR, error));
callback(null, result);
});
}
function memoryUsage(containerId, callback) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof callback, 'function');
var container = exports.connection.getContainer(containerId);
container.stats({ stream: false }, function (error, result) {
if (error && error.statusCode === 404) return callback(new DockerError(DockerError.NOT_FOUND));
if (error) return callback(new DockerError(DockerError.INTERNAL_ERROR, error));
callback(null, result);
});
}
@@ -522,7 +558,7 @@ function clearVolume(app, name, subdir, callback) {
assert.strictEqual(typeof subdir, 'string');
assert.strictEqual(typeof callback, 'function');
shell.sudo('removeVolume', [ RMVOLUME_CMD, app.id, subdir ], callback);
shell.sudo('removeVolume', [ RMVOLUME_CMD, app.id, subdir ], {}, callback);
}
function removeVolume(app, name, subdir, callback) {
@@ -540,6 +576,6 @@ function removeVolume(app, name, subdir, callback) {
callback(error);
}
shell.sudo('removeVolume', [ RMVOLUME_CMD, app.id, subdir ], callback);
shell.sudo('removeVolume', [ RMVOLUME_CMD, app.id, subdir ], {}, callback);
});
}

View File

@@ -8,8 +8,7 @@ exports = module.exports = {
getAll: getAll,
update: update,
del: del,
_clear: clear
clear: clear
};
var assert = require('assert'),

View File

@@ -6,12 +6,10 @@ module.exports = exports = {
getAll: getAll,
update: update,
del: del,
clear: clear,
isLocked: isLocked,
renewCerts: renewCerts,
fqdn: fqdn,
setAdmin: setAdmin,
getDnsRecords: getDnsRecords,
upsertDnsRecords: upsertDnsRecords,
@@ -35,26 +33,20 @@ module.exports = exports = {
};
var assert = require('assert'),
caas = require('./caas.js'),
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:domains'),
domaindb = require('./domaindb.js'),
eventlog = require('./eventlog.js'),
path = require('path'),
reverseProxy = require('./reverseproxy.js'),
ReverseProxyError = reverseProxy.ReverseProxyError,
safe = require('safetydance'),
shell = require('./shell.js'),
sysinfo = require('./sysinfo.js'),
tld = require('tldjs'),
util = require('util'),
_ = require('underscore');
var RESTART_CMD = path.join(__dirname, 'scripts/restart.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function DomainsError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
@@ -369,6 +361,16 @@ function del(domain, auditSource, callback) {
});
}
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
domaindb.clear(function (error) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
return callback(null);
});
}
// returns the 'name' that needs to be inserted into zone
function getName(domain, subdomain, type) {
// hack for supporting special caas domains. if we want to remove this, we have to fix the appstore domain API first
@@ -468,31 +470,6 @@ function waitForDnsRecord(subdomain, domain, type, value, options, callback) {
});
}
function setAdmin(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
debug('setAdmin domain:%s', domain);
get(domain, function (error, result) {
if (error) return callback(error);
var setPtrRecord = config.provider() === 'caas' ? caas.setPtrRecord : function (d, next) { next(); };
setPtrRecord(domain, function (error) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Error setting PTR record:' + error.message));
config.setAdminDomain(result.domain);
config.setAdminLocation('my');
config.setAdminFqdn('my' + (result.config.hyphenatedSubdomains ? '-' : '.') + result.domain);
callback();
shell.sudo('restart', [ RESTART_CMD ], NOOP_CALLBACK);
});
});
}
// removes all fields that are strictly private and should never be returned by API calls
function removePrivateFields(domain) {
var result = _.pick(domain, 'domain', 'zoneName', 'provider', 'config', 'tlsConfig', 'fallbackCertificate', 'locked');
@@ -517,16 +494,3 @@ function makeWildcard(hostname) {
parts[0] = '*';
return parts.join('.');
}
function renewCerts(domain, auditSource, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
// trigger renewal in the background
reverseProxy.renewCerts({ domain: domain }, auditSource, function (error) {
debug('renewCerts', error);
});
callback();
}

View File

@@ -31,12 +31,10 @@ function startGraphite(existingInfra, callback) {
--dns-search=. \
-p 127.0.0.1:2003:2003 \
-p 127.0.0.1:2004:2004 \
-p 127.0.0.1:8000:8000 \
-p 127.0.0.1:8417:8000 \
-v "${dataDir}/graphite:/var/lib/graphite" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
shell.execSync('startGraphite', cmd);
callback();
shell.exec('startGraphite', cmd, callback);
}

View File

@@ -34,7 +34,7 @@ function get(groupId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ' FROM groups WHERE id = ? ORDER BY name', [ groupId ], function (error, result) {
database.query('SELECT ' + GROUPS_FIELDS + ' FROM userGroups WHERE id = ? ORDER BY name', [ groupId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
@@ -47,9 +47,9 @@ function getWithMembers(groupId, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ',GROUP_CONCAT(groupMembers.userId) AS userIds ' +
' FROM groups LEFT OUTER JOIN groupMembers ON groups.id = groupMembers.groupId ' +
' WHERE groups.id = ? ' +
' GROUP BY groups.id', [ groupId ], function (error, results) {
' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' +
' WHERE userGroups.id = ? ' +
' GROUP BY userGroups.id', [ groupId ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
@@ -63,7 +63,7 @@ function getWithMembers(groupId, callback) {
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ' FROM groups', function (error, results) {
database.query('SELECT ' + GROUPS_FIELDS + ' FROM userGroups', function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, results);
@@ -72,8 +72,8 @@ function getAll(callback) {
function getAllWithMembers(callback) {
database.query('SELECT ' + GROUPS_FIELDS + ',GROUP_CONCAT(groupMembers.userId) AS userIds ' +
' FROM groups LEFT OUTER JOIN groupMembers ON groups.id = groupMembers.groupId ' +
' GROUP BY groups.id', function (error, results) {
' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' +
' GROUP BY userGroups.id', function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
@@ -88,7 +88,7 @@ function add(id, name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO groups (id, name) VALUES (?, ?)', [ id, name ], function (error, result) {
database.query('INSERT INTO userGroups (id, name) VALUES (?, ?)', [ id, name ], function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -112,8 +112,8 @@ function update(id, data, callback) {
}
args.push(id);
database.query('UPDATE groups SET ' + fields.join(', ') + ' WHERE id = ?', args, function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('groups_name') !== -1) return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'name already exists'));
database.query('UPDATE userGroups SET ' + fields.join(', ') + ' WHERE id = ?', args, function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('userGroups_name') !== -1) return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'name already exists'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
@@ -128,7 +128,7 @@ function del(id, callback) {
// also cleanup the groupMembers table
var queries = [];
queries.push({ query: 'DELETE FROM groupMembers WHERE groupId = ?', args: [ id ] });
queries.push({ query: 'DELETE FROM groups WHERE id = ?', args: [ id ] });
queries.push({ query: 'DELETE FROM userGroups WHERE id = ?', args: [ id ] });
database.transaction(queries, function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -141,7 +141,7 @@ function del(id, callback) {
function count(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT COUNT(*) AS total FROM groups', function (error, result) {
database.query('SELECT COUNT(*) AS total FROM userGroups', function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
return callback(null, result[0].total);
@@ -152,7 +152,7 @@ function clear(callback) {
database.query('DELETE FROM groupMembers', function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
database.query('DELETE FROM groups', function (error) {
database.query('DELETE FROM userGroups', function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(error);
@@ -266,7 +266,7 @@ function getGroups(userId, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ' ' +
' FROM groups INNER JOIN groupMembers ON groups.id = groupMembers.groupId AND groupMembers.userId = ?', [ userId ], function (error, results) {
' FROM userGroups INNER JOIN groupMembers ON userGroups.id = groupMembers.groupId AND groupMembers.userId = ?', [ userId ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, results);

View File

@@ -19,7 +19,7 @@ exports = module.exports = {
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.0.2@sha256:6dcee0731dfb9b013ed94d56205eee219040ee806c7e251db3b3886eaa4947ff' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.0.2@sha256:95e006390ddce7db637e1672eb6f3c257d3c2652747424f529b1dee3cbe6728c' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.0.0@sha256:8a88dd334b62b578530a014ca1a2425a54cb9df1e475f5d3a36806e5cfa22121' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.0.0@sha256:3c0fbb2a042ac471940ac3e9f6ffa900c8a294941fb7de509b2e3309b09fbffd' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.0@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' }
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.0.1@sha256:deee3739011670d45abd8997a8a0b8d3c4cd577a93f235417614dea58338e0f9' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.2@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' }
}
};

View File

@@ -271,7 +271,6 @@ function mailboxSearch(req, res, next) {
cn: `${mailbox.name}@${mailbox.domain}`,
uid: `${mailbox.name}@${mailbox.domain}`,
mail: `${mailbox.name}@${mailbox.domain}`,
ownerType: mailbox.ownerType,
displayname: 'Max Mustermann',
givenName: 'Max',
username: 'mmustermann',
@@ -297,9 +296,6 @@ function mailboxSearch(req, res, next) {
var results = [];
// only send user mailboxes
result = result.filter(function (m) { return m.ownerType === mailboxdb.OWNER_TYPE_USER; });
// send mailbox objects
result.forEach(function (mailbox) {
var dn = ldap.parseDN(`cn=${mailbox.name}@${domain},domain=${domain},ou=mailboxes,dc=cloudron`);
@@ -311,8 +307,7 @@ function mailboxSearch(req, res, next) {
objectcategory: 'mailbox',
cn: `${mailbox.name}@${domain}`,
uid: `${mailbox.name}@${domain}`,
mail: `${mailbox.name}@${domain}`,
ownerType: mailbox.ownerType
mail: `${mailbox.name}@${domain}`
}
};
@@ -464,30 +459,30 @@ function authenticateMailbox(req, res, next) {
var parts = email.split('@');
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
const addonId = req.dn.rdns[1].attrs.ou.value.toLowerCase(); // 'sendmail' or 'recvmail'
mail.getDomain(parts[1], function (error, domain) {
if (error && error.reason === MailError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
mail.getDomain(parts[1], function (error, domain) {
if (error && error.reason === MailError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
if (addonId === 'recvmail' && !domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (mailbox.ownerType === mailboxdb.OWNER_TYPE_APP) {
var addonId = req.dn.rdns[1].attrs.ou.value.toLowerCase(); // 'sendmail' or 'recvmail'
var name;
if (addonId === 'sendmail') name = 'MAIL_SMTP_PASSWORD';
else if (addonId === 'recvmail') name = 'MAIL_IMAP_PASSWORD';
else return next(new ldap.OperationsError('Invalid DN'));
let name;
if (addonId === 'sendmail') name = 'MAIL_SMTP_PASSWORD';
else if (addonId === 'recvmail') name = 'MAIL_IMAP_PASSWORD';
else return next(new ldap.OperationsError('Invalid DN'));
appdb.getAddonConfigByName(mailbox.ownerId, addonId, name, function (error, value) {
if (error) return next(new ldap.OperationsError(error.message));
if (req.credentials !== value) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
// note: with sendmail addon, apps can send mail without a mailbox (unlike users)
appdb.getAppIdByAddonConfigValue(addonId, name, req.credentials || '', function (error, appId) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return next(new ldap.OperationsError(error.message));
if (appId) { // matched app password
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: email }, { appId: appId, addonId: addonId });
return res.end();
}
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: name }, { appId: mailbox.ownerId, addonId: addonId });
return res.end();
});
} else if (mailbox.ownerType === mailboxdb.OWNER_TYPE_USER) {
if (!domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
users.verify(mailbox.ownerId, req.credentials || '', function (error, result) {
if (error && error.reason === UsersError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
@@ -497,9 +492,7 @@ function authenticateMailbox(req, res, next) {
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) });
res.end();
});
} else {
return next(new ldap.OperationsError('Unknown ownerType for mailbox'));
}
});
});
});
}
@@ -527,9 +520,8 @@ function start(callback) {
gServer.search('ou=mailaliases,dc=cloudron', mailAliasSearch);
gServer.search('ou=mailinglists,dc=cloudron', mailingListSearch);
gServer.bind('ou=mailboxes,dc=cloudron', authenticateMailbox);
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailbox);
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailbox);
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailbox); // dovecot
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailbox); // haraka
gServer.compare('cn=users,ou=groups,dc=cloudron', authenticateApp, groupUsersCompare);
gServer.compare('cn=admins,ou=groups,dc=cloudron', authenticateApp, groupAdminsCompare);

View File

@@ -18,7 +18,6 @@ Locker.prototype.OP_BOX_UPDATE = 'box_update';
Locker.prototype.OP_PLATFORM_START = 'platform_start';
Locker.prototype.OP_FULL_BACKUP = 'full_backup';
Locker.prototype.OP_APPTASK = 'apptask';
Locker.prototype.OP_MIGRATE = 'migrate';
Locker.prototype.lock = function (operation) {
assert.strictEqual(typeof operation, 'string');

View File

@@ -22,6 +22,8 @@ function collectLogs(unitName, callback) {
assert.strictEqual(typeof callback, 'function');
var logs = safe.child_process.execSync('sudo ' + COLLECT_LOGS_CMD + ' ' + unitName, { encoding: 'utf8' });
if (!logs) return callback(safe.error);
logs = logs + '\n\n=====================================\n\n';
callback(null, logs);

View File

@@ -8,6 +8,7 @@ exports = module.exports = {
getDomain: getDomain,
addDomain: addDomain,
removeDomain: removeDomain,
clearDomains: clearDomains,
setDnsRecords: setDnsRecords,
@@ -22,11 +23,11 @@ exports = module.exports = {
sendTestMail: sendTestMail,
getMailboxes: getMailboxes,
listMailboxes: listMailboxes,
removeMailboxes: removeMailboxes,
getMailbox: getMailbox,
addMailbox: addMailbox,
updateMailbox: updateMailbox,
updateMailboxOwner: updateMailboxOwner,
removeMailbox: removeMailbox,
listAliases: listAliases,
@@ -52,13 +53,13 @@ var assert = require('assert'),
dns = require('./native-dns.js'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
hat = require('./hat.js'),
infra = require('./infra_version.js'),
mailboxdb = require('./mailboxdb.js'),
maildb = require('./maildb.js'),
mailer = require('./mailer.js'),
net = require('net'),
nodemailer = require('nodemailer'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
@@ -109,9 +110,6 @@ function validateName(name) {
// also need to consider valid LDAP characters here (e.g '+' is reserved)
if (/[^a-zA-Z0-9.-]/.test(name)) return new MailError(MailError.BAD_FIELD, 'mailbox name can only contain alphanumerals and dot');
// app emails are sent using the .app suffix
if (name.indexOf('.app') !== -1) return new MailError(MailError.BAD_FIELD, 'mailbox name pattern is reserved for apps');
return null;
}
@@ -555,6 +553,7 @@ function restartMail(callback) {
const tag = infra.images.mail.tag;
const memoryLimit = 4 * 256;
const cloudronToken = hat(8 * 128);
// admin and mail share the same certificate
reverseProxy.getCertificate({ fqdn: config.adminFqdn(), domain: config.adminDomain() }, function (error, bundle) {
@@ -567,34 +566,35 @@ function restartMail(callback) {
if (!safe.child_process.execSync(`cp ${bundle.certFilePath} ${mailCertFilePath}`)) return callback(new Error('Could not create cert file:' + safe.error.message));
if (!safe.child_process.execSync(`cp ${bundle.keyFilePath} ${mailKeyFilePath}`)) return callback(new Error('Could not create key file:' + safe.error.message));
shell.execSync('startMail', 'docker rm -f mail || true');
createMailConfig(function (error, allowInbound) {
shell.exec('startMail', 'docker rm -f mail || true', function (error) {
if (error) return callback(error);
var ports = allowInbound ? '-p 587:2525 -p 993:9993 -p 4190:4190 -p 25:2525' : '';
createMailConfig(function (error, allowInbound) {
if (error) return callback(error);
const cmd = `docker run --restart=always -d --name="mail" \
--net cloudron \
--net-alias mail \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=mail \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-v "${paths.MAIL_DATA_DIR}:/app/data" \
-v "${paths.PLATFORM_DATA_DIR}/addons/mail:/etc/mail" \
${ports} \
-p 127.0.0.1:2020:2020 \
--label isCloudronManaged=true \
--read-only -v /run -v /tmp ${tag}`;
var ports = allowInbound ? '-p 587:2525 -p 993:9993 -p 4190:4190 -p 25:2525' : '';
shell.execSync('startMail', cmd);
const cmd = `docker run --restart=always -d --name="mail" \
--net cloudron \
--net-alias mail \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=mail \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_MAIL_TOKEN="${cloudronToken}" \
-v "${paths.MAIL_DATA_DIR}:/app/data" \
-v "${paths.PLATFORM_DATA_DIR}/addons/mail:/etc/mail" \
${ports} \
-p 127.0.0.1:2020:2020 \
--label isCloudronManaged=true \
--read-only -v /run -v /tmp ${tag}`;
callback();
shell.exec('startMail', cmd, callback);
});
});
});
}
@@ -628,7 +628,7 @@ function getDomain(domain, callback) {
function getDomains(callback) {
assert.strictEqual(typeof callback, 'function');
maildb.getAll(function (error, results) {
maildb.list(function (error, results) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
return callback(null, results);
@@ -805,6 +805,16 @@ function removeDomain(domain, callback) {
});
}
function clearDomains(callback) {
assert.strictEqual(typeof callback, 'function');
maildb.clear(function (error) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
callback();
});
}
function setMailFromValidation(domain, enabled, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof enabled, 'boolean');
@@ -886,7 +896,7 @@ function sendTestMail(domain, to, callback) {
});
}
function getMailboxes(domain, callback) {
function listMailboxes(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -933,7 +943,7 @@ function addMailbox(name, domain, userId, auditSource, callback) {
var error = validateName(name);
if (error) return callback(error);
mailboxdb.addMailbox(name, domain, userId, mailboxdb.OWNER_TYPE_USER, function (error) {
mailboxdb.addMailbox(name, domain, userId, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, `mailbox ${name} already exists`));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
@@ -943,7 +953,7 @@ function addMailbox(name, domain, userId, auditSource, callback) {
});
}
function updateMailbox(name, domain, userId, callback) {
function updateMailboxOwner(name, domain, userId, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof userId, 'string');
@@ -951,10 +961,7 @@ function updateMailbox(name, domain, userId, callback) {
name = name.toLowerCase();
var error = validateName(name);
if (error) return callback(error);
mailboxdb.updateMailbox(name, domain, userId, mailboxdb.OWNER_TYPE_USER, function (error) {
mailboxdb.updateMailboxOwner(name, domain, userId, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such mailbox'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));

View File

@@ -4,7 +4,7 @@ exports = module.exports = {
addMailbox: addMailbox,
addGroup: addGroup,
updateMailbox: updateMailbox,
updateMailboxOwner: updateMailboxOwner,
updateList: updateList,
del: del,
@@ -29,11 +29,7 @@ exports = module.exports = {
TYPE_MAILBOX: 'mailbox',
TYPE_LIST: 'list',
TYPE_ALIAS: 'alias',
OWNER_TYPE_USER: 'user',
OWNER_TYPE_APP: 'app',
OWNER_TYPE_GROUP: 'group' // obsolete
TYPE_ALIAS: 'alias'
};
var assert = require('assert'),
@@ -42,7 +38,7 @@ var assert = require('assert'),
safe = require('safetydance'),
util = require('util');
var MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'ownerType', 'aliasTarget', 'creationTime', 'membersJson', 'domain' ].join(',');
var MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'aliasTarget', 'creationTime', 'membersJson', 'domain' ].join(',');
function postProcess(data) {
data.members = safe.JSON.parse(data.membersJson) || [ ];
@@ -51,14 +47,13 @@ function postProcess(data) {
return data;
}
function addMailbox(name, domain, ownerId, ownerType, callback) {
function addMailbox(name, domain, ownerId, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof ownerType, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, ownerType) VALUES (?, ?, ?, ?, ?)', [ name, exports.TYPE_MAILBOX, domain, ownerId, ownerType ], function (error) {
database.query('INSERT INTO mailboxes (name, type, domain, ownerId) VALUES (?, ?, ?, ?)', [ name, exports.TYPE_MAILBOX, domain, ownerId ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'mailbox already exists'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -66,14 +61,13 @@ function addMailbox(name, domain, ownerId, ownerType, callback) {
});
}
function updateMailbox(name, domain, ownerId, ownerType, callback) {
function updateMailboxOwner(name, domain, ownerId, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof ownerType, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('UPDATE mailboxes SET ownerId = ? WHERE name = ? AND domain = ? AND ownerType = ?', [ ownerId, name, domain, ownerType ], function (error, result) {
database.query('UPDATE mailboxes SET ownerId = ? WHERE name = ? AND domain = ?', [ ownerId, name, domain ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
@@ -87,8 +81,8 @@ function addGroup(name, domain, members, callback) {
assert(Array.isArray(members));
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, ownerType, membersJson) VALUES (?, ?, ?, ?, ?, ?)',
[ name, exports.TYPE_LIST, domain, 'admin', exports.OWNER_TYPE_GROUP, JSON.stringify(members) ], function (error) {
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, membersJson) VALUES (?, ?, ?, ?, ?)',
[ name, exports.TYPE_LIST, domain, 'admin', JSON.stringify(members) ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'mailbox already exists'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -259,8 +253,8 @@ function setAliasesForName(name, domain, aliases, callback) {
// clear existing aliases
queries.push({ query: 'DELETE FROM mailboxes WHERE aliasTarget = ? AND domain = ? AND type = ?', args: [ name, domain, exports.TYPE_ALIAS ] });
aliases.forEach(function (alias) {
queries.push({ query: 'INSERT INTO mailboxes (name, type, domain, aliasTarget, ownerId, ownerType) VALUES (?, ?, ?, ?, ?, ?)',
args: [ alias, exports.TYPE_ALIAS, domain, name, results[0].ownerId, results[0].ownerType ] });
queries.push({ query: 'INSERT INTO mailboxes (name, type, domain, aliasTarget, ownerId) VALUES (?, ?, ?, ?, ?)',
args: [ alias, exports.TYPE_ALIAS, domain, name, results[0].ownerId ] });
});
database.transaction(queries, function (error) {

View File

@@ -4,10 +4,10 @@ exports = module.exports = {
add: add,
del: del,
get: get,
getAll: getAll,
list: list,
update: update,
_clear: clear,
clear: clear,
TYPE_USER: 'user',
TYPE_APP: 'app',
@@ -49,7 +49,8 @@ function add(domain, callback) {
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('TRUNCATE TABLE mail', [], function (error) {
// using TRUNCATE makes it fail foreign key check
database.query('DELETE FROM mail', [], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
@@ -81,7 +82,7 @@ function get(domain, callback) {
});
}
function getAll(callback) {
function list(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILDB_FIELDS + ' FROM mail ORDER BY domain', function (error, results) {

View File

@@ -7,7 +7,6 @@ var config = require('./config.js'),
exports = module.exports = {
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
INFRA_VERSION_FILE: path.join(config.baseDir(), 'platformdata/INFRA_VERSION'),
BACKUP_RESULT_FILE: path.join(config.baseDir(), 'platformdata/backup/result.txt'),
OLD_DATA_DIR: path.join(config.baseDir(), 'data'),
PLATFORM_DATA_DIR: path.join(config.baseDir(), 'platformdata'),
@@ -34,6 +33,9 @@ exports = module.exports = {
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'boxdata/updatechecker.json'),
LOG_DIR: path.join(config.baseDir(), 'platformdata/logs'),
TASKS_LOG_DIR: path.join(config.baseDir(), 'platformdata/logs/tasks'),
// this pattern is for the cloudron logs API route to work
BACKUP_LOG_FILE: path.join(config.baseDir(), 'platformdata/logs/backup/app.log'),
UPDATER_LOG_FILE: path.join(config.baseDir(), 'platformdata/logs/updater/app.log')
};

View File

@@ -16,7 +16,6 @@ var addons = require('./addons.js'),
async = require('async'),
config = require('./config.js'),
debug = require('debug')('box:platform'),
execSync = require('child_process').execSync,
fs = require('fs'),
graphs = require('./graphs.js'),
infra = require('./infra_version.js'),
@@ -64,8 +63,7 @@ function start(callback) {
// mark app state before we start addons. this gives the db import logic a chance to mark an app as errored
startApps.bind(null, existingInfra),
graphs.startGraphite.bind(null, existingInfra),
addons.startAddons.bind(null, existingInfra),
pruneInfraImages,
addons.startServices.bind(null, existingInfra),
fs.writeFile.bind(fs, paths.INFRA_VERSION_FILE, JSON.stringify(infra, null, 4))
], function (error) {
if (error) return callback(error);
@@ -86,7 +84,9 @@ function onPlatformReady() {
debug('onPlatformReady: platform is ready');
exports._isReady = true;
taskmanager.resumeTasks();
applyPlatformConfig(NOOP_CALLBACK);
pruneInfraImages(NOOP_CALLBACK);
}
function applyPlatformConfig(callback) {
@@ -97,8 +97,8 @@ function applyPlatformConfig(callback) {
settings.getPlatformConfig(function (error, platformConfig) {
if (error) return retryCallback(error);
addons.updateAddonConfig(platformConfig, function (error) {
if (error) debug('Error updating addons. Will rety in 5 minutes', platformConfig, error);
addons.updateServiceConfig(platformConfig, function (error) {
if (error) debug('Error updating services. Will rety in 5 minutes', platformConfig, error);
retryCallback(error);
});
@@ -110,21 +110,22 @@ function pruneInfraImages(callback) {
debug('pruneInfraImages: checking existing images');
// cannot blindly remove all unused images since redis image may not be used
let images = infra.baseImages.concat(Object.keys(infra.images).map(function (addon) { return infra.images[addon]; }));
const images = infra.baseImages.concat(Object.keys(infra.images).map(function (addon) { return infra.images[addon]; }));
async.eachSeries(images, function (image, iteratorCallback) {
let output = safe.child_process.execSync(`docker images --digests ${image.repo} --format "{{.ID}} {{.Repository}}:{{.Tag}}@{{.Digest}}"`, { encoding: 'utf8' });
if (output === null) return iteratorCallback(safe.error);
for (let image of images) {
let output = execSync(`docker images --digests ${image.repo} --format "{{.ID}} {{.Repository}}:{{.Tag}}@{{.Digest}}"`, { encoding: 'utf8' });
let lines = output.trim().split('\n');
for (let line of lines) {
if (!line) continue;
let parts = line.split(' '); // [ ID, Repo:Tag@Digest ]
if (image.tag === parts[1]) continue; // keep
debug(`pruneInfraImages: removing unused image of ${image.repo}: ${line}`);
shell.execSync('pruneInfraImages', `docker rmi ${parts[0]}`);
}
}
callback();
shell.exec('pruneInfraImages', `docker rmi ${parts[0]}`, iteratorCallback);
}
}, callback);
}
function stopContainers(existingInfra, callback) {
@@ -132,8 +133,10 @@ function stopContainers(existingInfra, callback) {
if (existingInfra.version !== infra.version) {
// TODO: only nuke containers with isCloudronManaged=true
debug('stopping all containers for infra upgrade');
shell.execSync('stopContainers', 'docker ps -qa | xargs --no-run-if-empty docker stop');
shell.execSync('stopContainers', 'docker ps -qa | xargs --no-run-if-empty docker rm -f');
async.series([
shell.exec.bind(null, 'stopContainers', 'docker ps -qa | xargs --no-run-if-empty docker stop'),
shell.exec.bind(null, 'stopContainers', 'docker ps -qa | xargs --no-run-if-empty docker rm -f')
], callback);
} else {
assert(typeof infra.images, 'object');
var changedAddons = [ ];
@@ -144,11 +147,11 @@ function stopContainers(existingInfra, callback) {
debug('stopContainer: stopping addons for incremental infra update: %j', changedAddons);
let filterArg = changedAddons.map(function (c) { return `--filter 'name=${c}'`; }).join(' '); // name=c matches *c*. required for redis-{appid}
// ignore error if container not found (and fail later) so that this code works across restarts
shell.execSync('stopContainers', `docker ps -qa ${filterArg} | xargs --no-run-if-empty docker stop || true`);
shell.execSync('stopContainers', `docker ps -qa ${filterArg} | xargs --no-run-if-empty docker rm -f || true`);
async.series([
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} | xargs --no-run-if-empty docker stop || true`),
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} | xargs --no-run-if-empty docker rm -f || true`)
], callback);
}
callback();
}
function startApps(existingInfra, callback) {
@@ -165,12 +168,13 @@ function startApps(existingInfra, callback) {
}
}
function handleCertChanged(cn) {
function handleCertChanged(cn, callback) {
assert.strictEqual(typeof cn, 'string');
assert.strictEqual(typeof callback, 'function');
debug('handleCertChanged', cn);
if (cn === '*.' + config.adminDomain() || cn === config.adminFqdn()) {
mail.startMail(NOOP_CALLBACK);
}
if (cn === '*.' + config.adminDomain() || cn === config.adminFqdn()) return mail.startMail(callback);
callback();
}

View File

@@ -1,59 +0,0 @@
'use strict';
exports = module.exports = {
set: set,
setDetail: setDetail,
clear: clear,
getAll: getAll,
UPDATE: 'update',
BACKUP: 'backup',
MIGRATE: 'migrate'
};
var assert = require('assert'),
debug = require('debug')('box:progress');
// if progress.update or progress.backup are object, they will contain 'percent' and 'message' properties
// otherwise no such operation is currently ongoing
var progress = {
update: null,
backup: null,
migrate: null
};
// We use -1 for percentage to indicate errors
function set(tag, percent, message) {
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof percent, 'number');
assert.strictEqual(typeof message, 'string');
progress[tag] = {
percent: percent,
message: message,
detail: ''
};
debug('%s: %s %s', tag, percent, message);
}
function setDetail(tag, detail) {
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof detail, 'string');
if (!progress[tag]) return debug('[%s] %s', tag, detail);
progress[tag].detail = detail;
}
function clear(tag) {
assert.strictEqual(typeof tag, 'string');
progress[tag] = null;
debug('clearing %s', tag);
}
function getAll() {
return progress;
}

View File

@@ -1,11 +1,11 @@
'use strict';
exports = module.exports = {
provision: provision,
setup: setup,
restore: restore,
activate: activate,
SetupError: SetupError
ProvisionError: ProvisionError
};
var assert = require('assert'),
@@ -16,7 +16,7 @@ var assert = require('assert'),
constants = require('./constants.js'),
clients = require('./clients.js'),
cloudron = require('./cloudron.js'),
debug = require('debug')('box:setup'),
debug = require('debug')('box:provision'),
domains = require('./domains.js'),
DomainsError = domains.DomainsError,
eventlog = require('./eventlog.js'),
@@ -37,7 +37,7 @@ var RESTART_CMD = path.join(__dirname, 'scripts/restart.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function SetupError(reason, errorOrMessage) {
function ProvisionError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
@@ -55,13 +55,13 @@ function SetupError(reason, errorOrMessage) {
this.nestedError = errorOrMessage;
}
}
util.inherits(SetupError, Error);
SetupError.BAD_FIELD = 'Field error';
SetupError.BAD_STATE = 'Bad State';
SetupError.ALREADY_SETUP = 'Already Setup';
SetupError.INTERNAL_ERROR = 'Internal Error';
SetupError.EXTERNAL_ERROR = 'External Error';
SetupError.ALREADY_PROVISIONED = 'Already Provisioned';
util.inherits(ProvisionError, Error);
ProvisionError.BAD_FIELD = 'Field error';
ProvisionError.BAD_STATE = 'Bad State';
ProvisionError.ALREADY_SETUP = 'Already Setup';
ProvisionError.INTERNAL_ERROR = 'Internal Error';
ProvisionError.EXTERNAL_ERROR = 'External Error';
ProvisionError.ALREADY_PROVISIONED = 'Already Provisioned';
function autoprovision(autoconf, callback) {
assert.strictEqual(typeof autoconf, 'object');
@@ -89,64 +89,72 @@ function autoprovision(autoconf, callback) {
return iteratorDone();
}
}, function (error) {
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
callback(null);
});
}
function provision(dnsConfig, autoconf, auditSource, callback) {
function unprovision(callback) {
assert.strictEqual(typeof callback, 'function');
debug('unprovision');
config.setAdminDomain('');
config.setAdminFqdn('');
config.setAdminLocation('my');
// TODO: also cancel any existing configureWebadmin task
async.series([
mail.clearDomains,
domains.clear
], callback);
}
function setup(dnsConfig, autoconf, auditSource, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof autoconf, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
if (config.adminDomain()) return callback(new SetupError(SetupError.ALREADY_SETUP));
users.isActivated(function (error, activated) {
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (activated) return callback(new ProvisionError(ProvisionError.ALREADY_SETUP));
let webadminStatus = cloudron.getWebadminStatus();
unprovision(function (error) {
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (webadminStatus.configuring || webadminStatus.restore.active) return callback(new SetupError(SetupError.BAD_STATE, 'Already restoring or configuring'));
let webadminStatus = cloudron.getWebadminStatus();
const domain = dnsConfig.domain.toLowerCase();
const zoneName = dnsConfig.zoneName ? dnsConfig.zoneName : (tld.getDomain(domain) || domain);
if (webadminStatus.configuring || webadminStatus.restore.active) return callback(new ProvisionError(ProvisionError.BAD_STATE, 'Already restoring or configuring'));
const adminFqdn = 'my' + (dnsConfig.config.hyphenatedSubdomains ? '-' : '.') + domain;
const domain = dnsConfig.domain.toLowerCase();
const zoneName = dnsConfig.zoneName ? dnsConfig.zoneName : (tld.getDomain(domain) || domain);
debug(`provision: Setting up Cloudron with domain ${domain} and zone ${zoneName} using admin fqdn ${adminFqdn}`);
const adminFqdn = 'my' + (dnsConfig.config.hyphenatedSubdomains ? '-' : '.') + domain;
domains.get(domain, function (error, result) {
if (error && error.reason !== DomainsError.NOT_FOUND) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
debug(`provision: Setting up Cloudron with domain ${domain} and zone ${zoneName} using admin fqdn ${adminFqdn}`);
if (result) return callback(new SetupError(SetupError.BAD_STATE, 'Domain already exists'));
let data = {
zoneName: zoneName,
provider: dnsConfig.provider,
config: dnsConfig.config,
fallbackCertificate: dnsConfig.fallbackCertificate || null,
tlsConfig: dnsConfig.tlsConfig || { provider: 'letsencrypt-prod' }
};
let data = {
zoneName: zoneName,
provider: dnsConfig.provider,
config: dnsConfig.config,
fallbackCertificate: dnsConfig.fallbackCertificate || null,
tlsConfig: dnsConfig.tlsConfig || { provider: 'letsencrypt-prod' }
};
domains.add(domain, data, auditSource, function (error) {
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
if (error && error.reason === DomainsError.ALREADY_EXISTS) return callback(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
async.series([
domains.add.bind(null, domain, data, auditSource),
mail.addDomain.bind(null, domain)
], function (error) {
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
if (error && error.reason === DomainsError.ALREADY_EXISTS) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
config.setAdminDomain(domain); // set fqdn only after dns config is valid, otherwise cannot re-setup if we failed
config.setAdminFqdn(adminFqdn);
config.setAdminLocation('my');
eventlog.add(eventlog.ACTION_PROVISION, auditSource, { });
clients.addDefaultClients(config.adminOrigin(), callback);
async.series([
autoprovision.bind(null, autoconf),
cloudron.configureWebadmin
], NOOP_CALLBACK);
async.series([
mail.addDomain.bind(null, domain),
cloudron.setDashboardDomain.bind(null, domain), // triggers task to setup my. dns/cert/reverseproxy
autoprovision.bind(null, autoconf),
eventlog.add.bind(null, eventlog.ACTION_PROVISION, auditSource, { })
], callback);
});
});
});
}
@@ -190,12 +198,12 @@ function activate(username, password, email, displayName, ip, auditSource, callb
setTimeZone(ip, function () { }); // TODO: get this from user. note that timezone is detected based on the browser location and not the cloudron region
users.createOwner(username, password, email, displayName, auditSource, function (error, userObject) {
if (error && error.reason === UsersError.ALREADY_EXISTS) return callback(new SetupError(SetupError.ALREADY_PROVISIONED));
if (error && error.reason === UsersError.BAD_FIELD) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
if (error && error.reason === UsersError.ALREADY_EXISTS) return callback(new ProvisionError(ProvisionError.ALREADY_PROVISIONED, 'Already activated'));
if (error && error.reason === UsersError.BAD_FIELD) return callback(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { });
@@ -218,21 +226,21 @@ function restore(backupConfig, backupId, version, autoconf, auditSource, callbac
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
if (!semver.valid(version)) return callback(new SetupError(SetupError.BAD_STATE, 'version is not a valid semver'));
if (semver.major(config.version()) !== semver.major(version) || semver.minor(config.version()) !== semver.minor(version)) return callback(new SetupError(SetupError.BAD_STATE, `Run cloudron-setup with --version ${version} to restore from this backup`));
if (!semver.valid(version)) return callback(new ProvisionError(ProvisionError.BAD_STATE, 'version is not a valid semver'));
if (semver.major(config.version()) !== semver.major(version) || semver.minor(config.version()) !== semver.minor(version)) return callback(new ProvisionError(ProvisionError.BAD_STATE, `Run cloudron-setup with --version ${version} to restore from this backup`));
let webadminStatus = cloudron.getWebadminStatus();
if (webadminStatus.configuring || webadminStatus.restore.active) return callback(new SetupError(SetupError.BAD_STATE, 'Already restoring or configuring'));
if (webadminStatus.configuring || webadminStatus.restore.active) return callback(new ProvisionError(ProvisionError.BAD_STATE, 'Already restoring or configuring'));
users.isActivated(function (error, activated) {
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
if (activated) return callback(new SetupError(SetupError.ALREADY_PROVISIONED, 'Already activated'));
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (activated) return callback(new ProvisionError(ProvisionError.ALREADY_PROVISIONED, 'Already activated'));
backups.testConfig(backupConfig, function (error) {
if (error && error.reason === BackupsError.BAD_FIELD) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new SetupError(SetupError.EXTERNAL_ERROR, error.message));
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
if (error && error.reason === BackupsError.BAD_FIELD) return callback(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new ProvisionError(ProvisionError.EXTERNAL_ERROR, error.message));
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
debug(`restore: restoring from ${backupId} from provider ${backupConfig.provider} with format ${backupConfig.format}`);
@@ -242,14 +250,14 @@ function restore(backupConfig, backupId, version, autoconf, auditSource, callbac
callback(null); // do no block
async.series([
backups.restore.bind(null, backupConfig, backupId),
backups.restore.bind(null, backupConfig, backupId, (progress) => debug(`restore: ${progress}`)),
eventlog.add.bind(null, eventlog.ACTION_RESTORE, auditSource, { backupId }),
autoprovision.bind(null, autoconf),
// currently, our suggested restore flow is after a dnsSetup. The dnSetup creates DKIM keys and updates the DNS
// for this reason, we have to re-setup DNS after a restore so it has DKIm from the backup
// Once we have a 100% IP based restore, we can skip this
mail.setDnsRecords.bind(null, config.adminDomain()),
shell.sudo.bind(null, 'restart', [ RESTART_CMD ])
shell.sudo.bind(null, 'restart', [ RESTART_CMD ], {})
], function (error) {
debug('restore:', error);
if (error) webadminStatus.restore.error = error.message;

View File

@@ -114,6 +114,8 @@ function isExpiringSync(certFilePath, hours) {
var result = safe.child_process.spawnSync('/usr/bin/openssl', [ 'x509', '-checkend', String(60 * 60 * hours), '-in', certFilePath ]);
if (!result) return 3; // some error
debug('isExpiringSync: %s %s %s', certFilePath, result.stdout.toString('utf8').trim(), result.status);
return result.status === 1; // 1 - expired 0 - not expired
@@ -130,19 +132,21 @@ function providerMatchesSync(domainObject, certFilePath, apiOptions) {
if (apiOptions.fallback) return certFilePath.includes('.host.cert');
const subjectAndIssuer = safe.child_process.execSync(`/usr/bin/openssl x509 -noout -subject -issuer -in "${certFilePath}"`, { encoding: 'utf8' });
if (!subjectAndIssuer) return false; // something bad happenned
const subject = subjectAndIssuer.match(/^subject=(.*)$/m)[1];
const domain = subject.substr(subject.indexOf('=') + 1).trim(); // subject can be /CN=, CN=, CN = and other forms
const issuer = subjectAndIssuer.match(/^issuer=(.*)$/m)[1];
const isWildcardCert = subject.includes('*');
const isWildcardCert = domain.includes('*');
const isLetsEncryptProd = issuer.includes('Let\'s Encrypt Authority');
const issuerMismatch = (apiOptions.prod && !isLetsEncryptProd) || (!apiOptions.prod && isLetsEncryptProd);
// bare domain is not part of wildcard SAN
const wildcardMismatch = (subject !== domainObject.domain) && (apiOptions.wildcard && !isWildcardCert) || (!apiOptions.wildcard && isWildcardCert);
const wildcardMismatch = (domain !== domainObject.domain) && (apiOptions.wildcard && !isWildcardCert) || (!apiOptions.wildcard && isWildcardCert);
const mismatch = issuerMismatch || wildcardMismatch;
debug(`providerMatchesSync: ${certFilePath} subject=${subject} issuer=${issuer} wildcard=${isWildcardCert}/${apiOptions.wildcard} prod=${isLetsEncryptProd}/${apiOptions.prod} match=${!mismatch}`);
debug(`providerMatchesSync: ${certFilePath} subject=${subject} domain=${domain} issuer=${issuer} wildcard=${isWildcardCert}/${apiOptions.wildcard} prod=${isLetsEncryptProd}/${apiOptions.prod} match=${!mismatch}`);
return !mismatch;
}
@@ -164,13 +168,17 @@ function validateCertificate(location, domainObject, certificate) {
const fqdn = domains.fqdn(location, domainObject);
var result = safe.child_process.execSync(`openssl x509 -noout -checkhost "${fqdn}"`, { encoding: 'utf8', input: cert });
if (!result) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Unable to get certificate subject.');
if (result === null) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Unable to get certificate subject:' + safe.error.message);
if (result.indexOf('does match certificate') === -1) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, `Certificate is not valid for this domain. Expecting ${fqdn}`);
// http://httpd.apache.org/docs/2.0/ssl/ssl_faq.html#verify
var certModulus = safe.child_process.execSync('openssl x509 -noout -modulus', { encoding: 'utf8', input: cert });
if (certModulus === null) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, `Unable to get cert modulus: ${safe.error.message}`);
var keyModulus = safe.child_process.execSync('openssl rsa -noout -modulus', { encoding: 'utf8', input: key });
if (keyModulus === null) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, `Unable to get key modulus: ${safe.error.message}`);
if (certModulus !== keyModulus) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Key does not match the certificate.');
// check expiration
@@ -183,7 +191,7 @@ function validateCertificate(location, domainObject, certificate) {
function reload(callback) {
if (process.env.BOX_ENV === 'test') return callback();
shell.sudo('reload', [ RELOAD_NGINX_CMD ], callback);
shell.sudo('reload', [ RELOAD_NGINX_CMD ], {}, callback);
}
function generateFallbackCertificateSync(domainObject) {
@@ -234,12 +242,14 @@ function setFallbackCertificate(domain, fallback, callback) {
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.key`), fallback.key)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
}
platform.handleCertChanged('*.' + domain);
reload(function (error) {
platform.handleCertChanged('*.' + domain, function (error) {
if (error) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, error));
return callback(null);
reload(function (error) {
if (error) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, error));
return callback(null);
});
});
}
@@ -497,9 +507,10 @@ function unconfigureApp(app, callback) {
});
}
function renewCerts(options, auditSource, callback) {
function renewCerts(options, auditSource, progressCallback, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
apps.getAll(function (error, allApps) {
@@ -522,7 +533,11 @@ function renewCerts(options, auditSource, callback) {
if (options.domain) appDomains = appDomains.filter(function (appDomain) { return appDomain.domain === options.domain; });
let progress = 1;
async.eachSeries(appDomains, function (appDomain, iteratorCallback) {
progressCallback({ percent: progress, message: `Renewing certs of ${appDomain.fqdn}` });
progress += Math.round(100/appDomains.length);
ensureCertificate(appDomain.fqdn, appDomain.domain, auditSource, function (error, bundle) {
if (error) return iteratorCallback(error); // this can happen if cloudron is not setup yet
@@ -537,17 +552,15 @@ function renewCerts(options, auditSource, callback) {
if (appDomain.type === 'webadmin') configureFunc = writeAdminConfig.bind(null, bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn());
else if (appDomain.type === 'main') configureFunc = writeAppConfig.bind(null, appDomain.app, bundle);
else if (appDomain.type === 'alternate') configureFunc = writeAppRedirectConfig.bind(null, appDomain.app, appDomain.fqdn, bundle);
else return callback(new Error(`Unknown domain type for ${appDomain.fqdn}. This should never happen`));
else return iteratorCallback(new Error(`Unknown domain type for ${appDomain.fqdn}. This should never happen`));
configureFunc(function (ignoredError) {
if (ignoredError) debug('renewAll: error reconfiguring app', ignoredError);
platform.handleCertChanged(appDomain.fqdn);
iteratorCallback(); // move to next domain
platform.handleCertChanged(appDomain.fqdn, iteratorCallback);
});
});
});
}, callback);
});
}
@@ -578,8 +591,10 @@ function configureDefaultServer(callback) {
debug('configureDefaultServer: create new cert');
var cn = 'cloudron-' + (new Date()).toISOString(); // randomize date a bit to keep firefox happy
var certCommand = util.format('openssl req -x509 -newkey rsa:2048 -keyout %s -out %s -days 3650 -subj /CN=%s -nodes', keyFilePath, certFilePath, cn);
safe.child_process.execSync(certCommand);
if (!safe.child_process.execSync(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=${cn} -nodes`)) {
debug(`configureDefaultServer: could not generate certificate: ${safe.error.message}`);
return callback(safe.error);
}
}
writeAdminConfig({ certFilePath, keyFilePath }, constants.NGINX_DEFAULT_CONFIG_FILE_NAME, '', function (error) {

View File

@@ -134,6 +134,7 @@ function installApp(req, res, next) {
if ('sso' in data && typeof data.sso !== 'boolean') return next(new HttpError(400, 'sso must be a boolean'));
if ('enableBackup' in data && typeof data.enableBackup !== 'boolean') return next(new HttpError(400, 'enableBackup must be a boolean'));
if ('enableAutomaticUpdate' in data && typeof data.enableAutomaticUpdate !== 'boolean') return next(new HttpError(400, 'enableAutomaticUpdate must be a boolean'));
if (('debugMode' in data) && typeof data.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object'));
@@ -174,6 +175,10 @@ function configureApp(req, res, next) {
if ('location' in data && typeof data.location !== 'string') return next(new HttpError(400, 'location must be string'));
if ('domain' in data && typeof data.domain !== 'string') return next(new HttpError(400, 'domain must be string'));
// domain, location must both be provided since they are unique together
if ('location' in data && !('domain' in data)) return next(new HttpError(400, 'domain must be provided'));
if (!('location' in data) && 'domain' in data) return next(new HttpError(400, 'location must be provided'));
if ('portBindings' in data && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
if ('accessRestriction' in data && typeof data.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction must be an object'));
@@ -187,6 +192,7 @@ function configureApp(req, res, next) {
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
if ('enableBackup' in data && typeof data.enableBackup !== 'boolean') return next(new HttpError(400, 'enableBackup must be a boolean'));
if ('enableAutomaticUpdate' in data && typeof data.enableAutomaticUpdate !== 'boolean') return next(new HttpError(400, 'enableAutomaticUpdate must be a boolean'));
if (('debugMode' in data) && typeof data.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object'));

View File

@@ -1,8 +1,8 @@
'use strict';
exports = module.exports = {
get: get,
create: create
list: list,
startBackup: startBackup
};
var backupdb = require('../backupdb.js'),
@@ -16,7 +16,7 @@ function auditSource(req) {
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
function get(req, res, next) {
function list(req, res, next) {
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
@@ -31,13 +31,11 @@ function get(req, res, next) {
});
}
function create(req, res, next) {
// note that cloudron.backup only waits for backup initiation and not for backup to complete
// backup progress can be checked up ny polling the progress api call
backups.backup(auditSource(req), function (error) {
function startBackup(req, res, next) {
backups.startBackupTask(auditSource(req), function (error, taskId) {
if (error && error.reason === BackupsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
next(new HttpSuccess(202, { taskId }));
});
}

View File

@@ -1,60 +0,0 @@
'use strict';
exports = module.exports = {
getConfig: getConfig,
changePlan: changePlan
};
var caas = require('../caas.js'),
CaasError = require('../caas.js').CaasError,
config = require('../config.js'),
debug = require('debug')('box:routes/cloudron'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
_ = require('underscore');
function getConfig(req, res, next) {
if (config.provider() !== 'caas') return next(new HttpError(422, 'Cannot use this API with this provider'));
caas.getBoxAndUserDetails(function (error, result) {
if (error) return next(new HttpError(500, error));
// the result is { box: { region, size, plan }, user: { billing, currency } }
next(new HttpSuccess(200, {
region: result.box.region,
size: result.box.size,
billing: !!result.user.billing,
plan: result.box.plan,
currency: result.user.currency
}));
});
}
function changePlan(req, res, next) {
if (config.provider() !== 'caas') return next(new HttpError(422, 'Cannot use this API with this provider'));
if ('size' in req.body && typeof req.body.size !== 'string') return next(new HttpError(400, 'size must be string'));
if ('region' in req.body && typeof req.body.region !== 'string') return next(new HttpError(400, 'region must be string'));
if ('domain' in req.body) {
if (typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be string'));
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider must be string'));
}
if ('zoneName' in req.body && typeof req.body.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be string'));
debug('Migration requested domain:%s size:%s region:%s', req.body.domain, req.body.size, req.body.region);
var options = _.pick(req.body, 'domain', 'size', 'region');
if (Object.keys(options).length === 0) return next(new HttpError(400, 'no migrate option provided'));
if (options.domain) options.domain = options.domain.toLowerCase();
caas.changePlan(req.body, function (error) { // pass req.body because 'domain' can have arbitrary options
if (error && error.reason === CaasError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === CaasError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}

View File

@@ -2,7 +2,7 @@
exports = module.exports = {
reboot: reboot,
getProgress: getProgress,
isRebootRequired: isRebootRequired,
getConfig: getConfig,
getDisks: getDisks,
getUpdateInfo: getUpdateInfo,
@@ -12,6 +12,8 @@ exports = module.exports = {
getLogs: getLogs,
getLogStream: getLogStream,
getStatus: getStatus,
setDashboardDomain: setDashboardDomain,
renewCerts: renewCerts
};
var appstore = require('../appstore.js'),
@@ -22,7 +24,6 @@ var appstore = require('../appstore.js'),
CloudronError = cloudron.CloudronError,
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
progress = require('../progress.js'),
updater = require('../updater.js'),
updateChecker = require('../updatechecker.js'),
UpdaterError = require('../updater.js').UpdaterError,
@@ -33,15 +34,19 @@ function auditSource(req) {
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
function getProgress(req, res, next) {
return next(new HttpSuccess(200, progress.getAll()));
}
function reboot(req, res, next) {
// Finish the request, to let the appstore know we triggered the restore it
// Finish the request, to let the appstore know we triggered the reboot
next(new HttpSuccess(202, {}));
cloudron.reboot(function () { });
cloudron.reboot(function () {});
}
function isRebootRequired(req, res, next) {
cloudron.isRebootRequired(function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { rebootRequired: result }));
});
}
function getConfig(req, res, next) {
@@ -61,13 +66,12 @@ function getDisks(req, res, next) {
function update(req, res, next) {
// this only initiates the update, progress can be checked via the progress route
updater.updateToLatest(auditSource(req), function (error) {
updater.updateToLatest(auditSource(req), function (error, taskId) {
if (error && error.reason === UpdaterError.ALREADY_UPTODATE) return next(new HttpError(422, error.message));
if (error && error.reason === UpdaterError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === UpdaterError.SELF_UPGRADE_NOT_SUPPORTED) return next(new HttpError(412, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
next(new HttpSuccess(202, { taskId }));
});
}
@@ -171,6 +175,17 @@ function getLogStream(req, res, next) {
});
}
function setDashboardDomain(req, res, next) {
if (!req.body.domain || typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
cloudron.setDashboardDomain(req.body.domain, function (error) {
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204, {}));
});
}
function getStatus(req, res, next) {
cloudron.getStatus(function (error, status) {
if (error) return next(new HttpError(500, error));
@@ -178,3 +193,12 @@ function getStatus(req, res, next) {
next(new HttpSuccess(200, status));
});
}
function renewCerts(req, res, next) {
cloudron.renewCerts({ domain: req.body.domain || null }, auditSource(req), function (error, taskId) {
if (error && error.reason === CloudronError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, { taskId }));
});
}

View File

@@ -7,8 +7,6 @@ exports = module.exports = {
update: update,
del: del,
renewCerts: renewCerts,
verifyDomainLock: verifyDomainLock
};
@@ -154,16 +152,3 @@ function del(req, res, next) {
next(new HttpSuccess(204));
});
}
function renewCerts(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
domains.renewCerts(req.params.domain, auditSource(req), function (error) {
if (error && error.reason === DomainsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
next(new HttpSuccess(202, {}));
}

View File

@@ -7,7 +7,7 @@ exports = module.exports = {
var middleware = require('../middleware/index.js'),
url = require('url');
var graphiteProxy = middleware.proxy(url.parse('http://127.0.0.1:8000'));
var graphiteProxy = middleware.proxy(url.parse('http://127.0.0.1:8417'));
function getGraphs(req, res, next) {
var parsedUrl = url.parse(req.url, true /* parseQueryString */);

View File

@@ -4,7 +4,6 @@ exports = module.exports = {
accesscontrol: require('./accesscontrol.js'),
apps: require('./apps.js'),
backups: require('./backups.js'),
caas: require('./caas.js'),
clients: require('./clients.js'),
cloudron: require('./cloudron.js'),
developer: require('./developer.js'),
@@ -15,9 +14,11 @@ exports = module.exports = {
oauth2: require('./oauth2.js'),
mail: require('./mail.js'),
profile: require('./profile.js'),
setup: require('./setup.js'),
sysadmin: require('./sysadmin.js'),
provision: require('./provision.js'),
services: require('./services.js'),
settings: require('./settings.js'),
sysadmin: require('./sysadmin.js'),
ssh: require('./ssh.js'),
tasks: require('./tasks.js'),
users: require('./users.js')
};

View File

@@ -17,7 +17,7 @@ exports = module.exports = {
sendTestMail: sendTestMail,
getMailboxes: getMailboxes,
listMailboxes: listMailboxes,
getMailbox: getMailbox,
addMailbox: addMailbox,
updateMailbox: updateMailbox,
@@ -214,10 +214,10 @@ function sendTestMail(req, res, next) {
});
}
function getMailboxes(req, res, next) {
function listMailboxes(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.getMailboxes(req.params.domain, function (error, result) {
mail.listMailboxes(req.params.domain, function (error, result) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
@@ -259,7 +259,7 @@ function updateMailbox(req, res, next) {
if (typeof req.body.userId !== 'string') return next(new HttpError(400, 'userId must be a string'));
mail.updateMailbox(req.params.name, req.params.domain, req.body.userId, function (error) {
mail.updateMailboxOwner(req.params.name, req.params.domain, req.body.userId, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));

View File

@@ -3,7 +3,7 @@
exports = module.exports = {
providerTokenAuth: providerTokenAuth,
setupTokenAuth: setupTokenAuth,
provision: provision,
setup: setup,
activate: activate,
restore: restore
};
@@ -15,8 +15,8 @@ var assert = require('assert'),
debug = require('debug')('box:routes/setup'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
setup = require('../setup.js'),
SetupError = require('../setup.js').SetupError,
provision = require('../provision.js'),
ProvisionError = require('../provision.js').ProvisionError,
superagent = require('superagent');
function auditSource(req) {
@@ -61,7 +61,7 @@ function setupTokenAuth(req, res, next) {
});
}
function provision(req, res, next) {
function setup(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.dnsConfig || typeof req.body.dnsConfig !== 'object') return next(new HttpError(400, 'dnsConfig is required'));
@@ -83,10 +83,10 @@ function provision(req, res, next) {
// it can take sometime to setup DNS, register cloudron
req.clearTimeout();
setup.provision(dnsConfig, req.body.autoconf || {}, auditSource(req), function (error) {
if (error && error.reason === SetupError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === SetupError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === SetupError.BAD_STATE) return next(new HttpError(409, error.message));
provision.setup(dnsConfig, req.body.autoconf || {}, auditSource(req), function (error) {
if (error && error.reason === ProvisionError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === ProvisionError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === ProvisionError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
@@ -109,9 +109,9 @@ function activate(req, res, next) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
debug('activate: username:%s ip:%s', username, ip);
setup.activate(username, password, email, displayName, ip, auditSource(req), function (error, info) {
if (error && error.reason === SetupError.ALREADY_PROVISIONED) return next(new HttpError(409, 'Already setup'));
if (error && error.reason === SetupError.BAD_FIELD) return next(new HttpError(400, error.message));
provision.activate(username, password, email, displayName, ip, auditSource(req), function (error, info) {
if (error && error.reason === ProvisionError.ALREADY_PROVISIONED) return next(new HttpError(409, 'Already setup'));
if (error && error.reason === ProvisionError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
// only in caas case do we have to notify the api server about activation
@@ -146,11 +146,11 @@ function restore(req, res, next) {
// TODO: validate subfields of these objects
if (req.body.autoconf && typeof req.body.autoconf !== 'object') return next(new HttpError(400, 'autoconf must be an object'));
setup.restore(backupConfig, req.body.backupId, req.body.version, req.body.autoconf || {}, auditSource(req), function (error) {
if (error && error.reason === SetupError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === SetupError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === SetupError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === SetupError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
provision.restore(backupConfig, req.body.backupId, req.body.version, req.body.autoconf || {}, auditSource(req), function (error) {
if (error && error.reason === ProvisionError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === ProvisionError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === ProvisionError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === ProvisionError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));

134
src/routes/services.js Normal file
View File

@@ -0,0 +1,134 @@
'use strict';
exports = module.exports = {
getAll: getAll,
get: get,
configure: configure,
getLogs: getLogs,
getLogStream: getLogStream,
restart: restart
};
var addons = require('../addons.js'),
AddonsError = addons.AddonsError,
assert = require('assert'),
debug = require('debug')('box:routes/addons'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
function getAll(req, res, next) {
addons.getServices(function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { services: result }));
});
}
function get(req, res, next) {
assert.strictEqual(typeof req.params.service, 'string');
addons.getService(req.params.service, function (error, result) {
if (error && error.reason === AddonsError.NOT_FOUND) return next(new HttpError(404, 'No such service'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { service: result }));
});
}
function configure(req, res, next) {
assert.strictEqual(typeof req.params.service, 'string');
if (typeof req.body.memory !== 'number') return next(new HttpError(400, 'memory must be a number'));
const data = {
memory: req.body.memory,
memorySwap: req.body.memory * 2
};
addons.configureService(req.params.service, data, function (error) {
if (error && error.reason === AddonsError.NOT_FOUND) return next(new HttpError(404, 'No such service'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}
function getLogs(req, res, next) {
assert.strictEqual(typeof req.params.service, 'string');
var lines = req.query.lines ? parseInt(req.query.lines, 10) : 100;
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number'));
debug(`Getting logs of service ${req.params.service}`);
var options = {
lines: lines,
follow: false,
format: req.query.format
};
addons.getServiceLogs(req.params.service, options, function (error, logStream) {
if (error && error.reason === AddonsError.NOT_FOUND) return next(new HttpError(404, 'No such service'));
if (error) return next(new HttpError(500, error));
res.writeHead(200, {
'Content-Type': 'application/x-logs',
'Content-Disposition': 'attachment; filename="log.txt"',
'Cache-Control': 'no-cache',
'X-Accel-Buffering': 'no' // disable nginx buffering
});
logStream.pipe(res);
});
}
// this route is for streaming logs
function getLogStream(req, res, next) {
assert.strictEqual(typeof req.params.service, 'string');
debug(`Getting logstream of service ${req.params.service}`);
var lines = req.query.lines ? parseInt(req.query.lines, 10) : -10; // we ignore last-event-id
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
function sse(id, data) { return 'id: ' + id + '\ndata: ' + data + '\n\n'; }
if (req.headers.accept !== 'text/event-stream') return next(new HttpError(400, 'This API call requires EventStream'));
var options = {
lines: lines,
follow: true
};
addons.getServiceLogs(req.params.service, options, function (error, logStream) {
if (error && error.reason === AddonsError.NOT_FOUND) return next(new HttpError(404, 'No such service'));
if (error) return next(new HttpError(500, error));
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no', // disable nginx buffering
'Access-Control-Allow-Origin': '*'
});
res.write('retry: 3000\n');
res.on('close', logStream.close);
logStream.on('data', function (data) {
var obj = JSON.parse(data);
res.write(sse(obj.monotonicTimestamp, JSON.stringify(obj))); // send timestamp as id
});
logStream.on('end', res.end.bind(res));
logStream.on('error', res.end.bind(res, null));
});
}
function restart(req, res, next) {
assert.strictEqual(typeof req.params.service, 'string');
debug(`Restarting service ${req.params.service}`);
addons.restartService(req.params.service, function (error) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}

View File

@@ -26,11 +26,11 @@ function backup(req, res, next) {
// note that cloudron.backup only waits for backup initiation and not for backup to complete
// backup progress can be checked up ny polling the progress api call
var auditSource = { userId: null, username: 'sysadmin' };
backups.backup(auditSource, function (error) {
backups.startBackupTask(auditSource, function (error, taskId) {
if (error && error.reason === BackupsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
next(new HttpSuccess(202, { taskId }));
});
}
@@ -39,13 +39,12 @@ function update(req, res, next) {
// this only initiates the update, progress can be checked via the progress route
var auditSource = { userId: null, username: 'sysadmin' };
updater.updateToLatest(auditSource, function (error) {
updater.updateToLatest(auditSource, function (error, taskId) {
if (error && error.reason === UpdaterError.ALREADY_UPTODATE) return next(new HttpError(422, error.message));
if (error && error.reason === UpdaterError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === UpdaterError.SELF_UPGRADE_NOT_SUPPORTED) return next(new HttpError(412, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
next(new HttpSuccess(202, { taskId }));
});
}

121
src/routes/tasks.js Normal file
View File

@@ -0,0 +1,121 @@
'use strict';
exports = module.exports = {
get: get,
stopTask: stopTask,
list: list,
getLogs: getLogs,
getLogStream: getLogStream
};
let assert = require('assert'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
TaskError = require('../tasks.js').TaskError,
tasks = require('../tasks.js');
function stopTask(req, res, next) {
assert.strictEqual(typeof req.params.taskId, 'string');
tasks.stopTask(req.params.taskId, function (error) {
if (error && error.reason === TaskError.NOT_FOUND) return next(new HttpError(404, 'No such task'));
if (error && error.reason === TaskError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204, {}));
});
}
function get(req, res, next) {
assert.strictEqual(typeof req.params.taskId, 'string');
tasks.get(req.params.taskId, function (error, task) {
if (error && error.reason === TaskError.NOT_FOUND) return next(new HttpError(404, 'No such task'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, tasks.removePrivateFields(task)));
});
}
function list(req, res, next) {
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
if (req.query.type && typeof req.query.type !== 'string') return next(new HttpError(400, 'type must be a string'));
tasks.listByTypePaged(req.query.type || null, page, perPage, function (error, result) {
if (error) return next(new HttpError(500, error));
result = result.map(tasks.removePrivateFields);
next(new HttpSuccess(200, { tasks: result }));
});
}
function getLogs(req, res, next) {
assert.strictEqual(typeof req.params.taskId, 'string');
var lines = req.query.lines ? parseInt(req.query.lines, 10) : 100;
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number'));
var options = {
lines: lines,
follow: false,
format: req.query.format
};
tasks.getLogs(req.params.taskId, options, function (error, logStream) {
if (error && error.reason === TaskError.NOT_FOUND) return next(new HttpError(404, 'No such task'));
if (error) return next(new HttpError(500, error));
res.writeHead(200, {
'Content-Type': 'application/x-logs',
'Content-Disposition': 'attachment; filename="log.txt"',
'Cache-Control': 'no-cache',
'X-Accel-Buffering': 'no' // disable nginx buffering
});
logStream.pipe(res);
});
}
// this route is for streaming logs
function getLogStream(req, res, next) {
assert.strictEqual(typeof req.params.taskId, 'string');
var lines = req.query.lines ? parseInt(req.query.lines, 10) : -10; // we ignore last-event-id
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
function sse(id, data) { return 'id: ' + id + '\ndata: ' + data + '\n\n'; }
if (req.headers.accept !== 'text/event-stream') return next(new HttpError(400, 'This API call requires EventStream'));
var options = {
lines: lines,
follow: true
};
tasks.getLogs(req.params.taskId, options, function (error, logStream) {
if (error && error.reason === TaskError.NOT_FOUND) return next(new HttpError(404, 'No such task'));
if (error) return next(new HttpError(500, error));
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no', // disable nginx buffering
'Access-Control-Allow-Origin': '*'
});
res.write('retry: 3000\n');
res.on('close', logStream.close);
logStream.on('data', function (data) {
var obj = JSON.parse(data);
res.write(sse(obj.monotonicTimestamp, JSON.stringify(obj))); // send timestamp as id
});
logStream.on('end', res.end.bind(res));
logStream.on('error', res.end.bind(res, null));
});
}

View File

@@ -264,8 +264,6 @@ function stopBox(done) {
}
describe('App API', function () {
this.timeout(100000);
before(startBox);
after(stopBox);
@@ -618,11 +616,7 @@ describe('App API', function () {
});
describe('App installation', function () {
this.timeout(100000);
var apiHockInstance = hock.createHock({ throwOnUnmatched: false });
var apiHockServer;
var validCert1, validKey1;
before(function (done) {
@@ -820,7 +814,6 @@ describe('App installation', function () {
});
it('installation - app can check addons', function (done) {
this.timeout(120000);
console.log('This test can take a while as it waits for scheduler addon to tick 3');
checkAddons(appEntry, done);
});
@@ -949,7 +942,6 @@ describe('App installation', function () {
});
it('installation - app can check addons', function (done) {
this.timeout(120000);
console.log('This test can take a while as it waits for scheduler addon to tick 2');
checkAddons(appEntry, done);
});
@@ -1076,7 +1068,6 @@ describe('App installation', function () {
});
it('installation - app can check addons', function (done) {
this.timeout(120000);
console.log('This test can take a while as it waits for scheduler addon to tick 4');
checkAddons(appEntry, done);
});

View File

@@ -145,29 +145,6 @@ describe('Caas', function () {
});
});
describe('get config', function () {
before(setup);
after(cleanup);
it('succeeds (admin)', function (done) {
var scope = nock(config.apiServerOrigin())
.get('/api/v1/boxes/BOX_ID?token=ACCESS_TOKEN2')
.reply(200, { box: { region: 'sfo', size: '1gb' }, user: { }});
superagent.get(SERVER_URL + '/api/v1/caas/config')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.size).to.eql('1gb');
expect(result.body.region).to.eql('sfo');
expect(scope.isDone()).to.be.ok();
done();
});
});
});
describe('Backups API', function () {
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/BOX_ID/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey' } }, { 'Content-Type': 'application/json' });
@@ -191,169 +168,5 @@ describe('Caas', function () {
});
});
});
xdescribe('migrate', function () {
before(function (done) {
async.series([
setup,
function (callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/BOX_ID/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/BOX_ID/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash token for further use
token = result.body.token;
callback();
});
}
], done);
});
after(function (done) {
locker.unlock(locker._operation); // migrate never unlocks
cleanup(done);
});
it('fails without token', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo'})
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails without password', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo'})
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds without size', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
done();
});
});
it('fails with wrong size type', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 4, region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds without region', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
done();
});
});
it('fails with wrong region type', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 4, password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails when in wrong state', function (done) {
var scope2 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/BOX_ID/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
var scope3 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/BOX_ID/backupDone?token=APPSTORE_TOKEN', function (body) {
return body.boxVersion && body.restoreKey && !body.appId && !body.appVersion && body.appBackupIds.length === 0;
})
.reply(200, { id: 'someid' });
var scope1 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/BOX_ID/migrate?token=APPSTORE_TOKEN', function (body) {
return body.size && body.region && body.restoreKey;
}).reply(409, {});
injectShellMock();
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
function checkAppstoreServerCalled() {
if (scope1.isDone() && scope2.isDone() && scope3.isDone()) {
restoreShellMock();
return done();
}
setTimeout(checkAppstoreServerCalled, 100);
}
checkAppstoreServerCalled();
});
});
it('succeeds', function (done) {
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/BOX_ID/migrate?token=APPSTORE_TOKEN', function (body) {
return body.size && body.region && body.restoreKey;
}).reply(202, {});
var scope2 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/BOX_ID/backupDone?token=APPSTORE_TOKEN', function (body) {
return body.boxVersion && body.restoreKey && !body.appId && !body.appVersion && body.appBackupIds.length === 0;
})
.reply(200, { id: 'someid' });
var scope3 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/BOX_ID/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
injectShellMock();
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
function checkAppstoreServerCalled() {
if (scope1.isDone() && scope2.isDone() && scope3.isDone()) {
restoreShellMock();
return done();
}
setTimeout(checkAppstoreServerCalled, 100);
}
checkAppstoreServerCalled();
});
});
});
});

View File

@@ -483,8 +483,6 @@ describe('Clients', function () {
});
describe('delete tokens by client', function () {
this.timeout(5000);
before(setup2);
after(cleanup);

View File

@@ -191,7 +191,6 @@ describe('Cloudron', function () {
expect(result.body.apiServerOrigin).to.eql('http://localhost:6060');
expect(result.body.webServerOrigin).to.eql(null);
expect(result.body.adminFqdn).to.eql(config.adminFqdn());
expect(result.body.progress).to.be.an('object');
expect(result.body.version).to.eql(config.version());
expect(result.body.memory).to.eql(os.totalmem());
expect(result.body.cloudronName).to.be.a('string');

View File

@@ -37,8 +37,6 @@ function cleanup(done) {
}
describe('Developer API', function () {
this.timeout(20000);
describe('login', function () {
before(function (done) {
async.series([

View File

@@ -43,8 +43,6 @@ var DOMAIN_1 = {
};
describe('Domains API', function () {
this.timeout(10000);
before(function (done) {
config._reset();
config.setFqdn(DOMAIN);

View File

@@ -78,8 +78,6 @@ function cleanup(done) {
}
describe('Eventlog API', function () {
this.timeout(10000);
before(setup);
after(cleanup);

View File

@@ -105,8 +105,6 @@ function cleanup(done) {
}
describe('Mail API', function () {
this.timeout(10000);
before(setup);
after(cleanup);
@@ -231,8 +229,6 @@ describe('Mail API', function () {
var dnsAnswerQueue = [];
var dkimDomain, spfDomain, mxDomain, dmarcDomain;
this.timeout(10000);
before(function (done) {
var dns = require('../../native-dns.js');
@@ -765,7 +761,6 @@ describe('Mail API', function () {
expect(res.body.mailbox).to.be.an('object');
expect(res.body.mailbox.name).to.equal(MAILBOX_NAME);
expect(res.body.mailbox.ownerId).to.equal(userId);
expect(res.body.mailbox.ownerType).to.equal('user');
expect(res.body.mailbox.aliasTarget).to.equal(null);
expect(res.body.mailbox.domain).to.equal(DOMAIN_0.domain);
done();
@@ -781,7 +776,6 @@ describe('Mail API', function () {
expect(res.body.mailboxes[0]).to.be.an('object');
expect(res.body.mailboxes[0].name).to.equal(MAILBOX_NAME);
expect(res.body.mailboxes[0].ownerId).to.equal(userId);
expect(res.body.mailboxes[0].ownerType).to.equal('user');
expect(res.body.mailboxes[0].aliasTarget).to.equal(null);
expect(res.body.mailboxes[0].domain).to.equal(DOMAIN_0.domain);
done();
@@ -914,12 +908,10 @@ describe('Mail API', function () {
expect(res.body.aliases.length).to.eql(2);
expect(res.body.aliases[0].name).to.equal('hello');
expect(res.body.aliases[0].ownerId).to.equal(userId);
expect(res.body.aliases[0].ownerType).to.equal('user');
expect(res.body.aliases[0].aliasTarget).to.equal(MAILBOX_NAME);
expect(res.body.aliases[0].domain).to.equal(DOMAIN_0.domain);
expect(res.body.aliases[1].name).to.equal('there');
expect(res.body.aliases[1].ownerId).to.equal(userId);
expect(res.body.aliases[1].ownerType).to.equal('user');
expect(res.body.aliases[1].aliasTarget).to.equal(MAILBOX_NAME);
expect(res.body.aliases[1].domain).to.equal(DOMAIN_0.domain);
done();
@@ -1031,7 +1023,6 @@ describe('Mail API', function () {
expect(res.body.list).to.be.an('object');
expect(res.body.list.name).to.equal(LIST_NAME);
expect(res.body.list.ownerId).to.equal('admin');
expect(res.body.list.ownerType).to.equal('group');
expect(res.body.list.aliasTarget).to.equal(null);
expect(res.body.list.domain).to.equal(DOMAIN_0.domain);
expect(res.body.list.members).to.eql([ 'admin2', 'superadmin' ]);
@@ -1048,7 +1039,6 @@ describe('Mail API', function () {
expect(res.body.lists.length).to.equal(1);
expect(res.body.lists[0].name).to.equal(LIST_NAME);
expect(res.body.lists[0].ownerId).to.equal('admin');
expect(res.body.lists[0].ownerType).to.equal('group');
expect(res.body.lists[0].aliasTarget).to.equal(null);
expect(res.body.lists[0].domain).to.equal(DOMAIN_0.domain);
expect(res.body.lists[0].members).to.eql([ 'admin2', 'superadmin' ]);

View File

@@ -24,8 +24,6 @@ const EMAIL_0_NEW_FALLBACK = 'stupIDfallback@me.com';
const DISPLAY_NAME_0_NEW = 'New Name';
describe('Profile API', function () {
this.timeout(5000);
var user_0 = null;
var token_0;

View File

@@ -137,12 +137,12 @@ describe('REST API', function () {
});
});
it('dns setup twice fails', function (done) {
it('dns setup twice succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
.send({ dnsConfig: { provider: 'noop', domain: DOMAIN, DOMAIN, config: {} } })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(409);
expect(result.statusCode).to.eql(200);
done();
});
@@ -247,6 +247,17 @@ describe('REST API', function () {
});
});
it('dns setup after activation fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
.send({ dnsConfig: { provider: 'noop', domain: DOMAIN, DOMAIN, config: {} } })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(409);
done();
});
});
it('does not crash with invalid JSON', function (done) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })

View File

@@ -61,8 +61,6 @@ function cleanup(done) {
}
describe('SSH API', function () {
this.timeout(10000);
before(setup);
after(cleanup);

View File

@@ -65,29 +65,27 @@ function cleanup(done) {
}
describe('Internal API', function () {
this.timeout(5000);
before(setup);
after(cleanup);
describe('backup', function () {
it('succeeds', function (done) {
superagent.post(config.sysadminOrigin() + '/api/v1/backup')
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
function checkBackupStartEvent() {
eventlog.getAllPaged([ eventlog.ACTION_BACKUP_START ], '', 1, 100, function (error, result) {
expect(error).to.equal(null);
function checkBackupStartEvent() {
eventlog.getAllPaged([ eventlog.ACTION_BACKUP_START ], '', 1, 100, function (error, result) {
expect(error).to.equal(null);
if (result.length === 0) return setTimeout(checkBackupStartEvent, 1000);
if (result.length === 0) return setTimeout(checkBackupStartEvent, 1000);
done();
});
}
done();
});
}
checkBackupStartEvent();
});
checkBackupStartEvent();
});
});
});
});

View File

@@ -0,0 +1,138 @@
'use strict';
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
var async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
server = require('../../server.js'),
superagent = require('superagent'),
tasks = require('../../tasks.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
function setup(done) {
config._reset();
config.setFqdn('example-tasks-test.com');
config.setAdminFqdn('my.example-tasks-test.com');
async.series([
server.start.bind(null),
database._clear.bind(null),
function createAdmin(callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
// stash token for further use
token = result.body.token;
callback();
});
}
], done);
}
function cleanup(done) {
database._clear(function (error) {
expect(!error).to.be.ok();
server.stop(done);
});
}
describe('Tasks API', function () {
before(setup);
after(cleanup);
it('can get task', function (done) {
let taskId = null;
let task = tasks.startTask(tasks._TASK_IDENTITY, [ 'ping' ]);
task.on('error', done);
task.on('start', (tid) => { taskId = tid; });
task.on('finish', function () {
superagent.get(SERVER_URL + '/api/v1/tasks/' + taskId)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.percent).to.be(100);
expect(res.body.args).to.be(undefined);
expect(res.body.active).to.be(false); // finished
expect(res.body.result).to.be('ping');
expect(res.body.errorMessage).to.be(null);
done();
});
});
});
it('can get logs', function (done) {
let taskId = null;
let task = tasks.startTask(tasks._TASK_CRASH, [ 'ping' ]);
task.on('error', done);
task.on('start', (tid) => { taskId = tid; });
task.on('finish', function () {
superagent.get(SERVER_URL + '/api/v1/tasks/' + taskId + '/logs')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
done();
});
});
});
it('cannot stop inactive task', function (done) {
let taskId = null;
let task = tasks.startTask(tasks._TASK_IDENTITY, [ 'ping' ]);
task.on('error', done);
task.on('start', (tid) => { taskId = tid; });
task.on('finish', function () {
superagent.post(SERVER_URL + '/api/v1/tasks/' + taskId + '/stop')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
done();
});
});
});
it('can stop task', function (done) {
let taskId = null;
let task = tasks.startTask(tasks._TASK_SLEEP, [ 10000 ]);
task.on('error', done);
task.on('start', (tid) => {
taskId = tid;
superagent.post(SERVER_URL + '/api/v1/tasks/' + taskId + '/stop')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
});
});
task.on('finish', () => {
superagent.get(SERVER_URL + '/api/v1/tasks/' + taskId)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.percent).to.be(100);
expect(res.body.active).to.be(false); // finished
expect(res.body.result).to.be(null);
expect(res.body.errorMessage).to.contain('signal SIGTERM');
done();
});
});
});
});

View File

@@ -82,8 +82,6 @@ function checkMails(number, done) {
}
describe('Users API', function () {
this.timeout(5000);
var user_0, user_1, user_2, user_4;
var token = null, userToken = null;
var token_1 = tokendb.generateToken();

View File

@@ -9,17 +9,10 @@ if (process.argv[2] === '--check') return console.log('OK');
require('supererror')({ splatchError: true });
// remove timestamp from debug() based output
require('debug').formatArgs = function formatArgs(args) {
args[0] = this.namespace + ' ' + args[0];
};
var assert = require('assert'),
backups = require('./backups.js'),
database = require('./database.js'),
debug = require('debug')('box:backuptask'),
paths = require('./paths.js'),
safe = require('safetydance');
backups = require('../backups.js'),
database = require('../database.js'),
debug = require('debug')('box:backupupload');
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -38,17 +31,21 @@ process.on('SIGTERM', function () {
process.exit(0);
});
// this can happen when the backup task is terminated (not box code)
process.on('disconnect', function () {
debug('parent process died');
process.exit(0);
});
initialize(function (error) {
if (error) throw error;
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, '');
backups.upload(backupId, format, dataDir, function resultHandler(error) {
backups.upload(backupId, format, dataDir, (progress) => process.send(progress), function resultHandler(error) {
if (error) debug('upload completed with error', error);
debug('upload completed');
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, error ? error.message : '');
process.send({ result: error ? error.message : '' });
// https://nodejs.org/api/process.html are exit codes used by node. apps.js uses the value below
// to check apptask crashes

18
src/scripts/restartdocker.sh Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
if [[ $# == 1 && "$1" == "--check" ]]; then
echo "OK"
exit 0
fi
if [[ "${BOX_ENV}" == "cloudron" ]]; then
systemctl restart docker
fi

19
src/scripts/restartunbound.sh Executable file
View File

@@ -0,0 +1,19 @@
#!/bin/bash
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
if [[ $# == 1 && "$1" == "--check" ]]; then
echo "OK"
exit 0
fi
if [[ "${BOX_ENV}" == "cloudron" ]]; then
unbound-anchor -a /var/lib/unbound/root.key
systemctl restart unbound
fi

View File

@@ -30,21 +30,32 @@ if systemctl reset-failed "${UPDATER_SERVICE}"; then
echo "=> service has failed earlier"
fi
echo "=> Run installer.sh as cloudron-updater.service"
if ! systemd-run --unit "${UPDATER_SERVICE}" ${installer_path}; then
echo "Failed to install cloudron. See ${LOG_FILE} for details"
# StandardError will follow StandardOutput in default inherit mode. https://www.freedesktop.org/software/systemd/man/systemd.exec.html
echo "=> Run installer.sh as ${UPDATER_SERVICE}."
if [[ "$(systemd --version | head -n1)" != "systemd 22"* ]]; then
readonly DATETIME=`date '+%Y-%m-%d_%H-%M-%S'`
readonly LOG_FILE="/home/yellowtent/platformdata/logs/updater/cloudron-updater-${DATETIME}.log"
update_service_options="-p StandardOutput=file:${LOG_FILE}"
echo "=> starting service (ubuntu 18.04) ${UPDATER_SERVICE}. see logs at ${LOG_FILE}"
else
update_service_options=""
echo "=> starting service (ubuntu 16.04) ${UPDATER_SERVICE}. see logs using journalctl -u ${UPDATER_SERVICE}"
fi
if ! systemd-run --unit "${UPDATER_SERVICE}" $update_service_options ${installer_path}; then
echo "Failed to install cloudron. See log for details"
exit 1
fi
echo "=> service ${UPDATER_SERVICE} started."
echo "=> See logs with journalctl -u ${UPDATER_SERVICE} -f"
while true; do
if systemctl is-failed "${UPDATER_SERVICE}"; then
if systemctl is-failed "${UPDATER_SERVICE}" >/dev/null 2>&1; then
echo "=> ${UPDATER_SERVICE} has failed"
exit 1
fi
echo "${UPDATER_SERVICE} is still active. will check in 5 seconds"
sleep 5
# this loop will stop once the update process stopped the box unit and thus terminating this child process
done

View File

@@ -108,12 +108,11 @@ function initializeExpressSync() {
var csrf = routes.oauth2.csrf();
// public routes
router.post('/api/v1/cloudron/setup', routes.setup.providerTokenAuth, routes.setup.provision); // only available until no-domain
router.post('/api/v1/cloudron/restore', routes.setup.restore); // only available until activated
router.post('/api/v1/cloudron/activate', routes.setup.setupTokenAuth, routes.setup.activate);
router.post('/api/v1/cloudron/setup', routes.provision.providerTokenAuth, routes.provision.setup); // only available until no-domain
router.post('/api/v1/cloudron/restore', routes.provision.restore); // only available until activated
router.post('/api/v1/cloudron/activate', routes.provision.setupTokenAuth, routes.provision.activate);
router.get ('/api/v1/cloudron/status', routes.cloudron.getStatus);
router.get ('/api/v1/cloudron/progress', routes.cloudron.getProgress);
router.get ('/api/v1/cloudron/avatar', routes.settings.getCloudronAvatar); // this is a public alias for /api/v1/settings/cloudron_avatar
// developer routes
@@ -122,7 +121,10 @@ function initializeExpressSync() {
// cloudron routes
router.get ('/api/v1/cloudron/update', cloudronScope, routes.cloudron.getUpdateInfo);
router.post('/api/v1/cloudron/update', cloudronScope, routes.cloudron.update);
router.post('/api/v1/cloudron/set_dashboard_domain', cloudronScope, routes.cloudron.setDashboardDomain);
router.post('/api/v1/cloudron/renew_certs', cloudronScope, routes.cloudron.renewCerts);
router.post('/api/v1/cloudron/check_for_updates', cloudronScope, routes.cloudron.checkForUpdates);
router.get ('/api/v1/cloudron/reboot', cloudronScope, routes.cloudron.isRebootRequired);
router.post('/api/v1/cloudron/reboot', cloudronScope, routes.cloudron.reboot);
router.get ('/api/v1/cloudron/graphs', cloudronScope, routes.graphs.getGraphs);
router.get ('/api/v1/cloudron/disks', cloudronScope, routes.cloudron.getDisks);
@@ -134,6 +136,17 @@ function initializeExpressSync() {
router.del ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, isUnmanaged, routes.ssh.delAuthorizedKey);
router.get ('/api/v1/cloudron/eventlog', cloudronScope, routes.eventlog.get);
// tasks
router.get ('/api/v1/tasks', settingsScope, routes.tasks.list);
router.get ('/api/v1/tasks/:taskId', settingsScope, routes.tasks.get);
router.get ('/api/v1/tasks/:taskId/logs', cloudronScope, routes.tasks.getLogs);
router.get ('/api/v1/tasks/:taskId/logstream', cloudronScope, routes.tasks.getLogStream);
router.post('/api/v1/tasks/:taskId/stop', settingsScope, routes.tasks.stopTask);
// backups
router.get ('/api/v1/backups', settingsScope, routes.backups.list);
router.post('/api/v1/backups', settingsScope, routes.backups.startBackup);
// config route (for dashboard)
router.get ('/api/v1/config', profileScope, routes.cloudron.getConfig);
@@ -252,7 +265,7 @@ function initializeExpressSync() {
router.post('/api/v1/mail/:domain/enable', mailScope, routes.mail.setMailEnabled);
router.post('/api/v1/mail/:domain/dns', mailScope, routes.mail.setDnsRecords);
router.post('/api/v1/mail/:domain/send_test_mail', mailScope, routes.mail.sendTestMail);
router.get ('/api/v1/mail/:domain/mailboxes', mailScope, routes.mail.getMailboxes);
router.get ('/api/v1/mail/:domain/mailboxes', mailScope, routes.mail.listMailboxes);
router.get ('/api/v1/mail/:domain/mailboxes/:name', mailScope, routes.mail.getMailbox);
router.post('/api/v1/mail/:domain/mailboxes', mailScope, routes.mail.addMailbox);
router.post('/api/v1/mail/:domain/mailboxes/:name', mailScope, routes.mail.updateMailbox);
@@ -269,21 +282,20 @@ function initializeExpressSync() {
// feedback
router.post('/api/v1/feedback', cloudronScope, isUnmanaged, routes.cloudron.feedback);
// backup routes
router.get ('/api/v1/backups', settingsScope, routes.backups.get);
router.post('/api/v1/backups', settingsScope, routes.backups.create);
// domain routes
router.post('/api/v1/domains', domainsManageScope, routes.domains.add);
router.get ('/api/v1/domains', domainsReadScope, routes.domains.getAll);
router.get ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.domains.get); // this is manage scope because it returns non-restricted fields
router.put ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.domains.update);
router.post ('/api/v1/domains/:domain/renew_certs', domainsManageScope, verifyDomainLock, routes.domains.renewCerts);
router.del ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.users.verifyPassword, routes.domains.del);
// caas routes
router.get('/api/v1/caas/config', cloudronScope, routes.caas.getConfig);
router.post('/api/v1/caas/change_plan', cloudronScope, routes.users.verifyPassword, routes.caas.changePlan);
// addon routes
router.get ('/api/v1/services', cloudronScope, routes.services.getAll);
router.get ('/api/v1/services/:service', cloudronScope, routes.services.get);
router.post('/api/v1/services/:service', cloudronScope, routes.services.configure);
router.get ('/api/v1/services/:service/logs', cloudronScope, routes.services.getLogs);
router.get ('/api/v1/services/:service/logstream', cloudronScope, routes.services.getLogStream);
router.post('/api/v1/services/:service/restart', cloudronScope, routes.services.restart);
// disable server socket "idle" timeout. we use the timeout middleware to handle timeouts on a route level
// we rely on nginx for timeouts on the TCP level (see client_header_timeout)

View File

@@ -86,6 +86,7 @@ var gDefaults = (function () {
provider: 'filesystem',
key: '',
backupFolder: '/var/backups',
format: 'tgz',
retentionSecs: 2 * 24 * 60 * 60, // 2 days
intervalSecs: 24 * 60 * 60 // ~1 day
};
@@ -393,7 +394,7 @@ function setPlatformConfig(platformConfig, callback) {
settingsdb.set(exports.PLATFORM_CONFIG_KEY, JSON.stringify(platformConfig), function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
addons.updateAddonConfig(platformConfig, callback);
addons.updateServiceConfig(platformConfig, callback);
});
}

View File

@@ -1,10 +1,9 @@
'use strict';
exports = module.exports = {
spawn: spawn,
exec: exec,
execSync: execSync,
sudo: sudo,
sudoSync: sudoSync
sudo: sudo
};
var assert = require('assert'),
@@ -15,21 +14,22 @@ var assert = require('assert'),
var SUDO = '/usr/bin/sudo';
function execSync(tag, cmd, callback) {
function exec(tag, cmd, callback) {
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof cmd, 'string');
assert.strictEqual(typeof callback, 'function');
debug(cmd);
try {
child_process.execSync(cmd, { stdio: 'inherit' });
} catch (e) {
if (callback) return callback(e);
throw e;
}
if (callback) callback();
debug(`${tag} exec: ${cmd}`);
child_process.exec(cmd, function (error, stdout, stderr) {
debug(`${tag} (stdout): %s`, stdout.toString('utf8'));
debug(`${tag} (stderr): %s`, stderr.toString('utf8'));
callback(error);
});
}
function exec(tag, file, args, options, callback) {
function spawn(tag, file, args, options, callback) {
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof file, 'string');
assert(util.isArray(args));
@@ -38,7 +38,9 @@ function exec(tag, file, args, options, callback) {
callback = once(callback); // exit may or may not be called after an 'error'
debug(tag + ' execFile: %s %s', file, args.join(' '));
debug(tag + ' spawn: %s %s', file, args.join(' '));
if (options.ipc) options.stdio = ['pipe', 'pipe', 'pipe', 'ipc'];
var cp = child_process.spawn(file, args, options);
if (options.logStream) {
@@ -50,7 +52,7 @@ function exec(tag, file, args, options, callback) {
});
cp.stderr.on('data', function (data) {
debug(tag + ' (stderr): %s', data.toString('utf8'));
debug(tag + ' (stdout): %s', data.toString('utf8'));
});
}
@@ -75,33 +77,14 @@ function exec(tag, file, args, options, callback) {
function sudo(tag, args, options, callback) {
assert.strictEqual(typeof tag, 'string');
assert(util.isArray(args));
if (typeof options === 'function') {
callback = options;
options = { };
}
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
// -S makes sudo read stdin for password. -E preserves environment
var cp = exec(tag, SUDO, [ options.env ? '-SE' : '-S' ].concat(args), options, callback);
let sudoArgs = [ '-S' ]; // -S makes sudo read stdin for password
if (options.preserveEnv) sudoArgs.push('-E'); // -E preserves environment
if (options.ipc) sudoArgs.push('--close-from=4'); // keep the ipc open. requires closefrom_override in sudoers file
var cp = spawn(tag, SUDO, sudoArgs.concat(args), options, callback);
cp.stdin.end();
return cp;
}
function sudoSync(tag, cmd, callback) {
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof cmd, 'string');
// -S makes sudo read stdin for password
cmd = 'sudo -S ' + cmd;
debug(cmd);
try {
child_process.execSync(cmd, { stdio: 'inherit' });
} catch (e) {
if (callback) return callback(e);
throw e;
}
if (callback) callback();
}

View File

@@ -14,7 +14,6 @@ exports = module.exports = {
var assert = require('assert'),
config = require('./config.js'),
debug = require('debug')('box:ssh'),
fs = require('fs'),
path = require('path'),
safe = require('safetydance'),
shell = require('./shell.js'),
@@ -56,61 +55,72 @@ function clear(callback) {
callback();
}
function saveKeys(keys) {
function saveKeys(keys, callback) {
assert(Array.isArray(keys));
assert.strictEqual(typeof callback, 'function');
if (!safe.fs.writeFileSync(AUTHORIZED_KEYS_TMP_FILEPATH, keys.map(function (k) { return k.key; }).join('\n'))) {
debug('Error writing to temporary file', safe.error);
return false;
return callback(safe.error);
}
try {
// 600 = rw-------
fs.chmodSync(AUTHORIZED_KEYS_TMP_FILEPATH, '600');
} catch (e) {
debug('Failed to adjust permissions of %s %j', AUTHORIZED_KEYS_TMP_FILEPATH, e);
return false;
if (!safe.fs.chmodSync(AUTHORIZED_KEYS_TMP_FILEPATH, '600')) { // 600 = rw-------
debug('Failed to adjust permissions of %s %s', AUTHORIZED_KEYS_TMP_FILEPATH, safe.error);
return callback(safe.error);
}
var user = config.TEST ? process.env.USER : ((config.provider() === 'ec2' || config.provider() === 'lightsail' || config.provider() === 'ami') ? 'ubuntu' : 'root');
shell.sudoSync('authorized_keys', util.format('%s %s %s %s', AUTHORIZED_KEYS_CMD, user, AUTHORIZED_KEYS_TMP_FILEPATH, AUTHORIZED_KEYS_FILEPATH));
shell.sudo('authorized_keys', [ AUTHORIZED_KEYS_CMD, user, AUTHORIZED_KEYS_TMP_FILEPATH, AUTHORIZED_KEYS_FILEPATH ], {}, function (error) {
if (error) return callback(error);
return true;
callback(null);
});
}
function getKeys() {
shell.sudoSync('authorized_keys', util.format('%s %s %s %s', AUTHORIZED_KEYS_CMD, process.env.USER, AUTHORIZED_KEYS_FILEPATH, AUTHORIZED_KEYS_TMP_FILEPATH));
function getKeys(callback) {
assert.strictEqual(typeof callback, 'function');
var content = safe.fs.readFileSync(AUTHORIZED_KEYS_TMP_FILEPATH, 'utf8');
if (!content) return [];
shell.sudo('authorized_keys', [ AUTHORIZED_KEYS_CMD, process.env.USER, AUTHORIZED_KEYS_FILEPATH, AUTHORIZED_KEYS_TMP_FILEPATH ], {}, function (error) {
if (error) return callback(error);
var keys = content.split('\n')
.filter(function (k) { return !!k.trim(); })
.map(function (k) { return { identifier: k.split(' ')[2], key: k }; })
.filter(function (k) { return k.identifier && k.key; });
var content = safe.fs.readFileSync(AUTHORIZED_KEYS_TMP_FILEPATH, 'utf8');
if (!content) return callback(null, []);
safe.fs.unlinkSync(AUTHORIZED_KEYS_TMP_FILEPATH);
var keys = content.split('\n')
.filter(function (k) { return !!k.trim(); })
.map(function (k) { return { identifier: k.split(' ')[2], key: k }; })
.filter(function (k) { return k.identifier && k.key; });
return keys;
safe.fs.unlinkSync(AUTHORIZED_KEYS_TMP_FILEPATH);
return callback(null, keys);
});
}
function getAuthorizedKeys(callback) {
assert.strictEqual(typeof callback, 'function');
return callback(null, getKeys().sort(function (a, b) { return a.identifier.localeCompare(b.identifier); }));
getKeys(function (error, keys) {
if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error));
return callback(null, keys.sort(function (a, b) { return a.identifier.localeCompare(b.identifier); }));
});
}
function getAuthorizedKey(identifier, callback) {
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof callback, 'function');
var keys = getKeys();
if (keys.length === 0) return callback(new SshError(SshError.NOT_FOUND));
getKeys(function (error, keys) {
if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error));
var key = keys.find(function (k) { return k.identifier === identifier; });
if (!key) return callback(new SshError(SshError.NOT_FOUND));
if (keys.length === 0) return callback(new SshError(SshError.NOT_FOUND));
callback(null, key);
var key = keys.find(function (k) { return k.identifier === identifier; });
if (!key) return callback(new SshError(SshError.NOT_FOUND));
callback(null, key);
});
}
function addAuthorizedKey(key, callback) {
@@ -124,28 +134,38 @@ function addAuthorizedKey(key, callback) {
var identifier = tmp[2];
var keys = getKeys();
var index = keys.findIndex(function (k) { return k.identifier === identifier; });
if (index !== -1) keys[index] = { identifier: identifier, key: key };
else keys.push({ identifier: identifier, key: key });
getKeys(function (error, keys) {
if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error));
if (!saveKeys(keys)) return callback(new SshError(SshError.INTERNAL_ERROR));
var index = keys.findIndex(function (k) { return k.identifier === identifier; });
if (index !== -1) keys[index] = { identifier: identifier, key: key };
else keys.push({ identifier: identifier, key: key });
callback();
saveKeys(keys, function (error) {
if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error));
callback(null);
});
});
}
function delAuthorizedKey(identifier, callback) {
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof callback, 'function');
var keys = getKeys();
var index = keys.findIndex(function (k) { return k.identifier === identifier; });
if (index === -1) return callback(new SshError(SshError.NOT_FOUND));
getKeys(function (error, keys) {
if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error));
// now remove the key
keys.splice(index, 1);
let index = keys.findIndex(function (k) { return k.identifier === identifier; });
if (index === -1) return callback(new SshError(SshError.NOT_FOUND));
if (!saveKeys(keys)) return callback(new SshError(SshError.INTERNAL_ERROR));
// now remove the key
keys.splice(index, 1);
callback();
saveKeys(keys, function (error) {
if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error));
callback(null);
});
});
}

View File

@@ -120,7 +120,7 @@ function copy(apiConfig, oldFilePath, newFilePath) {
// this will hardlink backups saving space
var cpOptions = apiConfig.noHardlinks ? '-a' : '-al';
shell.exec('copy', '/bin/cp', [ cpOptions, oldFilePath, newFilePath ], { }, function (error) {
shell.spawn('copy', '/bin/cp', [ cpOptions, oldFilePath, newFilePath ], { }, function (error) {
if (error) return events.emit('done', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
events.emit('done', null);
@@ -155,7 +155,7 @@ function removeDir(apiConfig, pathPrefix) {
events.emit('progress', `removeDir: ${pathPrefix}`);
shell.exec('removeDir', '/bin/rm', [ '-rf', pathPrefix ], { }, function (error) {
shell.spawn('removeDir', '/bin/rm', [ '-rf', pathPrefix ], { }, function (error) {
if (error) return events.emit('done', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
events.emit('done', null);

115
src/taskdb.js Normal file
View File

@@ -0,0 +1,115 @@
'use strict';
exports = module.exports = {
get: get,
add: add,
update: update,
del: del,
listByTypePaged: listByTypePaged
};
let assert = require('assert'),
database = require('./database.js'),
DatabaseError = require('./databaseerror'),
safe = require('safetydance');
const TASKS_FIELDS = [ 'id', 'type', 'argsJson', 'percent', 'message', 'errorMessage', 'creationTime', 'result', 'ts' ];
function postProcess(result) {
assert.strictEqual(typeof result, 'object');
assert(result.argsJson === null || typeof result.argsJson === 'string');
result.args = safe.JSON.parse(result.argsJson) || [];
delete result.argsJson;
result.id = String(result.id);
}
function add(task, callback) {
assert.strictEqual(typeof task, 'object');
assert.strictEqual(typeof callback, 'function');
const query = 'INSERT INTO tasks (type, argsJson, percent, message) VALUES (?, ?, ?, ?)';
const args = [ task.type, JSON.stringify(task.args), task.percent, task.message ];
database.query(query, args, function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, String(result.insertId));
});
}
function update(id, data, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
let args = [ ];
let fields = [ ];
for (let k in data) {
fields.push(k + ' = ?');
args.push(data[k]);
}
args.push(id);
database.query('UPDATE tasks SET ' + fields.join(', ') + ' WHERE id = ?', args, function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
return callback(null);
});
}
function get(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + TASKS_FIELDS + ' FROM tasks WHERE 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));
postProcess(result[0]);
callback(null, result[0]);
});
}
function del(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM tasks WHERE id = ?', [ id ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null);
});
}
function listByTypePaged(type, page, perPage, callback) {
assert(typeof type === 'string' || type === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
var data = [];
var query = 'SELECT ' + TASKS_FIELDS + ' FROM tasks';
if (type) {
query += ' WHERE TYPE=?';
data.push(type);
}
query += ' ORDER BY creationTime DESC LIMIT ?,?';
data.push((page-1)*perPage);
data.push(perPage);
database.query(query, data, function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(postProcess);
callback(null, results);
});
}

223
src/tasks.js Normal file
View File

@@ -0,0 +1,223 @@
'use strict';
exports = module.exports = {
get: get,
update: update,
listByTypePaged: listByTypePaged,
getLogs: getLogs,
startTask: startTask,
stopTask: stopTask,
removePrivateFields: removePrivateFields,
TaskError: TaskError,
// task types. if you add a task here, fill up the function table in taskworker
TASK_BACKUP: 'backup',
TASK_UPDATE: 'update',
TASK_RENEW_CERTS: 'renewcerts',
// testing
_TASK_IDENTITY: '_identity',
_TASK_CRASH: '_crash',
_TASK_ERROR: '_error',
_TASK_SLEEP: '_sleep'
};
let assert = require('assert'),
child_process = require('child_process'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:tasks'),
EventEmitter = require('events'),
paths = require('./paths.js'),
safe = require('safetydance'),
spawn = require('child_process').spawn,
split = require('split'),
taskdb = require('./taskdb.js'),
util = require('util'),
_ = require('underscore');
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
let gTasks = {}; // indexed by task id
function TaskError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(TaskError, Error);
TaskError.INTERNAL_ERROR = 'Internal Error';
TaskError.BAD_STATE = 'Bad State';
TaskError.NOT_FOUND = 'Not Found';
function get(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
taskdb.get(id, function (error, task) {
if (error && error.reason == DatabaseError.NOT_FOUND) return callback(new TaskError(TaskError.NOT_FOUND));
if (error) return callback(new TaskError(TaskError.INTERNAL_ERROR, error));
task.active = !!gTasks[id];
callback(null, task);
});
}
function update(id, task, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof task, 'object');
assert.strictEqual(typeof callback, 'function');
debug(`${id}: ${JSON.stringify(task)}`);
taskdb.update(id, task, function (error) {
if (error && error.reason == DatabaseError.NOT_FOUND) return callback(new TaskError(TaskError.NOT_FOUND));
if (error) return callback(new TaskError(TaskError.INTERNAL_ERROR, error));
callback();
});
}
function startTask(type, args) {
assert.strictEqual(typeof type, 'string');
assert(Array.isArray(args));
let events = new EventEmitter();
taskdb.add({ type: type, percent: 0, message: 'Starting', args: args }, function (error, taskId) {
if (error) return events.emit('error', new TaskError(TaskError.INTERNAL_ERROR, error));
const logFile = `${paths.TASKS_LOG_DIR}/${taskId}.log`;
let fd = safe.fs.openSync(logFile, 'a'); // will autoclose
if (!fd) {
debug(`startTask: unable to get log filedescriptor ${safe.error.message}`);
return events.emit('error', new TaskError(TaskError.INTERNAL_ERROR, error.message));
}
debug(`startTask - starting task ${type}. logs at ${logFile} id ${taskId}`);
gTasks[taskId] = child_process.fork(`${__dirname}/taskworker.js`, [ taskId ], { stdio: [ 'pipe', fd, fd, 'ipc' ]}); // fork requires ipc
gTasks[taskId].once('exit', function (code, signal) {
debug(`startTask: ${taskId} completed with code ${code} and signal ${signal}`);
get(taskId, function (error, task) {
if (!error && task.percent !== 100) { // task crashed or was killed by us (code 50)
error = code === 0 ? new Error(`${taskId} task stopped`) : new Error(`${taskId} task crashed with code ${code} and signal ${signal}`);
update(taskId, { percent: 100, errorMessage: error.message }, NOOP_CALLBACK);
} else if (!error && task.errorMessage) {
error = new Error(task.errorMessage);
} else if (!task) { // db got cleared in tests
error = new Error(`No such task ${taskId}`);
}
gTasks[taskId] = null;
events.emit('finish', error, task ? task.result : null);
debug(`startTask: ${taskId} done`);
});
});
events.emit('start', taskId);
});
return events;
}
function stopTask(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
if (!gTasks[id]) return callback(new TaskError(TaskError.BAD_STATE, 'task is not active'));
debug(`stopTask: stopping task ${id}`);
gTasks[id].kill('SIGTERM'); // this will end up calling the 'exit' signal handler
callback(null);
}
function listByTypePaged(type, page, perPage, callback) {
assert(typeof type === 'string' || type === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
taskdb.listByTypePaged(type, page, perPage, function (error, tasks) {
if (error) return callback(new TaskError(TaskError.INTERNAL_ERROR, error));
tasks.forEach((task) => { task.active = !!gTasks[task.id]; });
callback(null, tasks);
});
}
function getLogs(taskId, options, callback) {
assert.strictEqual(typeof taskId, 'string');
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
debug(`Getting logs for ${taskId}`);
var lines = options.lines || 100,
format = options.format || 'json',
follow = !!options.follow;
assert.strictEqual(typeof lines, 'number');
assert.strictEqual(typeof format, 'string');
let cmd = '/usr/bin/tail';
var args = [ '--lines=' + lines ];
if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs
args.push(`${paths.TASKS_LOG_DIR}/${taskId}.log`);
var cp = spawn(cmd, args);
var transformStream = split(function mapper(line) {
if (format !== 'json') return line + '\n';
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
var timestamp = (new Date(data[0])).getTime();
if (isNaN(timestamp)) timestamp = 0;
var message = line.slice(data[0].length+1);
// ignore faulty empty logs
if (!timestamp && !message) return;
return JSON.stringify({
realtimeTimestamp: timestamp * 1000,
message: message,
source: taskId
}) + '\n';
});
transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process
cp.stdout.pipe(transformStream);
callback(null, transformStream);
}
// removes all fields that are strictly private and should never be returned by API calls
function removePrivateFields(task) {
var result = _.pick(task, 'id', 'type', 'percent', 'message', 'errorMessage', 'active', 'creationTime', 'result', 'ts');
return result;
}

51
src/taskworker.js Executable file
View File

@@ -0,0 +1,51 @@
'use strict';
require('supererror')({ splatchError: true });
var assert = require('assert'),
backups = require('./backups.js'),
database = require('./database.js'),
debug = require('debug')('box:taskworker'),
reverseProxy = require('./reverseproxy.js'),
tasks = require('./tasks.js'),
updater = require('./updater.js');
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
const TASKS = { // indexed by task type
backup: backups.backupBoxAndApps,
update: updater.update,
renewcerts: reverseProxy.renewCerts,
_identity: (arg, progressCallback, callback) => callback(null, arg),
_error: (arg, progressCallback, callback) => callback(new Error(`Failed for arg: ${arg}`)),
_crash: (arg) => { throw new Error(`Crashing for arg: ${arg}`); },
_sleep: (arg) => setTimeout(process.exit, arg)
};
process.on('SIGTERM', function () {
process.exit(0);
});
assert.strictEqual(process.argv.length, 3, 'Pass the taskid as argument');
const taskId = process.argv[2];
// Main process starts here
debug(`Staring task ${taskId}`);
database.initialize(function (error) {
if (error) return process.exit(50);
tasks.get(taskId, function (error, task) {
if (error) return process.exit(50);
const progressCallback = (progress) => tasks.update(taskId, progress, NOOP_CALLBACK);
const resultCallback = (error, result) => {
const progress = { percent: 100, result: result || null, errorMessage: error ? error.message : null };
tasks.update(taskId, progress, () => process.exit(error ? 50 : 0));
};
TASKS[task.type].apply(null, task.args.concat(progressCallback).concat(resultCallback));
});
});

View File

@@ -16,73 +16,31 @@ var async = require('async'),
fs = require('fs'),
os = require('os'),
mkdirp = require('mkdirp'),
readdirp = require('readdirp'),
path = require('path'),
progress = require('../progress.js'),
rimraf = require('rimraf'),
settings = require('../settings.js'),
SettingsError = require('../settings.js').SettingsError;
function compareDirectories(one, two, callback) {
readdirp({ root: one }, function (error, treeOne) {
if (error) return callback(error);
readdirp({ root: two }, function (error, treeTwo) {
if (error) return callback(error);
var mismatch = [];
function compareDirs(a, b) {
a.forEach(function (tmpA) {
var found = b.find(function (tmpB) {
return tmpA.path === tmpB.path;
});
if (!found) mismatch.push(tmpA);
});
}
function compareFiles(a, b) {
a.forEach(function (tmpA) {
var found = b.find(function (tmpB) {
// TODO check file or symbolic link
return tmpA.path === tmpB.path && tmpA.mode === tmpB.mode;
});
if (!found) mismatch.push(tmpA);
});
}
compareDirs(treeOne.directories, treeTwo.directories);
compareDirs(treeTwo.directories, treeOne.directories);
compareFiles(treeOne.files, treeTwo.files);
compareFiles(treeTwo.files, treeOne.files);
if (mismatch.length) {
console.error('Files not found in both: %j', mismatch);
return callback(new Error('file mismatch'));
}
callback(null);
});
});
}
SettingsError = require('../settings.js').SettingsError,
tasks = require('../tasks.js');
function createBackup(callback) {
backups.backup({ username: 'test' }, function (error) { // this call does not wait for the backup!
backups.startBackupTask({ username: 'test' }, function (error, taskId) { // this call does not wait for the backup!
if (error) return callback(error);
function waitForBackup() {
var p = progress.getAll();
if (p.backup.percent !== 100) return setTimeout(waitForBackup, 1000);
if (p.backup.message) return callback(new Error('backup failed:' + p.backup.message));
backups.getByStatePaged(backupdb.BACKUP_STATE_NORMAL, 1, 1, function (error, result) {
tasks.get(taskId, function (error, p) {
if (error) return callback(error);
if (result.length !== 1) return callback(new Error('result is not of length 1'));
callback(null, result[0]);
if (p.percent !== 100) return setTimeout(waitForBackup, 1000);
if (p.errorMessage) return callback(new Error('backup failed:' + p));
if (!p.result) return callback(new Error('backup has no result:' + p));
backups.getByStatePaged(backupdb.BACKUP_STATE_NORMAL, 1, 1, function (error, result) {
if (error) return callback(error);
if (result.length !== 1) return callback(new Error('result is not of length 1'));
callback(null, result[0]);
});
});
}
@@ -115,8 +73,6 @@ describe('backups', function () {
});
describe('cleanup', function () {
this.timeout(20000);
var BACKUP_0 = {
id: 'backup-box-0',
version: '1.0.0',
@@ -305,7 +261,6 @@ describe('backups', function () {
after(function (done) {
rimraf.sync(gBackupConfig.backupFolder);
progress.clear(progress.BACKUP);
done();
});
@@ -329,8 +284,6 @@ describe('backups', function () {
});
it('can backup', function (done) {
this.timeout(6000);
createBackup(function (error, result) {
expect(error).to.be(null);
expect(fs.statSync(path.join(gBackupConfig.backupFolder, 'snapshot/box.tar.gz')).nlink).to.be(2); // hard linked to a rotated backup
@@ -343,8 +296,6 @@ describe('backups', function () {
});
it('can take another backup', function (done) {
this.timeout(6000);
createBackup(function (error, result) {
expect(error).to.be(null);
expect(fs.statSync(path.join(gBackupConfig.backupFolder, 'snapshot/box.tar.gz')).nlink).to.be(2); // hard linked to a rotated backup

View File

@@ -10,14 +10,17 @@ sudo -k || sudo --reset-timestamp
# checks if all scripts are sudo access
scripts=("${SOURCE_DIR}/src/scripts/rmvolume.sh" \
"${SOURCE_DIR}/src/scripts/rmaddon.sh" \
"${SOURCE_DIR}/src/scripts/rmaddondir.sh" \
"${SOURCE_DIR}/src/scripts/reloadnginx.sh" \
"${SOURCE_DIR}/src/scripts/reboot.sh" \
"${SOURCE_DIR}/src/scripts/restart.sh" \
"${SOURCE_DIR}/src/scripts/restartdocker.sh" \
"${SOURCE_DIR}/src/scripts/restartunbound.sh" \
"${SOURCE_DIR}/src/scripts/update.sh" \
"${SOURCE_DIR}/src/scripts/collectlogs.sh" \
"${SOURCE_DIR}/src/scripts/configurecollectd.sh" \
"${SOURCE_DIR}/src/scripts/authorized_keys.sh" \
"${SOURCE_DIR}/src/backuptask.js" \
"${SOURCE_DIR}/src/scripts/backupupload.js" \
"${SOURCE_DIR}/src/scripts/configurelogrotate.sh")
for script in "${scripts[@]}"; do

View File

@@ -21,6 +21,7 @@ var appdb = require('../appdb.js'),
mailboxdb = require('../mailboxdb.js'),
maildb = require('../maildb.js'),
settingsdb = require('../settingsdb.js'),
taskdb = require('../taskdb.js'),
tokendb = require('../tokendb.js'),
userdb = require('../userdb.js'),
_ = require('underscore');
@@ -98,8 +99,6 @@ const TEST_DOMAIN = {
};
describe('database', function () {
this.timeout(5000);
before(function (done) {
config._reset();
config.setFqdn(TEST_DOMAIN.domain);
@@ -234,7 +233,9 @@ describe('database', function () {
robotsTxt: null,
enableBackup: true,
ownerId: USER_0.id,
env: {}
env: {},
mailboxName: 'talktome',
enableAutomaticUpdate: true
};
it('cannot delete referenced domain', function (done) {
@@ -755,7 +756,9 @@ describe('database', function () {
alternateDomains: [],
env: {
'CUSTOM_KEY': 'CUSTOM_VALUE'
}
},
mailboxName: 'talktome',
enableAutomaticUpdate: true
};
var APP_1 = {
@@ -783,7 +786,9 @@ describe('database', function () {
enableBackup: true,
ownerId: USER_0.id,
alternateDomains: [],
env: {}
env: {},
mailboxName: 'callme',
enableAutomaticUpdate: true
};
before(function (done) {
@@ -871,6 +876,7 @@ describe('database', function () {
var data = {
installationState: APP_0.installationState,
location: APP_0.location,
domain: APP_0.domain,
manifest: APP_0.manifest,
accessRestriction: APP_0.accessRestriction,
httpPort: APP_0.httpPort,
@@ -1094,6 +1100,82 @@ describe('database', function () {
});
});
describe('tasks', function () {
let taskId;
let TASK = {
type: 'tasktype',
args: { x: 1 },
percent: 0,
message: 'starting task'
};
it('add succeeds', function (done) {
taskdb.add(TASK, function (error, id) {
expect(error).to.be(null);
expect(id).to.be.ok();
taskId = id;
done();
});
});
it('get succeeds', function (done) {
taskdb.get(taskId, function (error, task) {
expect(error).to.be(null);
expect(_.pick(task, Object.keys(TASK))).to.eql(TASK);
done();
});
});
it('update succeeds', function (done) {
TASK.percent = 34;
TASK.message = 'almost ther';
taskdb.update(taskId, { percent: TASK.percent, message: TASK.message }, function (error) {
expect(error).to.be(null);
taskdb.get(taskId, function (error, task) {
expect(_.pick(task, Object.keys(TASK))).to.eql(TASK);
done();
});
});
});
it('list succeeds - does not exist', function (done) {
taskdb.listByTypePaged('randomtask', 1, 1, function (error, tasks) {
expect(error).to.be(null);
expect(tasks.length).to.be(0);
done();
});
});
it('list succeeds - by type', function (done) {
taskdb.listByTypePaged(TASK.type, 1, 1, function (error, tasks) {
expect(error).to.be(null);
expect(tasks.length).to.be(1);
expect(_.pick(tasks[0], Object.keys(TASK))).to.eql(TASK);
done();
});
});
it('list succeeds - all', function (done) {
taskdb.listByTypePaged(null, 1, 1, function (error, tasks) {
expect(error).to.be(null);
expect(tasks.length).to.be(1);
expect(_.pick(tasks[0], Object.keys(TASK))).to.eql(TASK);
done();
});
});
it('del succeeds', function (done) {
taskdb.del(taskId, function (error) {
expect(error).to.be(null);
taskdb.get(taskId, function (error) {
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
done();
});
});
});
});
describe('client', function () {
var CLIENT_0 = {
id: 'cid-0',
@@ -1634,21 +1716,21 @@ describe('database', function () {
});
it('add user mailbox succeeds', function (done) {
mailboxdb.addMailbox('girish', DOMAIN_0.domain, 'uid-0', mailboxdb.OWNER_TYPE_USER, function (error) {
mailboxdb.addMailbox('girish', DOMAIN_0.domain, 'uid-0', function (error) {
expect(error).to.be(null);
done();
});
});
it('cannot add dup entry', function (done) {
mailboxdb.addMailbox('girish', DOMAIN_0.domain, 'uid-1', mailboxdb.OWNER_TYPE_APP, function (error) {
mailboxdb.addMailbox('girish', DOMAIN_0.domain, 'uid-1', function (error) {
expect(error.reason).to.be(DatabaseError.ALREADY_EXISTS);
done();
});
});
it('add app mailbox succeeds', function (done) {
mailboxdb.addMailbox('support', DOMAIN_0.domain, 'osticket', mailboxdb.OWNER_TYPE_APP, function (error) {
mailboxdb.addMailbox('support', DOMAIN_0.domain, 'osticket', function (error) {
expect(error).to.be(null);
done();
});
@@ -1671,7 +1753,6 @@ describe('database', function () {
expect(error).to.be(null);
expect(mailboxes.length).to.be(2);
expect(mailboxes[0].name).to.be('girish');
expect(mailboxes[0].ownerType).to.be(mailboxdb.OWNER_TYPE_USER);
expect(mailboxes[1].name).to.be('support');
done();
@@ -1817,7 +1898,7 @@ describe('database', function () {
});
it('can get all domains', function (done) {
maildb.getAll(function (error, result) {
maildb.list(function (error, result) {
expect(error).to.equal(null);
expect(result).to.be.an(Array);
expect(result[0]).to.be.an('object');

View File

@@ -42,7 +42,6 @@ describe('Dockerproxy', function () {
});
// uncomment this to run the proxy for manual testing
// this.timeout(1000000);
// it('wait', function (done) {} );
it('can get info', function (done) {

View File

@@ -79,7 +79,8 @@ var APP_0 = {
restoreConfig: null,
oldConfig: null,
memoryLimit: 4294967296,
ownerId: null
ownerId: null,
mailboxName: 'some-location-0.app'
};
var dockerProxy;
@@ -112,7 +113,7 @@ function setup(done) {
appdb.update.bind(null, APP_0.id, { containerId: APP_0.containerId }),
appdb.setAddonConfig.bind(null, APP_0.id, 'sendmail', [{ name: 'MAIL_SMTP_PASSWORD', value : 'sendmailpassword' }]),
appdb.setAddonConfig.bind(null, APP_0.id, 'recvmail', [{ name: 'MAIL_IMAP_PASSWORD', value : 'recvmailpassword' }]),
mailboxdb.addMailbox.bind(null, APP_0.location + '.app', APP_0.domain, APP_0.id, mailboxdb.OWNER_TYPE_APP),
mailboxdb.addMailbox.bind(null, APP_0.location + '.app', APP_0.domain, APP_0.id),
function (callback) {
users.create(USER_1.username, USER_1.password, USER_1.email, USER_0.displayName, { invitor: USER_0 }, AUDIT_SOURCE, function (error, result) {
@@ -202,8 +203,6 @@ function cleanup(done) {
}
describe('Ldap', function () {
this.timeout(10000);
before(setup);
after(cleanup);
@@ -814,7 +813,7 @@ describe('Ldap', function () {
describe('search mailbox', function () {
before(function (done) {
mailboxdb.addMailbox(USER_0.username.toLowerCase(), DOMAIN_0.domain, USER_0.id, mailboxdb.OWNER_TYPE_USER, done);
mailboxdb.addMailbox(USER_0.username.toLowerCase(), DOMAIN_0.domain, USER_0.id, done);
});
it('get specific mailbox by email', function (done) {
@@ -925,7 +924,7 @@ describe('Ldap', function () {
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
client.bind('cn=' + USER_0.username + '@example.com,ou=sendmail,dc=cloudron', USER_0.password + 'nope', function (error) {
expect(error).to.be.a(ldap.NoSuchObjectError);
expect(error).to.be.a(ldap.InvalidCredentialsError);
client.unbind(done);
});
});
@@ -993,7 +992,7 @@ describe('Ldap', function () {
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
client.bind('cn=' + APP_0.location + '.app@example.com,ou=sendmail,dc=cloudron', 'nope', function (error) {
expect(error).to.be.a(ldap.InvalidCredentialsError);
expect(error).to.be.a(ldap.NoSuchObjectError);
client.unbind(done);
});
});
@@ -1084,7 +1083,7 @@ describe('Ldap', function () {
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
client.bind('cn=' + APP_0.location + '.app@example.com,ou=recvmail,dc=cloudron', 'nope', function (error) {
expect(error).to.be.a(ldap.InvalidCredentialsError);
expect(error).to.be.a(ldap.NoSuchObjectError);
client.unbind(done);
});
});

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