Compare commits
447 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cbe60a484 | ||
|
|
ded9a6e377 | ||
|
|
ea205363a0 | ||
|
|
ad13445c93 | ||
|
|
eb5c2ed30b | ||
|
|
bd3080a6b3 | ||
|
|
be5290c5ca | ||
|
|
43fd207164 | ||
|
|
34c53694a0 | ||
|
|
927f8483ce | ||
|
|
a19205e3ad | ||
|
|
49e5c60422 | ||
|
|
57b623ee44 | ||
|
|
0c904af927 | ||
|
|
9cd025972c | ||
|
|
21111eccc4 | ||
|
|
917079f341 | ||
|
|
4d6d768be1 | ||
|
|
c54cd992ca | ||
|
|
d5ec599dd1 | ||
|
|
0542ab16d4 | ||
|
|
7e75ef7685 | ||
|
|
f296265461 | ||
|
|
fb4eade215 | ||
|
|
8b3e85907c | ||
|
|
ca4876649d | ||
|
|
7ebc2abe5d | ||
|
|
37e132319b | ||
|
|
b2728118e9 | ||
|
|
c428f649aa | ||
|
|
7baf979a59 | ||
|
|
ccecaca047 | ||
|
|
c7ee684f25 | ||
|
|
52156c9a35 | ||
|
|
4fba216af9 | ||
|
|
1d00c788d1 | ||
|
|
d891d39587 | ||
|
|
cfde6e31ad | ||
|
|
243772d1f5 | ||
|
|
1c36b8eaf7 | ||
|
|
120fa4924a | ||
|
|
c3c9c2f39a | ||
|
|
fc90829ba2 | ||
|
|
ce9224c690 | ||
|
|
18a2107247 | ||
|
|
f13d05dad7 | ||
|
|
86586444a9 | ||
|
|
4e47d0595d | ||
|
|
45e85e4d53 | ||
|
|
a3420f885d | ||
|
|
a266fe13d0 | ||
|
|
44aba5d6e1 | ||
|
|
3fe5307ae3 | ||
|
|
d03fb0e71f | ||
|
|
d9723b72e4 | ||
|
|
6ba61f1bda | ||
|
|
d1df647ddd | ||
|
|
95c4a1f90c | ||
|
|
e00325e694 | ||
|
|
85c13cae58 | ||
|
|
00fd9e5b7f | ||
|
|
dde81ee847 | ||
|
|
c46fc96500 | ||
|
|
1914a9a703 | ||
|
|
1a061e4446 | ||
|
|
29ce80cebe | ||
|
|
4b6ac538ac | ||
|
|
70b9000b0e | ||
|
|
24dcb1b79c | ||
|
|
384915883f | ||
|
|
4cfc75f1d1 | ||
|
|
c49cbb524d | ||
|
|
b401c3d930 | ||
|
|
890a7cfb37 | ||
|
|
70a1ef1af3 | ||
|
|
38a0cdc0be | ||
|
|
93344a5a4a | ||
|
|
9f792fc04b | ||
|
|
7cb95faacb | ||
|
|
bf122f0f56 | ||
|
|
78e9446a05 | ||
|
|
138e1595fa | ||
|
|
37b02ad36a | ||
|
|
02f0055594 | ||
|
|
ec1f0f9320 | ||
|
|
bfe6389f62 | ||
|
|
30db3e8973 | ||
|
|
5b67f2cf29 | ||
|
|
a007b74b1c | ||
|
|
a89482d4fa | ||
|
|
0cd4f133aa | ||
|
|
e5ba4ff973 | ||
|
|
ce133b997d | ||
|
|
217632354f | ||
|
|
9841351190 | ||
|
|
f3341f4b7f | ||
|
|
ff1f448860 | ||
|
|
37f28746fc | ||
|
|
9a22ba3af7 | ||
|
|
2942da78de | ||
|
|
89ff6be971 | ||
|
|
be0d7bcce1 | ||
|
|
851b257678 | ||
|
|
579eacb644 | ||
|
|
f52c5b584e | ||
|
|
8980c18deb | ||
|
|
b05a9ce064 | ||
|
|
1974314c1f | ||
|
|
2bde023d4d | ||
|
|
3a10003246 | ||
|
|
1b08710b7e | ||
|
|
101d09eeb3 | ||
|
|
00f949f156 | ||
|
|
adbe46d369 | ||
|
|
3198926cd6 | ||
|
|
957a6a20fe | ||
|
|
94f75bb0d7 | ||
|
|
0f442755e5 | ||
|
|
cd2e782d48 | ||
|
|
e97606ca87 | ||
|
|
00ada80230 | ||
|
|
34db98c489 | ||
|
|
110695355c | ||
|
|
021fb4bb94 | ||
|
|
dea033e4b0 | ||
|
|
7dfe40739e | ||
|
|
9f0d1b515c | ||
|
|
2691d46d50 | ||
|
|
78c8f1de71 | ||
|
|
d27ee4bfbc | ||
|
|
cc5daa428d | ||
|
|
3e2189aeed | ||
|
|
79f9963792 | ||
|
|
6f53723169 | ||
|
|
d8cb100fc0 | ||
|
|
5f9b2f1159 | ||
|
|
801ca7eda1 | ||
|
|
45a2d3745c | ||
|
|
551fe4d846 | ||
|
|
791981c2f2 | ||
|
|
a18a620847 | ||
|
|
99e63ffc3f | ||
|
|
e10a6d9de5 | ||
|
|
147f16571a | ||
|
|
bd1fbc4a05 | ||
|
|
0843f78ec8 | ||
|
|
9769fbfcf2 | ||
|
|
7e73197eb9 | ||
|
|
e3964fd710 | ||
|
|
e66961b814 | ||
|
|
4176e5a98e | ||
|
|
45cf8a62d1 | ||
|
|
b1380819ba | ||
|
|
57fa457596 | ||
|
|
de1e218ce9 | ||
|
|
e117ee2bef | ||
|
|
a9e101d9f4 | ||
|
|
a2f8203a42 | ||
|
|
b9ee127775 | ||
|
|
6668bb3e8a | ||
|
|
5fd129e509 | ||
|
|
d59c1f53b9 | ||
|
|
d2f38c1abc | ||
|
|
c0a1db6941 | ||
|
|
fc10b4a79b | ||
|
|
9da2117e99 | ||
|
|
7e030b149b | ||
|
|
bd23abd265 | ||
|
|
dd0fb8292c | ||
|
|
b4cbf63519 | ||
|
|
4fd04fa349 | ||
|
|
c22cdb8d81 | ||
|
|
eb963b2eb4 | ||
|
|
7d299908c9 | ||
|
|
2585282f86 | ||
|
|
f25d5b3304 | ||
|
|
6e878faa8b | ||
|
|
15a6cbe62b | ||
|
|
76b0b214ec | ||
|
|
f5c643c960 | ||
|
|
ca8e0613fb | ||
|
|
0c9334d0d2 | ||
|
|
712dc97e9b | ||
|
|
4df48c97ec | ||
|
|
fe3ea53cda | ||
|
|
d385c80882 | ||
|
|
b823213c94 | ||
|
|
4b86311ab9 | ||
|
|
b9efa8f445 | ||
|
|
f8db12346d | ||
|
|
4d3948f81f | ||
|
|
5431d50206 | ||
|
|
6db078c26a | ||
|
|
f61e9c7f27 | ||
|
|
567d92ce00 | ||
|
|
7a6d26c5da | ||
|
|
046ac85177 | ||
|
|
f0fd088247 | ||
|
|
5ec0d1e691 | ||
|
|
9391a934c3 | ||
|
|
bb62e6a318 | ||
|
|
0da6539c48 | ||
|
|
9cf833dab2 | ||
|
|
ed57260fcf | ||
|
|
c98f625c4c | ||
|
|
f3008064e4 | ||
|
|
1faee00764 | ||
|
|
a40505e2ee | ||
|
|
484202b4c6 | ||
|
|
6a7fc17c60 | ||
|
|
05d3897ae2 | ||
|
|
9f1210202a | ||
|
|
be6b172d6f | ||
|
|
fef9e0a5c1 | ||
|
|
b84b033bf3 | ||
|
|
b30ff1f55a | ||
|
|
c6be0b290b | ||
|
|
33cfd7a629 | ||
|
|
5952a5c69d | ||
|
|
20de563925 | ||
|
|
7da80b4c62 | ||
|
|
15d765be6d | ||
|
|
bfe2f116a7 | ||
|
|
f535b3de2f | ||
|
|
e560c18b57 | ||
|
|
aecb99b6a3 | ||
|
|
7da17f8190 | ||
|
|
1964270a4f | ||
|
|
f45b61d95c | ||
|
|
ff11c38169 | ||
|
|
3e67067431 | ||
|
|
824f00d1e8 | ||
|
|
96d19f59a4 | ||
|
|
42c6fe50d2 | ||
|
|
9242f7095a | ||
|
|
99c9fbc38f | ||
|
|
0d31207ad7 | ||
|
|
8af7dbc35a | ||
|
|
d0a373cb15 | ||
|
|
3dc87bbca8 | ||
|
|
a55c399585 | ||
|
|
f74aa24dd2 | ||
|
|
1aa7eb4478 | ||
|
|
0c7002ba59 | ||
|
|
fd6dd1ea18 | ||
|
|
aa74d5cd82 | ||
|
|
8fc10a0bdd | ||
|
|
809ed0f0dc | ||
|
|
b8a4e1c4a3 | ||
|
|
d9e45f732b | ||
|
|
ca025b36f7 | ||
|
|
bfb719d35e | ||
|
|
2a1b61107f | ||
|
|
969cee7c90 | ||
|
|
7a3f579d3e | ||
|
|
288d5efa88 | ||
|
|
7be821963c | ||
|
|
a236f8992a | ||
|
|
a5c2257f39 | ||
|
|
9d3b4ba816 | ||
|
|
43bf0767f1 | ||
|
|
b301e5b151 | ||
|
|
2b484c0382 | ||
|
|
f40ab4e2d5 | ||
|
|
c0a27380e9 | ||
|
|
0d7a3f43c4 | ||
|
|
8195e439f3 | ||
|
|
b5edbf716c | ||
|
|
466265fde1 | ||
|
|
40033e09cd | ||
|
|
573663412c | ||
|
|
17599417f7 | ||
|
|
0ece6d8b0e | ||
|
|
e0ac0393fe | ||
|
|
6d38b3255c | ||
|
|
477ff424d6 | ||
|
|
a843104348 | ||
|
|
0f4bc0981a | ||
|
|
07f6351465 | ||
|
|
1b26e86365 | ||
|
|
94b4bf94c0 | ||
|
|
d5de05b633 | ||
|
|
0ab6cad048 | ||
|
|
9833ad548b | ||
|
|
aa1ba3b226 | ||
|
|
3774d4de28 | ||
|
|
e4961726bc | ||
|
|
77cf7d0da6 | ||
|
|
a993e0b228 | ||
|
|
43671a9fd6 | ||
|
|
49cfd1e9b7 | ||
|
|
58d4a4f54f | ||
|
|
e4e328ba6a | ||
|
|
fd6bc955ff | ||
|
|
511a18e0ed | ||
|
|
e29d224a92 | ||
|
|
bb48ffb01f | ||
|
|
31fd3411f7 | ||
|
|
a737d2675e | ||
|
|
fd462659cd | ||
|
|
cb10d0d465 | ||
|
|
61f1c4884c | ||
|
|
2cd00de6e3 | ||
|
|
d3c5d53eae | ||
|
|
6dfafae342 | ||
|
|
2f861c3309 | ||
|
|
af388f0f16 | ||
|
|
c36cc86c5f | ||
|
|
02f195b25c | ||
|
|
18623fd9b7 | ||
|
|
9b74bb73aa | ||
|
|
ee9636b496 | ||
|
|
5c2cbd7840 | ||
|
|
7fbac6cc17 | ||
|
|
9e7e9d66bf | ||
|
|
7fe66aa7fa | ||
|
|
2dda0efe83 | ||
|
|
59620ca473 | ||
|
|
12eae1eff2 | ||
|
|
b03bf87b7d | ||
|
|
c32718b164 | ||
|
|
a6ea12fedc | ||
|
|
2d260eb0d5 | ||
|
|
d7dd069ae0 | ||
|
|
6a77a58489 | ||
|
|
c30ac5f927 | ||
|
|
437f7ef890 | ||
|
|
1f7347e8de | ||
|
|
96f59d7cfe | ||
|
|
d55f65c7c9 | ||
|
|
9a0d5b918f | ||
|
|
3553fbc7b6 | ||
|
|
55d53f13d9 | ||
|
|
27369a650c | ||
|
|
913f0d5d97 | ||
|
|
ada63ec697 | ||
|
|
117f06e971 | ||
|
|
9f03a9a6e2 | ||
|
|
ce406c7088 | ||
|
|
e7127df30d | ||
|
|
10e2817257 | ||
|
|
337a47c62b | ||
|
|
14bdac20ef | ||
|
|
88e2b3f9aa | ||
|
|
22d731f06d | ||
|
|
e3d288ef7d | ||
|
|
455f597543 | ||
|
|
8c9e626920 | ||
|
|
5a000c1ff4 | ||
|
|
ddf634bfb2 | ||
|
|
89d3b8cc6a | ||
|
|
49af6d09a2 | ||
|
|
e5b0cac284 | ||
|
|
6f33900f85 | ||
|
|
514823af7d | ||
|
|
65b058f563 | ||
|
|
7c8560deff | ||
|
|
6bbe2613b4 | ||
|
|
5771478e4b | ||
|
|
e13030bc89 | ||
|
|
0a0ac93a55 | ||
|
|
214fb50e74 | ||
|
|
959f8ee31e | ||
|
|
cb0d75be37 | ||
|
|
11353e9e3a | ||
|
|
8cd5c15c2b | ||
|
|
b86b8b8ee1 | ||
|
|
c5f6e6b028 | ||
|
|
592d8abc58 | ||
|
|
d93068fc62 | ||
|
|
a864af52df | ||
|
|
1eedd4b185 | ||
|
|
9d38edfe95 | ||
|
|
f895ebba73 | ||
|
|
511287b16e | ||
|
|
530e06ec66 | ||
|
|
9cab383b43 | ||
|
|
9785ab82ed | ||
|
|
9d237e7bd6 | ||
|
|
7e9885012d | ||
|
|
1de785d97c | ||
|
|
2bd6566537 | ||
|
|
88fa4cf188 | ||
|
|
b26167481e | ||
|
|
1b6af9bd12 | ||
|
|
0159963cb0 | ||
|
|
996041cabc | ||
|
|
cb0352e33c | ||
|
|
3169f032c8 | ||
|
|
5ff8ee1a8f | ||
|
|
d3f31a3ace | ||
|
|
ac7e7f0db9 | ||
|
|
4c1e967dad | ||
|
|
f3ccd5c074 | ||
|
|
8369c0e2c0 | ||
|
|
122a966e72 | ||
|
|
9c2ff2f862 | ||
|
|
0ba45e746b | ||
|
|
54c06cdabb | ||
|
|
5a2e10317c | ||
|
|
8292d52acf | ||
|
|
7d21470fc7 | ||
|
|
eb0530bcba | ||
|
|
8855092faa | ||
|
|
2e02a3c71e | ||
|
|
5b5303ba7f | ||
|
|
022a54278e | ||
|
|
19b50dc428 | ||
|
|
e7eac003a9 | ||
|
|
cc17c6b2cd | ||
|
|
23d16b07aa | ||
|
|
7ecb3dd771 | ||
|
|
e43f974d34 | ||
|
|
e16cd38722 | ||
|
|
9d2f81d6b9 | ||
|
|
3fe539436b | ||
|
|
76f94eb559 | ||
|
|
7630ef921d | ||
|
|
625127d298 | ||
|
|
f24c4d2805 | ||
|
|
194340afa0 | ||
|
|
fdc9639aba | ||
|
|
f95ec53a85 | ||
|
|
3d425b7030 | ||
|
|
37c6c24e0e | ||
|
|
50bdd7ec7b | ||
|
|
769cb3e251 | ||
|
|
9447c45406 | ||
|
|
66a3962cfe | ||
|
|
d145eacbaf | ||
|
|
ed03ed7bad | ||
|
|
953b463799 | ||
|
|
6d28bb0489 | ||
|
|
c2f464ea75 | ||
|
|
4c56ffc767 | ||
|
|
885aa8833c | ||
|
|
63310c44c0 | ||
|
|
05dd65718f | ||
|
|
05d3f8a667 | ||
|
|
3fa45ea728 | ||
|
|
a7d2098f09 | ||
|
|
e1ecb49d59 | ||
|
|
6facfac4c5 | ||
|
|
97d2494fe3 | ||
|
|
a54be69c96 | ||
|
|
800e25a7a7 | ||
|
|
c1ce2977fa |
106
CHANGES
106
CHANGES
@@ -1593,3 +1593,109 @@
|
||||
* Make it easier to import email
|
||||
* Give SFTP access only to admins
|
||||
|
||||
[4.0.2]
|
||||
* Fix GCDNS crash
|
||||
* Add option to update without backing up
|
||||
|
||||
[4.0.3]
|
||||
* Fix dashboard issue for non-admins
|
||||
|
||||
[4.1.0]
|
||||
* Remove password requirement for uninstalling apps and users
|
||||
* Hosting provider edition
|
||||
* Enforce limits in mail container
|
||||
* Fix crash when using unauthenticated relay
|
||||
* Fix domain and tag filtering
|
||||
* Customizable app icons
|
||||
* Remove obsolete X-Frame-Options from nginx configs
|
||||
* Give SFTP access based on access restriction
|
||||
|
||||
[4.1.1]
|
||||
* Add UI hint about SFTP access restriction
|
||||
|
||||
[4.1.2]
|
||||
* Accept incoming mail from a private relay
|
||||
* Fix issue where unused addon images were not pruned
|
||||
* Add UI for redirect from multiple domains
|
||||
* Allow apps to be relocated to custom data directory
|
||||
* Make all cloudron env vars have CLOUDRON_ prefix
|
||||
* Update manifest version to 2
|
||||
* Fix issue where DKIM keys were inaccessible
|
||||
* Fix DKIM selector conflict when adding same domain across multiple cloudrons
|
||||
* Fix name.com DNS backend issue for naked domains
|
||||
* Add DigitalOcean Frankfurt (fra1) region for backup storage
|
||||
|
||||
[4.1.3]
|
||||
* Update manifest format package
|
||||
|
||||
[4.1.4]
|
||||
* Add CLOUDRON_ prefix to MySQL addon variables
|
||||
|
||||
[4.1.5]
|
||||
* Make the terminal addon button inject variables based on manifest version
|
||||
* Preserve addon passwords correctly when using v2 manifest
|
||||
* Show error message instead of logging out user when invalid 2FA token is provided
|
||||
* Ensure redis vars are renamed with manifest v2
|
||||
* Add missing Scaleway Object Storage to restore UI
|
||||
* Fix Exoscale endpoints in restore UI
|
||||
* Reset the app icon when showing the configure UI
|
||||
|
||||
[4.1.6]
|
||||
* Fix issue where CLOUDRON_APP_HOSTNAME was incorrectly set
|
||||
* Remove chat link from the footer of login screen
|
||||
* Add support for oplog tailing in mongodb
|
||||
* Fix LDAP not accessible via scheduler containers
|
||||
|
||||
[4.1.7]
|
||||
* Fix issue where login looped when admin bit was removed
|
||||
|
||||
[4.2.0]
|
||||
* Fix issue where tar backups with files > 8GB was corrupt
|
||||
* Add SparkPost as mail relay backend
|
||||
* Add Wasabi storage backend
|
||||
* TOTP tokens are now checked for with +- 60 seconds
|
||||
* IP based restore
|
||||
* Fix issue where task logs were not getting rotated correctly
|
||||
* Add notification for box update
|
||||
* User enable/disable flag
|
||||
* Check disk space before various operations like install, update, backup etc
|
||||
* Collect per app du information
|
||||
* Set Cloudron specific UA for healthchecks
|
||||
* Show message why an app task is 'pending'
|
||||
* Rework app task system so that we can now pass dynamic arguments
|
||||
* Add external LDAP server integration
|
||||
|
||||
[4.2.1]
|
||||
* Rework the app configuration routes & UI
|
||||
* Fine grained eventlog for app configuration
|
||||
* Update Haraka to 2.8.24
|
||||
* Set sieve_max_redirects to 64
|
||||
* SRS support for mail forwarding
|
||||
* Fix issue where sieve responses were not sent via the relay
|
||||
* File based session store
|
||||
* Fix API token error reporting for namecheap backend
|
||||
|
||||
[4.2.2]
|
||||
* Fix typos in migration
|
||||
|
||||
[4.2.3]
|
||||
* Remove flicker of custom icon
|
||||
* Preserve PROVIDER setting from cloudron.conf
|
||||
* Add Skip backup option when updating an app
|
||||
* Fix bug where nginx was not reloaded on cert renewal
|
||||
|
||||
[4.2.4]
|
||||
* Fix demo settings state regression
|
||||
|
||||
[4.2.5]
|
||||
* Fix the demo settins fix
|
||||
|
||||
[4.2.6]
|
||||
* Fix configuration of empty app location (subdomain)
|
||||
|
||||
[4.2.7]
|
||||
* Fix issue where the icon for normal users was displayed incorrectly
|
||||
* Kill stuck backup processes after 12 hours and notify admins
|
||||
* Reconfigure email apps when mail domain is added/removed
|
||||
* Fix crash when only udp ports are defined
|
||||
|
||||
|
||||
@@ -41,12 +41,7 @@ Try our demo at https://my.demo.cloudron.io (username: cloudron password: cloudr
|
||||
|
||||
## Installing
|
||||
|
||||
You can install the Cloudron platform on your own server or get a managed server
|
||||
from cloudron.io. In either case, the Cloudron platform will keep your server and
|
||||
apps up-to-date and secure.
|
||||
|
||||
* [Selfhosting](https://cloudron.io/documentation/installation/) - [Pricing](https://cloudron.io/pricing.html)
|
||||
* [Managed Hosting](https://cloudron.io/managed.html)
|
||||
[Install script](https://cloudron.io/documentation/installation/) - [Pricing](https://cloudron.io/pricing.html)
|
||||
|
||||
**Note:** This repo is a small part of what gets installed on your server - there is
|
||||
the dashboard, database addons, graph container, base image etc. Cloudron also relies
|
||||
|
||||
@@ -33,6 +33,7 @@ gpg_package=$([[ "${ubuntu_version}" == "16.04" ]] && echo "gnupg" || echo "gpg"
|
||||
apt-get -y install \
|
||||
acl \
|
||||
build-essential \
|
||||
cifs-utils \
|
||||
cron \
|
||||
curl \
|
||||
debconf-utils \
|
||||
@@ -40,18 +41,22 @@ apt-get -y install \
|
||||
$gpg_package \
|
||||
iptables \
|
||||
libpython2.7 \
|
||||
linux-generic \
|
||||
logrotate \
|
||||
mysql-server-5.7 \
|
||||
nginx-full \
|
||||
openssh-server \
|
||||
pwgen \
|
||||
resolvconf \
|
||||
sudo \
|
||||
swaks \
|
||||
tzdata \
|
||||
unattended-upgrades \
|
||||
unbound \
|
||||
xfsprogs
|
||||
|
||||
# on some providers like scaleway the sudo file is changed and we want to keep the old one
|
||||
apt-get -o Dpkg::Options::="--force-confold" install -y sudo
|
||||
|
||||
# this ensures that unattended upgades are enabled, if it was disabled during ubuntu install time (see #346)
|
||||
# debconf-set-selection of unattended-upgrades/enable_auto_updates + dpkg-reconfigure does not work
|
||||
cp /usr/share/unattended-upgrades/20auto-upgrades /etc/apt/apt.conf.d/20auto-upgrades
|
||||
|
||||
17
box.js
17
box.js
@@ -14,25 +14,14 @@
|
||||
require('supererror')({ splatchError: true });
|
||||
|
||||
let async = require('async'),
|
||||
config = require('./src/config.js'),
|
||||
ldap = require('./src/ldap.js'),
|
||||
constants = require('./src/constants.js'),
|
||||
dockerProxy = require('./src/dockerproxy.js'),
|
||||
ldap = require('./src/ldap.js'),
|
||||
server = require('./src/server.js');
|
||||
|
||||
console.log();
|
||||
console.log('==========================================');
|
||||
console.log(' Cloudron will use the following settings ');
|
||||
console.log('==========================================');
|
||||
console.log();
|
||||
console.log(' Environment: ', config.CLOUDRON ? 'CLOUDRON' : 'TEST');
|
||||
console.log(' Version: ', config.version());
|
||||
console.log(' Admin Origin: ', config.adminOrigin());
|
||||
console.log(' Appstore API server origin: ', config.apiServerOrigin());
|
||||
console.log(' Appstore Web server origin: ', config.webServerOrigin());
|
||||
console.log(' SysAdmin Port: ', config.get('sysadminPort'));
|
||||
console.log(' LDAP Server Port: ', config.get('ldapPort'));
|
||||
console.log(' Docker Proxy Port: ', config.get('dockerProxyPort'));
|
||||
console.log();
|
||||
console.log(` Cloudron ${constants.VERSION} `);
|
||||
console.log('==========================================');
|
||||
console.log();
|
||||
|
||||
|
||||
15
migrations/20190610195323-mail-add-dkimSelector.js
Normal file
15
migrations/20190610195323-mail-add-dkimSelector.js
Normal file
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE mail ADD COLUMN dkimSelector VARCHAR(128) NOT NULL DEFAULT "cloudron"', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE mail DROP COLUMN dkimSelector', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
14
migrations/20190703031708-apps-remove-ownerId.js
Normal file
14
migrations/20190703031708-apps-remove-ownerId.js
Normal file
@@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE apps DROP FOREIGN KEY apps_owner_constraint'),
|
||||
db.runSql.bind(db, 'ALTER TABLE apps DROP COLUMN ownerId')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
29
migrations/20190725172940-settings-migrate-cloudron-conf.js
Normal file
29
migrations/20190725172940-settings-migrate-cloudron-conf.js
Normal file
@@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async'),
|
||||
fs = require('fs');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
if (!fs.existsSync('/etc/cloudron/cloudron.conf')) {
|
||||
console.log('Unable to locate cloudron.conf');
|
||||
return callback();
|
||||
}
|
||||
|
||||
const config = JSON.parse(fs.readFileSync('/etc/cloudron/cloudron.conf', 'utf8'));
|
||||
|
||||
async.series([
|
||||
fs.writeFile.bind(null, '/etc/cloudron/PROVIDER', config.provider, 'utf8'),
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
// we use replace instead of insert because the cloudron-setup adds api/web_server_origin even for legacy setups
|
||||
db.runSql.bind(db, 'REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'api_server_origin', config.apiServerOrigin ]),
|
||||
db.runSql.bind(db, 'REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'web_server_origin', config.webServerOrigin ]),
|
||||
db.runSql.bind(db, 'REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'admin_domain', config.adminDomain ]),
|
||||
db.runSql.bind(db, 'REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'admin_fqdn', config.adminFqdn ]),
|
||||
db.runSql.bind(db, 'REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'demo', config.isDemo ]),
|
||||
db.runSql.bind(db, 'COMMIT')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
17
migrations/20190808123531-users-add-active.js
Normal file
17
migrations/20190808123531-users-add-active.js
Normal file
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users ADD COLUMN active BOOLEAN DEFAULT 1', function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users DROP COLUMN active', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
17
migrations/20190826210725-apps-add-taskId.js
Normal file
17
migrations/20190826210725-apps-add-taskId.js
Normal 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 ADD COLUMN taskId INTEGER'),
|
||||
db.runSql.bind(db, 'ALTER TABLE apps ADD CONSTRAINT apps_task_constraint FOREIGN KEY(taskId) REFERENCES tasks(id)')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE app DROP FOREIGN KEY apps_task_constraint'),
|
||||
db.runSql.bind(db, 'ALTER TABLE apps DROP COLUMN taskId'),
|
||||
], callback);
|
||||
};
|
||||
12
migrations/20190827221616-apps-drop-task-args.js
Normal file
12
migrations/20190827221616-apps-drop-task-args.js
Normal file
@@ -0,0 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP updateConfigJson, DROP restoreConfigJson, DROP oldConfigJson', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps CHANGE installationProgress errorJson TEXT', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps CHANGE errorJson installationProgress TEXT', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
17
migrations/20190829153300-users-add-source.js
Normal file
17
migrations/20190829153300-users-add-source.js
Normal file
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users ADD COLUMN source VARCHAR(128) DEFAULT ""', function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users DROP COLUMN source', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
'use strict';
|
||||
|
||||
let async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE tasks CHANGE errorMessage errorJson TEXT', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
// convert error messages into json
|
||||
db.all('SELECT id, errorJson FROM apps', function (error, apps) {
|
||||
async.eachSeries(apps, function (app, iteratorDone) {
|
||||
if (app.errorJson === 'null') return iteratorDone();
|
||||
if (app.errorJson === null) return iteratorDone();
|
||||
|
||||
db.runSql('UPDATE apps SET errorJson = ? WHERE id = ?', [ JSON.stringify({ message: app.errorJson }), app.id ], iteratorDone);
|
||||
}, callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE tasks CHANGE errorJson errorMessage TEXT', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
// imports mailbox entries for existing users
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT * FROM mailboxes', function (error, mailboxes) {
|
||||
async.eachSeries(mailboxes, function (mailbox, iteratorDone) {
|
||||
if (!mailbox.membersJson) return iteratorDone();
|
||||
|
||||
let members = JSON.parse(mailbox.membersJson);
|
||||
members = members.map((m) => m.indexOf('@') === -1 ? `${m}@${mailbox.domain}` : m); // only because we don't do things in a xction
|
||||
|
||||
db.runSql('UPDATE mailboxes SET membersJson=? WHERE name=? AND domain=?', [ JSON.stringify(members), mailbox.name, mailbox.domain ], iteratorDone);
|
||||
}, callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
19
migrations/20190923050431-apps-make-runState-not-null.js
Normal file
19
migrations/20190923050431-apps-make-runState-not-null.js
Normal file
@@ -0,0 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('UPDATE apps SET runState=? WHERE runState IS NULL', [ 'running' ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
db.runSql('ALTER TABLE apps MODIFY runState VARCHAR(512) NOT NULL', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE app MODIFY runState VARCHAR(512)', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
10
migrations/20191002103646-fix-demo-settings-boolean.js
Normal file
10
migrations/20191002103646-fix-demo-settings-boolean.js
Normal file
@@ -0,0 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
// We clear all demo state in the Cloudron...the demo cloudron needs manual intervention afterwards
|
||||
db.runSql('REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'demo', '' ], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -27,6 +27,7 @@ CREATE TABLE IF NOT EXISTS users(
|
||||
twoFactorAuthenticationSecret VARCHAR(128) DEFAULT "",
|
||||
twoFactorAuthenticationEnabled BOOLEAN DEFAULT false,
|
||||
admin BOOLEAN DEFAULT false,
|
||||
source VARCHAR(128) DEFAULT "",
|
||||
|
||||
PRIMARY KEY(id));
|
||||
|
||||
@@ -63,9 +64,8 @@ CREATE TABLE IF NOT EXISTS clients(
|
||||
CREATE TABLE IF NOT EXISTS apps(
|
||||
id VARCHAR(128) NOT NULL UNIQUE,
|
||||
appStoreId VARCHAR(128) NOT NULL,
|
||||
installationState VARCHAR(512) NOT NULL,
|
||||
installationProgress TEXT,
|
||||
runState VARCHAR(512),
|
||||
installationState VARCHAR(512) NOT NULL, // the active task on the app
|
||||
runState VARCHAR(512) NOT NULL, // if the app is stopped
|
||||
health VARCHAR(128),
|
||||
healthTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app last responded
|
||||
containerId VARCHAR(128),
|
||||
@@ -87,15 +87,11 @@ CREATE TABLE IF NOT EXISTS apps(
|
||||
mailboxName VARCHAR(128), // mailbox of this app. default allocated as '.app'
|
||||
label VARCHAR(128), // display name
|
||||
tagsJson VARCHAR(2048), // array of tags
|
||||
dataDir VARCHAR(256) UNIQUE,
|
||||
taskId INTEGER, // current task
|
||||
errorJson TEXT,
|
||||
|
||||
// the following fields do not belong here, they can be removed when we use a queue for apptask
|
||||
restoreConfigJson VARCHAR(256), // used to pass backupId to restore from to apptask
|
||||
oldConfigJson TEXT, // used to pass old config to apptask (configure, restore)
|
||||
updateConfigJson TEXT, // used to pass new config to apptask (update)
|
||||
|
||||
ownerId VARCHAR(128),
|
||||
|
||||
FOREIGN KEY(ownerId) REFERENCES users(id),
|
||||
FOREIGN KEY(taskId) REFERENCES tasks(id),
|
||||
PRIMARY KEY(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS appPortBindings(
|
||||
@@ -174,6 +170,8 @@ CREATE TABLE IF NOT EXISTS mail(
|
||||
catchAllJson TEXT,
|
||||
relayJson TEXT,
|
||||
|
||||
dkimSelector VARCHAR(128) NOT NULL DEFAULT "cloudron",
|
||||
|
||||
FOREIGN KEY(domain) REFERENCES domains(domain),
|
||||
PRIMARY KEY(domain))
|
||||
|
||||
@@ -191,7 +189,7 @@ CREATE TABLE IF NOT EXISTS mailboxes(
|
||||
type VARCHAR(16) NOT NULL, /* 'mailbox', 'alias', 'list' */
|
||||
ownerId VARCHAR(128) NOT NULL, /* user id */
|
||||
aliasTarget VARCHAR(128), /* the target name type is an alias */
|
||||
membersJson TEXT, /* members of a group */
|
||||
membersJson TEXT, /* members of a group. fully qualified */
|
||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
domain VARCHAR(128),
|
||||
|
||||
@@ -202,7 +200,7 @@ CREATE TABLE IF NOT EXISTS subdomains(
|
||||
appId VARCHAR(128) NOT NULL,
|
||||
domain VARCHAR(128) NOT NULL,
|
||||
subdomain VARCHAR(128) NOT NULL,
|
||||
type VARCHAR(128) NOT NULL,
|
||||
type VARCHAR(128) NOT NULL, /* primary or redirect */
|
||||
|
||||
FOREIGN KEY(domain) REFERENCES domains(domain),
|
||||
FOREIGN KEY(appId) REFERENCES apps(id),
|
||||
@@ -213,8 +211,8 @@ CREATE TABLE IF NOT EXISTS tasks(
|
||||
type VARCHAR(32) NOT NULL,
|
||||
percent INTEGER DEFAULT 0,
|
||||
message TEXT,
|
||||
errorMessage TEXT,
|
||||
result TEXT,
|
||||
errorJson TEXT,
|
||||
resultJson TEXT,
|
||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id));
|
||||
|
||||
1242
package-lock.json
generated
1242
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
50
package.json
50
package.json
@@ -14,40 +14,40 @@
|
||||
"node": ">=4.0.0 <=4.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google-cloud/dns": "^0.9.2",
|
||||
"@google-cloud/dns": "^1.1.0",
|
||||
"@google-cloud/storage": "^2.5.0",
|
||||
"@sindresorhus/df": "^3.1.0",
|
||||
"@sindresorhus/df": "git+https://github.com/cloudron-io/df.git#type",
|
||||
"async": "^2.6.2",
|
||||
"aws-sdk": "^2.441.0",
|
||||
"body-parser": "^1.18.3",
|
||||
"cloudron-manifestformat": "^2.14.2",
|
||||
"connect": "^3.6.6",
|
||||
"aws-sdk": "^2.476.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"cloudron-manifestformat": "^2.15.0",
|
||||
"connect": "^3.7.0",
|
||||
"connect-ensure-login": "^0.1.1",
|
||||
"connect-lastmile": "^1.0.2",
|
||||
"connect-lastmile": "^1.2.1",
|
||||
"connect-timeout": "^1.9.0",
|
||||
"cookie-parser": "^1.4.4",
|
||||
"cookie-session": "^1.3.3",
|
||||
"cron": "^1.7.0",
|
||||
"csurf": "^1.9.0",
|
||||
"db-migrate": "^0.11.5",
|
||||
"cron": "^1.7.1",
|
||||
"csurf": "^1.10.0",
|
||||
"db-migrate": "^0.11.6",
|
||||
"db-migrate-mysql": "^1.1.10",
|
||||
"debug": "^4.1.1",
|
||||
"dockerode": "^2.5.8",
|
||||
"ejs": "^2.6.1",
|
||||
"ejs-cli": "^2.0.1",
|
||||
"express": "^4.16.4",
|
||||
"express-session": "^1.16.1",
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.16.2",
|
||||
"js-yaml": "^3.13.1",
|
||||
"json": "^9.0.6",
|
||||
"ldapjs": "^1.0.2",
|
||||
"lodash": "^4.17.11",
|
||||
"lodash.chunk": "^4.2.0",
|
||||
"mime": "^2.4.2",
|
||||
"mime": "^2.4.4",
|
||||
"moment-timezone": "^0.5.25",
|
||||
"morgan": "^1.9.1",
|
||||
"multiparty": "^4.2.1",
|
||||
"mysql": "^2.17.1",
|
||||
"namecheap": "github:joshuakarjala/node-namecheap#464a952",
|
||||
"nodemailer": "^6.1.1",
|
||||
"nodemailer": "^6.2.1",
|
||||
"nodemailer-smtp-transport": "^2.7.4",
|
||||
"oauth2orize": "^1.11.0",
|
||||
"once": "^1.4.0",
|
||||
@@ -57,28 +57,31 @@
|
||||
"passport-http-bearer": "^1.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"passport-oauth2-client-password": "^0.1.2",
|
||||
"pretty-bytes": "^5.3.0",
|
||||
"progress-stream": "^2.0.0",
|
||||
"proxy-middleware": "^0.15.0",
|
||||
"qrcode": "^1.3.3",
|
||||
"readdirp": "^3.0.0",
|
||||
"readdirp": "^3.0.2",
|
||||
"request": "^2.88.0",
|
||||
"rimraf": "^2.6.3",
|
||||
"s3-block-read-stream": "^0.5.0",
|
||||
"safetydance": "^0.7.1",
|
||||
"semver": "^6.0.0",
|
||||
"semver": "^6.1.1",
|
||||
"session-file-store": "^1.3.1",
|
||||
"showdown": "^1.9.0",
|
||||
"speakeasy": "^2.0.0",
|
||||
"split": "^1.0.1",
|
||||
"superagent": "^5.0.2",
|
||||
"superagent": "^5.0.9",
|
||||
"supererror": "^0.7.2",
|
||||
"tar-fs": "github:cloudron-io/tar-fs#ignore_stat_error",
|
||||
"tar-stream": "^2.0.1",
|
||||
"tar-stream": "^2.1.0",
|
||||
"tldjs": "^2.3.1",
|
||||
"underscore": "^1.9.1",
|
||||
"uuid": "^3.3.2",
|
||||
"valid-url": "^1.0.9",
|
||||
"validator": "^10.11.0",
|
||||
"ws": "^6.2.1"
|
||||
"validator": "^11.0.0",
|
||||
"ws": "^7.0.0",
|
||||
"xml2js": "^0.4.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"expect.js": "*",
|
||||
@@ -87,9 +90,8 @@
|
||||
"mocha": "^6.1.4",
|
||||
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
|
||||
"nock": "^10.0.6",
|
||||
"node-sass": "^4.11.0",
|
||||
"recursive-readdir": "^2.2.2",
|
||||
"sinon": "^7.3.2"
|
||||
"node-sass": "^4.12.0",
|
||||
"recursive-readdir": "^2.2.2"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "./runTests",
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400 --http1.1"
|
||||
|
||||
ip=""
|
||||
dns_config=""
|
||||
tls_cert_file=""
|
||||
tls_key_file=""
|
||||
license_file=""
|
||||
backup_config=""
|
||||
|
||||
args=$(getopt -o "" -l "ip:,backup-config:,license:,dns-config:,tls-cert:,tls-key:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--ip) ip="$2"; shift 2;;
|
||||
--dns-config) dns_config="$2"; shift 2;;
|
||||
--tls-cert) tls_cert_file="$2"; shift 2;;
|
||||
--tls-key) tls_key_file="$2"; shift 2;;
|
||||
--license) license_file="$2"; shift 2;;
|
||||
--backup-config) backup_config="$2"; shift 2;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
# validate arguments in the absence of data
|
||||
if [[ -z "${ip}" ]]; then
|
||||
echo "--ip is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${dns_config}" ]]; then
|
||||
echo "--dns-config is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "${license_file}" ]]; then
|
||||
echo "--license must be a valid license file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
function get_status() {
|
||||
key="$1"
|
||||
if status=$($curl -q -f -k "https://${ip}/api/v1/cloudron/status" 2>/dev/null); then
|
||||
currentValue=$(echo "${status}" | python3 -c 'import sys, json; print(json.dumps(json.load(sys.stdin)[sys.argv[1]]))' "${key}")
|
||||
echo "${currentValue}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
function wait_for_status() {
|
||||
key="$1"
|
||||
expectedValue="$2"
|
||||
|
||||
echo "wait_for_status: $key to be $expectedValue"
|
||||
while true; do
|
||||
if currentValue=$(get_status "${key}"); then
|
||||
echo "wait_for_status: $key is current: $currentValue expecting: $expectedValue"
|
||||
if [[ "${currentValue}" == $expectedValue ]]; then
|
||||
break
|
||||
fi
|
||||
fi
|
||||
sleep 3
|
||||
done
|
||||
}
|
||||
|
||||
echo "=> Waiting for cloudron to be ready"
|
||||
wait_for_status "version" '*'
|
||||
|
||||
domain=$(echo "${dns_config}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["domain"])')
|
||||
|
||||
echo "Provisioning Cloudron ${domain}"
|
||||
if [[ -n "${tls_cert_file}" && -n "${tls_key_file}" ]]; then
|
||||
tls_cert=$(cat "${tls_cert_file}" | awk '{printf "%s\\n", $0}')
|
||||
tls_key=$(cat "${tls_key_file}" | awk '{printf "%s\\n", $0}')
|
||||
fallback_cert=$(printf '{ "cert": "%s", "key": "%s", "provider": "fallback", "restricted": true }' "${tls_cert}" "${tls_key}")
|
||||
else
|
||||
fallback_cert=None
|
||||
fi
|
||||
|
||||
tls_config='{ "provider": "fallback" }'
|
||||
dns_config=$(echo "${dns_config}" | python3 -c "import json,sys;obj=json.load(sys.stdin);obj.update(tlsConfig=${tls_config});obj.update(fallbackCertficate=${fallback_cert});print(json.dumps(obj))")
|
||||
|
||||
license=$(cat "${license_file}")
|
||||
|
||||
if [[ -z "${backup_config:-}" ]]; then
|
||||
backup_config='{ "provider": "filesystem", "backupFolder": "/var/backups", "format": "tgz" }'
|
||||
fi
|
||||
|
||||
setupData=$(printf '{ "dnsConfig": %s, "autoconf": { "appstoreConfig": %s, "backupConfig": %s } }' "${dns_config}" "${license}" "${backup_config}")
|
||||
|
||||
if ! setupResult=$($curl -kq -X POST -H "Content-Type: application/json" -d "${setupData}" https://${ip}/api/v1/cloudron/setup); then
|
||||
echo "Failed to setup with ${setupData} ${setupResult}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
wait_for_status "webadminStatus" '*"tls": true*'
|
||||
|
||||
echo "Cloudron is ready at https://my-${domain}"
|
||||
|
||||
@@ -92,8 +92,9 @@ fi
|
||||
echo "Running cloudron-setup with args : $@" > "${LOG_FILE}"
|
||||
|
||||
# validate arguments in the absence of data
|
||||
readonly AVAILABLE_PROVIDERS="azure, caas, cloudscale, contabo, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, interox, lightsail, linode, netcup, ovh, rosehosting, scaleway, skysilk, time4vps, upcloud, vultr or generic"
|
||||
if [[ -z "${provider}" ]]; then
|
||||
echo "--provider is required (azure, contabo, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, lightsail, linode, netcup, ovh, rosehosting, scaleway, upcloud, vultr or generic)"
|
||||
echo "--provider is required ($AVAILABLE_PROVIDERS)"
|
||||
exit 1
|
||||
elif [[ \
|
||||
"${provider}" != "ami" && \
|
||||
@@ -106,9 +107,10 @@ elif [[ \
|
||||
"${provider}" != "ec2" && \
|
||||
"${provider}" != "exoscale" && \
|
||||
"${provider}" != "galaxygate" && \
|
||||
"${provider}" != "digitalocean" && \
|
||||
"${provider}" != "gce" && \
|
||||
"${provider}" != "hetzner" && \
|
||||
"${provider}" != "interox" && \
|
||||
"${provider}" != "interox-image" && \
|
||||
"${provider}" != "lightsail" && \
|
||||
"${provider}" != "linode" && \
|
||||
"${provider}" != "linode-stackscript" && \
|
||||
@@ -117,12 +119,16 @@ elif [[ \
|
||||
"${provider}" != "ovh" && \
|
||||
"${provider}" != "rosehosting" && \
|
||||
"${provider}" != "scaleway" && \
|
||||
"${provider}" != "skysilk" && \
|
||||
"${provider}" != "skysilk-image" && \
|
||||
"${provider}" != "time4vps" && \
|
||||
"${provider}" != "time4vps-image" && \
|
||||
"${provider}" != "upcloud" && \
|
||||
"${provider}" != "upcloud-image" && \
|
||||
"${provider}" != "vultr" && \
|
||||
"${provider}" != "generic" \
|
||||
]]; then
|
||||
echo "--provider must be one of: azure, cloudscale.ch, contabo, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, lightsail, linode, netcup, ovh, rosehosting, scaleway, upcloud, vultr or generic"
|
||||
echo "--provider must be one of: $AVAILABLE_PROVIDERS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -156,7 +162,7 @@ if [[ "${initBaseImage}" == "true" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! apt-get install curl python3 ubuntu-standard -y &>> "${LOG_FILE}"; then
|
||||
if ! DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" -y install curl python3 ubuntu-standard -y &>> "${LOG_FILE}"; then
|
||||
echo "Could not install setup dependencies (curl). See ${LOG_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
@@ -199,6 +205,10 @@ fi
|
||||
# NOTE: this install script only supports 3.x and above
|
||||
echo "=> Installing version ${version} (this takes some time) ..."
|
||||
mkdir -p /etc/cloudron
|
||||
# this file is used >= 4.2
|
||||
echo "${provider}" > /etc/cloudron/PROVIDER
|
||||
|
||||
# this file is unused <= 4.2 and exists to make legacy installations work. the start script will remove this file anyway
|
||||
cat > "/etc/cloudron/cloudron.conf" <<CONF_END
|
||||
{
|
||||
"apiServerOrigin": "${apiServerOrigin}",
|
||||
@@ -214,6 +224,10 @@ if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" &>> "${LOG_FILE}"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# only needed for >= 4.2
|
||||
mysql -uroot -ppassword -e "REPLACE INTO box.settings (name, value) VALUES ('api_server_origin', '${apiServerOrigin}');" 2>/dev/null
|
||||
mysql -uroot -ppassword -e "REPLACE INTO box.settings (name, value) VALUES ('web_server_origin', '${webServerOrigin}');" 2>/dev/null
|
||||
|
||||
echo -n "=> Waiting for cloudron to be ready (this takes some time) ..."
|
||||
while true; do
|
||||
echo -n "."
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
# This script collects diagnostic information to help debug server related issues
|
||||
# It also enables SSH access for the cloudron support team
|
||||
|
||||
@@ -11,25 +13,33 @@ HELP_MESSAGE="
|
||||
This script collects diagnostic information to help debug server related issues
|
||||
|
||||
Options:
|
||||
--admin-login Login as administrator
|
||||
--enable-ssh Enable SSH access for the Cloudron support team
|
||||
--help Show this message
|
||||
"
|
||||
|
||||
# We require root
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root." > /dev/stderr
|
||||
echo "This script should be run as root. Run with sudo"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
enableSSH="false"
|
||||
|
||||
args=$(getopt -o "" -l "help,enable-ssh" -n "$0" -- "$@")
|
||||
args=$(getopt -o "" -l "help,enable-ssh,admin-login" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--help) echo -e "${HELP_MESSAGE}"; exit 0;;
|
||||
--enable-ssh) enableSSH="true"; shift;;
|
||||
--admin-login)
|
||||
admin_username=$(mysql -NB -uroot -ppassword -e "SELECT username FROM box.users WHERE admin=1 LIMIT 1" 2>/dev/null)
|
||||
admin_password=$(pwgen -1s 12)
|
||||
printf '{"%s":"%s"}\n' "${admin_username}" "${admin_password}" > /tmp/cloudron_ghost.json
|
||||
echo "Login as ${admin_username} / ${admin_password} . Remove /tmp/cloudron_ghost.json when done."
|
||||
exit 0
|
||||
;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
@@ -42,7 +52,7 @@ if [[ "`df --output="avail" / | sed -n 2p`" -lt "10240" ]]; then
|
||||
echo ""
|
||||
df -h
|
||||
echo ""
|
||||
echo "To recover from a full disk, follow the guide at https://cloudron.io/documentation/server/#recovery-after-disk-full"
|
||||
echo "To recover from a full disk, follow the guide at https://cloudron.io/documentation/troubleshooting/#recovery-after-disk-full"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -58,23 +68,8 @@ echo -n "Generating Cloudron Support stats..."
|
||||
# clear file
|
||||
rm -rf $OUT
|
||||
|
||||
ssh_port=$(cat /etc/ssh/sshd_config | grep "Port " | sed -e "s/.*Port //")
|
||||
if [[ $SUDO_USER == "" ]]; then
|
||||
ssh_user="root"
|
||||
ssh_folder="/root/.ssh/"
|
||||
authorized_key_file="${ssh_folder}/authorized_keys"
|
||||
else
|
||||
ssh_user="$SUDO_USER"
|
||||
ssh_folder="/home/$SUDO_USER/.ssh/"
|
||||
authorized_key_file="${ssh_folder}/authorized_keys"
|
||||
fi
|
||||
|
||||
echo -e $LINE"SSH"$LINE >> $OUT
|
||||
echo "Username: ${ssh_user}" >> $OUT
|
||||
echo "Port: ${ssh_port}" >> $OUT
|
||||
|
||||
echo -e $LINE"cloudron.conf"$LINE >> $OUT
|
||||
cat /etc/cloudron/cloudron.conf &>> $OUT
|
||||
echo -e $LINE"PROVIDER"$LINE >> $OUT
|
||||
cat /etc/cloudron/PROVIDER &>> $OUT || true
|
||||
|
||||
echo -e $LINE"Docker container"$LINE >> $OUT
|
||||
if ! timeout --kill-after 10s 15s docker ps -a &>> $OUT 2>&1; then
|
||||
@@ -99,25 +94,50 @@ systemctl status --lines=100 cloudron.target box mysql unbound cloudron-syslog n
|
||||
echo -e $LINE"Box logs"$LINE >> $OUT
|
||||
tail -n 100 /home/yellowtent/platformdata/logs/box.log &>> $OUT
|
||||
|
||||
echo -e $LINE"Firewall chains"$LINE >> $OUT
|
||||
ip addr &>> $OUT
|
||||
|
||||
echo -e $LINE"Firewall chains"$LINE >> $OUT
|
||||
iptables -L &>> $OUT
|
||||
|
||||
echo "Done"
|
||||
|
||||
if [[ "${enableSSH}" == "true" ]]; then
|
||||
ssh_port=$(cat /etc/ssh/sshd_config | grep "Port " | sed -e "s/.*Port //")
|
||||
permit_root_login=$(grep -q ^PermitRootLogin.*yes /etc/ssh/sshd_config && echo "yes" || echo "no")
|
||||
|
||||
# support.js uses similar logic
|
||||
if $(grep -q "ec2\|lightsail\|ami" /etc/cloudron/PROVIDER); then
|
||||
ssh_user="ubuntu"
|
||||
keys_file="/home/ubuntu/.ssh/authorized_keys"
|
||||
else
|
||||
ssh_user="root"
|
||||
keys_file="/root/.ssh/authorized_keys"
|
||||
fi
|
||||
|
||||
echo -e $LINE"SSH"$LINE >> $OUT
|
||||
echo "Username: ${ssh_user}" >> $OUT
|
||||
echo "Port: ${ssh_port}" >> $OUT
|
||||
echo "PermitRootLogin: ${permit_root_login}" >> $OUT
|
||||
echo "Key file: ${keys_file}" >> $OUT
|
||||
|
||||
echo -n "Enabling ssh access for the Cloudron support team..."
|
||||
mkdir -p $(dirname "${keys_file}") # .ssh does not exist sometimes
|
||||
touch "${keys_file}" # required for concat to work
|
||||
if ! grep -q "${CLOUDRON_SUPPORT_PUBLIC_KEY}" "${keys_file}"; then
|
||||
echo -e "\n${CLOUDRON_SUPPORT_PUBLIC_KEY}" >> "${keys_file}"
|
||||
chmod 600 "${keys_file}"
|
||||
chown "${ssh_user}" "${keys_file}"
|
||||
fi
|
||||
|
||||
echo "Done"
|
||||
fi
|
||||
|
||||
echo -n "Uploading information..."
|
||||
# for some reason not using $(cat $OUT) will not contain newlines!?
|
||||
paste_key=$(curl -X POST ${PASTEBIN}/documents --silent -d "$(cat $OUT)" | python3 -c "import sys, json; print(json.load(sys.stdin)['key'])")
|
||||
echo "Done"
|
||||
|
||||
if [[ "${enableSSH}" == "true" ]]; then
|
||||
echo -n "Enabling ssh access for the Cloudron support team..."
|
||||
mkdir -p "${ssh_folder}"
|
||||
echo -e "\n${CLOUDRON_SUPPORT_PUBLIC_KEY}" >> ${authorized_key_file}
|
||||
chown -R ${ssh_user} "${ssh_folder}"
|
||||
chmod 600 "${authorized_key_file}"
|
||||
echo "Done"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Please email the following link to support@cloudron.io"
|
||||
echo ""
|
||||
|
||||
@@ -108,11 +108,6 @@ systemctl restart unbound
|
||||
# ensure cloudron-syslog runs
|
||||
systemctl restart cloudron-syslog
|
||||
|
||||
$json -f /etc/cloudron/cloudron.conf -I -e "delete this.edition" # can be removed after 4.0
|
||||
|
||||
echo "==> Clearing custom.yml"
|
||||
rm -f /etc/cloudron/custom.yml
|
||||
|
||||
echo "==> Configuring sudoers"
|
||||
rm -f /etc/sudoers.d/${USER}
|
||||
cp "${script_dir}/start/sudoers" /etc/sudoers.d/${USER}
|
||||
@@ -127,8 +122,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
|
||||
rm -f "${PLATFORM_DATA_DIR}/logrotate.d/"*
|
||||
cp "${script_dir}/start/logrotate/"* "${PLATFORM_DATA_DIR}/logrotate.d/"
|
||||
rm -f "${PLATFORM_DATA_DIR}/logrotate.d/box-logrotate" "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate" # remove pre 3.6 config files
|
||||
|
||||
# logrotate files have to be owned by root, this is here to fixup existing installations where we were resetting the owner to yellowtent
|
||||
chown root:root "${PLATFORM_DATA_DIR}/logrotate.d/"
|
||||
@@ -174,7 +169,13 @@ mysqladmin -u root -ppassword password password # reset default root password
|
||||
mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
|
||||
|
||||
echo "==> Migrating data"
|
||||
(cd "${BOX_SRC_DIR}" && BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up)
|
||||
cd "${BOX_SRC_DIR}"
|
||||
if ! BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up; then
|
||||
echo "DB migration failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -f /etc/cloudron/cloudron.conf
|
||||
|
||||
if [[ ! -f "${BOX_DATA_DIR}/dhparams.pem" ]]; then
|
||||
echo "==> Generating dhparams (takes forever)"
|
||||
|
||||
@@ -240,8 +240,23 @@ LoadPlugin write_graphite
|
||||
Interactive false
|
||||
|
||||
Import "df"
|
||||
# <Module df>
|
||||
# </Module>
|
||||
|
||||
Import "du"
|
||||
<Module du>
|
||||
<Path>
|
||||
Instance maildata
|
||||
Dir "/home/yellowtent/boxdata/mail"
|
||||
</Path>
|
||||
<Path>
|
||||
Instance boxdata
|
||||
Dir "/home/yellowtent/boxdata"
|
||||
Exclude "mail"
|
||||
</Path>
|
||||
<Path>
|
||||
Instance platformdata
|
||||
Dir "/home/yellowtent/platformdata"
|
||||
</Path>
|
||||
</Module>
|
||||
</Plugin>
|
||||
|
||||
<Plugin write_graphite>
|
||||
|
||||
@@ -21,6 +21,7 @@ def read():
|
||||
except:
|
||||
continue
|
||||
|
||||
# type comes from https://github.com/collectd/collectd/blob/master/src/types.db
|
||||
val = collectd.Values(type='df_complex', plugin='df', plugin_instance=instance)
|
||||
|
||||
free = st.f_bavail * st.f_frsize # bavail is for non-root user. bfree is total
|
||||
|
||||
79
setup/start/collectd/du.py
Normal file
79
setup/start/collectd/du.py
Normal file
@@ -0,0 +1,79 @@
|
||||
import collectd,os,subprocess,sys,re,time
|
||||
|
||||
# https://www.programcreek.com/python/example/106897/collectd.register_read
|
||||
|
||||
PATHS = [] # { name, dir, exclude }
|
||||
INTERVAL = 60 * 60 * 12 # twice a day. change values in docker-graphite if you change this
|
||||
|
||||
def du(pathinfo):
|
||||
cmd = 'timeout 1800 du -Dsb "{}"'.format(pathinfo['dir'])
|
||||
if pathinfo['exclude'] != '':
|
||||
cmd += ' --exclude "{}"'.format(pathinfo['exclude'])
|
||||
|
||||
collectd.info('computing size with command: %s' % cmd);
|
||||
try:
|
||||
size = subprocess.check_output(cmd, shell=True).split()[0].decode('utf-8')
|
||||
collectd.info('\tsize of %s is %s (time: %i)' % (pathinfo['dir'], size, int(time.time())))
|
||||
return size
|
||||
except Exception as e:
|
||||
collectd.info('\terror getting the size of %s: %s' % (pathinfo['dir'], str(e)))
|
||||
return 0
|
||||
|
||||
def parseSize(size):
|
||||
units = {"B": 1, "KB": 10**3, "MB": 10**6, "GB": 10**9, "TB": 10**12}
|
||||
number, unit, _ = re.split('([a-zA-Z]+)', size.upper())
|
||||
return int(float(number)*units[unit])
|
||||
|
||||
def dockerSize():
|
||||
# use --format '{{json .}}' to dump the string. '{{if eq .Type "Images"}}{{.Size}}{{end}}' still creates newlines
|
||||
cmd = 'timeout 1800 docker system df --format "{{.Size}}" | head -n1'
|
||||
try:
|
||||
size = subprocess.check_output(cmd, shell=True).strip().decode('utf-8')
|
||||
collectd.info('size of docker images is %s (%s) (time: %i)' % (size, parseSize(size), int(time.time())))
|
||||
return parseSize(size)
|
||||
except Exception as e:
|
||||
collectd.info('error getting docker images size : %s' % str(e))
|
||||
return 0
|
||||
|
||||
# configure is called for each module block. this is called before init
|
||||
def configure(config):
|
||||
global PATHS
|
||||
|
||||
for child in config.children:
|
||||
if child.key != 'Path':
|
||||
collectd.info('du plugin: Unknown config key "%s"' % key)
|
||||
continue
|
||||
|
||||
pathinfo = { 'name': '', 'dir': '', 'exclude': '' }
|
||||
for node in child.children:
|
||||
if node.key == 'Instance':
|
||||
pathinfo['name'] = node.values[0]
|
||||
elif node.key == 'Dir':
|
||||
pathinfo['dir'] = node.values[0]
|
||||
elif node.key == 'Exclude':
|
||||
pathinfo['exclude'] = node.values[0]
|
||||
|
||||
PATHS.append(pathinfo);
|
||||
collectd.info('du plugin: monitoring %s' % pathinfo['dir']);
|
||||
|
||||
def init():
|
||||
global PATHS
|
||||
collectd.info('custom du plugin initialized with %s %s' % (PATHS, sys.version))
|
||||
|
||||
def read():
|
||||
for pathinfo in PATHS:
|
||||
size = du(pathinfo)
|
||||
|
||||
# type comes from https://github.com/collectd/collectd/blob/master/src/types.db
|
||||
val = collectd.Values(type='capacity', plugin='du', plugin_instance=pathinfo['name'])
|
||||
val.dispatch(values=[size], type_instance='usage')
|
||||
|
||||
size = dockerSize()
|
||||
val = collectd.Values(type='capacity', plugin='du', plugin_instance='docker')
|
||||
val.dispatch(values=[size], type_instance='usage')
|
||||
|
||||
|
||||
|
||||
collectd.register_init(init)
|
||||
collectd.register_config(configure)
|
||||
collectd.register_read(read, INTERVAL)
|
||||
@@ -1,16 +1,40 @@
|
||||
# add customizations here
|
||||
# after making changes run "sudo systemctl restart box"
|
||||
|
||||
# features:
|
||||
# configureBackup: true
|
||||
# appstore:
|
||||
# blacklist:
|
||||
# - io.wekan.cloudronapp
|
||||
# - io.cloudron.openvpn
|
||||
# whitelist:
|
||||
# org.wordpress.cloudronapp: {}
|
||||
# chat.rocket.cloudronapp: {}
|
||||
# com.nextcloud.cloudronapp: {}
|
||||
#
|
||||
# backups:
|
||||
# configurable: true
|
||||
#
|
||||
# domains:
|
||||
# dynamicDns: true
|
||||
# subscription: true
|
||||
# remoteSupport: true
|
||||
#
|
||||
# changeDashboardDomain: true
|
||||
#
|
||||
# subscription:
|
||||
# configurable: true
|
||||
#
|
||||
# support:
|
||||
# email: support@cloudron.io
|
||||
#
|
||||
# remoteSupport: true
|
||||
#
|
||||
# ticketFormBody: |
|
||||
# Use this form to open support tickets. You can also write directly to [support@cloudron.io](mailto:support@cloudron.io).
|
||||
# * [Knowledge Base & App Docs](https://cloudron.io/documentation/apps/?support_view)
|
||||
# * [Custom App Packaging & API](https://cloudron.io/developer/packaging/?support_view)
|
||||
# * [Forum](https://forum.cloudron.io/)
|
||||
#
|
||||
# submitTickets: true
|
||||
#
|
||||
# alerts:
|
||||
# email: support@cloudron.io
|
||||
# notifyCloudronAdmins: false
|
||||
|
||||
#
|
||||
# footer:
|
||||
# body: '© 2019 [Cloudron](https://cloudron.io) [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)'
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
# logrotate config for app, crash, addon and task logs
|
||||
|
||||
# man 7 glob
|
||||
/home/yellowtent/platformdata/logs/[!t][!a][!s][!k][!s]/*.log {
|
||||
# only keep one rotated file, we currently do not send that over the api
|
||||
rotate 1
|
||||
size 10M
|
||||
# we never compress so we can simply tail the files
|
||||
nocompress
|
||||
copytruncate
|
||||
}
|
||||
|
||||
/home/yellowtent/platformdata/logs/tasks/*.log {
|
||||
monthly
|
||||
rotate 0
|
||||
missingok
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# logrotate config for box logs
|
||||
|
||||
# keep upto 5 logs of size 10M each
|
||||
/home/yellowtent/platformdata/logs/box.log {
|
||||
rotate 10
|
||||
rotate 5
|
||||
size 10M
|
||||
# we never compress so we can simply tail the files
|
||||
nocompress
|
||||
|
||||
31
setup/start/logrotate/platform
Normal file
31
setup/start/logrotate/platform
Normal file
@@ -0,0 +1,31 @@
|
||||
# logrotate config for app, crash, addon and task logs
|
||||
|
||||
# man 7 glob
|
||||
/home/yellowtent/platformdata/logs/graphite/*.log
|
||||
/home/yellowtent/platformdata/logs/mail/*.log
|
||||
/home/yellowtent/platformdata/logs/mysql/*.log
|
||||
/home/yellowtent/platformdata/logs/mongodb/*.log
|
||||
/home/yellowtent/platformdata/logs/postgresql/*.log
|
||||
/home/yellowtent/platformdata/logs/sftp/*.log
|
||||
/home/yellowtent/platformdata/logs/redis-*/*.log
|
||||
/home/yellowtent/platformdata/logs/crash/*.log
|
||||
/home/yellowtent/platformdata/logs/updater/*.log {
|
||||
# only keep one rotated file, we currently do not send that over the api
|
||||
rotate 1
|
||||
size 10M
|
||||
missingok
|
||||
# we never compress so we can simply tail the files
|
||||
nocompress
|
||||
copytruncate
|
||||
}
|
||||
|
||||
# keep task logs for a week. the 'nocreate' option ensures empty log files are not
|
||||
# created post rotation
|
||||
/home/yellowtent/platformdata/logs/tasks/*.log {
|
||||
minage 7
|
||||
daily
|
||||
rotate 0
|
||||
missingok
|
||||
nocreate
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:accesscontrol'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
@@ -130,6 +129,8 @@ function validateToken(accessToken, callback) {
|
||||
if (error && error.reason === UsersError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
|
||||
if (error) return callback(error);
|
||||
|
||||
if (!user.active) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
|
||||
|
||||
scopesForUser(user, function (error, userScopes) {
|
||||
if (error) return callback(error);
|
||||
|
||||
|
||||
294
src/addons.js
294
src/addons.js
@@ -36,16 +36,17 @@ var accesscontrol = require('./accesscontrol.js'),
|
||||
apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
clients = require('./clients.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
ClientsError = clients.ClientsError,
|
||||
crypto = require('crypto'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:addons'),
|
||||
docker = require('./docker.js'),
|
||||
dockerConnection = docker.connection,
|
||||
DockerError = docker.DockerError,
|
||||
fs = require('fs'),
|
||||
graphs = require('./graphs.js'),
|
||||
hat = require('./hat.js'),
|
||||
infra = require('./infra_version.js'),
|
||||
mail = require('./mail.js'),
|
||||
@@ -57,6 +58,7 @@ var accesscontrol = require('./accesscontrol.js'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
settings = require('./settings.js'),
|
||||
sftp = require('./sftp.js'),
|
||||
shell = require('./shell.js'),
|
||||
spawn = require('child_process').spawn,
|
||||
split = require('split'),
|
||||
@@ -269,6 +271,10 @@ function restartContainer(serviceName, callback) {
|
||||
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
|
||||
|
||||
docker.startContainer(serviceName, function (error) {
|
||||
if (error && error.reason === BoxError.NOT_FOUND) {
|
||||
callback(null); // callback early since rebuilding takes long
|
||||
return rebuildService(serviceName, function (error) { if (error) console.error(`Unable to rebuild service ${serviceName}`, error); });
|
||||
}
|
||||
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
@@ -276,13 +282,31 @@ function restartContainer(serviceName, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function rebuildService(serviceName, callback) {
|
||||
assert.strictEqual(typeof serviceName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
assert(KNOWN_SERVICES[serviceName], `Unknown service ${serviceName}`);
|
||||
|
||||
// this attempts to recreate the service docker container if they don't exist but platform infra version is unchanged
|
||||
// passing an infra version of 'none' will not attempt to purge existing data, not sure if this is good or bad
|
||||
if (serviceName === 'mongodb') return startMongodb({ version: 'none' }, callback);
|
||||
if (serviceName === 'postgresql') return startPostgresql({ version: 'none' }, callback);
|
||||
if (serviceName === 'mysql') return startMysql({ version: 'none' }, callback);
|
||||
if (serviceName === 'sftp') return sftp.startSftp({ version: 'none' }, callback);
|
||||
if (serviceName === 'graphite') return graphs.startGraphite({ version: 'none' }, callback);
|
||||
|
||||
// nothing to rebuild for now
|
||||
callback();
|
||||
}
|
||||
|
||||
function getServiceDetails(containerName, tokenEnvName, callback) {
|
||||
assert.strictEqual(typeof containerName, 'string');
|
||||
assert.strictEqual(typeof tokenEnvName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
docker.inspect(containerName, function (error, result) {
|
||||
if (error && error.reason === DockerError.NOT_FOUND) return callback(new AddonsError(AddonsError.NOT_ACTIVE, error));
|
||||
if (error && error.reason === BoxError.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);
|
||||
@@ -364,7 +388,7 @@ function getService(serviceName, callback) {
|
||||
}
|
||||
|
||||
KNOWN_SERVICES[serviceName].status(function (error, result) {
|
||||
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(error);
|
||||
|
||||
tmp.status = result.status;
|
||||
tmp.memoryUsed = result.memoryUsed;
|
||||
@@ -620,7 +644,9 @@ function importDatabase(addon, callback) {
|
||||
if (!error) return iteratorCallback();
|
||||
|
||||
debug(`importDatabase: Error importing ${addon} of app ${app.id}. Marking as errored`, error);
|
||||
appdb.update(app.id, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message }, iteratorCallback);
|
||||
// FIXME: there is no way to 'repair' if we are here. we need to make a separate apptask that re-imports db
|
||||
// not clear, if repair workflow should be part of addon or per-app
|
||||
appdb.update(app.id, { installationState: apps.ISTATE_ERROR, error: { message: error.message } }, iteratorCallback);
|
||||
});
|
||||
}, callback);
|
||||
});
|
||||
@@ -685,7 +711,7 @@ function getEnvironment(app, callback) {
|
||||
appdb.getAddonConfigByAppId(app.id, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (app.manifest.addons['docker']) result.push({ name: 'DOCKER_HOST', value: `tcp://172.18.0.1:${config.get('dockerProxyPort')}` });
|
||||
if (app.manifest.addons['docker']) result.push({ name: 'DOCKER_HOST', value: `tcp://172.18.0.1:${constants.DOCKER_PROXY_PORT}` });
|
||||
|
||||
return callback(null, result.map(function (e) { return e.name + '=' + e.value; }));
|
||||
});
|
||||
@@ -795,10 +821,12 @@ function setupOauth(app, options, callback) {
|
||||
clients.add(appId, clients.TYPE_OAUTH, redirectURI, scope, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
var env = [
|
||||
{ name: 'OAUTH_CLIENT_ID', value: result.id },
|
||||
{ name: 'OAUTH_CLIENT_SECRET', value: result.clientSecret },
|
||||
{ name: 'OAUTH_ORIGIN', value: config.adminOrigin() }
|
||||
{ name: `${envPrefix}OAUTH_CLIENT_ID`, value: result.id },
|
||||
{ name: `${envPrefix}OAUTH_CLIENT_SECRET`, value: result.clientSecret },
|
||||
{ name: `${envPrefix}OAUTH_ORIGIN`, value: settings.adminOrigin() }
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting oauth addon config to %j', env);
|
||||
@@ -832,17 +860,19 @@ function setupEmail(app, options, callback) {
|
||||
|
||||
const mailInDomains = mailDomains.filter(function (d) { return d.enabled; }).map(function (d) { return d.domain; }).join(',');
|
||||
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
// note that "external" access info can be derived from MAIL_DOMAIN (since it's part of user documentation)
|
||||
var env = [
|
||||
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_SMTP_PORT', value: '2525' },
|
||||
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_IMAP_PORT', value: '9993' },
|
||||
{ name: 'MAIL_SIEVE_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_SIEVE_PORT', value: '4190' },
|
||||
{ name: 'MAIL_DOMAIN', value: app.domain },
|
||||
{ name: 'MAIL_DOMAINS', value: mailInDomains },
|
||||
{ name: 'LDAP_MAILBOXES_BASE_DN', value: 'ou=mailboxes,dc=cloudron' }
|
||||
{ name: `${envPrefix}MAIL_SMTP_SERVER`, value: 'mail' },
|
||||
{ name: `${envPrefix}MAIL_SMTP_PORT`, value: '2525' },
|
||||
{ name: `${envPrefix}MAIL_IMAP_SERVER`, value: 'mail' },
|
||||
{ name: `${envPrefix}MAIL_IMAP_PORT`, value: '9993' },
|
||||
{ name: `${envPrefix}MAIL_SIEVE_SERVER`, value: 'mail' },
|
||||
{ name: `${envPrefix}MAIL_SIEVE_PORT`, value: '4190' },
|
||||
{ name: `${envPrefix}MAIL_DOMAIN`, value: app.domain },
|
||||
{ name: `${envPrefix}MAIL_DOMAINS`, value: mailInDomains },
|
||||
{ name: `${envPrefix}LDAP_MAILBOXES_BASE_DN`, value: 'ou=mailboxes,dc=cloudron' }
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting up Email');
|
||||
@@ -868,14 +898,16 @@ function setupLdap(app, options, callback) {
|
||||
|
||||
if (!app.sso) return callback(null);
|
||||
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
var env = [
|
||||
{ name: 'LDAP_SERVER', value: '172.18.0.1' },
|
||||
{ name: 'LDAP_PORT', value: '' + config.get('ldapPort') },
|
||||
{ name: 'LDAP_URL', value: 'ldap://172.18.0.1:' + config.get('ldapPort') },
|
||||
{ name: 'LDAP_USERS_BASE_DN', value: 'ou=users,dc=cloudron' },
|
||||
{ name: 'LDAP_GROUPS_BASE_DN', value: 'ou=groups,dc=cloudron' },
|
||||
{ name: 'LDAP_BIND_DN', value: 'cn='+ app.id + ',ou=apps,dc=cloudron' },
|
||||
{ name: 'LDAP_BIND_PASSWORD', value: hat(4 * 128) } // this is ignored
|
||||
{ name: `${envPrefix}LDAP_SERVER`, value: '172.18.0.1' },
|
||||
{ name: `${envPrefix}LDAP_PORT`, value: '' + constants.LDAP_PORT },
|
||||
{ name: `${envPrefix}LDAP_URL`, value: 'ldap://172.18.0.1:' + constants.LDAP_PORT },
|
||||
{ name: `${envPrefix}LDAP_USERS_BASE_DN`, value: 'ou=users,dc=cloudron' },
|
||||
{ name: `${envPrefix}LDAP_GROUPS_BASE_DN`, value: 'ou=groups,dc=cloudron' },
|
||||
{ name: `${envPrefix}LDAP_BIND_DN`, value: 'cn='+ app.id + ',ou=apps,dc=cloudron' },
|
||||
{ name: `${envPrefix}LDAP_BIND_PASSWORD`, value: hat(4 * 128) } // this is ignored
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting up LDAP');
|
||||
@@ -900,19 +932,21 @@ function setupSendMail(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Setting up SendMail');
|
||||
|
||||
appdb.getAddonConfigByName(app.id, 'sendmail', 'MAIL_SMTP_PASSWORD', function (error, existingPassword) {
|
||||
appdb.getAddonConfigByName(app.id, 'sendmail', '%MAIL_SMTP_PASSWORD', function (error, existingPassword) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
|
||||
var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length
|
||||
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
var env = [
|
||||
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_SMTP_PORT', value: '2525' },
|
||||
{ name: 'MAIL_SMTPS_PORT', value: '2465' },
|
||||
{ name: 'MAIL_SMTP_USERNAME', value: app.mailboxName + '@' + app.domain },
|
||||
{ name: 'MAIL_SMTP_PASSWORD', value: password },
|
||||
{ name: 'MAIL_FROM', value: app.mailboxName + '@' + app.domain },
|
||||
{ name: 'MAIL_DOMAIN', value: app.domain }
|
||||
{ name: `${envPrefix}MAIL_SMTP_SERVER`, value: 'mail' },
|
||||
{ name: `${envPrefix}MAIL_SMTP_PORT`, value: '2525' },
|
||||
{ name: `${envPrefix}MAIL_SMTPS_PORT`, value: '2465' },
|
||||
{ name: `${envPrefix}MAIL_SMTP_USERNAME`, value: app.mailboxName + '@' + app.domain },
|
||||
{ name: `${envPrefix}MAIL_SMTP_PASSWORD`, value: password },
|
||||
{ name: `${envPrefix}MAIL_FROM`, value: app.mailboxName + '@' + app.domain },
|
||||
{ name: `${envPrefix}MAIL_DOMAIN`, value: app.domain }
|
||||
];
|
||||
debugApp(app, 'Setting sendmail addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
|
||||
@@ -936,18 +970,20 @@ function setupRecvMail(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Setting up recvmail');
|
||||
|
||||
appdb.getAddonConfigByName(app.id, 'recvmail', 'MAIL_IMAP_PASSWORD', function (error, existingPassword) {
|
||||
appdb.getAddonConfigByName(app.id, 'recvmail', '%MAIL_IMAP_PASSWORD', function (error, existingPassword) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
|
||||
var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length
|
||||
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
var env = [
|
||||
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_IMAP_PORT', value: '9993' },
|
||||
{ name: 'MAIL_IMAP_USERNAME', value: app.mailboxName + '@' + app.domain },
|
||||
{ name: 'MAIL_IMAP_PASSWORD', value: password },
|
||||
{ name: 'MAIL_TO', value: app.mailboxName + '@' + app.domain },
|
||||
{ name: 'MAIL_DOMAIN', value: app.domain }
|
||||
{ name: `${envPrefix}MAIL_IMAP_SERVER`, value: 'mail' },
|
||||
{ name: `${envPrefix}MAIL_IMAP_PORT`, value: '9993' },
|
||||
{ name: `${envPrefix}MAIL_IMAP_USERNAME`, value: app.mailboxName + '@' + app.domain },
|
||||
{ name: `${envPrefix}MAIL_IMAP_PASSWORD`, value: password },
|
||||
{ name: `${envPrefix}MAIL_TO`, value: app.mailboxName + '@' + app.domain },
|
||||
{ name: `${envPrefix}MAIL_DOMAIN`, value: app.domain }
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting sendmail addon config to %j', env);
|
||||
@@ -992,6 +1028,7 @@ function startMysql(existingInfra, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const cmd = `docker run --restart=always -d --name="mysql" \
|
||||
--hostname mysql \
|
||||
--net cloudron \
|
||||
--net-alias mysql \
|
||||
--log-driver syslog \
|
||||
@@ -1029,7 +1066,7 @@ function setupMySql(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Setting up mysql');
|
||||
|
||||
appdb.getAddonConfigByName(app.id, 'mysql', 'MYSQL_PASSWORD', function (error, existingPassword) {
|
||||
appdb.getAddonConfigByName(app.id, 'mysql', '%MYSQL_PASSWORD', function (error, existingPassword) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
|
||||
const tmp = mysqlDatabaseName(app.id);
|
||||
@@ -1048,19 +1085,21 @@ function setupMySql(app, options, callback) {
|
||||
if (error) return callback(new Error('Error setting up mysql: ' + error));
|
||||
if (response.statusCode !== 201) return callback(new Error(`Error setting up mysql. Status code: ${response.statusCode} message: ${response.body.message}`));
|
||||
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
var env = [
|
||||
{ name: 'MYSQL_USERNAME', value: data.username },
|
||||
{ name: 'MYSQL_PASSWORD', value: data.password },
|
||||
{ name: 'MYSQL_HOST', value: 'mysql' },
|
||||
{ name: 'MYSQL_PORT', value: '3306' }
|
||||
{ name: `${envPrefix}MYSQL_USERNAME`, value: data.username },
|
||||
{ name: `${envPrefix}MYSQL_PASSWORD`, value: data.password },
|
||||
{ name: `${envPrefix}MYSQL_HOST`, value: 'mysql' },
|
||||
{ name: `${envPrefix}MYSQL_PORT`, value: '3306' }
|
||||
];
|
||||
|
||||
if (options.multipleDatabases) {
|
||||
env = env.concat({ name: 'MYSQL_DATABASE_PREFIX', value: `${data.prefix}_` });
|
||||
env = env.concat({ name: `${envPrefix}MYSQL_DATABASE_PREFIX`, value: `${data.prefix}_` });
|
||||
} else {
|
||||
env = env.concat(
|
||||
{ name: 'MYSQL_URL', value: `mysql://${data.username}:${data.password}@mysql/${data.database}` },
|
||||
{ name: 'MYSQL_DATABASE', value: data.database }
|
||||
{ name: `${envPrefix}MYSQL_URL`, value: `mysql://${data.username}:${data.password}@mysql/${data.database}` },
|
||||
{ name: `${envPrefix}MYSQL_DATABASE`, value: data.database }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1204,6 +1243,7 @@ function startPostgresql(existingInfra, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const cmd = `docker run --restart=always -d --name="postgresql" \
|
||||
--hostname postgresql \
|
||||
--net cloudron \
|
||||
--net-alias postgresql \
|
||||
--log-driver syslog \
|
||||
@@ -1242,7 +1282,7 @@ function setupPostgreSql(app, options, callback) {
|
||||
|
||||
const { database, username } = postgreSqlNames(app.id);
|
||||
|
||||
appdb.getAddonConfigByName(app.id, 'postgresql', 'POSTGRESQL_PASSWORD', function (error, existingPassword) {
|
||||
appdb.getAddonConfigByName(app.id, 'postgresql', '%POSTGRESQL_PASSWORD', function (error, existingPassword) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
|
||||
const data = {
|
||||
@@ -1258,13 +1298,15 @@ function setupPostgreSql(app, options, callback) {
|
||||
if (error) return callback(new Error('Error setting up postgresql: ' + error));
|
||||
if (response.statusCode !== 201) return callback(new Error(`Error setting up postgresql. Status code: ${response.statusCode} message: ${response.body.message}`));
|
||||
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
var env = [
|
||||
{ name: 'POSTGRESQL_URL', value: `postgres://${data.username}:${data.password}@postgresql/${data.database}` },
|
||||
{ name: 'POSTGRESQL_USERNAME', value: data.username },
|
||||
{ name: 'POSTGRESQL_PASSWORD', value: data.password },
|
||||
{ name: 'POSTGRESQL_HOST', value: 'postgresql' },
|
||||
{ name: 'POSTGRESQL_PORT', value: '5432' },
|
||||
{ name: 'POSTGRESQL_DATABASE', value: data.database }
|
||||
{ name: `${envPrefix}POSTGRESQL_URL`, value: `postgres://${data.username}:${data.password}@postgresql/${data.database}` },
|
||||
{ name: `${envPrefix}POSTGRESQL_USERNAME`, value: data.username },
|
||||
{ name: `${envPrefix}POSTGRESQL_PASSWORD`, value: data.password },
|
||||
{ name: `${envPrefix}POSTGRESQL_HOST`, value: 'postgresql' },
|
||||
{ name: `${envPrefix}POSTGRESQL_PORT`, value: '5432' },
|
||||
{ name: `${envPrefix}POSTGRESQL_DATABASE`, value: data.database }
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting postgresql addon config to %j', env);
|
||||
@@ -1378,6 +1420,7 @@ function startMongodb(existingInfra, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const cmd = `docker run --restart=always -d --name="mongodb" \
|
||||
--hostname mongodb \
|
||||
--net cloudron \
|
||||
--net-alias mongodb \
|
||||
--log-driver syslog \
|
||||
@@ -1414,13 +1457,14 @@ function setupMongoDb(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Setting up mongodb');
|
||||
|
||||
appdb.getAddonConfigByName(app.id, 'mongodb', 'MONGODB_PASSWORD', function (error, existingPassword) {
|
||||
appdb.getAddonConfigByName(app.id, 'mongodb', '%MONGODB_PASSWORD', function (error, existingPassword) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
|
||||
const data = {
|
||||
database: app.id,
|
||||
username: app.id,
|
||||
password: error ? hat(4 * 128) : existingPassword
|
||||
password: error ? hat(4 * 128) : existingPassword,
|
||||
oplog: !!options.oplog
|
||||
};
|
||||
|
||||
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
||||
@@ -1430,15 +1474,21 @@ function setupMongoDb(app, options, callback) {
|
||||
if (error) return callback(new Error('Error setting up mongodb: ' + error));
|
||||
if (response.statusCode !== 201) return callback(new Error(`Error setting up mongodb. Status code: ${response.statusCode}`));
|
||||
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
var env = [
|
||||
{ name: 'MONGODB_URL', value : `mongodb://${data.username}:${data.password}@mongodb/${data.database}` },
|
||||
{ name: 'MONGODB_USERNAME', value : data.username },
|
||||
{ name: 'MONGODB_PASSWORD', value: data.password },
|
||||
{ name: 'MONGODB_HOST', value : 'mongodb' },
|
||||
{ name: 'MONGODB_PORT', value : '27017' },
|
||||
{ name: 'MONGODB_DATABASE', value : data.database }
|
||||
{ name: `${envPrefix}MONGODB_URL`, value : `mongodb://${data.username}:${data.password}@mongodb:27017/${data.database}` },
|
||||
{ name: `${envPrefix}MONGODB_USERNAME`, value : data.username },
|
||||
{ name: `${envPrefix}MONGODB_PASSWORD`, value: data.password },
|
||||
{ name: `${envPrefix}MONGODB_HOST`, value : 'mongodb' },
|
||||
{ name: `${envPrefix}MONGODB_PORT`, value : '27017' },
|
||||
{ name: `${envPrefix}MONGODB_DATABASE`, value : data.database }
|
||||
];
|
||||
|
||||
if (options.oplog) {
|
||||
env.push({ name: `${envPrefix}MONGODB_OPLOG_URL`, value : `mongodb://${data.username}:${data.password}@mongodb:27017/local?authSource=${data.database}` });
|
||||
}
|
||||
|
||||
debugApp(app, 'Setting mongodb addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'mongodb', env, callback);
|
||||
});
|
||||
@@ -1545,65 +1595,69 @@ function setupRedis(app, options, callback) {
|
||||
|
||||
const redisName = 'redis-' + app.id;
|
||||
|
||||
docker.inspect(redisName, function (error, result) {
|
||||
if (!error) {
|
||||
debug(`Re-using existing redis container with state: ${result.State}`);
|
||||
return callback();
|
||||
appdb.getAddonConfigByName(app.id, 'redis', '%REDIS_PASSWORD', function (error, existingPassword) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
|
||||
const redisPassword = error ? hat(4 * 48) : existingPassword; // see box#362 for password length
|
||||
const redisServiceToken = hat(4 * 48);
|
||||
|
||||
// Compute redis memory limit based on app's memory limit (this is arbitrary)
|
||||
var memoryLimit = app.memoryLimit || app.manifest.memoryLimit || 0;
|
||||
|
||||
if (memoryLimit === -1) { // unrestricted (debug mode)
|
||||
memoryLimit = 0;
|
||||
} else if (memoryLimit === 0 || memoryLimit <= (2 * 1024 * 1024 * 1024)) { // less than 2G (ram+swap)
|
||||
memoryLimit = 150 * 1024 * 1024; // 150m
|
||||
} else {
|
||||
memoryLimit = 600 * 1024 * 1024; // 600m
|
||||
}
|
||||
|
||||
appdb.getAddonConfigByName(app.id, 'redis', 'REDIS_PASSWORD', function (error, existingPassword) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
const tag = infra.images.redis.tag;
|
||||
const label = app.fqdn;
|
||||
// note that we do not add appId label because this interferes with the stop/start app logic
|
||||
const cmd = `docker run --restart=always -d --name=${redisName} \
|
||||
--hostname ${redisName} \
|
||||
--label=location=${label} \
|
||||
--net cloudron \
|
||||
--net-alias ${redisName} \
|
||||
--log-driver syslog \
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag="${redisName}" \
|
||||
-m ${memoryLimit/2} \
|
||||
--memory-swap ${memoryLimit} \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
-e CLOUDRON_REDIS_PASSWORD="${redisPassword}" \
|
||||
-e CLOUDRON_REDIS_TOKEN="${redisServiceToken}" \
|
||||
-v "${paths.PLATFORM_DATA_DIR}/redis/${app.id}:/var/lib/redis" \
|
||||
--label isCloudronManaged=true \
|
||||
--read-only -v /tmp -v /run ${tag}`;
|
||||
|
||||
const redisPassword = error ? hat(4 * 48) : existingPassword; // see box#362 for password length
|
||||
const redisServiceToken = hat(4 * 48);
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
// Compute redis memory limit based on app's memory limit (this is arbitrary)
|
||||
var memoryLimit = app.memoryLimit || app.manifest.memoryLimit || 0;
|
||||
var env = [
|
||||
{ name: `${envPrefix}REDIS_URL`, value: 'redis://redisuser:' + redisPassword + '@redis-' + app.id },
|
||||
{ name: `${envPrefix}REDIS_PASSWORD`, value: redisPassword },
|
||||
{ name: `${envPrefix}REDIS_HOST`, value: redisName },
|
||||
{ name: `${envPrefix}REDIS_PORT`, value: '6379' }
|
||||
];
|
||||
|
||||
if (memoryLimit === -1) { // unrestricted (debug mode)
|
||||
memoryLimit = 0;
|
||||
} else if (memoryLimit === 0 || memoryLimit <= (2 * 1024 * 1024 * 1024)) { // less than 2G (ram+swap)
|
||||
memoryLimit = 150 * 1024 * 1024; // 150m
|
||||
} else {
|
||||
memoryLimit = 600 * 1024 * 1024; // 600m
|
||||
}
|
||||
|
||||
const tag = infra.images.redis.tag;
|
||||
const label = app.fqdn;
|
||||
// note that we do not add appId label because this interferes with the stop/start app logic
|
||||
const cmd = `docker run --restart=always -d --name=${redisName} \
|
||||
--label=location=${label} \
|
||||
--net cloudron \
|
||||
--net-alias ${redisName} \
|
||||
--log-driver syslog \
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag="${redisName}" \
|
||||
-m ${memoryLimit/2} \
|
||||
--memory-swap ${memoryLimit} \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
-e CLOUDRON_REDIS_PASSWORD="${redisPassword}" \
|
||||
-e CLOUDRON_REDIS_TOKEN="${redisServiceToken}" \
|
||||
-v "${paths.PLATFORM_DATA_DIR}/redis/${app.id}:/var/lib/redis" \
|
||||
--label isCloudronManaged=true \
|
||||
--read-only -v /tmp -v /run ${tag}`;
|
||||
|
||||
var env = [
|
||||
{ name: 'REDIS_URL', value: 'redis://redisuser:' + redisPassword + '@redis-' + app.id },
|
||||
{ name: 'REDIS_PASSWORD', value: redisPassword },
|
||||
{ name: 'REDIS_HOST', value: redisName },
|
||||
{ name: 'REDIS_PORT', value: '6379' }
|
||||
];
|
||||
|
||||
async.series([
|
||||
shell.exec.bind(null, 'startRedis', cmd),
|
||||
appdb.setAddonConfig.bind(null, app.id, 'redis', env),
|
||||
waitForService.bind(null, 'redis-' + app.id, 'CLOUDRON_REDIS_TOKEN')
|
||||
], function (error) {
|
||||
if (error) debug('Error setting up redis: ', error);
|
||||
callback(error);
|
||||
});
|
||||
async.series([
|
||||
(next) => {
|
||||
docker.inspect(redisName, function (inspectError, result) {
|
||||
if (!inspectError) {
|
||||
debug(`Re-using existing redis container with state: ${JSON.stringify(result.State)}`);
|
||||
return next();
|
||||
}
|
||||
shell.exec('startRedis', cmd, next);
|
||||
});
|
||||
},
|
||||
appdb.setAddonConfig.bind(null, app.id, 'redis', env),
|
||||
waitForService.bind(null, 'redis-' + app.id, 'CLOUDRON_REDIS_TOKEN')
|
||||
], function (error) {
|
||||
if (error) debug('Error setting up redis: ', error);
|
||||
callback(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1751,7 +1805,7 @@ function statusSftp(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
docker.inspect('sftp', function (error, container) {
|
||||
if (error && error.reason === DockerError.NOT_FOUND) return callback(new AddonsError(AddonsError.NOT_ACTIVE, error));
|
||||
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, { status: exports.SERVICE_STATUS_STOPPED });
|
||||
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
|
||||
|
||||
docker.memoryUsage('sftp', function (error, result) {
|
||||
@@ -1772,10 +1826,10 @@ function statusGraphite(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
docker.inspect('graphite', function (error, container) {
|
||||
if (error && error.reason === DockerError.NOT_FOUND) return callback(new AddonsError(AddonsError.NOT_ACTIVE, error));
|
||||
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, { status: exports.SERVICE_STATUS_STOPPED });
|
||||
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
|
||||
|
||||
request.get('http://127.0.0.1:8417', { timeout: 3000 }, function (error, response) {
|
||||
request.get('http://127.0.0.1:8417/graphite-web/dashboard', { timeout: 3000 }, function (error, response) {
|
||||
if (error) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for graphite: ${error.message}` });
|
||||
if (response.statusCode !== 200) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for graphite. Status code: ${response.statusCode} message: ${response.body.message}` });
|
||||
|
||||
|
||||
@@ -72,10 +72,6 @@ server {
|
||||
ssl_dhparam /home/yellowtent/boxdata/dhparams.pem;
|
||||
add_header Strict-Transport-Security "max-age=15768000";
|
||||
|
||||
# https://developer.mozilla.org/en-US/docs/Web/HTTP/X-Frame-Options
|
||||
add_header X-Frame-Options "<%= xFrameOptions %>";
|
||||
proxy_hide_header X-Frame-Options;
|
||||
|
||||
# https://github.com/twitter/secureheaders
|
||||
# https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#tab=Compatibility_Matrix
|
||||
# https://wiki.mozilla.org/Security/Guidelines/Web_Security
|
||||
@@ -100,7 +96,7 @@ server {
|
||||
|
||||
<% if ( endpoint === 'admin' ) { -%>
|
||||
# CSP headers for the admin/dashboard resources
|
||||
add_header Content-Security-Policy "default-src 'none'; connect-src wss: https: 'self' *.cloudron.io; script-src https: 'self' 'unsafe-inline' 'unsafe-eval'; img-src * data:; style-src https: 'unsafe-inline'; object-src 'none'; font-src https: 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self';";
|
||||
add_header Content-Security-Policy "default-src 'none'; frame-src 'self' cloudron.io *.cloudron.io; connect-src wss: https: 'self' *.cloudron.io; script-src https: 'self' 'unsafe-inline' 'unsafe-eval'; img-src * data:; style-src https: 'unsafe-inline'; object-src 'none'; font-src https: 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self';";
|
||||
<% } -%>
|
||||
|
||||
proxy_http_version 1.1;
|
||||
@@ -167,9 +163,9 @@ server {
|
||||
client_max_body_size 0;
|
||||
}
|
||||
|
||||
# graphite paths (uncomment block below and visit /graphite/index.html)
|
||||
# graphite paths (uncomment block below and visit /graphite-web/dashboard)
|
||||
# remember to comment out the CSP policy as well to access the graphite dashboard
|
||||
# location ~ ^/(graphite|content|metrics|dashboard|render|browser|composer)/ {
|
||||
# location ~ ^/graphite-web/ {
|
||||
# proxy_pass http://127.0.0.1:8417;
|
||||
# client_max_body_size 1m;
|
||||
# }
|
||||
|
||||
174
src/appdb.js
174
src/appdb.js
@@ -21,36 +21,9 @@ exports = module.exports = {
|
||||
getAppIdByAddonConfigValue: getAppIdByAddonConfigValue,
|
||||
|
||||
setHealth: setHealth,
|
||||
setInstallationCommand: setInstallationCommand,
|
||||
setRunCommand: setRunCommand,
|
||||
setTask: setTask,
|
||||
getAppStoreIds: getAppStoreIds,
|
||||
|
||||
setOwner: setOwner,
|
||||
transferOwnership: transferOwnership,
|
||||
|
||||
// installation codes (keep in sync in UI)
|
||||
ISTATE_PENDING_INSTALL: 'pending_install', // installs and fresh reinstalls
|
||||
ISTATE_PENDING_CLONE: 'pending_clone', // clone
|
||||
ISTATE_PENDING_CONFIGURE: 'pending_configure', // config (location, port) changes and on infra update
|
||||
ISTATE_PENDING_UNINSTALL: 'pending_uninstall', // uninstallation
|
||||
ISTATE_PENDING_RESTORE: 'pending_restore', // restore to previous backup or on upgrade
|
||||
ISTATE_PENDING_UPDATE: 'pending_update', // update from installed state preserving data
|
||||
ISTATE_PENDING_FORCE_UPDATE: 'pending_force_update', // update from any state preserving data
|
||||
ISTATE_PENDING_BACKUP: 'pending_backup', // backup the app
|
||||
ISTATE_ERROR: 'error', // error executing last pending_* command
|
||||
ISTATE_INSTALLED: 'installed', // app is installed
|
||||
|
||||
RSTATE_RUNNING: 'running',
|
||||
RSTATE_PENDING_START: 'pending_start',
|
||||
RSTATE_PENDING_STOP: 'pending_stop',
|
||||
RSTATE_STOPPED: 'stopped', // app stopped by us
|
||||
|
||||
// run codes (keep in sync in UI)
|
||||
HEALTH_HEALTHY: 'healthy',
|
||||
HEALTH_UNHEALTHY: 'unhealthy',
|
||||
HEALTH_ERROR: 'error',
|
||||
HEALTH_DEAD: 'dead',
|
||||
|
||||
// subdomain table types
|
||||
SUBDOMAIN_TYPE_PRIMARY: 'primary',
|
||||
SUBDOMAIN_TYPE_REDIRECT: 'redirect',
|
||||
@@ -65,12 +38,12 @@ var assert = require('assert'),
|
||||
safe = require('safetydance'),
|
||||
util = require('util');
|
||||
|
||||
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.installationProgress', 'apps.runState',
|
||||
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.errorJson', 'apps.runState',
|
||||
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'subdomains.subdomain AS location', 'subdomains.domain',
|
||||
'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit',
|
||||
'apps.label', 'apps.tagsJson',
|
||||
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
|
||||
'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.mailboxName', 'apps.enableAutomaticUpdate',
|
||||
'apps.accessRestrictionJson', 'apps.memoryLimit',
|
||||
'apps.label', 'apps.tagsJson', 'apps.taskId',
|
||||
'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
|
||||
'apps.creationTime', 'apps.updateTime', 'apps.mailboxName', 'apps.enableAutomaticUpdate',
|
||||
'apps.dataDir', 'apps.ts', 'apps.healthTime' ].join(',');
|
||||
|
||||
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(',');
|
||||
@@ -84,18 +57,6 @@ function postProcess(result) {
|
||||
result.manifest = safe.JSON.parse(result.manifestJson);
|
||||
delete result.manifestJson;
|
||||
|
||||
assert(result.oldConfigJson === null || typeof result.oldConfigJson === 'string');
|
||||
result.oldConfig = safe.JSON.parse(result.oldConfigJson);
|
||||
delete result.oldConfigJson;
|
||||
|
||||
assert(result.updateConfigJson === null || typeof result.updateConfigJson === 'string');
|
||||
result.updateConfig = safe.JSON.parse(result.updateConfigJson);
|
||||
delete result.updateConfigJson;
|
||||
|
||||
assert(result.restoreConfigJson === null || typeof result.restoreConfigJson === 'string');
|
||||
result.restoreConfig = safe.JSON.parse(result.restoreConfigJson);
|
||||
delete result.restoreConfigJson;
|
||||
|
||||
assert(result.tagsJson === null || typeof result.tagsJson === 'string');
|
||||
result.tags = safe.JSON.parse(result.tagsJson) || [];
|
||||
delete result.tagsJson;
|
||||
@@ -121,9 +82,6 @@ function postProcess(result) {
|
||||
if (result.accessRestriction && !result.accessRestriction.users) result.accessRestriction.users = [];
|
||||
delete result.accessRestrictionJson;
|
||||
|
||||
// TODO remove later once all apps have this attribute
|
||||
result.xFrameOptions = result.xFrameOptions || 'SAMEORIGIN';
|
||||
|
||||
result.sso = !!result.sso; // make it bool
|
||||
result.enableBackup = !!result.enableBackup; // make it bool
|
||||
result.enableAutomaticUpdate = !!result.enableAutomaticUpdate; // make it bool
|
||||
@@ -146,8 +104,10 @@ function postProcess(result) {
|
||||
if (envNames[i]) result.env[envNames[i]] = envValues[i];
|
||||
}
|
||||
|
||||
// in the db, we store dataDir as unique/nullable
|
||||
result.dataDir = result.dataDir || '';
|
||||
result.error = safe.JSON.parse(result.errorJson);
|
||||
delete result.errorJson;
|
||||
|
||||
result.taskId = result.taskId ? String(result.taskId) : null;
|
||||
}
|
||||
|
||||
function get(id, callback) {
|
||||
@@ -260,14 +220,13 @@ function getAll(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function add(id, appStoreId, manifest, location, domain, ownerId, portBindings, data, callback) {
|
||||
function add(id, appStoreId, manifest, location, domain, portBindings, data, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof appStoreId, 'string');
|
||||
assert(manifest && typeof manifest === 'object');
|
||||
assert.strictEqual(typeof manifest.version, 'string');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof ownerId, 'string');
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
assert(data && typeof data === 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -279,9 +238,8 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
|
||||
const accessRestriction = data.accessRestriction || null;
|
||||
const accessRestrictionJson = JSON.stringify(accessRestriction);
|
||||
const memoryLimit = data.memoryLimit || 0;
|
||||
const xFrameOptions = data.xFrameOptions || '';
|
||||
const installationState = data.installationState || exports.ISTATE_PENDING_INSTALL;
|
||||
const restoreConfigJson = data.restoreConfig ? JSON.stringify(data.restoreConfig) : null; // used when cloning
|
||||
const installationState = data.installationState;
|
||||
const runState = data.runState;
|
||||
const sso = 'sso' in data ? data.sso : null;
|
||||
const robotsTxt = 'robotsTxt' in data ? data.robotsTxt : null;
|
||||
const debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null;
|
||||
@@ -293,11 +251,11 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
|
||||
var queries = [];
|
||||
|
||||
queries.push({
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions,'
|
||||
+ 'restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId, mailboxName, label, tagsJson) '
|
||||
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson,
|
||||
sso, debugModeJson, robotsTxt, ownerId, mailboxName, label, tagsJson ]
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, '
|
||||
+ 'sso, debugModeJson, robotsTxt, mailboxName, label, tagsJson) '
|
||||
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit,
|
||||
sso, debugModeJson, robotsTxt, mailboxName, label, tagsJson ]
|
||||
});
|
||||
|
||||
queries.push({
|
||||
@@ -462,7 +420,7 @@ function updateWithConstraints(id, app, constraints, callback) {
|
||||
|
||||
var fields = [ ], values = [ ];
|
||||
for (var p in app) {
|
||||
if (p === 'manifest' || p === 'oldConfig' || p === 'updateConfig' || p === 'restoreConfig' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode') {
|
||||
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error') {
|
||||
fields.push(`${p}Json = ?`);
|
||||
values.push(JSON.stringify(app[p]));
|
||||
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'env') {
|
||||
@@ -485,7 +443,6 @@ function updateWithConstraints(id, app, constraints, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
// not sure if health should influence runState
|
||||
function setHealth(appId, health, healthTime, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof health, 'string');
|
||||
@@ -494,53 +451,22 @@ function setHealth(appId, health, healthTime, callback) {
|
||||
|
||||
var values = { health, healthTime };
|
||||
|
||||
var constraints = 'AND runState NOT LIKE "pending_%" AND installationState = "installed"';
|
||||
|
||||
updateWithConstraints(appId, values, constraints, callback);
|
||||
updateWithConstraints(appId, values, '', callback);
|
||||
}
|
||||
|
||||
function setInstallationCommand(appId, installationState, values, callback) {
|
||||
function setTask(appId, values, options, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof installationState, 'string');
|
||||
|
||||
if (typeof values === 'function') {
|
||||
callback = values;
|
||||
values = { };
|
||||
} else {
|
||||
assert.strictEqual(typeof values, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
}
|
||||
|
||||
values.installationState = installationState;
|
||||
values.installationProgress = '';
|
||||
|
||||
// Rules are:
|
||||
// uninstall is allowed in any state
|
||||
// force update is allowed in any state including pending_uninstall! (for better or worse)
|
||||
// restore is allowed from installed or error state or currently restoring
|
||||
// configure is allowed in installed state or currently configuring or in error state
|
||||
// update and backup are allowed only in installed state
|
||||
|
||||
if (installationState === exports.ISTATE_PENDING_UNINSTALL || installationState === exports.ISTATE_PENDING_FORCE_UPDATE) {
|
||||
updateWithConstraints(appId, values, '', callback);
|
||||
} else if (installationState === exports.ISTATE_PENDING_RESTORE) {
|
||||
updateWithConstraints(appId, values, 'AND (installationState = "installed" OR installationState = "error" OR installationState = "pending_restore")', callback);
|
||||
} else if (installationState === exports.ISTATE_PENDING_UPDATE || installationState === exports.ISTATE_PENDING_BACKUP) {
|
||||
updateWithConstraints(appId, values, 'AND installationState = "installed"', callback);
|
||||
} else if (installationState === exports.ISTATE_PENDING_CONFIGURE) {
|
||||
updateWithConstraints(appId, values, 'AND (installationState = "installed" OR installationState = "pending_configure" OR installationState = "error")', callback);
|
||||
} else {
|
||||
callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, 'invalid installationState'));
|
||||
}
|
||||
}
|
||||
|
||||
function setRunCommand(appId, runState, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof runState, 'string');
|
||||
assert.strictEqual(typeof values, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var values = { runState: runState };
|
||||
updateWithConstraints(appId, values, 'AND runState NOT LIKE "pending_%" AND installationState = "installed"', callback);
|
||||
if (!options.requireNullTaskId) return updateWithConstraints(appId, values, '', callback);
|
||||
|
||||
if (options.requiredState === null) {
|
||||
updateWithConstraints(appId, values, 'AND taskId IS NULL', callback);
|
||||
} else {
|
||||
updateWithConstraints(appId, values, `AND taskId IS NULL AND installationState = "${options.requiredState}"`, callback);
|
||||
}
|
||||
}
|
||||
|
||||
function getAppStoreIds(callback) {
|
||||
@@ -625,13 +551,13 @@ function getAddonConfigByAppId(appId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getAppIdByAddonConfigValue(addonId, name, value, callback) {
|
||||
function getAppIdByAddonConfigValue(addonId, namePattern, value, callback) {
|
||||
assert.strictEqual(typeof addonId, 'string');
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof namePattern, 'string');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT appId FROM appAddonConfigs WHERE addonId = ? AND name = ? AND value = ?', [ addonId, name, value ], function (error, results) {
|
||||
database.query('SELECT appId FROM appAddonConfigs WHERE addonId = ? AND name LIKE ? AND value = ?', [ addonId, namePattern, value ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
@@ -639,44 +565,16 @@ function getAppIdByAddonConfigValue(addonId, name, value, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getAddonConfigByName(appId, addonId, name, callback) {
|
||||
function getAddonConfigByName(appId, addonId, namePattern, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof addonId, 'string');
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof namePattern, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT value FROM appAddonConfigs WHERE appId = ? AND addonId = ? AND name = ?', [ appId, addonId, name ], function (error, results) {
|
||||
database.query('SELECT value FROM appAddonConfigs WHERE appId = ? AND addonId = ? AND name LIKE ?', [ appId, addonId, namePattern ], 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].value);
|
||||
});
|
||||
}
|
||||
|
||||
function setOwner(appId, ownerId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof ownerId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('UPDATE apps SET ownerId=? WHERE appId=?', [ ownerId, appId ], function (error, results) {
|
||||
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such user'));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such app'));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function transferOwnership(oldOwnerId, newOwnerId, callback) {
|
||||
assert.strictEqual(typeof oldOwnerId, 'string');
|
||||
assert.strictEqual(typeof newOwnerId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('UPDATE apps SET ownerId=? WHERE ownerId=?', [ newOwnerId, oldOwnerId ], function (error) {
|
||||
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such user'));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -36,16 +36,16 @@ function setHealth(app, health, callback) {
|
||||
|
||||
let now = new Date(), healthTime = app.healthTime, curHealth = app.health;
|
||||
|
||||
if (health === appdb.HEALTH_HEALTHY) {
|
||||
if (health === apps.HEALTH_HEALTHY) {
|
||||
healthTime = now;
|
||||
if (curHealth && curHealth !== appdb.HEALTH_HEALTHY) { // app starts out with null health
|
||||
if (curHealth && curHealth !== apps.HEALTH_HEALTHY) { // app starts out with null health
|
||||
debugApp(app, 'app switched from %s to healthy', curHealth);
|
||||
|
||||
// do not send mails for dev apps
|
||||
if (!app.debugMode) eventlog.add(eventlog.ACTION_APP_UP, auditSource.HEALTH_MONITOR, { app: app });
|
||||
}
|
||||
} else if (Math.abs(now - healthTime) > UNHEALTHY_THRESHOLD) {
|
||||
if (curHealth === appdb.HEALTH_HEALTHY) {
|
||||
if (curHealth === apps.HEALTH_HEALTHY) {
|
||||
debugApp(app, 'marking as unhealthy since not seen for more than %s minutes', UNHEALTHY_THRESHOLD/(60 * 1000));
|
||||
|
||||
// do not send mails for dev apps
|
||||
@@ -72,7 +72,7 @@ function checkAppHealth(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
|
||||
if (app.installationState !== apps.ISTATE_INSTALLED || app.runState !== apps.RSTATE_RUNNING) {
|
||||
debugApp(app, 'skipped. istate:%s rstate:%s', app.installationState, app.runState);
|
||||
return callback(null);
|
||||
}
|
||||
@@ -82,34 +82,34 @@ function checkAppHealth(app, callback) {
|
||||
docker.inspect(app.containerId, function (error, data) {
|
||||
if (error || !data || !data.State) {
|
||||
debugApp(app, 'Error inspecting container');
|
||||
return setHealth(app, appdb.HEALTH_ERROR, callback);
|
||||
return setHealth(app, apps.HEALTH_ERROR, callback);
|
||||
}
|
||||
|
||||
if (data.State.Running !== true) {
|
||||
debugApp(app, 'exited');
|
||||
return setHealth(app, appdb.HEALTH_DEAD, callback);
|
||||
return setHealth(app, apps.HEALTH_DEAD, callback);
|
||||
}
|
||||
|
||||
// non-appstore apps may not have healthCheckPath
|
||||
if (!manifest.healthCheckPath) return setHealth(app, appdb.HEALTH_HEALTHY, callback);
|
||||
if (!manifest.healthCheckPath) return setHealth(app, apps.HEALTH_HEALTHY, callback);
|
||||
|
||||
// poll through docker network instead of nginx to bypass any potential oauth proxy
|
||||
var healthCheckUrl = 'http://127.0.0.1:' + app.httpPort + manifest.healthCheckPath;
|
||||
superagent
|
||||
.get(healthCheckUrl)
|
||||
.set('Host', app.fqdn) // required for some apache configs with rewrite rules
|
||||
.set('User-Agent', 'Mozilla') // required for some apps (e.g. minio)
|
||||
.set('User-Agent', 'Mozilla (CloudronHealth)') // required for some apps (e.g. minio)
|
||||
.redirects(0)
|
||||
.timeout(HEALTHCHECK_INTERVAL)
|
||||
.end(function (error, res) {
|
||||
if (error && !error.response) {
|
||||
debugApp(app, 'not alive (network error): %s', error.message);
|
||||
setHealth(app, appdb.HEALTH_UNHEALTHY, callback);
|
||||
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
|
||||
} else if (res.statusCode >= 400) { // 2xx and 3xx are ok
|
||||
debugApp(app, 'not alive : %s', error || res.status);
|
||||
setHealth(app, appdb.HEALTH_UNHEALTHY, callback);
|
||||
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
|
||||
} else {
|
||||
setHealth(app, appdb.HEALTH_HEALTHY, callback);
|
||||
setHealth(app, apps.HEALTH_HEALTHY, callback);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -187,7 +187,7 @@ function processApp(callback) {
|
||||
if (error) console.error(error);
|
||||
|
||||
var alive = result
|
||||
.filter(function (a) { return a.installationState === appdb.ISTATE_INSTALLED && a.runState === appdb.RSTATE_RUNNING && a.health === appdb.HEALTH_HEALTHY; })
|
||||
.filter(function (a) { return a.installationState === apps.ISTATE_INSTALLED && a.runState === apps.RSTATE_RUNNING && a.health === apps.HEALTH_HEALTHY; })
|
||||
.map(function (a) { return (a.location || 'naked_domain') + '|' + a.manifest.id; }).join(', ');
|
||||
|
||||
debug('apps alive: [%s]', alive);
|
||||
|
||||
1189
src/apps.js
1189
src/apps.js
File diff suppressed because it is too large
Load Diff
@@ -27,17 +27,20 @@ exports = module.exports = {
|
||||
var apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
custom = require('./custom.js'),
|
||||
debug = require('debug')('box:appstore'),
|
||||
domains = require('./domains.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
groups = require('./groups.js'),
|
||||
mail = require('./mail.js'),
|
||||
os = require('os'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
settings = require('./settings.js'),
|
||||
superagent = require('superagent'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
users = require('./users.js'),
|
||||
util = require('util');
|
||||
|
||||
function AppstoreError(reason, errorOrMessage) {
|
||||
@@ -95,7 +98,7 @@ function login(email, password, totpToken, callback) {
|
||||
totpToken: totpToken
|
||||
};
|
||||
|
||||
const url = config.apiServerOrigin() + '/api/v1/login';
|
||||
const url = settings.apiServerOrigin() + '/api/v1/login';
|
||||
superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.ACCESS_DENIED));
|
||||
@@ -115,7 +118,7 @@ function registerUser(email, password, callback) {
|
||||
password: password,
|
||||
};
|
||||
|
||||
const url = config.apiServerOrigin() + '/api/v1/register_user';
|
||||
const url = settings.apiServerOrigin() + '/api/v1/register_user';
|
||||
superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
if (result.statusCode === 409) return callback(new AppstoreError(AppstoreError.ALREADY_EXISTS));
|
||||
@@ -131,7 +134,7 @@ function getSubscription(callback) {
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const url = config.apiServerOrigin() + '/api/v1/subscription';
|
||||
const url = settings.apiServerOrigin() + '/api/v1/subscription';
|
||||
superagent.get(url).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
|
||||
@@ -158,7 +161,7 @@ function purchaseApp(data, callback) {
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const url = `${config.apiServerOrigin()}/api/v1/cloudronapps`;
|
||||
const url = `${settings.apiServerOrigin()}/api/v1/cloudronapps`;
|
||||
|
||||
superagent.post(url).send(data).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
@@ -183,7 +186,7 @@ function unpurchaseApp(appId, data, callback) {
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const url = `${config.apiServerOrigin()}/api/v1/cloudronapps/${appId}`;
|
||||
const url = `${settings.apiServerOrigin()}/api/v1/cloudronapps/${appId}`;
|
||||
|
||||
superagent.get(url).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
@@ -206,7 +209,7 @@ function unpurchaseApp(appId, data, callback) {
|
||||
function sendAliveStatus(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
var allSettings, allDomains, mailDomains, loginEvents;
|
||||
let allSettings, allDomains, mailDomains, loginEvents, userCount, groupCount;
|
||||
|
||||
async.series([
|
||||
function (callback) {
|
||||
@@ -236,6 +239,20 @@ function sendAliveStatus(callback) {
|
||||
loginEvents = result;
|
||||
callback();
|
||||
});
|
||||
},
|
||||
function (callback) {
|
||||
users.count(function (error, result) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
userCount = result;
|
||||
callback();
|
||||
});
|
||||
},
|
||||
function (callback) {
|
||||
groups.count(function (error, result) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
groupCount = result;
|
||||
callback();
|
||||
});
|
||||
}
|
||||
], function (error) {
|
||||
if (error) return callback(error);
|
||||
@@ -255,15 +272,17 @@ function sendAliveStatus(callback) {
|
||||
catchAllCount: mailDomains.filter(function (d) { return d.catchAll.length !== 0; }).length,
|
||||
relayProviders: Array.from(new Set(mailDomains.map(function (d) { return d.relay.provider; })))
|
||||
},
|
||||
userCount: userCount,
|
||||
groupCount: groupCount,
|
||||
appAutoupdatePattern: allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY],
|
||||
boxAutoupdatePattern: allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY],
|
||||
timeZone: allSettings[settings.TIME_ZONE_KEY],
|
||||
};
|
||||
|
||||
var data = {
|
||||
version: config.version(),
|
||||
adminFqdn: config.adminFqdn(),
|
||||
provider: config.provider(),
|
||||
version: constants.VERSION,
|
||||
adminFqdn: settings.adminFqdn(),
|
||||
provider: sysinfo.provider(),
|
||||
backendSettings: backendSettings,
|
||||
machine: {
|
||||
cpus: os.cpus(),
|
||||
@@ -277,7 +296,7 @@ function sendAliveStatus(callback) {
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const url = `${config.apiServerOrigin()}/api/v1/alive`;
|
||||
const url = `${settings.apiServerOrigin()}/api/v1/alive`;
|
||||
superagent.post(url).send(data).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
|
||||
@@ -297,9 +316,9 @@ function getBoxUpdate(callback) {
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const url = `${config.apiServerOrigin()}/api/v1/boxupdate`;
|
||||
const url = `${settings.apiServerOrigin()}/api/v1/boxupdate`;
|
||||
|
||||
superagent.get(url).query({ accessToken: token, boxVersion: config.version() }).timeout(10 * 1000).end(function (error, result) {
|
||||
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION }).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
|
||||
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR, result.body.message));
|
||||
@@ -308,7 +327,7 @@ function getBoxUpdate(callback) {
|
||||
|
||||
var updateInfo = result.body;
|
||||
|
||||
if (!semver.valid(updateInfo.version) || semver.gt(config.version(), updateInfo.version)) {
|
||||
if (!semver.valid(updateInfo.version) || semver.gt(constants.VERSION, updateInfo.version)) {
|
||||
return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Invalid update version: %s %s', result.statusCode, result.text)));
|
||||
}
|
||||
|
||||
@@ -332,9 +351,9 @@ function getAppUpdate(app, callback) {
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const url = `${config.apiServerOrigin()}/api/v1/appupdate`;
|
||||
const url = `${settings.apiServerOrigin()}/api/v1/appupdate`;
|
||||
|
||||
superagent.get(url).query({ accessToken: token, boxVersion: config.version(), appId: app.appStoreId, appVersion: app.manifest.version }).timeout(10 * 1000).end(function (error, result) {
|
||||
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION, appId: app.appStoreId, appVersion: app.manifest.version }).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
|
||||
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR, result.body.message));
|
||||
@@ -362,7 +381,7 @@ function registerCloudron(data, callback) {
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const url = `${config.apiServerOrigin()}/api/v1/register_cloudron`;
|
||||
const url = `${settings.apiServerOrigin()}/api/v1/register_cloudron`;
|
||||
|
||||
superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
@@ -387,14 +406,15 @@ function registerCloudron(data, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function registerWithLicense(license, callback) {
|
||||
function registerWithLicense(license, domain, callback) {
|
||||
assert.strictEqual(typeof license, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getCloudronToken(function (error, token) {
|
||||
if (token) return callback(new AppstoreError(AppstoreError.ALREADY_REGISTERED));
|
||||
|
||||
registerCloudron({ license }, callback);
|
||||
registerCloudron({ license, domain }, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -417,7 +437,7 @@ function registerWithLoginCredentials(options, callback) {
|
||||
login(options.email, options.password, options.totpToken || '', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
registerCloudron({ domain: config.adminDomain(), accessToken: result.accessToken }, callback);
|
||||
registerCloudron({ domain: settings.adminDomain(), accessToken: result.accessToken }, callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -444,9 +464,9 @@ function createTicket(info, callback) {
|
||||
if (error) console.error('Unable to get app info', error);
|
||||
if (result) info.app = result;
|
||||
|
||||
let url = config.apiServerOrigin() + '/api/v1/ticket';
|
||||
let url = settings.apiServerOrigin() + '/api/v1/ticket';
|
||||
|
||||
info.supportEmail = custom.supportEmail(); // destination address for tickets
|
||||
info.supportEmail = custom.spec().support.email; // destination address for tickets
|
||||
|
||||
superagent.post(url).query({ accessToken: token }).send(info).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
@@ -468,8 +488,8 @@ function getApps(callback) {
|
||||
|
||||
settings.getUnstableAppsConfig(function (error, unstable) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
const url = `${config.apiServerOrigin()}/api/v1/apps`;
|
||||
superagent.get(url).query({ accessToken: token, boxVersion: config.version(), unstable: unstable }).timeout(10 * 1000).end(function (error, result) {
|
||||
const url = `${settings.apiServerOrigin()}/api/v1/apps`;
|
||||
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION, unstable: unstable }).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
|
||||
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR, result.body.message));
|
||||
@@ -490,7 +510,7 @@ function getAppVersion(appId, version, callback) {
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
let url = `${config.apiServerOrigin()}/api/v1/apps/${appId}`;
|
||||
let url = `${settings.apiServerOrigin()}/api/v1/apps/${appId}`;
|
||||
if (version !== 'latest') url += `/versions/${version}`;
|
||||
|
||||
superagent.get(url).query({ accessToken: token }).timeout(10 * 1000).end(function (error, result) {
|
||||
|
||||
1077
src/apptask.js
1077
src/apptask.js
File diff suppressed because it is too large
Load Diff
86
src/apptaskmanager.js
Normal file
86
src/apptaskmanager.js
Normal file
@@ -0,0 +1,86 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
scheduleTask: scheduleTask
|
||||
};
|
||||
|
||||
let assert = require('assert'),
|
||||
debug = require('debug')('box:apptaskmanager'),
|
||||
fs = require('fs'),
|
||||
locker = require('./locker.js'),
|
||||
safe = require('safetydance'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
tasks = require('./tasks.js');
|
||||
|
||||
let gActiveTasks = { }; // indexed by app id
|
||||
let gPendingTasks = [ ];
|
||||
let gInitialized = false;
|
||||
|
||||
const TASK_CONCURRENCY = 3;
|
||||
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
function waitText(lockOperation) {
|
||||
if (lockOperation === locker.OP_BOX_UPDATE) return 'Waiting for Cloudron to finish updating. See the Settings view';
|
||||
if (lockOperation === locker.OP_PLATFORM_START) return 'Waiting for Cloudron to initialize';
|
||||
if (lockOperation === locker.OP_FULL_BACKUP) return 'Wait for Cloudron to finish backup. See the Backups view';
|
||||
|
||||
return ''; // cannot happen
|
||||
}
|
||||
|
||||
function initializeSync() {
|
||||
gInitialized = true;
|
||||
locker.on('unlocked', startNextTask);
|
||||
}
|
||||
|
||||
// callback is called when task is finished
|
||||
function scheduleTask(appId, taskId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof taskId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!gInitialized) initializeSync();
|
||||
|
||||
if (appId in gActiveTasks) {
|
||||
return callback(new Error(`Task for %s is already active: ${appId}`));
|
||||
}
|
||||
|
||||
if (Object.keys(gActiveTasks).length >= TASK_CONCURRENCY) {
|
||||
debug(`Reached concurrency limit, queueing task id ${taskId}`);
|
||||
tasks.update(taskId, { percent: 0, message: 'Waiting for other app tasks to complete' }, NOOP_CALLBACK);
|
||||
gPendingTasks.push({ appId, taskId, callback });
|
||||
return;
|
||||
}
|
||||
|
||||
var lockError = locker.recursiveLock(locker.OP_APPTASK);
|
||||
|
||||
if (lockError) {
|
||||
debug(`Could not get lock. ${lockError.message}, queueing task id ${taskId}`);
|
||||
tasks.update(taskId, { percent: 0, message: waitText(lockError.operation) }, NOOP_CALLBACK);
|
||||
gPendingTasks.push({ appId, taskId, callback });
|
||||
return;
|
||||
}
|
||||
|
||||
gActiveTasks[appId] = {};
|
||||
|
||||
const logFile = path.join(paths.LOG_DIR, appId, 'apptask.log');
|
||||
|
||||
if (!fs.existsSync(path.dirname(logFile))) safe.fs.mkdirSync(path.dirname(logFile)); // ensure directory
|
||||
|
||||
tasks.startTask(taskId, { logFile }, function (error, result) {
|
||||
callback(error, result);
|
||||
|
||||
delete gActiveTasks[appId];
|
||||
locker.unlock(locker.OP_APPTASK); // unlock event will trigger next task
|
||||
});
|
||||
}
|
||||
|
||||
function startNextTask() {
|
||||
if (gPendingTasks.length === 0) return;
|
||||
|
||||
assert(Object.keys(gActiveTasks).length < TASK_CONCURRENCY);
|
||||
|
||||
const t = gPendingTasks.shift();
|
||||
scheduleTask(t.appId, t.taskId, t.callback);
|
||||
}
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
exports = module.exports = {
|
||||
CRON: { userId: null, username: 'cron' },
|
||||
HEALTH_MONITOR: { userId: null, username: 'healthmonitor' },
|
||||
SYSADMIN: { userId: null, username: 'sysadmin' },
|
||||
TASK_MANAGER: { userId: null, username: 'taskmanager' },
|
||||
APP_TASK: { userId: null, username: 'apptask' },
|
||||
EXTERNAL_LDAP_TASK: { userId: null, username: 'externalldap' },
|
||||
|
||||
fromRequest: fromRequest
|
||||
};
|
||||
|
||||
146
src/backups.js
146
src/backups.js
@@ -40,18 +40,18 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var addons = require('./addons.js'),
|
||||
appdb = require('./appdb.js'),
|
||||
apps = require('./apps.js'),
|
||||
AppsError = require('./apps.js').AppsError,
|
||||
async = require('async'),
|
||||
assert = require('assert'),
|
||||
backupdb = require('./backupdb.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
crypto = require('crypto'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
DataLayout = require('./datalayout.js'),
|
||||
debug = require('debug')('box:backups'),
|
||||
df = require('@sindresorhus/df'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
fs = require('fs'),
|
||||
locker = require('./locker.js'),
|
||||
@@ -60,6 +60,7 @@ var addons = require('./addons.js'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
progressStream = require('progress-stream'),
|
||||
prettyBytes = require('pretty-bytes'),
|
||||
safe = require('safetydance'),
|
||||
shell = require('./shell.js'),
|
||||
settings = require('./settings.js'),
|
||||
@@ -112,6 +113,7 @@ function api(provider) {
|
||||
case 's3-v4-compat': return require('./storage/s3.js');
|
||||
case 'digitalocean-spaces': return require('./storage/s3.js');
|
||||
case 'exoscale-sos': return require('./storage/s3.js');
|
||||
case 'wasabi': return require('./storage/s3.js');
|
||||
case 'scaleway-objectstorage': return require('./storage/s3.js');
|
||||
case 'noop': return require('./storage/noop.js');
|
||||
default: return null;
|
||||
@@ -291,6 +293,9 @@ function tarPack(dataLayout, key, callback) {
|
||||
},
|
||||
map: function(header) {
|
||||
header.name = dataLayout.toRemotePath(header.name);
|
||||
// the tar pax format allows us to encode filenames > 100 and size > 8GB (see #640)
|
||||
// https://www.systutorials.com/docs/linux/man/5-star/
|
||||
if (header.size > 8589934590 || header.name > 99) header.pax = { size: header.size };
|
||||
return header;
|
||||
},
|
||||
strict: false // do not error for unknown types (skip fifo, char/block devices)
|
||||
@@ -408,6 +413,34 @@ function saveFsMetadata(dataLayout, metadataFile, callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
// the du call in the function below requires root
|
||||
function checkFreeDiskSpace(backupConfig, dataLayout, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (backupConfig.provider !== 'filesystem') return callback();
|
||||
|
||||
let used = 0;
|
||||
for (let localPath of dataLayout.localPaths()) {
|
||||
debug(`checkFreeDiskSpace: getting disk usage of ${localPath}`);
|
||||
let result = safe.child_process.execSync(`du -Dsb ${localPath}`, { encoding: 'utf8' });
|
||||
if (!result) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, safe.error));
|
||||
used += parseInt(result, 10);
|
||||
}
|
||||
|
||||
debug(`checkFreeDiskSpace: ${used} bytes`);
|
||||
|
||||
df.file(backupConfig.backupFolder).then(function (diskUsage) {
|
||||
const needed = used + (1024 * 1024 * 1024); // check if there is atleast 1GB left afterwards
|
||||
if (diskUsage.available <= needed) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, `Not enough disk space for backup. Needed: ${prettyBytes(needed)} Available: ${prettyBytes(diskUsage.available)}`));
|
||||
|
||||
callback(null);
|
||||
}).catch(function (error) {
|
||||
callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
});
|
||||
}
|
||||
|
||||
// this function is called via backupupload (since it needs root to traverse app's directory)
|
||||
function upload(backupId, format, dataLayoutString, progressCallback, callback) {
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
@@ -423,29 +456,33 @@ function upload(backupId, format, dataLayoutString, progressCallback, callback)
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (format === 'tgz') {
|
||||
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
|
||||
retryCallback = once(retryCallback); // protect again upload() erroring much later after tar stream error
|
||||
checkFreeDiskSpace(backupConfig, dataLayout, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
tarPack(dataLayout, backupConfig.key || null, function (error, tarStream) {
|
||||
if (error) return retryCallback(error);
|
||||
if (format === 'tgz') {
|
||||
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
|
||||
retryCallback = once(retryCallback); // protect again upload() erroring much later after tar stream error
|
||||
|
||||
tarStream.on('progress', function(progress) {
|
||||
const transferred = Math.round(progress.transferred/1024/1024), speed = Math.round(progress.speed/1024/1024);
|
||||
if (!transferred && !speed) return progressCallback({ message: 'Uploading backup' }); // 0M@0Mbps looks wrong
|
||||
progressCallback({ message: `Uploading backup ${transferred}M@${speed}Mbps` });
|
||||
tarPack(dataLayout, backupConfig.key || null, function (error, tarStream) {
|
||||
if (error) return retryCallback(error);
|
||||
|
||||
tarStream.on('progress', function(progress) {
|
||||
const transferred = Math.round(progress.transferred/1024/1024), speed = Math.round(progress.speed/1024/1024);
|
||||
if (!transferred && !speed) return progressCallback({ message: 'Uploading backup' }); // 0M@0Mbps looks wrong
|
||||
progressCallback({ message: `Uploading backup ${transferred}M@${speed}Mbps` });
|
||||
});
|
||||
tarStream.on('error', retryCallback); // already returns BackupsError
|
||||
|
||||
api(backupConfig.provider).upload(backupConfig, getBackupFilePath(backupConfig, backupId, format), tarStream, retryCallback);
|
||||
});
|
||||
tarStream.on('error', retryCallback); // already returns BackupsError
|
||||
|
||||
api(backupConfig.provider).upload(backupConfig, getBackupFilePath(backupConfig, backupId, format), tarStream, retryCallback);
|
||||
});
|
||||
}, callback);
|
||||
} else {
|
||||
async.series([
|
||||
saveFsMetadata.bind(null, dataLayout, `${dataLayout.localRoot()}/fsmetadata.json`),
|
||||
sync.bind(null, backupConfig, backupId, dataLayout, progressCallback)
|
||||
], callback);
|
||||
}
|
||||
}, callback);
|
||||
} else {
|
||||
async.series([
|
||||
saveFsMetadata.bind(null, dataLayout, `${dataLayout.localRoot()}/fsmetadata.json`),
|
||||
sync.bind(null, backupConfig, backupId, dataLayout, progressCallback)
|
||||
], callback);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -638,7 +675,7 @@ function restore(backupConfig, backupId, progressCallback, callback) {
|
||||
|
||||
debug('restore: database imported');
|
||||
|
||||
callback();
|
||||
settings.initCache(callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -689,7 +726,7 @@ function runBackupUpload(backupId, format, dataLayout, progressCallback, callbac
|
||||
callback();
|
||||
}).on('message', function (message) {
|
||||
if (!message.result) return progressCallback(message);
|
||||
debug(`runBackupUpload: result - ${message}`);
|
||||
debug(`runBackupUpload: result - ${JSON.stringify(message)}`);
|
||||
result = message.result;
|
||||
});
|
||||
}
|
||||
@@ -764,12 +801,12 @@ function rotateBoxBackup(backupConfig, tag, appBackupIds, progressCallback, call
|
||||
if (!snapshotInfo) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, 'Snapshot info missing or corrupt'));
|
||||
|
||||
const snapshotTime = snapshotInfo.timestamp.replace(/[T.]/g, '-').replace(/[:Z]/g,''); // add this to filename to make it unique, so it's easy to download them
|
||||
const backupId = util.format('%s/box_%s_v%s', tag, snapshotTime, config.version());
|
||||
const backupId = util.format('%s/box_%s_v%s', tag, snapshotTime, constants.VERSION);
|
||||
const format = backupConfig.format;
|
||||
|
||||
debug(`Rotating box backup to id ${backupId}`);
|
||||
|
||||
backupdb.add(backupId, { version: config.version(), type: backupdb.BACKUP_TYPE_BOX, dependsOn: appBackupIds, manifest: null, format: format }, function (error) {
|
||||
backupdb.add(backupId, { version: constants.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));
|
||||
@@ -809,10 +846,10 @@ function backupBoxWithAppBackupIds(appBackupIds, tag, progressCallback, callback
|
||||
function canBackupApp(app) {
|
||||
// only backup apps that are installed or pending configure or called from apptask. Rest of them are in some
|
||||
// state not good for consistent backup (i.e addons may not have been setup completely)
|
||||
return (app.installationState === appdb.ISTATE_INSTALLED && app.health === appdb.HEALTH_HEALTHY) ||
|
||||
app.installationState === appdb.ISTATE_PENDING_CONFIGURE ||
|
||||
app.installationState === appdb.ISTATE_PENDING_BACKUP || // called from apptask
|
||||
app.installationState === appdb.ISTATE_PENDING_UPDATE; // called from apptask
|
||||
return (app.installationState === apps.ISTATE_INSTALLED && app.health === apps.HEALTH_HEALTHY) ||
|
||||
app.installationState === apps.ISTATE_PENDING_CONFIGURE ||
|
||||
app.installationState === apps.ISTATE_PENDING_BACKUP || // called from apptask
|
||||
app.installationState === apps.ISTATE_PENDING_UPDATE; // called from apptask
|
||||
}
|
||||
|
||||
function snapshotApp(app, progressCallback, callback) {
|
||||
@@ -822,7 +859,7 @@ function snapshotApp(app, progressCallback, callback) {
|
||||
|
||||
progressCallback({ message: `Snapshotting app ${app.fqdn}` });
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/config.json'), JSON.stringify(apps.getAppConfig(app)))) {
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/config.json'), JSON.stringify(app))) {
|
||||
return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error creating config.json: ' + safe.error.message));
|
||||
}
|
||||
|
||||
@@ -982,19 +1019,22 @@ function startBackupTask(auditSource, callback) {
|
||||
let error = locker.lock(locker.OP_FULL_BACKUP);
|
||||
if (error) return callback(new BackupsError(BackupsError.BAD_STATE, `Cannot backup now: ${error.message}`));
|
||||
|
||||
let task = tasks.startTask(tasks.TASK_BACKUP, []);
|
||||
task.on('error', (error) => callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)));
|
||||
task.on('start', (taskId) => {
|
||||
tasks.add(tasks.TASK_BACKUP, [ ], function (error, taskId) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
eventlog.add(eventlog.ACTION_BACKUP_START, auditSource, { taskId });
|
||||
|
||||
tasks.startTask(taskId, { timeout: 12 * 60 * 60 * 1000 /* 12 hours */ }, function (error, backupId) {
|
||||
locker.unlock(locker.OP_FULL_BACKUP);
|
||||
|
||||
const errorMessage = error ? error.message : '';
|
||||
const timedOut = error ? error.code === tasks.ETIMEOUT : false;
|
||||
|
||||
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { taskId, errorMessage, timedOut, backupId });
|
||||
});
|
||||
|
||||
callback(null, taskId);
|
||||
});
|
||||
task.on('finish', (error, result) => {
|
||||
locker.unlock(locker.OP_FULL_BACKUP);
|
||||
|
||||
const errorMessage = error ? error.message : '';
|
||||
|
||||
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { taskId: task.id, errorMessage: errorMessage, backupId: result });
|
||||
});
|
||||
}
|
||||
|
||||
function ensureBackup(auditSource, callback) {
|
||||
@@ -1218,18 +1258,20 @@ function cleanup(auditSource, progressCallback, callback) {
|
||||
}
|
||||
|
||||
function startCleanupTask(auditSource, callback) {
|
||||
let task = tasks.startTask(tasks.TASK_CLEAN_BACKUPS, [ auditSource ]);
|
||||
task.on('error', (error) => callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)));
|
||||
task.on('start', (taskId) => {
|
||||
eventlog.add(eventlog.ACTION_BACKUP_CLEANUP_START, auditSource, { taskId });
|
||||
callback(null, taskId);
|
||||
});
|
||||
task.on('finish', (error, result) => { // result is { removedBoxBackups, removedAppBackups }
|
||||
eventlog.add(eventlog.ACTION_BACKUP_CLEANUP_FINISH, auditSource, {
|
||||
errorMessage: error ? error.message : null,
|
||||
removedBoxBackups: result ? result.removedBoxBackups : [],
|
||||
removedAppBackups: result ? result.removedAppBackups : []
|
||||
|
||||
tasks.add(tasks.TASK_CLEAN_BACKUPS, [ auditSource ], function (error, taskId) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
tasks.startTask(taskId, {}, (error, result) => { // result is { removedBoxBackups, removedAppBackups }
|
||||
eventlog.add(eventlog.ACTION_BACKUP_CLEANUP_FINISH, auditSource, {
|
||||
taskId,
|
||||
errorMessage: error ? error.message : null,
|
||||
removedBoxBackups: result ? result.removedBoxBackups : [],
|
||||
removedAppBackups: result ? result.removedAppBackups : []
|
||||
});
|
||||
});
|
||||
|
||||
callback(null, taskId);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
56
src/boxerror.js
Normal file
56
src/boxerror.js
Normal file
@@ -0,0 +1,56 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
exports = module.exports = BoxError;
|
||||
|
||||
function BoxError(reason, errorOrMessage, details) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
assert(typeof details === 'object' || typeof details === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
this.details = details || {};
|
||||
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else { // error object
|
||||
this.message = errorOrMessage.message;
|
||||
this.nestedError = errorOrMessage;
|
||||
_.extend(this.details, errorOrMessage); // copy enumerable properies
|
||||
}
|
||||
}
|
||||
util.inherits(BoxError, Error);
|
||||
BoxError.ACCESS_DENIED = 'Access Denied';
|
||||
BoxError.ALREADY_EXISTS = 'Already Exists';
|
||||
BoxError.BAD_FIELD = 'Bad Field';
|
||||
BoxError.COLLECTD_ERROR = 'Collectd Error';
|
||||
BoxError.CONFLICT = 'Conflict';
|
||||
BoxError.DATABASE_ERROR = 'Database Error';
|
||||
BoxError.DNS_ERROR = 'DNS Error';
|
||||
BoxError.DOCKER_ERROR = 'Docker Error';
|
||||
BoxError.EXTERNAL_ERROR = 'External Error';
|
||||
BoxError.FS_ERROR = 'FileSystem Error';
|
||||
BoxError.INTERNAL_ERROR = 'Internal Error';
|
||||
BoxError.LOGROTATE_ERROR = 'Logrotate Error';
|
||||
BoxError.NETWORK_ERROR = 'Network Error';
|
||||
BoxError.NOT_FOUND = 'Not found';
|
||||
BoxError.OPENSSL_ERROR = 'OpenSSL Error';
|
||||
BoxError.REVERSEPROXY_ERROR = 'ReverseProxy Error';
|
||||
BoxError.TASK_ERROR = 'Task Error';
|
||||
BoxError.TRY_AGAIN = 'Try Again';
|
||||
BoxError.UNKNOWN_ERROR = 'Unknown Error'; // only used for porting
|
||||
|
||||
BoxError.prototype.toPlainObject = function () {
|
||||
return _.extend({}, { message: this.message, reason: this.reason }, this.details);
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
BoxError = require('../boxerror.js'),
|
||||
crypto = require('crypto'),
|
||||
debug = require('debug')('box:cert/acme2'),
|
||||
domains = require('../domains.js'),
|
||||
@@ -24,31 +25,6 @@ exports = module.exports = {
|
||||
_getChallengeSubdomain: getChallengeSubdomain
|
||||
};
|
||||
|
||||
function Acme2Error(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(Acme2Error, Error);
|
||||
Acme2Error.INTERNAL_ERROR = 'Internal Error';
|
||||
Acme2Error.EXTERNAL_ERROR = 'External Error';
|
||||
Acme2Error.ALREADY_EXISTS = 'Already Exists';
|
||||
Acme2Error.NOT_COMPLETED = 'Not Completed';
|
||||
Acme2Error.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
|
||||
@@ -158,8 +134,8 @@ Acme2.prototype.updateContact = function (registrationUri, callback) {
|
||||
|
||||
const that = this;
|
||||
this.sendSignedRequest(registrationUri, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
|
||||
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to update contact. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error when updating contact: ${error.message}`));
|
||||
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to update contact. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
debug(`updateContact: contact of user updated to ${that.email}`);
|
||||
|
||||
@@ -178,9 +154,9 @@ Acme2.prototype.registerUser = function (callback) {
|
||||
|
||||
var that = this;
|
||||
this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when registering new account: ' + error.message));
|
||||
if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error when registering user: ${error.message}`));
|
||||
// 200 if already exists. 201 for new accounts
|
||||
if (result.statusCode !== 200 && result.statusCode !== 201) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to register new account. Expecting 200 or 201, got %s %s', result.statusCode, result.text)));
|
||||
if (result.statusCode !== 200 && result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to register new account. Expecting 200 or 201, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
debug(`registerUser: user registered keyid: ${result.headers.location}`);
|
||||
|
||||
@@ -204,17 +180,17 @@ Acme2.prototype.newOrder = function (domain, callback) {
|
||||
debug('newOrder: %s', domain);
|
||||
|
||||
this.sendSignedRequest(this.directory.newOrder, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when registering domain: ' + error.message));
|
||||
if (result.statusCode === 403) return callback(new Acme2Error(Acme2Error.FORBIDDEN, result.body.detail));
|
||||
if (result.statusCode !== 201) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error when creating new order: ${error.message}`));
|
||||
if (result.statusCode === 403) return callback(new BoxError(BoxError.ACCESS_DENIED, `Forbidden sending signed request: ${result.body.detail}`));
|
||||
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
debug('newOrder: created order %s %j', domain, result.body);
|
||||
|
||||
const order = result.body, orderUrl = result.headers.location;
|
||||
|
||||
if (!Array.isArray(order.authorizations)) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'invalid authorizations in order'));
|
||||
if (typeof order.finalize !== 'string') return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'invalid finalize in order'));
|
||||
if (typeof orderUrl !== 'string') return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'invalid order location in order header'));
|
||||
if (!Array.isArray(order.authorizations)) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'invalid authorizations in order'));
|
||||
if (typeof order.finalize !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'invalid finalize in order'));
|
||||
if (typeof orderUrl !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'invalid order location in order header'));
|
||||
|
||||
callback(null, order, orderUrl);
|
||||
});
|
||||
@@ -232,18 +208,18 @@ Acme2.prototype.waitForOrder = function (orderUrl, callback) {
|
||||
superagent.get(orderUrl).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) {
|
||||
debug('waitForOrder: network error getting uri %s', orderUrl);
|
||||
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message)); // network error
|
||||
return retryCallback(new BoxError(BoxError.NETWORK_ERROR, `Network error waiting for order: ${error.message}`)); // network error
|
||||
}
|
||||
if (result.statusCode !== 200) {
|
||||
debug('waitForOrder: invalid response code getting uri %s', result.statusCode);
|
||||
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
|
||||
return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
|
||||
}
|
||||
|
||||
debug('waitForOrder: status is "%s %j', result.body.status, result.body);
|
||||
|
||||
if (result.body.status === 'pending' || result.body.status === 'processing') return retryCallback(new Acme2Error(Acme2Error.NOT_COMPLETED));
|
||||
if (result.body.status === 'pending' || result.body.status === 'processing') return retryCallback(new BoxError(BoxError.TRY_AGAIN, `Request is in ${result.body.status} state`));
|
||||
else if (result.body.status === 'valid' && result.body.certificate) return retryCallback(null, result.body.certificate);
|
||||
else return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Unexpected status or invalid response: ' + result.body));
|
||||
else return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, 'Unexpected status or invalid response: ' + result.body));
|
||||
});
|
||||
}, callback);
|
||||
};
|
||||
@@ -277,8 +253,8 @@ Acme2.prototype.notifyChallengeReady = function (challenge, callback) {
|
||||
};
|
||||
|
||||
this.sendSignedRequest(challenge.url, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when notifying challenge: ' + error.message));
|
||||
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error when notifying challenge: ${error.message}`));
|
||||
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
callback();
|
||||
});
|
||||
@@ -296,18 +272,18 @@ Acme2.prototype.waitForChallenge = function (challenge, callback) {
|
||||
superagent.get(challenge.url).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) {
|
||||
debug('waitForChallenge: network error getting uri %s', challenge.url);
|
||||
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message)); // network error
|
||||
return retryCallback(new BoxError(BoxError.NETWORK_ERROR, `Network error waiting for challenge: ${error.message}`));
|
||||
}
|
||||
if (result.statusCode !== 200) {
|
||||
debug('waitForChallenge: invalid response code getting uri %s', result.statusCode);
|
||||
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
|
||||
return retryCallback(new BoxError(BoxError.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 Acme2Error(Acme2Error.NOT_COMPLETED));
|
||||
if (result.body.status === 'pending') return retryCallback(new BoxError(BoxError.TRY_AGAIN));
|
||||
else if (result.body.status === 'valid') return retryCallback();
|
||||
else return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Unexpected status: ' + result.body.status));
|
||||
else return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, 'Unexpected status: ' + result.body.status));
|
||||
});
|
||||
}, function retryFinished(error) {
|
||||
// async.retry will pass 'undefined' as second arg making it unusable with async.waterfall()
|
||||
@@ -329,9 +305,9 @@ Acme2.prototype.signCertificate = function (domain, finalizationUrl, csrDer, cal
|
||||
debug('signCertificate: sending sign request');
|
||||
|
||||
this.sendSignedRequest(finalizationUrl, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when signing certificate: ' + error.message));
|
||||
if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error when signing certificate: ${error.message}`));
|
||||
// 429 means we reached the cert limit for this domain
|
||||
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
@@ -351,15 +327,15 @@ Acme2.prototype.createKeyAndCsr = function (hostname, callback) {
|
||||
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
|
||||
} else {
|
||||
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));
|
||||
if (!key) return callback(new BoxError(BoxError.OPENSSL_ERROR, safe.error));
|
||||
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new BoxError(BoxError.FS_ERROR, safe.error));
|
||||
|
||||
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
|
||||
}
|
||||
|
||||
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
|
||||
if (!csrDer) return callback(new BoxError(BoxError.OPENSSL_ERROR, safe.error));
|
||||
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new BoxError(BoxError.FS_ERROR, safe.error)); // bookkeeping
|
||||
|
||||
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFile);
|
||||
|
||||
@@ -373,25 +349,29 @@ Acme2.prototype.downloadCertificate = function (hostname, certUrl, callback) {
|
||||
|
||||
var outdir = paths.APP_CERTS_DIR;
|
||||
|
||||
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 Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when downloading certificate'));
|
||||
if (result.statusCode === 202) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, 'Retry not implemented yet'));
|
||||
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
|
||||
debug('downloadCertificate: downloading certificate');
|
||||
|
||||
const fullChainPem = result.text;
|
||||
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 retryCallback(new BoxError(BoxError.NETWORK_ERROR, `Network error when downloading certificate: ${error.message}`));
|
||||
if (result.statusCode === 202) return retryCallback(new BoxError(BoxError.TRY_AGAIN, 'Retry'));
|
||||
if (result.statusCode !== 200) return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
const certName = hostname.replace('*.', '_.');
|
||||
var certificateFile = path.join(outdir, `${certName}.cert`);
|
||||
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
|
||||
const fullChainPem = result.text;
|
||||
|
||||
debug('downloadCertificate: cert file for %s saved at %s', hostname, certificateFile);
|
||||
const certName = hostname.replace('*.', '_.');
|
||||
var certificateFile = path.join(outdir, `${certName}.cert`);
|
||||
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return retryCallback(new BoxError(BoxError.FS_ERROR, safe.error));
|
||||
|
||||
callback();
|
||||
});
|
||||
debug('downloadCertificate: cert file for %s saved at %s', hostname, certificateFile);
|
||||
|
||||
retryCallback(null);
|
||||
});
|
||||
}, callback);
|
||||
};
|
||||
|
||||
Acme2.prototype.prepareHttpChallenge = function (hostname, domain, authorization, callback) {
|
||||
@@ -402,7 +382,7 @@ Acme2.prototype.prepareHttpChallenge = function (hostname, domain, authorization
|
||||
|
||||
debug('acmeFlow: challenges: %j', authorization);
|
||||
let httpChallenges = authorization.challenges.filter(function(x) { return x.type === 'http-01'; });
|
||||
if (httpChallenges.length === 0) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'no http challenges'));
|
||||
if (httpChallenges.length === 0) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'no http challenges'));
|
||||
let challenge = httpChallenges[0];
|
||||
|
||||
debug('prepareHttpChallenge: preparing for challenge %j', challenge);
|
||||
@@ -412,7 +392,7 @@ Acme2.prototype.prepareHttpChallenge = function (hostname, domain, authorization
|
||||
debug('prepareHttpChallenge: writing %s to %s', keyAuthorization, path.join(paths.ACME_CHALLENGES_DIR, challenge.token));
|
||||
|
||||
fs.writeFile(path.join(paths.ACME_CHALLENGES_DIR, challenge.token), keyAuthorization, function (error) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.FS_ERROR, error));
|
||||
|
||||
callback(null, challenge);
|
||||
});
|
||||
@@ -454,7 +434,7 @@ Acme2.prototype.prepareDnsChallenge = function (hostname, domain, authorization,
|
||||
|
||||
debug('acmeFlow: challenges: %j', authorization);
|
||||
let dnsChallenges = authorization.challenges.filter(function(x) { return x.type === 'dns-01'; });
|
||||
if (dnsChallenges.length === 0) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'no dns challenges'));
|
||||
if (dnsChallenges.length === 0) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'no dns challenges'));
|
||||
let challenge = dnsChallenges[0];
|
||||
|
||||
const keyAuthorization = this.getKeyAuthorization(challenge.token);
|
||||
@@ -467,10 +447,10 @@ Acme2.prototype.prepareDnsChallenge = function (hostname, domain, authorization,
|
||||
debug(`prepareDnsChallenge: update ${challengeSubdomain} with ${txtValue}`);
|
||||
|
||||
domains.upsertDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message));
|
||||
if (error) return callback(error);
|
||||
|
||||
domains.waitForDnsRecord(challengeSubdomain, domain, 'TXT', txtValue, { interval: 5000, times: 200 }, function (error) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message));
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, challenge);
|
||||
});
|
||||
@@ -493,7 +473,7 @@ Acme2.prototype.cleanupDnsChallenge = function (hostname, domain, challenge, cal
|
||||
debug(`cleanupDnsChallenge: remove ${challengeSubdomain} with ${txtValue}`);
|
||||
|
||||
domains.removeDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error));
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -505,10 +485,12 @@ Acme2.prototype.prepareChallenge = function (hostname, domain, authorizationUrl,
|
||||
assert.strictEqual(typeof authorizationUrl, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`prepareChallenge: http: ${this.performHttpAuthorization}`);
|
||||
|
||||
const that = this;
|
||||
superagent.get(authorizationUrl).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 getting authorization : ' + response.statusCode));
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error when preparing challenge: ${error.message}`));
|
||||
if (response.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response code getting authorization : ' + response.statusCode));
|
||||
|
||||
const authorization = response.body;
|
||||
|
||||
@@ -526,6 +508,8 @@ Acme2.prototype.cleanupChallenge = function (hostname, domain, challenge, callba
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`cleanupChallenge: http: ${this.performHttpAuthorization}`);
|
||||
|
||||
if (this.performHttpAuthorization) {
|
||||
this.cleanupHttpChallenge(hostname, domain, challenge, callback);
|
||||
} else {
|
||||
@@ -541,7 +525,7 @@ Acme2.prototype.acmeFlow = function (hostname, domain, callback) {
|
||||
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 Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
|
||||
if (!this.accountKeyPem) return callback(new BoxError(BoxError.OPENSSL_ERROR, safe.error));
|
||||
|
||||
safe.fs.writeFileSync(paths.ACME_ACCOUNT_KEY_FILE, this.accountKeyPem);
|
||||
} else {
|
||||
@@ -586,8 +570,8 @@ Acme2.prototype.getDirectory = function (callback) {
|
||||
const that = this;
|
||||
|
||||
superagent.get(this.caDirectory).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 directory : ' + response.statusCode));
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error getting directory: ${error.message}`));
|
||||
if (response.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response code when fetching directory : ' + response.statusCode));
|
||||
|
||||
if (typeof response.body.newNonce !== 'string' ||
|
||||
typeof response.body.newOrder !== 'string' ||
|
||||
@@ -631,6 +615,11 @@ function getCertificate(hostname, domain, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var acme = new Acme2(options || { });
|
||||
acme.getCertificate(hostname, domain, callback);
|
||||
let attempt = 1;
|
||||
async.retry({ times: 3, interval: 0 }, function (retryCallback) {
|
||||
debug(`getCertificate: attempt ${attempt++}`);
|
||||
|
||||
let acme = new Acme2(options || { });
|
||||
acme.getCertificate(hostname, domain, retryCallback);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
38
src/changelog.js
Normal file
38
src/changelog.js
Normal file
@@ -0,0 +1,38 @@
|
||||
'use strict';
|
||||
|
||||
let assert = require('assert'),
|
||||
fs = require('fs'),
|
||||
path = require('path');
|
||||
|
||||
exports = module.exports = {
|
||||
getChanges: getChanges
|
||||
};
|
||||
|
||||
function getChanges(version) {
|
||||
assert.strictEqual(typeof version, 'string');
|
||||
|
||||
let changelog = [ ];
|
||||
const lines = fs.readFileSync(path.join(__dirname, '../CHANGES'), 'utf8').split('\n');
|
||||
|
||||
version = version.replace(/[+-].*/, ''); // strip prerelease
|
||||
|
||||
let i;
|
||||
for (i = 0; i < lines.length; i++) {
|
||||
if (lines[i] === '[' + version + ']') break;
|
||||
}
|
||||
|
||||
for (i = i + 1; i < lines.length; i++) {
|
||||
if (lines[i] === '') continue;
|
||||
if (lines[i][0] === '[') break;
|
||||
|
||||
lines[i] = lines[i].trim();
|
||||
|
||||
// detect and remove list style - and * in changelog lines
|
||||
if (lines[i].indexOf('-') === 0) lines[i] = lines[i].slice(1).trim();
|
||||
if (lines[i].indexOf('*') === 0) lines[i] = lines[i].slice(1).trim();
|
||||
|
||||
changelog.push(lines[i]);
|
||||
}
|
||||
|
||||
return changelog;
|
||||
}
|
||||
161
src/cloudron.js
161
src/cloudron.js
@@ -6,7 +6,6 @@ exports = module.exports = {
|
||||
initialize: initialize,
|
||||
uninitialize: uninitialize,
|
||||
getConfig: getConfig,
|
||||
getDisks: getDisks,
|
||||
getLogs: getLogs,
|
||||
|
||||
reboot: reboot,
|
||||
@@ -19,24 +18,22 @@ exports = module.exports = {
|
||||
setDashboardAndMailDomain: setDashboardAndMailDomain,
|
||||
renewCerts: renewCerts,
|
||||
|
||||
runSystemChecks: runSystemChecks,
|
||||
setupDashboard: setupDashboard,
|
||||
|
||||
// exposed for testing
|
||||
_checkDiskSpace: checkDiskSpace
|
||||
runSystemChecks: runSystemChecks,
|
||||
};
|
||||
|
||||
var apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
auditSource = require('./auditsource.js'),
|
||||
backups = require('./backups.js'),
|
||||
clients = require('./clients.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
cron = require('./cron.js'),
|
||||
debug = require('debug')('box:cloudron'),
|
||||
domains = require('./domains.js'),
|
||||
DomainsError = require('./domains.js').DomainsError,
|
||||
df = require('@sindresorhus/df'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
custom = require('./custom.js'),
|
||||
fs = require('fs'),
|
||||
@@ -47,11 +44,14 @@ var apps = require('./apps.js'),
|
||||
paths = require('./paths.js'),
|
||||
platform = require('./platform.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'),
|
||||
TaskError = require('./tasks.js').TaskError,
|
||||
users = require('./users.js'),
|
||||
util = require('util');
|
||||
|
||||
@@ -89,7 +89,7 @@ function initialize(callback) {
|
||||
|
||||
runStartupTasks();
|
||||
|
||||
callback();
|
||||
notifyUpdate(callback);
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
@@ -113,13 +113,32 @@ function onActivated(callback) {
|
||||
], callback);
|
||||
}
|
||||
|
||||
function notifyUpdate(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const version = safe.fs.readFileSync(paths.VERSION_FILE, 'utf8');
|
||||
if (version === constants.VERSION) return callback();
|
||||
|
||||
eventlog.add(eventlog.ACTION_UPDATE_FINISH, auditSource.CRON, { oldVersion: version || 'dev', newVersion: constants.VERSION }, function (error) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
tasks.setCompletedByType(tasks.TASK_UPDATE, { error: null }, function (error) {
|
||||
if (error && error.reason !== TaskError.NOT_FOUND) return callback(error); // when hotfixing, task may not exist
|
||||
|
||||
safe.fs.writeFileSync(paths.VERSION_FILE, constants.VERSION, 'utf8');
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// each of these tasks can fail. we will add some routes to fix/re-run them
|
||||
function runStartupTasks() {
|
||||
// configure nginx to be reachable by IP
|
||||
reverseProxy.configureDefaultServer(NOOP_CALLBACK);
|
||||
reverseProxy.writeDefaultConfig(NOOP_CALLBACK);
|
||||
|
||||
// always generate webadmin config since we have no versioning mechanism for the ejs
|
||||
if (config.adminDomain()) reverseProxy.writeAdminConfig(config.adminDomain(), NOOP_CALLBACK);
|
||||
if (settings.adminDomain()) reverseProxy.writeAdminConfig(settings.adminDomain(), NOOP_CALLBACK);
|
||||
|
||||
// check activation state and start the platform
|
||||
users.isActivated(function (error, activated) {
|
||||
@@ -130,32 +149,6 @@ function runStartupTasks() {
|
||||
});
|
||||
}
|
||||
|
||||
function getDisks(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var disks = {
|
||||
boxDataDisk: null,
|
||||
platformDataDisk: null,
|
||||
appsDataDisk: null
|
||||
};
|
||||
|
||||
df.file(paths.BOX_DATA_DIR).then(function (result) {
|
||||
disks.boxDataDisk = result.filesystem;
|
||||
|
||||
return df.file(paths.PLATFORM_DATA_DIR);
|
||||
}).then(function (result) {
|
||||
disks.platformDataDisk = result.filesystem;
|
||||
|
||||
return df.file(paths.APPS_DATA_DIR);
|
||||
}).then(function (result) {
|
||||
disks.appsDataDisk = result.filesystem;
|
||||
|
||||
callback(null, disks);
|
||||
}).catch(function (error) {
|
||||
callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
});
|
||||
}
|
||||
|
||||
function getConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -164,18 +157,17 @@ function getConfig(callback) {
|
||||
|
||||
// be picky about what we send out here since this is sent for 'normal' users as well
|
||||
callback(null, {
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin(),
|
||||
adminDomain: config.adminDomain(),
|
||||
adminFqdn: config.adminFqdn(),
|
||||
mailFqdn: config.mailFqdn(),
|
||||
version: config.version(),
|
||||
isDemo: config.isDemo(),
|
||||
apiServerOrigin: settings.apiServerOrigin(),
|
||||
webServerOrigin: settings.webServerOrigin(),
|
||||
adminDomain: settings.adminDomain(),
|
||||
adminFqdn: settings.adminFqdn(),
|
||||
mailFqdn: settings.mailFqdn(),
|
||||
version: constants.VERSION,
|
||||
isDemo: settings.isDemo(),
|
||||
memory: os.totalmem(),
|
||||
provider: config.provider(),
|
||||
provider: sysinfo.provider(),
|
||||
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
|
||||
features: custom.features(),
|
||||
supportEmail: custom.supportEmail()
|
||||
uiSpec: custom.uiSpec()
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -195,7 +187,6 @@ function isRebootRequired(callback) {
|
||||
function runSystemChecks() {
|
||||
async.parallel([
|
||||
checkBackupConfiguration,
|
||||
checkDiskSpace,
|
||||
checkMailStatus,
|
||||
checkRebootRequired
|
||||
], function (error) {
|
||||
@@ -215,45 +206,6 @@ function checkBackupConfiguration(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function checkDiskSpace(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('Checking disk space');
|
||||
|
||||
getDisks(function (error, disks) {
|
||||
if (error) {
|
||||
debug('df error %s', error.message);
|
||||
return callback();
|
||||
}
|
||||
|
||||
df().then(function (entries) {
|
||||
/*
|
||||
[{
|
||||
filesystem: '/dev/disk1',
|
||||
size: 499046809600,
|
||||
used: 443222245376,
|
||||
available: 55562420224,
|
||||
capacity: 0.89,
|
||||
mountpoint: '/'
|
||||
}, ...]
|
||||
*/
|
||||
var oos = entries.some(function (entry) {
|
||||
// ignore other filesystems but where box, app and platform data is
|
||||
if (entry.filesystem !== disks.boxDataDisk && entry.filesystem !== disks.platformDataDisk && entry.filesystem !== disks.appsDataDisk) return false;
|
||||
|
||||
return (entry.available <= (1.25 * 1024 * 1024 * 1024)); // 1.5G
|
||||
});
|
||||
|
||||
debug('Disk space checked. ok: %s', !oos);
|
||||
|
||||
notifications.alert(notifications.ALERT_DISK_SPACE, 'Server is running out of disk space', oos ? JSON.stringify(entries, null, 4) : '', callback);
|
||||
}).catch(function (error) {
|
||||
if (error) console.error(error);
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function checkMailStatus(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -343,9 +295,13 @@ function prepareDashboardDomain(domain, auditSource, callback) {
|
||||
const conflict = result.filter(app => app.fqdn === fqdn);
|
||||
if (conflict.length) return callback(new CloudronError(CloudronError.BAD_STATE, 'Dashboard location conflicts with an existing app'));
|
||||
|
||||
let task = tasks.startTask(tasks.TASK_PREPARE_DASHBOARD_DOMAIN, [ domain, auditSource ]);
|
||||
task.on('error', (error) => callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)));
|
||||
task.on('start', (taskId) => callback(null, taskId));
|
||||
tasks.add(tasks.TASK_PREPARE_DASHBOARD_DOMAIN, [ domain, auditSource ], function (error, taskId) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
tasks.startTask(taskId, {}, NOOP_CALLBACK);
|
||||
|
||||
callback(null, taskId);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -367,10 +323,10 @@ function setDashboardDomain(domain, auditSource, callback) {
|
||||
|
||||
const fqdn = domains.fqdn(constants.ADMIN_LOCATION, domainObject);
|
||||
|
||||
config.setAdminDomain(domain);
|
||||
config.setAdminFqdn(fqdn);
|
||||
|
||||
clients.addDefaultClients(config.adminOrigin(), function (error) {
|
||||
async.series([
|
||||
(done) => settings.setAdmin(domain, fqdn, done),
|
||||
(done) => clients.addDefaultClients(settings.adminOrigin(), done)
|
||||
], function (error) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
eventlog.add(eventlog.ACTION_DASHBOARD_DOMAIN_UPDATE, auditSource, { domain: domain, fqdn: fqdn });
|
||||
@@ -398,12 +354,27 @@ function setDashboardAndMailDomain(domain, auditSource, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function setupDashboard(auditSource, progressCallback, callback) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
async.series([
|
||||
domains.prepareDashboardDomain.bind(null, settings.adminDomain(), auditSource, progressCallback),
|
||||
setDashboardDomain.bind(null, settings.adminDomain(), auditSource)
|
||||
], callback);
|
||||
}
|
||||
|
||||
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));
|
||||
tasks.add(tasks.TASK_RENEW_CERTS, [ options, auditSource ], function (error, taskId) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
tasks.startTask(taskId, {}, NOOP_CALLBACK);
|
||||
|
||||
callback(null, taskId);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -30,3 +30,13 @@ LoadPlugin "table"
|
||||
</Result>
|
||||
</Table>
|
||||
</Plugin>
|
||||
|
||||
<Plugin python>
|
||||
<Module du>
|
||||
<Path>
|
||||
Instance "<%= appId %>"
|
||||
Dir "<%= appDataDir %>"
|
||||
</Path>
|
||||
</Module>
|
||||
</Plugin>
|
||||
|
||||
|
||||
203
src/config.js
203
src/config.js
@@ -1,203 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
baseDir: baseDir,
|
||||
|
||||
// values set here will be lost after a upgrade/update. use the sqlite database
|
||||
// for persistent values that need to be backed up
|
||||
get: get,
|
||||
set: set,
|
||||
|
||||
// ifdefs to check environment
|
||||
CLOUDRON: process.env.BOX_ENV === 'cloudron',
|
||||
TEST: process.env.BOX_ENV === 'test',
|
||||
|
||||
// convenience getters
|
||||
provider: provider,
|
||||
apiServerOrigin: apiServerOrigin,
|
||||
webServerOrigin: webServerOrigin,
|
||||
adminDomain: adminDomain,
|
||||
setFqdn: setAdminDomain,
|
||||
setAdminDomain: setAdminDomain,
|
||||
setAdminFqdn: setAdminFqdn,
|
||||
version: version,
|
||||
database: database,
|
||||
|
||||
// these values are derived
|
||||
adminOrigin: adminOrigin,
|
||||
internalAdminOrigin: internalAdminOrigin,
|
||||
sysadminOrigin: sysadminOrigin, // localhost routes
|
||||
adminFqdn: adminFqdn,
|
||||
mailFqdn: mailFqdn,
|
||||
hasIPv6: hasIPv6,
|
||||
|
||||
isDemo: isDemo,
|
||||
|
||||
// for testing resets to defaults
|
||||
_reset: _reset
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
safe = require('safetydance'),
|
||||
_ = require('underscore');
|
||||
|
||||
|
||||
// assert on unknown environment can't proceed
|
||||
assert(exports.CLOUDRON || exports.TEST, 'Unknown environment. This should not happen!');
|
||||
|
||||
var data = { };
|
||||
|
||||
function baseDir() {
|
||||
const homeDir = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
|
||||
if (exports.CLOUDRON) return homeDir;
|
||||
if (exports.TEST) return path.join(homeDir, '.cloudron_test');
|
||||
// cannot reach
|
||||
}
|
||||
|
||||
const cloudronConfigFileName = exports.CLOUDRON ? '/etc/cloudron/cloudron.conf' : path.join(baseDir(), 'cloudron.conf');
|
||||
|
||||
function saveSync() {
|
||||
// only save values we want to have in the cloudron.conf, see start.sh
|
||||
var conf = {
|
||||
apiServerOrigin: data.apiServerOrigin,
|
||||
webServerOrigin: data.webServerOrigin,
|
||||
adminDomain: data.adminDomain,
|
||||
adminFqdn: data.adminFqdn,
|
||||
provider: data.provider,
|
||||
isDemo: data.isDemo
|
||||
};
|
||||
|
||||
fs.writeFileSync(cloudronConfigFileName, JSON.stringify(conf, null, 4)); // functions are ignored by JSON.stringify
|
||||
}
|
||||
|
||||
function _reset(callback) {
|
||||
safe.fs.unlinkSync(cloudronConfigFileName);
|
||||
|
||||
initConfig();
|
||||
|
||||
if (callback) callback();
|
||||
}
|
||||
|
||||
function initConfig() {
|
||||
// setup defaults
|
||||
data.adminFqdn = '';
|
||||
data.adminDomain = '';
|
||||
data.port = 3000;
|
||||
data.apiServerOrigin = null;
|
||||
data.webServerOrigin = null;
|
||||
data.provider = 'generic';
|
||||
data.smtpPort = 2525; // this value comes from mail container
|
||||
data.sysadminPort = 3001;
|
||||
data.ldapPort = 3002;
|
||||
data.dockerProxyPort = 3003;
|
||||
|
||||
// keep in sync with start.sh
|
||||
data.database = {
|
||||
hostname: '127.0.0.1',
|
||||
username: 'root',
|
||||
password: 'password',
|
||||
port: 3306,
|
||||
name: 'box'
|
||||
};
|
||||
|
||||
// overrides for local testings
|
||||
if (exports.TEST) {
|
||||
data.port = 5454;
|
||||
data.apiServerOrigin = 'http://localhost:6060'; // hock doesn't support https
|
||||
|
||||
// see setupTest script how the mysql-server is run
|
||||
data.database.hostname = require('child_process').execSync('docker inspect -f "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" mysql-server').toString().trim();
|
||||
}
|
||||
|
||||
// overwrite defaults with saved config
|
||||
var existingData = safe.JSON.parse(safe.fs.readFileSync(cloudronConfigFileName, 'utf8'));
|
||||
_.extend(data, existingData);
|
||||
}
|
||||
|
||||
initConfig();
|
||||
|
||||
// set(obj) or set(key, value)
|
||||
function set(key, value) {
|
||||
if (typeof key === 'object') {
|
||||
var obj = key;
|
||||
for (var k in obj) {
|
||||
assert(k in data, 'config.js is missing key "' + k + '"');
|
||||
data[k] = obj[k];
|
||||
}
|
||||
} else {
|
||||
data = safe.set(data, key, value);
|
||||
}
|
||||
|
||||
saveSync();
|
||||
}
|
||||
|
||||
function get(key) {
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
|
||||
return safe.query(data, key);
|
||||
}
|
||||
|
||||
function apiServerOrigin() {
|
||||
return get('apiServerOrigin');
|
||||
}
|
||||
|
||||
function webServerOrigin() {
|
||||
return get('webServerOrigin');
|
||||
}
|
||||
|
||||
function setAdminDomain(domain) {
|
||||
set('adminDomain', domain);
|
||||
}
|
||||
|
||||
function adminDomain() {
|
||||
return get('adminDomain');
|
||||
}
|
||||
|
||||
function setAdminFqdn(adminFqdn) {
|
||||
set('adminFqdn', adminFqdn);
|
||||
}
|
||||
|
||||
function adminFqdn() {
|
||||
return get('adminFqdn');
|
||||
}
|
||||
|
||||
function mailFqdn() {
|
||||
return adminFqdn();
|
||||
}
|
||||
|
||||
function adminOrigin() {
|
||||
return 'https://' + adminFqdn();
|
||||
}
|
||||
|
||||
function internalAdminOrigin() {
|
||||
return 'http://127.0.0.1:' + get('port');
|
||||
}
|
||||
|
||||
function sysadminOrigin() {
|
||||
return 'http://127.0.0.1:' + get('sysadminPort');
|
||||
}
|
||||
|
||||
function version() {
|
||||
if (exports.TEST) return '3.0.0-test';
|
||||
return fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim();
|
||||
}
|
||||
|
||||
function database() {
|
||||
return get('database');
|
||||
}
|
||||
|
||||
function isDemo() {
|
||||
return get('isDemo') === true;
|
||||
}
|
||||
|
||||
function provider() {
|
||||
return get('provider');
|
||||
}
|
||||
|
||||
function hasIPv6() {
|
||||
const IPV6_PROC_FILE = '/proc/net/if_inet6';
|
||||
// on contabo, /proc/net/if_inet6 is an empty file. so just exists is not enough
|
||||
return fs.existsSync(IPV6_PROC_FILE) && fs.readFileSync(IPV6_PROC_FILE, 'utf8').trim().length !== 0;
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
let fs = require('fs'),
|
||||
path = require('path');
|
||||
|
||||
const CLOUDRON = process.env.BOX_ENV === 'cloudron',
|
||||
TEST = process.env.BOX_ENV === 'test';
|
||||
|
||||
exports = module.exports = {
|
||||
API_LOCATION: 'api', // this is unused but reserved for future use (#403)
|
||||
SMTP_LOCATION: 'smtp',
|
||||
IMAP_LOCATION: 'imap',
|
||||
|
||||
@@ -18,7 +23,12 @@ exports = module.exports = {
|
||||
],
|
||||
|
||||
ADMIN_LOCATION: 'my',
|
||||
DKIM_SELECTOR: 'cloudron',
|
||||
|
||||
PORT: CLOUDRON ? 3000 : 5454,
|
||||
INTERNAL_SMTP_PORT: 2525, // this value comes from the mail container
|
||||
SYSADMIN_PORT: 3001, // unused
|
||||
LDAP_PORT: 3002,
|
||||
DOCKER_PROXY_PORT: 3003,
|
||||
|
||||
NGINX_DEFAULT_CONFIG_FILE_NAME: 'default.conf',
|
||||
|
||||
@@ -32,6 +42,11 @@ exports = module.exports = {
|
||||
|
||||
AUTOUPDATE_PATTERN_NEVER: 'never',
|
||||
|
||||
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8)
|
||||
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8),
|
||||
|
||||
CLOUDRON: CLOUDRON,
|
||||
TEST: TEST,
|
||||
|
||||
VERSION: process.env.BOX_ENV === 'cloudron' ? fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim() : '4.2.0-test'
|
||||
};
|
||||
|
||||
|
||||
18
src/cron.js
18
src/cron.js
@@ -15,10 +15,10 @@ var appHealthMonitor = require('./apphealthmonitor.js'),
|
||||
auditSource = require('./auditsource.js'),
|
||||
backups = require('./backups.js'),
|
||||
cloudron = require('./cloudron.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
CronJob = require('cron').CronJob,
|
||||
debug = require('debug')('box:cron'),
|
||||
disks = require('./disks.js'),
|
||||
dyndns = require('./dyndns.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
janitor = require('./janitor.js'),
|
||||
@@ -35,6 +35,7 @@ var gJobs = {
|
||||
backup: null,
|
||||
boxUpdateChecker: null,
|
||||
systemChecks: null,
|
||||
diskSpaceChecker: null,
|
||||
certificateRenew: null,
|
||||
cleanupBackups: null,
|
||||
cleanupEventlog: null,
|
||||
@@ -112,6 +113,15 @@ function recreateJobs(tz) {
|
||||
timeZone: tz
|
||||
});
|
||||
|
||||
if (gJobs.diskSpaceChecker) gJobs.diskSpaceChecker.stop();
|
||||
gJobs.diskSpaceChecker = new CronJob({
|
||||
cronTime: '00 30 * * * *', // every 30 minutes. if you change this interval, change the notification messages with correct duration
|
||||
onTick: () => disks.checkDiskSpace(NOOP_CALLBACK),
|
||||
start: true,
|
||||
runOnInit: true, // run system check immediately
|
||||
timeZone: tz
|
||||
});
|
||||
|
||||
// randomized pattern per cloudron every hour
|
||||
var randomMinute = Math.floor(60*Math.random());
|
||||
|
||||
@@ -165,7 +175,7 @@ function recreateJobs(tz) {
|
||||
|
||||
if (gJobs.schedulerSync) gJobs.schedulerSync.stop();
|
||||
gJobs.schedulerSync = new CronJob({
|
||||
cronTime: config.TEST ? '*/10 * * * * *' : '00 */1 * * * *', // every minute
|
||||
cronTime: constants.TEST ? '*/10 * * * * *' : '00 */1 * * * *', // every minute
|
||||
onTick: scheduler.sync,
|
||||
start: true,
|
||||
timeZone: tz
|
||||
@@ -204,7 +214,7 @@ function boxAutoupdatePatternChanged(pattern) {
|
||||
var updateInfo = updateChecker.getUpdateInfo();
|
||||
if (updateInfo.box) {
|
||||
debug('Starting autoupdate to %j', updateInfo.box);
|
||||
updater.updateToLatest(auditSource.CRON, NOOP_CALLBACK);
|
||||
updater.updateToLatest({ skipBackup: false }, auditSource.CRON, NOOP_CALLBACK);
|
||||
} else {
|
||||
debug('No box auto updates available');
|
||||
}
|
||||
@@ -248,7 +258,7 @@ function dynamicDnsChanged(enabled) {
|
||||
|
||||
if (enabled) {
|
||||
gJobs.dynamicDns = new CronJob({
|
||||
cronTime: '00 */10 * * * *',
|
||||
cronTime: '5 * * * * *', // we only update the records if the ip has changed.
|
||||
onTick: dyndns.sync.bind(null, auditSource.CRON, NOOP_CALLBACK),
|
||||
start: true,
|
||||
timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack
|
||||
|
||||
@@ -1,44 +1,66 @@
|
||||
'use strict';
|
||||
|
||||
let debug = require('debug')('box:features'),
|
||||
let debug = require('debug')('box:custom'),
|
||||
lodash = require('lodash'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
yaml = require('js-yaml');
|
||||
|
||||
exports = module.exports = {
|
||||
features: features,
|
||||
supportEmail: supportEmail,
|
||||
alertsEmail: alertsEmail,
|
||||
sendAlertsToCloudronAdmins: sendAlertsToCloudronAdmins
|
||||
uiSpec: uiSpec,
|
||||
spec: spec
|
||||
};
|
||||
|
||||
const gCustom = (function () {
|
||||
const DEFAULT_SPEC = {
|
||||
appstore: {
|
||||
blacklist: [],
|
||||
whitelist: null // null imples, not set. this is an object and not an array
|
||||
},
|
||||
backups: {
|
||||
configurable: true
|
||||
},
|
||||
domains: {
|
||||
dynamicDns: true,
|
||||
changeDashboardDomain: true
|
||||
},
|
||||
subscription: {
|
||||
configurable: true
|
||||
},
|
||||
support: {
|
||||
email: 'support@cloudron.io',
|
||||
remoteSupport: true,
|
||||
ticketFormBody:
|
||||
'Use this form to open support tickets. You can also write directly to [support@cloudron.io](mailto:support@cloudron.io).\n\n'
|
||||
+ '* [Knowledge Base & App Docs](https://cloudron.io/documentation/apps/?support_view)\n'
|
||||
+ '* [Custom App Packaging & API](https://cloudron.io/developer/packaging/?support_view)\n'
|
||||
+ '* [Forum](https://forum.cloudron.io/)\n\n',
|
||||
submitTickets: true
|
||||
},
|
||||
alerts: {
|
||||
email: '',
|
||||
notifyCloudronAdmins: true
|
||||
},
|
||||
footer: {
|
||||
body: '© 2019 [Cloudron](https://cloudron.io) [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)'
|
||||
}
|
||||
};
|
||||
|
||||
const gSpec = (function () {
|
||||
try {
|
||||
if (!safe.fs.existsSync(paths.CUSTOM_FILE)) return {};
|
||||
return yaml.safeLoad(safe.fs.readFileSync(paths.CUSTOM_FILE, 'utf8'));
|
||||
if (!safe.fs.existsSync(paths.CUSTOM_FILE)) return DEFAULT_SPEC;
|
||||
const c = yaml.safeLoad(safe.fs.readFileSync(paths.CUSTOM_FILE, 'utf8'));
|
||||
return lodash.merge({}, DEFAULT_SPEC, c);
|
||||
} catch (e) {
|
||||
debug(`Error loading features file from ${paths.CUSTOM_FILE} : ${e.message}`);
|
||||
return {};
|
||||
return DEFAULT_SPEC;
|
||||
}
|
||||
})();
|
||||
|
||||
function features() {
|
||||
return {
|
||||
dynamicDns: safe.query(gCustom, 'features.dynamicDns', true),
|
||||
remoteSupport: safe.query(gCustom, 'features.remoteSupport', true),
|
||||
subscription: safe.query(gCustom, 'features.subscription', true),
|
||||
configureBackup: safe.query(gCustom, 'features.configureBackup', true)
|
||||
};
|
||||
// flags sent to the UI. this is separate because we have values that are secret to the backend
|
||||
function uiSpec() {
|
||||
return gSpec;
|
||||
}
|
||||
|
||||
function supportEmail() {
|
||||
return safe.query(gCustom, 'support.email', 'support@cloudron.io');
|
||||
}
|
||||
|
||||
function alertsEmail() {
|
||||
return safe.query(gCustom, 'alerts.email', '');
|
||||
}
|
||||
|
||||
function sendAlertsToCloudronAdmins() {
|
||||
return safe.query(gCustom, 'alerts.notifyCloudronAdmins', true);
|
||||
}
|
||||
function spec() {
|
||||
return gSpec;
|
||||
}
|
||||
@@ -15,7 +15,7 @@ exports = module.exports = {
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
child_process = require('child_process'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
mysql = require('mysql'),
|
||||
once = require('once'),
|
||||
util = require('util');
|
||||
@@ -23,25 +23,38 @@ var assert = require('assert'),
|
||||
var gConnectionPool = null,
|
||||
gDefaultConnection = null;
|
||||
|
||||
const gDatabase = {
|
||||
hostname: '127.0.0.1',
|
||||
username: 'root',
|
||||
password: 'password',
|
||||
port: 3306,
|
||||
name: 'box'
|
||||
};
|
||||
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (gConnectionPool !== null) return callback(null);
|
||||
|
||||
if (constants.TEST) {
|
||||
// see setupTest script how the mysql-server is run
|
||||
gDatabase.hostname = require('child_process').execSync('docker inspect -f "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" mysql-server').toString().trim();
|
||||
}
|
||||
|
||||
gConnectionPool = mysql.createPool({
|
||||
connectionLimit: 5, // this has to be > 1 since we store one connection as 'default'. the rest for transactions
|
||||
host: config.database().hostname,
|
||||
user: config.database().username,
|
||||
password: config.database().password,
|
||||
port: config.database().port,
|
||||
database: config.database().name,
|
||||
host: gDatabase.hostname,
|
||||
user: gDatabase.username,
|
||||
password: gDatabase.password,
|
||||
port: gDatabase.port,
|
||||
database: gDatabase.name,
|
||||
multipleStatements: false,
|
||||
ssl: false,
|
||||
timezone: 'Z' // mysql follows the SYSTEM timezone. on Cloudron, this is UTC
|
||||
});
|
||||
|
||||
gConnectionPool.on('connection', function (connection) {
|
||||
connection.query('USE ' + config.database().name);
|
||||
connection.query('USE ' + gDatabase.name);
|
||||
connection.query('SET SESSION sql_mode = \'strict_all_tables\'');
|
||||
});
|
||||
|
||||
@@ -87,8 +100,8 @@ function clear(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var cmd = util.format('mysql --host="%s" --user="%s" --password="%s" -Nse "SHOW TABLES" %s | grep -v "^migrations$" | while read table; do mysql --host="%s" --user="%s" --password="%s" -e "SET FOREIGN_KEY_CHECKS = 0; TRUNCATE TABLE $table" %s; done',
|
||||
config.database().hostname, config.database().username, config.database().password, config.database().name,
|
||||
config.database().hostname, config.database().username, config.database().password, config.database().name);
|
||||
gDatabase.hostname, gDatabase.username, gDatabase.password, gDatabase.name,
|
||||
gDatabase.hostname, gDatabase.username, gDatabase.password, gDatabase.name);
|
||||
|
||||
async.series([
|
||||
child_process.exec.bind(null, cmd),
|
||||
@@ -178,7 +191,7 @@ function importFromFile(file, callback) {
|
||||
assert.strictEqual(typeof file, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var cmd = `/usr/bin/mysql -h "${config.database().hostname}" -u ${config.database().username} -p${config.database().password} ${config.database().name} < ${file}`;
|
||||
var cmd = `/usr/bin/mysql -h "${gDatabase.hostname}" -u ${gDatabase.username} -p${gDatabase.password} ${gDatabase.name} < ${file}`;
|
||||
|
||||
async.series([
|
||||
query.bind(null, 'CREATE DATABASE IF NOT EXISTS box'),
|
||||
@@ -190,7 +203,7 @@ function exportToFile(file, callback) {
|
||||
assert.strictEqual(typeof file, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var cmd = `/usr/bin/mysqldump -h "${config.database().hostname}" -u root -p${config.database().password} --single-transaction --routines --triggers ${config.database().name} > "${file}"`;
|
||||
var cmd = `/usr/bin/mysqldump -h "${gDatabase.hostname}" -u root -p${gDatabase.password} --single-transaction --routines --triggers ${gDatabase.name} > "${file}"`;
|
||||
|
||||
child_process.exec(cmd, callback);
|
||||
}
|
||||
|
||||
118
src/disks.js
Normal file
118
src/disks.js
Normal file
@@ -0,0 +1,118 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
getDisks: getDisks,
|
||||
checkDiskSpace: checkDiskSpace
|
||||
};
|
||||
|
||||
const apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
debug = require('debug')('box:disks'),
|
||||
df = require('@sindresorhus/df'),
|
||||
docker = require('./docker.js'),
|
||||
notifications = require('./notifications.js'),
|
||||
paths = require('./paths.js'),
|
||||
util = require('util');
|
||||
|
||||
function DisksError(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(DisksError, Error);
|
||||
DisksError.INTERNAL_ERROR = 'Internal Error';
|
||||
DisksError.EXTERNAL_ERROR = 'External Error';
|
||||
|
||||
function getDisks(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const dfAsync = async.asyncify(df), dfFileAsync = async.asyncify(df.file);
|
||||
|
||||
docker.info(function (error, info) {
|
||||
if (error) return callback(new DisksError(DisksError.INTERNAL_ERROR, error));
|
||||
|
||||
async.series([
|
||||
dfAsync,
|
||||
dfFileAsync.bind(null, paths.BOX_DATA_DIR),
|
||||
dfFileAsync.bind(null, paths.PLATFORM_DATA_DIR),
|
||||
dfFileAsync.bind(null, paths.APPS_DATA_DIR),
|
||||
dfFileAsync.bind(null, info.DockerRootDir)
|
||||
], function (error, values) {
|
||||
if (error) return callback(new DisksError(DisksError.INTERNAL_ERROR, error));
|
||||
|
||||
// filter by ext4 and then sort to make sure root disk is first
|
||||
const ext4Disks = values[0].filter((r) => r.type === 'ext4').sort((a, b) => a.mountpoint.localeCompare(b.mountpoint));
|
||||
|
||||
const disks = {
|
||||
disks: ext4Disks, // root disk is first
|
||||
boxDataDisk: values[1].filesystem,
|
||||
mailDataDisk: values[1].filesystem,
|
||||
platformDataDisk: values[2].filesystem,
|
||||
appsDataDisk: values[3].filesystem,
|
||||
dockerDataDisk: values[4].filesystem,
|
||||
apps: {}
|
||||
};
|
||||
|
||||
apps.getAll(function (error, allApps) {
|
||||
if (error) return callback(new DisksError(DisksError.INTERNAL_ERROR, error));
|
||||
|
||||
async.eachSeries(allApps, function (app, iteratorDone) {
|
||||
if (!app.dataDir) {
|
||||
disks.apps[app.id] = disks.appsDataDisk;
|
||||
return iteratorDone();
|
||||
}
|
||||
|
||||
dfFileAsync(app.dataDir, function (error, result) {
|
||||
disks.apps[app.id] = error ? disks.appsDataDisk : result.filesystem; // ignore any errors
|
||||
iteratorDone();
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) return callback(new DisksError(DisksError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, disks);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function checkDiskSpace(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('Checking disk space');
|
||||
|
||||
getDisks(function (error, disks) {
|
||||
if (error) {
|
||||
debug('checkDiskSpace: error getting disks %s', error.message);
|
||||
return callback();
|
||||
}
|
||||
|
||||
var oos = disks.disks.some(function (entry) {
|
||||
// ignore other filesystems but where box, app and platform data is
|
||||
if (entry.filesystem !== disks.boxDataDisk
|
||||
&& entry.filesystem !== disks.platformDataDisk
|
||||
&& entry.filesystem !== disks.appsDataDisk
|
||||
&& entry.filesystem !== disks.dockerDataDisk) return false;
|
||||
|
||||
return (entry.available <= (1.25 * 1024 * 1024 * 1024)); // 1.5G
|
||||
});
|
||||
|
||||
debug('checkDiskSpace: disk space checked. ok: %s', !oos);
|
||||
|
||||
notifications.alert(notifications.ALERT_DISK_SPACE, 'Server is running out of disk space', oos ? JSON.stringify(disks.disks, null, 4) : '', callback);
|
||||
});
|
||||
}
|
||||
@@ -11,10 +11,10 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
config = require('../config.js'),
|
||||
debug = require('debug')('box:dns/caas'),
|
||||
domains = require('../domains.js'),
|
||||
DomainsError = require('../domains.js').DomainsError,
|
||||
settings = require('../settings.js'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util'),
|
||||
waitForDns = require('./waitfordns.js');
|
||||
@@ -58,7 +58,7 @@ function upsert(domainObject, location, type, values, callback) {
|
||||
};
|
||||
|
||||
superagent
|
||||
.post(config.apiServerOrigin() + '/api/v1/caas/domains/' + fqdn)
|
||||
.post(settings.apiServerOrigin() + '/api/v1/caas/domains/' + fqdn)
|
||||
.query({ token: dnsConfig.token })
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
@@ -84,7 +84,7 @@ function get(domainObject, location, type, callback) {
|
||||
debug('get: zoneName: %s subdomain: %s type: %s fqdn: %s', domainObject.domain, location, type, fqdn);
|
||||
|
||||
superagent
|
||||
.get(config.apiServerOrigin() + '/api/v1/caas/domains/' + fqdn)
|
||||
.get(settings.apiServerOrigin() + '/api/v1/caas/domains/' + fqdn)
|
||||
.query({ token: dnsConfig.token, type: type })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
@@ -111,7 +111,7 @@ function del(domainObject, location, type, values, callback) {
|
||||
};
|
||||
|
||||
superagent
|
||||
.del(config.apiServerOrigin() + '/api/v1/caas/domains/' + getFqdn(location, domainObject.domain))
|
||||
.del(settings.apiServerOrigin() + '/api/v1/caas/domains/' + getFqdn(location, domainObject.domain))
|
||||
.query({ token: dnsConfig.token })
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
|
||||
@@ -66,12 +66,10 @@ function getInternal(dnsConfig, zoneName, name, type, callback) {
|
||||
|
||||
nextPage = (result.body.links && result.body.links.pages) ? result.body.links.pages.next : null;
|
||||
|
||||
debug(`getInternal: next page - ${nextPage}`);
|
||||
|
||||
iteratorDone();
|
||||
});
|
||||
}, function () { return !!nextPage; }, function (error) {
|
||||
debug('getInternal:', error, matchingRecords);
|
||||
debug('getInternal:', error, JSON.stringify(matchingRecords));
|
||||
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -111,7 +109,7 @@ function upsert(domainObject, location, type, values, callback) {
|
||||
name: name,
|
||||
data: value,
|
||||
priority: priority,
|
||||
ttl: 1
|
||||
ttl: 30 // Recent DO DNS API break means this value must atleast be 30
|
||||
};
|
||||
|
||||
if (i >= result.length) {
|
||||
|
||||
@@ -15,7 +15,7 @@ var assert = require('assert'),
|
||||
dns = require('../native-dns.js'),
|
||||
domains = require('../domains.js'),
|
||||
DomainsError = require('../domains.js').DomainsError,
|
||||
GCDNS = require('@google-cloud/dns'),
|
||||
GCDNS = require('@google-cloud/dns').DNS,
|
||||
util = require('util'),
|
||||
waitForDns = require('./waitfordns.js'),
|
||||
_ = require('underscore');
|
||||
@@ -46,7 +46,7 @@ function getZoneByName(dnsConfig, zoneName, callback) {
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var gcdns = GCDNS(getDnsCredentials(dnsConfig));
|
||||
var gcdns = new GCDNS(getDnsCredentials(dnsConfig));
|
||||
|
||||
gcdns.getZones(function (error, zones) {
|
||||
if (error && error.message === 'invalid_grant') return callback(new DomainsError(DomainsError.ACCESS_DENIED, 'The key was probably revoked'));
|
||||
|
||||
@@ -15,14 +15,14 @@ var assert = require('assert'),
|
||||
dns = require('../native-dns.js'),
|
||||
domains = require('../domains.js'),
|
||||
DomainsError = require('../domains.js').DomainsError,
|
||||
Namecheap = require('namecheap'),
|
||||
safe = require('safetydance'),
|
||||
superagent = require('superagent'),
|
||||
sysinfo = require('../sysinfo.js'),
|
||||
util = require('util'),
|
||||
waitForDns = require('./waitfordns.js');
|
||||
waitForDns = require('./waitfordns.js'),
|
||||
xml2js = require('xml2js');
|
||||
|
||||
function formatError(response) {
|
||||
return util.format('NameCheap DNS error [%s] %j', response.code, response.message);
|
||||
}
|
||||
const ENDPOINT = 'https://api.namecheap.com/xml.response';
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
domainObject.config.token = domains.SECRET_PLACEHOLDER;
|
||||
@@ -33,37 +33,19 @@ function injectPrivateFields(newConfig, currentConfig) {
|
||||
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||
}
|
||||
|
||||
// Only send required fields - https://www.namecheap.com/support/api/methods/domains-dns/set-hosts.aspx
|
||||
function mapHosts(hosts) {
|
||||
return hosts.map(function (host) {
|
||||
let tmp = {};
|
||||
|
||||
tmp.TTL = '300';
|
||||
tmp.RecordType = host.RecordType || host.Type;
|
||||
tmp.HostName = host.HostName || host.Name;
|
||||
tmp.Address = host.Address;
|
||||
|
||||
if (tmp.RecordType === 'MX') {
|
||||
tmp.EmailType = 'MX';
|
||||
if (host.MXPref) tmp.MXPref = host.MXPref;
|
||||
}
|
||||
|
||||
return tmp;
|
||||
});
|
||||
}
|
||||
|
||||
function getApi(dnsConfig, callback) {
|
||||
function getQuery(dnsConfig, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
// Note that for all NameCheap calls to go through properly, the public IP returned by the getPublicIp method below must be whitelisted on NameCheap's API dashboard
|
||||
let namecheap = new Namecheap(dnsConfig.username, dnsConfig.token, ip);
|
||||
namecheap.setUsername(dnsConfig.username);
|
||||
|
||||
callback(null, namecheap);
|
||||
callback(null, {
|
||||
ApiUser: dnsConfig.username,
|
||||
ApiKey: dnsConfig.token,
|
||||
UserName: dnsConfig.username,
|
||||
ClientIp: ip
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -74,15 +56,36 @@ function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getApi(dnsConfig, function (error, namecheap) {
|
||||
getQuery(dnsConfig, function (error, query) {
|
||||
if (error) return callback(error);
|
||||
|
||||
namecheap.domains.dns.getHosts(zoneName, function (error, result) {
|
||||
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(error)));
|
||||
query.Command = 'namecheap.domains.dns.getHosts';
|
||||
query.SLD = zoneName.split('.')[0];
|
||||
query.TLD = zoneName.split('.')[1];
|
||||
|
||||
debug('entire getInternal response: %j', result);
|
||||
superagent.get(ENDPOINT).query(query).end(function (error, result) {
|
||||
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, result['DomainDNSGetHostsResult']['host']);
|
||||
var parser = new xml2js.Parser();
|
||||
parser.parseString(result.text, function (error, result) {
|
||||
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error));
|
||||
|
||||
var tmp = result.ApiResponse;
|
||||
if (tmp['$'].Status !== 'OK') {
|
||||
var errorMessage = safe.query(tmp, 'Errors[0].Error[0]._', 'Invalid response');
|
||||
if (errorMessage === 'API Key is invalid or API access has not been enabled') return callback(new DomainsError(DomainsError.ACCESS_DENIED, errorMessage));
|
||||
|
||||
return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, errorMessage));
|
||||
}
|
||||
if (!tmp.CommandResponse[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
|
||||
if (!tmp.CommandResponse[0].DomainDNSGetHostsResult[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
|
||||
|
||||
var hosts = result.ApiResponse.CommandResponse[0].DomainDNSGetHostsResult[0].host.map(function (h) {
|
||||
return h['$'];
|
||||
});
|
||||
|
||||
callback(null, hosts);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -93,15 +96,47 @@ function setInternal(dnsConfig, zoneName, hosts, callback) {
|
||||
assert(Array.isArray(hosts));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let mappedHosts = mapHosts(hosts);
|
||||
|
||||
getApi(dnsConfig, function (error, namecheap) {
|
||||
getQuery(dnsConfig, function (error, query) {
|
||||
if (error) return callback(error);
|
||||
|
||||
namecheap.domains.dns.setHosts(zoneName, mappedHosts, function (error, result) {
|
||||
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(error)));
|
||||
query.Command = 'namecheap.domains.dns.setHosts';
|
||||
query.SLD = zoneName.split('.')[0];
|
||||
query.TLD = zoneName.split('.')[1];
|
||||
|
||||
return callback(null, result);
|
||||
// Map to query params https://www.namecheap.com/support/api/methods/domains-dns/set-hosts.aspx
|
||||
hosts.forEach(function (host, i) {
|
||||
var n = i+1; // api starts with 1 not 0
|
||||
query['TTL' + n] = '300'; // keep it low
|
||||
query['HostName' + n] = host.HostName || host.Name;
|
||||
query['RecordType' + n] = host.RecordType || host.Type;
|
||||
query['Address' + n] = host.Address;
|
||||
|
||||
if (host.Type === 'MX') {
|
||||
query['EmailType' + n] = 'MX';
|
||||
if (host.MXPref) query['MXPref' + n] = host.MXPref;
|
||||
}
|
||||
});
|
||||
|
||||
superagent.post(ENDPOINT).query(query).end(function (error, result) {
|
||||
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error));
|
||||
|
||||
var parser = new xml2js.Parser();
|
||||
parser.parseString(result.text, function (error, result) {
|
||||
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error));
|
||||
|
||||
var tmp = result.ApiResponse;
|
||||
if (tmp['$'].Status !== 'OK') {
|
||||
var errorMessage = safe.query(tmp, 'Errors[0].Error[0]._', 'Invalid response');
|
||||
if (errorMessage === 'API Key is invalid or API access has not been enabled') return callback(new DomainsError(DomainsError.ACCESS_DENIED, errorMessage));
|
||||
|
||||
return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, errorMessage));
|
||||
}
|
||||
if (!tmp.CommandResponse[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
|
||||
if (!tmp.CommandResponse[0].DomainDNSSetHostsResult[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
|
||||
if (tmp.CommandResponse[0].DomainDNSSetHostsResult[0]['$'].IsSuccess !== 'true') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ function getInternal(dnsConfig, zoneName, name, type, callback) {
|
||||
|
||||
result.body.records.forEach(function (r) {
|
||||
// name.com api simply strips empty properties
|
||||
r.host = r.host || '@';
|
||||
r.host = r.host || '';
|
||||
});
|
||||
|
||||
var results = result.body.records.filter(function (r) {
|
||||
@@ -153,7 +153,7 @@ function upsert(domainObject, location, type, values, callback) {
|
||||
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
name = domains.getName(domainObject, location, type) || '@';
|
||||
name = domains.getName(domainObject, location, type) || '';
|
||||
|
||||
debug(`upsert: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
|
||||
@@ -174,7 +174,7 @@ function get(domainObject, location, type, callback) {
|
||||
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
name = domains.getName(domainObject, location, type) || '@';
|
||||
name = domains.getName(domainObject, location, type) || '';
|
||||
|
||||
getInternal(dnsConfig, zoneName, name, type, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
@@ -196,7 +196,7 @@ function del(domainObject, location, type, values, callback) {
|
||||
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
name = domains.getName(domainObject, location, type) || '@';
|
||||
name = domains.getName(domainObject, location, type) || '';
|
||||
|
||||
debug(`del: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
|
||||
|
||||
153
src/docker.js
153
src/docker.js
@@ -1,13 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
DockerError: DockerError,
|
||||
|
||||
connection: connectionInstance(),
|
||||
setRegistryConfig: setRegistryConfig,
|
||||
|
||||
ping: ping,
|
||||
|
||||
info: info,
|
||||
downloadImage: downloadImage,
|
||||
createContainer: createContainer,
|
||||
startContainer: startContainer,
|
||||
@@ -33,31 +32,22 @@ exports = module.exports = {
|
||||
// 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, 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', timeout: timeout });
|
||||
}
|
||||
|
||||
var docker = new Docker({ socketPath: '/var/run/docker.sock', timeout: timeout });
|
||||
return docker;
|
||||
}
|
||||
|
||||
var addons = require('./addons.js'),
|
||||
async = require('async'),
|
||||
assert = require('assert'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
child_process = require('child_process'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:docker.js'),
|
||||
once = require('once'),
|
||||
path = require('path'),
|
||||
settings = require('./settings.js'),
|
||||
shell = require('./shell.js'),
|
||||
safe = require('safetydance'),
|
||||
spawn = child_process.spawn,
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
@@ -65,30 +55,7 @@ var addons = require('./addons.js'),
|
||||
const CLEARVOLUME_CMD = path.join(__dirname, 'scripts/clearvolume.sh'),
|
||||
MKDIRVOLUME_CMD = path.join(__dirname, 'scripts/mkdirvolume.sh');
|
||||
|
||||
function DockerError(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(DockerError, Error);
|
||||
DockerError.INTERNAL_ERROR = 'Internal Error';
|
||||
DockerError.NOT_FOUND = 'Not found';
|
||||
DockerError.BAD_FIELD = 'Bad field';
|
||||
|
||||
function debugApp(app, args) {
|
||||
function debugApp(app) {
|
||||
assert(typeof app === 'object');
|
||||
|
||||
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
||||
@@ -103,8 +70,8 @@ function setRegistryConfig(auth, callback) {
|
||||
// currently, auth info is not stashed in the db but maybe it should for restore to work?
|
||||
const cmd = isLogin ? `docker login ${auth.serveraddress} --username ${auth.username} --password ${auth.password}` : `docker logout ${auth.serveraddress}`;
|
||||
|
||||
child_process.exec(cmd, { }, function (error, stdout, stderr) {
|
||||
if (error) return callback(new DockerError(DockerError.BAD_FIELD, stderr));
|
||||
child_process.exec(cmd, { }, function (error /*, stdout, stderr */) {
|
||||
if (error) return callback(new BoxError(BoxError.ACCESS_DENIED, error.message));
|
||||
|
||||
callback();
|
||||
});
|
||||
@@ -117,8 +84,8 @@ function ping(callback) {
|
||||
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'));
|
||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||
if (result !== 'OK') return callback(new BoxError(BoxError.DOCKER_ERROR, 'Unable to ping the docker daemon'));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -129,23 +96,32 @@ function pullImage(manifest, callback) {
|
||||
|
||||
// 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.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'));
|
||||
}
|
||||
docker.pull(manifest.dockerImage, function (error, stream) {
|
||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Unable to pull image. statusCode: ' + error.statusCode));
|
||||
|
||||
var image = docker.getImage(manifest.dockerImage);
|
||||
// https://github.com/dotcloud/docker/issues/1074 says each status message
|
||||
// is emitted as a chunk
|
||||
stream.on('data', function (chunk) {
|
||||
var data = safe.JSON.parse(chunk) || { };
|
||||
debug('pullImage %s: %j', manifest.id, data);
|
||||
|
||||
image.inspect(function (err, data) {
|
||||
if (err) return callback(new Error('Error inspecting image:' + err.message));
|
||||
if (!data || !data.Config) return callback(new Error('Missing Config in image:' + JSON.stringify(data, null, 4)));
|
||||
if (!data.Config.Entrypoint && !data.Config.Cmd) return callback(new Error('Only images with entry point are allowed'));
|
||||
// The data.status here is useless because this is per layer as opposed to per image
|
||||
if (!data.status && data.error) {
|
||||
debug('pullImage error %s: %s', manifest.id, data.errorDetail.message);
|
||||
}
|
||||
});
|
||||
|
||||
if (data.Config.ExposedPorts) debug('This image of %s exposes ports: %j', manifest.id, data.Config.ExposedPorts);
|
||||
stream.on('end', function () {
|
||||
debug('downloaded image %s of %s successfully', manifest.dockerImage, manifest.id);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
|
||||
stream.on('error', function (error) {
|
||||
debug('error pulling image %s of %s: %j', manifest.dockerImage, manifest.id, error);
|
||||
|
||||
callback(new BoxError(BoxError.DOCKER_ERROR, error.message));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -176,19 +152,24 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var docker = exports.connection,
|
||||
isAppContainer = !cmd; // non app-containers are like scheduler containers
|
||||
isAppContainer = !cmd; // non app-containers are like scheduler and exec (terminal) containers
|
||||
|
||||
var manifest = app.manifest;
|
||||
var exposedPorts = {}, dockerPortBindings = { };
|
||||
var domain = app.fqdn;
|
||||
// TODO: these should all have the CLOUDRON_ prefix
|
||||
var stdEnv = [
|
||||
const hostname = isAppContainer ? app.id : name;
|
||||
|
||||
const envPrefix = manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
let stdEnv = [
|
||||
'CLOUDRON=1',
|
||||
'CLOUDRON_PROXY_IP=172.18.0.1',
|
||||
'WEBADMIN_ORIGIN=' + config.adminOrigin(),
|
||||
'API_ORIGIN=' + config.adminOrigin(),
|
||||
'APP_ORIGIN=https://' + domain,
|
||||
'APP_DOMAIN=' + domain
|
||||
`CLOUDRON_APP_HOSTNAME=${app.id}`,
|
||||
`CLOUDRON_ADMIN_EMAIL=${app.adminEmail}`,
|
||||
`${envPrefix}WEBADMIN_ORIGIN=${settings.adminOrigin()}`,
|
||||
`${envPrefix}API_ORIGIN=${settings.adminOrigin()}`,
|
||||
`${envPrefix}APP_ORIGIN=https://${domain}`,
|
||||
`${envPrefix}APP_DOMAIN=${domain}`
|
||||
];
|
||||
|
||||
// docker portBindings requires ports to be exposed
|
||||
@@ -199,7 +180,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
var portEnv = [];
|
||||
for (let portName in app.portBindings) {
|
||||
const hostPort = app.portBindings[portName];
|
||||
const portType = portName in manifest.tcpPorts ? 'tcp' : 'udp';
|
||||
const portType = (manifest.tcpPorts && portName in manifest.tcpPorts) ? 'tcp' : 'udp';
|
||||
const ports = portType == 'tcp' ? manifest.tcpPorts : manifest.udpPorts;
|
||||
|
||||
var containerPort = ports[portName].containerPort || hostPort;
|
||||
@@ -235,9 +216,9 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
// Note that Hostname has no effect on DNS. We have to use the --net-alias for dns.
|
||||
// Hostname cannot be set with container NetworkMode
|
||||
var containerOptions = {
|
||||
name: name, // used for filtering logs
|
||||
name: name, // for referencing containers
|
||||
Tty: isAppContainer,
|
||||
Hostname: app.id, // set to something 'constant' so app containers can use this to communicate (across app updates)
|
||||
Hostname: hostname,
|
||||
Image: app.manifest.dockerImage,
|
||||
Cmd: (isAppContainer && app.debugMode && app.debugMode.cmd) ? app.debugMode.cmd : cmd,
|
||||
Env: stdEnv.concat(addonEnv).concat(portEnv).concat(appEnv),
|
||||
@@ -273,10 +254,17 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
},
|
||||
CpuShares: 512, // relative to 1024 for system processes
|
||||
VolumesFrom: isAppContainer ? null : [ app.containerId + ':rw' ],
|
||||
NetworkMode: 'cloudron',
|
||||
NetworkMode: 'cloudron', // user defined bridge network
|
||||
Dns: ['172.18.0.1'], // use internal dns
|
||||
DnsSearch: ['.'], // use internal dns
|
||||
SecurityOpt: [ 'apparmor=docker-cloudron-app' ]
|
||||
},
|
||||
NetworkingConfig: {
|
||||
EndpointsConfig: {
|
||||
cloudron: {
|
||||
Aliases: [ name ] // this allows sub-containers reach app containers by name
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -309,7 +297,8 @@ function startContainer(containerId, callback) {
|
||||
debug('Starting container %s', containerId);
|
||||
|
||||
container.start(function (error) {
|
||||
if (error && error.statusCode !== 304) return callback(new Error('Error starting container :' + error));
|
||||
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||
if (error && error.statusCode !== 304) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
@@ -385,7 +374,7 @@ function deleteContainers(appId, options, callback) {
|
||||
if (options.managedOnly) labels.push('isCloudronManaged=true');
|
||||
|
||||
docker.listContainers({ all: 1, filters: JSON.stringify({ label: labels }) }, function (error, containers) {
|
||||
if (error) return callback(error);
|
||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||
|
||||
async.eachSeries(containers, function (container, iteratorDone) {
|
||||
deleteContainer(container.Id, iteratorDone);
|
||||
@@ -402,7 +391,7 @@ function stopContainers(appId, callback) {
|
||||
debug('stopping containers of %s', appId);
|
||||
|
||||
docker.listContainers({ all: 1, filters: JSON.stringify({ label: [ 'appId=' + appId ] }) }, function (error, containers) {
|
||||
if (error) return callback(error);
|
||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||
|
||||
async.eachSeries(containers, function (container, iteratorDone) {
|
||||
stopContainer(container.Id, iteratorDone);
|
||||
@@ -446,7 +435,7 @@ function getContainerIdByIp(ip, callback) {
|
||||
|
||||
docker.getNetwork('cloudron').inspect(function (error, bridge) {
|
||||
if (error && error.statusCode === 404) return callback(new Error('Unable to find the cloudron network'));
|
||||
if (error) return callback(error);
|
||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||
|
||||
var containerId;
|
||||
for (var id in bridge.Containers) {
|
||||
@@ -468,8 +457,8 @@ function inspect(containerId, callback) {
|
||||
var container = exports.connection.getContainer(containerId);
|
||||
|
||||
container.inspect(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));
|
||||
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
@@ -482,7 +471,7 @@ function getEvents(options, callback) {
|
||||
let docker = exports.connection;
|
||||
|
||||
docker.getEvents(options, function (error, stream) {
|
||||
if (error) return callback(new DockerError(DockerError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||
|
||||
callback(null, stream);
|
||||
});
|
||||
@@ -495,8 +484,8 @@ function memoryUsage(containerId, callback) {
|
||||
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));
|
||||
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
@@ -560,7 +549,7 @@ function createVolume(app, name, volumeDataDir, callback) {
|
||||
if (error) return callback(new Error(`Error creating app data dir: ${error.message}`));
|
||||
|
||||
docker.createVolume(volumeOptions, function (error) {
|
||||
if (error) return callback(error);
|
||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||
|
||||
callback();
|
||||
});
|
||||
@@ -577,7 +566,7 @@ function clearVolume(app, name, options, callback) {
|
||||
let volume = docker.getVolume(name);
|
||||
volume.inspect(function (error, v) {
|
||||
if (error && error.statusCode === 404) return callback();
|
||||
if (error) return callback(error);
|
||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||
|
||||
const volumeDataDir = v.Options.device;
|
||||
shell.sudo('clearVolume', [ CLEARVOLUME_CMD, options.removeDirectory ? 'rmdir' : 'clear', volumeDataDir ], {}, callback);
|
||||
@@ -599,3 +588,15 @@ function removeVolume(app, name, callback) {
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function info(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let docker = exports.connection;
|
||||
|
||||
docker.info(function (error, result) {
|
||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Error connecting to docker'));
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ exports = module.exports = {
|
||||
var apps = require('./apps.js'),
|
||||
AppsError = apps.AppsError,
|
||||
assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
express = require('express'),
|
||||
debug = require('debug')('box:dockerproxy'),
|
||||
http = require('http'),
|
||||
@@ -29,7 +29,7 @@ function authorizeApp(req, res, next) {
|
||||
// - only allow managing and inspection of containers belonging to the app
|
||||
|
||||
// make the tests pass for now
|
||||
if (config.TEST) {
|
||||
if (constants.TEST) {
|
||||
req.app = { id: 'testappid' };
|
||||
return next();
|
||||
}
|
||||
@@ -120,7 +120,7 @@ function start(callback) {
|
||||
|
||||
let proxyServer = express();
|
||||
|
||||
if (config.TEST) {
|
||||
if (constants.TEST) {
|
||||
proxyServer.use(function (req, res, next) {
|
||||
debug('proxying: ' + req.method, req.url);
|
||||
next();
|
||||
@@ -135,9 +135,9 @@ function start(callback) {
|
||||
.use(middleware.lastMile());
|
||||
|
||||
gHttpServer = http.createServer(proxyServer);
|
||||
gHttpServer.listen(config.get('dockerProxyPort'), '0.0.0.0', callback);
|
||||
gHttpServer.listen(constants.DOCKER_PROXY_PORT, '0.0.0.0', callback);
|
||||
|
||||
debug(`startDockerProxy: started proxy on port ${config.get('dockerProxyPort')}`);
|
||||
debug(`startDockerProxy: started proxy on port ${constants.DOCKER_PROXY_PORT}`);
|
||||
|
||||
gHttpServer.on('upgrade', function (req, client, head) {
|
||||
// Create a new tcp connection to the TCP server
|
||||
@@ -150,7 +150,7 @@ function start(callback) {
|
||||
if (req.headers['content-type'] === 'application/json') {
|
||||
// TODO we have to parse the immediate upgrade request body, but I don't know how
|
||||
let plainBody = '{"Detach":false,"Tty":false}\r\n';
|
||||
upgradeMessage += `Content-Type: application/json\r\n`;
|
||||
upgradeMessage += 'Content-Type: application/json\r\n';
|
||||
upgradeMessage += `Content-Length: ${Buffer.byteLength(plainBody)}\r\n`;
|
||||
upgradeMessage += '\r\n';
|
||||
upgradeMessage += plainBody;
|
||||
|
||||
@@ -26,6 +26,8 @@ module.exports = exports = {
|
||||
|
||||
parentDomain: parentDomain,
|
||||
|
||||
checkDnsRecords: checkDnsRecords,
|
||||
|
||||
prepareDashboardDomain: prepareDashboardDomain,
|
||||
|
||||
DomainsError: DomainsError,
|
||||
@@ -35,7 +37,6 @@ module.exports = exports = {
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:domains'),
|
||||
@@ -44,6 +45,7 @@ var assert = require('assert'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
ReverseProxyError = reverseProxy.ReverseProxyError,
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
tld = require('tldjs'),
|
||||
util = require('util'),
|
||||
@@ -76,7 +78,7 @@ DomainsError.BAD_FIELD = 'Bad Field';
|
||||
DomainsError.STILL_BUSY = 'Still busy';
|
||||
DomainsError.IN_USE = 'In Use';
|
||||
DomainsError.INTERNAL_ERROR = 'Internal error';
|
||||
DomainsError.ACCESS_DENIED = 'Access denied';
|
||||
DomainsError.ACCESS_DENIED = 'Access Denied';
|
||||
DomainsError.INVALID_PROVIDER = 'provider must be route53, gcdns, digitalocean, gandi, cloudflare, namecom, noop, wildcard, manual or caas';
|
||||
|
||||
// choose which subdomain backend we use for test purpose we use route53
|
||||
@@ -145,13 +147,12 @@ function validateHostname(location, domainObject) {
|
||||
const hostname = fqdn(location, domainObject);
|
||||
|
||||
const RESERVED_LOCATIONS = [
|
||||
constants.API_LOCATION,
|
||||
constants.SMTP_LOCATION,
|
||||
constants.IMAP_LOCATION
|
||||
];
|
||||
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new DomainsError(DomainsError.BAD_FIELD, location + ' is reserved');
|
||||
|
||||
if (hostname === config.adminFqdn()) return new DomainsError(DomainsError.BAD_FIELD, location + ' is reserved');
|
||||
if (hostname === settings.adminFqdn()) return new DomainsError(DomainsError.BAD_FIELD, location + ' is reserved');
|
||||
|
||||
// workaround https://github.com/oncletom/tld.js/issues/73
|
||||
var tmp = hostname.replace('_', '-');
|
||||
@@ -344,7 +345,7 @@ function del(domain, auditSource, callback) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (domain === config.adminDomain()) return callback(new DomainsError(DomainsError.IN_USE));
|
||||
if (domain === settings.adminDomain()) return callback(new DomainsError(DomainsError.IN_USE));
|
||||
|
||||
domaindb.del(domain, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
|
||||
@@ -395,7 +396,7 @@ function getDnsRecords(location, domain, type, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
get(domain, function (error, domainObject) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(error);
|
||||
|
||||
api(domainObject.provider).get(domainObject, location, type, function (error, values) {
|
||||
if (error) return callback(error);
|
||||
@@ -405,6 +406,25 @@ function getDnsRecords(location, domain, type, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function checkDnsRecords(location, domain, callback) {
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getDnsRecords(location, domain, 'A', function (error, values) {
|
||||
if (error) return callback(error);
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
|
||||
|
||||
if (values.length === 0) return callback(null, { needsOverwrite: false }); // does not exist
|
||||
if (values[0] === ip) return callback(null, { needsOverwrite: false }); // exists but in sync
|
||||
|
||||
callback(null, { needsOverwrite: true });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// note: for TXT records the values must be quoted
|
||||
function upsertDnsRecords(location, domain, type, values, callback) {
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
@@ -494,15 +514,17 @@ function prepareDashboardDomain(domain, auditSource, progressCallback, callback)
|
||||
get(domain, function (error, domainObject) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const adminFqdn = fqdn(constants.ADMIN_LOCATION, domainObject);
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
|
||||
|
||||
async.series([
|
||||
(done) => { progressCallback({ percent: 10, message: 'Updating DNS' }); done(); },
|
||||
(done) => { progressCallback({ percent: 10, message: `Updating DNS of ${adminFqdn}` }); done(); },
|
||||
upsertDnsRecords.bind(null, constants.ADMIN_LOCATION, domain, 'A', [ ip ]),
|
||||
(done) => { progressCallback({ percent: 40, message: 'Waiting for DNS' }); done(); },
|
||||
(done) => { progressCallback({ percent: 40, message: `Waiting for DNS of ${adminFqdn}` }); done(); },
|
||||
waitForDnsRecord.bind(null, constants.ADMIN_LOCATION, domain, 'A', ip, { interval: 30000, times: 50000 }),
|
||||
(done) => { progressCallback({ percent: 70, message: 'Getting certificate' }); done(); },
|
||||
(done) => { progressCallback({ percent: 70, message: `Getting certificate of ${adminFqdn}` }); done(); },
|
||||
reverseProxy.ensureCertificate.bind(null, fqdn(constants.ADMIN_LOCATION, domainObject), domain, auditSource)
|
||||
], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -4,17 +4,16 @@ exports = module.exports = {
|
||||
sync: sync
|
||||
};
|
||||
|
||||
var appdb = require('./appdb.js'),
|
||||
apps = require('./apps.js'),
|
||||
let apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:dyndns'),
|
||||
domains = require('./domains.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
sysinfo = require('./sysinfo.js');
|
||||
|
||||
// called for dynamic dns setups where we have to update the IP
|
||||
@@ -33,7 +32,7 @@ function sync(auditSource, callback) {
|
||||
|
||||
debug(`refreshDNS: updating ip from ${info.ip} to ${ip}`);
|
||||
|
||||
domains.upsertDnsRecords(constants.ADMIN_LOCATION, config.adminDomain(), 'A', [ ip ], function (error) {
|
||||
domains.upsertDnsRecords(constants.ADMIN_LOCATION, settings.adminDomain(), 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('refreshDNS: updated admin location');
|
||||
@@ -43,7 +42,7 @@ function sync(auditSource, callback) {
|
||||
|
||||
async.each(result, function (app, callback) {
|
||||
// do not change state of installing apps since apptask will error if dns record already exists
|
||||
if (app.installationState !== appdb.ISTATE_INSTALLED) return callback();
|
||||
if (app.installationState !== apps.ISTATE_INSTALLED) return callback();
|
||||
|
||||
domains.upsertDnsRecords(app.location, app.domain, 'A', [ ip ], callback);
|
||||
}, function (error) {
|
||||
|
||||
@@ -13,6 +13,7 @@ exports = module.exports = {
|
||||
ACTION_ACTIVATE: 'cloudron.activate',
|
||||
ACTION_APP_CLONE: 'app.clone',
|
||||
ACTION_APP_CONFIGURE: 'app.configure',
|
||||
ACTION_APP_REPAIR: 'app.repair',
|
||||
ACTION_APP_INSTALL: 'app.install',
|
||||
ACTION_APP_RESTORE: 'app.restore',
|
||||
ACTION_APP_UNINSTALL: 'app.uninstall',
|
||||
@@ -22,13 +23,10 @@ exports = module.exports = {
|
||||
ACTION_APP_OOM: 'app.oom',
|
||||
ACTION_APP_UP: 'app.up',
|
||||
ACTION_APP_DOWN: 'app.down',
|
||||
ACTION_APP_TASK_START: 'app.task.start',
|
||||
ACTION_APP_TASK_CRASH: 'app.task.crash',
|
||||
ACTION_APP_TASK_SUCCESS: 'app.task.success',
|
||||
|
||||
ACTION_BACKUP_FINISH: 'backup.finish',
|
||||
ACTION_BACKUP_START: 'backup.start',
|
||||
ACTION_BACKUP_CLEANUP_START: 'backup.cleanup.start',
|
||||
ACTION_BACKUP_CLEANUP_START: 'backup.cleanup.start', // obsolete
|
||||
ACTION_BACKUP_CLEANUP_FINISH: 'backup.cleanup.finish',
|
||||
|
||||
ACTION_CERTIFICATE_NEW: 'certificate.new',
|
||||
@@ -51,6 +49,7 @@ exports = module.exports = {
|
||||
ACTION_RESTORE: 'cloudron.restore', // unused
|
||||
ACTION_START: 'cloudron.start',
|
||||
ACTION_UPDATE: 'cloudron.update',
|
||||
ACTION_UPDATE_FINISH: 'cloudron.update.finish',
|
||||
|
||||
ACTION_USER_ADD: 'user.add',
|
||||
ACTION_USER_LOGIN: 'user.login',
|
||||
|
||||
231
src/externalldap.js
Normal file
231
src/externalldap.js
Normal file
@@ -0,0 +1,231 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
ExternalLdapError: ExternalLdapError,
|
||||
|
||||
verifyPassword: verifyPassword,
|
||||
|
||||
testConfig: testConfig,
|
||||
startSyncer: startSyncer,
|
||||
|
||||
sync: sync
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
auditsource = require('./auditsource.js'),
|
||||
debug = require('debug')('box:externalldap'),
|
||||
ldap = require('ldapjs'),
|
||||
settings = require('./settings.js'),
|
||||
tasks = require('./tasks.js'),
|
||||
users = require('./users.js'),
|
||||
UserError = users.UsersError,
|
||||
util = require('util');
|
||||
|
||||
function ExternalLdapError(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(ExternalLdapError, Error);
|
||||
ExternalLdapError.EXTERNAL_ERROR = 'external error';
|
||||
ExternalLdapError.INTERNAL_ERROR = 'internal error';
|
||||
ExternalLdapError.INVALID_CREDENTIALS = 'invalid credentials';
|
||||
ExternalLdapError.BAD_STATE = 'bad state';
|
||||
ExternalLdapError.BAD_FIELD = 'bad field';
|
||||
ExternalLdapError.NOT_FOUND = 'not found';
|
||||
|
||||
// performs service bind if required
|
||||
function getClient(externalLdapConfig, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// basic validation to not crash
|
||||
try { ldap.parseDN(externalLdapConfig.baseDn); } catch (e) { return callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'invalid baseDn')); }
|
||||
try { ldap.parseFilter(externalLdapConfig.filter); } catch (e) { return callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'invalid filter')); }
|
||||
if (externalLdapConfig.bindDn) try { ldap.parseFilter(externalLdapConfig.bindDn); } catch (e) { return callback(new ExternalLdapError(ExternalLdapError.INVALID_CREDENTIALS)); }
|
||||
|
||||
var client;
|
||||
try {
|
||||
client = ldap.createClient({ url: externalLdapConfig.url });
|
||||
} catch (e) {
|
||||
if (e instanceof ldap.ProtocolError) return callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'url protocol is invalid'));
|
||||
return callback(new ExternalLdapError(ExternalLdapError.INTERNAL_ERROR, e));
|
||||
}
|
||||
|
||||
if (!externalLdapConfig.bindDn) return callback(null, client);
|
||||
|
||||
client.bind(externalLdapConfig.bindDn, externalLdapConfig.bindPassword, function (error) {
|
||||
if (error instanceof ldap.InvalidCredentialsError) return callback(new ExternalLdapError(ExternalLdapError.INVALID_CREDENTIALS));
|
||||
if (error) return callback(new ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, error));
|
||||
|
||||
callback(null, client, externalLdapConfig);
|
||||
});
|
||||
}
|
||||
|
||||
function testConfig(config, callback) {
|
||||
assert.strictEqual(typeof config, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!config.enabled) return callback();
|
||||
|
||||
if (!config.url) return callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'url must not be empty'));
|
||||
if (!config.baseDn) return callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'basedn must not be empty'));
|
||||
if (!config.filter) return callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'filter must not be empty'));
|
||||
|
||||
getClient(config, function (error, client) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var opts = {
|
||||
filter: config.filter,
|
||||
scope: 'sub'
|
||||
};
|
||||
|
||||
client.search(config.baseDn, opts, function (error, result) {
|
||||
if (error) return callback(new ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, error));
|
||||
|
||||
result.on('searchEntry', function (entry) {});
|
||||
result.on('error', function (error) { callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'Unable to search directory')); });
|
||||
result.on('end', function (result) { callback(); });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function verifyPassword(user, password, callback) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.getExternalLdapConfig(function (error, externalLdapConfig) {
|
||||
if (error) return callback(new ExternalLdapError(ExternalLdapError.INTERNAL_ERROR, error));
|
||||
if (!externalLdapConfig.enabled) return callback(new ExternalLdapError(ExternalLdapError.BAD_STATE, 'not enabled'));
|
||||
|
||||
getClient(externalLdapConfig, function (error, client) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const dn = `uid=${user.username},${externalLdapConfig.baseDn}`;
|
||||
|
||||
client.bind(dn, password, function (error) {
|
||||
if (error instanceof ldap.InvalidCredentialsError) return callback(new ExternalLdapError(ExternalLdapError.INVALID_CREDENTIALS));
|
||||
if (error) return callback(new ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, error));
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function startSyncer(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.getExternalLdapConfig(function (error, externalLdapConfig) {
|
||||
if (error) return callback(new ExternalLdapError(ExternalLdapError.INTERNAL_ERROR, error));
|
||||
if (!externalLdapConfig.enabled) return callback(new ExternalLdapError(ExternalLdapError.BAD_STATE, 'not enabled'));
|
||||
|
||||
tasks.add(tasks.TASK_SYNC_EXTERNAL_LDAP, [], function (error, taskId) {
|
||||
if (error) return callback(new ExternalLdapError(ExternalLdapError.INTERNAL_ERROR, error));
|
||||
|
||||
tasks.startTask(taskId, {}, function (error, result) {
|
||||
debug('sync: done', error, result);
|
||||
});
|
||||
|
||||
callback(null, taskId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sync(progressCallback, callback) {
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('Start user syncing ...');
|
||||
|
||||
settings.getExternalLdapConfig(function (error, externalLdapConfig) {
|
||||
if (error) return callback(new ExternalLdapError(ExternalLdapError.INTERNAL_ERROR, error));
|
||||
if (!externalLdapConfig.enabled) return callback(new ExternalLdapError(ExternalLdapError.BAD_STATE, 'not enabled'));
|
||||
|
||||
getClient(externalLdapConfig, function (error, client) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var opts = {
|
||||
paged: true,
|
||||
filter: externalLdapConfig.filter,
|
||||
scope: 'sub' // We may have to make this configurable
|
||||
};
|
||||
|
||||
debug(`Listing users at ${externalLdapConfig.baseDn} with filter ${externalLdapConfig.filter}`);
|
||||
|
||||
client.search(externalLdapConfig.baseDn, opts, function (error, result) {
|
||||
if (error) return callback(new ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, error));
|
||||
|
||||
var ldapUsers = [];
|
||||
|
||||
result.on('searchEntry', function (entry) {
|
||||
ldapUsers.push(entry.object);
|
||||
});
|
||||
|
||||
result.on('error', function (error) {
|
||||
callback(new ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, error));
|
||||
});
|
||||
|
||||
result.on('end', function (result) {
|
||||
if (result.status !== 0) return callback(new ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, 'Server returned status ' + result.status));
|
||||
|
||||
debug(`Found ${ldapUsers.length} users`);
|
||||
|
||||
// we ignore all errors here and just log them for now
|
||||
async.eachSeries(ldapUsers, function (user, callback) {
|
||||
|
||||
// ignore the bindDn user if any
|
||||
if (user.dn === externalLdapConfig.bindDn) return callback();
|
||||
|
||||
users.getByUsername(user.uid, function (error, result) {
|
||||
if (error && error.reason !== UserError.NOT_FOUND) {
|
||||
console.error(error);
|
||||
return callback();
|
||||
}
|
||||
|
||||
if (error) {
|
||||
debug('[adding user] ', user.uid, user.mail, user.cn);
|
||||
|
||||
users.create(user.uid, null, user.mail, user.cn, { source: 'ldap' }, auditsource.EXTERNAL_LDAP_TASK, function (error) {
|
||||
if (error) console.error('Failed to create user', user, error);
|
||||
callback();
|
||||
});
|
||||
} else if (result.source !== 'ldap') {
|
||||
debug('[conflicting user]', user.uid, user.mail, user.cn);
|
||||
|
||||
callback();
|
||||
} else if (result.email !== user.mail || result.displayName !== user.cn) {
|
||||
debug('[updating user] ', user.uid, user.mail, user.cn);
|
||||
|
||||
users.update(result.id, { email: user.mail, fallbackEmail: user.mail, displayName: user.cn }, auditsource.EXTERNAL_LDAP_TASK, function (error) {
|
||||
if (error) console.error('Failed to update user', user, error);
|
||||
callback();
|
||||
});
|
||||
} else {
|
||||
// user known and up-to-date
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}, function () {
|
||||
debug('User sync done.');
|
||||
callback();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -19,6 +19,7 @@ function startGraphite(existingInfra, callback) {
|
||||
if (existingInfra.version === infra.version && infra.images.graphite.tag === existingInfra.images.graphite.tag) return callback();
|
||||
|
||||
const cmd = `docker run --restart=always -d --name="graphite" \
|
||||
--hostname graphite \
|
||||
--net cloudron \
|
||||
--net-alias graphite \
|
||||
--log-driver syslog \
|
||||
|
||||
@@ -20,7 +20,9 @@ exports = module.exports = {
|
||||
getGroups: getGroups,
|
||||
|
||||
setMembership: setMembership,
|
||||
getMembership: getMembership
|
||||
getMembership: getMembership,
|
||||
|
||||
count: count
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
@@ -268,3 +270,13 @@ function getGroups(userId, callback) {
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
function count(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
groupdb.count(function (error, count) {
|
||||
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, count);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
exports = module.exports = {
|
||||
// a version change recreates all containers with latest docker config
|
||||
'version': '48.14.0',
|
||||
'version': '48.16.0',
|
||||
|
||||
'baseImages': [
|
||||
{ repo: 'cloudron/base', tag: 'cloudron/base:1.0.0@sha256:147a648a068a2e746644746bbfb42eb7a50d682437cead3c67c933c546357617' }
|
||||
@@ -17,10 +17,10 @@ exports = module.exports = {
|
||||
'images': {
|
||||
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.0.2@sha256:a28320f313785816be60e3f865e09065504170a3d20ed37de675c719b32b01eb' },
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.0.2@sha256:6dcee0731dfb9b013ed94d56205eee219040ee806c7e251db3b3886eaa4947ff' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.0.2@sha256:95e006390ddce7db637e1672eb6f3c257d3c2652747424f529b1dee3cbe6728c' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.1.0@sha256:6d1bf221cfe6124957e2c58b57c0a47214353496009296acb16adf56df1da9d5' },
|
||||
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.0.0@sha256:8a88dd334b62b578530a014ca1a2425a54cb9df1e475f5d3a36806e5cfa22121' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.2.0@sha256:20e4d2508dcf712eb56481067993ae39bf541d793d44f99f6a41d630ad941d9e' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.2@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.4.0@sha256:209f76833ff8cce58be8a09897c378e3b706e1e318249870afb7294a4ee83cad' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.2.0@sha256:fc9ca69d16e6ebdbd98ed53143d4a0d2212eef60cb638dc71219234e6f427a2c' },
|
||||
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:0.1.0@sha256:e177c5bf5f38c84ce1dea35649c22a1b05f96eec67a54a812c5a35e585670f0f' }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -63,7 +63,7 @@ function cleanupTmpVolume(containerInfo, callback) {
|
||||
assert.strictEqual(typeof containerInfo, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var cmd = 'find /tmp -mtime +10 -exec rm -rf {} +'.split(' '); // 10 days old
|
||||
var cmd = 'find /tmp -type f -mtime +10 -exec rm -rf {} +'.split(' '); // 10 day old files
|
||||
|
||||
debug('cleanupTmpVolume %j', containerInfo.Names);
|
||||
|
||||
|
||||
59
src/ldap.js
59
src/ldap.js
@@ -9,7 +9,7 @@ var assert = require('assert'),
|
||||
appdb = require('./appdb.js'),
|
||||
apps = require('./apps.js'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:ldap'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
@@ -135,7 +135,7 @@ function userSearch(req, res, next) {
|
||||
var dn = ldap.parseDN('cn=' + entry.id + ',ou=users,dc=cloudron');
|
||||
|
||||
var groups = [ GROUP_USERS_DN ];
|
||||
if (entry.admin || req.app.ownerId === entry.id) groups.push(GROUP_ADMINS_DN);
|
||||
if (entry.admin) groups.push(GROUP_ADMINS_DN);
|
||||
|
||||
var displayName = entry.displayName || entry.username || ''; // displayName can be empty and username can be null
|
||||
var nameParts = displayName.split(' ');
|
||||
@@ -155,7 +155,7 @@ function userSearch(req, res, next) {
|
||||
givenName: firstName,
|
||||
username: entry.username,
|
||||
samaccountname: entry.username, // to support ActiveDirectory clients
|
||||
isadmin: (entry.admin || req.app.ownerId === entry.id) ? 1 : 0,
|
||||
isadmin: entry.admin,
|
||||
memberof: groups
|
||||
}
|
||||
};
|
||||
@@ -195,7 +195,7 @@ function groupSearch(req, res, next) {
|
||||
|
||||
groups.forEach(function (group) {
|
||||
var dn = ldap.parseDN('cn=' + group.name + ',ou=groups,dc=cloudron');
|
||||
var members = group.admin ? result.filter(function (entry) { return entry.admin || req.app.ownerId === entry.id; }) : result;
|
||||
var members = group.admin ? result.filter(function (entry) { return entry.admin; }) : result;
|
||||
|
||||
var obj = {
|
||||
dn: dn.toString(),
|
||||
@@ -244,7 +244,7 @@ function groupAdminsCompare(req, res, next) {
|
||||
// we only support memberuid here, if we add new group attributes later add them here
|
||||
if (req.attribute === 'memberuid') {
|
||||
var found = result.find(function (u) { return u.id === req.value; });
|
||||
if (found && (found.admin || req.app.ownerId == found.id)) return res.end(true);
|
||||
if (found && found.admin) return res.end(true);
|
||||
}
|
||||
|
||||
res.end(false);
|
||||
@@ -371,7 +371,7 @@ function mailingListSearch(req, res, next) {
|
||||
var parts = email.split('@');
|
||||
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
mailboxdb.getGroup(parts[0], parts[1], function (error, group) {
|
||||
mailboxdb.getList(parts[0], parts[1], function (error, list) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
|
||||
@@ -382,9 +382,9 @@ function mailingListSearch(req, res, next) {
|
||||
attributes: {
|
||||
objectclass: ['mailGroup'],
|
||||
objectcategory: 'mailGroup',
|
||||
cn: `${group.name}@${group.domain}`, // fully qualified
|
||||
mail: `${group.name}@${group.domain}`,
|
||||
mgrpRFC822MailMember: group.members.map(function (m) { return `${m}@${group.domain}`; })
|
||||
cn: `${list.name}@${list.domain}`, // fully qualified
|
||||
mail: `${list.name}@${list.domain}`,
|
||||
mgrpRFC822MailMember: list.members // fully qualified
|
||||
}
|
||||
};
|
||||
|
||||
@@ -520,22 +520,25 @@ function userSearchSftp(req, res, next) {
|
||||
users.getByUsername(username, function (error, user) {
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
|
||||
if (!user.admin) return next(new ldap.InsufficientAccessRightsError('Not authorized'));
|
||||
apps.hasAccessTo(app, user, function (error, hasAccess) {
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
if (!hasAccess) return next(new ldap.InsufficientAccessRightsError('Not authorized'));
|
||||
|
||||
var obj = {
|
||||
dn: ldap.parseDN(`cn=${username}@${appFqdn},ou=sftp,dc=cloudron`).toString(),
|
||||
attributes: {
|
||||
homeDirectory: path.join('/app/data', app.id, 'data'),
|
||||
objectclass: ['user'],
|
||||
objectcategory: 'person',
|
||||
cn: user.id,
|
||||
uid: `${username}@${appFqdn}`, // for bind after search
|
||||
uidNumber: uidNumber, // unix uid for ftp access
|
||||
gidNumber: uidNumber // unix gid for ftp access
|
||||
}
|
||||
};
|
||||
var obj = {
|
||||
dn: ldap.parseDN(`cn=${username}@${appFqdn},ou=sftp,dc=cloudron`).toString(),
|
||||
attributes: {
|
||||
homeDirectory: path.join('/app/data', app.id, 'data'),
|
||||
objectclass: ['user'],
|
||||
objectcategory: 'person',
|
||||
cn: user.id,
|
||||
uid: `${username}@${appFqdn}`, // for bind after search
|
||||
uidNumber: uidNumber, // unix uid for ftp access
|
||||
gidNumber: uidNumber // unix gid for ftp access
|
||||
}
|
||||
};
|
||||
|
||||
finalSend([ obj ], req, res, next);
|
||||
finalSend([ obj ], req, res, next);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -557,13 +560,13 @@ function authenticateMailAddon(req, res, next) {
|
||||
|
||||
if (addonId === 'recvmail' && !domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
let name;
|
||||
if (addonId === 'sendmail') name = 'MAIL_SMTP_PASSWORD';
|
||||
else if (addonId === 'recvmail') name = 'MAIL_IMAP_PASSWORD';
|
||||
let namePattern; // manifest v2 has a CLOUDRON_ prefix for names
|
||||
if (addonId === 'sendmail') namePattern = '%MAIL_SMTP_PASSWORD';
|
||||
else if (addonId === 'recvmail') namePattern = '%MAIL_IMAP_PASSWORD';
|
||||
else return next(new ldap.OperationsError('Invalid DN'));
|
||||
|
||||
// note: with sendmail addon, apps can send mail without a mailbox (unlike users)
|
||||
appdb.getAppIdByAddonConfigValue(addonId, name, req.credentials || '', function (error, appId) {
|
||||
appdb.getAppIdByAddonConfigValue(addonId, namePattern, req.credentials || '', function (error, appId) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return next(new ldap.OperationsError(error.message));
|
||||
if (appId) { // matched app password
|
||||
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: email }, { appId: appId, addonId: addonId });
|
||||
@@ -637,7 +640,7 @@ function start(callback) {
|
||||
res.end();
|
||||
});
|
||||
|
||||
gServer.listen(config.get('ldapPort'), '0.0.0.0', callback);
|
||||
gServer.listen(constants.LDAP_PORT, '0.0.0.0', callback);
|
||||
}
|
||||
|
||||
function stop(callback) {
|
||||
|
||||
@@ -22,7 +22,11 @@ Locker.prototype.OP_APPTASK = 'apptask';
|
||||
Locker.prototype.lock = function (operation) {
|
||||
assert.strictEqual(typeof operation, 'string');
|
||||
|
||||
if (this._operation !== null) return new Error('Already locked for ' + this._operation);
|
||||
if (this._operation !== null) {
|
||||
let error = new Error(`Locked for ${this._operation}`);
|
||||
error.operation = this._operation;
|
||||
return error;
|
||||
}
|
||||
|
||||
this._operation = operation;
|
||||
++this._lockDepth;
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
# Generated by apptask for the /run mount
|
||||
# Generated by apptask
|
||||
|
||||
# keep upto 7 rotated logs. rotation triggered daily or ahead of time if size is > 1M
|
||||
<%= volumePath %>/*.log <%= volumePath %>/*/*.log <%= volumePath %>/*/*/*.log {
|
||||
rotate 7
|
||||
daily
|
||||
compress
|
||||
maxsize=1M
|
||||
missingok
|
||||
delaycompress
|
||||
copytruncate
|
||||
rotate 7
|
||||
daily
|
||||
compress
|
||||
maxsize 1M
|
||||
missingok
|
||||
delaycompress
|
||||
copytruncate
|
||||
}
|
||||
|
||||
/home/yellowtent/platformdata/logs/<%= appId %>/*.log {
|
||||
# only keep one rotated file, we currently do not send that over the api
|
||||
rotate 1
|
||||
size 10M
|
||||
missingok
|
||||
# we never compress so we can simply tail the files
|
||||
nocompress
|
||||
copytruncate
|
||||
}
|
||||
|
||||
|
||||
121
src/mail.js
121
src/mail.js
@@ -53,7 +53,6 @@ exports = module.exports = {
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:mail'),
|
||||
@@ -71,11 +70,13 @@ var assert = require('assert'),
|
||||
paths = require('./paths.js'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
shell = require('./shell.js'),
|
||||
smtpTransport = require('nodemailer-smtp-transport'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
users = require('./users.js'),
|
||||
util = require('util'),
|
||||
validator = require('validator'),
|
||||
_ = require('underscore');
|
||||
|
||||
const DNS_OPTIONS = { timeout: 5000 };
|
||||
@@ -126,7 +127,6 @@ function checkOutboundPort25(callback) {
|
||||
'smtp.gmail.com',
|
||||
'smtp.live.com',
|
||||
'smtp.mail.yahoo.com',
|
||||
'smtp.o2.ie',
|
||||
'smtp.comcast.net',
|
||||
'smtp.1und1.de',
|
||||
]);
|
||||
@@ -210,10 +210,14 @@ function verifyRelay(relay, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function checkDkim(domain, callback) {
|
||||
var dkim = {
|
||||
domain: `${constants.DKIM_SELECTOR}._domainkey.${domain}`,
|
||||
name: `${constants.DKIM_SELECTOR}._domainkey`,
|
||||
function checkDkim(mailDomain, callback) {
|
||||
assert.strictEqual(typeof mailDomain, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const domain = mailDomain.domain;
|
||||
let dkim = {
|
||||
domain: `${mailDomain.dkimSelector}._domainkey.${domain}`,
|
||||
name: `${mailDomain.dkimSelector}._domainkey`,
|
||||
type: 'TXT',
|
||||
expected: null,
|
||||
value: null,
|
||||
@@ -259,14 +263,14 @@ function checkSpf(domain, mailFqdn, callback) {
|
||||
let txtRecord = txtRecords[i].join(''); // https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
|
||||
if (txtRecord.indexOf('v=spf1 ') !== 0) continue; // not SPF
|
||||
spf.value = txtRecord;
|
||||
spf.status = spf.value.indexOf(' a:' + config.adminFqdn()) !== -1;
|
||||
spf.status = spf.value.indexOf(' a:' + settings.adminFqdn()) !== -1;
|
||||
break;
|
||||
}
|
||||
|
||||
if (spf.status) {
|
||||
spf.expected = spf.value;
|
||||
} else if (i !== txtRecords.length) {
|
||||
spf.expected = 'v=spf1 a:' + config.adminFqdn() + ' ' + spf.value.slice('v=spf1 '.length);
|
||||
spf.expected = 'v=spf1 a:' + settings.adminFqdn() + ' ' + spf.value.slice('v=spf1 '.length);
|
||||
}
|
||||
|
||||
callback(null, spf);
|
||||
@@ -289,13 +293,25 @@ function checkMx(domain, mailFqdn, callback) {
|
||||
|
||||
dns.resolve(mx.domain, mx.type, DNS_OPTIONS, function (error, mxRecords) {
|
||||
if (error) return callback(error, mx);
|
||||
if (mxRecords.length === 0) return callback(null, mx);
|
||||
|
||||
if (mxRecords.length !== 0) {
|
||||
mx.status = mxRecords.length == 1 && mxRecords[0].exchange === mailFqdn;
|
||||
mx.value = mxRecords.map(function (r) { return r.priority + ' ' + r.exchange + '.'; }).join(' ');
|
||||
}
|
||||
mx.status = mxRecords.length == 1 && mxRecords[0].exchange === mailFqdn;
|
||||
mx.value = mxRecords.map(function (r) { return r.priority + ' ' + r.exchange + '.'; }).join(' ');
|
||||
|
||||
callback(null, mx);
|
||||
if (mx.status) return callback(null, mx); // MX record is "my."
|
||||
|
||||
// cloudflare might create a conflict subdomain (https://support.cloudflare.com/hc/en-us/articles/360020296512-DNS-Troubleshooting-FAQ)
|
||||
dns.resolve(mxRecords[0].exchange, 'A', DNS_OPTIONS, function (error, mxIps) {
|
||||
if (error || mxIps.length !== 1) return callback(null, mx);
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(null, mx);
|
||||
|
||||
mx.status = mxIps[0] === ip;
|
||||
|
||||
callback(null, mx);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -481,30 +497,30 @@ function getStatus(domain, callback) {
|
||||
};
|
||||
}
|
||||
|
||||
const mailFqdn = config.mailFqdn();
|
||||
const mailFqdn = settings.mailFqdn();
|
||||
|
||||
getDomain(domain, function (error, result) {
|
||||
getDomain(domain, function (error, mailDomain) {
|
||||
if (error) return callback(error);
|
||||
|
||||
let checks = [];
|
||||
if (result.enabled) {
|
||||
if (mailDomain.enabled) {
|
||||
checks.push(
|
||||
recordResult('dns.mx', checkMx.bind(null, domain, mailFqdn)),
|
||||
recordResult('dns.dmarc', checkDmarc.bind(null, domain))
|
||||
);
|
||||
}
|
||||
|
||||
if (result.relay.provider === 'cloudron-smtp') {
|
||||
if (mailDomain.relay.provider === 'cloudron-smtp') {
|
||||
// these tests currently only make sense when using Cloudron's SMTP server at this point
|
||||
checks.push(
|
||||
recordResult('dns.spf', checkSpf.bind(null, domain, mailFqdn)),
|
||||
recordResult('dns.dkim', checkDkim.bind(null, domain)),
|
||||
recordResult('dns.dkim', checkDkim.bind(null, mailDomain)),
|
||||
recordResult('dns.ptr', checkPtr.bind(null, mailFqdn)),
|
||||
recordResult('relay', checkOutboundPort25),
|
||||
recordResult('rbl', checkRblStatus.bind(null, domain))
|
||||
);
|
||||
} else if (result.relay.provider !== 'noop') {
|
||||
checks.push(recordResult('relay', checkSmtpRelay.bind(null, result.relay)));
|
||||
} else if (mailDomain.relay.provider !== 'noop') {
|
||||
checks.push(recordResult('relay', checkSmtpRelay.bind(null, mailDomain.relay)));
|
||||
}
|
||||
|
||||
async.parallel(checks, function () {
|
||||
@@ -552,15 +568,16 @@ function checkConfiguration(callback) {
|
||||
markdownMessage += '\n\n';
|
||||
});
|
||||
|
||||
if (markdownMessage) markdownMessage += 'Email Status is checked every 30 minutes\n See the [troubleshooting docs](https://cloudron.io/documentation/troubleshooting/#mail-dns) for more information.\n';
|
||||
if (markdownMessage) markdownMessage += 'Email Status is checked every 30 minutes.\n See the [troubleshooting docs](https://cloudron.io/documentation/troubleshooting/#mail-dns) for more information.\n';
|
||||
|
||||
callback(null, markdownMessage); // empty message means all status checks succeeded
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createMailConfig(mailFqdn, callback) {
|
||||
function createMailConfig(mailFqdn, mailDomain, callback) {
|
||||
assert.strictEqual(typeof mailFqdn, 'string');
|
||||
assert.strictEqual(typeof mailDomain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('createMailConfig: generating mail config');
|
||||
@@ -571,8 +588,9 @@ function createMailConfig(mailFqdn, callback) {
|
||||
const mailOutDomains = mailDomains.filter(d => d.relay.provider !== 'noop').map(d => d.domain).join(',');
|
||||
const mailInDomains = mailDomains.filter(function (d) { return d.enabled; }).map(function (d) { return d.domain; }).join(',');
|
||||
|
||||
// mail_domain is used for SRS
|
||||
if (!safe.fs.writeFileSync(path.join(paths.ADDON_CONFIG_DIR, 'mail/mail.ini'),
|
||||
`mail_in_domains=${mailInDomains}\nmail_out_domains=${mailOutDomains}\nmail_server_name=${mailFqdn}\n\n`, 'utf8')) {
|
||||
`mail_in_domains=${mailInDomains}\nmail_out_domains=${mailOutDomains}\nmail_server_name=${mailFqdn}\nmail_domain=${mailDomain}\n\n`, 'utf8')) {
|
||||
return callback(new Error('Could not create mail var file:' + safe.error.message));
|
||||
}
|
||||
|
||||
@@ -596,13 +614,14 @@ function createMailConfig(mailFqdn, callback) {
|
||||
const enableRelay = relay.provider !== 'cloudron-smtp' && relay.provider !== 'noop',
|
||||
host = relay.host || '',
|
||||
port = relay.port || 25,
|
||||
authType = relay.username ? 'plain' : '',
|
||||
username = relay.username || '',
|
||||
password = relay.password || '';
|
||||
|
||||
if (!enableRelay) return;
|
||||
|
||||
if (!safe.fs.appendFileSync(paths.ADDON_CONFIG_DIR + '/mail/smtp_forward.ini',
|
||||
`[${domain.domain}]\nenable_outbound=true\nhost=${host}\nport=${port}\nenable_tls=true\nauth_type=plain\nauth_user=${username}\nauth_pass=${password}\n\n`, 'utf8')) {
|
||||
`[${domain.domain}]\nenable_outbound=true\nhost=${host}\nport=${port}\nenable_tls=true\nauth_type=${authType}\nauth_user=${username}\nauth_pass=${password}\n\n`, 'utf8')) {
|
||||
return callback(new Error('Could not create mail var file:' + safe.error.message));
|
||||
}
|
||||
});
|
||||
@@ -638,7 +657,7 @@ function configureMail(mailFqdn, mailDomain, callback) {
|
||||
shell.exec('startMail', 'docker rm -f mail || true', function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
createMailConfig(mailFqdn, function (error, allowInbound) {
|
||||
createMailConfig(mailFqdn, mailDomain, function (error, allowInbound) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var ports = allowInbound ? '-p 587:2525 -p 993:9993 -p 4190:4190 -p 25:2525' : '';
|
||||
@@ -674,8 +693,8 @@ function restartMail(callback) {
|
||||
|
||||
if (process.env.BOX_ENV === 'test' && !process.env.TEST_CREATE_INFRA) return callback();
|
||||
|
||||
debug(`restartMail: restarting mail container with ${config.mailFqdn()} ${config.adminDomain()}`);
|
||||
configureMail(config.mailFqdn(), config.adminDomain(), callback);
|
||||
debug(`restartMail: restarting mail container with ${settings.mailFqdn()} ${settings.adminDomain()}`);
|
||||
configureMail(settings.mailFqdn(), settings.adminDomain(), callback);
|
||||
}
|
||||
|
||||
function restartMailIfActivated(callback) {
|
||||
@@ -757,9 +776,10 @@ function txtRecordsWithSpf(domain, mailFqdn, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function ensureDkimKeySync(domain) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
function ensureDkimKeySync(mailDomain) {
|
||||
assert.strictEqual(typeof mailDomain, 'object');
|
||||
|
||||
const domain = mailDomain.domain;
|
||||
const dkimPath = path.join(paths.MAIL_DATA_DIR, `dkim/${domain}`);
|
||||
const dkimPrivateKeyFile = path.join(dkimPath, 'private');
|
||||
const dkimPublicKeyFile = path.join(dkimPath, 'public');
|
||||
@@ -782,7 +802,10 @@ function ensureDkimKeySync(domain) {
|
||||
if (!safe.child_process.execSync('openssl genrsa -out ' + dkimPrivateKeyFile + ' 1024')) return new MailError(MailError.INTERNAL_ERROR, safe.error);
|
||||
if (!safe.child_process.execSync('openssl rsa -in ' + dkimPrivateKeyFile + ' -out ' + dkimPublicKeyFile + ' -pubout -outform PEM')) return new MailError(MailError.INTERNAL_ERROR, safe.error);
|
||||
|
||||
if (!safe.fs.writeFileSync(dkimSelectorFile, constants.DKIM_SELECTOR, 'utf8')) return new MailError(MailError.INTERNAL_ERROR, safe.error);
|
||||
if (!safe.fs.writeFileSync(dkimSelectorFile, mailDomain.dkimSelector, 'utf8')) return new MailError(MailError.INTERNAL_ERROR, safe.error);
|
||||
|
||||
// if the 'yellowtent' user of OS and the 'cloudron' user of mail container don't match, the keys become inaccessible by mail code
|
||||
if (!safe.fs.chmodSync(dkimPrivateKeyFile, 0o644)) return new MailError(MailError.INTERNAL_ERROR, safe.error);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -813,11 +836,11 @@ function upsertDnsRecords(domain, mailFqdn, callback) {
|
||||
|
||||
debug(`upsertDnsRecords: updating mail dns records of domain ${domain} and mail fqdn ${mailFqdn}`);
|
||||
|
||||
maildb.get(domain, function (error, result) {
|
||||
maildb.get(domain, function (error, mailDomain) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND));
|
||||
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
|
||||
|
||||
error = ensureDkimKeySync(domain);
|
||||
error = ensureDkimKeySync(mailDomain);
|
||||
if (error) return callback(error);
|
||||
|
||||
if (process.env.BOX_ENV === 'test') return callback();
|
||||
@@ -826,11 +849,11 @@ function upsertDnsRecords(domain, mailFqdn, callback) {
|
||||
if (!dkimKey) return callback(new MailError(MailError.INTERNAL_ERROR, new Error('Failed to read dkim public key')));
|
||||
|
||||
// t=s limits the domainkey to this domain and not it's subdomains
|
||||
var dkimRecord = { subdomain: `${constants.DKIM_SELECTOR}._domainkey`, domain: domain, type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
|
||||
var dkimRecord = { subdomain: `${mailDomain.dkimSelector}._domainkey`, domain: domain, type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
|
||||
|
||||
var records = [ ];
|
||||
records.push(dkimRecord);
|
||||
if (result.enabled) {
|
||||
if (mailDomain.enabled) {
|
||||
records.push({ subdomain: '_dmarc', domain: domain, type: 'TXT', values: [ '"v=DMARC1; p=reject; pct=100"' ] });
|
||||
records.push({ subdomain: '', domain: domain, type: 'MX', values: [ '10 ' + mailFqdn + '.' ] });
|
||||
}
|
||||
@@ -862,14 +885,14 @@ function setDnsRecords(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
upsertDnsRecords(domain, config.mailFqdn(), callback);
|
||||
upsertDnsRecords(domain, settings.mailFqdn(), callback);
|
||||
}
|
||||
|
||||
function onMailFqdnChanged(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const mailFqdn = config.mailFqdn(),
|
||||
mailDomain = config.adminDomain();
|
||||
const mailFqdn = settings.mailFqdn(),
|
||||
mailDomain = settings.adminDomain();
|
||||
|
||||
domains.getAll(function (error, allDomains) {
|
||||
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
|
||||
@@ -888,13 +911,15 @@ function addDomain(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
maildb.add(domain, function (error) {
|
||||
const dkimSelector = domain === settings.adminDomain() ? 'cloudron' : ('cloudron-' + settings.adminDomain().replace(/\./g, ''));
|
||||
|
||||
maildb.add(domain, { dkimSelector }, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, 'Domain already exists'));
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'No such domain'));
|
||||
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
|
||||
|
||||
async.series([
|
||||
upsertDnsRecords.bind(null, domain, config.mailFqdn()), // do this first to ensure DKIM keys
|
||||
upsertDnsRecords.bind(null, domain, settings.mailFqdn()), // do this first to ensure DKIM keys
|
||||
restartMailIfActivated
|
||||
], NOOP_CALLBACK); // do these asynchronously
|
||||
|
||||
@@ -906,7 +931,7 @@ function removeDomain(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (domain === config.adminDomain()) return callback(new MailError(MailError.IN_USE));
|
||||
if (domain === settings.adminDomain()) return callback(new MailError(MailError.IN_USE));
|
||||
|
||||
maildb.del(domain, function (error) {
|
||||
if (error && error.reason === DatabaseError.IN_USE) return callback(new MailError(MailError.IN_USE));
|
||||
@@ -1181,7 +1206,7 @@ function getLists(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
mailboxdb.listGroups(domain, function (error, result) {
|
||||
mailboxdb.getLists(domain, function (error, result) {
|
||||
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, result);
|
||||
@@ -1193,7 +1218,7 @@ function getList(domain, listName, callback) {
|
||||
assert.strictEqual(typeof listName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
mailboxdb.getGroup(listName, domain, function (error, result) {
|
||||
mailboxdb.getList(listName, domain, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such list'));
|
||||
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -1214,13 +1239,10 @@ function addList(name, domain, members, auditSource, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
for (var i = 0; i < members.length; i++) {
|
||||
members[i] = members[i].toLowerCase();
|
||||
|
||||
error = validateName(members[i]);
|
||||
if (error) return callback(error);
|
||||
if (!validator.isEmail(members[i])) return callback(new MailError(MailError.BAD_FIELD, 'Invalid mail member: ' + members[i]));
|
||||
}
|
||||
|
||||
mailboxdb.addGroup(name, domain, members, function (error) {
|
||||
mailboxdb.addList(name, domain, members, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, 'list already exits'));
|
||||
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -1242,10 +1264,7 @@ function updateList(name, domain, members, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
for (var i = 0; i < members.length; i++) {
|
||||
members[i] = members[i].toLowerCase();
|
||||
|
||||
error = validateName(members[i]);
|
||||
if (error) return callback(error);
|
||||
if (!validator.isEmail(members[i])) return callback(new MailError(MailError.BAD_FIELD, 'Invalid email: ' + members[i]));
|
||||
}
|
||||
|
||||
mailboxdb.updateList(name, domain, members, function (error) {
|
||||
|
||||
@@ -4,10 +4,37 @@ Dear Cloudron Admin,
|
||||
|
||||
The application '<%= title %>' installed at <%= appFqdn %> was updated to app package version <%= version %>.
|
||||
|
||||
Changes:
|
||||
<%= changelog %>
|
||||
|
||||
Powered by https://cloudron.io
|
||||
|
||||
Sent at: <%= new Date().toUTCString() %>
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<center>
|
||||
|
||||
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
|
||||
|
||||
<h3>Dear <%= cloudronName %> Admin,</h3>
|
||||
|
||||
<br/>
|
||||
|
||||
<div style="width: 650px; text-align: left;">
|
||||
The application '<%= title %>' installed at <%= appFqdn %> was updated to app package version <%= version %>.
|
||||
|
||||
<h5>Changelog:</h5>
|
||||
<%- changelogHTML %>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div style="font-size: 10px; color: #333333; background: #ffffff;">
|
||||
Powered by <a href="https://cloudron.io">Cloudron</a>.<br/>
|
||||
Sent at: <%= new Date().toUTCString() %>
|
||||
</div>
|
||||
|
||||
</center>
|
||||
<% } %>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Dear Cloudron Admin,
|
||||
|
||||
<% for (var i = 0; i < apps.length; i++) { -%>
|
||||
A new version <%= apps[i].updateInfo.manifest.version %> of the app '<%= apps[i].app.manifest.title %>' installed at <%= apps[i].app.fqdn %> is available!
|
||||
A new version <%= apps[i].updateInfo.manifest.version %> of the app '<%= apps[i].app.manifest.title %>' installed at <%= apps[i].app.fqdn %> is available.
|
||||
|
||||
Changes:
|
||||
<%= apps[i].updateInfo.manifest.changelog %>
|
||||
@@ -28,10 +28,12 @@ Sent at: <%= new Date().toUTCString() %>
|
||||
|
||||
<h3>Dear <%= cloudronName %> Admin,</h3>
|
||||
|
||||
<br/>
|
||||
|
||||
<div style="width: 650px; text-align: left;">
|
||||
<% for (var i = 0; i < apps.length; i++) { -%>
|
||||
<p>
|
||||
A new version <%= apps[i].updateInfo.manifest.version %> of the app '<%= apps[i].app.manifest.title %>' installed at <a href="https://<%= apps[i].app.fqdn %>"><%= apps[i].app.fqdn %></a> is available!
|
||||
A new version <%= apps[i].updateInfo.manifest.version %> of the app '<%= apps[i].app.manifest.title %>' installed at <a href="https://<%= apps[i].app.fqdn %>"><%= apps[i].app.fqdn %></a> is available.
|
||||
</p>
|
||||
|
||||
<h5>Changelog:</h5>
|
||||
@@ -40,21 +42,20 @@ Sent at: <%= new Date().toUTCString() %>
|
||||
<br/>
|
||||
<% } -%>
|
||||
|
||||
<% if (!hasSubscription) { %>
|
||||
<% if (!hasSubscription) { -%>
|
||||
<p>Keep your Cloudron automatically up-to-date and secure by upgrading to a <a href="<%= webadminUrl %>/#/settings">paid plan</a>.</p>
|
||||
<% } else { -%>
|
||||
<% } else { -%>
|
||||
<p>
|
||||
<br/>
|
||||
<center><a href="<%= webadminUrl %>">Update now</a></center>
|
||||
<br/>
|
||||
</p>
|
||||
<% } -%>
|
||||
|
||||
<br/>
|
||||
<% } -%>
|
||||
</div>
|
||||
|
||||
<div style="font-size: 10px; color: #333333; background: #ffffff;">
|
||||
Powered by <a href="https://cloudron.io">Cloudron</a>.
|
||||
Powered by <a href="https://cloudron.io">Cloudron</a>.<br/>
|
||||
Sent at: <%= new Date().toUTCString() %>
|
||||
</div>
|
||||
|
||||
</center>
|
||||
|
||||
@@ -1,58 +1,48 @@
|
||||
{
|
||||
"app_updated.ejs": {
|
||||
"format": "html",
|
||||
"title": "WordPress",
|
||||
"appFqdn": "updated.smartserver.io",
|
||||
"version": "1.3.4",
|
||||
"changelog": "* This has changed\n * and that as well",
|
||||
"changelogHTML": "<ul><li>This has changed</li><li>and that as well</li></ul>",
|
||||
"cloudronName": "Smartserver",
|
||||
"cloudronAvatarUrl": "https://cloudron.io/img/logo.png"
|
||||
},
|
||||
"app_updates_available.ejs": {
|
||||
"format": "html",
|
||||
"webadminUrl": "https://my.cloudron.io",
|
||||
"cloudronName": "Smartserver",
|
||||
"cloudronAvatarUrl": "https://cloudron.io/img/logo.png",
|
||||
"info": {
|
||||
"pendingBoxUpdate": {
|
||||
"version": "1.3.7",
|
||||
"changelog": [
|
||||
"Feature one",
|
||||
"Feature two"
|
||||
]
|
||||
},
|
||||
"pendingAppUpdates": [{
|
||||
"manifest": {
|
||||
"title": "Wordpress",
|
||||
"version": "1.2.3",
|
||||
"changelog": "* This has changed\n * and that as well"
|
||||
}
|
||||
}],
|
||||
"finishedBoxUpdates": [{
|
||||
"boxUpdateInfo": {
|
||||
"version": "1.0.1",
|
||||
"changelog": [
|
||||
"Feature one",
|
||||
"Feature two"
|
||||
]
|
||||
}
|
||||
}, {
|
||||
"boxUpdateInfo": {
|
||||
"version": "1.0.2",
|
||||
"changelog": [
|
||||
"Feature one",
|
||||
"Feature two",
|
||||
"Feature three"
|
||||
]
|
||||
}
|
||||
}],
|
||||
"finishedAppUpdates": [{
|
||||
"toManifest": {
|
||||
"title": "Rocket.Chat",
|
||||
"version": "0.2.1",
|
||||
"changelog": "* This has changed\n * and that as well\n * some more"
|
||||
}
|
||||
}, {
|
||||
"toManifest": {
|
||||
"title": "Redmine",
|
||||
"version": "1.2.1",
|
||||
"changelog": "* This has changed\n * and that as well\n * some more"
|
||||
}
|
||||
}],
|
||||
"certRenewals": [],
|
||||
"finishedBackups": [],
|
||||
"usersAdded": [],
|
||||
"usersRemoved": [],
|
||||
"hasSubscription": false
|
||||
}
|
||||
"hasSubscription": true,
|
||||
"apps": [{
|
||||
"updateInfo": {
|
||||
"manifest": {
|
||||
"version": "1.4.3"
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"fqdn": "site.smartserver.io",
|
||||
"manifest": {
|
||||
"title": "WordPress"
|
||||
}
|
||||
},
|
||||
"changelog": "* This has changed\n * and that as well",
|
||||
"changelogHTML": "<ul><li>This has changed</li><li>and that as well</li></ul>"
|
||||
}, {
|
||||
"updateInfo": {
|
||||
"manifest": {
|
||||
"version": "0.1.3"
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"fqdn": "another.smartserver.io",
|
||||
"manifest": {
|
||||
"title": "RocketChat"
|
||||
}
|
||||
},
|
||||
"changelog": "* This has changed\n * and that as well",
|
||||
"changelogHTML": "<ul><li>This has changed</li><li>and that as well</li></ul>"
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
exports = module.exports = {
|
||||
addMailbox: addMailbox,
|
||||
addGroup: addGroup,
|
||||
addList: addList,
|
||||
|
||||
updateMailboxOwner: updateMailboxOwner,
|
||||
updateList: updateList,
|
||||
@@ -10,10 +10,10 @@ exports = module.exports = {
|
||||
|
||||
listAliases: listAliases,
|
||||
listMailboxes: listMailboxes,
|
||||
listGroups: listGroups,
|
||||
getLists: getLists,
|
||||
|
||||
getMailbox: getMailbox,
|
||||
getGroup: getGroup,
|
||||
getList: getList,
|
||||
getAlias: getAlias,
|
||||
|
||||
getAliasesForName: getAliasesForName,
|
||||
@@ -75,7 +75,7 @@ function updateMailboxOwner(name, domain, ownerId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function addGroup(name, domain, members, callback) {
|
||||
function addList(name, domain, members, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert(Array.isArray(members));
|
||||
@@ -197,7 +197,7 @@ function listMailboxes(domain, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function listGroups(domain, callback) {
|
||||
function getLists(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -211,7 +211,7 @@ function listGroups(domain, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getGroup(name, domain, callback) {
|
||||
function getList(name, domain, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -19,7 +19,7 @@ var assert = require('assert'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
safe = require('safetydance');
|
||||
|
||||
var MAILDB_FIELDS = [ 'domain', 'enabled', 'mailFromValidation', 'catchAllJson', 'relayJson' ].join(',');
|
||||
var MAILDB_FIELDS = [ 'domain', 'enabled', 'mailFromValidation', 'catchAllJson', 'relayJson', 'dkimSelector' ].join(',');
|
||||
|
||||
function postProcess(data) {
|
||||
data.enabled = !!data.enabled; // int to boolean
|
||||
@@ -34,10 +34,12 @@ function postProcess(data) {
|
||||
return data;
|
||||
}
|
||||
|
||||
function add(domain, callback) {
|
||||
function add(domain, data, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('INSERT INTO mail (domain) VALUES (?)', [ domain ], function (error) {
|
||||
database.query('INSERT INTO mail (domain, dkimSelector) VALUES (?, ?)', [ domain, data.dkimSelector || 'cloudron' ], function (error) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'mail domain already exists'));
|
||||
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND), 'no such domain');
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -24,7 +24,7 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
custom = require('./custom.js'),
|
||||
debug = require('debug')('box:mailer'),
|
||||
docker = require('./docker.js').connection,
|
||||
@@ -54,7 +54,7 @@ function getMailConfig(callback) {
|
||||
|
||||
callback(null, {
|
||||
cloudronName: cloudronName,
|
||||
notificationFrom: `"${cloudronName}" <no-reply@${config.adminDomain()}>`
|
||||
notificationFrom: `"${cloudronName}" <no-reply@${settings.adminDomain()}>`
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -84,9 +84,9 @@ function sendMail(mailOptions, callback) {
|
||||
|
||||
var transport = nodemailer.createTransport(smtpTransport({
|
||||
host: mailServerIp,
|
||||
port: config.get('smtpPort'),
|
||||
port: constants.INTERNAL_SMTP_PORT,
|
||||
auth: {
|
||||
user: mailOptions.authUser || `no-reply@${config.adminDomain()}`,
|
||||
user: mailOptions.authUser || `no-reply@${settings.adminDomain()}`,
|
||||
pass: relayToken
|
||||
}
|
||||
}));
|
||||
@@ -146,11 +146,11 @@ function sendInvite(user, invitor) {
|
||||
|
||||
var templateData = {
|
||||
user: user,
|
||||
webadminUrl: config.adminOrigin(),
|
||||
setupLink: `${config.adminOrigin()}/api/v1/session/account/setup.html?reset_token=${user.resetToken}&email=${encodeURIComponent(user.email)}`,
|
||||
webadminUrl: settings.adminOrigin(),
|
||||
setupLink: `${settings.adminOrigin()}/api/v1/session/account/setup.html?reset_token=${user.resetToken}&email=${encodeURIComponent(user.email)}`,
|
||||
invitor: invitor,
|
||||
cloudronName: mailConfig.cloudronName,
|
||||
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
};
|
||||
|
||||
var templateDataText = JSON.parse(JSON.stringify(templateData));
|
||||
@@ -183,7 +183,7 @@ function userAdded(mailTo, user) {
|
||||
var templateData = {
|
||||
user: user,
|
||||
cloudronName: mailConfig.cloudronName,
|
||||
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
};
|
||||
|
||||
var templateDataText = JSON.parse(JSON.stringify(templateData));
|
||||
@@ -233,9 +233,9 @@ function passwordReset(user) {
|
||||
|
||||
var templateData = {
|
||||
user: user,
|
||||
resetLink: `${config.adminOrigin()}/api/v1/session/password/reset.html?reset_token=${user.resetToken}&email=${encodeURIComponent(user.email)}`,
|
||||
resetLink: `${settings.adminOrigin()}/api/v1/session/password/reset.html?reset_token=${user.resetToken}&email=${encodeURIComponent(user.email)}`,
|
||||
cloudronName: mailConfig.cloudronName,
|
||||
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
};
|
||||
|
||||
var templateDataText = JSON.parse(JSON.stringify(templateData));
|
||||
@@ -289,7 +289,7 @@ function appDied(mailTo, app) {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: mailTo,
|
||||
subject: util.format('[%s] App %s is down', mailConfig.cloudronName, app.fqdn),
|
||||
text: render('app_down.ejs', { title: app.manifest.title, appFqdn: app.fqdn, supportEmail: custom.supportEmail(), format: 'text' })
|
||||
text: render('app_down.ejs', { title: app.manifest.title, appFqdn: app.fqdn, supportEmail: custom.spec().support.email, format: 'text' })
|
||||
};
|
||||
|
||||
sendMail(mailOptions);
|
||||
@@ -306,11 +306,29 @@ function appUpdated(mailTo, app, callback) {
|
||||
getMailConfig(function (error, mailConfig) {
|
||||
if (error) return debug('Error getting mail details:', error);
|
||||
|
||||
var converter = new showdown.Converter();
|
||||
var templateData = {
|
||||
title: app.manifest.title,
|
||||
appFqdn: app.fqdn,
|
||||
version: app.manifest.version,
|
||||
changelog: app.manifest.changelog,
|
||||
changelogHTML: converter.makeHtml(app.manifest.changelog),
|
||||
cloudronName: mailConfig.cloudronName,
|
||||
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
};
|
||||
|
||||
var templateDataText = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataText.format = 'text';
|
||||
|
||||
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataHTML.format = 'html';
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: mailTo,
|
||||
subject: util.format('[%s] App %s was updated', mailConfig.cloudronName, app.fqdn),
|
||||
text: render('app_updated.ejs', { title: app.manifest.title, appFqdn: app.fqdn, version: app.manifest.version, format: 'text' })
|
||||
subject: `[${mailConfig.cloudronName}] App ${app.fqdn} was updated`,
|
||||
text: render('app_updated.ejs', templateDataText),
|
||||
html: render('app_updated.ejs', templateDataHTML)
|
||||
};
|
||||
|
||||
sendMail(mailOptions, callback);
|
||||
@@ -332,11 +350,11 @@ function appUpdatesAvailable(mailTo, apps, hasSubscription, callback) {
|
||||
});
|
||||
|
||||
var templateData = {
|
||||
webadminUrl: config.adminOrigin(),
|
||||
webadminUrl: settings.adminOrigin(),
|
||||
hasSubscription: hasSubscription,
|
||||
apps: apps,
|
||||
cloudronName: mailConfig.cloudronName,
|
||||
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
};
|
||||
|
||||
var templateDataText = JSON.parse(JSON.stringify(templateData));
|
||||
|
||||
@@ -5,7 +5,7 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
dns = require('dns'),
|
||||
_ = require('underscore');
|
||||
|
||||
@@ -24,7 +24,7 @@ function resolve(hostname, rrtype, options, callback) {
|
||||
options = _.extend({ }, DEFAULT_OPTIONS, options);
|
||||
|
||||
// Only use unbound on a Cloudron
|
||||
if (config.CLOUDRON) resolver.setServers([ options.server ]);
|
||||
if (constants.CLOUDRON) resolver.setServers([ options.server ]);
|
||||
|
||||
// should callback with ECANCELLED but looks like we might hit https://github.com/nodejs/node/issues/14814
|
||||
const timerId = setTimeout(resolver.cancel.bind(resolver), options.timeout || 5000);
|
||||
|
||||
@@ -112,7 +112,7 @@ function listByUserIdPaged(userId, page, perPage, callback) {
|
||||
var data = [ userId ];
|
||||
var query = 'SELECT ' + NOTIFICATION_FIELDS + ' FROM notifications WHERE userId=?';
|
||||
|
||||
query += ' ORDER BY creationTime, title DESC LIMIT ?,?';
|
||||
query += ' ORDER BY creationTime DESC LIMIT ?,?';
|
||||
|
||||
data.push((page-1)*perPage);
|
||||
data.push(perPage);
|
||||
|
||||
@@ -24,13 +24,15 @@ exports = module.exports = {
|
||||
|
||||
let assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
auditSource = require('./auditsource.js'),
|
||||
changelog = require('./changelog.js'),
|
||||
custom = require('./custom.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:notifications'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
mailer = require('./mailer.js'),
|
||||
notificationdb = require('./notificationdb.js'),
|
||||
settings = require('./settings.js'),
|
||||
users = require('./users.js'),
|
||||
util = require('util');
|
||||
|
||||
@@ -144,7 +146,7 @@ function userAdded(performedBy, eventId, user, callback) {
|
||||
|
||||
actionForAllAdmins([ performedBy, user.id ], function (admin, done) {
|
||||
mailer.userAdded(admin.email, user);
|
||||
add(admin.id, eventId, 'User added', `User ${user.fallbackEmail} was added`, done);
|
||||
add(admin.id, eventId, `User '${user.displayName}' added`, `User '${user.username || user.email || user.fallbackEmail}' was added.`, done);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
@@ -156,7 +158,7 @@ function userRemoved(performedBy, eventId, user, callback) {
|
||||
|
||||
actionForAllAdmins([ performedBy, user.id ], function (admin, done) {
|
||||
mailer.userRemoved(admin.email, user);
|
||||
add(admin.id, eventId, 'User removed', `User ${user.username || user.email || user.fallbackEmail} was removed`, done);
|
||||
add(admin.id, eventId, `User '${user.displayName}' removed`, `User '${user.username || user.email || user.fallbackEmail}' was removed.`, done);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
@@ -167,7 +169,7 @@ function adminChanged(performedBy, eventId, user, callback) {
|
||||
|
||||
actionForAllAdmins([ performedBy, user.id ], function (admin, done) {
|
||||
mailer.adminChanged(admin.email, user, user.admin);
|
||||
add(admin.id, eventId, 'Admin status change', `User ${user.username || user.email || user.fallbackEmail} ${user.admin ? 'is now an admin' : 'is no more an admin'}`, done);
|
||||
add(admin.id, eventId, `User '${user.displayName} ' ${user.admin ? 'is now an admin' : 'is no more an admin'}`, `User '${user.username || user.email || user.fallbackEmail}' ${user.admin ? 'is now an admin' : 'is no more an admin'}.`, done);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
@@ -193,8 +195,8 @@ function oomEvent(eventId, app, addon, containerId, event, callback) {
|
||||
message = 'The container has been restarted automatically. Consider increasing the [memory limit](https://docs.docker.com/v17.09/edge/engine/reference/commandline/update/#update-a-containers-kernel-memory-constraints)';
|
||||
}
|
||||
|
||||
if (custom.alertsEmail()) mailer.oomEvent(custom.alertsEmail(), program, event);
|
||||
if (!custom.sendAlertsToCloudronAdmins()) return callback();
|
||||
if (custom.spec().alerts.email) mailer.oomEvent(custom.spec().alerts.email, program, event);
|
||||
if (!custom.spec().alerts.notifyCloudronAdmins) return callback();
|
||||
|
||||
actionForAllAdmins([], function (admin, done) {
|
||||
mailer.oomEvent(admin.email, program, event);
|
||||
@@ -208,8 +210,8 @@ function appUp(eventId, app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (custom.alertsEmail()) mailer.appUp(custom.alertsEmail(), app);
|
||||
if (!custom.sendAlertsToCloudronAdmins()) return callback();
|
||||
if (custom.spec().alerts.email) mailer.appUp(custom.spec().alerts.email, app);
|
||||
if (!custom.spec().alerts.notifyCloudronAdmins) return callback();
|
||||
|
||||
actionForAllAdmins([], function (admin, done) {
|
||||
mailer.appUp(admin.email, app);
|
||||
@@ -222,8 +224,8 @@ function appDied(eventId, app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (custom.alertsEmail()) mailer.appDied(custom.alertsEmail(), app);
|
||||
if (!custom.sendAlertsToCloudronAdmins()) return callback();
|
||||
if (custom.spec().alerts.email) mailer.appDied(custom.spec().alerts.email, app);
|
||||
if (!custom.spec().alerts.notifyCloudronAdmins) return callback();
|
||||
|
||||
actionForAllAdmins([], function (admin, callback) {
|
||||
mailer.appDied(admin.email, app);
|
||||
@@ -236,8 +238,13 @@ function appUpdated(eventId, app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const tmp = app.manifest.description.match(/<upstream>(.*)<\/upstream>/i);
|
||||
const upstreamVersion = (tmp && tmp[1]) ? tmp[1] : '';
|
||||
const title = upstreamVersion ? `${app.manifest.title} at ${app.fqdn} updated to ${upstreamVersion} (package version ${app.manifest.version})`
|
||||
: `${app.manifest.title} at ${app.fqdn} updated to package version ${app.manifest.version}`;
|
||||
|
||||
actionForAllAdmins([], function (admin, done) {
|
||||
add(admin.id, eventId, `App ${app.fqdn} updated`, `The application ${app.manifest.title} installed at https://${app.fqdn} was updated to package version ${app.manifest.version}.`, function (error) {
|
||||
add(admin.id, eventId, title, `The application ${app.manifest.title} installed at https://${app.fqdn} was updated.\n\nChangelog:\n${app.manifest.changelog}\n`, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
mailer.appUpdated(admin.email, app, function (error) {
|
||||
@@ -248,14 +255,27 @@ function appUpdated(eventId, app, callback) {
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function boxUpdated(oldVersion, newVersion, callback) {
|
||||
assert.strictEqual(typeof oldVersion, 'string');
|
||||
assert.strictEqual(typeof newVersion, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const changes = changelog.getChanges(newVersion);
|
||||
const changelogMarkdown = changes.map((m) => `* ${m}\n`).join('');
|
||||
|
||||
actionForAllAdmins([], function (admin, done) {
|
||||
add(admin.id, null, `Cloudron updated to v${newVersion}`, `Cloudron was updated from v${oldVersion} to v${newVersion}.\n\nChangelog:\n${changelogMarkdown}\n`, done);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function certificateRenewalError(eventId, vhost, errorMessage, callback) {
|
||||
assert.strictEqual(typeof eventId, 'string');
|
||||
assert.strictEqual(typeof vhost, 'string');
|
||||
assert.strictEqual(typeof errorMessage, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (custom.alertsEmail()) mailer.certificateRenewalError(custom.alertsEmail(), vhost, errorMessage);
|
||||
if (!custom.sendAlertsToCloudronAdmins()) return callback();
|
||||
if (custom.spec().alerts.email) mailer.certificateRenewalError(custom.spec().alerts.email, vhost, errorMessage);
|
||||
if (!custom.spec().alerts.notifyCloudronAdmins) return callback();
|
||||
|
||||
actionForAllAdmins([], function (admin, callback) {
|
||||
mailer.certificateRenewalError(admin.email, vhost, errorMessage);
|
||||
@@ -269,12 +289,12 @@ function backupFailed(eventId, taskId, errorMessage, callback) {
|
||||
assert.strictEqual(typeof errorMessage, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (custom.alertsEmail()) mailer.backupFailed(custom.alertsEmail(), errorMessage, `${config.adminOrigin()}/logs.html?taskId=${taskId}`);
|
||||
if (!custom.sendAlertsToCloudronAdmins()) return callback();
|
||||
if (custom.spec().alerts.email) mailer.backupFailed(custom.spec().alerts.email, errorMessage, `${settings.adminOrigin()}/logs.html?taskId=${taskId}`);
|
||||
if (!custom.spec().alerts.notifyCloudronAdmins) return callback();
|
||||
|
||||
actionForAllAdmins([], function (admin, callback) {
|
||||
mailer.backupFailed(admin.email, errorMessage, `${config.adminOrigin()}/logs.html?taskId=${taskId}`);
|
||||
add(admin.id, eventId, 'Failed to backup', `Backup failed: ${errorMessage}. Logs are available [here](/logs.html?taskId=${taskId}). Will be retried in 4 hours`, callback);
|
||||
mailer.backupFailed(admin.email, errorMessage, `${settings.adminOrigin()}/logs.html?taskId=${taskId}`);
|
||||
add(admin.id, eventId, 'Backup failed', `Backup failed: ${errorMessage}. Logs are available [here](/logs.html?taskId=${taskId}). Will be retried in 4 hours`, callback);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
@@ -326,6 +346,9 @@ function onEvent(id, action, source, data, callback) {
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// external ldap syncer does not generate notifications - FIXME username might be an issue here
|
||||
if (source.username === auditSource.EXTERNAL_LDAP_TASK.username) return callback();
|
||||
|
||||
switch (action) {
|
||||
case eventlog.ACTION_USER_ADD:
|
||||
return userAdded(source.userId, id, data.user, callback);
|
||||
@@ -347,6 +370,7 @@ function onEvent(id, action, source, data, callback) {
|
||||
return appUp(id, data.app, callback);
|
||||
|
||||
case eventlog.ACTION_APP_UPDATE_FINISH:
|
||||
if (!data.app.appStoreId) return callback(); // skip notification of dev apps
|
||||
return appUpdated(id, data.app, callback);
|
||||
|
||||
case eventlog.ACTION_CERTIFICATE_RENEWAL:
|
||||
@@ -355,8 +379,13 @@ function onEvent(id, action, source, data, callback) {
|
||||
return certificateRenewalError(id, data.domain, data.errorMessage, callback);
|
||||
|
||||
case eventlog.ACTION_BACKUP_FINISH:
|
||||
if (!data.errorMessage || source.username !== 'cron') return callback();
|
||||
return backupFailed(id, data.taskId, data.errorMessage, callback); // only notify for automated backups
|
||||
if (!data.errorMessage) return callback();
|
||||
if (source.username !== auditSource.CRON.username && !data.timedOut) return callback(); // manual stop by user
|
||||
|
||||
return backupFailed(id, data.taskId, data.errorMessage, callback); // only notify for automated backups or timedout
|
||||
|
||||
case eventlog.ACTION_UPDATE_FINISH:
|
||||
return boxUpdated(data.oldVersion, data.newVersion, callback);
|
||||
|
||||
default:
|
||||
return callback();
|
||||
|
||||
@@ -61,7 +61,7 @@ app.controller('Controller', ['$scope', function ($scope) {
|
||||
<div class="control-label" ng-show="setupForm.password.$dirty && setupForm.password.$invalid">
|
||||
<small ng-show="setupForm.password.$dirty && setupForm.password.$invalid">Password must be atleast 8 characters</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="password" name="password" ng-pattern="/^.{8,30}$/" required>
|
||||
<input type="password" class="form-control" ng-model="password" name="password" ng-pattern="/^.{8,}$/" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)) }">
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
|
||||
<footer class="text-center">
|
||||
<span class="text-muted">© 2016-19 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
|
||||
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fa fa-twitter"></i></a></span>
|
||||
<span class="text-muted"><a href="https://chat.cloudron.io" target="_blank">Chat <i class="fa fa-comments"></i></a></span>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<link href="<%= adminOrigin %>/theme.css" rel="stylesheet">
|
||||
|
||||
<!-- Custom Fonts -->
|
||||
<link href="<%= adminOrigin %>/3rdparty/css/font-awesome.min.css" rel="stylesheet" rel="stylesheet" type="text/css">
|
||||
<link href="<%= adminOrigin %>/3rdparty/fontawesome/css/all.min.css" rel="stylesheet" rel="stylesheet" type="text/css">
|
||||
|
||||
<!-- jQuery-->
|
||||
<script src="<%= adminOrigin %>/3rdparty/js/jquery.min.js"></script>
|
||||
|
||||
71
src/paths.js
71
src/paths.js
@@ -1,45 +1,60 @@
|
||||
'use strict';
|
||||
|
||||
var config = require('./config.js'),
|
||||
var constants = require('./constants.js'),
|
||||
path = require('path');
|
||||
|
||||
function baseDir() {
|
||||
const homeDir = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
|
||||
if (constants.CLOUDRON) return homeDir;
|
||||
if (constants.TEST) return path.join(homeDir, '.cloudron_test');
|
||||
// cannot reach
|
||||
}
|
||||
|
||||
// keep these values in sync with start.sh
|
||||
exports = module.exports = {
|
||||
baseDir: baseDir,
|
||||
|
||||
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
|
||||
INFRA_VERSION_FILE: path.join(config.baseDir(), 'platformdata/INFRA_VERSION'),
|
||||
INFRA_VERSION_FILE: path.join(baseDir(), 'platformdata/INFRA_VERSION'),
|
||||
|
||||
LICENSE_FILE: '/etc/cloudron/LICENSE',
|
||||
CUSTOM_FILE: '/etc/cloudron/custom.yml',
|
||||
PROVIDER_FILE: '/etc/cloudron/PROVIDER',
|
||||
|
||||
PLATFORM_DATA_DIR: path.join(config.baseDir(), 'platformdata'),
|
||||
APPS_DATA_DIR: path.join(config.baseDir(), 'appsdata'),
|
||||
BOX_DATA_DIR: path.join(config.baseDir(), 'boxdata'),
|
||||
PLATFORM_DATA_DIR: path.join(baseDir(), 'platformdata'),
|
||||
APPS_DATA_DIR: path.join(baseDir(), 'appsdata'),
|
||||
BOX_DATA_DIR: path.join(baseDir(), 'boxdata'),
|
||||
|
||||
ACME_CHALLENGES_DIR: path.join(config.baseDir(), 'platformdata/acme'),
|
||||
ADDON_CONFIG_DIR: path.join(config.baseDir(), 'platformdata/addons'),
|
||||
COLLECTD_APPCONFIG_DIR: path.join(config.baseDir(), 'platformdata/collectd/collectd.conf.d'),
|
||||
LOGROTATE_CONFIG_DIR: path.join(config.baseDir(), 'platformdata/logrotate.d'),
|
||||
NGINX_CONFIG_DIR: path.join(config.baseDir(), 'platformdata/nginx'),
|
||||
NGINX_APPCONFIG_DIR: path.join(config.baseDir(), 'platformdata/nginx/applications'),
|
||||
NGINX_CERT_DIR: path.join(config.baseDir(), 'platformdata/nginx/cert'),
|
||||
BACKUP_INFO_DIR: path.join(config.baseDir(), 'platformdata/backup'),
|
||||
UPDATE_DIR: path.join(config.baseDir(), 'platformdata/update'),
|
||||
SNAPSHOT_INFO_FILE: path.join(config.baseDir(), 'platformdata/backup/snapshot-info.json'),
|
||||
DYNDNS_INFO_FILE: path.join(config.baseDir(), 'platformdata/dyndns-info.json'),
|
||||
CUSTOM_FILE: path.join(baseDir(), 'boxdata/custom.yml'),
|
||||
|
||||
ACME_CHALLENGES_DIR: path.join(baseDir(), 'platformdata/acme'),
|
||||
ADDON_CONFIG_DIR: path.join(baseDir(), 'platformdata/addons'),
|
||||
COLLECTD_APPCONFIG_DIR: path.join(baseDir(), 'platformdata/collectd/collectd.conf.d'),
|
||||
LOGROTATE_CONFIG_DIR: path.join(baseDir(), 'platformdata/logrotate.d'),
|
||||
NGINX_CONFIG_DIR: path.join(baseDir(), 'platformdata/nginx'),
|
||||
NGINX_APPCONFIG_DIR: path.join(baseDir(), 'platformdata/nginx/applications'),
|
||||
NGINX_CERT_DIR: path.join(baseDir(), 'platformdata/nginx/cert'),
|
||||
BACKUP_INFO_DIR: path.join(baseDir(), 'platformdata/backup'),
|
||||
UPDATE_DIR: path.join(baseDir(), 'platformdata/update'),
|
||||
SNAPSHOT_INFO_FILE: path.join(baseDir(), 'platformdata/backup/snapshot-info.json'),
|
||||
DYNDNS_INFO_FILE: path.join(baseDir(), 'platformdata/dyndns-info.json'),
|
||||
VERSION_FILE: path.join(baseDir(), 'platformdata/VERSION'),
|
||||
|
||||
SESSION_SECRET_FILE: path.join(baseDir(), 'boxdata/session.secret'),
|
||||
SESSION_DIR: path.join(baseDir(), 'platformdata/sessions'),
|
||||
|
||||
// this is not part of appdata because an icon may be set before install
|
||||
APP_ICONS_DIR: path.join(config.baseDir(), 'boxdata/appicons'),
|
||||
MAIL_DATA_DIR: path.join(config.baseDir(), 'boxdata/mail'),
|
||||
ACME_ACCOUNT_KEY_FILE: path.join(config.baseDir(), 'boxdata/acme/acme.key'),
|
||||
APP_CERTS_DIR: path.join(config.baseDir(), 'boxdata/certs'),
|
||||
CLOUDRON_AVATAR_FILE: path.join(config.baseDir(), 'boxdata/avatar.png'),
|
||||
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'boxdata/updatechecker.json'),
|
||||
APP_ICONS_DIR: path.join(baseDir(), 'boxdata/appicons'),
|
||||
MAIL_DATA_DIR: path.join(baseDir(), 'boxdata/mail'),
|
||||
ACME_ACCOUNT_KEY_FILE: path.join(baseDir(), 'boxdata/acme/acme.key'),
|
||||
APP_CERTS_DIR: path.join(baseDir(), 'boxdata/certs'),
|
||||
CLOUDRON_AVATAR_FILE: path.join(baseDir(), 'boxdata/avatar.png'),
|
||||
UPDATE_CHECKER_FILE: path.join(baseDir(), 'boxdata/updatechecker.json'),
|
||||
|
||||
LOG_DIR: path.join(config.baseDir(), 'platformdata/logs'),
|
||||
TASKS_LOG_DIR: path.join(config.baseDir(), 'platformdata/logs/tasks'),
|
||||
CRASH_LOG_DIR: path.join(config.baseDir(), 'platformdata/logs/crash'),
|
||||
LOG_DIR: path.join(baseDir(), 'platformdata/logs'),
|
||||
TASKS_LOG_DIR: path.join(baseDir(), 'platformdata/logs/tasks'),
|
||||
CRASH_LOG_DIR: path.join(baseDir(), 'platformdata/logs/crash'),
|
||||
|
||||
// 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')
|
||||
BACKUP_LOG_FILE: path.join(baseDir(), 'platformdata/logs/backup/app.log'),
|
||||
UPDATER_LOG_FILE: path.join(baseDir(), 'platformdata/logs/updater/app.log')
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@ var addons = require('./addons.js'),
|
||||
settings = require('./settings.js'),
|
||||
sftp = require('./sftp.js'),
|
||||
shell = require('./shell.js'),
|
||||
taskmanager = require('./taskmanager.js'),
|
||||
tasks = require('./tasks.js'),
|
||||
_ = require('underscore');
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
@@ -75,13 +75,14 @@ function start(callback) {
|
||||
}
|
||||
|
||||
function stop(callback) {
|
||||
taskmanager.pauseTasks(callback);
|
||||
tasks.stopAllTasks(callback);
|
||||
}
|
||||
|
||||
function onPlatformReady() {
|
||||
debug('onPlatformReady: platform is ready');
|
||||
exports._isReady = true;
|
||||
taskmanager.resumeTasks();
|
||||
|
||||
apps.schedulePendingTasks(NOOP_CALLBACK);
|
||||
|
||||
applyPlatformConfig(NOOP_CALLBACK);
|
||||
pruneInfraImages(NOOP_CALLBACK);
|
||||
@@ -119,10 +120,13 @@ function pruneInfraImages(callback) {
|
||||
if (!line) continue;
|
||||
let parts = line.split(' '); // [ ID, Repo:Tag@Digest ]
|
||||
if (image.tag === parts[1]) continue; // keep
|
||||
debug(`pruneInfraImages: removing unused image of ${image.repo}: ${line}`);
|
||||
debug(`pruneInfraImages: removing unused image of ${image.repo}: tag: ${parts[1]} id: ${parts[0]}`);
|
||||
|
||||
shell.exec('pruneInfraImages', `docker rmi ${parts[0]}`, iteratorCallback);
|
||||
let result = safe.child_process.execSync(`docker rmi ${parts[0]}`, { encoding: 'utf8' });
|
||||
if (result === null) debug(`Erroring removing image ${parts[0]}: ${safe.error.mesage}`);
|
||||
}
|
||||
|
||||
iteratorCallback();
|
||||
}, callback);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ var appstore = require('./appstore.js'),
|
||||
async = require('async'),
|
||||
backups = require('./backups.js'),
|
||||
BackupsError = require('./backups.js').BackupsError,
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
clients = require('./clients.js'),
|
||||
cloudron = require('./cloudron.js'),
|
||||
@@ -32,6 +31,7 @@ var appstore = require('./appstore.js'),
|
||||
semver = require('semver'),
|
||||
settings = require('./settings.js'),
|
||||
superagent = require('superagent'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
users = require('./users.js'),
|
||||
UsersError = users.UsersError,
|
||||
tld = require('tldjs'),
|
||||
@@ -78,6 +78,7 @@ ProvisionError.BAD_STATE = 'Bad State';
|
||||
ProvisionError.ALREADY_SETUP = 'Already Setup';
|
||||
ProvisionError.INTERNAL_ERROR = 'Internal Error';
|
||||
ProvisionError.EXTERNAL_ERROR = 'External Error';
|
||||
ProvisionError.LICENSE_ERROR = 'License Error';
|
||||
ProvisionError.ALREADY_PROVISIONED = 'Already Provisioned';
|
||||
|
||||
function setProgress(task, message, callback) {
|
||||
@@ -86,29 +87,8 @@ function setProgress(task, message, callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
function autoprovision(autoconf, callback) {
|
||||
assert.strictEqual(typeof autoconf, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
async.eachSeries(Object.keys(autoconf), function (key, iteratorDone) {
|
||||
debug(`autoprovision: ${key}`);
|
||||
|
||||
switch (key) {
|
||||
case 'backupConfig':
|
||||
settings.setBackupConfig(autoconf[key], iteratorDone);
|
||||
break;
|
||||
default:
|
||||
debug(`autoprovision: ${key} ignored`);
|
||||
return iteratorDone();
|
||||
}
|
||||
}, function (error) {
|
||||
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function autoRegister(callback) {
|
||||
function autoRegister(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!fs.existsSync(paths.LICENSE_FILE)) return callback();
|
||||
@@ -118,7 +98,7 @@ function autoRegister(callback) {
|
||||
|
||||
debug('Auto-registering cloudron');
|
||||
|
||||
appstore.registerWithLicense(license.trim(), function (error) {
|
||||
appstore.registerWithLicense(license.trim(), domain, function (error) {
|
||||
if (error && error.reason !== AppstoreError.ALREADY_REGISTERED) {
|
||||
debug('Failed to auto-register cloudron', error);
|
||||
return callback(new ProvisionError(ProvisionError.LICENSE_ERROR, 'Failed to auto-register Cloudron with license. Please contact support@cloudron.io'));
|
||||
@@ -133,20 +113,18 @@ function unprovision(callback) {
|
||||
|
||||
debug('unprovision');
|
||||
|
||||
config.setAdminDomain('');
|
||||
config.setAdminFqdn('');
|
||||
|
||||
// TODO: also cancel any existing configureWebadmin task
|
||||
async.series([
|
||||
settings.setAdmin.bind(null, '', ''),
|
||||
mail.clearDomains,
|
||||
domains.clear
|
||||
], callback);
|
||||
}
|
||||
|
||||
|
||||
function setup(dnsConfig, autoconf, auditSource, callback) {
|
||||
function setup(dnsConfig, backupConfig, auditSource, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof autoconf, 'object');
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -188,12 +166,11 @@ function setup(dnsConfig, autoconf, auditSource, callback) {
|
||||
callback(); // now that args are validated run the task in the background
|
||||
|
||||
async.series([
|
||||
autoRegister,
|
||||
autoRegister.bind(null, domain),
|
||||
domains.prepareDashboardDomain.bind(null, domain, auditSource, (progress) => setProgress('setup', progress.message, NOOP_CALLBACK)),
|
||||
cloudron.setDashboardDomain.bind(null, domain, auditSource), // this sets up the config.fqdn()
|
||||
mail.addDomain.bind(null, domain), // this relies on config.mailFqdn()
|
||||
setProgress.bind(null, 'setup', 'Applying auto-configuration'),
|
||||
autoprovision.bind(null, autoconf),
|
||||
cloudron.setDashboardDomain.bind(null, domain, auditSource),
|
||||
mail.addDomain.bind(null, domain), // this relies on settings.mailFqdn() and settings.adminDomain()
|
||||
(next) => { if (!backupConfig) return next(); settings.setBackupConfig(backupConfig, next); },
|
||||
setProgress.bind(null, 'setup', 'Done'),
|
||||
eventlog.add.bind(null, eventlog.ACTION_PROVISION, auditSource, { })
|
||||
], function (error) {
|
||||
@@ -264,16 +241,15 @@ function activate(username, password, email, displayName, ip, auditSource, callb
|
||||
});
|
||||
}
|
||||
|
||||
function restore(backupConfig, backupId, version, autoconf, auditSource, callback) {
|
||||
function restore(backupConfig, backupId, version, auditSource, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof version, 'string');
|
||||
assert.strictEqual(typeof autoconf, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
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`));
|
||||
if (semver.major(constants.VERSION) !== semver.major(version) || semver.minor(constants.VERSION) !== semver.minor(version)) return callback(new ProvisionError(ProvisionError.BAD_STATE, `Run cloudron-setup with --version ${version} to restore from this backup`));
|
||||
|
||||
if (gProvisionStatus.setup.active || gProvisionStatus.restore.active) return callback(new ProvisionError(ProvisionError.BAD_STATE, 'Already setting up or restoring'));
|
||||
|
||||
@@ -287,7 +263,7 @@ function restore(backupConfig, backupId, version, autoconf, auditSource, callbac
|
||||
|
||||
users.isActivated(function (error, activated) {
|
||||
if (error) return done(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
|
||||
if (activated) return done(new ProvisionError(ProvisionError.ALREADY_PROVISIONED, 'Already activated'));
|
||||
if (activated) return done(new ProvisionError(ProvisionError.ALREADY_PROVISIONED, 'Already activated. Restore with a fresh Cloudron installation.'));
|
||||
|
||||
backups.testConfig(backupConfig, function (error) {
|
||||
if (error && error.reason === BackupsError.BAD_FIELD) return done(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
|
||||
@@ -301,12 +277,8 @@ function restore(backupConfig, backupId, version, autoconf, auditSource, callbac
|
||||
async.series([
|
||||
setProgress.bind(null, 'restore', 'Downloading backup'),
|
||||
backups.restore.bind(null, backupConfig, backupId, (progress) => setProgress('restore', progress.message, NOOP_CALLBACK)),
|
||||
setProgress.bind(null, 'restore', 'Applying auto-configuration'),
|
||||
autoprovision.bind(null, autoconf),
|
||||
// currently, our suggested restore flow is after a dnsSetup. The dnSetup creates DKIM keys and updates the DNS
|
||||
// for this reason, we have to re-setup DNS after a restore so it has DKIm from the backup
|
||||
// Once we have a 100% IP based restore, we can skip this
|
||||
mail.setDnsRecords.bind(null, config.adminDomain()),
|
||||
cloudron.setupDashboard.bind(null, auditSource, (progress) => setProgress('restore', progress.message, NOOP_CALLBACK)),
|
||||
settings.setBackupConfig.bind(null, backupConfig), // update with the latest backupConfig
|
||||
eventlog.add.bind(null, eventlog.ACTION_RESTORE, auditSource, { backupId }),
|
||||
], function (error) {
|
||||
gProvisionStatus.restore.active = false;
|
||||
@@ -328,11 +300,11 @@ function getStatus(callback) {
|
||||
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, _.extend({
|
||||
version: config.version(),
|
||||
apiServerOrigin: config.apiServerOrigin(), // used by CaaS tool
|
||||
provider: config.provider(),
|
||||
version: constants.VERSION,
|
||||
apiServerOrigin: settings.apiServerOrigin(), // used by CaaS tool
|
||||
provider: sysinfo.provider(),
|
||||
cloudronName: cloudronName,
|
||||
adminFqdn: config.adminDomain() ? config.adminFqdn() : null,
|
||||
adminFqdn: settings.adminDomain() ? settings.adminFqdn() : null,
|
||||
activated: activated,
|
||||
}, gProvisionStatus));
|
||||
});
|
||||
|
||||
@@ -16,15 +16,16 @@ exports = module.exports = {
|
||||
|
||||
renewCerts: renewCerts,
|
||||
|
||||
// the 'configure' functions always ensure a certificate
|
||||
configureDefaultServer: configureDefaultServer,
|
||||
// the 'configure' ensure a certificate and generate nginx config
|
||||
configureAdmin: configureAdmin,
|
||||
configureApp: configureApp,
|
||||
unconfigureApp: unconfigureApp,
|
||||
|
||||
// these only generate nginx config
|
||||
writeDefaultConfig: writeDefaultConfig,
|
||||
writeAdminConfig: writeAdminConfig,
|
||||
writeAppConfig: writeAppConfig,
|
||||
|
||||
reload: reload,
|
||||
removeAppConfigs: removeAppConfigs,
|
||||
|
||||
// exported for testing
|
||||
@@ -36,7 +37,6 @@ var acme2 = require('./cert/acme2.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
caas = require('./cert/caas.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
crypto = require('crypto'),
|
||||
debug = require('debug')('box:reverseproxy'),
|
||||
@@ -51,7 +51,9 @@ var acme2 = require('./cert/acme2.js'),
|
||||
paths = require('./paths.js'),
|
||||
rimraf = require('rimraf'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
shell = require('./shell.js'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
users = require('./users.js'),
|
||||
util = require('util');
|
||||
|
||||
@@ -90,8 +92,8 @@ function getCertApi(domainObject, callback) {
|
||||
var api = domainObject.tlsConfig.provider === 'caas' ? caas : acme2;
|
||||
|
||||
var options = { prod: false, performHttpAuthorization: false, wildcard: false, email: '' };
|
||||
if (domainObject.tlsConfig.provider !== 'caas') { // matches 'le-prod' or 'letsencrypt-prod'
|
||||
options.prod = domainObject.tlsConfig.provider.match(/.*-prod/) !== null;
|
||||
if (domainObject.tlsConfig.provider !== 'caas') {
|
||||
options.prod = domainObject.tlsConfig.provider.match(/.*-prod/) !== null; // matches 'le-prod' or 'letsencrypt-prod'
|
||||
options.performHttpAuthorization = domainObject.provider.match(/noop|manual|wildcard/) !== null;
|
||||
options.wildcard = !!domainObject.tlsConfig.wildcard;
|
||||
}
|
||||
@@ -259,13 +261,13 @@ function getFallbackCertificate(domain, callback) {
|
||||
var certFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`);
|
||||
var keyFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`);
|
||||
|
||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath, type: 'provisioned' });
|
||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
|
||||
|
||||
// check for auto-generated or user set fallback certs
|
||||
certFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`);
|
||||
keyFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.key`);
|
||||
|
||||
callback(null, { certFilePath, keyFilePath, type: 'fallback' });
|
||||
callback(null, { certFilePath, keyFilePath });
|
||||
}
|
||||
|
||||
function setAppCertificateSync(location, domainObject, certificate) {
|
||||
@@ -331,7 +333,9 @@ function notifyCertChanged(vhost, callback) {
|
||||
assert.strictEqual(typeof vhost, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (vhost !== config.mailFqdn()) return callback();
|
||||
debug(`notifyCertChanged: vhost: ${vhost} mailFqdn: ${settings.mailFqdn()}`);
|
||||
|
||||
if (vhost !== settings.mailFqdn()) return callback();
|
||||
|
||||
mail.handleCertChanged(callback);
|
||||
}
|
||||
@@ -348,12 +352,12 @@ function ensureCertificate(vhost, domain, auditSource, callback) {
|
||||
getCertApi(domainObject, function (error, api, apiOptions) {
|
||||
if (error) return callback(error);
|
||||
|
||||
getCertificateByHostname(vhost, domainObject, function (error, currentBundle) {
|
||||
getCertificateByHostname(vhost, domainObject, function (_error, currentBundle) {
|
||||
if (currentBundle) {
|
||||
debug(`ensureCertificate: ${vhost} certificate already exists at ${currentBundle.keyFilePath}`);
|
||||
|
||||
if (currentBundle.certFilePath.endsWith('.user.cert')) return callback(null, currentBundle); // user certs cannot be renewed
|
||||
if (!isExpiringSync(currentBundle.certFilePath, 24 * 30) && providerMatchesSync(domainObject, currentBundle.certFilePath, apiOptions)) return callback(null, currentBundle);
|
||||
if (currentBundle.certFilePath.endsWith('.user.cert')) return callback(null, currentBundle, { renewed: false }); // user certs cannot be renewed
|
||||
if (!isExpiringSync(currentBundle.certFilePath, 24 * 30) && providerMatchesSync(domainObject, currentBundle.certFilePath, apiOptions)) return callback(null, currentBundle, { renewed: false });
|
||||
debug(`ensureCertificate: ${vhost} cert require renewal`);
|
||||
} else {
|
||||
debug(`ensureCertificate: ${vhost} cert does not exist`);
|
||||
@@ -362,15 +366,28 @@ function ensureCertificate(vhost, domain, auditSource, callback) {
|
||||
debug('ensureCertificate: getting certificate for %s with options %j', vhost, apiOptions);
|
||||
|
||||
api.getCertificate(vhost, domain, apiOptions, function (error, certFilePath, keyFilePath) {
|
||||
debug(`ensureCertificate: error: ${error ? error.message : 'null'} cert: ${certFilePath}`);
|
||||
|
||||
eventlog.add(currentBundle ? eventlog.ACTION_CERTIFICATE_RENEWAL : eventlog.ACTION_CERTIFICATE_NEW, auditSource, { domain: vhost, errorMessage: error ? error.message : '' });
|
||||
|
||||
if (error && currentBundle && !isExpiringSync(currentBundle.certFilePath, 0)) {
|
||||
debug('ensureCertificate: continue using existing bundle since renewal failed');
|
||||
return callback(null, currentBundle, { renewed: false });
|
||||
}
|
||||
|
||||
notifyCertChanged(vhost, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// if no cert was returned use fallback. the fallback/caas provider will not provide any for example
|
||||
if (!certFilePath || !keyFilePath) return getFallbackCertificate(domain, callback);
|
||||
if (certFilePath && keyFilePath) return callback(null, { certFilePath, keyFilePath }, { renewed: true });
|
||||
|
||||
callback(null, { certFilePath, keyFilePath });
|
||||
debug(`ensureCertificate: renewal of ${vhost} failed. using fallback certificates for ${domain}`);
|
||||
|
||||
// if no cert was returned use fallback. the fallback/caas provider will not provide any for example
|
||||
getFallbackCertificate(domain, function (error, bundle) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, bundle, { renewed: false });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -386,13 +403,12 @@ function writeAdminNginxConfig(bundle, configFileName, vhost, callback) {
|
||||
|
||||
var data = {
|
||||
sourceDir: path.resolve(__dirname, '..'),
|
||||
adminOrigin: config.adminOrigin(),
|
||||
adminOrigin: settings.adminOrigin(),
|
||||
vhost: vhost, // if vhost is empty it will become the default_server
|
||||
hasIPv6: config.hasIPv6(),
|
||||
hasIPv6: sysinfo.hasIPv6(),
|
||||
endpoint: 'admin',
|
||||
certFilePath: bundle.certFilePath,
|
||||
keyFilePath: bundle.keyFilePath,
|
||||
xFrameOptions: 'SAMEORIGIN',
|
||||
robotsTxtQuoted: JSON.stringify('User-agent: *\nDisallow: /\n')
|
||||
};
|
||||
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
|
||||
@@ -400,8 +416,6 @@ function writeAdminNginxConfig(bundle, configFileName, vhost, callback) {
|
||||
|
||||
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) return callback(safe.error);
|
||||
|
||||
if (vhost) safe.fs.unlinkSync(path.join(paths.NGINX_APPCONFIG_DIR, 'admin.conf')); // remove legacy admin.conf. remove after 3.5
|
||||
|
||||
reload(callback);
|
||||
}
|
||||
|
||||
@@ -427,6 +441,8 @@ function writeAdminConfig(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`writeAdminConfig: writing admin config for ${domain}`);
|
||||
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -450,20 +466,19 @@ function writeAppNginxConfig(app, bundle, callback) {
|
||||
|
||||
var data = {
|
||||
sourceDir: sourceDir,
|
||||
adminOrigin: config.adminOrigin(),
|
||||
adminOrigin: settings.adminOrigin(),
|
||||
vhost: app.fqdn,
|
||||
hasIPv6: config.hasIPv6(),
|
||||
hasIPv6: sysinfo.hasIPv6(),
|
||||
port: app.httpPort,
|
||||
endpoint: endpoint,
|
||||
certFilePath: bundle.certFilePath,
|
||||
keyFilePath: bundle.keyFilePath,
|
||||
robotsTxtQuoted: app.robotsTxt ? JSON.stringify(app.robotsTxt) : null,
|
||||
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN' // once all apps have been updated/
|
||||
robotsTxtQuoted: app.robotsTxt ? JSON.stringify(app.robotsTxt) : null
|
||||
};
|
||||
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
|
||||
|
||||
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
|
||||
debug('writing config for "%s" to %s with options %j', app.fqdn, nginxConfigFilename, data);
|
||||
debug('writeAppNginxConfig: writing config for "%s" to %s with options %j', app.fqdn, nginxConfigFilename, data);
|
||||
|
||||
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) {
|
||||
debug('Error creating nginx config for "%s" : %s', app.fqdn, safe.error.message);
|
||||
@@ -483,12 +498,11 @@ function writeAppRedirectNginxConfig(app, fqdn, bundle, callback) {
|
||||
sourceDir: path.resolve(__dirname, '..'),
|
||||
vhost: fqdn,
|
||||
redirectTo: app.fqdn,
|
||||
hasIPv6: config.hasIPv6(),
|
||||
hasIPv6: sysinfo.hasIPv6(),
|
||||
endpoint: 'redirect',
|
||||
certFilePath: bundle.certFilePath,
|
||||
keyFilePath: bundle.keyFilePath,
|
||||
robotsTxtQuoted: null,
|
||||
xFrameOptions: 'SAMEORIGIN'
|
||||
robotsTxtQuoted: null
|
||||
};
|
||||
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
|
||||
|
||||
@@ -504,6 +518,27 @@ function writeAppRedirectNginxConfig(app, fqdn, bundle, callback) {
|
||||
reload(callback);
|
||||
}
|
||||
|
||||
function writeAppConfig(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getCertificate(app.fqdn, app.domain, function (error, bundle) {
|
||||
if (error) return callback(error);
|
||||
|
||||
writeAppNginxConfig(app, bundle, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(app.alternateDomains, function (alternateDomain, iteratorDone) {
|
||||
getCertificate(alternateDomain.fqdn, alternateDomain.domain, function (error, bundle) {
|
||||
if (error) return iteratorDone(error);
|
||||
|
||||
writeAppRedirectNginxConfig(app, alternateDomain.fqdn, bundle, iteratorDone);
|
||||
});
|
||||
}, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function configureApp(app, auditSource, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
@@ -515,11 +550,11 @@ function configureApp(app, auditSource, callback) {
|
||||
writeAppNginxConfig(app, bundle, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(app.alternateDomains, function (alternateDomain, callback) {
|
||||
async.eachSeries(app.alternateDomains, function (alternateDomain, iteratorDone) {
|
||||
ensureCertificate(alternateDomain.fqdn, alternateDomain.domain, auditSource, function (error, bundle) {
|
||||
if (error) return callback(error);
|
||||
if (error) return iteratorDone(error);
|
||||
|
||||
writeAppRedirectNginxConfig(app, alternateDomain.fqdn, bundle, callback);
|
||||
writeAppRedirectNginxConfig(app, alternateDomain.fqdn, bundle, iteratorDone);
|
||||
});
|
||||
}, callback);
|
||||
});
|
||||
@@ -550,7 +585,7 @@ function renewCerts(options, auditSource, progressCallback, callback) {
|
||||
var appDomains = [];
|
||||
|
||||
// add webadmin domain
|
||||
appDomains.push({ domain: config.adminDomain(), fqdn: config.adminFqdn(), type: 'webadmin', nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, `${config.adminFqdn()}.conf`) });
|
||||
appDomains.push({ domain: settings.adminDomain(), fqdn: settings.adminFqdn(), type: 'webadmin', nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, `${settings.adminFqdn()}.conf`) });
|
||||
|
||||
// add app main
|
||||
allApps.forEach(function (app) {
|
||||
@@ -564,14 +599,17 @@ function renewCerts(options, auditSource, progressCallback, callback) {
|
||||
|
||||
if (options.domain) appDomains = appDomains.filter(function (appDomain) { return appDomain.domain === options.domain; });
|
||||
|
||||
let progress = 1;
|
||||
let progress = 1, renewed = [];
|
||||
|
||||
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) {
|
||||
ensureCertificate(appDomain.fqdn, appDomain.domain, auditSource, function (error, bundle, state) {
|
||||
if (error) return iteratorCallback(error); // this can happen if cloudron is not setup yet
|
||||
|
||||
if (state.renewed) renewed.push(appDomain.fqdn);
|
||||
|
||||
// hack to check if the app's cert changed or not. this doesn't handle prod/staging le change since they use same file name
|
||||
let currentNginxConfig = safe.fs.readFileSync(appDomain.nginxConfigFilename, 'utf8') || '';
|
||||
if (currentNginxConfig.includes(bundle.certFilePath)) return iteratorCallback();
|
||||
@@ -580,14 +618,22 @@ function renewCerts(options, auditSource, progressCallback, callback) {
|
||||
|
||||
// reconfigure since the cert changed
|
||||
var configureFunc;
|
||||
if (appDomain.type === 'webadmin') configureFunc = writeAdminNginxConfig.bind(null, bundle, `${config.adminFqdn()}.conf`, config.adminFqdn());
|
||||
if (appDomain.type === 'webadmin') configureFunc = writeAdminNginxConfig.bind(null, bundle, `${settings.adminFqdn()}.conf`, settings.adminFqdn());
|
||||
else if (appDomain.type === 'main') configureFunc = writeAppNginxConfig.bind(null, appDomain.app, bundle);
|
||||
else if (appDomain.type === 'alternate') configureFunc = writeAppRedirectNginxConfig.bind(null, appDomain.app, appDomain.fqdn, bundle);
|
||||
else return iteratorCallback(new Error(`Unknown domain type for ${appDomain.fqdn}. This should never happen`));
|
||||
|
||||
configureFunc(iteratorCallback);
|
||||
});
|
||||
}, callback);
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug(`renewCerts: Renewed certs of ${JSON.stringify(renewed)}`);
|
||||
if (renewed.length === 0) return callback(null);
|
||||
|
||||
// reload nginx if any certs were updated but the config was not rewritten
|
||||
reload(callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -599,18 +645,18 @@ function removeAppConfigs() {
|
||||
}
|
||||
}
|
||||
|
||||
function configureDefaultServer(callback) {
|
||||
function writeDefaultConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var certFilePath = path.join(paths.NGINX_CERT_DIR, 'default.cert');
|
||||
var keyFilePath = path.join(paths.NGINX_CERT_DIR, 'default.key');
|
||||
|
||||
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
|
||||
debug('configureDefaultServer: create new cert');
|
||||
debug('writeDefaultConfig: create new cert');
|
||||
|
||||
var cn = 'cloudron-' + (new Date()).toISOString(); // randomize date a bit to keep firefox happy
|
||||
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}`);
|
||||
debug(`writeDefaultConfig: could not generate certificate: ${safe.error.message}`);
|
||||
return callback(safe.error);
|
||||
}
|
||||
}
|
||||
@@ -618,7 +664,7 @@ function configureDefaultServer(callback) {
|
||||
writeAdminNginxConfig({ certFilePath, keyFilePath }, constants.NGINX_DEFAULT_CONFIG_FILE_NAME, '', function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('configureDefaultServer: done');
|
||||
debug('writeDefaultConfig: done');
|
||||
|
||||
callback(null);
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ exports = module.exports = {
|
||||
getApps: getApps,
|
||||
getAppIcon: getAppIcon,
|
||||
installApp: installApp,
|
||||
configureApp: configureApp,
|
||||
uninstallApp: uninstallApp,
|
||||
restoreApp: restoreApp,
|
||||
backupApp: backupApp,
|
||||
@@ -13,6 +12,22 @@ exports = module.exports = {
|
||||
getLogs: getLogs,
|
||||
getLogStream: getLogStream,
|
||||
listBackups: listBackups,
|
||||
repairApp: repairApp,
|
||||
|
||||
setAccessRestriction: setAccessRestriction,
|
||||
setLabel: setLabel,
|
||||
setTags: setTags,
|
||||
setIcon: setIcon,
|
||||
setMemoryLimit: setMemoryLimit,
|
||||
setAutomaticBackup: setAutomaticBackup,
|
||||
setAutomaticUpdate: setAutomaticUpdate,
|
||||
setRobotsTxt: setRobotsTxt,
|
||||
setCertificate: setCertificate,
|
||||
setDebugMode: setDebugMode,
|
||||
setEnvironment: setEnvironment,
|
||||
setMailbox: setMailbox,
|
||||
setLocation: setLocation,
|
||||
setDataDir: setDataDir,
|
||||
|
||||
stopApp: stopApp,
|
||||
startApp: startApp,
|
||||
@@ -21,8 +36,6 @@ exports = module.exports = {
|
||||
|
||||
cloneApp: cloneApp,
|
||||
|
||||
setOwner: setOwner,
|
||||
|
||||
uploadFile: uploadFile,
|
||||
downloadFile: downloadFile
|
||||
};
|
||||
@@ -31,22 +44,37 @@ var apps = require('../apps.js'),
|
||||
AppsError = apps.AppsError,
|
||||
assert = require('assert'),
|
||||
auditSource = require('../auditsource.js'),
|
||||
config = require('../config.js'),
|
||||
debug = require('debug')('box:routes/apps'),
|
||||
fs = require('fs'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
paths = require('../paths.js'),
|
||||
safe = require('safetydance'),
|
||||
util = require('util'),
|
||||
WebSocket = require('ws');
|
||||
|
||||
function toHttpError(appError) {
|
||||
switch (appError.reason) {
|
||||
case AppsError.NOT_FOUND:
|
||||
return new HttpError(404, appError);
|
||||
case AppsError.ALREADY_EXISTS:
|
||||
case AppsError.BAD_STATE:
|
||||
return new HttpError(409, appError);
|
||||
case AppsError.BAD_FIELD:
|
||||
return new HttpError(400, appError);
|
||||
case AppsError.PLAN_LIMIT:
|
||||
return new HttpError(402, appError);
|
||||
case AppsError.EXTERNAL_ERROR:
|
||||
return new HttpError(424, appError);
|
||||
case AppsError.INTERNAL_ERROR:
|
||||
default:
|
||||
return new HttpError(500, appError);
|
||||
}
|
||||
}
|
||||
|
||||
function getApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
apps.get(req.params.id, function (error, app) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(200, apps.removeInternalFields(app)));
|
||||
});
|
||||
@@ -56,7 +84,7 @@ function getApps(req, res, next) {
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
|
||||
apps.getAllByUser(req.user, function (error, allApps) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
allApps = allApps.map(apps.removeRestrictedFields);
|
||||
|
||||
@@ -67,9 +95,9 @@ function getApps(req, res, next) {
|
||||
function getAppIcon(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
var iconPath = paths.APP_ICONS_DIR + '/' + req.params.id + '.png';
|
||||
fs.exists(iconPath, function (exists) {
|
||||
if (!exists) return next(new HttpError(404, 'No such icon'));
|
||||
apps.getIconPath(req.params.id, { original: req.query.original }, function (error, iconPath) {
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
res.sendFile(iconPath);
|
||||
});
|
||||
}
|
||||
@@ -78,7 +106,6 @@ function installApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
var data = req.body;
|
||||
data.ownerId = req.user.id;
|
||||
|
||||
// atleast one
|
||||
if ('manifest' in data && typeof data.manifest !== 'object') return next(new HttpError(400, 'manifest must be an object'));
|
||||
@@ -107,8 +134,6 @@ function installApp(req, res, next) {
|
||||
|
||||
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
|
||||
|
||||
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
|
||||
|
||||
if ('sso' in data && typeof data.sso !== 'boolean') return next(new HttpError(400, 'sso must be a boolean'));
|
||||
if ('enableBackup' in data && typeof data.enableBackup !== 'boolean') return next(new HttpError(400, 'enableBackup must be a boolean'));
|
||||
if ('enableAutomaticUpdate' in data && typeof data.enableAutomaticUpdate !== 'boolean') return next(new HttpError(400, 'enableAutomaticUpdate must be a boolean'));
|
||||
@@ -127,82 +152,240 @@ function installApp(req, res, next) {
|
||||
if (Object.keys(data.env).some(function (key) { return typeof data.env[key] !== 'string'; })) return next(new HttpError(400, 'env must contain values as strings'));
|
||||
}
|
||||
|
||||
if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean'));
|
||||
|
||||
debug('Installing app :%j', data);
|
||||
|
||||
apps.install(data, req.user, auditSource.fromRequest(req), function (error, app) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
|
||||
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
|
||||
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === AppsError.PLAN_LIMIT) return next(new HttpError(402, error.message));
|
||||
if (error && error.reason === AppsError.BAD_CERTIFICATE) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
apps.install(data, req.user, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, app));
|
||||
next(new HttpSuccess(202, { id: result.id, taskId: result.taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
function configureApp(req, res, next) {
|
||||
function setAccessRestriction(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
var data = req.body;
|
||||
if (typeof req.body.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction must be an object'));
|
||||
|
||||
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'));
|
||||
apps.setAccessRestriction(req.params.id, req.body.accessRestriction, auditSource.fromRequest(req), function (error) {
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
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'));
|
||||
next(new HttpSuccess(200, {}));
|
||||
});
|
||||
}
|
||||
|
||||
// falsy values in cert and key unset the cert
|
||||
if (data.key && typeof data.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
|
||||
if (data.cert && typeof data.key !== 'string') return next(new HttpError(400, 'key must be a string'));
|
||||
if (data.cert && !data.key) return next(new HttpError(400, 'key must be provided'));
|
||||
if (!data.cert && data.key) return next(new HttpError(400, 'cert must be provided'));
|
||||
function setLabel(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
|
||||
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
|
||||
if (typeof req.body.label !== 'string') return next(new HttpError(400, 'label 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'));
|
||||
apps.setLabel(req.params.id, req.body.label, auditSource.fromRequest(req), function (error) {
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
if (('debugMode' in data) && typeof data.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object'));
|
||||
next(new HttpSuccess(200, {}));
|
||||
});
|
||||
}
|
||||
|
||||
if (data.robotsTxt && typeof data.robotsTxt !== 'string') return next(new HttpError(400, 'robotsTxt must be a string'));
|
||||
function setTags(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
if ('mailboxName' in data && typeof data.mailboxName !== 'string') return next(new HttpError(400, 'mailboxName must be a string'));
|
||||
if (!Array.isArray(req.body.tags)) return next(new HttpError(400, 'tags must be an array'));
|
||||
if (req.body.tags.some((t) => typeof t !== 'string')) return next(new HttpError(400, 'tags array must contain strings'));
|
||||
|
||||
apps.setTags(req.params.id, req.body.tags, auditSource.fromRequest(req), function (error) {
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(200, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function setIcon(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
if (req.body.icon !== null && typeof req.body.icon !== 'string') return next(new HttpError(400, 'icon is null or a base-64 image string'));
|
||||
|
||||
apps.setIcon(req.params.id, req.body.icon, auditSource.fromRequest(req), function (error) {
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(200, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function setMemoryLimit(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
if (typeof req.body.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
|
||||
|
||||
apps.setMemoryLimit(req.params.id, req.body.memoryLimit, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
function setAutomaticBackup(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enable must be a boolean'));
|
||||
|
||||
apps.setAutomaticBackup(req.params.id, req.body.enable, auditSource.fromRequest(req), function (error) {
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(200, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function setAutomaticUpdate(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enable must be a boolean'));
|
||||
|
||||
apps.setAutomaticUpdate(req.params.id, req.body.enable, auditSource.fromRequest(req), function (error) {
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(200, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function setRobotsTxt(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
if (req.body.robotsTxt !== null && typeof req.body.robotsTxt !== 'string') return next(new HttpError(400, 'robotsTxt is not a string'));
|
||||
|
||||
apps.setRobotsTxt(req.params.id, req.body.robotsTxt, auditSource.fromRequest(req), function (error) {
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(200, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function setCertificate(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
if (req.body.key !== null && typeof req.body.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
|
||||
if (req.body.cert !== null && typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
|
||||
if (req.body.cert && !req.body.key) return next(new HttpError(400, 'key must be provided'));
|
||||
if (!req.body.cert && req.body.key) return next(new HttpError(400, 'cert must be provided'));
|
||||
|
||||
apps.setCertificate(req.params.id, req.body, auditSource.fromRequest(req), function (error) {
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(200, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function setEnvironment(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
if (!req.body.env || typeof req.body.env !== 'object') return next(new HttpError(400, 'env must be an object'));
|
||||
if (Object.keys(req.body.env).some((key) => typeof req.body.env[key] !== 'string')) return next(new HttpError(400, 'env must contain values as strings'));
|
||||
|
||||
apps.setEnvironment(req.params.id, req.body.env, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
function setDebugMode(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
if (req.body.debugMode !== null && typeof req.body.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object'));
|
||||
|
||||
apps.setDebugMode(req.params.id, req.body.debugMode, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
function setMailbox(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
if (req.body.mailboxName !== null && typeof req.body.mailboxName !== 'string') return next(new HttpError(400, 'mailboxName must be a string'));
|
||||
|
||||
apps.setMailbox(req.params.id, req.body.mailboxName, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
function setLocation(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
if (typeof req.body.location !== 'string') return next(new HttpError(400, 'location must be string')); // location may be an empty string
|
||||
if (!req.body.domain) return next(new HttpError(400, 'domain is required'));
|
||||
if (typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be string'));
|
||||
|
||||
if ('portBindings' in req.body && typeof req.body.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
|
||||
|
||||
if ('alternateDomains' in req.body) {
|
||||
if (!Array.isArray(req.body.alternateDomains)) return next(new HttpError(400, 'alternateDomains must be an array'));
|
||||
if (req.body.alternateDomains.some(function (d) { return (typeof d.domain !== 'string' || typeof d.subdomain !== 'string'); })) return next(new HttpError(400, 'alternateDomains array must contain objects with domain and subdomain strings'));
|
||||
}
|
||||
|
||||
if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean'));
|
||||
|
||||
apps.setLocation(req.params.id, req.body, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
function setDataDir(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
if (req.body.dataDir !== null && typeof req.body.dataDir !== 'string') return next(new HttpError(400, 'dataDir must be a string'));
|
||||
|
||||
apps.setDataDir(req.params.id, req.body.dataDir, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
function repairApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
debug('Repair app id:%s', req.params.id);
|
||||
|
||||
const data = req.body;
|
||||
|
||||
if (data.backupId && typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be string or null'));
|
||||
if (data.backupFormat && typeof data.backupFormat !== 'string') return next(new HttpError(400, 'backupFormat must be string or null'));
|
||||
|
||||
if (data.location && typeof data.location !== 'string') return next(new HttpError(400, 'location is required'));
|
||||
if (data.domain && typeof data.domain !== 'string') return next(new HttpError(400, 'domain is required'));
|
||||
|
||||
if ('alternateDomains' in data) {
|
||||
if (!Array.isArray(data.alternateDomains)) return next(new HttpError(400, 'alternateDomains must be an array'));
|
||||
if (data.alternateDomains.some(function (d) { return (typeof d.domain !== 'string' || typeof d.subdomain !== 'string'); })) return next(new HttpError(400, 'alternateDomains array must contain objects with domain and subdomain strings'));
|
||||
}
|
||||
|
||||
if ('env' in data) {
|
||||
if (!data.env || typeof data.env !== 'object') return next(new HttpError(400, 'env must be an object'));
|
||||
if (Object.keys(data.env).some(function (key) { return typeof data.env[key] !== 'string'; })) return next(new HttpError(400, 'env must contain values as strings'));
|
||||
}
|
||||
if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean'));
|
||||
|
||||
if ('label' in data && typeof data.label !== 'string') return next(new HttpError(400, 'label must be a string'));
|
||||
if ('dataDir' in data && typeof data.dataDir !== 'string') return next(new HttpError(400, 'dataDir must be a string'));
|
||||
apps.repair(req.params.id, data, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
debug('Configuring app id:%s data:%j', req.params.id, data);
|
||||
|
||||
apps.configure(req.params.id, data, req.user, auditSource.fromRequest(req), function (error) {
|
||||
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
|
||||
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === AppsError.BAD_CERTIFICATE) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, { }));
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -217,14 +400,10 @@ function restoreApp(req, res, next) {
|
||||
if (!('backupId' in req.body)) return next(new HttpError(400, 'backupId is required'));
|
||||
if (data.backupId !== null && typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be string or null'));
|
||||
|
||||
apps.restore(req.params.id, data, auditSource.fromRequest(req), function (error) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
apps.restore(req.params.id, data, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { }));
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -233,7 +412,6 @@ function cloneApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
var data = req.body;
|
||||
data.ownerId = req.user.id;
|
||||
|
||||
debug('Clone app id:%s', req.params.id);
|
||||
|
||||
@@ -242,19 +420,12 @@ function cloneApp(req, res, next) {
|
||||
if (typeof data.domain !== 'string') return next(new HttpError(400, 'domain is required'));
|
||||
if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
|
||||
|
||||
apps.clone(req.params.id, data, req.user, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
|
||||
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
|
||||
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === AppsError.PLAN_LIMIT) return next(new HttpError(402, error.message));
|
||||
if (error && error.reason === AppsError.BAD_CERTIFICATE) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean'));
|
||||
|
||||
next(new HttpSuccess(201, { id: result.id }));
|
||||
apps.clone(req.params.id, data, req.user, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(201, { id: result.id, taskId: result.taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -263,13 +434,10 @@ function backupApp(req, res, next) {
|
||||
|
||||
debug('Backup app id:%s', req.params.id);
|
||||
|
||||
apps.backup(req.params.id, function (error) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
apps.backup(req.params.id, function (error, result) {
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { }));
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -278,12 +446,10 @@ function uninstallApp(req, res, next) {
|
||||
|
||||
debug('Uninstalling app id:%s', req.params.id);
|
||||
|
||||
apps.uninstall(req.params.id, auditSource.fromRequest(req), function (error) {
|
||||
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error));
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
apps.uninstall(req.params.id, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { }));
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -292,12 +458,10 @@ function startApp(req, res, next) {
|
||||
|
||||
debug('Start app id:%s', req.params.id);
|
||||
|
||||
apps.start(req.params.id, function (error) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
apps.start(req.params.id, function (error, result) {
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { }));
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -306,12 +470,10 @@ function stopApp(req, res, next) {
|
||||
|
||||
debug('Stop app id:%s', req.params.id);
|
||||
|
||||
apps.stop(req.params.id, function (error) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
apps.stop(req.params.id, function (error, result) {
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { }));
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -326,18 +488,15 @@ function updateApp(req, res, next) {
|
||||
if ('appStoreId' in data && typeof data.appStoreId !== 'string') return next(new HttpError(400, 'appStoreId must be a string'));
|
||||
if (!data.manifest && !data.appStoreId) return next(new HttpError(400, 'appStoreId or manifest is required'));
|
||||
|
||||
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
|
||||
if ('skipBackup' in data && typeof data.skipBackup !== 'boolean') return next(new HttpError(400, 'skipBackup must be a boolean'));
|
||||
if ('force' in data && typeof data.force !== 'boolean') return next(new HttpError(400, 'force must be a boolean'));
|
||||
|
||||
debug('Update app id:%s to manifest:%j', req.params.id, data.manifest);
|
||||
|
||||
apps.update(req.params.id, req.body, auditSource.fromRequest(req), function (error) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
apps.update(req.params.id, req.body, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { }));
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -361,9 +520,7 @@ function getLogStream(req, res, next) {
|
||||
};
|
||||
|
||||
apps.getLogs(req.params.id, options, function (error, logStream) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(412, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
@@ -398,9 +555,7 @@ function getLogs(req, res, next) {
|
||||
};
|
||||
|
||||
apps.getLogs(req.params.id, options, function (error, logStream) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(412, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/x-logs',
|
||||
@@ -454,9 +609,7 @@ function exec(req, res, next) {
|
||||
var tty = req.query.tty === 'true' ? true : false;
|
||||
|
||||
apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
if (req.headers['upgrade'] !== 'tcp') return next(new HttpError(404, 'exec requires TCP upgrade'));
|
||||
|
||||
@@ -496,9 +649,7 @@ function execWebSocket(req, res, next) {
|
||||
var tty = req.query.tty === 'true' ? true : false;
|
||||
|
||||
apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
debug('Connected to terminal');
|
||||
|
||||
@@ -538,8 +689,7 @@ function listBackups(req, res, next) {
|
||||
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
|
||||
|
||||
apps.listBackups(page, perPage, req.params.id, function (error, result) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(200, { backups: result }));
|
||||
});
|
||||
@@ -554,8 +704,7 @@ function uploadFile(req, res, next) {
|
||||
if (!req.files.file) return next(new HttpError(400, 'file must be provided as multipart'));
|
||||
|
||||
apps.uploadFile(req.params.id, req.files.file.path, req.query.file, function (error) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
debug('uploadFile: done');
|
||||
|
||||
@@ -571,8 +720,7 @@ function downloadFile(req, res, next) {
|
||||
if (typeof req.query.file !== 'string' || !req.query.file) return next(new HttpError(400, 'file query argument must be provided'));
|
||||
|
||||
apps.downloadFile(req.params.id, req.query.file, function (error, stream, info) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
if (error) return next(toHttpError(error));
|
||||
|
||||
var headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
@@ -585,17 +733,3 @@ function downloadFile(req, res, next) {
|
||||
stream.pipe(res);
|
||||
});
|
||||
}
|
||||
|
||||
function setOwner(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.ownerId !== 'string') return next(new HttpError(400, 'ownerId must be a string'));
|
||||
|
||||
apps.setOwner(req.params.id, req.body.ownerId, function (error) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, { }));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,9 +12,19 @@ exports = module.exports = {
|
||||
var appstore = require('../appstore.js'),
|
||||
AppstoreError = appstore.AppstoreError,
|
||||
assert = require('assert'),
|
||||
custom = require('../custom.js'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess;
|
||||
|
||||
function isAppAllowed(appstoreId) {
|
||||
if (custom.spec().appstore.blacklist.includes(appstoreId)) return false;
|
||||
|
||||
if (!custom.spec().appstore.whitelist) return true;
|
||||
if (!custom.spec().appstore.whitelist[appstoreId]) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getApps(req, res, next) {
|
||||
appstore.getApps(function (error, apps) {
|
||||
if (error && error.reason === AppstoreError.INVALID_TOKEN) return next(new HttpError(402, error.message));
|
||||
@@ -22,13 +32,18 @@ function getApps(req, res, next) {
|
||||
if (error && error.reason === AppstoreError.NOT_REGISTERED) return next(new HttpError(412, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, { apps: apps }));
|
||||
let filteredApps = apps.filter((app) => !custom.spec().appstore.blacklist.includes(app.id));
|
||||
if (custom.spec().appstore.whitelist) filteredApps = filteredApps.filter((app) => app.id in custom.spec().appstore.whitelist);
|
||||
|
||||
next(new HttpSuccess(200, { apps: filteredApps }));
|
||||
});
|
||||
}
|
||||
|
||||
function getApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.appstoreId, 'string');
|
||||
|
||||
if (!isAppAllowed(req.params.appstoreId)) return next(new HttpError(405, 'feature disabled by admin'));
|
||||
|
||||
appstore.getApp(req.params.appstoreId, function (error, app) {
|
||||
if (error && error.reason === AppstoreError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppstoreError.INVALID_TOKEN) return next(new HttpError(402, error.message));
|
||||
@@ -44,6 +59,8 @@ function getAppVersion(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.appstoreId, 'string');
|
||||
assert.strictEqual(typeof req.params.versionId, 'string');
|
||||
|
||||
if (!isAppAllowed(req.params.appstoreId)) return next(new HttpError(405, 'feature disabled by admin'));
|
||||
|
||||
appstore.getAppVersion(req.params.appstoreId, req.params.versionId, function (error, manifest) {
|
||||
if (error && error.reason === AppstoreError.NOT_FOUND) return next(new HttpError(404, 'No such app or version'));
|
||||
if (error && error.reason === AppstoreError.INVALID_TOKEN) return next(new HttpError(402, error.message));
|
||||
|
||||
@@ -12,7 +12,8 @@ exports = module.exports = {
|
||||
getLogStream: getLogStream,
|
||||
setDashboardAndMailDomain: setDashboardAndMailDomain,
|
||||
prepareDashboardDomain: prepareDashboardDomain,
|
||||
renewCerts: renewCerts
|
||||
renewCerts: renewCerts,
|
||||
syncExternalLdap: syncExternalLdap
|
||||
};
|
||||
|
||||
let assert = require('assert'),
|
||||
@@ -20,6 +21,9 @@ let assert = require('assert'),
|
||||
auditSource = require('../auditsource.js'),
|
||||
cloudron = require('../cloudron.js'),
|
||||
CloudronError = cloudron.CloudronError,
|
||||
custom = require('../custom.js'),
|
||||
disks = require('../disks.js'),
|
||||
externalldap = require('../externalldap.js'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
updater = require('../updater.js'),
|
||||
@@ -50,15 +54,17 @@ function getConfig(req, res, next) {
|
||||
}
|
||||
|
||||
function getDisks(req, res, next) {
|
||||
cloudron.getDisks(function (error, result) {
|
||||
disks.getDisks(function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
next(new HttpSuccess(200, result));
|
||||
});
|
||||
}
|
||||
|
||||
function update(req, res, next) {
|
||||
if ('skipBackup' in req.body && typeof req.body.skipBackup !== 'boolean') return next(new HttpError(400, 'skipBackup must be a boolean'));
|
||||
|
||||
// this only initiates the update, progress can be checked via the progress route
|
||||
updater.updateToLatest(auditSource.fromRequest(req), function (error, taskId) {
|
||||
updater.updateToLatest(req.body, auditSource.fromRequest(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) return next(new HttpError(500, error));
|
||||
@@ -150,6 +156,8 @@ function getLogStream(req, res, next) {
|
||||
function setDashboardAndMailDomain(req, res, next) {
|
||||
if (!req.body.domain || typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
|
||||
|
||||
if (!custom.spec().domains.changeDashboardDomain) return next(new HttpError(405, 'feature disabled by admin'));
|
||||
|
||||
cloudron.setDashboardAndMailDomain(req.body.domain, auditSource.fromRequest(req), function (error) {
|
||||
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(404, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
@@ -161,6 +169,8 @@ function setDashboardAndMailDomain(req, res, next) {
|
||||
function prepareDashboardDomain(req, res, next) {
|
||||
if (!req.body.domain || typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
|
||||
|
||||
if (!custom.spec().domains.changeDashboardDomain) return next(new HttpError(405, 'feature disabled by admin'));
|
||||
|
||||
cloudron.prepareDashboardDomain(req.body.domain, auditSource.fromRequest(req), function (error, taskId) {
|
||||
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(404, error.message));
|
||||
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
@@ -178,3 +188,11 @@ function renewCerts(req, res, next) {
|
||||
next(new HttpSuccess(202, { taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
function syncExternalLdap(req, res, next) {
|
||||
externalldap.startSyncer(function (error, taskId) {
|
||||
if (error) return next(new HttpError(500, error.message));
|
||||
|
||||
next(new HttpSuccess(202, { taskId: taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ function login(req, res, next) {
|
||||
if (!user.ghost && user.twoFactorAuthenticationEnabled) {
|
||||
if (!req.body.totpToken) return next(new HttpError(401, 'A totpToken must be provided'));
|
||||
|
||||
let verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken });
|
||||
let verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken, window: 2 });
|
||||
if (!verified) return next(new HttpError(401, 'Invalid totpToken'));
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ exports = module.exports = {
|
||||
update: update,
|
||||
del: del,
|
||||
|
||||
checkDnsRecords: checkDnsRecords,
|
||||
|
||||
verifyDomainLock: verifyDomainLock
|
||||
};
|
||||
|
||||
@@ -152,3 +154,21 @@ function del(req, res, next) {
|
||||
next(new HttpSuccess(204));
|
||||
});
|
||||
}
|
||||
|
||||
function checkDnsRecords(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.domain, 'string');
|
||||
|
||||
if (!('subdomain' in req.query)) return next(new HttpError(400, 'subdomain is required'));
|
||||
|
||||
// some DNS providers like DigitalOcean take a really long time to verify credentials (https://github.com/expressjs/timeout/issues/26)
|
||||
req.clearTimeout();
|
||||
|
||||
domains.checkDnsRecords(req.query.subdomain, req.params.domain, function (error, result) {
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return next(new HttpError(404, error.message)); // domain (and not record!) not found
|
||||
if (error && error.reason === DomainsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
|
||||
if (error && error.reason === DomainsError.ACCESS_DENIED) return next(new HttpSuccess(200, { error: { reason: error.reason, message: error.message }}));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, { needsOverwrite: result.needsOverwrite }));
|
||||
});
|
||||
}
|
||||
@@ -5,21 +5,34 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var middleware = require('../middleware/index.js'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
url = require('url');
|
||||
|
||||
var graphiteProxy = middleware.proxy(url.parse('http://127.0.0.1:8417'));
|
||||
// for testing locally: curl 'http://127.0.0.1:8417/graphite-web/render?format=json&from=-1min&target=absolute(collectd.localhost.du-docker.capacity-usage)'
|
||||
// the datapoint is (value, timestamp) https://buildmedia.readthedocs.org/media/pdf/graphite/0.9.16/graphite.pdf
|
||||
const graphiteProxy = middleware.proxy(url.parse('http://127.0.0.1:8417'));
|
||||
|
||||
function getGraphs(req, res, next) {
|
||||
var parsedUrl = url.parse(req.url, true /* parseQueryString */);
|
||||
delete parsedUrl.query['access_token'];
|
||||
delete req.headers['authorization'];
|
||||
delete req.headers['cookies'];
|
||||
req.url = url.format({ pathname: 'render', query: parsedUrl.query });
|
||||
|
||||
// 'graphite-web' is the URL_PREFIX in docker-graphite
|
||||
req.url = url.format({ pathname: 'graphite-web/render', query: parsedUrl.query });
|
||||
|
||||
// graphs may take very long to respond so we run into headers already sent issues quite often
|
||||
// nginx still has a request timeout which can deal with this then.
|
||||
req.clearTimeout();
|
||||
|
||||
graphiteProxy(req, res, next);
|
||||
graphiteProxy(req, res, function (error) {
|
||||
if (!error) return next();
|
||||
|
||||
if (error.code === 'ECONNREFUSED') return next(new HttpError(424, 'Unable to connect to graphite'));
|
||||
// ECONNRESET here is most likely because of a bug in the query or the uwsgi buffer size is too small
|
||||
if (error.code === 'ECONNRESET') return next(new HttpError(424, 'Unable to query graphite'));
|
||||
|
||||
next(new HttpError(500, error));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ exports = module.exports = {
|
||||
services: require('./services.js'),
|
||||
settings: require('./settings.js'),
|
||||
support: require('./support.js'),
|
||||
sysadmin: require('./sysadmin.js'),
|
||||
tasks: require('./tasks.js'),
|
||||
users: require('./users.js')
|
||||
};
|
||||
|
||||
@@ -350,6 +350,7 @@ function addList(req, res, next) {
|
||||
|
||||
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be a string'));
|
||||
if (!Array.isArray(req.body.members)) return next(new HttpError(400, 'members must be a string'));
|
||||
if (req.body.members.length === 0) return next(new HttpError(400, 'list must have atleast one member'));
|
||||
|
||||
for (var i = 0; i < req.body.members.length; i++) {
|
||||
if (typeof req.body.members[i] !== 'string') return next(new HttpError(400, 'member must be a string'));
|
||||
@@ -370,6 +371,7 @@ function updateList(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.name, 'string');
|
||||
|
||||
if (!Array.isArray(req.body.members)) return next(new HttpError(400, 'members must be a string'));
|
||||
if (req.body.members.length === 0) return next(new HttpError(400, 'list must have atleast one member'));
|
||||
|
||||
for (var i = 0; i < req.body.members.length; i++) {
|
||||
if (typeof req.body.members[i] !== 'string') return next(new HttpError(400, 'member must be a string'));
|
||||
|
||||
@@ -24,7 +24,6 @@ var apps = require('../apps.js'),
|
||||
authcodedb = require('../authcodedb.js'),
|
||||
clients = require('../clients'),
|
||||
ClientsError = clients.ClientsError,
|
||||
config = require('../config.js'),
|
||||
constants = require('../constants.js'),
|
||||
DatabaseError = require('../databaseerror.js'),
|
||||
debug = require('debug')('box:routes/oauth2'),
|
||||
@@ -154,7 +153,7 @@ function renderTemplate(res, template, data) {
|
||||
|
||||
// amend template properties, for example used in the header
|
||||
data.title = data.title || 'Cloudron';
|
||||
data.adminOrigin = config.adminOrigin();
|
||||
data.adminOrigin = settings.adminOrigin();
|
||||
data.cloudronName = cloudronName;
|
||||
|
||||
res.render(template, data);
|
||||
@@ -214,8 +213,8 @@ function loginForm(req, res) {
|
||||
applicationName: applicationName,
|
||||
applicationLogo: applicationLogo,
|
||||
error: error,
|
||||
username: config.isDemo() ? constants.DEMO_USERNAME : '',
|
||||
password: config.isDemo() ? 'cloudron' : '',
|
||||
username: settings.isDemo() ? constants.DEMO_USERNAME : '',
|
||||
password: settings.isDemo() ? 'cloudron' : '',
|
||||
title: applicationName + ' Login'
|
||||
});
|
||||
}
|
||||
@@ -263,7 +262,7 @@ function login(req, res) {
|
||||
return res.redirect('/api/v1/session/login?' + failureQuery);
|
||||
}
|
||||
|
||||
let verified = speakeasy.totp.verify({ secret: req.user.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken });
|
||||
let verified = speakeasy.totp.verify({ secret: req.user.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken, window: 2 });
|
||||
if (!verified) {
|
||||
let failureQuery = querystring.stringify({ error: 'The 2FA token is invalid', returnTo: returnTo });
|
||||
return res.redirect('/api/v1/session/login?' + failureQuery);
|
||||
@@ -373,7 +372,7 @@ function accountSetup(req, res, next) {
|
||||
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
res.redirect(`${config.adminOrigin()}?accessToken=${result.accessToken}&expiresAt=${result.expires}`);
|
||||
res.redirect(`${settings.adminOrigin()}?accessToken=${result.accessToken}&expiresAt=${result.expires}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -421,7 +420,7 @@ function passwordReset(req, res, next) {
|
||||
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
res.redirect(`${config.adminOrigin()}?accessToken=${result.accessToken}&expiresAt=${result.expires}`);
|
||||
res.redirect(`${settings.adminOrigin()}?accessToken=${result.accessToken}&expiresAt=${result.expires}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,7 +27,8 @@ function get(req, res, next) {
|
||||
fallbackEmail: req.user.fallbackEmail,
|
||||
displayName: req.user.displayName,
|
||||
twoFactorAuthenticationEnabled: req.user.twoFactorAuthenticationEnabled,
|
||||
admin: req.user.admin
|
||||
admin: req.user.admin,
|
||||
source: req.user.source
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -85,7 +86,7 @@ function enableTwoFactorAuthentication(req, res, next) {
|
||||
|
||||
users.enableTwoFactorAuthentication(req.user.id, req.body.totpToken, function (error) {
|
||||
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
|
||||
if (error && error.reason === UsersError.BAD_TOKEN) return next(new HttpError(403, 'Invalid token'));
|
||||
if (error && error.reason === UsersError.BAD_TOKEN) return next(new HttpError(412, 'Invalid token'));
|
||||
if (error && error.reason === UsersError.ALREADY_EXISTS) return next(new HttpError(409, 'TwoFactor Authentication is already enabled'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
|
||||
@@ -10,18 +10,18 @@ exports = module.exports = {
|
||||
|
||||
var assert = require('assert'),
|
||||
auditSource = require('../auditsource'),
|
||||
config = require('../config.js'),
|
||||
debug = require('debug')('box:routes/setup'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
provision = require('../provision.js'),
|
||||
ProvisionError = require('../provision.js').ProvisionError,
|
||||
sysinfo = require('../sysinfo.js'),
|
||||
superagent = require('superagent');
|
||||
|
||||
function providerTokenAuth(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (config.provider() === 'ami') {
|
||||
if (sysinfo.provider() === 'ami') {
|
||||
if (typeof req.body.providerToken !== 'string' || !req.body.providerToken) return next(new HttpError(400, 'providerToken must be a non empty string'));
|
||||
|
||||
superagent.get('http://169.254.169.254/latest/meta-data/instance-id').timeout(30 * 1000).end(function (error, result) {
|
||||
@@ -53,19 +53,18 @@ function setup(req, res, next) {
|
||||
if ('tlsConfig' in dnsConfig && typeof dnsConfig.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be an object'));
|
||||
if (dnsConfig.tlsConfig && (!dnsConfig.tlsConfig.provider || typeof dnsConfig.tlsConfig.provider !== 'string')) return next(new HttpError(400, 'tlsConfig.provider must be a string'));
|
||||
|
||||
// TODO: validate subfields of these objects
|
||||
if (req.body.autoconf && typeof req.body.autoconf !== 'object') return next(new HttpError(400, 'autoconf must be an object'));
|
||||
if ('backupConfig' in req.body && typeof req.body.backupConfig !== 'object') return next(new HttpError(400, 'backupConfig must be an object'));
|
||||
|
||||
// it can take sometime to setup DNS, register cloudron
|
||||
req.clearTimeout();
|
||||
|
||||
provision.setup(dnsConfig, req.body.autoconf || {}, auditSource.fromRequest(req), function (error) {
|
||||
provision.setup(dnsConfig, req.body.backupConfig || null, auditSource.fromRequest(req), function (error) {
|
||||
if (error && error.reason === ProvisionError.ALREADY_SETUP) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === ProvisionError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === ProvisionError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200));
|
||||
next(new HttpSuccess(200, {}));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -108,17 +107,14 @@ function restore(req, res, next) {
|
||||
if (typeof req.body.backupId !== 'string') return next(new HttpError(400, 'backupId must be a string or null'));
|
||||
if (typeof req.body.version !== 'string') return next(new HttpError(400, 'version must be a string'));
|
||||
|
||||
// TODO: validate subfields of these objects
|
||||
if (req.body.autoconf && typeof req.body.autoconf !== 'object') return next(new HttpError(400, 'autoconf must be an object'));
|
||||
|
||||
provision.restore(backupConfig, req.body.backupId, req.body.version, req.body.autoconf || {}, auditSource.fromRequest(req), function (error) {
|
||||
provision.restore(backupConfig, req.body.backupId, req.body.version, auditSource.fromRequest(req), function (error) {
|
||||
if (error && error.reason === ProvisionError.ALREADY_SETUP) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === ProvisionError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === ProvisionError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === ProvisionError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200));
|
||||
next(new HttpSuccess(200, {}));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ var addons = require('../addons.js'),
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess;
|
||||
|
||||
function getAll(req, res, next) {
|
||||
req.clearTimeout(); // can take a while to get status of all services
|
||||
|
||||
addons.getServices(function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ var assert = require('assert'),
|
||||
backups = require('../backups.js'),
|
||||
custom = require('../custom.js'),
|
||||
docker = require('../docker.js'),
|
||||
DockerError = docker.DockerError,
|
||||
BoxError = require('../boxerror.js'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
safe = require('safetydance'),
|
||||
@@ -128,22 +128,22 @@ function getCloudronAvatar(req, res, next) {
|
||||
}
|
||||
|
||||
function getBackupConfig(req, res, next) {
|
||||
settings.getBackupConfig(function (error, config) {
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
// used by the UI to figure if backups are disabled
|
||||
if (!custom.features().configureBackup) {
|
||||
return next(new HttpSuccess(200, { provider: config.provider }));
|
||||
// always send provider as it is used by the UI to figure if backups are disabled ('noop' backend)
|
||||
if (!custom.spec().backups.configurable) {
|
||||
return next(new HttpSuccess(200, { provider: backupConfig.provider }));
|
||||
}
|
||||
|
||||
next(new HttpSuccess(200, backups.removePrivateFields(config)));
|
||||
next(new HttpSuccess(200, backups.removePrivateFields(backupConfig)));
|
||||
});
|
||||
}
|
||||
|
||||
function setBackupConfig(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (!custom.features().configureBackup) return next(new HttpError(405, 'feature disabled by admin'));
|
||||
if (!custom.spec().backups.configurable) return next(new HttpError(405, 'feature disabled by admin'));
|
||||
|
||||
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
|
||||
if (typeof req.body.retentionSecs !== 'number') return next(new HttpError(400, 'retentionSecs is required'));
|
||||
@@ -196,6 +196,33 @@ function setPlatformConfig(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function getExternalLdapConfig(req, res, next) {
|
||||
settings.getExternalLdapConfig(function (error, config) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, config));
|
||||
});
|
||||
}
|
||||
|
||||
function setExternalLdapConfig(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled must be a boolean'));
|
||||
if (typeof req.body.url !== 'string' || req.body.url === '') return next(new HttpError(400, 'url must be a non empty string'));
|
||||
if (typeof req.body.baseDn !== 'string' || req.body.baseDn === '') return next(new HttpError(400, 'baseDn must be a non empty string'));
|
||||
if (typeof req.body.filter !== 'string' || req.body.filter === '') return next(new HttpError(400, 'filter must be a non empty string'));
|
||||
if ('bindDn' in req.body && (typeof req.body.bindDn !== 'string' || req.body.bindDn === '')) return next(new HttpError(400, 'bindDn must be a non empty string'));
|
||||
if ('bindPassword' in req.body && typeof req.body.bindPassword !== 'string') return next(new HttpError(400, 'bindPassword must be a string'));
|
||||
|
||||
settings.setExternalLdapConfig(req.body, function (error) {
|
||||
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === SettingsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function getDynamicDnsConfig(req, res, next) {
|
||||
settings.getDynamicDnsConfig(function (error, enabled) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
@@ -207,7 +234,7 @@ function getDynamicDnsConfig(req, res, next) {
|
||||
function setDynamicDnsConfig(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (!custom.features().dynamicDns) return next(new HttpError(405, 'feature disabled by admin'));
|
||||
if (!custom.spec().domains.dynamicDns) return next(new HttpError(405, 'feature disabled by admin'));
|
||||
|
||||
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled boolean is required'));
|
||||
|
||||
@@ -248,7 +275,7 @@ function setRegistryConfig(req, res, next) {
|
||||
if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password is required'));
|
||||
|
||||
docker.setRegistryConfig(req.body, function (error) {
|
||||
if (error && error.reason === DockerError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === BoxError.ACCESS_DENIED) return next(new HttpError(424, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200));
|
||||
@@ -262,6 +289,7 @@ function get(req, res, next) {
|
||||
case settings.DYNAMIC_DNS_KEY: return getDynamicDnsConfig(req, res, next);
|
||||
case settings.BACKUP_CONFIG_KEY: return getBackupConfig(req, res, next);
|
||||
case settings.PLATFORM_CONFIG_KEY: return getPlatformConfig(req, res, next);
|
||||
case settings.EXTERNAL_LDAP_KEY: return getExternalLdapConfig(req, res, next);
|
||||
case settings.UNSTABLE_APPS_KEY: return getUnstableAppsConfig(req, res, next);
|
||||
|
||||
case settings.APP_AUTOUPDATE_PATTERN_KEY: return getAppAutoupdatePattern(req, res, next);
|
||||
@@ -282,6 +310,7 @@ function set(req, res, next) {
|
||||
case settings.DYNAMIC_DNS_KEY: return setDynamicDnsConfig(req, res, next);
|
||||
case settings.BACKUP_CONFIG_KEY: return setBackupConfig(req, res, next);
|
||||
case settings.PLATFORM_CONFIG_KEY: return setPlatformConfig(req, res, next);
|
||||
case settings.EXTERNAL_LDAP_KEY: return setExternalLdapConfig(req, res, next);
|
||||
case settings.UNSTABLE_APPS_KEY: return setUnstableAppsConfig(req, res, next);
|
||||
|
||||
case settings.APP_AUTOUPDATE_PATTERN_KEY: return setAppAutoupdatePattern(req, res, next);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user