Compare commits
391 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0c5561aac | ||
|
|
23bfc1a3b8 | ||
|
|
73a44d1fb2 | ||
|
|
a1970f3b65 | ||
|
|
c69f4e4a48 | ||
|
|
417a8de823 | ||
|
|
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 | ||
|
|
7177f82041 | ||
|
|
ecc9415679 | ||
|
|
23280f1635 | ||
|
|
af062631c5 | ||
|
|
99b866736c | ||
|
|
9e74b666fb | ||
|
|
1cc54f9a71 | ||
|
|
a2d8ef7c63 | ||
|
|
0835a6d690 | ||
|
|
67e0a19e13 | ||
|
|
6fa13f1bcc | ||
|
|
25604774b9 | ||
|
|
b0fb989be0 | ||
|
|
61c70b9922 | ||
|
|
8b5bdf4e88 | ||
|
|
84d8d4a745 | ||
|
|
04f93fb90f | ||
|
|
d559af15b8 | ||
|
|
1510e13066 | ||
|
|
d7cb8842d3 | ||
|
|
7596411d70 | ||
|
|
876cef5970 | ||
|
|
1ebc077721 | ||
|
|
f5acdc0c4b | ||
|
|
866321da4d | ||
|
|
58696c5132 | ||
|
|
3d11dfaae3 | ||
|
|
3cc320b1d0 | ||
|
|
2efeab3493 | ||
|
|
a78cab36ab | ||
|
|
41e525e037 | ||
|
|
33d5cb3061 | ||
|
|
0cf523c285 | ||
|
|
9e346e0780 | ||
|
|
9cd25e548f | ||
|
|
d221e40602 | ||
|
|
a6ce5ce796 | ||
|
|
23bfb7b8e6 | ||
|
|
b536c8196b | ||
|
|
8f677177f2 | ||
|
|
bb7cac4855 | ||
|
|
e62fa01ab3 | ||
|
|
5be48c5486 | ||
|
|
6265efd5e7 | ||
|
|
222d2840cf | ||
|
|
0e8fe8cd07 | ||
|
|
f96a8bc269 | ||
|
|
bbced03f3a | ||
|
|
ffe0e6dbbe | ||
|
|
7bb6016f7b | ||
|
|
d5f58eea34 | ||
|
|
204c659af2 | ||
|
|
ae5eb00bd0 | ||
|
|
61c059babe | ||
|
|
a4e4d30d5d | ||
|
|
c4fbfd32ee | ||
|
|
4f5af1f7be | ||
|
|
22efb96f66 | ||
|
|
b7d3014aab | ||
|
|
fc7e72835e | ||
|
|
d824ced70f | ||
|
|
2b9147bbc0 | ||
|
|
1ea9a0af50 | ||
|
|
7825cf06c0 | ||
|
|
20a2f56184 | ||
|
|
22be3ad948 | ||
|
|
dfa5a0f5fe | ||
|
|
b7d046533c | ||
|
|
c74cf59e3b | ||
|
|
92941260a5 | ||
|
|
74fdcb0781 | ||
|
|
6ab38633c2 | ||
|
|
e553ade936 | ||
|
|
b4b3fd9ab6 | ||
|
|
4fc6f5a094 | ||
|
|
0f521e2521 | ||
|
|
088b016083 | ||
|
|
ccb2fcf39b | ||
|
|
f6e82e93e4 | ||
|
|
02126caacd | ||
|
|
8f55565dbf | ||
|
|
3246edd5a8 | ||
|
|
7e7e067b5b | ||
|
|
fd0bdbce7e | ||
|
|
5ba8a8f7da | ||
|
|
a799b0931f | ||
|
|
d895786f55 | ||
|
|
4d89b4a8df | ||
|
|
e544e77fd9 | ||
|
|
7a5a94614e | ||
|
|
c935811e08 | ||
|
|
cd6d97c3eb | ||
|
|
292f0624fb | ||
|
|
9fb0d1f3bf | ||
|
|
d82b722887 | ||
|
|
3bcab14de1 | ||
|
|
3ded411c15 | ||
|
|
706c970314 | ||
|
|
35dfd540b5 | ||
|
|
7c4394d8cb | ||
|
|
69972e6460 | ||
|
|
dd19f11e29 | ||
|
|
b312414957 | ||
|
|
f3910f03ca | ||
|
|
080c667d9c | ||
|
|
2bf0614da1 | ||
|
|
bd5bdfa8b5 | ||
|
|
d1582a435d | ||
|
|
d27aa8821a | ||
|
|
f5f08cf9ff | ||
|
|
91877f7b2d | ||
|
|
7f9b078430 | ||
|
|
e9a01fa0c8 | ||
|
|
1f33674f30 | ||
|
|
badf58db3e | ||
|
|
58c98be9a7 | ||
|
|
71a5555816 | ||
|
|
aee20c0423 | ||
|
|
95faccd8d9 | ||
|
|
85ec4a3de7 | ||
|
|
e79b6ade51 | ||
|
|
79ccfa087d | ||
|
|
9a349eff32 | ||
|
|
e756a442f6 | ||
|
|
332c860e80 | ||
|
|
d934671202 | ||
|
|
747359478d | ||
|
|
f0f18db5a8 | ||
|
|
2f068c66d4 | ||
|
|
fa5415b1eb | ||
|
|
e23fd5e3c5 | ||
|
|
996c838320 | ||
|
|
083b8a6d7b | ||
|
|
58625c82a0 | ||
|
|
74211dd2c9 | ||
|
|
cd4808e5e2 | ||
|
|
e44ad19bda | ||
|
|
e31d413551 | ||
|
|
d170a3e5e1 | ||
|
|
10c3872902 | ||
|
|
a3c658d6b8 | ||
|
|
0c8cf9cc0d | ||
|
|
b92a79eb9d | ||
|
|
ba2c6c1219 | ||
|
|
f0a5cc6116 | ||
|
|
9fbe209c04 | ||
|
|
b8bbd73e07 | ||
|
|
2e40325f2a | ||
|
|
bdbd336db5 | ||
|
|
5c645ea224 | ||
|
|
5c2985cac7 | ||
|
|
a0d1016c01 | ||
|
|
816cf44117 | ||
|
|
1119a62576 | ||
|
|
152cb48340 | ||
|
|
5a4c2a4974 | ||
|
|
9063e8f4a4 | ||
|
|
519552083b | ||
|
|
f6ad66bd30 | ||
|
|
65affb5146 | ||
|
|
38d9378e48 | ||
|
|
cd8011e858 | ||
|
|
e352562c51 | ||
|
|
3dd2a19915 | ||
|
|
384286cf0a | ||
|
|
162281e753 | ||
|
|
9a3603fea3 | ||
|
|
121dc85e40 | ||
|
|
823bad2ace | ||
|
|
c9ed8543c8 | ||
|
|
d752c68790 | ||
|
|
39c0409d42 | ||
|
|
7937738258 | ||
|
|
ea092bda99 | ||
|
|
3b908a5857 | ||
|
|
536e14dfe8 | ||
|
|
edd2c5f779 | ||
|
|
ee76c2c06e | ||
|
|
0190a92c26 | ||
|
|
730da103b1 | ||
|
|
ffe30289ee | ||
|
|
cfe9af5c3c | ||
|
|
9647ce153d | ||
|
|
bec63c1ad0 | ||
|
|
3dec0ff85b | ||
|
|
8868d8e99e | ||
|
|
9a0d9506f8 | ||
|
|
ec57b813da | ||
|
|
57c4d96467 | ||
|
|
ebaf0a08b5 | ||
|
|
abae4b5106 | ||
|
|
81b721be2b | ||
|
|
e9108ae3f8 | ||
|
|
acc7b65649 | ||
|
|
77ed177855 | ||
|
|
71cb5c579d | ||
|
|
f22e4c261a | ||
|
|
4a046ca70e | ||
|
|
62ee3fa0f1 | ||
|
|
ef0403af0e | ||
|
|
044b27967e | ||
|
|
4942a2480b | ||
|
|
cde852f0f9 | ||
|
|
a435e88b25 | ||
|
|
add954db80 | ||
|
|
76ec639fea | ||
|
|
155decfca1 | ||
|
|
3ab131786b | ||
|
|
a273dea755 | ||
|
|
9a6d8e3a33 | ||
|
|
4d9ecef6fd | ||
|
|
4df72a4015 | ||
|
|
c3e2dc4ce8 | ||
|
|
5a1bcd53a3 | ||
|
|
4c8865ed5b | ||
|
|
cb6bd50db0 | ||
|
|
614dbb3674 | ||
|
|
8ed8bb2dad | ||
|
|
a1f6985534 | ||
|
|
6bdf2e38cc | ||
|
|
19ae55ebd4 | ||
|
|
61cef46a95 | ||
|
|
172738f7b7 | ||
|
|
5d0d60d89e | ||
|
|
3cfdc64735 | ||
|
|
b47069ccad | ||
|
|
2051b3921b | ||
|
|
46f46483f8 | ||
|
|
66b02b58b6 | ||
|
|
4428c3d7d8 | ||
|
|
2d4b9786fa | ||
|
|
d2d9c4be6f | ||
|
|
a9d6ac29f1 | ||
|
|
4d50bd5c78 | ||
|
|
fdd651b9cc | ||
|
|
7b56f102cc | ||
|
|
e329360daa | ||
|
|
5e8a431a92 | ||
|
|
cd3f21a92e | ||
|
|
03d3ae3eb4 | ||
|
|
0c350dcf6e | ||
|
|
c6b3d15d72 | ||
|
|
8d7f7cb438 | ||
|
|
b5a4121574 | ||
|
|
916ca87db4 | ||
|
|
bfea97f14e | ||
|
|
f98657aca8 | ||
|
|
45c5e770fa | ||
|
|
f4ce7ecf4b | ||
|
|
8dfe1fe97f | ||
|
|
4bf165efaf | ||
|
|
c7f6ae5be9 | ||
|
|
d83d2d5f4e | ||
|
|
2362b2a5a0 | ||
|
|
fb08a17ec8 | ||
|
|
1bcc2d544a | ||
|
|
6fd1205681 | ||
|
|
da2b00c9cf | ||
|
|
f6213595d1 | ||
|
|
b1b2bd5b97 | ||
|
|
aa19cbbfc7 | ||
|
|
8d39faddc9 | ||
|
|
52714dbcc9 | ||
|
|
be92d3a0bc | ||
|
|
f3189f72fd | ||
|
|
144c1d4e2f | ||
|
|
e5964f9d93 | ||
|
|
ea30cbe117 | ||
|
|
598a9664a7 | ||
|
|
d04628a42d | ||
|
|
7bce63d74e | ||
|
|
452fe9f76d | ||
|
|
7983ff5db2 | ||
|
|
c361ab954d | ||
|
|
a8735a6465 | ||
|
|
76255c0dd4 | ||
|
|
87655ff3cd | ||
|
|
fc7be2ac1a | ||
|
|
e93b95bee8 | ||
|
|
6a18d6918e | ||
|
|
578ce09b5e | ||
|
|
27f6177fc9 | ||
|
|
20c0deeac4 | ||
|
|
f1f8cdb6e9 | ||
|
|
345e4e846c | ||
|
|
6f57b36158 | ||
|
|
0264e10e69 | ||
|
|
067f5bf5a3 | ||
|
|
c81b643cdf | ||
|
|
388ad077d6 | ||
|
|
db93cdd95f | ||
|
|
68304a3fc1 | ||
|
|
13259c114a | ||
|
|
5131ba453d | ||
|
|
8fdc9939cd | ||
|
|
c15449492a | ||
|
|
1cab1e06d9 | ||
|
|
4831926869 | ||
|
|
4fcf25077b | ||
|
|
c32461f322 | ||
|
|
0abe6fc0b4 | ||
|
|
edc3d53f94 | ||
|
|
bb5fbbe746 | ||
|
|
36f3e3fe50 | ||
|
|
65c8000f66 | ||
|
|
2d45f8bc40 | ||
|
|
7a0d4ad508 | ||
|
|
5ae93bb569 | ||
|
|
aa6ca46792 |
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"env": {
|
||||
"es6": true,
|
||||
"node": true
|
||||
"node": true,
|
||||
"es6": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2017
|
||||
"ecmaVersion": 8
|
||||
},
|
||||
"rules": {
|
||||
"indent": [
|
||||
@@ -26,4 +26,4 @@
|
||||
],
|
||||
"no-console": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
94
CHANGES
94
CHANGES
@@ -1537,3 +1537,97 @@
|
||||
* Hide mail relay password
|
||||
* Do not return API tokens in REST API
|
||||
|
||||
[3.5.3]
|
||||
* Make reboot required check server side
|
||||
* Update node to 10.15.1
|
||||
* Enable gzip compression for large objects
|
||||
* Update docker to 18.09
|
||||
* Add a way to lock specific settings
|
||||
* Add UI to copy app's backup id
|
||||
* Block platform updates based on app manifest constraints
|
||||
* Make crash logs viewable via the dashboard
|
||||
* Fix issue where uploading of filenames with brackets and plus was not working
|
||||
* Add notification for cert renewal and backup failures
|
||||
* Fix issue where mail container was not updated with the latest certificate
|
||||
|
||||
[3.5.4]
|
||||
* Make reboot required check server side
|
||||
* Update node to 10.15.1
|
||||
* Enable gzip compression for large objects
|
||||
* Update docker to 18.09
|
||||
* Add a way to lock specific settings
|
||||
* Add UI to copy app's backup id
|
||||
* Block platform updates based on app manifest constraints
|
||||
* Make crash logs viewable via the dashboard
|
||||
* Fix issue where uploading of filenames with brackets and plus was not working
|
||||
* Add notification for cert renewal and backup failures
|
||||
* Fix issue where mail container was not updated with the latest certificate
|
||||
|
||||
[4.0.0]
|
||||
* (mail) Bump mail_max_userip_connections to 50
|
||||
* Fix issue where DKIM was not setup correctly during a restore
|
||||
* (mysql) Remove any stale lock file on restart
|
||||
* Add a way to disable outbound mail for a domain
|
||||
* Cleanup task logs
|
||||
* Fix issue where dashboard location might conflict with existing app location
|
||||
* Ad graphite to services
|
||||
* Add labels and tags to apps
|
||||
* Ensure MySQL is storing data/time in UTC
|
||||
* Fix bug where the UI redirects to login screen when enabling 2FA with invalid token
|
||||
* Use unbound resolver when resolving NS record of a domain
|
||||
* Various fixes for notifications
|
||||
* Add FTP support for apps
|
||||
* Add app version as part of info dialog
|
||||
* (backup) Do not abort archive if file(s) disappear
|
||||
* Show app upstream version in the info dialog
|
||||
* Add Scaleway ObjectStorage backup backend
|
||||
* Preserve update backups for 3 weeks
|
||||
* Make send test mail functionality work with secondary domain
|
||||
* Add support for an external email relay that does not require authentication
|
||||
* Add option to accept self-signed certs when using external mail relay
|
||||
* Allow publishing and listing community supported apps
|
||||
* Remove spaces support
|
||||
* Features implementation for customization
|
||||
|
||||
[4.0.1]
|
||||
* 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
|
||||
|
||||
|
||||
694
LICENSE
694
LICENSE
@@ -1,661 +1,35 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
The Cloudron Subscription license
|
||||
Copyright (c) 2019 Cloudron UG
|
||||
|
||||
With regard to the Cloudron Software:
|
||||
|
||||
This software and associated documentation files (the "Software") may only be
|
||||
used in production, if you (and any entity that you represent) have agreed to,
|
||||
and are in compliance with, the Cloudron Subscription Terms of Service, available
|
||||
at https://cloudron.io/legal/terms.html (the “Subscription Terms”), or other
|
||||
agreement governing the use of the Software, as agreed by you and Cloudron,
|
||||
and otherwise have a valid Cloudron Subscription. Subject to the foregoing sentence,
|
||||
you are free to modify this Software and publish patches to the Software. You agree
|
||||
that Subscription and/or its licensors (as applicable) retain all right, title and
|
||||
interest in and to all such modifications and/or patches, and all such modifications
|
||||
and/or patches may only be used, copied, modified, displayed, distributed, or otherwise
|
||||
exploited with a valid Cloudron subscription. Notwithstanding the foregoing, you may copy
|
||||
and modify the Software for development and testing purposes, without requiring a
|
||||
subscription. You agree that Cloudron and/or its licensors (as applicable) retain
|
||||
all right, title and interest in and to all such modifications. You are not
|
||||
granted any other rights beyond what is expressly stated herein. Subject to the
|
||||
foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
|
||||
and/or sell the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
For all third party components incorporated into the Cloudron Software, those
|
||||
components are licensed under the original license provided by the owner of the
|
||||
applicable component.
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
box
|
||||
Copyright (C) 2016-2019 Cloudron UG
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
10
README.md
10
README.md
@@ -37,16 +37,11 @@ anyone to effortlessly host web applications on their server on their own terms.
|
||||
|
||||
## Demo
|
||||
|
||||
Try our demo at https://my-demo.cloudron.me (username: cloudron password: cloudron).
|
||||
Try our demo at https://my.demo.cloudron.io (username: cloudron password: cloudron).
|
||||
|
||||
## 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
|
||||
@@ -64,6 +59,7 @@ the containers in the Cloudron.
|
||||
|
||||
## Community
|
||||
|
||||
* [Chat](https://chat.cloudron.io)
|
||||
* [Forum](https://forum.cloudron.io/)
|
||||
* [Support](mailto:support@cloudron.io)
|
||||
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
assertNotEmpty() {
|
||||
: "${!1:? "$1 is not set."}"
|
||||
}
|
||||
|
||||
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)"
|
||||
export JSON="${SOURCE_DIR}/node_modules/.bin/json"
|
||||
|
||||
revision=$(git rev-parse HEAD)
|
||||
box_name=""
|
||||
server_id=""
|
||||
server_ip=""
|
||||
destroy_server="yes"
|
||||
deploy_env="dev"
|
||||
|
||||
# Only GNU getopt supports long options. OS X comes bundled with the BSD getopt
|
||||
# brew install gnu-getopt to get the GNU getopt on OS X
|
||||
[[ $(uname -s) == "Darwin" ]] && GNU_GETOPT="/usr/local/opt/gnu-getopt/bin/getopt" || GNU_GETOPT="getopt"
|
||||
readonly GNU_GETOPT
|
||||
|
||||
args=$(${GNU_GETOPT} -o "" -l "revision:,regions:,size:,name:,no-destroy,env:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--env) deploy_env="$2"; shift 2;;
|
||||
--revision) revision="$2"; shift 2;;
|
||||
--name) box_name="$2"; destroy_server="no"; shift 2;;
|
||||
--no-destroy) destroy_server="no"; shift 2;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "Creating digitalocean image"
|
||||
if [[ "${deploy_env}" == "staging" ]]; then
|
||||
assertNotEmpty DIGITAL_OCEAN_TOKEN_STAGING
|
||||
export DIGITAL_OCEAN_TOKEN="${DIGITAL_OCEAN_TOKEN_STAGING}"
|
||||
elif [[ "${deploy_env}" == "dev" ]]; then
|
||||
assertNotEmpty DIGITAL_OCEAN_TOKEN_DEV
|
||||
export DIGITAL_OCEAN_TOKEN="${DIGITAL_OCEAN_TOKEN_DEV}"
|
||||
elif [[ "${deploy_env}" == "prod" ]]; then
|
||||
assertNotEmpty DIGITAL_OCEAN_TOKEN_PROD
|
||||
export DIGITAL_OCEAN_TOKEN="${DIGITAL_OCEAN_TOKEN_PROD}"
|
||||
else
|
||||
echo "No such env ${deploy_env}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
vps="/bin/bash ${SCRIPT_DIR}/digitalocean.sh"
|
||||
|
||||
readonly ssh_keys="${HOME}/.ssh/id_rsa_caas_${deploy_env}"
|
||||
readonly scp202="scp -P 202 -o ConnectTimeout=10 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
|
||||
readonly scp22="scp -o ConnectTimeout=10 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
|
||||
|
||||
readonly ssh202="ssh -p 202 -o IdentitiesOnly=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
|
||||
readonly ssh22="ssh -o IdentitiesOnly=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
|
||||
|
||||
if [[ ! -f "${ssh_keys}" ]]; then
|
||||
echo "caas ssh key is missing at ${ssh_keys} (pick it up from secrets repo)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
function get_pretty_revision() {
|
||||
local git_rev="$1"
|
||||
local sha1=$(git rev-parse --short "${git_rev}" 2>/dev/null)
|
||||
|
||||
echo "${sha1}"
|
||||
}
|
||||
|
||||
now=$(date "+%Y-%m-%d-%H%M%S")
|
||||
pretty_revision=$(get_pretty_revision "${revision}")
|
||||
|
||||
if [[ -z "${box_name}" ]]; then
|
||||
# if you change this, change the regexp is appstore/janitor.js
|
||||
box_name="box-${deploy_env}-${pretty_revision}-${now}" # remove slashes
|
||||
|
||||
# create a new server if no name given
|
||||
if ! caas_ssh_key_id=$($vps get_ssh_key_id "caas"); then
|
||||
echo "Could not query caas ssh key"
|
||||
exit 1
|
||||
fi
|
||||
echo "Detected caas ssh key id: ${caas_ssh_key_id}"
|
||||
|
||||
echo "Creating Server with name [${box_name}]"
|
||||
if ! server_id=$($vps create ${caas_ssh_key_id} ${box_name}); then
|
||||
echo "Failed to create server"
|
||||
exit 1
|
||||
fi
|
||||
echo "Created server with id: ${server_id}"
|
||||
|
||||
# If we run scripts overenthusiastically without the wait, setup script randomly fails
|
||||
echo -n "Waiting 120 seconds for server creation"
|
||||
for i in $(seq 1 24); do
|
||||
echo -n "."
|
||||
sleep 5
|
||||
done
|
||||
echo ""
|
||||
else
|
||||
if ! server_id=$($vps get_id "${box_name}"); then
|
||||
echo "Could not determine id from name"
|
||||
exit 1
|
||||
fi
|
||||
echo "Reusing server with id: ${server_id}"
|
||||
|
||||
$vps power_on "${server_id}"
|
||||
fi
|
||||
|
||||
# Query until we get an IP
|
||||
while true; do
|
||||
echo "Trying to get the server IP"
|
||||
if server_ip=$($vps get_ip "${server_id}"); then
|
||||
echo "Server IP : [${server_ip}]"
|
||||
break
|
||||
fi
|
||||
echo "Timedout, trying again in 10 seconds"
|
||||
sleep 10
|
||||
done
|
||||
|
||||
while true; do
|
||||
echo "Trying to copy init script to server"
|
||||
if $scp22 "${SCRIPT_DIR}/initializeBaseUbuntuImage.sh" root@${server_ip}:.; then
|
||||
break
|
||||
fi
|
||||
echo "Timedout, trying again in 30 seconds"
|
||||
sleep 30
|
||||
done
|
||||
|
||||
echo "Copying infra_version.js"
|
||||
$scp22 "${SCRIPT_DIR}/../src/infra_version.js" root@${server_ip}:.
|
||||
|
||||
echo "Copying box source"
|
||||
cd "${SOURCE_DIR}"
|
||||
git archive --format=tar HEAD | $ssh22 "root@${server_ip}" "cat - > /tmp/box.tar.gz"
|
||||
|
||||
echo "Executing init script"
|
||||
if ! $ssh22 "root@${server_ip}" "/bin/bash /root/initializeBaseUbuntuImage.sh caas"; then
|
||||
echo "Init script failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Shutting down server with id : ${server_id}"
|
||||
$ssh22 "root@${server_ip}" "shutdown -f now" || true # shutdown sometimes terminates ssh connection immediately making this command fail
|
||||
|
||||
# wait 10 secs for actual shutdown
|
||||
echo "Waiting for 10 seconds for server to shutdown"
|
||||
sleep 30
|
||||
|
||||
echo "Powering off server"
|
||||
if ! $vps power_off "${server_id}"; then
|
||||
echo "Could not power off server"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
snapshot_name="box-${deploy_env}-${pretty_revision}-${now}"
|
||||
echo "Snapshotting as ${snapshot_name}"
|
||||
if ! image_id=$($vps snapshot "${server_id}" "${snapshot_name}"); then
|
||||
echo "Could not snapshot and get image id"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${destroy_server}" == "yes" ]]; then
|
||||
echo "Destroying server"
|
||||
if ! $vps destroy "${server_id}"; then
|
||||
echo "Could not destroy server"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Skipping server destroy"
|
||||
fi
|
||||
|
||||
echo "Transferring image ${image_id} to other regions"
|
||||
$vps transfer_image_to_all_regions "${image_id}"
|
||||
|
||||
echo "Done."
|
||||
27
baseimage/initializeBaseUbuntuImage.sh
Normal file → Executable file
27
baseimage/initializeBaseUbuntuImage.sh
Normal file → Executable file
@@ -28,12 +28,14 @@ debconf-set-selections <<< 'mysql-server mysql-server/root_password_again passwo
|
||||
# this enables automatic security upgrades (https://help.ubuntu.com/community/AutomaticSecurityUpdates)
|
||||
# resolvconf is needed for unbound to work property after disabling systemd-resolved in 18.04
|
||||
ubuntu_version=$(lsb_release -rs)
|
||||
ubuntu_codename=$(lsb_release -cs)
|
||||
gpg_package=$([[ "${ubuntu_version}" == "16.04" ]] && echo "gnupg" || echo "gpg")
|
||||
apt-get -y install \
|
||||
acl \
|
||||
build-essential \
|
||||
cron \
|
||||
curl \
|
||||
debconf-utils \
|
||||
dmsetup \
|
||||
$gpg_package \
|
||||
iptables \
|
||||
@@ -44,35 +46,41 @@ apt-get -y install \
|
||||
openssh-server \
|
||||
pwgen \
|
||||
resolvconf \
|
||||
sudo \
|
||||
swaks \
|
||||
tzdata \
|
||||
unattended-upgrades \
|
||||
unbound \
|
||||
xfsprogs
|
||||
|
||||
# on some providers like scaleway the sudo file is changed and we want to keep the old one
|
||||
apt-get -o Dpkg::Options::="--force-confold" install -y sudo
|
||||
|
||||
# this ensures that unattended upgades are enabled, if it was disabled during ubuntu install time (see #346)
|
||||
# debconf-set-selection of unattended-upgrades/enable_auto_updates + dpkg-reconfigure does not work
|
||||
cp /usr/share/unattended-upgrades/20auto-upgrades /etc/apt/apt.conf.d/20auto-upgrades
|
||||
|
||||
echo "==> Installing node.js"
|
||||
mkdir -p /usr/local/node-8.9.3
|
||||
curl -sL https://nodejs.org/dist/v8.9.3/node-v8.9.3-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-8.9.3
|
||||
ln -sf /usr/local/node-8.9.3/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-8.9.3/bin/npm /usr/bin/npm
|
||||
mkdir -p /usr/local/node-10.15.1
|
||||
curl -sL https://nodejs.org/dist/v10.15.1/node-v10.15.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-10.15.1
|
||||
ln -sf /usr/local/node-10.15.1/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-10.15.1/bin/npm /usr/bin/npm
|
||||
apt-get install -y python # Install python which is required for npm rebuild
|
||||
[[ "$(python --version 2>&1)" == "Python 2.7."* ]] || die "Expecting python version to be 2.7.x"
|
||||
|
||||
# https://docs.docker.com/engine/installation/linux/ubuntulinux/
|
||||
echo "==> Installing Docker"
|
||||
|
||||
# create systemd drop-in file
|
||||
# create systemd drop-in file. if you channge options here, be sure to fixup installer.sh as well
|
||||
mkdir -p /etc/systemd/system/docker.service.d
|
||||
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=overlay2" > /etc/systemd/system/docker.service.d/cloudron.conf
|
||||
|
||||
curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_18.03.1~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
|
||||
# there are 3 packages for docker - containerd, CLI and the daemon
|
||||
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_1.2.2-3_amd64.deb" -o /tmp/containerd.deb
|
||||
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce-cli_18.09.2~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker-ce-cli.deb
|
||||
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce_18.09.2~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker.deb
|
||||
# apt install with install deps (as opposed to dpkg -i)
|
||||
apt install -y /tmp/docker.deb
|
||||
rm /tmp/docker.deb
|
||||
apt install -y /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb
|
||||
rm /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb
|
||||
|
||||
storage_driver=$(docker info | grep "Storage Driver" | sed 's/.*: //')
|
||||
if [[ "${storage_driver}" != "overlay2" ]]; then
|
||||
@@ -110,6 +118,7 @@ fi
|
||||
echo "==> Configuring host"
|
||||
sed -e 's/^#NTP=/NTP=0.ubuntu.pool.ntp.org 1.ubuntu.pool.ntp.org 2.ubuntu.pool.ntp.org 3.ubuntu.pool.ntp.org/' -i /etc/systemd/timesyncd.conf
|
||||
timedatectl set-ntp 1
|
||||
# mysql follows the system timezone
|
||||
timedatectl set-timezone UTC
|
||||
|
||||
# Disable bind for good measure (on online.net, kimsufi servers these are pre-installed and conflicts with unbound)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
var database = require('./src/database.js');
|
||||
|
||||
var sendFailureLogs = require('./src/logcollector').sendFailureLogs;
|
||||
var crashNotifier = require('./src/crashnotifier.js');
|
||||
|
||||
// This is triggered by systemd with the crashed unit name as argument
|
||||
function main() {
|
||||
@@ -17,7 +17,11 @@ function main() {
|
||||
database.initialize(function (error) {
|
||||
if (error) return console.error('Cannot connect to database. Unable to send crash log.', error);
|
||||
|
||||
sendFailureLogs(unitName);
|
||||
crashNotifier.sendFailureLogs(unitName, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
process.exit();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
17
migrations/20190221215805-settings-add-locked.js
Normal file
17
migrations/20190221215805-settings-add-locked.js
Normal file
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE settings ADD COLUMN locked BOOLEAN DEFAULT 0', function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE settings DROP COLUMN locked', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
14
migrations/20190228225735-notifications-drop-action.js
Normal file
14
migrations/20190228225735-notifications-drop-action.js
Normal file
@@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE notifications DROP COLUMN action', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE notifications ADD COLUMN action VARCHAR(512) NOT NULL', callback);
|
||||
};
|
||||
27
migrations/20190303024631-apps-default-mailboxName.js
Normal file
27
migrations/20190303024631-apps-default-mailboxName.js
Normal file
@@ -0,0 +1,27 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async'),
|
||||
crypto = require('crypto'),
|
||||
fs = require('fs'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
safe = require('safetydance'),
|
||||
tldjs = require('tldjs');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT * FROM apps, subdomains WHERE apps.id=subdomains.appId AND type="primary"', function (error, apps) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(apps, function (app, iteratorDone) {
|
||||
if (app.mailboxName) return iteratorDone();
|
||||
|
||||
const mailboxName = (app.subdomain ? app.subdomain : JSON.parse(app.manifestJson).title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
|
||||
|
||||
db.runSql('UPDATE apps SET mailboxName=? WHERE id=?', [ mailboxName, app.id ], iteratorDone);
|
||||
}, callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
15
migrations/20190322144543-apps-add-label.js
Normal file
15
migrations/20190322144543-apps-add-label.js
Normal file
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN label VARCHAR(128)', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN label', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
15
migrations/20190322144544-apps-add-tagsJson.js
Normal file
15
migrations/20190322144544-apps-add-tagsJson.js
Normal file
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN tagsJson VARCHAR(2048)', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN tagsJson ', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
15
migrations/20190413140945-backups-add-preserveSecs.js
Normal file
15
migrations/20190413140945-backups-add-preserveSecs.js
Normal file
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE backups ADD COLUMN preserveSecs INTEGER DEFAULT 0', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE backups DROP COLUMN preserveSecs', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
19
migrations/20190426172858-migrate-caas-backupConfig.js
Normal file
19
migrations/20190426172858-migrate-caas-backupConfig.js
Normal file
@@ -0,0 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
|
||||
if (error || results.length === 0) return callback(error);
|
||||
|
||||
var backupConfig = JSON.parse(results[0].value);
|
||||
if (backupConfig.provider !== 'caas') return callback();
|
||||
|
||||
backupConfig.boxId = backupConfig.prefix; // hack to set the boxId that happens to match the prefix
|
||||
delete backupConfig.fqdn;
|
||||
|
||||
db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [ JSON.stringify(backupConfig) ], callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
51
migrations/20190503170326-settings-get-license.js
Normal file
51
migrations/20190503170326-settings-get-license.js
Normal file
@@ -0,0 +1,51 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async'),
|
||||
fs = require('fs'),
|
||||
superagent = require('superagent');
|
||||
|
||||
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'));
|
||||
|
||||
db.all('SELECT * FROM settings WHERE name="appstore_config"', function (error, results) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (results.length === 0) {
|
||||
console.log('No appstore config, skipping license migration');
|
||||
return callback();
|
||||
}
|
||||
|
||||
console.log('Downloading license');
|
||||
|
||||
const appstoreConfig = JSON.parse(results[0].value);
|
||||
|
||||
superagent.get(`${config.apiServerOrigin}/api/v1/cloudron_license`)
|
||||
.query({ accessToken: appstoreConfig.token, cloudronId: appstoreConfig.cloudronId, provider: config.provider })
|
||||
.timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new Error('Network error getting license:' + error.message));
|
||||
if (result.statusCode !== 200) return callback(new Error(`Bad status getting license: ${result.status} ${result.text}`));
|
||||
|
||||
if (!result.body.cloudronId || !result.body.licenseKey || !result.body.cloudronToken) return callback(new Error(`Bad response getting license: ${result.text}`));
|
||||
|
||||
console.log('Adding license', result.body);
|
||||
|
||||
async.series([
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', [ 'cloudron_id', result.body.cloudronId ]),
|
||||
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', [ 'license_key', result.body.licenseKey ]),
|
||||
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', [ 'cloudron_token', result.body.cloudronToken ]),
|
||||
db.runSql.bind(db, 'DELETE FROM settings WHERE name=?', [ 'appstore_config' ]),
|
||||
db.runSql.bind(db, 'COMMIT')
|
||||
], callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
13
migrations/20190505204730-expire-existing-tokens.js
Normal file
13
migrations/20190505204730-expire-existing-tokens.js
Normal file
@@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('UPDATE tokens SET expires=? WHERE clientId=?', [ 1557089270832, 'cid-webadmin' ], function (error) { // force webadmin to get a new token
|
||||
if (error) console.error(error);
|
||||
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
15
migrations/20190507030217-settings-drop-locked.js
Normal file
15
migrations/20190507030217-settings-drop-locked.js
Normal file
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE settings DROP COLUMN locked', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE settings ADD COLUMN locked BOOLEAN DEFAULT 0', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
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);
|
||||
});
|
||||
};
|
||||
@@ -8,6 +8,7 @@
|
||||
#### TEXT - stored offline from table row (use for strings)
|
||||
#### BLOB - stored offline from table row (use for binary data)
|
||||
#### https://dev.mysql.com/doc/refman/5.0/en/storage-requirements.html
|
||||
#### Times are stored in the database in UTC. And precision is seconds
|
||||
|
||||
# The code uses zero dates. Make sure sql_mode does NOT have NO_ZERO_DATE
|
||||
# http://johnemb.blogspot.com/2014/09/adding-or-removing-individual-sql-modes.html
|
||||
@@ -83,7 +84,9 @@ CREATE TABLE IF NOT EXISTS apps(
|
||||
robotsTxt TEXT,
|
||||
enableBackup BOOLEAN DEFAULT 1, // misnomer: controls automatic daily backups
|
||||
enableAutomaticUpdate BOOLEAN DEFAULT 1,
|
||||
mailboxName VARCHAR(128), // mailbox of this app
|
||||
mailboxName VARCHAR(128), // mailbox of this app. default allocated as '.app'
|
||||
label VARCHAR(128), // display name
|
||||
tagsJson VARCHAR(2048), // array of tags
|
||||
|
||||
// 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
|
||||
@@ -137,6 +140,7 @@ CREATE TABLE IF NOT EXISTS backups(
|
||||
state VARCHAR(16) NOT NULL,
|
||||
manifestJson TEXT, /* to validate if the app can be installed in this version of box */
|
||||
format VARCHAR(16) DEFAULT "tgz",
|
||||
preserveSecs INTEGER DEFAULT 0,
|
||||
|
||||
PRIMARY KEY (id));
|
||||
|
||||
@@ -155,6 +159,7 @@ CREATE TABLE IF NOT EXISTS domains(
|
||||
provider VARCHAR(16) NOT NULL,
|
||||
configJson TEXT, /* JSON containing the dns backend provider config */
|
||||
tlsConfigJson TEXT, /* JSON containing the tls provider config */
|
||||
locked BOOLEAN,
|
||||
|
||||
PRIMARY KEY (domain))
|
||||
|
||||
@@ -169,6 +174,8 @@ CREATE TABLE IF NOT EXISTS mail(
|
||||
catchAllJson TEXT,
|
||||
relayJson TEXT,
|
||||
|
||||
dkimSelector VARCHAR(128) NOT NULL DEFAULT "cloudron",
|
||||
|
||||
FOREIGN KEY(domain) REFERENCES domains(domain),
|
||||
PRIMARY KEY(domain))
|
||||
|
||||
@@ -217,10 +224,9 @@ CREATE TABLE IF NOT EXISTS tasks(
|
||||
CREATE TABLE IF NOT EXISTS notifications(
|
||||
id int NOT NULL AUTO_INCREMENT,
|
||||
userId VARCHAR(128) NOT NULL,
|
||||
eventId VARCHAR(128),
|
||||
eventId VARCHAR(128), // reference to eventlog. can be null
|
||||
title VARCHAR(512) NOT NULL,
|
||||
message TEXT,
|
||||
action VARCHAR(512) NOT NULL,
|
||||
acknowledged BOOLEAN DEFAULT false,
|
||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
|
||||
8891
package-lock.json
generated
8891
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
92
package.json
92
package.json
@@ -14,42 +14,43 @@
|
||||
"node": ">=4.0.0 <=4.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google-cloud/dns": "^0.7.2",
|
||||
"@google-cloud/storage": "^1.7.0",
|
||||
"@sindresorhus/df": "^2.1.0",
|
||||
"async": "^2.6.1",
|
||||
"aws-sdk": "^2.253.1",
|
||||
"@google-cloud/dns": "^0.9.2",
|
||||
"@google-cloud/storage": "^2.5.0",
|
||||
"@sindresorhus/df": "^3.1.0",
|
||||
"async": "^2.6.2",
|
||||
"aws-sdk": "^2.441.0",
|
||||
"body-parser": "^1.18.3",
|
||||
"cloudron-manifestformat": "^2.14.2",
|
||||
"cloudron-manifestformat": "^2.15.0",
|
||||
"connect": "^3.6.6",
|
||||
"connect-ensure-login": "^0.1.1",
|
||||
"connect-lastmile": "^1.0.2",
|
||||
"connect-timeout": "^1.9.0",
|
||||
"cookie-parser": "^1.3.5",
|
||||
"cookie-session": "^1.3.2",
|
||||
"cron": "^1.5.1",
|
||||
"csurf": "^1.6.6",
|
||||
"db-migrate": "^0.11.1",
|
||||
"cookie-parser": "^1.4.4",
|
||||
"cookie-session": "^1.3.3",
|
||||
"cron": "^1.7.0",
|
||||
"csurf": "^1.9.0",
|
||||
"db-migrate": "^0.11.5",
|
||||
"db-migrate-mysql": "^1.1.10",
|
||||
"debug": "^3.1.0",
|
||||
"dockerode": "^2.5.5",
|
||||
"debug": "^4.1.1",
|
||||
"dockerode": "^2.5.8",
|
||||
"ejs": "^2.6.1",
|
||||
"ejs-cli": "^2.0.1",
|
||||
"express": "^4.16.3",
|
||||
"express-session": "^1.15.6",
|
||||
"json": "^9.0.3",
|
||||
"express": "^4.16.4",
|
||||
"express-session": "^1.16.1",
|
||||
"js-yaml": "^3.13.1",
|
||||
"json": "^9.0.6",
|
||||
"ldapjs": "^1.0.2",
|
||||
"lodash": "^4.17.11",
|
||||
"lodash.chunk": "^4.2.0",
|
||||
"mime": "^2.3.1",
|
||||
"moment-timezone": "^0.5.17",
|
||||
"morgan": "^1.9.0",
|
||||
"multiparty": "^4.1.4",
|
||||
"mysql": "^2.15.0",
|
||||
"namecheap": "github:joshuakarjala/node-namecheap#464a952",
|
||||
"nodemailer": "^4.6.5",
|
||||
"mime": "^2.4.2",
|
||||
"moment-timezone": "^0.5.25",
|
||||
"morgan": "^1.9.1",
|
||||
"multiparty": "^4.2.1",
|
||||
"mysql": "^2.17.1",
|
||||
"nodemailer": "^6.1.1",
|
||||
"nodemailer-smtp-transport": "^2.7.4",
|
||||
"oauth2orize": "^1.11.0",
|
||||
"once": "^1.3.2",
|
||||
"once": "^1.4.0",
|
||||
"parse-links": "^0.1.0",
|
||||
"passport": "^0.4.0",
|
||||
"passport-http": "^0.3.0",
|
||||
@@ -58,41 +59,40 @@
|
||||
"passport-oauth2-client-password": "^0.1.2",
|
||||
"progress-stream": "^2.0.0",
|
||||
"proxy-middleware": "^0.15.0",
|
||||
"qrcode": "^1.2.0",
|
||||
"readdirp": "^2.1.0",
|
||||
"request": "^2.87.0",
|
||||
"rimraf": "^2.6.2",
|
||||
"qrcode": "^1.3.3",
|
||||
"readdirp": "^3.0.0",
|
||||
"request": "^2.88.0",
|
||||
"rimraf": "^2.6.3",
|
||||
"s3-block-read-stream": "^0.5.0",
|
||||
"safetydance": "^0.7.1",
|
||||
"semver": "^5.5.0",
|
||||
"showdown": "^1.8.6",
|
||||
"semver": "^6.0.0",
|
||||
"showdown": "^1.9.0",
|
||||
"speakeasy": "^2.0.0",
|
||||
"split": "^1.0.0",
|
||||
"superagent": "^3.8.3",
|
||||
"split": "^1.0.1",
|
||||
"superagent": "^5.0.2",
|
||||
"supererror": "^0.7.2",
|
||||
"tar-fs": "^1.16.2",
|
||||
"tar-stream": "^1.6.1",
|
||||
"tar-fs": "github:cloudron-io/tar-fs#ignore_stat_error",
|
||||
"tar-stream": "^2.0.1",
|
||||
"tldjs": "^2.3.1",
|
||||
"underscore": "^1.9.1",
|
||||
"uuid": "^3.2.1",
|
||||
"uuid": "^3.3.2",
|
||||
"valid-url": "^1.0.9",
|
||||
"validator": "^10.3.0",
|
||||
"ws": "^5.2.0"
|
||||
"validator": "^10.11.0",
|
||||
"ws": "^6.2.1",
|
||||
"xml2js": "^0.4.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"expect.js": "*",
|
||||
"hock": "^1.3.2",
|
||||
"istanbul": "*",
|
||||
"js2xmlparser": "^3.0.0",
|
||||
"mocha": "^5.2.0",
|
||||
"hock": "^1.3.3",
|
||||
"js2xmlparser": "^4.0.0",
|
||||
"mocha": "^6.1.4",
|
||||
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
|
||||
"nock": "^9.0.14",
|
||||
"node-sass": "^4.6.1",
|
||||
"recursive-readdir": "^2.2.2",
|
||||
"sinon": "^7.2.2"
|
||||
"nock": "^10.0.6",
|
||||
"node-sass": "^4.11.0",
|
||||
"recursive-readdir": "^2.2.2"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- --no-timeouts --exit -R spec ./src/test ./src/routes/test/[^a]*js",
|
||||
"test": "./runTests",
|
||||
"postmerge": "/bin/true",
|
||||
"precommit": "/bin/true",
|
||||
"prepush": "npm test",
|
||||
|
||||
71
runTests
Executable file
71
runTests
Executable file
@@ -0,0 +1,71 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu
|
||||
|
||||
readonly SOURCE_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly DATA_DIR="${HOME}/.cloudron_test"
|
||||
readonly DEFAULT_TESTS="./src/test/*-test.js ./src/routes/test/*-test.js"
|
||||
|
||||
! "${SOURCE_dir}/src/test/checkInstall" && exit 1
|
||||
|
||||
# cleanup old data dirs some of those docker container data requires sudo to be removed
|
||||
echo "=> Provide root password to purge any leftover data in ${DATA_DIR} and load apparmor profile:"
|
||||
sudo rm -rf ${DATA_DIR}
|
||||
|
||||
# archlinux does not have apparmor
|
||||
if hash apparmor_parser 2>/dev/null; then
|
||||
echo "=> Loading app armor profile"
|
||||
sudo apparmor_parser --replace --write-cache ./setup/start/docker-cloudron-app.apparmor
|
||||
fi
|
||||
|
||||
# create dir structure
|
||||
mkdir -p ${DATA_DIR}
|
||||
cd ${DATA_DIR}
|
||||
mkdir -p appsdata
|
||||
mkdir -p boxdata/appicons boxdata/mail boxdata/certs boxdata/mail/dkim/localhost boxdata/mail/dkim/foobar.com
|
||||
mkdir -p platformdata/addons/mail platformdata/nginx/cert platformdata/nginx/applications platformdata/collectd/collectd.conf.d platformdata/addons platformdata/logrotate.d platformdata/backup platformdata/logs/tasks
|
||||
|
||||
# put cert
|
||||
echo "=> Generating a localhost selfsigned cert"
|
||||
openssl req -x509 -newkey rsa:2048 -keyout platformdata/nginx/cert/host.key -out platformdata/nginx/cert/host.cert -days 3650 -subj '/CN=localhost' -nodes -config <(cat /etc/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.localhost"))
|
||||
|
||||
# clear out any containers
|
||||
echo "=> Delete all docker containers first"
|
||||
docker ps -qa | xargs --no-run-if-empty docker rm -f
|
||||
|
||||
# create docker network (while the infra code does this, most tests skip infra setup)
|
||||
docker network create --subnet=172.18.0.0/16 cloudron || true
|
||||
|
||||
# create the same mysql server version to test with
|
||||
OUT=`docker inspect mysql-server` || true
|
||||
if [[ "${OUT}" = "[]" ]]; then
|
||||
echo "=> Starting mysql-server..."
|
||||
docker run --name mysql-server -e MYSQL_ROOT_PASSWORD=password -d mysql:5.7
|
||||
else
|
||||
echo "=> mysql-server already running. If you want to start fresh, run 'docker rm --force mysql-server'"
|
||||
fi
|
||||
|
||||
export MYSQL_IP=`docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' mysql-server`
|
||||
|
||||
echo "=> Waiting for mysql server to be ready..."
|
||||
while ! mysqladmin ping -h"${MYSQL_IP}" --silent; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "=> Starting cloudron-syslog"
|
||||
cloudron-syslog --logdir "${DATA_DIR}/platformdata/logs/" &
|
||||
|
||||
echo "=> Ensure database"
|
||||
mysql -h"${MYSQL_IP}" -uroot -ppassword -e 'CREATE DATABASE IF NOT EXISTS box'
|
||||
|
||||
echo "=> Run database migrations"
|
||||
cd "${SOURCE_dir}"
|
||||
BOX_ENV=test DATABASE_URL=mysql://root:password@${MYSQL_IP}/box node_modules/.bin/db-migrate up
|
||||
|
||||
echo "=> Run tests with mocha"
|
||||
TESTS=${DEFAULT_TESTS}
|
||||
if [[ $# -gt 0 ]]; then
|
||||
TESTS="$*"
|
||||
fi
|
||||
|
||||
BOX_ENV=test ./node_modules/mocha/bin/_mocha --bail --no-timeouts --exit -R spec ${TESTS}
|
||||
@@ -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}"
|
||||
|
||||
@@ -43,22 +43,20 @@ fi
|
||||
initBaseImage="true"
|
||||
# provisioning data
|
||||
provider=""
|
||||
edition=""
|
||||
requestedVersion=""
|
||||
apiServerOrigin="https://api.cloudron.io"
|
||||
webServerOrigin="https://cloudron.io"
|
||||
sourceTarballUrl=""
|
||||
rebootServer="true"
|
||||
baseDataDir=""
|
||||
license=""
|
||||
|
||||
args=$(getopt -o "" -l "help,skip-baseimage-init,provider:,version:,env:,edition:,skip-reboot" -n "$0" -- "$@")
|
||||
args=$(getopt -o "" -l "help,skip-baseimage-init,provider:,version:,env:,skip-reboot,license:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--help) echo "See https://cloudron.io/documentation/installation/ on how to install Cloudron"; exit 0;;
|
||||
--provider) provider="$2"; shift 2;;
|
||||
--edition) edition="$2"; shift 2;;
|
||||
--version) requestedVersion="$2"; shift 2;;
|
||||
--env)
|
||||
if [[ "$2" == "dev" ]]; then
|
||||
@@ -69,6 +67,7 @@ while true; do
|
||||
webServerOrigin="https://staging.cloudron.io"
|
||||
fi
|
||||
shift 2;;
|
||||
--license) license="$2"; shift 2;;
|
||||
--skip-baseimage-init) initBaseImage="false"; shift;;
|
||||
--skip-reboot) rebootServer="false"; shift;;
|
||||
--) break;;
|
||||
@@ -94,7 +93,7 @@ echo "Running cloudron-setup with args : $@" > "${LOG_FILE}"
|
||||
|
||||
# validate arguments in the absence of data
|
||||
if [[ -z "${provider}" ]]; then
|
||||
echo "--provider is required (azure, digitalocean, ec2, exoscale, gce, hetzner, lightsail, linode, netcup, ovh, rosehosting, scaleway, vultr or generic)"
|
||||
echo "--provider is required (azure, contabo, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, lightsail, linode, netcup, ovh, rosehosting, scaleway, upcloud, vultr or generic)"
|
||||
exit 1
|
||||
elif [[ \
|
||||
"${provider}" != "ami" && \
|
||||
@@ -112,20 +111,18 @@ elif [[ \
|
||||
"${provider}" != "hetzner" && \
|
||||
"${provider}" != "lightsail" && \
|
||||
"${provider}" != "linode" && \
|
||||
"${provider}" != "linode-stackscript" && \
|
||||
"${provider}" != "netcup" && \
|
||||
"${provider}" != "netcup-image" && \
|
||||
"${provider}" != "ovh" && \
|
||||
"${provider}" != "rosehosting" && \
|
||||
"${provider}" != "scaleway" && \
|
||||
"${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, vultr or generic"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "${baseDataDir}" && ! -d "${baseDataDir}" ]]; then
|
||||
echo "${baseDataDir} does not exist"
|
||||
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"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -201,41 +198,20 @@ fi
|
||||
|
||||
# NOTE: this install script only supports 3.x and above
|
||||
echo "=> Installing version ${version} (this takes some time) ..."
|
||||
if [[ "${version}" =~ 3\.[0-2]+\.[0-9]+ ]]; then
|
||||
readonly DATA_FILE="/root/cloudron-install-data.json"
|
||||
data=$(cat <<EOF
|
||||
{
|
||||
"provider": "${provider}",
|
||||
"edition": "${edition}",
|
||||
"apiServerOrigin": "${apiServerOrigin}",
|
||||
"webServerOrigin": "${webServerOrigin}",
|
||||
"version": "${version}"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
echo "${data}" > "${DATA_FILE}"
|
||||
|
||||
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" &>> "${LOG_FILE}"; then
|
||||
echo "Failed to install cloudron. See ${LOG_FILE} for details"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm "${DATA_FILE}"
|
||||
else
|
||||
mkdir -p /etc/cloudron
|
||||
cat > "/etc/cloudron/cloudron.conf" <<CONF_END
|
||||
mkdir -p /etc/cloudron
|
||||
cat > "/etc/cloudron/cloudron.conf" <<CONF_END
|
||||
{
|
||||
"apiServerOrigin": "${apiServerOrigin}",
|
||||
"webServerOrigin": "${webServerOrigin}",
|
||||
"provider": "${provider}",
|
||||
"edition": "${edition}"
|
||||
"provider": "${provider}"
|
||||
}
|
||||
CONF_END
|
||||
|
||||
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" &>> "${LOG_FILE}"; then
|
||||
echo "Failed to install cloudron. See ${LOG_FILE} for details"
|
||||
exit 1
|
||||
fi
|
||||
[[ -n "${license}" ]] && echo -n "$license" > /etc/cloudron/LICENSE
|
||||
|
||||
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" &>> "${LOG_FILE}"; then
|
||||
echo "Failed to install cloudron. See ${LOG_FILE} for details"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -n "=> Waiting for cloudron to be ready (this takes some time) ..."
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
# This script collects diagnostic information to help debug server related issues
|
||||
# It also enables SSH access for the cloudron support team
|
||||
|
||||
@@ -17,7 +19,7 @@ This script collects diagnostic information to help debug server related issues
|
||||
|
||||
# We require root
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root." > /dev/stderr
|
||||
echo "This script should be run as root. Run with sudo"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -58,21 +60,6 @@ echo -n "Generating Cloudron Support stats..."
|
||||
# clear file
|
||||
rm -rf $OUT
|
||||
|
||||
ssh_port=$(cat /etc/ssh/sshd_config | grep "Port " | sed -e "s/.*Port //")
|
||||
if [[ $SUDO_USER == "" ]]; then
|
||||
ssh_user="root"
|
||||
ssh_folder="/root/.ssh/"
|
||||
authorized_key_file="${ssh_folder}/authorized_keys"
|
||||
else
|
||||
ssh_user="$SUDO_USER"
|
||||
ssh_folder="/home/$SUDO_USER/.ssh/"
|
||||
authorized_key_file="${ssh_folder}/authorized_keys"
|
||||
fi
|
||||
|
||||
echo -e $LINE"SSH"$LINE >> $OUT
|
||||
echo "Username: ${ssh_user}" >> $OUT
|
||||
echo "Port: ${ssh_port}" >> $OUT
|
||||
|
||||
echo -e $LINE"cloudron.conf"$LINE >> $OUT
|
||||
cat /etc/cloudron/cloudron.conf &>> $OUT
|
||||
|
||||
@@ -84,31 +71,65 @@ fi
|
||||
echo -e $LINE"Filesystem stats"$LINE >> $OUT
|
||||
df -h &>> $OUT
|
||||
|
||||
echo -e $LINE"Appsdata stats"$LINE >> $OUT
|
||||
du -hcsL /home/yellowtent/appsdata/* &>> $OUT
|
||||
|
||||
echo -e $LINE"Boxdata stats"$LINE >> $OUT
|
||||
du -hcsL /home/yellowtent/boxdata/* &>> $OUT
|
||||
|
||||
echo -e $LINE"Backup stats (possibly misleading)"$LINE >> $OUT
|
||||
du -hcsL /var/backups/* &>> $OUT
|
||||
|
||||
echo -e $LINE"System daemon status"$LINE >> $OUT
|
||||
systemctl status --lines=100 cloudron.target box mysql unbound cloudron-syslog nginx collectd docker &>> $OUT
|
||||
|
||||
echo -e $LINE"Box logs"$LINE >> $OUT
|
||||
tail -n 100 /home/yellowtent/platformdata/logs/box.log &>> $OUT
|
||||
|
||||
echo -e $LINE"Firewall chains"$LINE >> $OUT
|
||||
ip addr &>> $OUT
|
||||
|
||||
echo -e $LINE"Firewall chains"$LINE >> $OUT
|
||||
iptables -L &>> $OUT
|
||||
|
||||
echo "Done"
|
||||
|
||||
if [[ "${enableSSH}" == "true" ]]; then
|
||||
ssh_port=$(cat /etc/ssh/sshd_config | grep "Port " | sed -e "s/.*Port //")
|
||||
permit_root_login=$(grep -q ^PermitRootLogin.*yes /etc/ssh/sshd_config && echo "yes" || echo "no")
|
||||
|
||||
# support.js uses similar logic
|
||||
if $(grep -q "ec2\|lightsail\|ami" /etc/cloudron/cloudron.conf); then
|
||||
ssh_user="ubuntu"
|
||||
keys_file="/home/ubuntu/.ssh/authorized_keys"
|
||||
else
|
||||
ssh_user="root"
|
||||
keys_file="/root/.ssh/authorized_keys"
|
||||
fi
|
||||
|
||||
echo -e $LINE"SSH"$LINE >> $OUT
|
||||
echo "Username: ${ssh_user}" >> $OUT
|
||||
echo "Port: ${ssh_port}" >> $OUT
|
||||
echo "PermitRootLogin: ${permit_root_login}" >> $OUT
|
||||
echo "Key file: ${keys_file}" >> $OUT
|
||||
|
||||
echo -n "Enabling ssh access for the Cloudron support team..."
|
||||
mkdir -p $(dirname "${keys_file}") # .ssh does not exist sometimes
|
||||
touch "${keys_file}" # required for concat to work
|
||||
if ! grep -q "${CLOUDRON_SUPPORT_PUBLIC_KEY}" "${keys_file}"; then
|
||||
echo -e "\n${CLOUDRON_SUPPORT_PUBLIC_KEY}" >> "${keys_file}"
|
||||
chmod 600 "${keys_file}"
|
||||
chown "${ssh_user}" "${keys_file}"
|
||||
fi
|
||||
|
||||
echo "Done"
|
||||
fi
|
||||
|
||||
echo -n "Uploading information..."
|
||||
# for some reason not using $(cat $OUT) will not contain newlines!?
|
||||
paste_key=$(curl -X POST ${PASTEBIN}/documents --silent -d "$(cat $OUT)" | python3 -c "import sys, json; print(json.load(sys.stdin)['key'])")
|
||||
echo "Done"
|
||||
|
||||
if [[ "${enableSSH}" == "true" ]]; then
|
||||
echo -n "Enabling ssh access for the Cloudron support team..."
|
||||
mkdir -p "${ssh_folder}"
|
||||
echo "${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 ""
|
||||
|
||||
@@ -41,8 +41,8 @@ if ! $(cd "${SOURCE_DIR}/../dashboard" && git diff --exit-code >/dev/null); then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$(node --version)" != "v8.11.2" ]]; then
|
||||
echo "This script requires node 8.11.2"
|
||||
if [[ "$(node --version)" != "v10.15.1" ]]; then
|
||||
echo "This script requires node 10.15.1"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -19,17 +19,18 @@ readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max
|
||||
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly box_src_tmp_dir="$(realpath ${script_dir}/..)"
|
||||
|
||||
readonly ubuntu_version=$(lsb_release -rs)
|
||||
readonly ubuntu_codename=$(lsb_release -cs)
|
||||
|
||||
readonly is_update=$(systemctl is-active box && echo "yes" || echo "no")
|
||||
|
||||
echo "==> installer: updating docker"
|
||||
if [[ $(docker version --format {{.Client.Version}}) != "18.03.1-ce" ]]; then
|
||||
$curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_18.03.1~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
|
||||
|
||||
# https://download.docker.com/linux/ubuntu/dists/xenial/stable/binary-amd64/Packages
|
||||
if [[ $(sha256sum /tmp/docker.deb | cut -d' ' -f1) != "54f4c9268492a4fd2ec2e6bcc95553855b025f35dcc8b9f60ac34e0aa307279b" ]]; then
|
||||
echo "==> installer: docker binary download is corrupt"
|
||||
exit 5
|
||||
fi
|
||||
if [[ $(docker version --format {{.Client.Version}}) != "18.09.2" ]]; then
|
||||
# there are 3 packages for docker - containerd, CLI and the daemon
|
||||
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_1.2.2-3_amd64.deb" -o /tmp/containerd.deb
|
||||
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce-cli_18.09.2~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker-ce-cli.deb
|
||||
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce_18.09.2~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker.deb
|
||||
|
||||
echo "==> installer: Waiting for all dpkg tasks to finish..."
|
||||
while fuser /var/lib/dpkg/lock; do
|
||||
@@ -47,21 +48,21 @@ if [[ $(docker version --format {{.Client.Version}}) != "18.03.1-ce" ]]; then
|
||||
sleep 1
|
||||
done
|
||||
|
||||
while ! apt install -y /tmp/docker.deb; do
|
||||
while ! apt install -y /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb; do
|
||||
echo "==> installer: Failed to install docker. Retry"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
rm /tmp/docker.deb
|
||||
rm /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb
|
||||
fi
|
||||
|
||||
echo "==> installer: updating node"
|
||||
if [[ "$(node --version)" != "v8.11.2" ]]; then
|
||||
mkdir -p /usr/local/node-8.11.2
|
||||
$curl -sL https://nodejs.org/dist/v8.11.2/node-v8.11.2-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-8.11.2
|
||||
ln -sf /usr/local/node-8.11.2/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-8.11.2/bin/npm /usr/bin/npm
|
||||
rm -rf /usr/local/node-6.11.5
|
||||
if [[ "$(node --version)" != "v10.15.1" ]]; then
|
||||
mkdir -p /usr/local/node-10.15.1
|
||||
$curl -sL https://nodejs.org/dist/v10.15.1/node-v10.15.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-10.15.1
|
||||
ln -sf /usr/local/node-10.15.1/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-10.15.1/bin/npm /usr/bin/npm
|
||||
rm -rf /usr/local/node-8.11.2 /usr/local/node-8.9.3
|
||||
fi
|
||||
|
||||
# this is here (and not in updater.js) because rebuild requires the above node
|
||||
|
||||
@@ -26,22 +26,6 @@ systemctl enable apparmor
|
||||
systemctl restart apparmor
|
||||
|
||||
usermod ${USER} -a -G docker
|
||||
# preserve the existing storage driver (user might be using overlay2)
|
||||
storage_driver=$(docker info | grep "Storage Driver" | sed 's/.*: //')
|
||||
[[ -n "${storage_driver}" ]] || storage_driver="overlay2" # if the above command fails
|
||||
|
||||
temp_file=$(mktemp)
|
||||
# create systemd drop-in. some apps do not work with aufs
|
||||
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=${storage_driver}" > "${temp_file}"
|
||||
|
||||
systemctl enable docker
|
||||
# restart docker if options changed
|
||||
if [[ ! -f /etc/systemd/system/docker.service.d/cloudron.conf ]] || ! diff -q /etc/systemd/system/docker.service.d/cloudron.conf "${temp_file}" >/dev/null; then
|
||||
mkdir -p /etc/systemd/system/docker.service.d
|
||||
mv "${temp_file}" /etc/systemd/system/docker.service.d/cloudron.conf
|
||||
systemctl daemon-reload
|
||||
systemctl restart docker
|
||||
fi
|
||||
docker network create --subnet=172.18.0.0/16 cloudron || true
|
||||
|
||||
mkdir -p "${BOX_DATA_DIR}"
|
||||
@@ -60,7 +44,10 @@ mkdir -p "${PLATFORM_DATA_DIR}/collectd/collectd.conf.d"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/logrotate.d"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/acme"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/backup"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/logs/backup" "${PLATFORM_DATA_DIR}/logs/updater" "${PLATFORM_DATA_DIR}/logs/tasks"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/logs/backup" \
|
||||
"${PLATFORM_DATA_DIR}/logs/updater" \
|
||||
"${PLATFORM_DATA_DIR}/logs/tasks" \
|
||||
"${PLATFORM_DATA_DIR}/logs/crash"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/update"
|
||||
|
||||
mkdir -p "${BOX_DATA_DIR}/appicons"
|
||||
@@ -121,6 +108,8 @@ 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 "==> Configuring sudoers"
|
||||
rm -f /etc/sudoers.d/${USER}
|
||||
cp "${script_dir}/start/sudoers" /etc/sudoers.d/${USER}
|
||||
@@ -135,8 +124,11 @@ echo "==> Configuring logrotate"
|
||||
if ! grep -q "^include ${PLATFORM_DATA_DIR}/logrotate.d" /etc/logrotate.conf; then
|
||||
echo -e "\ninclude ${PLATFORM_DATA_DIR}/logrotate.d\n" >> /etc/logrotate.conf
|
||||
fi
|
||||
cp "${script_dir}/start/box-logrotate" "${script_dir}/start/app-logrotate" "${PLATFORM_DATA_DIR}/logrotate.d/"
|
||||
chown root:root "${PLATFORM_DATA_DIR}/logrotate.d/box-logrotate" "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate"
|
||||
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/"
|
||||
|
||||
echo "==> Adding motd message for admins"
|
||||
cp "${script_dir}/start/cloudron-motd" /etc/update-motd.d/92-cloudron
|
||||
@@ -189,6 +181,10 @@ else
|
||||
cp "${BOX_DATA_DIR}/dhparams.pem" "${PLATFORM_DATA_DIR}/addons/mail/dhparams.pem"
|
||||
fi
|
||||
|
||||
# old installations used to create appdata/<app>/redis which is now part of old backups and prevents restore
|
||||
echo "==> Cleaning up stale redis directories"
|
||||
find "${APPS_DATA_DIR}" -maxdepth 2 -type d -name redis -exec rm -rf {} +
|
||||
|
||||
echo "==> Changing ownership"
|
||||
# be careful of what is chown'ed here. subdirs like mysql,redis etc are owned by the containers and will stop working if perms change
|
||||
chown -R "${USER}" /etc/cloudron
|
||||
@@ -197,9 +193,6 @@ chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}/INFRA_VERSION" 2>/dev/null || true
|
||||
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}"
|
||||
chown "${USER}:${USER}" "${APPS_DATA_DIR}"
|
||||
|
||||
# logrotate files have to be owned by root, this is here to fixup existing installations where we were resetting the owner to yellowtent
|
||||
chown root:root -R "${PLATFORM_DATA_DIR}/logrotate.d"
|
||||
|
||||
# do not chown the boxdata/mail directory; dovecot gets upset
|
||||
chown "${USER}:${USER}" "${BOX_DATA_DIR}"
|
||||
find "${BOX_DATA_DIR}" -mindepth 1 -maxdepth 1 -not -path "${BOX_DATA_DIR}/mail" -exec chown -R "${USER}:${USER}" {} \;
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# logrotate config for app logs
|
||||
|
||||
/home/yellowtent/platformdata/logs/*/*.log {
|
||||
# only keep one rotated file, we currently do not send that over the api
|
||||
rotate 1
|
||||
size 10M
|
||||
# we never compress so we can simply tail the files
|
||||
nocompress
|
||||
copytruncate
|
||||
}
|
||||
@@ -9,7 +9,7 @@ iptables -t filter -F CLOUDRON # empty any existing rules
|
||||
# NOTE: keep these in sync with src/apps.js validatePortBindings
|
||||
# allow ssh, http, https, ping, dns
|
||||
iptables -t filter -I CLOUDRON -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
# caas has ssh on port 202
|
||||
# ssh is allowed alternately on port 202
|
||||
iptables -A CLOUDRON -p tcp -m tcp -m multiport --dports 22,25,80,202,443,587,993,4190 -j ACCEPT
|
||||
|
||||
iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-request -j ACCEPT
|
||||
|
||||
@@ -17,11 +17,6 @@ else
|
||||
printf "are automatically installed on this server every night.\n"
|
||||
printf "\n"
|
||||
printf "Read more at https://cloudron.io/documentation/security/#os-updates\n"
|
||||
|
||||
if grep -q "^PasswordAuthentication yes" /etc/ssh/sshd_config; then
|
||||
printf "\nPlease disable password based SSH access to secure your server. Read more at\n"
|
||||
printf "https://cloudron.io/documentation/security/#securing-ssh-access\n"
|
||||
fi
|
||||
fi
|
||||
|
||||
printf "\nFor help and more information, visit https://forum.cloudron.io\n\n"
|
||||
|
||||
40
setup/start/custom.yml
Normal file
40
setup/start/custom.yml
Normal file
@@ -0,0 +1,40 @@
|
||||
# add customizations here
|
||||
# after making changes run "sudo systemctl restart box"
|
||||
|
||||
# appstore:
|
||||
# blacklist:
|
||||
# - io.wekan.cloudronapp
|
||||
# - io.cloudron.openvpn
|
||||
# whitelist:
|
||||
# org.wordpress.cloudronapp: {}
|
||||
# chat.rocket.cloudronapp: {}
|
||||
# com.nextcloud.cloudronapp: {}
|
||||
#
|
||||
# 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).
|
||||
# * [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)'
|
||||
18
setup/start/logrotate/app
Normal file
18
setup/start/logrotate/app
Normal file
@@ -0,0 +1,18 @@
|
||||
# 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
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ max_allowed_packet=32M
|
||||
character-set-server = utf8mb4
|
||||
collation-server = utf8mb4_unicode_ci
|
||||
|
||||
# set timezone to UTC
|
||||
default_time_zone='+00:00'
|
||||
|
||||
[mysqldump]
|
||||
quick
|
||||
quote-names
|
||||
|
||||
@@ -49,3 +49,4 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartdocker.s
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/restartunbound.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartunbound.sh
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
exports = module.exports = {
|
||||
SCOPE_APPS_READ: 'apps:read',
|
||||
SCOPE_APPS_MANAGE: 'apps:manage',
|
||||
SCOPE_APPSTORE: 'appstore',
|
||||
SCOPE_CLIENTS: 'clients',
|
||||
SCOPE_CLOUDRON: 'cloudron',
|
||||
SCOPE_DOMAINS_READ: 'domains:read',
|
||||
@@ -10,10 +11,10 @@ exports = module.exports = {
|
||||
SCOPE_MAIL: 'mail',
|
||||
SCOPE_PROFILE: 'profile',
|
||||
SCOPE_SETTINGS: 'settings',
|
||||
SCOPE_SUBSCRIPTION: 'subscription',
|
||||
SCOPE_USERS_READ: 'users:read',
|
||||
SCOPE_USERS_MANAGE: 'users:manage',
|
||||
SCOPE_APPSTORE: 'appstore',
|
||||
VALID_SCOPES: [ 'apps', 'appstore', 'clients', 'cloudron', 'domains', 'mail', 'profile', 'settings', 'users' ], // keep this sorted
|
||||
VALID_SCOPES: [ 'apps', 'appstore', 'clients', 'cloudron', 'domains', 'mail', 'profile', 'settings', 'subscription', 'users' ], // keep this sorted
|
||||
|
||||
SCOPE_ANY: '*',
|
||||
|
||||
@@ -114,7 +115,7 @@ function scopesForUser(user, callback) {
|
||||
|
||||
if (user.admin) return callback(null, exports.VALID_SCOPES);
|
||||
|
||||
callback(null, config.isSpacesEnabled() ? [ 'profile', 'apps', 'domains:read', 'users:read' ] : [ 'profile', 'apps:read' ]);
|
||||
callback(null, [ 'profile', 'apps:read' ]);
|
||||
}
|
||||
|
||||
function validateToken(accessToken, callback) {
|
||||
|
||||
264
src/addons.js
264
src/addons.js
@@ -184,7 +184,7 @@ var KNOWN_ADDONS = {
|
||||
const KNOWN_SERVICES = {
|
||||
mail: {
|
||||
status: containerStatus.bind(null, 'mail', 'CLOUDRON_MAIL_TOKEN'),
|
||||
restart: restartContainer.bind(null, 'mail'),
|
||||
restart: mail.restartMail,
|
||||
defaultMemoryLimit: Math.max((1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 128, 256) * 1024 * 1024
|
||||
},
|
||||
mongodb: {
|
||||
@@ -211,10 +211,25 @@ const KNOWN_SERVICES = {
|
||||
status: statusUnbound,
|
||||
restart: restartUnbound,
|
||||
defaultMemoryLimit: 0
|
||||
},
|
||||
sftp: {
|
||||
status: statusSftp,
|
||||
restart: restartContainer.bind(null, 'sftp'),
|
||||
defaultMemoryLimit: 256 * 1024 * 1024
|
||||
},
|
||||
graphite: {
|
||||
status: statusGraphite,
|
||||
restart: restartContainer.bind(null, 'graphite'),
|
||||
defaultMemoryLimit: 75 * 1024 * 1024
|
||||
},
|
||||
nginx: {
|
||||
status: statusNginx,
|
||||
restart: restartNginx,
|
||||
defaultMemoryLimit: 0
|
||||
}
|
||||
};
|
||||
|
||||
function debugApp(app, args) {
|
||||
function debugApp(app /*, args */) {
|
||||
assert(typeof app === 'object');
|
||||
|
||||
debug((app.fqdn || app.location) + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
||||
@@ -261,6 +276,30 @@ function restartContainer(serviceName, 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) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
|
||||
|
||||
const ip = safe.query(result, 'NetworkSettings.Networks.cloudron.IPAddress', null);
|
||||
if (!ip) return callback(new AddonsError(AddonsError.NOT_ACTIVE, `Error getting ${containerName} container ip`));
|
||||
|
||||
// extract the cloudron token for auth
|
||||
const env = safe.query(result, 'Config.Env', null);
|
||||
if (!env) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, `Error getting ${containerName} env`));
|
||||
const tmp = env.find(function (e) { return e.indexOf(tokenEnvName) === 0; });
|
||||
if (!tmp) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, `Error getting ${containerName} cloudron token env var`));
|
||||
const token = tmp.slice(tokenEnvName.length + 1); // +1 for the = sign
|
||||
if (!token) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, `Error getting ${containerName} cloudron token`));
|
||||
|
||||
callback(null, { ip: ip, token: token, state: result.State });
|
||||
});
|
||||
}
|
||||
|
||||
function containerStatus(addonName, addonTokenName, callback) {
|
||||
assert.strictEqual(typeof addonName, 'string');
|
||||
assert.strictEqual(typeof addonTokenName, 'string');
|
||||
@@ -438,30 +477,6 @@ function restartService(serviceName, callback) {
|
||||
KNOWN_SERVICES[serviceName].restart(callback);
|
||||
}
|
||||
|
||||
function getServiceDetails(containerName, tokenEnvName, callback) {
|
||||
assert.strictEqual(typeof containerName, 'string');
|
||||
assert.strictEqual(typeof tokenEnvName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
docker.inspect(containerName, function (error, result) {
|
||||
if (error && error.reason === DockerError.NOT_FOUND) return callback(new AddonsError(AddonsError.NOT_ACTIVE, error));
|
||||
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
|
||||
|
||||
const ip = safe.query(result, 'NetworkSettings.Networks.cloudron.IPAddress', null);
|
||||
if (!ip) return callback(new AddonsError(AddonsError.NOT_ACTIVE, `Error getting ${containerName} container ip`));
|
||||
|
||||
// extract the cloudron token for auth
|
||||
const env = safe.query(result, 'Config.Env', null);
|
||||
if (!env) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, `Error getting ${containerName} env`));
|
||||
const tmp = env.find(function (e) { return e.indexOf(tokenEnvName) === 0; });
|
||||
if (!tmp) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, `Error getting ${containerName} cloudron token env var`));
|
||||
const token = tmp.slice(tokenEnvName.length + 1); // +1 for the = sign
|
||||
if (!token) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, `Error getting ${containerName} cloudron token`));
|
||||
|
||||
callback(null, { ip: ip, token: token, state: result.State });
|
||||
});
|
||||
}
|
||||
|
||||
function waitForService(containerName, tokenEnvName, callback) {
|
||||
assert.strictEqual(typeof containerName, 'string');
|
||||
assert.strictEqual(typeof tokenEnvName, 'string');
|
||||
@@ -617,7 +632,7 @@ function updateServiceConfig(platformConfig, callback) {
|
||||
debug('updateServiceConfig: %j', platformConfig);
|
||||
|
||||
// TODO: this should possibly also rollback memory to default
|
||||
async.eachSeries([ 'mysql', 'postgresql', 'mail', 'mongodb' ], function iterator(serviceName, iteratorCallback) {
|
||||
async.eachSeries([ 'mysql', 'postgresql', 'mail', 'mongodb', 'graphite' ], function iterator(serviceName, iteratorCallback) {
|
||||
const containerConfig = platformConfig[serviceName];
|
||||
let memory, memorySwap;
|
||||
if (containerConfig && containerConfig.memory && containerConfig.memorySwap) {
|
||||
@@ -780,10 +795,12 @@ function setupOauth(app, options, callback) {
|
||||
clients.add(appId, clients.TYPE_OAUTH, redirectURI, scope, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
var env = [
|
||||
{ name: 'OAUTH_CLIENT_ID', value: result.id },
|
||||
{ name: 'OAUTH_CLIENT_SECRET', value: result.clientSecret },
|
||||
{ name: 'OAUTH_ORIGIN', value: config.adminOrigin() }
|
||||
{ name: `${envPrefix}OAUTH_CLIENT_ID`, value: result.id },
|
||||
{ name: `${envPrefix}OAUTH_CLIENT_SECRET`, value: result.clientSecret },
|
||||
{ name: `${envPrefix}OAUTH_ORIGIN`, value: config.adminOrigin() }
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting oauth addon config to %j', env);
|
||||
@@ -817,17 +834,19 @@ function setupEmail(app, options, callback) {
|
||||
|
||||
const mailInDomains = mailDomains.filter(function (d) { return d.enabled; }).map(function (d) { return d.domain; }).join(',');
|
||||
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
// note that "external" access info can be derived from MAIL_DOMAIN (since it's part of user documentation)
|
||||
var env = [
|
||||
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_SMTP_PORT', value: '2525' },
|
||||
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_IMAP_PORT', value: '9993' },
|
||||
{ name: 'MAIL_SIEVE_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_SIEVE_PORT', value: '4190' },
|
||||
{ name: 'MAIL_DOMAIN', value: app.domain },
|
||||
{ name: 'MAIL_DOMAINS', value: mailInDomains },
|
||||
{ name: 'LDAP_MAILBOXES_BASE_DN', value: 'ou=mailboxes,dc=cloudron' }
|
||||
{ name: `${envPrefix}MAIL_SMTP_SERVER`, value: 'mail' },
|
||||
{ name: `${envPrefix}MAIL_SMTP_PORT`, value: '2525' },
|
||||
{ name: `${envPrefix}MAIL_IMAP_SERVER`, value: 'mail' },
|
||||
{ name: `${envPrefix}MAIL_IMAP_PORT`, value: '9993' },
|
||||
{ name: `${envPrefix}MAIL_SIEVE_SERVER`, value: 'mail' },
|
||||
{ name: `${envPrefix}MAIL_SIEVE_PORT`, value: '4190' },
|
||||
{ name: `${envPrefix}MAIL_DOMAIN`, value: app.domain },
|
||||
{ name: `${envPrefix}MAIL_DOMAINS`, value: mailInDomains },
|
||||
{ name: `${envPrefix}LDAP_MAILBOXES_BASE_DN`, value: 'ou=mailboxes,dc=cloudron' }
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting up Email');
|
||||
@@ -853,14 +872,16 @@ function setupLdap(app, options, callback) {
|
||||
|
||||
if (!app.sso) return callback(null);
|
||||
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
var env = [
|
||||
{ name: 'LDAP_SERVER', value: '172.18.0.1' },
|
||||
{ name: 'LDAP_PORT', value: '' + config.get('ldapPort') },
|
||||
{ name: 'LDAP_URL', value: 'ldap://172.18.0.1:' + config.get('ldapPort') },
|
||||
{ name: 'LDAP_USERS_BASE_DN', value: 'ou=users,dc=cloudron' },
|
||||
{ name: 'LDAP_GROUPS_BASE_DN', value: 'ou=groups,dc=cloudron' },
|
||||
{ name: 'LDAP_BIND_DN', value: 'cn='+ app.id + ',ou=apps,dc=cloudron' },
|
||||
{ name: 'LDAP_BIND_PASSWORD', value: hat(4 * 128) } // this is ignored
|
||||
{ name: `${envPrefix}LDAP_SERVER`, value: '172.18.0.1' },
|
||||
{ name: `${envPrefix}LDAP_PORT`, value: '' + config.get('ldapPort') },
|
||||
{ name: `${envPrefix}LDAP_URL`, value: 'ldap://172.18.0.1:' + config.get('ldapPort') },
|
||||
{ name: `${envPrefix}LDAP_USERS_BASE_DN`, value: 'ou=users,dc=cloudron' },
|
||||
{ name: `${envPrefix}LDAP_GROUPS_BASE_DN`, value: 'ou=groups,dc=cloudron' },
|
||||
{ name: `${envPrefix}LDAP_BIND_DN`, value: 'cn='+ app.id + ',ou=apps,dc=cloudron' },
|
||||
{ name: `${envPrefix}LDAP_BIND_PASSWORD`, value: hat(4 * 128) } // this is ignored
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting up LDAP');
|
||||
@@ -890,14 +911,16 @@ function setupSendMail(app, options, callback) {
|
||||
|
||||
var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length
|
||||
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
var env = [
|
||||
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_SMTP_PORT', value: '2525' },
|
||||
{ name: 'MAIL_SMTPS_PORT', value: '2465' },
|
||||
{ name: 'MAIL_SMTP_USERNAME', value: app.mailboxName + '@' + app.domain },
|
||||
{ name: 'MAIL_SMTP_PASSWORD', value: password },
|
||||
{ name: 'MAIL_FROM', value: app.mailboxName + '@' + app.domain },
|
||||
{ name: 'MAIL_DOMAIN', value: app.domain }
|
||||
{ name: `${envPrefix}MAIL_SMTP_SERVER`, value: 'mail' },
|
||||
{ name: `${envPrefix}MAIL_SMTP_PORT`, value: '2525' },
|
||||
{ name: `${envPrefix}MAIL_SMTPS_PORT`, value: '2465' },
|
||||
{ name: `${envPrefix}MAIL_SMTP_USERNAME`, value: app.mailboxName + '@' + app.domain },
|
||||
{ name: `${envPrefix}MAIL_SMTP_PASSWORD`, value: password },
|
||||
{ name: `${envPrefix}MAIL_FROM`, value: app.mailboxName + '@' + app.domain },
|
||||
{ name: `${envPrefix}MAIL_DOMAIN`, value: app.domain }
|
||||
];
|
||||
debugApp(app, 'Setting sendmail addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
|
||||
@@ -926,13 +949,15 @@ function setupRecvMail(app, options, callback) {
|
||||
|
||||
var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length
|
||||
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
var env = [
|
||||
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_IMAP_PORT', value: '9993' },
|
||||
{ name: 'MAIL_IMAP_USERNAME', value: app.mailboxName + '@' + app.domain },
|
||||
{ name: 'MAIL_IMAP_PASSWORD', value: password },
|
||||
{ name: 'MAIL_TO', value: app.mailboxName + '@' + app.domain },
|
||||
{ name: 'MAIL_DOMAIN', value: app.domain }
|
||||
{ name: `${envPrefix}MAIL_IMAP_SERVER`, value: 'mail' },
|
||||
{ name: `${envPrefix}MAIL_IMAP_PORT`, value: '9993' },
|
||||
{ name: `${envPrefix}MAIL_IMAP_USERNAME`, value: app.mailboxName + '@' + app.domain },
|
||||
{ name: `${envPrefix}MAIL_IMAP_PASSWORD`, value: password },
|
||||
{ name: `${envPrefix}MAIL_TO`, value: app.mailboxName + '@' + app.domain },
|
||||
{ name: `${envPrefix}MAIL_DOMAIN`, value: app.domain }
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting sendmail addon config to %j', env);
|
||||
@@ -977,6 +1002,7 @@ function startMysql(existingInfra, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const cmd = `docker run --restart=always -d --name="mysql" \
|
||||
--hostname mysql \
|
||||
--net cloudron \
|
||||
--net-alias mysql \
|
||||
--log-driver syslog \
|
||||
@@ -1033,19 +1059,21 @@ function setupMySql(app, options, callback) {
|
||||
if (error) return callback(new Error('Error setting up mysql: ' + error));
|
||||
if (response.statusCode !== 201) return callback(new Error(`Error setting up mysql. Status code: ${response.statusCode} message: ${response.body.message}`));
|
||||
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
var env = [
|
||||
{ name: 'MYSQL_USERNAME', value: data.username },
|
||||
{ name: 'MYSQL_PASSWORD', value: data.password },
|
||||
{ name: 'MYSQL_HOST', value: 'mysql' },
|
||||
{ name: 'MYSQL_PORT', value: '3306' }
|
||||
{ name: `${envPrefix}MYSQL_USERNAME`, value: data.username },
|
||||
{ name: `${envPrefix}MYSQL_PASSWORD`, value: data.password },
|
||||
{ name: `${envPrefix}MYSQL_HOST`, value: 'mysql' },
|
||||
{ name: `${envPrefix}MYSQL_PORT`, value: '3306' }
|
||||
];
|
||||
|
||||
if (options.multipleDatabases) {
|
||||
env = env.concat({ name: 'MYSQL_DATABASE_PREFIX', value: `${data.prefix}_` });
|
||||
env = env.concat({ name: `${envPrefix}MYSQL_DATABASE_PREFIX`, value: `${data.prefix}_` });
|
||||
} else {
|
||||
env = env.concat(
|
||||
{ name: 'MYSQL_URL', value: `mysql://${data.username}:${data.password}@mysql/${data.database}` },
|
||||
{ name: 'MYSQL_DATABASE', value: data.database }
|
||||
{ name: `${envPrefix}MYSQL_URL`, value: `mysql://${data.username}:${data.password}@mysql/${data.database}` },
|
||||
{ name: `${envPrefix}MYSQL_DATABASE`, value: data.database }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1189,6 +1217,7 @@ function startPostgresql(existingInfra, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const cmd = `docker run --restart=always -d --name="postgresql" \
|
||||
--hostname postgresql \
|
||||
--net cloudron \
|
||||
--net-alias postgresql \
|
||||
--log-driver syslog \
|
||||
@@ -1243,13 +1272,15 @@ function setupPostgreSql(app, options, callback) {
|
||||
if (error) return callback(new Error('Error setting up postgresql: ' + error));
|
||||
if (response.statusCode !== 201) return callback(new Error(`Error setting up postgresql. Status code: ${response.statusCode} message: ${response.body.message}`));
|
||||
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
var env = [
|
||||
{ name: 'POSTGRESQL_URL', value: `postgres://${data.username}:${data.password}@postgresql/${data.database}` },
|
||||
{ name: 'POSTGRESQL_USERNAME', value: data.username },
|
||||
{ name: 'POSTGRESQL_PASSWORD', value: data.password },
|
||||
{ name: 'POSTGRESQL_HOST', value: 'postgresql' },
|
||||
{ name: 'POSTGRESQL_PORT', value: '5432' },
|
||||
{ name: 'POSTGRESQL_DATABASE', value: data.database }
|
||||
{ name: `${envPrefix}POSTGRESQL_URL`, value: `postgres://${data.username}:${data.password}@postgresql/${data.database}` },
|
||||
{ name: `${envPrefix}POSTGRESQL_USERNAME`, value: data.username },
|
||||
{ name: `${envPrefix}POSTGRESQL_PASSWORD`, value: data.password },
|
||||
{ name: `${envPrefix}POSTGRESQL_HOST`, value: 'postgresql' },
|
||||
{ name: `${envPrefix}POSTGRESQL_PORT`, value: '5432' },
|
||||
{ name: `${envPrefix}POSTGRESQL_DATABASE`, value: data.database }
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting postgresql addon config to %j', env);
|
||||
@@ -1363,6 +1394,7 @@ function startMongodb(existingInfra, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const cmd = `docker run --restart=always -d --name="mongodb" \
|
||||
--hostname mongodb \
|
||||
--net cloudron \
|
||||
--net-alias mongodb \
|
||||
--log-driver syslog \
|
||||
@@ -1415,13 +1447,15 @@ function setupMongoDb(app, options, callback) {
|
||||
if (error) return callback(new Error('Error setting up mongodb: ' + error));
|
||||
if (response.statusCode !== 201) return callback(new Error(`Error setting up mongodb. Status code: ${response.statusCode}`));
|
||||
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
var env = [
|
||||
{ name: 'MONGODB_URL', value : `mongodb://${data.username}:${data.password}@mongodb/${data.database}` },
|
||||
{ name: 'MONGODB_USERNAME', value : data.username },
|
||||
{ name: 'MONGODB_PASSWORD', value: data.password },
|
||||
{ name: 'MONGODB_HOST', value : 'mongodb' },
|
||||
{ name: 'MONGODB_PORT', value : '27017' },
|
||||
{ name: 'MONGODB_DATABASE', value : data.database }
|
||||
{ name: `${envPrefix}MONGODB_URL`, value : `mongodb://${data.username}:${data.password}@mongodb/${data.database}` },
|
||||
{ name: `${envPrefix}MONGODB_USERNAME`, value : data.username },
|
||||
{ name: `${envPrefix}MONGODB_PASSWORD`, value: data.password },
|
||||
{ name: `${envPrefix}MONGODB_HOST`, value : 'mongodb' },
|
||||
{ name: `${envPrefix}MONGODB_PORT`, value : '27017' },
|
||||
{ name: `${envPrefix}MONGODB_DATABASE`, value : data.database }
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting mongodb addon config to %j', env);
|
||||
@@ -1557,6 +1591,7 @@ function setupRedis(app, options, callback) {
|
||||
const label = app.fqdn;
|
||||
// note that we do not add appId label because this interferes with the stop/start app logic
|
||||
const cmd = `docker run --restart=always -d --name=${redisName} \
|
||||
--hostname ${redisName} \
|
||||
--label=location=${label} \
|
||||
--net cloudron \
|
||||
--net-alias ${redisName} \
|
||||
@@ -1574,11 +1609,13 @@ function setupRedis(app, options, callback) {
|
||||
--label isCloudronManaged=true \
|
||||
--read-only -v /tmp -v /run ${tag}`;
|
||||
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
var env = [
|
||||
{ name: 'REDIS_URL', value: 'redis://redisuser:' + redisPassword + '@redis-' + app.id },
|
||||
{ name: 'REDIS_PASSWORD', value: redisPassword },
|
||||
{ name: 'REDIS_HOST', value: redisName },
|
||||
{ name: 'REDIS_PORT', value: '6379' }
|
||||
{ name: `${envPrefix}REDIS_URL`, value: 'redis://redisuser:' + redisPassword + '@redis-' + app.id },
|
||||
{ name: `${envPrefix}REDIS_PASSWORD`, value: redisPassword },
|
||||
{ name: `${envPrefix}REDIS_HOST`, value: redisName },
|
||||
{ name: `${envPrefix}REDIS_PORT`, value: '6379' }
|
||||
];
|
||||
|
||||
async.series([
|
||||
@@ -1715,3 +1752,66 @@ function restartUnbound(callback) {
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function statusNginx(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
shell.exec('statusNginx', 'systemctl is-active nginx', function (error) {
|
||||
callback(null, { status: error ? exports.SERVICE_STATUS_STOPPED : exports.SERVICE_STATUS_ACTIVE });
|
||||
});
|
||||
}
|
||||
|
||||
function restartNginx(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
shell.sudo('reloadnginx', [ path.join(__dirname, 'scripts/reloadnginx.sh') ], {}, NOOP_CALLBACK);
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
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) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
|
||||
|
||||
docker.memoryUsage('sftp', function (error, result) {
|
||||
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
|
||||
|
||||
var tmp = {
|
||||
status: container.State.Running ? exports.SERVICE_STATUS_ACTIVE : exports.SERVICE_STATUS_STOPPED,
|
||||
memoryUsed: result.memory_stats.usage,
|
||||
memoryPercent: parseInt(100 * result.memory_stats.usage / result.memory_stats.limit)
|
||||
};
|
||||
|
||||
callback(null, tmp);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function 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) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
|
||||
|
||||
request.get('http://127.0.0.1:8417', { 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}` });
|
||||
|
||||
docker.memoryUsage('graphite', function (error, result) {
|
||||
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
|
||||
|
||||
var tmp = {
|
||||
status: container.State.Running ? exports.SERVICE_STATUS_ACTIVE : exports.SERVICE_STATUS_STOPPED,
|
||||
memoryUsed: result.memory_stats.usage,
|
||||
memoryPercent: parseInt(100 * result.memory_stats.usage / result.memory_stats.limit)
|
||||
};
|
||||
|
||||
callback(null, tmp);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -90,6 +86,14 @@ server {
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade";
|
||||
proxy_hide_header Referrer-Policy;
|
||||
|
||||
# gzip responses that are > 50k and not images
|
||||
gzip on;
|
||||
gzip_min_length 50k;
|
||||
gzip_types text/css text/javascript text/xml text/plain application/javascript application/x-javascript application/json;
|
||||
|
||||
# enable for proxied requests as well
|
||||
gzip_proxied any;
|
||||
|
||||
<% 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';";
|
||||
|
||||
48
src/appdb.js
48
src/appdb.js
@@ -68,7 +68,8 @@ var assert = require('assert'),
|
||||
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.installationProgress', 'apps.runState',
|
||||
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'subdomains.subdomain AS location', 'subdomains.domain',
|
||||
'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit',
|
||||
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
|
||||
'apps.label', 'apps.tagsJson',
|
||||
'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
|
||||
'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.mailboxName', 'apps.enableAutomaticUpdate',
|
||||
'apps.dataDir', 'apps.ts', 'apps.healthTime' ].join(',');
|
||||
|
||||
@@ -95,6 +96,10 @@ function postProcess(result) {
|
||||
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;
|
||||
|
||||
assert(result.hostPorts === null || typeof result.hostPorts === 'string');
|
||||
assert(result.environmentVariables === null || typeof result.environmentVariables === 'string');
|
||||
|
||||
@@ -116,9 +121,6 @@ function postProcess(result) {
|
||||
if (result.accessRestriction && !result.accessRestriction.users) result.accessRestriction.users = [];
|
||||
delete result.accessRestrictionJson;
|
||||
|
||||
// TODO remove later once all apps have this attribute
|
||||
result.xFrameOptions = result.xFrameOptions || 'SAMEORIGIN';
|
||||
|
||||
result.sso = !!result.sso; // make it bool
|
||||
result.enableBackup = !!result.enableBackup; // make it bool
|
||||
result.enableAutomaticUpdate = !!result.enableAutomaticUpdate; // make it bool
|
||||
@@ -271,24 +273,27 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
|
||||
|
||||
var manifestJson = JSON.stringify(manifest);
|
||||
|
||||
var accessRestriction = data.accessRestriction || null;
|
||||
var accessRestrictionJson = JSON.stringify(accessRestriction);
|
||||
var memoryLimit = data.memoryLimit || 0;
|
||||
var xFrameOptions = data.xFrameOptions || '';
|
||||
var installationState = data.installationState || exports.ISTATE_PENDING_INSTALL;
|
||||
var restoreConfigJson = data.restoreConfig ? JSON.stringify(data.restoreConfig) : null; // used when cloning
|
||||
var sso = 'sso' in data ? data.sso : null;
|
||||
var robotsTxt = 'robotsTxt' in data ? data.robotsTxt : null;
|
||||
var debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null;
|
||||
var env = data.env || {};
|
||||
const accessRestriction = data.accessRestriction || null;
|
||||
const accessRestrictionJson = JSON.stringify(accessRestriction);
|
||||
const memoryLimit = data.memoryLimit || 0;
|
||||
const installationState = data.installationState || exports.ISTATE_PENDING_INSTALL;
|
||||
const restoreConfigJson = data.restoreConfig ? JSON.stringify(data.restoreConfig) : null; // used when cloning
|
||||
const sso = 'sso' in data ? data.sso : null;
|
||||
const robotsTxt = 'robotsTxt' in data ? data.robotsTxt : null;
|
||||
const debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null;
|
||||
const env = data.env || {};
|
||||
const label = data.label || null;
|
||||
const tagsJson = data.tags ? JSON.stringify(data.tags) : null;
|
||||
const mailboxName = data.mailboxName || null;
|
||||
|
||||
var queries = [];
|
||||
|
||||
queries.push({
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId, mailboxName) ' +
|
||||
' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId, mailboxName ]
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, '
|
||||
+ 'restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId, mailboxName, label, tagsJson) '
|
||||
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, restoreConfigJson,
|
||||
sso, debugModeJson, robotsTxt, ownerId, mailboxName, label, tagsJson ]
|
||||
});
|
||||
|
||||
queries.push({
|
||||
@@ -414,6 +419,7 @@ function updateWithConstraints(id, app, constraints, callback) {
|
||||
assert(!('portBindings' in app) || typeof app.portBindings === 'object');
|
||||
assert(!('accessRestriction' in app) || typeof app.accessRestriction === 'object' || app.accessRestriction === '');
|
||||
assert(!('alternateDomains' in app) || Array.isArray(app.alternateDomains));
|
||||
assert(!('tags' in app) || Array.isArray(app.tags));
|
||||
assert(!('env' in app) || typeof app.env === 'object');
|
||||
|
||||
var queries = [ ];
|
||||
@@ -452,7 +458,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 === 'accessRestriction' || p === 'debugMode') {
|
||||
if (p === 'manifest' || p === 'oldConfig' || p === 'updateConfig' || p === 'restoreConfig' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode') {
|
||||
fields.push(`${p}Json = ?`);
|
||||
values.push(JSON.stringify(app[p]));
|
||||
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'env') {
|
||||
@@ -615,13 +621,13 @@ function getAddonConfigByAppId(appId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getAppIdByAddonConfigValue(addonId, name, value, callback) {
|
||||
function getAppIdByAddonConfigValue(addonId, namePattern, value, callback) {
|
||||
assert.strictEqual(typeof addonId, 'string');
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof namePattern, 'string');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT appId FROM appAddonConfigs WHERE addonId = ? AND name = ? AND value = ?', [ addonId, name, value ], function (error, results) {
|
||||
database.query('SELECT appId FROM appAddonConfigs WHERE addonId = ? AND name LIKE ? AND value = ?', [ addonId, namePattern, value ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ var appdb = require('./appdb.js'),
|
||||
apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
auditSource = require('./auditsource.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:apphealthmonitor'),
|
||||
docker = require('./docker.js').connection,
|
||||
docker = require('./docker.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
safe = require('safetydance'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util');
|
||||
|
||||
@@ -18,11 +20,9 @@ exports = module.exports = {
|
||||
const HEALTHCHECK_INTERVAL = 10 * 1000; // every 10 seconds. this needs to be small since the UI makes only healthy apps clickable
|
||||
const UNHEALTHY_THRESHOLD = 10 * 60 * 1000; // 10 minutes
|
||||
|
||||
const OOM_MAIL_LIMIT = 60 * 60 * 1000; // 60 minutes
|
||||
const OOM_EVENT_LIMIT = 60 * 60 * 1000; // 60 minutes
|
||||
let gLastOomMailTime = Date.now() - (5 * 60 * 1000); // pretend we sent email 5 minutes ago
|
||||
|
||||
const AUDIT_SOURCE = { userId: null, username: 'healthmonitor' };
|
||||
|
||||
function debugApp(app) {
|
||||
assert(typeof app === 'object');
|
||||
|
||||
@@ -42,14 +42,14 @@ function setHealth(app, health, callback) {
|
||||
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, AUDIT_SOURCE, { app: app });
|
||||
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) {
|
||||
debugApp(app, 'marking as unhealthy since not seen for more than %s minutes', UNHEALTHY_THRESHOLD/(60 * 1000));
|
||||
|
||||
// do not send mails for dev apps
|
||||
if (!app.debugMode) eventlog.add(eventlog.ACTION_APP_DOWN, AUDIT_SOURCE, { app: app });
|
||||
if (!app.debugMode) eventlog.add(eventlog.ACTION_APP_DOWN, auditSource.HEALTH_MONITOR, { app: app });
|
||||
}
|
||||
} else {
|
||||
debugApp(app, 'waiting for %s seconds to update the app health', (UNHEALTHY_THRESHOLD - Math.abs(now - healthTime))/1000);
|
||||
@@ -77,11 +77,10 @@ function checkAppHealth(app, callback) {
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
var container = docker.getContainer(app.containerId),
|
||||
manifest = app.manifest;
|
||||
const manifest = app.manifest;
|
||||
|
||||
container.inspect(function (err, data) {
|
||||
if (err || !data || !data.State) {
|
||||
docker.inspect(app.containerId, function (error, data) {
|
||||
if (error || !data || !data.State) {
|
||||
debugApp(app, 'Error inspecting container');
|
||||
return setHealth(app, appdb.HEALTH_ERROR, callback);
|
||||
}
|
||||
@@ -116,6 +115,18 @@ function checkAppHealth(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getContainerInfo(containerId, callback) {
|
||||
docker.inspect(containerId, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const appId = safe.query(result, 'Config.Labels.appId', null);
|
||||
|
||||
if (!appId) return callback(null, null /* app */, { name: result.Name }); // addon
|
||||
|
||||
apps.get(appId, callback); // don't get by container id as this can be an exec container
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
OOM can be tested using stress tool like so:
|
||||
docker run -ti -m 100M cloudron/base:0.10.0 /bin/bash
|
||||
@@ -134,21 +145,20 @@ function processDockerEvents(intervalSecs, callback) {
|
||||
|
||||
stream.setEncoding('utf8');
|
||||
stream.on('data', function (data) {
|
||||
var ev = JSON.parse(data);
|
||||
var containerId = ev.id;
|
||||
const event = JSON.parse(data);
|
||||
const containerId = String(event.id);
|
||||
|
||||
appdb.getByContainerId(containerId, function (error, app) { // this can error for addons
|
||||
var program = error || !app.id ? containerId : `app-${app.id}`;
|
||||
var now = Date.now();
|
||||
getContainerInfo(containerId, function (error, app, addon) {
|
||||
const program = error ? containerId : (app ? app.fqdn : addon.name);
|
||||
const now = Date.now();
|
||||
const notifyUser = !(app && app.debugMode) && ((now - gLastOomMailTime) > OOM_EVENT_LIMIT);
|
||||
|
||||
const notifyUser = (!app || !app.debugMode) && (now - gLastOomMailTime > OOM_MAIL_LIMIT);
|
||||
|
||||
debug('OOM %s notifyUser: %s. lastOomTime: %s (now: %s)', program, notifyUser, gLastOomMailTime, now, ev);
|
||||
debug('OOM %s notifyUser: %s. lastOomTime: %s (now: %s)', program, notifyUser, gLastOomMailTime, now);
|
||||
|
||||
// do not send mails for dev apps
|
||||
if (notifyUser) {
|
||||
// app can be null for addon containers
|
||||
eventlog.add(eventlog.ACTION_APP_OOM, AUDIT_SOURCE, { ev: ev, containerId: containerId, app: app || null });
|
||||
eventlog.add(eventlog.ACTION_APP_OOM, auditSource.HEALTH_MONITOR, { event: event, containerId: containerId, addon: addon || null, app: app || null });
|
||||
|
||||
gLastOomMailTime = now;
|
||||
}
|
||||
|
||||
399
src/apps.js
399
src/apps.js
@@ -8,7 +8,9 @@ exports = module.exports = {
|
||||
removeRestrictedFields: removeRestrictedFields,
|
||||
|
||||
get: get,
|
||||
getByContainerId: getByContainerId,
|
||||
getByIpAddress: getByIpAddress,
|
||||
getByFqdn: getByFqdn,
|
||||
getAll: getAll,
|
||||
getAllByUser: getAllByUser,
|
||||
install: install,
|
||||
@@ -32,6 +34,7 @@ exports = module.exports = {
|
||||
|
||||
checkManifestConstraints: checkManifestConstraints,
|
||||
|
||||
canAutoupdateApp: canAutoupdateApp,
|
||||
autoupdateApps: autoupdateApps,
|
||||
|
||||
restoreInstalledApps: restoreInstalledApps,
|
||||
@@ -74,6 +77,7 @@ var appdb = require('./appdb.js'),
|
||||
fs = require('fs'),
|
||||
mail = require('./mail.js'),
|
||||
manifestFormat = require('cloudron-manifestformat'),
|
||||
once = require('once'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
@@ -86,7 +90,6 @@ var appdb = require('./appdb.js'),
|
||||
taskmanager = require('./taskmanager.js'),
|
||||
TransformStream = require('stream').Transform,
|
||||
updateChecker = require('./updatechecker.js'),
|
||||
url = require('url'),
|
||||
util = require('util'),
|
||||
uuid = require('uuid'),
|
||||
validator = require('validator'),
|
||||
@@ -121,7 +124,7 @@ AppsError.BAD_FIELD = 'Bad Field';
|
||||
AppsError.BAD_STATE = 'Bad State';
|
||||
AppsError.PORT_RESERVED = 'Port Reserved';
|
||||
AppsError.PORT_CONFLICT = 'Port Conflict';
|
||||
AppsError.BILLING_REQUIRED = 'Billing Required';
|
||||
AppsError.PLAN_LIMIT = 'Plan Limit';
|
||||
AppsError.ACCESS_DENIED = 'Access denied';
|
||||
AppsError.BAD_CERTIFICATE = 'Invalid certificate';
|
||||
|
||||
@@ -139,7 +142,8 @@ function validatePortBindings(portBindings, manifest) {
|
||||
53, /* dns */
|
||||
80, /* http */
|
||||
143, /* imap */
|
||||
202, /* caas ssh */
|
||||
202, /* alternate ssh */
|
||||
222, /* proftd */
|
||||
443, /* https */
|
||||
465, /* smtps */
|
||||
587, /* submission */
|
||||
@@ -197,21 +201,6 @@ function translatePortBindings(portBindings, manifest) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function postProcess(app) {
|
||||
let result = {};
|
||||
for (let portName in app.portBindings) {
|
||||
result[portName] = app.portBindings[portName].hostPort;
|
||||
}
|
||||
app.portBindings = result;
|
||||
}
|
||||
|
||||
function addSpacesSuffix(location, user) {
|
||||
if (user.admin || !config.isSpacesEnabled()) return location;
|
||||
|
||||
const spacesSuffix = user.username.replace(/\./g, '-');
|
||||
return location === '' ? spacesSuffix : `${location}-${spacesSuffix}`;
|
||||
}
|
||||
|
||||
function validateAccessRestriction(accessRestriction) {
|
||||
assert.strictEqual(typeof accessRestriction, 'object');
|
||||
|
||||
@@ -251,20 +240,6 @@ function validateMemoryLimit(manifest, memoryLimit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// https://tools.ietf.org/html/rfc7034
|
||||
function validateXFrameOptions(xFrameOptions) {
|
||||
assert.strictEqual(typeof xFrameOptions, 'string');
|
||||
|
||||
if (xFrameOptions === 'DENY') return null;
|
||||
if (xFrameOptions === 'SAMEORIGIN') return null;
|
||||
|
||||
var parts = xFrameOptions.split(' ');
|
||||
if (parts.length !== 2 || parts[0] !== 'ALLOW-FROM') return new AppsError(AppsError.BAD_FIELD, 'xFrameOptions must be "DENY", "SAMEORIGIN" or "ALLOW-FROM uri"' );
|
||||
|
||||
var uri = url.parse(parts[1]);
|
||||
return (uri.protocol === 'http:' || uri.protocol === 'https:') ? null : new AppsError(AppsError.BAD_FIELD, 'xFrameOptions ALLOW-FROM uri must be a valid http[s] uri' );
|
||||
}
|
||||
|
||||
function validateDebugMode(debugMode) {
|
||||
assert.strictEqual(typeof debugMode, 'object');
|
||||
|
||||
@@ -287,11 +262,31 @@ function validateRobotsTxt(robotsTxt) {
|
||||
}
|
||||
|
||||
function validateBackupFormat(format) {
|
||||
assert.strictEqual(typeof format, 'string');
|
||||
|
||||
if (format === 'tgz' || format == 'rsync') return null;
|
||||
|
||||
return new AppsError(AppsError.BAD_FIELD, 'Invalid backup format');
|
||||
}
|
||||
|
||||
function validateLabel(label) {
|
||||
if (label === null) return null;
|
||||
|
||||
if (label.length > 128) return new AppsError(AppsError.BAD_FIELD, 'label must be less than 128');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateTags(tags) {
|
||||
if (!Array.isArray(tags)) return new AppsError(AppsError.BAD_FIELD, 'tags must be an array of strings');
|
||||
if (tags.length > 64) return new AppsError(AppsError.BAD_FIELD, 'Can only set up to 64 tags');
|
||||
|
||||
if (tags.some(tag => (!tag || typeof tag !== 'string'))) return new AppsError(AppsError.BAD_FIELD, 'tags must be an array of non-empty strings');
|
||||
if (tags.some(tag => tag.length > 128)) return new AppsError(AppsError.BAD_FIELD, 'tag must be less than 128');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateEnv(env) {
|
||||
for (let key in env) {
|
||||
if (key.length > 512) return new AppsError(AppsError.BAD_FIELD, 'Max env var key length is 512');
|
||||
@@ -326,10 +321,12 @@ function validateDataDir(dataDir) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function getDuplicateErrorDetails(location, portBindings, error) {
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
function getDuplicateErrorDetails(error, location, domainObject, portBindings, alternateDomains) {
|
||||
assert.strictEqual(error.reason, DatabaseError.ALREADY_EXISTS);
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
assert(Array.isArray(alternateDomains));
|
||||
|
||||
var match = error.message.match(/ER_DUP_ENTRY: Duplicate entry '(.*)' for key '(.*)'/);
|
||||
if (!match) {
|
||||
@@ -337,8 +334,18 @@ function getDuplicateErrorDetails(location, portBindings, error) {
|
||||
return new AppsError(AppsError.INTERNAL_ERROR, error);
|
||||
}
|
||||
|
||||
// check if the location conflicts
|
||||
if (match[1] === location) return new AppsError(AppsError.ALREADY_EXISTS, `${match[2]} '${match[1]}' is in use`);
|
||||
// check if the location or alternateDomains conflicts
|
||||
if (match[2] === 'subdomain') {
|
||||
// mysql reports a unique conflict with a dash: eg. domain:example.com subdomain:test => test-example.com
|
||||
if (match[1] === `${location}-${domainObject.domain}`) return new AppsError(AppsError.ALREADY_EXISTS, `Domain '${domains.fqdn(location, domainObject)}' is in use`);
|
||||
|
||||
// check alternateDomains
|
||||
let tmp = alternateDomains.filter(function (d) {
|
||||
return match[1] === `${d.subdomain}-${d.domain}`;
|
||||
});
|
||||
|
||||
if (tmp.length > 0) return new AppsError(AppsError.ALREADY_EXISTS, `Alternate domain '${tmp[0].subdomain}.${tmp[0].domain}' is in use`);
|
||||
}
|
||||
|
||||
// check if any of the port bindings conflict
|
||||
for (let portName in portBindings) {
|
||||
@@ -358,7 +365,7 @@ function getAppConfig(app) {
|
||||
accessRestriction: app.accessRestriction,
|
||||
portBindings: app.portBindings,
|
||||
memoryLimit: app.memoryLimit,
|
||||
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN',
|
||||
|
||||
robotsTxt: app.robotsTxt,
|
||||
sso: app.sso,
|
||||
alternateDomains: app.alternateDomains || [],
|
||||
@@ -375,21 +382,40 @@ function removeInternalFields(app) {
|
||||
return _.pick(app,
|
||||
'id', 'appStoreId', 'installationState', 'installationProgress', 'runState', 'health',
|
||||
'location', 'domain', 'fqdn', 'mailboxName',
|
||||
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'xFrameOptions',
|
||||
'sso', 'debugMode', 'robotsTxt', 'enableBackup', 'creationTime', 'updateTime', 'ts',
|
||||
'alternateDomains', 'ownerId', 'env', 'enableAutomaticUpdate', 'dataDir');
|
||||
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit',
|
||||
'sso', 'debugMode', 'robotsTxt', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags',
|
||||
'label', 'alternateDomains', 'ownerId', 'env', 'enableAutomaticUpdate', 'dataDir');
|
||||
}
|
||||
|
||||
// non-admins can only see these
|
||||
function removeRestrictedFields(app) {
|
||||
return _.pick(app,
|
||||
'id', 'appStoreId', 'installationState', 'installationProgress', 'runState', 'health', 'ownerId',
|
||||
'location', 'domain', 'fqdn', 'manifest', 'portBindings', 'iconUrl', 'creationTime', 'ts');
|
||||
'location', 'domain', 'fqdn', 'manifest', 'portBindings', 'iconUrl', 'creationTime', 'ts', 'tags', 'label');
|
||||
}
|
||||
|
||||
function getIconUrlSync(app) {
|
||||
var iconPath = paths.APP_ICONS_DIR + '/' + app.id + '.png';
|
||||
return fs.existsSync(iconPath) ? '/api/v1/apps/' + app.id + '/icon' : null;
|
||||
const iconUrl = '/api/v1/apps/' + app.id + '/icon';
|
||||
|
||||
const userIconPath = `${paths.APP_ICONS_DIR}/${app.id}.user.png`;
|
||||
if (safe.fs.existsSync(userIconPath)) return iconUrl;
|
||||
|
||||
const appstoreIconPath = `${paths.APP_ICONS_DIR}/${app.id}.png`;
|
||||
if (safe.fs.existsSync(appstoreIconPath)) return iconUrl;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function postProcess(app, domainObjectMap) {
|
||||
let result = {};
|
||||
for (let portName in app.portBindings) {
|
||||
result[portName] = app.portBindings[portName].hostPort;
|
||||
}
|
||||
app.portBindings = result;
|
||||
|
||||
app.iconUrl = getIconUrlSync(app);
|
||||
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
|
||||
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
|
||||
}
|
||||
|
||||
function hasAccessTo(app, user, callback) {
|
||||
@@ -411,25 +437,49 @@ function hasAccessTo(app, user, callback) {
|
||||
callback(null, false);
|
||||
}
|
||||
|
||||
function get(appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
function getDomainObjectMap(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
domaindb.getAll(function (error, domainObjects) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
let domainObjectMap = {};
|
||||
for (let d of domainObjects) { domainObjectMap[d.domain] = d; }
|
||||
|
||||
callback(null, domainObjectMap);
|
||||
});
|
||||
}
|
||||
|
||||
function get(appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getDomainObjectMap(function (error, domainObjectMap) {
|
||||
if (error) return callback(error);
|
||||
|
||||
appdb.get(appId, function (error, app) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
postProcess(app);
|
||||
postProcess(app, domainObjectMap);
|
||||
|
||||
let domainObjectMap = {};
|
||||
for (let d of domainObjects) { domainObjectMap[d.domain] = d; }
|
||||
callback(null, app);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
app.iconUrl = getIconUrlSync(app);
|
||||
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
|
||||
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
|
||||
function getByContainerId(containerId, callback) {
|
||||
assert.strictEqual(typeof containerId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getDomainObjectMap(function (error, domainObjectMap) {
|
||||
if (error) return callback(error);
|
||||
|
||||
appdb.getByContainerId(containerId, function (error, app) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
postProcess(app, domainObjectMap);
|
||||
|
||||
callback(null, app);
|
||||
});
|
||||
@@ -440,63 +490,39 @@ function getByIpAddress(ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
domaindb.getAll(function (error, domainObjects) {
|
||||
docker.getContainerIdByIp(ip, function (error, containerId) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
let domainObjectMap = {};
|
||||
for (let d of domainObjects) { domainObjectMap[d.domain] = d; }
|
||||
getByContainerId(containerId, callback);
|
||||
});
|
||||
}
|
||||
|
||||
docker.getContainerIdByIp(ip, function (error, containerId) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
function getByFqdn(fqdn, callback) {
|
||||
assert.strictEqual(typeof fqdn, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
appdb.getByContainerId(containerId, function (error, app) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
getAll(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
postProcess(app);
|
||||
var app = result.find(function (a) { return a.fqdn === fqdn; });
|
||||
if (!app) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
|
||||
domaindb.getAll(function (error, domainObjects) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
let domainObjectMap = {};
|
||||
for (let d of domainObjects) { domainObjectMap[d.domain] = d; }
|
||||
|
||||
app.iconUrl = getIconUrlSync(app);
|
||||
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
|
||||
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
|
||||
|
||||
callback(null, app);
|
||||
});
|
||||
});
|
||||
});
|
||||
callback(null, app);
|
||||
});
|
||||
}
|
||||
|
||||
function getAll(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
domaindb.getAll(function (error, domainObjects) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
let domainObjectMap = {};
|
||||
for (let d of domainObjects) { domainObjectMap[d.domain] = d; }
|
||||
getDomainObjectMap(function (error, domainObjectMap) {
|
||||
if (error) return callback(error);
|
||||
|
||||
appdb.getAll(function (error, apps) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
apps.forEach(postProcess);
|
||||
apps.forEach((app) => postProcess(app, domainObjectMap));
|
||||
|
||||
async.eachSeries(apps, function (app, iteratorDone) {
|
||||
app.iconUrl = getIconUrlSync(app);
|
||||
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
|
||||
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
|
||||
|
||||
iteratorDone(null, app);
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, apps);
|
||||
});
|
||||
callback(null, apps);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -552,7 +578,6 @@ function install(data, user, auditSource, callback) {
|
||||
cert = data.cert || null,
|
||||
key = data.key || null,
|
||||
memoryLimit = data.memoryLimit || 0,
|
||||
xFrameOptions = data.xFrameOptions || 'SAMEORIGIN',
|
||||
sso = 'sso' in data ? data.sso : null,
|
||||
debugMode = data.debugMode || null,
|
||||
robotsTxt = data.robotsTxt || null,
|
||||
@@ -563,7 +588,9 @@ function install(data, user, auditSource, callback) {
|
||||
ownerId = data.ownerId,
|
||||
alternateDomains = data.alternateDomains || [],
|
||||
env = data.env || {},
|
||||
mailboxName = data.mailboxName || '';
|
||||
mailboxName = data.mailboxName || '',
|
||||
label = data.label || null,
|
||||
tags = data.tags || [];
|
||||
|
||||
assert(data.appStoreId || data.manifest); // atleast one of them is required
|
||||
|
||||
@@ -585,9 +612,6 @@ function install(data, user, auditSource, callback) {
|
||||
error = validateMemoryLimit(manifest, memoryLimit);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validateXFrameOptions(xFrameOptions);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validateDebugMode(debugMode);
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -597,6 +621,12 @@ function install(data, user, auditSource, callback) {
|
||||
error = validateBackupFormat(backupFormat);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validateLabel(label);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validateTags(tags);
|
||||
if (error) return callback(error);
|
||||
|
||||
if ('sso' in data && !('optionalSso' in manifest)) return callback(new AppsError(AppsError.BAD_FIELD, 'sso can only be specified for apps with optionalSso'));
|
||||
// if sso was unspecified, enable it by default if possible
|
||||
if (sso === null) sso = !!manifest.addons['ldap'] || !!manifest.addons['oauth'];
|
||||
@@ -616,7 +646,7 @@ function install(data, user, auditSource, callback) {
|
||||
if (icon) {
|
||||
if (!validator.isBase64(icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.png'), new Buffer(icon, 'base64'))) {
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.png'), Buffer.from(icon, 'base64'))) {
|
||||
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
|
||||
}
|
||||
}
|
||||
@@ -625,9 +655,6 @@ function install(data, user, auditSource, callback) {
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
|
||||
|
||||
location = addSpacesSuffix(location, user);
|
||||
alternateDomains.forEach(function (ad) { ad.subdomain = addSpacesSuffix(ad.subdomain, user); }); // TODO: validate these
|
||||
|
||||
error = domains.validateHostname(location, domainObject);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message));
|
||||
|
||||
@@ -641,7 +668,6 @@ function install(data, user, auditSource, callback) {
|
||||
var data = {
|
||||
accessRestriction: accessRestriction,
|
||||
memoryLimit: memoryLimit,
|
||||
xFrameOptions: xFrameOptions,
|
||||
sso: sso,
|
||||
debugMode: debugMode,
|
||||
mailboxName: mailboxName,
|
||||
@@ -654,25 +680,12 @@ function install(data, user, auditSource, callback) {
|
||||
};
|
||||
|
||||
appdb.add(appId, appStoreId, manifest, location, domain, ownerId, translatePortBindings(portBindings, manifest), data, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, location, domainObject, portBindings, data.alternateDomains));
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, error.message));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
appstore.purchase(appId, { appstoreId: appStoreId, manifestId: manifest.id }, function (appstoreError) {
|
||||
// if purchase failed, rollback the appdb record
|
||||
if (appstoreError) {
|
||||
appdb.del(appId, function (error) {
|
||||
if (error) debug('install: Failed to rollback app installation.', error);
|
||||
|
||||
if (appstoreError.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, appstoreError.message));
|
||||
if (appstoreError && appstoreError.reason === AppstoreError.BILLING_REQUIRED) return callback(new AppsError(AppsError.BILLING_REQUIRED, appstoreError.message));
|
||||
if (appstoreError && appstoreError.reason === AppstoreError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, appstoreError.message));
|
||||
|
||||
callback(new AppsError(AppsError.INTERNAL_ERROR, appstoreError));
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
purchaseApp({ appId: appId, appstoreId: appStoreId, manifestId: manifest.id }, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// save cert to boxdata/certs
|
||||
if (cert && key) {
|
||||
@@ -736,12 +749,6 @@ function configure(appId, data, user, auditSource, callback) {
|
||||
if (error) return callback(error);
|
||||
}
|
||||
|
||||
if ('xFrameOptions' in data) {
|
||||
values.xFrameOptions = data.xFrameOptions;
|
||||
error = validateXFrameOptions(values.xFrameOptions);
|
||||
if (error) return callback(error);
|
||||
}
|
||||
|
||||
if ('debugMode' in data) {
|
||||
values.debugMode = data.debugMode;
|
||||
error = validateDebugMode(values.debugMode);
|
||||
@@ -769,7 +776,6 @@ function configure(appId, data, user, auditSource, callback) {
|
||||
if ('alternateDomains' in data) {
|
||||
// TODO validate all subdomains [{ domain: '', subdomain: ''}]
|
||||
values.alternateDomains = data.alternateDomains;
|
||||
values.alternateDomains.forEach(function (ad) { ad.subdomain = addSpacesSuffix(ad.subdomain, user); }); // TODO: validate these
|
||||
}
|
||||
|
||||
if ('env' in data) {
|
||||
@@ -784,12 +790,34 @@ function configure(appId, data, user, auditSource, callback) {
|
||||
values.dataDir = data.dataDir;
|
||||
}
|
||||
|
||||
if ('label' in data) {
|
||||
error = validateLabel(data.label);
|
||||
if (error) return callback(error);
|
||||
values.label = data.label;
|
||||
}
|
||||
|
||||
if ('tags' in data) {
|
||||
error = validateTags(data.tags);
|
||||
if (error) return callback(error);
|
||||
values.tags = data.tags;
|
||||
}
|
||||
|
||||
if ('icon' in data) {
|
||||
if (data.icon) {
|
||||
if (!validator.isBase64(data.icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png'), Buffer.from(data.icon, 'base64'))) {
|
||||
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
|
||||
}
|
||||
} else {
|
||||
safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png'));
|
||||
}
|
||||
}
|
||||
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
|
||||
|
||||
location = addSpacesSuffix(location, user);
|
||||
|
||||
error = domains.validateHostname(location, domainObject);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message));
|
||||
|
||||
@@ -809,10 +837,10 @@ function configure(appId, data, user, auditSource, callback) {
|
||||
|
||||
values.oldConfig = getAppConfig(app);
|
||||
|
||||
debug('Will configure app with id:%s values:%j', appId, values);
|
||||
debug(`configure: id:${appId}`);
|
||||
|
||||
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_CONFIGURE, values, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, location, domainObject, portBindings, data.alternateDomains));
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -837,7 +865,7 @@ function update(appId, data, auditSource, callback) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('Will update app with id:%s', appId);
|
||||
debug(`update: id:${appId}`);
|
||||
|
||||
get(appId, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
@@ -874,11 +902,11 @@ function update(appId, data, auditSource, callback) {
|
||||
if (data.icon) {
|
||||
if (!validator.isBase64(data.icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.png'), new Buffer(data.icon, 'base64'))) {
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png'), Buffer.from(data.icon, 'base64'))) {
|
||||
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
|
||||
}
|
||||
} else {
|
||||
safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, appId + '.png'));
|
||||
safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1008,6 +1036,29 @@ function restore(appId, data, auditSource, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function purchaseApp(data, callback) {
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
appstore.purchaseApp(data, function (error) {
|
||||
if (!error) return callback();
|
||||
|
||||
// if purchase failed, rollback the appdb record
|
||||
appdb.del(data.appId, function (delError) {
|
||||
if (delError) debug('install: Failed to rollback app installation.', delError);
|
||||
|
||||
if (error.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, error.message));
|
||||
if (error && error.reason === AppstoreError.PLAN_LIMIT) return callback(new AppsError(AppsError.PLAN_LIMIT, error.message));
|
||||
if (error && error.reason === AppstoreError.INVALID_TOKEN) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
|
||||
if (error && error.reason === AppstoreError.LICENSE_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
|
||||
if (error && error.reason === AppstoreError.NOT_REGISTERED) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
|
||||
if (error && error.reason === AppstoreError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
|
||||
|
||||
callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function clone(appId, data, user, auditSource, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
@@ -1060,7 +1111,6 @@ function clone(appId, data, user, auditSource, callback) {
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'No such domain'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
|
||||
|
||||
location = addSpacesSuffix(location, user);
|
||||
error = domains.validateHostname(location, domainObject);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message));
|
||||
|
||||
@@ -1070,7 +1120,6 @@ function clone(appId, data, user, auditSource, callback) {
|
||||
installationState: appdb.ISTATE_PENDING_CLONE,
|
||||
memoryLimit: app.memoryLimit,
|
||||
accessRestriction: app.accessRestriction,
|
||||
xFrameOptions: app.xFrameOptions,
|
||||
restoreConfig: { backupId: backupId, backupFormat: backupInfo.format },
|
||||
sso: !!app.sso,
|
||||
mailboxName: mailboxName,
|
||||
@@ -1080,24 +1129,11 @@ function clone(appId, data, user, auditSource, callback) {
|
||||
};
|
||||
|
||||
appdb.add(newAppId, app.appStoreId, manifest, location, domain, ownerId, translatePortBindings(portBindings, manifest), data, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, location, domainObject, portBindings, []));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
appstore.purchase(newAppId, { appstoreId: app.appStoreId, manifestId: manifest.id }, function (appstoreError) {
|
||||
// if purchase failed, rollback the appdb record
|
||||
if (appstoreError) {
|
||||
appdb.del(newAppId, function (error) {
|
||||
if (error) debug('install: Failed to rollback app installation.', error);
|
||||
|
||||
if (appstoreError.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, appstoreError.message));
|
||||
if (appstoreError && appstoreError.reason === AppstoreError.BILLING_REQUIRED) return callback(new AppsError(AppsError.BILLING_REQUIRED, appstoreError.message));
|
||||
if (appstoreError && appstoreError.reason === AppstoreError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, appstoreError.message));
|
||||
|
||||
callback(new AppsError(AppsError.INTERNAL_ERROR, appstoreError));
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
purchaseApp({ appId: newAppId, appstoreId: app.appStoreId, manifestId: manifest.id }, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
taskmanager.restartAppTask(newAppId);
|
||||
|
||||
@@ -1126,9 +1162,10 @@ function uninstall(appId, auditSource, callback) {
|
||||
get(appId, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
|
||||
appstore.unpurchase(appId, { appstoreId: app.appStoreId, manifestId: app.manifest.id }, function (error) {
|
||||
appstore.unpurchaseApp(appId, { appstoreId: app.appStoreId, manifestId: app.manifest.id }, function (error) {
|
||||
if (error && error.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
if (error && error.reason === AppstoreError.BILLING_REQUIRED) return callback(new AppsError(AppsError.BILLING_REQUIRED, error.message));
|
||||
if (error && error.reason === AppstoreError.INVALID_TOKEN) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
|
||||
if (error && error.reason === AppstoreError.LICENSE_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
|
||||
if (error && error.reason === AppstoreError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -1181,6 +1218,8 @@ function stop(appId, callback) {
|
||||
function checkManifestConstraints(manifest) {
|
||||
assert(manifest && typeof manifest === 'object');
|
||||
|
||||
if (manifest.manifestVersion > 2) return new AppsError(AppsError.BAD_FIELD, 'Manifest version must be <= 2');
|
||||
|
||||
if (!manifest.dockerImage) return new AppsError(AppsError.BAD_FIELD, 'Missing dockerImage'); // dockerImage is optional in manifest
|
||||
|
||||
if (semver.valid(manifest.maxBoxVersion) && semver.gt(config.version(), manifest.maxBoxVersion)) {
|
||||
@@ -1258,27 +1297,27 @@ function exec(appId, options, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function canAutoupdateApp(app, newManifest) {
|
||||
if (!app.enableAutomaticUpdate) return new Error('Automatic update disabled');
|
||||
if ((semver.major(app.manifest.version) !== 0) && (semver.major(app.manifest.version) !== semver.major(newManifest.version))) return new Error('Major version change'); // major changes are blocking
|
||||
|
||||
const newTcpPorts = newManifest.tcpPorts || { };
|
||||
const newUdpPorts = newManifest.udpPorts || { };
|
||||
const portBindings = app.portBindings; // this is never null
|
||||
|
||||
for (let portName in portBindings) {
|
||||
if (!(portName in newTcpPorts) && !(portName in newUdpPorts)) return new Error(`${portName} was in use but new update removes it`);
|
||||
}
|
||||
|
||||
// it's fine if one or more (unused) keys got removed
|
||||
return null;
|
||||
}
|
||||
|
||||
function autoupdateApps(updateInfo, auditSource, callback) { // updateInfo is { appId -> { manifest } }
|
||||
assert.strictEqual(typeof updateInfo, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
function canAutoupdateApp(app, newManifest) {
|
||||
if (!app.enableAutomaticUpdate) return new Error('Automatic update disabled');
|
||||
if ((semver.major(app.manifest.version) !== 0) && (semver.major(app.manifest.version) !== semver.major(newManifest.version))) return new Error('Major version change'); // major changes are blocking
|
||||
|
||||
const newTcpPorts = newManifest.tcpPorts || { };
|
||||
const newUdpPorts = newManifest.udpPorts || { };
|
||||
const portBindings = app.portBindings; // this is never null
|
||||
|
||||
for (let portName in portBindings) {
|
||||
if (!(portName in newTcpPorts) && !(portName in newUdpPorts)) return new Error(`${portName} was in use but new update removes it`);
|
||||
}
|
||||
|
||||
// it's fine if one or more (unused) keys got removed
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!updateInfo) return callback(null);
|
||||
|
||||
async.eachSeries(Object.keys(updateInfo), function iterator(appId, iteratorDone) {
|
||||
@@ -1392,6 +1431,7 @@ function downloadFile(appId, filePath, callback) {
|
||||
assert.strictEqual(typeof filePath, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`downloadFile: ${filePath}`); // no need to escape filePath because we don't rely on bash
|
||||
exec(appId, { cmd: [ 'stat', '--printf=%F-%s', filePath ], tty: true }, function (error, stream) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -1424,7 +1464,7 @@ function downloadFile(appId, filePath, callback) {
|
||||
transform: function (chunk, ignoredEncoding, callback) {
|
||||
this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
|
||||
|
||||
while (true) {
|
||||
for (;;) {
|
||||
if (this._buffer.length < 8) break; // header is 8 bytes
|
||||
|
||||
var type = this._buffer.readUInt8(0);
|
||||
@@ -1457,15 +1497,26 @@ function uploadFile(appId, sourceFilePath, destFilePath, callback) {
|
||||
assert.strictEqual(typeof destFilePath, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
exec(appId, { cmd: [ 'bash', '-c', 'cat - > ' + destFilePath ], tty: false }, function (error, stream) {
|
||||
if (error) return callback(error);
|
||||
const done = once(function (error) {
|
||||
safe.fs.unlinkSync(sourceFilePath); // remove file in /tmp
|
||||
callback(error);
|
||||
});
|
||||
|
||||
// the built-in bash printf understands "%q" but not /usr/bin/printf.
|
||||
// ' gets replaced with '\'' . the first closes the quote and last one starts a new one
|
||||
const escapedDestFilePath = safe.child_process.execSync(`printf %q '${destFilePath.replace(/'/g, '\'\\\'\'')}'`, { shell: '/bin/bash', encoding: 'utf8' });
|
||||
debug(`uploadFile: ${sourceFilePath} -> ${escapedDestFilePath}`);
|
||||
|
||||
exec(appId, { cmd: [ 'bash', '-c', `cat - > ${escapedDestFilePath}` ], tty: false }, function (error, stream) {
|
||||
if (error) return done(error);
|
||||
|
||||
var readFile = fs.createReadStream(sourceFilePath);
|
||||
readFile.on('error', callback);
|
||||
readFile.on('error', done);
|
||||
|
||||
stream.on('error', done);
|
||||
stream.on('finish', done);
|
||||
|
||||
readFile.pipe(stream);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
296
src/appstore.js
296
src/appstore.js
@@ -1,8 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
purchase: purchase,
|
||||
unpurchase: unpurchase,
|
||||
getApps: getApps,
|
||||
getApp: getApp,
|
||||
getAppVersion: getAppVersion,
|
||||
|
||||
registerWithLoginCredentials: registerWithLoginCredentials,
|
||||
registerWithLicense: registerWithLicense,
|
||||
|
||||
purchaseApp: purchaseApp,
|
||||
unpurchaseApp: unpurchaseApp,
|
||||
|
||||
getSubscription: getSubscription,
|
||||
isFreePlan: isFreePlan,
|
||||
@@ -12,9 +19,7 @@ exports = module.exports = {
|
||||
getAppUpdate: getAppUpdate,
|
||||
getBoxUpdate: getBoxUpdate,
|
||||
|
||||
getAccount: getAccount,
|
||||
|
||||
sendFeedback: sendFeedback,
|
||||
createTicket: createTicket,
|
||||
|
||||
AppstoreError: AppstoreError
|
||||
};
|
||||
@@ -23,6 +28,7 @@ var apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
custom = require('./custom.js'),
|
||||
debug = require('debug')('box:appstore'),
|
||||
domains = require('./domains.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
@@ -55,37 +61,85 @@ function AppstoreError(reason, errorOrMessage) {
|
||||
util.inherits(AppstoreError, Error);
|
||||
AppstoreError.INTERNAL_ERROR = 'Internal Error';
|
||||
AppstoreError.EXTERNAL_ERROR = 'External Error';
|
||||
AppstoreError.NOT_FOUND = 'Internal Error';
|
||||
AppstoreError.BILLING_REQUIRED = 'Billing Required';
|
||||
AppstoreError.ALREADY_EXISTS = 'Already Exists';
|
||||
AppstoreError.ACCESS_DENIED = 'Access Denied';
|
||||
AppstoreError.NOT_FOUND = 'Not Found';
|
||||
AppstoreError.PLAN_LIMIT = 'Plan limit reached'; // upstream 402 (subsciption_expired and subscription_required)
|
||||
AppstoreError.LICENSE_ERROR = 'License Error'; // upstream 422 (no license, invalid license)
|
||||
AppstoreError.INVALID_TOKEN = 'Invalid token'; // upstream 401 (invalid token)
|
||||
AppstoreError.NOT_REGISTERED = 'Not registered'; // upstream 412 (no token, not set yet)
|
||||
AppstoreError.ALREADY_REGISTERED = 'Already registered';
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
function getAppstoreConfig(callback) {
|
||||
function getCloudronToken(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.getAppstoreConfig(function (error, result) {
|
||||
settings.getCloudronToken(function (error, token) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
if (!result.token) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
|
||||
if (!token) return callback(new AppstoreError(AppstoreError.NOT_REGISTERED));
|
||||
|
||||
callback(null, result);
|
||||
callback(null, token);
|
||||
});
|
||||
}
|
||||
|
||||
function login(email, password, totpToken, callback) {
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof totpToken, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var data = {
|
||||
email: email,
|
||||
password: password,
|
||||
totpToken: totpToken
|
||||
};
|
||||
|
||||
const url = config.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));
|
||||
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, `login status code: ${result.statusCode}`));
|
||||
|
||||
callback(null, result.body); // { userId, accessToken }
|
||||
});
|
||||
}
|
||||
|
||||
function registerUser(email, password, callback) {
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var data = {
|
||||
email: email,
|
||||
password: password,
|
||||
};
|
||||
|
||||
const url = config.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));
|
||||
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, `register status code: ${result.statusCode}`));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function getSubscription(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/subscription';
|
||||
superagent.get(url).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
const url = config.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.EXTERNAL_ERROR, 'invalid appstore token'));
|
||||
if (result.statusCode === 403) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, 'wrong user'));
|
||||
if (result.statusCode === 502) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, 'stripe error'));
|
||||
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, 'unknown error'));
|
||||
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
|
||||
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR));
|
||||
if (result.statusCode === 502) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, `Stripe error: ${error.message}`));
|
||||
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, `Unknown error: ${error.message}`));
|
||||
|
||||
callback(null, result.body.subscription);
|
||||
callback(null, result.body); // { email, subscription }
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -95,22 +149,24 @@ function isFreePlan(subscription) {
|
||||
}
|
||||
|
||||
// See app.js install it will create a db record first but remove it again if appstore purchase fails
|
||||
function purchase(appId, data, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
function purchaseApp(data, callback) {
|
||||
assert.strictEqual(typeof data, 'object'); // { appstoreId, manifestId, appId }
|
||||
assert(data.appstoreId || data.manifestId);
|
||||
assert.strictEqual(typeof data.appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/apps/' + appId;
|
||||
const url = `${config.apiServerOrigin()}/api/v1/cloudronapps`;
|
||||
|
||||
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
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));
|
||||
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
|
||||
if (result.statusCode === 402) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED, result.body.message));
|
||||
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND)); // appstoreId does not exist
|
||||
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
|
||||
if (result.statusCode === 402) return callback(new AppstoreError(AppstoreError.PLAN_LIMIT, result.body.message));
|
||||
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR, result.body.message));
|
||||
// 200 if already purchased, 201 is newly purchased
|
||||
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
|
||||
|
||||
callback(null);
|
||||
@@ -118,26 +174,27 @@ function purchase(appId, data, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function unpurchase(appId, data, callback) {
|
||||
function unpurchaseApp(appId, data, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
assert.strictEqual(typeof data, 'object'); // { appstoreId, manifestId }
|
||||
assert(data.appstoreId || data.manifestId);
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/apps/' + appId;
|
||||
const url = `${config.apiServerOrigin()}/api/v1/cloudronapps/${appId}`;
|
||||
|
||||
superagent.get(url).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
|
||||
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 === 404) return callback(null); // was never purchased
|
||||
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));
|
||||
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
|
||||
|
||||
superagent.del(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
superagent.del(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 === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
|
||||
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
|
||||
if (result.statusCode !== 204) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
|
||||
|
||||
callback(null);
|
||||
@@ -217,13 +274,15 @@ function sendAliveStatus(callback) {
|
||||
}
|
||||
};
|
||||
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/alive';
|
||||
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
const url = `${config.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));
|
||||
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));
|
||||
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Sending alive status failed. %s %j', result.status, result.body)));
|
||||
|
||||
callback(null);
|
||||
@@ -235,13 +294,15 @@ function sendAliveStatus(callback) {
|
||||
function getBoxUpdate(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/boxupdate';
|
||||
const url = `${config.apiServerOrigin()}/api/v1/boxupdate`;
|
||||
|
||||
superagent.get(url).query({ accessToken: appstoreConfig.token, boxVersion: config.version() }).timeout(10 * 1000).end(function (error, result) {
|
||||
superagent.get(url).query({ accessToken: token, boxVersion: config.version() }).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
|
||||
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR, result.body.message));
|
||||
if (result.statusCode === 204) return callback(null); // no update
|
||||
if (result.statusCode !== 200 || !result.body) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
|
||||
@@ -251,7 +312,14 @@ function getBoxUpdate(callback) {
|
||||
return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Invalid update version: %s %s', result.statusCode, result.text)));
|
||||
}
|
||||
|
||||
// updateInfo: { version, changelog, upgrade, sourceTarballUrl, sourceTarballSigUrl, boxVersionsUrl, boxVersionsSigUrl }
|
||||
// updateInfo: { version, changelog, sourceTarballUrl, sourceTarballSigUrl, boxVersionsUrl, boxVersionsSigUrl }
|
||||
if (!updateInfo.version || typeof updateInfo.version !== 'string') return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response (bad version): %s %s', result.statusCode, result.text)));
|
||||
if (!updateInfo.changelog || !Array.isArray(updateInfo.changelog)) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response (bad version): %s %s', result.statusCode, result.text)));
|
||||
if (!updateInfo.sourceTarballUrl || typeof updateInfo.sourceTarballUrl !== 'string') return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response (bad sourceTarballUrl): %s %s', result.statusCode, result.text)));
|
||||
if (!updateInfo.sourceTarballSigUrl || typeof updateInfo.sourceTarballSigUrl !== 'string') return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response (bad sourceTarballSigUrl): %s %s', result.statusCode, result.text)));
|
||||
if (!updateInfo.boxVersionsUrl || typeof updateInfo.boxVersionsUrl !== 'string') return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response (bad boxVersionsUrl): %s %s', result.statusCode, result.text)));
|
||||
if (!updateInfo.boxVersionsSigUrl || typeof updateInfo.boxVersionsSigUrl !== 'string') return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response (bad boxVersionsSigUrl): %s %s', result.statusCode, result.text)));
|
||||
|
||||
callback(null, updateInfo);
|
||||
});
|
||||
});
|
||||
@@ -261,13 +329,15 @@ function getAppUpdate(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/appupdate';
|
||||
const url = `${config.apiServerOrigin()}/api/v1/appupdate`;
|
||||
|
||||
superagent.get(url).query({ accessToken: appstoreConfig.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: config.version(), appId: app.appStoreId, appVersion: app.manifest.version }).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
|
||||
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR, result.body.message));
|
||||
if (result.statusCode === 204) return callback(null); // no update
|
||||
if (result.statusCode !== 200 || !result.body) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
|
||||
@@ -288,25 +358,73 @@ function getAppUpdate(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getAccount(callback) {
|
||||
function registerCloudron(data, callback) {
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
if (error) return callback(error);
|
||||
const url = `${config.apiServerOrigin()}/api/v1/register_cloudron`;
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId;
|
||||
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 !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, `Unable to register cloudron: ${error.message}`));
|
||||
|
||||
superagent.get(url).query({ accessToken: appstoreConfig.token }).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
// cloudronId, token, licenseKey
|
||||
if (!result.body.cloudronId) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, 'Invalid response - no cloudron id'));
|
||||
if (!result.body.cloudronToken) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, 'Invalid response - no token'));
|
||||
if (!result.body.licenseKey) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, 'Invalid response - no license'));
|
||||
|
||||
// { profile: { id, email, groupId, billing, firstName, lastName, company, street, city, zip, state, country } }
|
||||
callback(null, result.body.profile);
|
||||
async.series([
|
||||
settings.setCloudronId.bind(null, result.body.cloudronId),
|
||||
settings.setCloudronToken.bind(null, result.body.cloudronToken),
|
||||
settings.setLicenseKey.bind(null, result.body.licenseKey),
|
||||
], function (error) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
|
||||
debug(`registerCloudron: Cloudron registered with id ${result.body.cloudronId}`);
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sendFeedback(info, 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, domain }, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function registerWithLoginCredentials(options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
function maybeSignup(done) {
|
||||
if (!options.signup) return done();
|
||||
|
||||
registerUser(options.email, options.password, done);
|
||||
}
|
||||
|
||||
getCloudronToken(function (error, token) {
|
||||
if (token) return callback(new AppstoreError(AppstoreError.ALREADY_REGISTERED));
|
||||
|
||||
maybeSignup(function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
login(options.email, options.password, options.totpToken || '', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
registerCloudron({ domain: config.adminDomain(), accessToken: result.accessToken }, callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createTicket(info, callback) {
|
||||
assert.strictEqual(typeof info, 'object');
|
||||
assert.strictEqual(typeof info.email, 'string');
|
||||
assert.strictEqual(typeof info.displayName, 'string');
|
||||
@@ -320,17 +438,21 @@ function sendFeedback(info, callback) {
|
||||
apps.get(info.appId, callback);
|
||||
}
|
||||
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
collectAppInfoIfNeeded(function (error, result) {
|
||||
if (error) console.error('Unable to get app info', error);
|
||||
if (result) info.app = result;
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/feedback';
|
||||
let url = config.apiServerOrigin() + '/api/v1/ticket';
|
||||
|
||||
superagent.post(url).query({ accessToken: appstoreConfig.token }).send(info).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
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));
|
||||
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));
|
||||
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
|
||||
callback(null);
|
||||
@@ -338,3 +460,55 @@ function sendFeedback(info, callback) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getApps(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
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) {
|
||||
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));
|
||||
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App listing failed. %s %j', result.status, result.body)));
|
||||
if (!result.body.apps) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
|
||||
callback(null, result.body.apps);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getAppVersion(appId, version, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof version, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
let url = `${config.apiServerOrigin()}/api/v1/apps/${appId}`;
|
||||
if (version !== 'latest') url += `/versions/${version}`;
|
||||
|
||||
superagent.get(url).query({ accessToken: token }).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 === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
|
||||
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR, result.body.message));
|
||||
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App fetch failed. %s %j', result.status, result.body)));
|
||||
|
||||
callback(null, result.body);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getApp(appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getAppVersion(appId, 'latest', callback);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ var addons = require('./addons.js'),
|
||||
apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
auditsource = require('./auditsource.js'),
|
||||
backups = require('./backups.js'),
|
||||
config = require('./config.js'),
|
||||
database = require('./database.js'),
|
||||
@@ -34,6 +35,7 @@ var addons = require('./addons.js'),
|
||||
domains = require('./domains.js'),
|
||||
DomainsError = domains.DomainsError,
|
||||
ejs = require('ejs'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
fs = require('fs'),
|
||||
manifestFormat = require('cloudron-manifestformat'),
|
||||
mkdirp = require('mkdirp'),
|
||||
@@ -295,7 +297,8 @@ function registerSubdomain(app, overwrite, callback) {
|
||||
|
||||
// get the current record before updating it
|
||||
domains.getDnsRecords(app.location, app.domain, 'A', function (error, values) {
|
||||
if (error) return retryCallback(error);
|
||||
if (error && error.reason === DomainsError.EXTERNAL_ERROR) return retryCallback(error); // try again
|
||||
if (error) return retryCallback(null, error); // give up for access and other errors
|
||||
|
||||
// refuse to update any existing DNS record for custom domains that we did not create
|
||||
if (values.length !== 0 && !overwrite) return retryCallback(null, new Error('DNS Record already exists'));
|
||||
@@ -417,10 +420,15 @@ function removeIcon(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
fs.unlink(path.join(paths.APP_ICONS_DIR, app.id + '.png'), function (error) {
|
||||
if (error && error.code !== 'ENOENT') debugApp(app, 'cannot remove icon : %s', error);
|
||||
callback(null);
|
||||
});
|
||||
if (!safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, app.id + '.png'))) {
|
||||
if (safe.error.code !== 'ENOENT') debugApp(app, 'cannot remove icon : %s', safe.error);
|
||||
}
|
||||
|
||||
if (!safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, app.id + '.user.png'))) {
|
||||
if (safe.error.code !== 'ENOENT') debugApp(app, 'cannot remove user icon : %s', safe.error);
|
||||
}
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function cleanupLogs(app, callback) {
|
||||
@@ -585,7 +593,7 @@ function backup(app, callback) {
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '10, Backing up' }),
|
||||
backups.backupApp.bind(null, app, (progress) => updateApp(app, { installationProgress: `30, ${progress.message}` }, NOOP_CALLBACK)),
|
||||
backups.backupApp.bind(null, app, { /* options */ }, (progress) => updateApp(app, { installationProgress: `30, ${progress.message}` }, NOOP_CALLBACK)),
|
||||
|
||||
// done!
|
||||
function (callback) {
|
||||
@@ -694,6 +702,8 @@ function update(app, callback) {
|
||||
// FIXME: this does not handle option changes (like multipleDatabases)
|
||||
var unusedAddons = _.omit(app.manifest.addons, Object.keys(app.updateConfig.manifest.addons));
|
||||
|
||||
const FORCED_UPDATE = (app.installationState === appdb.ISTATE_PENDING_FORCE_UPDATE);
|
||||
|
||||
async.series([
|
||||
// this protects against the theoretical possibility of an app being marked for update from
|
||||
// a previous version of box code
|
||||
@@ -701,11 +711,12 @@ function update(app, callback) {
|
||||
verifyManifest.bind(null, app.updateConfig.manifest),
|
||||
|
||||
function (next) {
|
||||
if (app.installationState === appdb.ISTATE_PENDING_FORCE_UPDATE) return next(null);
|
||||
if (FORCED_UPDATE) return next(null);
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '15, Backing up app' }),
|
||||
backups.backupApp.bind(null, app, (progress) => updateApp(app, { installationProgress: `15, Backup - ${progress.message}` }, NOOP_CALLBACK))
|
||||
// preserve update backups for 3 weeks
|
||||
backups.backupApp.bind(null, app, { preserveSecs: 3*7*24*60*60 }, (progress) => updateApp(app, { installationProgress: `15, Backup - ${progress.message}` }, NOOP_CALLBACK))
|
||||
], function (error) {
|
||||
if (error) error.backupError = true;
|
||||
next(error);
|
||||
@@ -787,7 +798,10 @@ function update(app, callback) {
|
||||
debugApp(app, 'Error updating app: %s', error);
|
||||
updateApp(app, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message, updateTime: new Date() }, callback.bind(null, error));
|
||||
} else {
|
||||
callback(null);
|
||||
// do not spam the notifcation view
|
||||
if (FORCED_UPDATE) return callback();
|
||||
|
||||
eventlog.add(eventlog.ACTION_APP_UPDATE_FINISH, auditsource.APP_TASK, { app: app, success: true }, callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
16
src/auditsource.js
Normal file
16
src/auditsource.js
Normal file
@@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
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' },
|
||||
|
||||
fromRequest: fromRequest
|
||||
};
|
||||
|
||||
function fromRequest(req) {
|
||||
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
|
||||
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
|
||||
}
|
||||
@@ -6,7 +6,7 @@ var assert = require('assert'),
|
||||
safe = require('safetydance'),
|
||||
util = require('util');
|
||||
|
||||
var BACKUPS_FIELDS = [ 'id', 'creationTime', 'version', 'type', 'dependsOn', 'state', 'manifestJson', 'format' ];
|
||||
var BACKUPS_FIELDS = [ 'id', 'creationTime', 'version', 'type', 'dependsOn', 'state', 'manifestJson', 'format', 'preserveSecs' ];
|
||||
|
||||
exports = module.exports = {
|
||||
add: add,
|
||||
@@ -103,21 +103,21 @@ function get(id, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function add(backup, callback) {
|
||||
assert(backup && typeof backup === 'object');
|
||||
assert.strictEqual(typeof backup.id, 'string');
|
||||
assert.strictEqual(typeof backup.version, 'string');
|
||||
assert(backup.type === exports.BACKUP_TYPE_APP || backup.type === exports.BACKUP_TYPE_BOX);
|
||||
assert(util.isArray(backup.dependsOn));
|
||||
assert.strictEqual(typeof backup.manifest, 'object');
|
||||
assert.strictEqual(typeof backup.format, 'string');
|
||||
function add(id, data, callback) {
|
||||
assert(data && typeof data === 'object');
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof data.version, 'string');
|
||||
assert(data.type === exports.BACKUP_TYPE_APP || data.type === exports.BACKUP_TYPE_BOX);
|
||||
assert(util.isArray(data.dependsOn));
|
||||
assert.strictEqual(typeof data.manifest, 'object');
|
||||
assert.strictEqual(typeof data.format, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var creationTime = backup.creationTime || new Date(); // allow tests to set the time
|
||||
var manifestJson = JSON.stringify(backup.manifest);
|
||||
var creationTime = data.creationTime || new Date(); // allow tests to set the time
|
||||
var manifestJson = JSON.stringify(data.manifest);
|
||||
|
||||
database.query('INSERT INTO backups (id, version, type, creationTime, state, dependsOn, manifestJson, format) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[ backup.id, backup.version, backup.type, creationTime, exports.BACKUP_STATE_NORMAL, backup.dependsOn.join(','), manifestJson, backup.format ],
|
||||
[ id, data.version, data.type, creationTime, exports.BACKUP_STATE_NORMAL, data.dependsOn.join(','), manifestJson, data.format ],
|
||||
function (error) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
121
src/backups.js
121
src/backups.js
@@ -29,6 +29,8 @@ exports = module.exports = {
|
||||
injectPrivateFields: injectPrivateFields,
|
||||
removePrivateFields: removePrivateFields,
|
||||
|
||||
checkConfiguration: checkConfiguration,
|
||||
|
||||
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8),
|
||||
|
||||
// for testing
|
||||
@@ -53,7 +55,6 @@ var addons = require('./addons.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
fs = require('fs'),
|
||||
locker = require('./locker.js'),
|
||||
mailer = require('./mailer.js'),
|
||||
mkdirp = require('mkdirp'),
|
||||
once = require('once'),
|
||||
path = require('path'),
|
||||
@@ -62,7 +63,6 @@ var addons = require('./addons.js'),
|
||||
safe = require('safetydance'),
|
||||
shell = require('./shell.js'),
|
||||
settings = require('./settings.js'),
|
||||
superagent = require('superagent'),
|
||||
syncer = require('./syncer.js'),
|
||||
tar = require('tar-fs'),
|
||||
tasks = require('./tasks.js'),
|
||||
@@ -105,7 +105,6 @@ BackupsError.NOT_FOUND = 'not found';
|
||||
// choose which storage backend we use for test purpose we use s3
|
||||
function api(provider) {
|
||||
switch (provider) {
|
||||
case 'caas': return require('./storage/s3.js');
|
||||
case 's3': return require('./storage/s3.js');
|
||||
case 'gcs': return require('./storage/gcs.js');
|
||||
case 'filesystem': return require('./storage/filesystem.js');
|
||||
@@ -113,6 +112,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 'scaleway-objectstorage': return require('./storage/s3.js');
|
||||
case 'noop': return require('./storage/noop.js');
|
||||
default: return null;
|
||||
}
|
||||
@@ -285,6 +285,10 @@ function tarPack(dataLayout, key, callback) {
|
||||
var pack = tar.pack('/', {
|
||||
dereference: false, // pack the symlink and not what it points to
|
||||
entries: dataLayout.localPaths(),
|
||||
ignoreStatError: (path, err) => {
|
||||
debug(`tarPack: error stat'ing ${path} - ${err.code}`);
|
||||
return err.code === 'ENOENT'; // ignore if file or dir got removed (probably some temporary file)
|
||||
},
|
||||
map: function(header) {
|
||||
header.name = dataLayout.toRemotePath(header.name);
|
||||
return header;
|
||||
@@ -505,9 +509,9 @@ function restoreFsMetadata(dataLayout, metadataFile, callback) {
|
||||
debug(`Recreating empty directories in ${dataLayout.toString()}`);
|
||||
|
||||
var metadataJson = safe.fs.readFileSync(metadataFile, 'utf8');
|
||||
if (metadataJson === null) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error loading fsmetadata.txt:' + safe.error.message));
|
||||
if (metadataJson === null) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error loading fsmetadata.json:' + safe.error.message));
|
||||
var metadata = safe.JSON.parse(metadataJson);
|
||||
if (metadata === null) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error parsing fsmetadata.txt:' + safe.error.message));
|
||||
if (metadata === null) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error parsing fsmetadata.json:' + safe.error.message));
|
||||
|
||||
async.eachSeries(metadata.emptyDirs, function createPath(emptyDir, iteratorDone) {
|
||||
mkdirp(dataLayout.toLocalPath(emptyDir), iteratorDone);
|
||||
@@ -749,37 +753,9 @@ function uploadBoxSnapshot(backupConfig, progressCallback, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function backupDone(apiConfig, backupId, appBackupIds, callback) {
|
||||
assert.strictEqual(typeof apiConfig, 'object');
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert(Array.isArray(appBackupIds));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (apiConfig.provider !== 'caas') return callback();
|
||||
|
||||
debug('[%s] backupDone: %s apps %j', backupId, backupId, appBackupIds);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + apiConfig.fqdn + '/backupDone';
|
||||
var data = {
|
||||
boxVersion: config.version(),
|
||||
backupId: backupId,
|
||||
appId: null, // now unused
|
||||
appVersion: null, // now unused
|
||||
appBackupIds: appBackupIds
|
||||
};
|
||||
|
||||
superagent.post(url).send(data).query({ token: apiConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode !== 200) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, result.text));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function rotateBoxBackup(backupConfig, timestamp, appBackupIds, progressCallback, callback) {
|
||||
function rotateBoxBackup(backupConfig, tag, appBackupIds, progressCallback, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof timestamp, 'string');
|
||||
assert.strictEqual(typeof tag, 'string');
|
||||
assert(Array.isArray(appBackupIds));
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -787,13 +763,13 @@ function rotateBoxBackup(backupConfig, timestamp, appBackupIds, progressCallback
|
||||
var snapshotInfo = getSnapshotInfo('box');
|
||||
if (!snapshotInfo) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, 'Snapshot info missing or corrupt'));
|
||||
|
||||
var snapshotTime = snapshotInfo.timestamp.replace(/[T.]/g, '-').replace(/[:Z]/g,'');
|
||||
var backupId = util.format('%s/box_%s_v%s', timestamp, snapshotTime, config.version());
|
||||
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 format = backupConfig.format;
|
||||
|
||||
debug(`Rotating box backup to id ${backupId}`);
|
||||
|
||||
backupdb.add({ id: backupId, version: config.version(), type: backupdb.BACKUP_TYPE_BOX, dependsOn: appBackupIds, manifest: null, format: format }, function (error) {
|
||||
backupdb.add(backupId, { version: config.version(), type: backupdb.BACKUP_TYPE_BOX, dependsOn: appBackupIds, manifest: null, format: format }, function (error) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
var copy = api(backupConfig.provider).copy(backupConfig, getBackupFilePath(backupConfig, 'snapshot/box', format), getBackupFilePath(backupConfig, backupId, format));
|
||||
@@ -807,19 +783,15 @@ function rotateBoxBackup(backupConfig, timestamp, appBackupIds, progressCallback
|
||||
|
||||
debug(`Rotated box backup successfully as id ${backupId}`);
|
||||
|
||||
backupDone(backupConfig, backupId, appBackupIds, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, backupId);
|
||||
});
|
||||
callback(null, backupId);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function backupBoxWithAppBackupIds(appBackupIds, timestamp, progressCallback, callback) {
|
||||
function backupBoxWithAppBackupIds(appBackupIds, tag, progressCallback, callback) {
|
||||
assert(Array.isArray(appBackupIds));
|
||||
assert.strictEqual(typeof timestamp, 'string');
|
||||
assert.strictEqual(typeof tag, 'string');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -829,7 +801,7 @@ function backupBoxWithAppBackupIds(appBackupIds, timestamp, progressCallback, ca
|
||||
uploadBoxSnapshot(backupConfig, progressCallback, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
rotateBoxBackup(backupConfig, timestamp, appBackupIds, progressCallback, callback);
|
||||
rotateBoxBackup(backupConfig, tag, appBackupIds, progressCallback, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -861,24 +833,25 @@ function snapshotApp(app, progressCallback, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function rotateAppBackup(backupConfig, app, timestamp, progressCallback, callback) {
|
||||
function rotateAppBackup(backupConfig, app, tag, options, progressCallback, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof timestamp, 'string');
|
||||
assert.strictEqual(typeof tag, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var snapshotInfo = getSnapshotInfo(app.id);
|
||||
if (!snapshotInfo) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, 'Snapshot info missing or corrupt'));
|
||||
|
||||
var snapshotTime = snapshotInfo.timestamp.replace(/[T.]/g, '-').replace(/[:Z]/g,'');
|
||||
var manifest = snapshotInfo.restoreConfig ? snapshotInfo.restoreConfig.manifest : snapshotInfo.manifest; // compat
|
||||
var backupId = util.format('%s/app_%s_%s_v%s', timestamp, app.id, snapshotTime, manifest.version);
|
||||
const snapshotTime = snapshotInfo.timestamp.replace(/[T.]/g, '-').replace(/[:Z]/g,''); // add this for unique filename which helps when downloading them
|
||||
const backupId = util.format('%s/app_%s_%s_v%s', tag, app.id, snapshotTime, manifest.version);
|
||||
const format = backupConfig.format;
|
||||
|
||||
debug(`Rotating app backup of ${app.id} to id ${backupId}`);
|
||||
|
||||
backupdb.add({ id: backupId, version: manifest.version, type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ], manifest: manifest, format: format }, function (error) {
|
||||
backupdb.add(backupId, { version: manifest.version, type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ], manifest: manifest, format: format }, function (error) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
var copy = api(backupConfig.provider).copy(backupConfig, getBackupFilePath(backupConfig, `snapshot/app_${app.id}`, format), getBackupFilePath(backupConfig, backupId, format));
|
||||
@@ -886,7 +859,7 @@ function rotateAppBackup(backupConfig, app, timestamp, progressCallback, callbac
|
||||
copy.on('done', function (copyBackupError) {
|
||||
const state = copyBackupError ? backupdb.BACKUP_STATE_ERROR : backupdb.BACKUP_STATE_NORMAL;
|
||||
|
||||
backupdb.update(backupId, { state: state }, function (error) {
|
||||
backupdb.update(backupId, { preserveSecs: options.preserveSecs || 0, state: state }, function (error) {
|
||||
if (copyBackupError) return callback(copyBackupError);
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -927,9 +900,10 @@ function uploadAppSnapshot(backupConfig, app, progressCallback, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function backupAppWithTimestamp(app, timestamp, progressCallback, callback) {
|
||||
function backupAppWithTag(app, tag, options, progressCallback, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof timestamp, 'string');
|
||||
assert.strictEqual(typeof tag, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -941,21 +915,22 @@ function backupAppWithTimestamp(app, timestamp, progressCallback, callback) {
|
||||
uploadAppSnapshot(backupConfig, app, progressCallback, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
rotateAppBackup(backupConfig, app, timestamp, progressCallback, callback);
|
||||
rotateAppBackup(backupConfig, app, tag, options, progressCallback, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function backupApp(app, progressCallback, callback) {
|
||||
function backupApp(app, options, progressCallback, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const timestamp = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
|
||||
const tag = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
|
||||
|
||||
debug(`backupApp - Backing up ${app.fqdn} with timestamp ${timestamp}`);
|
||||
debug(`backupApp - Backing up ${app.fqdn} with tag ${tag}`);
|
||||
|
||||
backupAppWithTimestamp(app, timestamp, progressCallback, callback);
|
||||
backupAppWithTag(app, tag, options, progressCallback, callback);
|
||||
}
|
||||
|
||||
// this function expects you to have a lock. Unlike other progressCallback this also has a progress field
|
||||
@@ -963,7 +938,7 @@ function backupBoxAndApps(progressCallback, callback) {
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var timestamp = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
|
||||
const tag = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
|
||||
|
||||
apps.getAll(function (error, allApps) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
@@ -980,7 +955,7 @@ function backupBoxAndApps(progressCallback, callback) {
|
||||
return iteratorCallback(null, null); // nothing to backup
|
||||
}
|
||||
|
||||
backupAppWithTimestamp(app, timestamp, (progress) => progressCallback({ percent: percent, message: progress.message }), function (error, backupId) {
|
||||
backupAppWithTag(app, tag, { /* options */ }, (progress) => progressCallback({ percent: percent, message: progress.message }), function (error, backupId) {
|
||||
if (error && error.reason !== BackupsError.BAD_STATE) {
|
||||
debugApp(app, 'Unable to backup', error);
|
||||
return iteratorCallback(error);
|
||||
@@ -998,7 +973,7 @@ function backupBoxAndApps(progressCallback, callback) {
|
||||
progressCallback({ percent: percent, message: 'Backing up system data' });
|
||||
percent += step;
|
||||
|
||||
backupBoxWithAppBackupIds(backupIds, timestamp, (progress) => progressCallback({ percent: percent, message: progress.message }), callback);
|
||||
backupBoxWithAppBackupIds(backupIds, tag, (progress) => progressCallback({ percent: percent, message: progress.message }), callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1016,9 +991,9 @@ function startBackupTask(auditSource, callback) {
|
||||
task.on('finish', (error, result) => {
|
||||
locker.unlock(locker.OP_FULL_BACKUP);
|
||||
|
||||
if (error) mailer.backupFailed(error);
|
||||
const errorMessage = error ? error.message : '';
|
||||
|
||||
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { errorMessage: error ? error.message : null, backupId: result });
|
||||
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { taskId: task.id, errorMessage: errorMessage, backupId: result });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1095,6 +1070,7 @@ function cleanupAppBackups(backupConfig, referencedAppBackups, callback) {
|
||||
|
||||
async.eachSeries(appBackups, function iterator(appBackup, iteratorDone) {
|
||||
if (referencedAppBackups.indexOf(appBackup.id) !== -1) return iteratorDone();
|
||||
if ((now - appBackup.creationTime) < (appBackup.preserveSecs * 1000)) return iteratorDone();
|
||||
if ((now - appBackup.creationTime) < (backupConfig.retentionSecs * 1000)) return iteratorDone();
|
||||
|
||||
debug('cleanupAppBackups: removing %s', appBackup.id);
|
||||
@@ -1256,3 +1232,20 @@ function startCleanupTask(auditSource, callback) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function checkConfiguration(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
let message = '';
|
||||
if (backupConfig.provider === 'noop') {
|
||||
message = 'Cloudron backups are disabled. Please ensure this server is backed up using alternate means. See https://cloudron.io/documentation/backups/#storage-providers for more information.';
|
||||
} else if (backupConfig.provider === 'filesystem' && !backupConfig.externalDisk) {
|
||||
message = 'Cloudron backups are currently on the same disk as the Cloudron server instance. This is dangerous and can lead to complete data loss if the disk fails. See https://cloudron.io/documentation/backups/#storage-providers for storing backups in an external location.';
|
||||
}
|
||||
|
||||
callback(null, message);
|
||||
});
|
||||
}
|
||||
|
||||
130
src/caas.js
130
src/caas.js
@@ -1,130 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
verifySetupToken: verifySetupToken,
|
||||
setupDone: setupDone,
|
||||
|
||||
sendHeartbeat: sendHeartbeat,
|
||||
setPtrRecord: setPtrRecord,
|
||||
|
||||
CaasError: CaasError
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:caas'),
|
||||
settings = require('./settings.js'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util');
|
||||
|
||||
function CaasError(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(CaasError, Error);
|
||||
CaasError.BAD_FIELD = 'Field error';
|
||||
CaasError.BAD_STATE = 'Bad state';
|
||||
CaasError.INVALID_TOKEN = 'Invalid Token';
|
||||
CaasError.INTERNAL_ERROR = 'Internal Error';
|
||||
CaasError.EXTERNAL_ERROR = 'External Error';
|
||||
|
||||
function getCaasConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.getCaasConfig(function (error, result) {
|
||||
if (error) return callback(new CaasError(CaasError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function verifySetupToken(setupToken, callback) {
|
||||
assert.strictEqual(typeof setupToken, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.getCaasConfig(function (error, caasConfig) {
|
||||
if (error) return callback(new CaasError(CaasError.INTERNAL_ERROR, error));
|
||||
|
||||
superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId + '/setup/verify').query({ setupToken: setupToken })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 403) return callback(new CaasError(CaasError.INVALID_TOKEN));
|
||||
if (result.statusCode === 409) return callback(new CaasError(CaasError.BAD_STATE, 'Already setup'));
|
||||
if (result.statusCode !== 200) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setupDone(setupToken, callback) {
|
||||
assert.strictEqual(typeof setupToken, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.getCaasConfig(function (error, caasConfig) {
|
||||
if (error) return callback(new CaasError(CaasError.INTERNAL_ERROR, error));
|
||||
|
||||
// Now let the api server know we got activated
|
||||
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId + '/setup/done').query({ setupToken: setupToken })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 403) return callback(new CaasError(CaasError.INVALID_TOKEN));
|
||||
if (result.statusCode === 409) return callback(new CaasError(CaasError.BAD_STATE, 'Already setup'));
|
||||
if (result.statusCode !== 201) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sendHeartbeat() {
|
||||
assert(config.provider() === 'caas', 'Heartbeat is only sent for managed cloudrons');
|
||||
|
||||
getCaasConfig(function (error, result) {
|
||||
if (error) return debug('Caas config missing', error);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + result.boxId + '/heartbeat';
|
||||
superagent.post(url).query({ token: result.token, version: config.version() }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) debug('Network error sending heartbeat.', error);
|
||||
else if (result.statusCode !== 200) debug('Server responded to heartbeat with %s %s', result.statusCode, result.text);
|
||||
else debug('Heartbeat sent to %s', url);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setPtrRecord(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getCaasConfig(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
superagent
|
||||
.post(config.apiServerOrigin() + '/api/v1/boxes/' + result.boxId + '/ptr')
|
||||
.query({ token: result.token })
|
||||
.send({ domain: domain })
|
||||
.timeout(5 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, 'Cannot reach appstore'));
|
||||
if (result.statusCode !== 202) return callback(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -80,7 +80,7 @@ function urlBase64Encode(string) {
|
||||
}
|
||||
|
||||
function b64(str) {
|
||||
var buf = util.isBuffer(str) ? str : new Buffer(str);
|
||||
var buf = util.isBuffer(str) ? str : Buffer.from(str);
|
||||
return urlBase64Encode(buf.toString('base64'));
|
||||
}
|
||||
|
||||
|
||||
107
src/cloudron.js
107
src/cloudron.js
@@ -16,6 +16,7 @@ exports = module.exports = {
|
||||
|
||||
prepareDashboardDomain: prepareDashboardDomain,
|
||||
setDashboardDomain: setDashboardDomain,
|
||||
setDashboardAndMailDomain: setDashboardAndMailDomain,
|
||||
renewCerts: renewCerts,
|
||||
|
||||
runSystemChecks: runSystemChecks,
|
||||
@@ -24,8 +25,10 @@ exports = module.exports = {
|
||||
_checkDiskSpace: checkDiskSpace
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
var apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
backups = require('./backups.js'),
|
||||
clients = require('./clients.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
@@ -35,6 +38,7 @@ var assert = require('assert'),
|
||||
DomainsError = require('./domains.js').DomainsError,
|
||||
df = require('@sindresorhus/df'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
custom = require('./custom.js'),
|
||||
fs = require('fs'),
|
||||
mail = require('./mail.js'),
|
||||
notifications = require('./notifications.js'),
|
||||
@@ -83,9 +87,9 @@ CloudronError.ALREADY_UPTODATE = 'No Update Available';
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
cron.startPreActivationJobs(callback);
|
||||
|
||||
runStartupTasks();
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
@@ -105,7 +109,7 @@ function onActivated(callback) {
|
||||
// 2. the restore code path can run without sudo (since mail/ is non-root)
|
||||
async.series([
|
||||
platform.start,
|
||||
cron.startPostActivationJobs
|
||||
cron.startJobs
|
||||
], callback);
|
||||
}
|
||||
|
||||
@@ -167,10 +171,10 @@ function getConfig(callback) {
|
||||
mailFqdn: config.mailFqdn(),
|
||||
version: config.version(),
|
||||
isDemo: config.isDemo(),
|
||||
edition: config.edition(),
|
||||
memory: os.totalmem(),
|
||||
provider: config.provider(),
|
||||
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY]
|
||||
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
|
||||
uiSpec: custom.uiSpec()
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -191,9 +195,10 @@ function runSystemChecks() {
|
||||
async.parallel([
|
||||
checkBackupConfiguration,
|
||||
checkDiskSpace,
|
||||
checkMailStatus
|
||||
], function () {
|
||||
debug('runSystemChecks: done');
|
||||
checkMailStatus,
|
||||
checkRebootRequired
|
||||
], function (error) {
|
||||
debug('runSystemChecks: done', error);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -202,14 +207,10 @@ function checkBackupConfiguration(callback) {
|
||||
|
||||
debug('Checking backup configuration');
|
||||
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return console.error(error);
|
||||
backups.checkConfiguration(function (error, message) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (backupConfig.provider === 'noop') {
|
||||
notifications.backupConfigWarning('Cloudron backups are disabled. Please ensure this server is backed up using alternate means. See https://cloudron.io/documentation/backups/#storage-providers for more information.');
|
||||
} else if (backupConfig.provider === 'filesystem' && !backupConfig.externalDisk) {
|
||||
notifications.backupConfigWarning('Cloudron backups are currently on the same disk as the Cloudron server instance. This is dangerous and can lead to complete data loss if the disk fails. See https://cloudron.io/documentation/backups/#storage-providers for storing backups in an external location.');
|
||||
}
|
||||
notifications.alert(notifications.ALERT_BACKUP_CONFIG, 'Backup configuration is unsafe', message, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -244,9 +245,7 @@ function checkDiskSpace(callback) {
|
||||
|
||||
debug('Disk space checked. ok: %s', !oos);
|
||||
|
||||
if (oos) notifications.diskSpaceWarning(JSON.stringify(entries, null, 4));
|
||||
|
||||
callback();
|
||||
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();
|
||||
@@ -259,28 +258,22 @@ function checkMailStatus(callback) {
|
||||
|
||||
debug('checking mail status');
|
||||
|
||||
domains.getAll(function (error, allDomains) {
|
||||
mail.checkConfiguration(function (error, message) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.filterSeries(allDomains, function (domainObject, iteratorCallback) {
|
||||
mail.getStatus(config.adminDomain(), function (error, result) {
|
||||
if (error) return iteratorCallback(null, true);
|
||||
notifications.alert(notifications.ALERT_MAIL_STATUS, 'Email is not configured properly', message, callback);
|
||||
});
|
||||
}
|
||||
|
||||
let mailError = Object.keys(result.dns).some((record) => !result.dns[record].status);
|
||||
if (result.relay && !result.relay.status) mailError = true;
|
||||
if (result.rbl && result.rbl.status === false) mailError = true; // rbl is an optional check
|
||||
function checkRebootRequired(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
iteratorCallback(null, mailError);
|
||||
});
|
||||
}, function (error, erroredDomainObjects) {
|
||||
if (error || erroredDomainObjects.length === 0) return callback(error);
|
||||
debug('checking if reboot required');
|
||||
|
||||
const erroredDomains = erroredDomainObjects.map((d) => d.domain);
|
||||
debug(`checkMailStatus: ${erroredDomains.join(',')} failed status checks`);
|
||||
if (erroredDomains.length) notifications.mailStatusWarning(`Email status check of the following domain(s) failed - ${erroredDomains.join(',')}. See the Status tab in the [Email view](/#/email/) for more information.`);
|
||||
isRebootRequired(function (error, rebootRequired) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback();
|
||||
});
|
||||
notifications.alert(notifications.ALERT_REBOOT, 'Reboot Required', rebootRequired ? 'To finish security updates, a [reboot](/#/system) is necessary.' : '', callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -304,7 +297,8 @@ function getLogs(unit, options, callback) {
|
||||
|
||||
// need to handle box.log without subdir
|
||||
if (unit === 'box') args.push(path.join(paths.LOG_DIR, 'box.log'));
|
||||
else args.push(path.join(paths.LOG_DIR, unit, 'app.log'));
|
||||
else if (unit.startsWith('crash-')) args.push(path.join(paths.CRASH_LOG_DIR, unit.slice(6) + '.log'));
|
||||
else return callback(new CloudronError(CloudronError.BAD_FIELD, 'No such unit'));
|
||||
|
||||
var cp = spawn('/usr/bin/tail', args);
|
||||
|
||||
@@ -336,11 +330,26 @@ function prepareDashboardDomain(domain, auditSource, callback) {
|
||||
|
||||
debug(`prepareDashboardDomain: ${domain}`);
|
||||
|
||||
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));
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new CloudronError(CloudronError.BAD_FIELD, 'No such domain'));
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
const fqdn = domains.fqdn(constants.ADMIN_LOCATION, domainObject);
|
||||
|
||||
apps.getAll(function (error, result) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
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));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// call this only pre activation since it won't start mail server
|
||||
function setDashboardDomain(domain, auditSource, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
@@ -358,7 +367,6 @@ function setDashboardDomain(domain, auditSource, callback) {
|
||||
const fqdn = domains.fqdn(constants.ADMIN_LOCATION, domainObject);
|
||||
|
||||
config.setAdminDomain(domain);
|
||||
config.setAdminLocation(constants.ADMIN_LOCATION);
|
||||
config.setAdminFqdn(fqdn);
|
||||
|
||||
clients.addDefaultClients(config.adminOrigin(), function (error) {
|
||||
@@ -366,14 +374,29 @@ function setDashboardDomain(domain, auditSource, callback) {
|
||||
|
||||
eventlog.add(eventlog.ACTION_DASHBOARD_DOMAIN_UPDATE, auditSource, { domain: domain, fqdn: fqdn });
|
||||
|
||||
mail.setMailFqdn(fqdn, domain, NOOP_CALLBACK);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// call this only post activation because it will restart mail server
|
||||
function setDashboardAndMailDomain(domain, auditSource, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`setDashboardAndMailDomain: ${domain}`);
|
||||
|
||||
setDashboardDomain(domain, auditSource, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
mail.onMailFqdnChanged(NOOP_CALLBACK); // this will update dns and re-configure mail server
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function renewCerts(options, auditSource, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
|
||||
@@ -20,28 +20,19 @@ exports = module.exports = {
|
||||
setFqdn: setAdminDomain,
|
||||
setAdminDomain: setAdminDomain,
|
||||
setAdminFqdn: setAdminFqdn,
|
||||
setAdminLocation: setAdminLocation,
|
||||
version: version,
|
||||
database: database,
|
||||
edition: edition,
|
||||
|
||||
// these values are derived
|
||||
adminOrigin: adminOrigin,
|
||||
internalAdminOrigin: internalAdminOrigin,
|
||||
sysadminOrigin: sysadminOrigin, // caas routes
|
||||
adminLocation: adminLocation,
|
||||
sysadminOrigin: sysadminOrigin, // localhost routes
|
||||
adminFqdn: adminFqdn,
|
||||
mailLocation: mailLocation,
|
||||
mailFqdn: mailFqdn,
|
||||
hasIPv6: hasIPv6,
|
||||
dkimSelector: dkimSelector,
|
||||
|
||||
isManaged: isManaged,
|
||||
isDemo: isDemo,
|
||||
|
||||
// feature flags based on editions (these have a separate license from standard edition)
|
||||
isSpacesEnabled: isSpacesEnabled,
|
||||
|
||||
// for testing resets to defaults
|
||||
_reset: _reset
|
||||
};
|
||||
@@ -74,10 +65,8 @@ function saveSync() {
|
||||
webServerOrigin: data.webServerOrigin,
|
||||
adminDomain: data.adminDomain,
|
||||
adminFqdn: data.adminFqdn,
|
||||
adminLocation: data.adminLocation,
|
||||
provider: data.provider,
|
||||
isDemo: data.isDemo,
|
||||
edition: data.edition
|
||||
isDemo: data.isDemo
|
||||
};
|
||||
|
||||
fs.writeFileSync(cloudronConfigFileName, JSON.stringify(conf, null, 4)); // functions are ignored by JSON.stringify
|
||||
@@ -95,7 +84,6 @@ function initConfig() {
|
||||
// setup defaults
|
||||
data.adminFqdn = '';
|
||||
data.adminDomain = '';
|
||||
data.adminLocation = 'my';
|
||||
data.port = 3000;
|
||||
data.apiServerOrigin = null;
|
||||
data.webServerOrigin = null;
|
||||
@@ -104,7 +92,6 @@ function initConfig() {
|
||||
data.sysadminPort = 3001;
|
||||
data.ldapPort = 3002;
|
||||
data.dockerProxyPort = 3003;
|
||||
data.edition = '';
|
||||
|
||||
// keep in sync with start.sh
|
||||
data.database = {
|
||||
@@ -168,19 +155,6 @@ function adminDomain() {
|
||||
return get('adminDomain');
|
||||
}
|
||||
|
||||
function mailLocation() {
|
||||
return get('adminLocation'); // not a typo! should be same as admin location until we figure out certificates
|
||||
}
|
||||
|
||||
function setAdminLocation(location) {
|
||||
set('adminLocation', location);
|
||||
}
|
||||
|
||||
|
||||
function adminLocation() {
|
||||
return get('adminLocation');
|
||||
}
|
||||
|
||||
function setAdminFqdn(adminFqdn) {
|
||||
set('adminFqdn', adminFqdn);
|
||||
}
|
||||
@@ -218,31 +192,12 @@ function isDemo() {
|
||||
return get('isDemo') === true;
|
||||
}
|
||||
|
||||
function isSpacesEnabled() {
|
||||
return get('edition') === 'education';
|
||||
}
|
||||
|
||||
function provider() {
|
||||
return get('provider');
|
||||
}
|
||||
|
||||
function isManaged() {
|
||||
return edition() === 'hostingprovider';
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// it has to change with the adminLocation so that multiple cloudrons
|
||||
// can send out emails at the same time.
|
||||
function dkimSelector() {
|
||||
var loc = adminLocation();
|
||||
return loc === 'my' ? 'cloudron' : `cloudron-${loc.replace(/\./g, '')}`;
|
||||
}
|
||||
|
||||
function edition() {
|
||||
return get('edition');
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
API_LOCATION: 'api', // this is unused but reserved for future use (#403)
|
||||
SMTP_LOCATION: 'smtp',
|
||||
IMAP_LOCATION: 'imap',
|
||||
|
||||
|
||||
60
src/crashnotifier.js
Normal file
60
src/crashnotifier.js
Normal file
@@ -0,0 +1,60 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
sendFailureLogs: sendFailureLogs
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
auditSource = require('./auditsource.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
safe = require('safetydance'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
util = require('util');
|
||||
|
||||
const COLLECT_LOGS_CMD = path.join(__dirname, 'scripts/collectlogs.sh');
|
||||
|
||||
const CRASH_LOG_TIMESTAMP_OFFSET = 1000 * 60 * 60; // 60 min
|
||||
const CRASH_LOG_TIMESTAMP_FILE = '/tmp/crashlog.timestamp';
|
||||
|
||||
function collectLogs(unitName, callback) {
|
||||
assert.strictEqual(typeof unitName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var logs = safe.child_process.execSync('sudo ' + COLLECT_LOGS_CMD + ' ' + unitName, { encoding: 'utf8' });
|
||||
if (!logs) return callback(safe.error);
|
||||
|
||||
callback(null, logs);
|
||||
}
|
||||
|
||||
function sendFailureLogs(unitName, callback) {
|
||||
assert.strictEqual(typeof unitName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// check if we already sent a mail in the last CRASH_LOG_TIME_OFFSET window
|
||||
const timestamp = safe.fs.readFileSync(CRASH_LOG_TIMESTAMP_FILE, 'utf8');
|
||||
if (timestamp && (parseInt(timestamp) + CRASH_LOG_TIMESTAMP_OFFSET) > Date.now()) {
|
||||
console.log('Crash log already sent within window');
|
||||
return callback();
|
||||
}
|
||||
|
||||
collectLogs(unitName, function (error, logs) {
|
||||
if (error) {
|
||||
console.error('Failed to collect logs.', error);
|
||||
logs = util.format('Failed to collect logs.', error);
|
||||
}
|
||||
|
||||
const crashId = `${new Date().toISOString()}`;
|
||||
console.log(`Creating crash log for ${unitName} with id ${crashId}`);
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.CRASH_LOG_DIR, `${crashId}.log`), logs)) console.log(`Failed to stash logs to ${crashId}.log:`, safe.error);
|
||||
|
||||
eventlog.add(eventlog.ACTION_PROCESS_CRASH, auditSource.HEALTH_MONITOR, { processName: unitName, crashId: crashId }, function (error) {
|
||||
if (error) console.log(`Error sending crashlog. Logs stashed at ${crashId}.log`);
|
||||
|
||||
safe.fs.writeFileSync(CRASH_LOG_TIMESTAMP_FILE, String(Date.now()));
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
81
src/cron.js
81
src/cron.js
@@ -1,24 +1,24 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
startPostActivationJobs: startPostActivationJobs,
|
||||
startPreActivationJobs: startPreActivationJobs,
|
||||
startJobs: startJobs,
|
||||
|
||||
stopJobs: stopJobs
|
||||
stopJobs: stopJobs,
|
||||
|
||||
handleSettingsChanged: handleSettingsChanged
|
||||
};
|
||||
|
||||
var appHealthMonitor = require('./apphealthmonitor.js'),
|
||||
apps = require('./apps.js'),
|
||||
appstore = require('./appstore.js'),
|
||||
assert = require('assert'),
|
||||
auditSource = require('./auditsource.js'),
|
||||
backups = require('./backups.js'),
|
||||
caas = require('./caas.js'),
|
||||
cloudron = require('./cloudron.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
CronJob = require('cron').CronJob,
|
||||
debug = require('debug')('box:cron'),
|
||||
digest = require('./digest.js'),
|
||||
dyndns = require('./dyndns.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
janitor = require('./janitor.js'),
|
||||
@@ -34,13 +34,11 @@ var gJobs = {
|
||||
appUpdateChecker: null,
|
||||
backup: null,
|
||||
boxUpdateChecker: null,
|
||||
caasHeartbeat: null,
|
||||
systemChecks: null,
|
||||
certificateRenew: null,
|
||||
cleanupBackups: null,
|
||||
cleanupEventlog: null,
|
||||
cleanupTokens: null,
|
||||
digestEmail: null,
|
||||
dockerVolumeCleaner: null,
|
||||
dynamicDns: null,
|
||||
schedulerSync: null,
|
||||
@@ -48,7 +46,6 @@ var gJobs = {
|
||||
};
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
var AUDIT_SOURCE = { userId: null, username: 'cron' };
|
||||
|
||||
// cron format
|
||||
// Seconds: 0-59
|
||||
@@ -58,24 +55,7 @@ var AUDIT_SOURCE = { userId: null, username: 'cron' };
|
||||
// Months: 0-11
|
||||
// Day of Week: 0-6
|
||||
|
||||
function startPreActivationJobs(callback) {
|
||||
if (config.provider() === 'caas') {
|
||||
// hack: send the first heartbeat only after we are running for 60 seconds
|
||||
// required as we end up sending a heartbeat and then cloudron-setup reboots the server
|
||||
var seconds = (new Date()).getSeconds() - 1;
|
||||
if (seconds === -1) seconds = 59;
|
||||
|
||||
gJobs.caasHeartbeat = new CronJob({
|
||||
cronTime: `${seconds} */1 * * * *`, // every minute
|
||||
onTick: caas.sendHeartbeat,
|
||||
start: true
|
||||
});
|
||||
}
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
function startPostActivationJobs(callback) {
|
||||
function startJobs(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var randomHourMinute = Math.floor(60*Math.random());
|
||||
@@ -85,11 +65,6 @@ function startPostActivationJobs(callback) {
|
||||
start: true
|
||||
});
|
||||
|
||||
settings.on(settings.TIME_ZONE_KEY, recreateJobs);
|
||||
settings.on(settings.APP_AUTOUPDATE_PATTERN_KEY, appAutoupdatePatternChanged);
|
||||
settings.on(settings.BOX_AUTOUPDATE_PATTERN_KEY, boxAutoupdatePatternChanged);
|
||||
settings.on(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
|
||||
|
||||
settings.getAll(function (error, allSettings) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -102,6 +77,19 @@ function startPostActivationJobs(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function handleSettingsChanged(key, value) {
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
|
||||
// value is a variant
|
||||
switch (key) {
|
||||
case settings.TIME_ZONE_KEY: recreateJobs(value); break;
|
||||
case settings.APP_AUTOUPDATE_PATTERN_KEY: appAutoupdatePatternChanged(value); break;
|
||||
case settings.BOX_AUTOUPDATE_PATTERN_KEY: boxAutoupdatePatternChanged(value); break;
|
||||
case settings.DYNAMIC_DNS_KEY: dynamicDnsChanged(value); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
function recreateJobs(tz) {
|
||||
assert.strictEqual(typeof tz, 'string');
|
||||
|
||||
@@ -110,14 +98,14 @@ function recreateJobs(tz) {
|
||||
if (gJobs.backup) gJobs.backup.stop();
|
||||
gJobs.backup = new CronJob({
|
||||
cronTime: '00 00 */6 * * *', // check every 6 hours
|
||||
onTick: backups.ensureBackup.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
|
||||
onTick: backups.ensureBackup.bind(null, auditSource.CRON, NOOP_CALLBACK),
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
|
||||
if (gJobs.systemChecks) gJobs.systemChecks.stop();
|
||||
gJobs.systemChecks = new CronJob({
|
||||
cronTime: '00 30 * * * *', // every hour
|
||||
cronTime: '00 30 * * * *', // every 30 minutes. if you change this interval, change the notification messages with correct duration
|
||||
onTick: cloudron.runSystemChecks,
|
||||
start: true,
|
||||
runOnInit: true, // run system check immediately
|
||||
@@ -130,7 +118,7 @@ function recreateJobs(tz) {
|
||||
if (gJobs.boxUpdateCheckerJob) gJobs.boxUpdateCheckerJob.stop();
|
||||
gJobs.boxUpdateCheckerJob = new CronJob({
|
||||
cronTime: '00 ' + randomMinute + ' * * * *', // once an hour
|
||||
onTick: updateChecker.checkBoxUpdates,
|
||||
onTick: () => updateChecker.checkBoxUpdates(NOOP_CALLBACK),
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
@@ -138,7 +126,7 @@ function recreateJobs(tz) {
|
||||
if (gJobs.appUpdateChecker) gJobs.appUpdateChecker.stop();
|
||||
gJobs.appUpdateChecker = new CronJob({
|
||||
cronTime: '00 ' + randomMinute + ' * * * *', // once an hour
|
||||
onTick: updateChecker.checkAppUpdates,
|
||||
onTick: () => updateChecker.checkAppUpdates(NOOP_CALLBACK),
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
@@ -154,7 +142,7 @@ function recreateJobs(tz) {
|
||||
if (gJobs.cleanupBackups) gJobs.cleanupBackups.stop();
|
||||
gJobs.cleanupBackups = new CronJob({
|
||||
cronTime: '00 45 */6 * * *', // every 6 hours. try not to overlap with ensureBackup job
|
||||
onTick: backups.startCleanupTask.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
|
||||
onTick: backups.startCleanupTask.bind(null, auditSource.CRON, NOOP_CALLBACK),
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
@@ -186,15 +174,7 @@ function recreateJobs(tz) {
|
||||
if (gJobs.certificateRenew) gJobs.certificateRenew.stop();
|
||||
gJobs.certificateRenew = new CronJob({
|
||||
cronTime: '00 00 */12 * * *', // every 12 hours
|
||||
onTick: cloudron.renewCerts.bind(null, {}, AUDIT_SOURCE, NOOP_CALLBACK),
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
|
||||
if (gJobs.digestEmail) gJobs.digestEmail.stop();
|
||||
gJobs.digestEmail = new CronJob({
|
||||
cronTime: '00 00 00 * * 3', // every wednesday
|
||||
onTick: digest.maybeSend,
|
||||
onTick: cloudron.renewCerts.bind(null, {}, auditSource.CRON, NOOP_CALLBACK),
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
@@ -224,7 +204,7 @@ function boxAutoupdatePatternChanged(pattern) {
|
||||
var updateInfo = updateChecker.getUpdateInfo();
|
||||
if (updateInfo.box) {
|
||||
debug('Starting autoupdate to %j', updateInfo.box);
|
||||
updater.updateToLatest(AUDIT_SOURCE, NOOP_CALLBACK);
|
||||
updater.updateToLatest({ skipBackup: false }, auditSource.CRON, NOOP_CALLBACK);
|
||||
} else {
|
||||
debug('No box auto updates available');
|
||||
}
|
||||
@@ -250,7 +230,7 @@ function appAutoupdatePatternChanged(pattern) {
|
||||
var updateInfo = updateChecker.getUpdateInfo();
|
||||
if (updateInfo.apps) {
|
||||
debug('Starting app update to %j', updateInfo.apps);
|
||||
apps.autoupdateApps(updateInfo.apps, AUDIT_SOURCE, NOOP_CALLBACK);
|
||||
apps.autoupdateApps(updateInfo.apps, auditSource.CRON, NOOP_CALLBACK);
|
||||
} else {
|
||||
debug('No app auto updates available');
|
||||
}
|
||||
@@ -269,7 +249,7 @@ function dynamicDnsChanged(enabled) {
|
||||
if (enabled) {
|
||||
gJobs.dynamicDns = new CronJob({
|
||||
cronTime: '00 */10 * * * *',
|
||||
onTick: dyndns.sync,
|
||||
onTick: dyndns.sync.bind(null, auditSource.CRON, NOOP_CALLBACK),
|
||||
start: true,
|
||||
timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack
|
||||
});
|
||||
@@ -282,11 +262,6 @@ function dynamicDnsChanged(enabled) {
|
||||
function stopJobs(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.removeListener(settings.TIME_ZONE_KEY, recreateJobs);
|
||||
settings.removeListener(settings.APP_AUTOUPDATE_PATTERN_KEY, appAutoupdatePatternChanged);
|
||||
settings.removeListener(settings.BOX_AUTOUPDATE_PATTERN_KEY, boxAutoupdatePatternChanged);
|
||||
settings.removeListener(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
|
||||
|
||||
for (var job in gJobs) {
|
||||
if (!gJobs[job]) continue;
|
||||
gJobs[job].stop();
|
||||
|
||||
67
src/custom.js
Normal file
67
src/custom.js
Normal file
@@ -0,0 +1,67 @@
|
||||
'use strict';
|
||||
|
||||
let config = require('./config.js'),
|
||||
debug = require('debug')('box:features'),
|
||||
lodash = require('lodash'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
yaml = require('js-yaml');
|
||||
|
||||
exports = module.exports = {
|
||||
uiSpec: uiSpec,
|
||||
spec: spec
|
||||
};
|
||||
|
||||
const DEFAULT_SPEC = {
|
||||
appstore: {
|
||||
blacklist: [],
|
||||
whitelist: null // null imples, not set. this is an object and not an array
|
||||
},
|
||||
backups: {
|
||||
configurable: true
|
||||
},
|
||||
domains: {
|
||||
dynamicDns: true,
|
||||
changeDashboardDomain: true
|
||||
},
|
||||
subscription: {
|
||||
configurable: true
|
||||
},
|
||||
support: {
|
||||
email: 'support@cloudron.io',
|
||||
remoteSupport: true,
|
||||
ticketFormBody:
|
||||
'Use this form to open support tickets. You can also write directly to [support@cloudron.io](mailto:support@cloudron.io).\n\n'
|
||||
+ `* [Knowledge Base & App Docs](${config.webServerOrigin()}/documentation/apps/?support_view)\n`
|
||||
+ `* [Custom App Packaging & API](${config.webServerOrigin()}/developer/packaging/?support_view)\n`
|
||||
+ '* [Forum](https://forum.cloudron.io/)\n\n',
|
||||
submitTickets: true
|
||||
},
|
||||
alerts: {
|
||||
email: '',
|
||||
notifyCloudronAdmins: false
|
||||
},
|
||||
footer: {
|
||||
body: '© 2019 [Cloudron](https://cloudron.io) [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)'
|
||||
}
|
||||
};
|
||||
|
||||
const gSpec = (function () {
|
||||
try {
|
||||
if (!safe.fs.existsSync(paths.CUSTOM_FILE)) return 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 DEFAULT_SPEC;
|
||||
}
|
||||
})();
|
||||
|
||||
// flags sent to the UI. this is separate because we have values that are secret to the backend
|
||||
function uiSpec() {
|
||||
return gSpec;
|
||||
}
|
||||
|
||||
function spec() {
|
||||
return gSpec;
|
||||
}
|
||||
@@ -36,7 +36,8 @@ function initialize(callback) {
|
||||
port: config.database().port,
|
||||
database: config.database().name,
|
||||
multipleStatements: false,
|
||||
ssl: false
|
||||
ssl: false,
|
||||
timezone: 'Z' // mysql follows the SYSTEM timezone. on Cloudron, this is UTC
|
||||
});
|
||||
|
||||
gConnectionPool.on('connection', function (connection) {
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var debug = require('debug')('box:digest'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
updatechecker = require('./updatechecker.js'),
|
||||
mailer = require('./mailer.js'),
|
||||
settings = require('./settings.js');
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
exports = module.exports = {
|
||||
maybeSend: maybeSend
|
||||
};
|
||||
|
||||
function maybeSend(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
settings.getEmailDigest(function (error, enabled) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (!enabled) {
|
||||
debug('Email digest is disabled');
|
||||
return callback();
|
||||
}
|
||||
|
||||
var updateInfo = updatechecker.getUpdateInfo();
|
||||
var pendingAppUpdates = updateInfo.apps || {};
|
||||
pendingAppUpdates = Object.keys(pendingAppUpdates).map(function (key) { return pendingAppUpdates[key]; });
|
||||
|
||||
eventlog.getByCreationTime(new Date(new Date() - 7*86400000), function (error, events) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var appUpdates = events.filter(function (e) { return e.action === eventlog.ACTION_APP_UPDATE; }).map(function (e) { return e.data; });
|
||||
var boxUpdates = events.filter(function (e) { return e.action === eventlog.ACTION_UPDATE; }).map(function (e) { return e.data; });
|
||||
var certRenewals = events.filter(function (e) { return e.action === eventlog.ACTION_CERTIFICATE_RENEWAL; }).map(function (e) { return e.data; });
|
||||
var usersAdded = events.filter(function (e) { return e.action === eventlog.ACTION_USER_ADD; }).map(function (e) { return e.data; });
|
||||
var usersRemoved = events.filter(function (e) { return e.action === eventlog.ACTION_USER_REMOVE; }).map(function (e) { return e.data; });
|
||||
var finishedBackups = events.filter(function (e) { return e.action === eventlog.ACTION_BACKUP_FINISH && !e.errorMessage; }).map(function (e) { return e.data; });
|
||||
|
||||
if (error) return callback(error);
|
||||
|
||||
var info = {
|
||||
pendingAppUpdates: pendingAppUpdates,
|
||||
pendingBoxUpdate: updateInfo.box || null,
|
||||
|
||||
finishedAppUpdates: appUpdates,
|
||||
finishedBoxUpdates: boxUpdates,
|
||||
|
||||
certRenewals: certRenewals,
|
||||
finishedBackups: finishedBackups, // only the successful backups
|
||||
usersAdded: usersAdded,
|
||||
usersRemoved: usersRemoved // unused because we don't have username to work with
|
||||
};
|
||||
|
||||
// always send digest for backup failure notification
|
||||
debug('maybeSend: sending digest email', info);
|
||||
mailer.sendDigest(info);
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -58,7 +58,7 @@ function upsert(domainObject, location, type, values, callback) {
|
||||
};
|
||||
|
||||
superagent
|
||||
.post(config.apiServerOrigin() + '/api/v1/domains/' + fqdn)
|
||||
.post(config.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/domains/' + fqdn)
|
||||
.get(config.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/domains/' + getFqdn(location, domainObject.domain))
|
||||
.del(config.apiServerOrigin() + '/api/v1/caas/domains/' + getFqdn(location, domainObject.domain))
|
||||
.query({ token: dnsConfig.token })
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
|
||||
@@ -45,17 +45,20 @@ function getInternal(dnsConfig, zoneName, name, type, callback) {
|
||||
|
||||
var nextPage = null, matchingRecords = [];
|
||||
|
||||
debug(`getInternal: getting dns records of ${zoneName} with ${name} and type ${type}`);
|
||||
|
||||
async.doWhilst(function (iteratorDone) {
|
||||
var url = nextPage ? nextPage : DIGITALOCEAN_ENDPOINT + '/v2/domains/' + zoneName + '/records';
|
||||
|
||||
superagent.get(url)
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.retry(5)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 404) return callback(new DomainsError(DomainsError.NOT_FOUND, formatError(result)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
|
||||
if (error && !error.response) return iteratorDone(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 404) return iteratorDone(new DomainsError(DomainsError.NOT_FOUND, formatError(result)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return iteratorDone(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 200) return iteratorDone(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
matchingRecords = matchingRecords.concat(result.body.domain_records.filter(function (record) {
|
||||
return (record.type === type && record.name === name);
|
||||
@@ -63,12 +66,14 @@ 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) {
|
||||
if (error) return callback(error);
|
||||
debug('getInternal:', error, matchingRecords);
|
||||
|
||||
debug('getInternal: %j', matchingRecords);
|
||||
if (error) return callback(error);
|
||||
|
||||
return callback(null, matchingRecords);
|
||||
});
|
||||
@@ -106,7 +111,7 @@ function upsert(domainObject, location, type, values, callback) {
|
||||
name: name,
|
||||
data: value,
|
||||
priority: priority,
|
||||
ttl: 1
|
||||
ttl: 30 // Recent DO DNS API break means this value must atleast be 30
|
||||
};
|
||||
|
||||
if (i >= result.length) {
|
||||
@@ -114,6 +119,7 @@ function upsert(domainObject, location, type, values, callback) {
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.retry(5)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return iteratorCallback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
|
||||
@@ -129,6 +135,7 @@ function upsert(domainObject, location, type, values, callback) {
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.retry(5)
|
||||
.end(function (error, result) {
|
||||
// increment, as we have consumed the record
|
||||
++i;
|
||||
@@ -143,7 +150,13 @@ function upsert(domainObject, location, type, values, callback) {
|
||||
return iteratorCallback(null);
|
||||
});
|
||||
}
|
||||
}, callback);
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('upsert: completed with recordIds:%j', recordIds);
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -196,6 +209,7 @@ function del(domainObject, location, type, values, callback) {
|
||||
superagent.del(DIGITALOCEAN_ENDPOINT + '/v2/domains/' + zoneName + '/records/' + tmp[0].id)
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.retry(5)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 404) return callback(null);
|
||||
@@ -245,7 +259,7 @@ function verifyDnsConfig(domainObject, callback) {
|
||||
|
||||
if (nameservers.map(function (n) { return n.toLowerCase(); }).indexOf('ns1.digitalocean.com') === -1) {
|
||||
debug('verifyDnsConfig: %j does not contains DO NS', nameservers);
|
||||
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Digital Ocean'));
|
||||
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to DigitalOcean'));
|
||||
}
|
||||
|
||||
const location = 'cloudrontestdns';
|
||||
|
||||
@@ -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,31 @@ function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getApi(dnsConfig, function (error, namecheap) {
|
||||
getQuery(dnsConfig, function (error, query) {
|
||||
if (error) return callback(error);
|
||||
|
||||
namecheap.domains.dns.getHosts(zoneName, function (error, result) {
|
||||
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(error)));
|
||||
query.Command = 'namecheap.domains.dns.getHosts';
|
||||
query.SLD = zoneName.split('.')[0];
|
||||
query.TLD = zoneName.split('.')[1];
|
||||
|
||||
debug('entire getInternal response: %j', result);
|
||||
superagent.get(ENDPOINT).query(query).end(function (error, result) {
|
||||
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, result['DomainDNSGetHostsResult']['host']);
|
||||
var parser = new xml2js.Parser();
|
||||
parser.parseString(result.text, function (error, result) {
|
||||
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error));
|
||||
|
||||
var tmp = result.ApiResponse;
|
||||
if (tmp['$'].Status !== 'OK') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, safe.query(tmp, 'Errors[0].Error[0]._', 'Invalid response')));
|
||||
if (!tmp.CommandResponse[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
|
||||
if (!tmp.CommandResponse[0].DomainDNSGetHostsResult[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
|
||||
|
||||
var hosts = result.ApiResponse.CommandResponse[0].DomainDNSGetHostsResult[0].host.map(function (h) {
|
||||
return h['$'];
|
||||
});
|
||||
|
||||
callback(null, hosts);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -93,15 +91,42 @@ function setInternal(dnsConfig, zoneName, hosts, callback) {
|
||||
assert(Array.isArray(hosts));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let mappedHosts = mapHosts(hosts);
|
||||
|
||||
getApi(dnsConfig, function (error, namecheap) {
|
||||
getQuery(dnsConfig, function (error, query) {
|
||||
if (error) return callback(error);
|
||||
|
||||
namecheap.domains.dns.setHosts(zoneName, mappedHosts, function (error, result) {
|
||||
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(error)));
|
||||
query.Command = 'namecheap.domains.dns.setHosts';
|
||||
query.SLD = zoneName.split('.')[0];
|
||||
query.TLD = zoneName.split('.')[1];
|
||||
|
||||
return callback(null, result);
|
||||
// Map to query params https://www.namecheap.com/support/api/methods/domains-dns/set-hosts.aspx
|
||||
hosts.forEach(function (host, i) {
|
||||
var n = i+1; // api starts with 1 not 0
|
||||
query['TTL' + n] = '300'; // keep it low
|
||||
query['HostName' + n] = host.HostName || host.Name;
|
||||
query['RecordType' + n] = host.RecordType || host.Type;
|
||||
query['Address' + n] = host.Address;
|
||||
|
||||
if (host.Type === 'MX') {
|
||||
query['EmailType' + n] = 'MX';
|
||||
if (host.MXPref) query['MXPref' + n] = host.MXPref;
|
||||
}
|
||||
});
|
||||
|
||||
superagent.post(ENDPOINT).query(query).end(function (error, result) {
|
||||
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error));
|
||||
|
||||
var parser = new xml2js.Parser();
|
||||
parser.parseString(result.text, function (error, result) {
|
||||
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error));
|
||||
|
||||
var tmp = result.ApiResponse;
|
||||
if (tmp['$'].Status !== 'OK') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, safe.query(tmp, 'Errors[0].Error[0]._', 'Invalid response')));
|
||||
if (!tmp.CommandResponse[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
|
||||
if (!tmp.CommandResponse[0].DomainDNSSetHostsResult[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
|
||||
if (tmp.CommandResponse[0].DomainDNSSetHostsResult[0]['$'].IsSuccess !== 'true') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ function getInternal(dnsConfig, zoneName, name, type, callback) {
|
||||
|
||||
result.body.records.forEach(function (r) {
|
||||
// name.com api simply strips empty properties
|
||||
r.host = r.host || '@';
|
||||
r.host = r.host || '';
|
||||
});
|
||||
|
||||
var results = result.body.records.filter(function (r) {
|
||||
@@ -153,7 +153,7 @@ function upsert(domainObject, location, type, values, callback) {
|
||||
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
name = domains.getName(domainObject, location, type) || '@';
|
||||
name = domains.getName(domainObject, location, type) || '';
|
||||
|
||||
debug(`upsert: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
|
||||
@@ -174,7 +174,7 @@ function get(domainObject, location, type, callback) {
|
||||
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
name = domains.getName(domainObject, location, type) || '@';
|
||||
name = domains.getName(domainObject, location, type) || '';
|
||||
|
||||
getInternal(dnsConfig, zoneName, name, type, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
@@ -196,7 +196,7 @@ function del(domainObject, location, type, values, callback) {
|
||||
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
name = domains.getName(domainObject, location, type) || '@';
|
||||
name = domains.getName(domainObject, location, type) || '';
|
||||
|
||||
debug(`del: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ exports = module.exports = {
|
||||
getContainerIdByIp: getContainerIdByIp,
|
||||
inspect: inspect,
|
||||
inspectByName: inspect,
|
||||
getEvents: getEvents,
|
||||
memoryUsage: memoryUsage,
|
||||
execContainer: execContainer,
|
||||
createVolume: createVolume,
|
||||
@@ -180,14 +181,17 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
var manifest = app.manifest;
|
||||
var exposedPorts = {}, dockerPortBindings = { };
|
||||
var domain = app.fqdn;
|
||||
// TODO: these should all have the CLOUDRON_ prefix
|
||||
var stdEnv = [
|
||||
|
||||
const envPrefix = manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
let stdEnv = [
|
||||
'CLOUDRON=1',
|
||||
'CLOUDRON_PROXY_IP=172.18.0.1',
|
||||
'WEBADMIN_ORIGIN=' + config.adminOrigin(),
|
||||
'API_ORIGIN=' + config.adminOrigin(),
|
||||
'APP_ORIGIN=https://' + domain,
|
||||
'APP_DOMAIN=' + domain
|
||||
`CLOUDRON_APP_HOSTNAME=${name}`,
|
||||
`${envPrefix}WEBADMIN_ORIGIN=${config.adminOrigin()}`,
|
||||
`${envPrefix}API_ORIGIN=${config.adminOrigin()}`,
|
||||
`${envPrefix}APP_ORIGIN=https://${domain}`,
|
||||
`${envPrefix}APP_DOMAIN=${domain}`
|
||||
];
|
||||
|
||||
// docker portBindings requires ports to be exposed
|
||||
@@ -234,9 +238,9 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
// Note that Hostname has no effect on DNS. We have to use the --net-alias for dns.
|
||||
// Hostname cannot be set with container NetworkMode
|
||||
var containerOptions = {
|
||||
name: name, // used for filtering logs
|
||||
name: name, // for referencing containers
|
||||
Tty: isAppContainer,
|
||||
Hostname: app.id, // set to something 'constant' so app containers can use this to communicate (across app updates)
|
||||
Hostname: name,
|
||||
Image: app.manifest.dockerImage,
|
||||
Cmd: (isAppContainer && app.debugMode && app.debugMode.cmd) ? app.debugMode.cmd : cmd,
|
||||
Env: stdEnv.concat(addonEnv).concat(portEnv).concat(appEnv),
|
||||
@@ -272,10 +276,17 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
},
|
||||
CpuShares: 512, // relative to 1024 for system processes
|
||||
VolumesFrom: isAppContainer ? null : [ app.containerId + ':rw' ],
|
||||
NetworkMode: 'cloudron',
|
||||
NetworkMode: 'cloudron', // user defined bridge network
|
||||
Dns: ['172.18.0.1'], // use internal dns
|
||||
DnsSearch: ['.'], // use internal dns
|
||||
SecurityOpt: [ 'apparmor=docker-cloudron-app' ]
|
||||
},
|
||||
NetworkingConfig: {
|
||||
EndpointsConfig: {
|
||||
cloudron: {
|
||||
Aliases: [ name ] // this allows sub-containers reach app containers by name
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -474,6 +485,19 @@ function inspect(containerId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getEvents(options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let docker = exports.connection;
|
||||
|
||||
docker.getEvents(options, function (error, stream) {
|
||||
if (error) return callback(new DockerError(DockerError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, stream);
|
||||
});
|
||||
}
|
||||
|
||||
function memoryUsage(containerId, callback) {
|
||||
assert.strictEqual(typeof containerId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -145,7 +145,6 @@ function validateHostname(location, domainObject) {
|
||||
const hostname = fqdn(location, domainObject);
|
||||
|
||||
const RESERVED_LOCATIONS = [
|
||||
constants.API_LOCATION,
|
||||
constants.SMTP_LOCATION,
|
||||
constants.IMAP_LOCATION
|
||||
];
|
||||
|
||||
@@ -6,8 +6,10 @@ exports = module.exports = {
|
||||
|
||||
var appdb = require('./appdb.js'),
|
||||
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'),
|
||||
@@ -15,11 +17,10 @@ var appdb = require('./appdb.js'),
|
||||
safe = require('safetydance'),
|
||||
sysinfo = require('./sysinfo.js');
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
// called for dynamic dns setups where we have to update the IP
|
||||
function sync(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
function sync(auditSource, callback) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
@@ -32,7 +33,7 @@ function sync(callback) {
|
||||
|
||||
debug(`refreshDNS: updating ip from ${info.ip} to ${ip}`);
|
||||
|
||||
domains.upsertDnsRecords(config.adminLocation(), config.adminDomain(), 'A', [ ip ], function (error) {
|
||||
domains.upsertDnsRecords(constants.ADMIN_LOCATION, config.adminDomain(), 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('refreshDNS: updated admin location');
|
||||
@@ -50,7 +51,7 @@ function sync(callback) {
|
||||
|
||||
debug('refreshDNS: updated apps');
|
||||
|
||||
eventlog.add(eventlog.ACTION_DYNDNS_UPDATE, { userId: null, username: 'cron' }, { fromIp: info.ip, toIp: ip });
|
||||
eventlog.add(eventlog.ACTION_DYNDNS_UPDATE, auditSource, { fromIp: info.ip, toIp: ip });
|
||||
info.ip = ip;
|
||||
safe.fs.writeFileSync(paths.DYNDNS_INFO_FILE, JSON.stringify(info), 'utf8');
|
||||
|
||||
|
||||
@@ -17,11 +17,14 @@ exports = module.exports = {
|
||||
ACTION_APP_RESTORE: 'app.restore',
|
||||
ACTION_APP_UNINSTALL: 'app.uninstall',
|
||||
ACTION_APP_UPDATE: 'app.update',
|
||||
ACTION_APP_UPDATE_FINISH: 'app.update.finish',
|
||||
ACTION_APP_LOGIN: 'app.login',
|
||||
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',
|
||||
@@ -105,28 +108,11 @@ function add(action, source, data, callback) {
|
||||
api(uuid.v4(), action, source, data, function (error, id) {
|
||||
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
|
||||
|
||||
// decide if we want to add notifications as well
|
||||
if (action === exports.ACTION_USER_ADD) {
|
||||
notifications.userAdded(source.userId, id, data.user);
|
||||
} else if (action === exports.ACTION_USER_REMOVE) {
|
||||
notifications.userRemoved(source.userId, id, data.user);
|
||||
} else if (action === exports.ACTION_USER_UPDATE && data.adminStatusChanged) {
|
||||
notifications.adminChanged(source.userId, id, data.user);
|
||||
} else if (action === exports.ACTION_APP_OOM) {
|
||||
notifications.oomEvent(id, data.app ? data.app.id : data.containerId, { app: data.app, details: data });
|
||||
} else if (action === exports.ACTION_APP_DOWN) {
|
||||
notifications.appDied(id, data.app);
|
||||
} else if (action === exports.ACTION_APP_UP) {
|
||||
notifications.appUp(id, data.app);
|
||||
} else if (action === exports.ACTION_APP_TASK_CRASH) {
|
||||
notifications.apptaskCrash(id, data.appId, data.crashLogFile);
|
||||
} else if (action === exports.ACTION_PROCESS_CRASH) {
|
||||
notifications.processCrash(id, data.processName, data.crashLogFile);
|
||||
} else {
|
||||
// no notification
|
||||
}
|
||||
notifications.onEvent(id, action, source, data, function (error) {
|
||||
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, { id: id });
|
||||
callback(null, { id: id });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ function clear(callback) {
|
||||
database.query('DELETE FROM eventlog', function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(error);
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -153,19 +153,19 @@ function delByCreationTime(creationTime, callback) {
|
||||
assert(util.isDate(creationTime));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// since notifications reference eventlog items, we have to clean them up as well
|
||||
database.query('SELECT * FROM eventlog WHERE creationTime < ?', [ creationTime ], function (error, result) {
|
||||
// remove notifications that reference the events as well
|
||||
database.query('SELECT * FROM eventlog WHERE creationTime <= ?', [ creationTime ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
async.eachSeries(result, function (item, callback) {
|
||||
database.query('DELETE FROM notifications WHERE eventId=?', [ item.id ], function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
async.eachSeries(result, function (item, iteratorCallback) {
|
||||
async.series([
|
||||
database.query.bind(null, 'DELETE FROM notifications WHERE eventId=?', [ item.id ]),
|
||||
database.query.bind(null, 'DELETE FROM eventlog WHERE id=?', [ item.id ])
|
||||
], iteratorCallback);
|
||||
}, function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
database.query('DELETE FROM eventlog WHERE id=?', [ item.id ], function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}, callback);
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ function startGraphite(existingInfra, callback) {
|
||||
if (existingInfra.version === infra.version && infra.images.graphite.tag === existingInfra.images.graphite.tag) return callback();
|
||||
|
||||
const cmd = `docker run --restart=always -d --name="graphite" \
|
||||
--hostname graphite \
|
||||
--net cloudron \
|
||||
--net-alias graphite \
|
||||
--log-driver syslog \
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
exports = module.exports = {
|
||||
// a version change recreates all containers with latest docker config
|
||||
'version': '48.12.1',
|
||||
'version': '48.15.0',
|
||||
|
||||
'baseImages': [
|
||||
{ repo: 'cloudron/base', tag: 'cloudron/base:1.0.0@sha256:147a648a068a2e746644746bbfb42eb7a50d682437cead3c67c933c546357617' }
|
||||
@@ -15,11 +15,12 @@ exports = module.exports = {
|
||||
// a major version bump in the db containers will trigger the restore logic that uses the db dumps
|
||||
// docker inspect --format='{{index .RepoDigests 0}}' $IMAGE to get the sha256
|
||||
'images': {
|
||||
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.0.1@sha256:5a13360da4a2085c7d474bea6b1090c5eb24732d4f73459942af7612d4993d7f' },
|
||||
'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' },
|
||||
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.0.0@sha256:8a88dd334b62b578530a014ca1a2425a54cb9df1e475f5d3a36806e5cfa22121' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.1.0@sha256:131db42dcb90111f679ab1f0f37c552f93f797d9b803b2346c7c202daf86ac36' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.2@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' }
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.3.1@sha256:9693e3ae42a12a7ac8cf5df94d828d46f5b22b4e2e1c7d1bc614d6ee2a22c365' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.2@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' },
|
||||
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:0.1.0@sha256:e177c5bf5f38c84ce1dea35649c22a1b05f96eec67a54a812c5a35e585670f0f' }
|
||||
}
|
||||
};
|
||||
|
||||
97
src/ldap.js
97
src/ldap.js
@@ -13,13 +13,14 @@ var assert = require('assert'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:ldap'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
users = require('./users.js'),
|
||||
UsersError = users.UsersError,
|
||||
ldap = require('ldapjs'),
|
||||
mail = require('./mail.js'),
|
||||
MailError = mail.MailError,
|
||||
mailboxdb = require('./mailboxdb.js'),
|
||||
safe = require('safetydance');
|
||||
path = require('path'),
|
||||
safe = require('safetydance'),
|
||||
users = require('./users.js'),
|
||||
UsersError = users.UsersError;
|
||||
|
||||
var gServer = null;
|
||||
|
||||
@@ -95,9 +96,9 @@ function finalSend(results, req, res, next) {
|
||||
|
||||
var resultCookie;
|
||||
if (last < max) {
|
||||
resultCookie = new Buffer(last.toString());
|
||||
resultCookie = Buffer.from(last.toString());
|
||||
} else {
|
||||
resultCookie = new Buffer('');
|
||||
resultCookie = Buffer.from('');
|
||||
}
|
||||
|
||||
res.controls.push(new ldap.PagedResultsControl({
|
||||
@@ -270,11 +271,7 @@ function mailboxSearch(req, res, next) {
|
||||
objectcategory: 'mailbox',
|
||||
cn: `${mailbox.name}@${mailbox.domain}`,
|
||||
uid: `${mailbox.name}@${mailbox.domain}`,
|
||||
mail: `${mailbox.name}@${mailbox.domain}`,
|
||||
displayname: 'Max Mustermann',
|
||||
givenName: 'Max',
|
||||
username: 'mmustermann',
|
||||
samaccountname: 'mmustermann'
|
||||
mail: `${mailbox.name}@${mailbox.domain}`
|
||||
}
|
||||
};
|
||||
|
||||
@@ -481,6 +478,71 @@ function authenticateUserMailbox(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function authenticateSftp(req, res, next) {
|
||||
debug('sftp auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
|
||||
|
||||
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
var email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
|
||||
var parts = email.split('@');
|
||||
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
// actual user bind
|
||||
users.verifyWithUsername(parts[0], req.credentials, function (error) {
|
||||
if (error) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
|
||||
|
||||
debug('sftp auth: success');
|
||||
|
||||
res.end();
|
||||
});
|
||||
}
|
||||
|
||||
function userSearchSftp(req, res, next) {
|
||||
debug('sftp user search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id);
|
||||
|
||||
if (req.filter.attribute !== 'username' || !req.filter.value) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
var parts = req.filter.value.split('@');
|
||||
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
var username = parts[0];
|
||||
var appFqdn = parts[1];
|
||||
|
||||
apps.getByFqdn(appFqdn, function (error, app) {
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
|
||||
// only allow apps which specify "ftp" support in the localstorage addon
|
||||
if (!safe.query(app.manifest.addons, 'localstorage.ftp.uid')) return next(new ldap.UnavailableError('Not supported'));
|
||||
if (typeof app.manifest.addons.localstorage.ftp.uid !== 'number') return next(new ldap.UnavailableError('Bad uid, must be a number'));
|
||||
|
||||
const uidNumber = app.manifest.addons.localstorage.ftp.uid;
|
||||
|
||||
users.getByUsername(username, function (error, user) {
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
|
||||
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
|
||||
}
|
||||
};
|
||||
|
||||
finalSend([ obj ], req, res, next);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function authenticateMailAddon(req, res, next) {
|
||||
debug('mail addon auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
|
||||
|
||||
@@ -498,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 });
|
||||
@@ -542,6 +604,10 @@ function start(callback) {
|
||||
|
||||
gServer = ldap.createServer({ log: logger });
|
||||
|
||||
gServer.on('error', function (error) {
|
||||
console.error('LDAP:', error);
|
||||
});
|
||||
|
||||
gServer.search('ou=users,dc=cloudron', authenticateApp, userSearch);
|
||||
gServer.search('ou=groups,dc=cloudron', authenticateApp, groupSearch);
|
||||
gServer.bind('ou=users,dc=cloudron', authenticateApp, authenticateUser, authorizeUserForApp);
|
||||
@@ -555,6 +621,9 @@ function start(callback) {
|
||||
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailAddon); // dovecot
|
||||
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailAddon); // haraka
|
||||
|
||||
gServer.bind('ou=sftp,dc=cloudron', authenticateSftp); // sftp
|
||||
gServer.search('ou=sftp,dc=cloudron', userSearchSftp);
|
||||
|
||||
gServer.compare('cn=users,ou=groups,dc=cloudron', authenticateApp, groupUsersCompare);
|
||||
gServer.compare('cn=admins,ou=groups,dc=cloudron', authenticateApp, groupAdminsCompare);
|
||||
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
sendFailureLogs: sendFailureLogs
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
safe = require('safetydance'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
util = require('util');
|
||||
|
||||
var COLLECT_LOGS_CMD = path.join(__dirname, 'scripts/collectlogs.sh');
|
||||
|
||||
var CRASH_LOG_TIMESTAMP_OFFSET = 1000 * 60 * 60; // 60 min
|
||||
var CRASH_LOG_TIMESTAMP_FILE = '/tmp/crashlog.timestamp';
|
||||
var CRASH_LOG_STASH_FILE = '/tmp/crashlog';
|
||||
|
||||
const AUDIT_SOURCE = { userId: null, username: 'healthmonitor' };
|
||||
|
||||
function collectLogs(unitName, callback) {
|
||||
assert.strictEqual(typeof unitName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var logs = safe.child_process.execSync('sudo ' + COLLECT_LOGS_CMD + ' ' + unitName, { encoding: 'utf8' });
|
||||
if (!logs) return callback(safe.error);
|
||||
|
||||
logs = logs + '\n\n=====================================\n\n';
|
||||
|
||||
// special case for box since the real logs are at path.join(paths.LOG_DIR, 'box.log')
|
||||
if (unitName === 'box.service') {
|
||||
logs += safe.child_process.execSync('tail --lines=500 ' + path.join(paths.LOG_DIR, 'box.log'), { encoding: 'utf8' });
|
||||
}
|
||||
|
||||
callback(null, logs);
|
||||
}
|
||||
|
||||
function sendFailureLogs(unitName) {
|
||||
assert.strictEqual(typeof unitName, 'string');
|
||||
|
||||
collectLogs(unitName, function (error, logs) {
|
||||
if (error) {
|
||||
console.error('Failed to collect logs.', error);
|
||||
logs = util.format('Failed to collect logs.', error);
|
||||
}
|
||||
|
||||
console.log('Sending failure logs for', unitName);
|
||||
|
||||
if (!safe.fs.writeFileSync(CRASH_LOG_STASH_FILE, logs)) console.log(`Failed to stash logs to ${CRASH_LOG_STASH_FILE}`);
|
||||
|
||||
var timestamp = safe.fs.readFileSync(CRASH_LOG_TIMESTAMP_FILE, 'utf8');
|
||||
|
||||
// check if we already sent a mail in the last CRASH_LOG_TIME_OFFSET window
|
||||
if (timestamp && (parseInt(timestamp) + CRASH_LOG_TIMESTAMP_OFFSET) > Date.now()) {
|
||||
console.log('Crash log already sent within window. Stashing logs.');
|
||||
return;
|
||||
}
|
||||
|
||||
eventlog.add(eventlog.ACTION_PROCESS_CRASH, AUDIT_SOURCE, { processName: unitName, crashLogFile: CRASH_LOG_STASH_FILE }, function (error) {
|
||||
if (error) console.log(`Error sending crashlog. Logs stashed at ${CRASH_LOG_STASH_FILE}`);
|
||||
|
||||
// write the new timestamp file and delete stash file
|
||||
safe.fs.writeFileSync(CRASH_LOG_TIMESTAMP_FILE, String(Date.now()));
|
||||
});
|
||||
});
|
||||
}
|
||||
245
src/mail.js
245
src/mail.js
@@ -2,6 +2,7 @@
|
||||
|
||||
exports = module.exports = {
|
||||
getStatus: getStatus,
|
||||
checkConfiguration: checkConfiguration,
|
||||
|
||||
getDomains: getDomains,
|
||||
|
||||
@@ -13,7 +14,7 @@ exports = module.exports = {
|
||||
removePrivateFields: removePrivateFields,
|
||||
|
||||
setDnsRecords: setDnsRecords,
|
||||
setMailFqdn: setMailFqdn,
|
||||
onMailFqdnChanged: onMailFqdnChanged,
|
||||
|
||||
validateName: validateName,
|
||||
|
||||
@@ -23,6 +24,8 @@ exports = module.exports = {
|
||||
setMailEnabled: setMailEnabled,
|
||||
|
||||
startMail: restartMail,
|
||||
restartMail: restartMail,
|
||||
handleCertChanged: handleCertChanged,
|
||||
|
||||
sendTestMail: sendTestMail,
|
||||
|
||||
@@ -75,7 +78,7 @@ var assert = require('assert'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
const DNS_OPTIONS = { server: '127.0.0.1', timeout: 5000 }; // unbound runs on 127.0.0.1
|
||||
const DNS_OPTIONS = { timeout: 5000 };
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
function MailError(reason, errorOrMessage) {
|
||||
@@ -103,7 +106,6 @@ MailError.BAD_FIELD = 'Bad Field';
|
||||
MailError.ALREADY_EXISTS = 'Already Exists';
|
||||
MailError.NOT_FOUND = 'Not Found';
|
||||
MailError.IN_USE = 'In Use';
|
||||
MailError.BILLING_REQUIRED = 'Billing Required';
|
||||
|
||||
function validateName(name) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
@@ -163,16 +165,24 @@ function checkSmtpRelay(relay, callback) {
|
||||
status: false
|
||||
};
|
||||
|
||||
var transporter = nodemailer.createTransport(smtpTransport({
|
||||
var options = {
|
||||
connectionTimeout: 5000,
|
||||
greetingTimeout: 5000,
|
||||
host: relay.host,
|
||||
port: relay.port,
|
||||
auth: {
|
||||
port: relay.port
|
||||
};
|
||||
|
||||
// only set auth if either username or password is provided, some relays auth based on IP (range)
|
||||
if (relay.username || relay.password) {
|
||||
options.auth = {
|
||||
user: relay.username,
|
||||
pass: relay.password
|
||||
}
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
if (relay.acceptSelfSignedCerts) options.tls = { rejectUnauthorized: false };
|
||||
|
||||
var transporter = nodemailer.createTransport(smtpTransport(options));
|
||||
|
||||
transporter.verify(function(error) {
|
||||
result.status = !error;
|
||||
@@ -191,7 +201,7 @@ function verifyRelay(relay, callback) {
|
||||
|
||||
// we used to verify cloudron-smtp with checkOutboundPort25 but that is unreliable given that we just
|
||||
// randomly select some smtp server
|
||||
if (relay.provider === 'cloudron-smtp') return callback();
|
||||
if (relay.provider === 'cloudron-smtp' || relay.provider === 'noop') return callback();
|
||||
|
||||
checkSmtpRelay(relay, function (error) {
|
||||
if (error) return callback(new MailError(MailError.BAD_FIELD, error.message));
|
||||
@@ -200,10 +210,14 @@ function verifyRelay(relay, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function checkDkim(domain, callback) {
|
||||
var dkim = {
|
||||
domain: config.dkimSelector() + '._domainkey.' + domain,
|
||||
name: config.dkimSelector() + '._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,
|
||||
@@ -279,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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -454,9 +480,9 @@ function getStatus(domain, callback) {
|
||||
|
||||
// ensure we always have a valid toplevel properties for the api
|
||||
var results = {
|
||||
dns: {},
|
||||
rbl: {},
|
||||
relay: {}
|
||||
dns: {}, // { mx: { expected, value }, dmarc: { expected, value }, dkim: { expected, value }, spf: { expected, value }, ptr: { expected, value } }
|
||||
rbl: {}, // { status, ip, servers: [{name,site,dns}]} optional. only for cloudron-smtp
|
||||
relay: {} // { status, value } always checked
|
||||
};
|
||||
|
||||
function recordResult(what, func) {
|
||||
@@ -473,28 +499,28 @@ function getStatus(domain, callback) {
|
||||
|
||||
const mailFqdn = config.mailFqdn();
|
||||
|
||||
getDomain(domain, function (error, result) {
|
||||
getDomain(domain, function (error, mailDomain) {
|
||||
if (error) return callback(error);
|
||||
|
||||
let checks = [];
|
||||
if (result.enabled) {
|
||||
if (mailDomain.enabled) {
|
||||
checks.push(
|
||||
recordResult('dns.mx', checkMx.bind(null, domain, mailFqdn)),
|
||||
recordResult('dns.dmarc', checkDmarc.bind(null, domain))
|
||||
);
|
||||
}
|
||||
|
||||
if (result.relay.provider === 'cloudron-smtp') {
|
||||
if (mailDomain.relay.provider === 'cloudron-smtp') {
|
||||
// these tests currently only make sense when using Cloudron's SMTP server at this point
|
||||
checks.push(
|
||||
recordResult('dns.spf', checkSpf.bind(null, domain, mailFqdn)),
|
||||
recordResult('dns.dkim', checkDkim.bind(null, domain)),
|
||||
recordResult('dns.dkim', checkDkim.bind(null, mailDomain)),
|
||||
recordResult('dns.ptr', checkPtr.bind(null, mailFqdn)),
|
||||
recordResult('relay', checkOutboundPort25),
|
||||
recordResult('rbl', checkRblStatus.bind(null, domain))
|
||||
);
|
||||
} else {
|
||||
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 () {
|
||||
@@ -503,6 +529,52 @@ function getStatus(domain, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function checkConfiguration(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let messages = {};
|
||||
|
||||
domains.getAll(function (error, allDomains) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(allDomains, function (domainObject, iteratorCallback) {
|
||||
getStatus(domainObject.domain, function (error, result) {
|
||||
if (error) return iteratorCallback(error);
|
||||
|
||||
let message = [];
|
||||
|
||||
Object.keys(result.dns).forEach((type) => {
|
||||
const record = result.dns[type];
|
||||
if (!record.status) message.push(`${type.toUpperCase()} DNS record did not match. Expected: \`${record.expected}\`. Actual: \`${record.value}\``);
|
||||
});
|
||||
if (result.relay && result.relay.status === false) message.push(`Relay error: ${result.relay.value}`);
|
||||
if (result.rbl && result.rbl.status === false) { // rbl field contents is optional
|
||||
const servers = result.rbl.servers.map((bs) => `[${bs.name}](${bs.site})`); // in markdown
|
||||
message.push(`This server's IP \`${result.rbl.ip}\` is blacklisted in the following servers - ${servers.join(', ')}`);
|
||||
}
|
||||
|
||||
if (message.length) messages[domainObject.domain] = message;
|
||||
|
||||
iteratorCallback(null);
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// create bulleted list for each domain
|
||||
let markdownMessage = '';
|
||||
Object.keys(messages).forEach((domain) => {
|
||||
markdownMessage += `**${domain}**\n`;
|
||||
markdownMessage += messages[domain].map((m) => `* ${m}\n`).join('');
|
||||
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';
|
||||
|
||||
callback(null, markdownMessage); // empty message means all status checks succeeded
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createMailConfig(mailFqdn, callback) {
|
||||
assert.strictEqual(typeof mailFqdn, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -512,53 +584,47 @@ function createMailConfig(mailFqdn, callback) {
|
||||
getDomains(function (error, mailDomains) {
|
||||
if (error) return callback(error);
|
||||
|
||||
users.getOwner(function (error, owner) {
|
||||
const alertsFrom = `no-reply@${config.adminDomain()}`;
|
||||
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(',');
|
||||
|
||||
const alertsTo = config.provider() === 'caas' ? [ 'support@cloudron.io' ] : [ ];
|
||||
alertsTo.concat(error ? [] : owner.email).join(','); // owner may not exist yet
|
||||
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')) {
|
||||
return callback(new Error('Could not create mail var file:' + safe.error.message));
|
||||
}
|
||||
|
||||
const mailOutDomains = mailDomains.map(function (d) { return d.domain; }).join(',');
|
||||
const mailInDomains = mailDomains.filter(function (d) { return d.enabled; }).map(function (d) { return d.domain; }).join(',');
|
||||
// enable_outbound makes plugin forward email for relayed mail. non-relayed mail always hits LMTP plugin first
|
||||
if (!safe.fs.writeFileSync(path.join(paths.ADDON_CONFIG_DIR, 'mail/smtp_forward.ini'), 'enable_outbound=false\ndomain_selector=mail_from\n', 'utf8')) {
|
||||
return callback(new Error('Could not create smtp forward file:' + safe.error.message));
|
||||
}
|
||||
|
||||
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}\nalerts_from=${alertsFrom}\nalerts_to=${alertsTo}\n\n`, 'utf8')) {
|
||||
// create sections for per-domain configuration
|
||||
mailDomains.forEach(function (domain) {
|
||||
const catchAll = domain.catchAll.map(function (c) { return `${c}@${domain.domain}`; }).join(',');
|
||||
const mailFromValidation = domain.mailFromValidation;
|
||||
|
||||
if (!safe.fs.appendFileSync(path.join(paths.ADDON_CONFIG_DIR, 'mail/mail.ini'),
|
||||
`[${domain.domain}]\ncatch_all=${catchAll}\nmail_from_validation=${mailFromValidation}\n\n`, 'utf8')) {
|
||||
return callback(new Error('Could not create mail var file:' + safe.error.message));
|
||||
}
|
||||
|
||||
// enable_outbound makes plugin forward email for relayed mail. non-relayed mail always hits LMTP plugin first
|
||||
if (!safe.fs.writeFileSync(path.join(paths.ADDON_CONFIG_DIR, 'mail/smtp_forward.ini'), 'enable_outbound=false\ndomain_selector=mail_from\n', 'utf8')) {
|
||||
return callback(new Error('Could not create smtp forward file:' + safe.error.message));
|
||||
const relay = domain.relay;
|
||||
|
||||
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=${authType}\nauth_user=${username}\nauth_pass=${password}\n\n`, 'utf8')) {
|
||||
return callback(new Error('Could not create mail var file:' + safe.error.message));
|
||||
}
|
||||
|
||||
// create sections for per-domain configuration
|
||||
mailDomains.forEach(function (domain) {
|
||||
const catchAll = domain.catchAll.map(function (c) { return `${c}@${domain.domain}`; }).join(',');
|
||||
const mailFromValidation = domain.mailFromValidation;
|
||||
|
||||
if (!safe.fs.appendFileSync(path.join(paths.ADDON_CONFIG_DIR, 'mail/mail.ini'),
|
||||
`[${domain.domain}]\ncatch_all=${catchAll}\nmail_from_validation=${mailFromValidation}\n\n`, 'utf8')) {
|
||||
return callback(new Error('Could not create mail var file:' + safe.error.message));
|
||||
}
|
||||
|
||||
const relay = domain.relay;
|
||||
|
||||
const enableRelay = relay.provider !== 'cloudron-smtp',
|
||||
host = relay.host || '',
|
||||
port = relay.port || 25,
|
||||
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')) {
|
||||
return callback(new Error('Could not create mail var file:' + safe.error.message));
|
||||
}
|
||||
});
|
||||
|
||||
callback(null, mailInDomains.length !== 0 /* allowInbound */);
|
||||
});
|
||||
|
||||
callback(null, mailInDomains.length !== 0 /* allowInbound */);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -625,6 +691,7 @@ 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);
|
||||
}
|
||||
|
||||
@@ -642,6 +709,13 @@ function restartMailIfActivated(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function handleCertChanged(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('handleCertChanged: will restart if activated');
|
||||
restartMailIfActivated(callback);
|
||||
}
|
||||
|
||||
function getDomain(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -700,9 +774,10 @@ function txtRecordsWithSpf(domain, mailFqdn, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function ensureDkimKeySync(domain) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
function ensureDkimKeySync(mailDomain) {
|
||||
assert.strictEqual(typeof mailDomain, 'object');
|
||||
|
||||
const domain = mailDomain.domain;
|
||||
const dkimPath = path.join(paths.MAIL_DATA_DIR, `dkim/${domain}`);
|
||||
const dkimPrivateKeyFile = path.join(dkimPath, 'private');
|
||||
const dkimPublicKeyFile = path.join(dkimPath, 'public');
|
||||
@@ -725,7 +800,10 @@ function ensureDkimKeySync(domain) {
|
||||
if (!safe.child_process.execSync('openssl genrsa -out ' + dkimPrivateKeyFile + ' 1024')) return new MailError(MailError.INTERNAL_ERROR, safe.error);
|
||||
if (!safe.child_process.execSync('openssl rsa -in ' + dkimPrivateKeyFile + ' -out ' + dkimPublicKeyFile + ' -pubout -outform PEM')) return new MailError(MailError.INTERNAL_ERROR, safe.error);
|
||||
|
||||
if (!safe.fs.writeFileSync(dkimSelectorFile, config.dkimSelector(), '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;
|
||||
}
|
||||
@@ -754,11 +832,13 @@ function upsertDnsRecords(domain, mailFqdn, callback) {
|
||||
assert.strictEqual(typeof mailFqdn, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
maildb.get(domain, function (error, result) {
|
||||
debug(`upsertDnsRecords: updating mail dns records of domain ${domain} and mail fqdn ${mailFqdn}`);
|
||||
|
||||
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();
|
||||
@@ -767,17 +847,15 @@ 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: config.dkimSelector() + '._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 + '.' ] });
|
||||
}
|
||||
|
||||
debug('upsertDnsRecords: %j', records);
|
||||
|
||||
txtRecordsWithSpf(domain, mailFqdn, function (error, txtRecords) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -808,11 +886,12 @@ function setDnsRecords(domain, callback) {
|
||||
upsertDnsRecords(domain, config.mailFqdn(), callback);
|
||||
}
|
||||
|
||||
function setMailFqdn(mailFqdn, mailDomain, callback) {
|
||||
assert.strictEqual(typeof mailFqdn, 'string');
|
||||
assert.strictEqual(typeof mailDomain, 'string');
|
||||
function onMailFqdnChanged(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const mailFqdn = config.mailFqdn(),
|
||||
mailDomain = config.adminDomain();
|
||||
|
||||
domains.getAll(function (error, allDomains) {
|
||||
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -830,7 +909,9 @@ function addDomain(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
maildb.add(domain, function (error) {
|
||||
const dkimSelector = domain === config.adminDomain() ? 'cloudron' : ('cloudron-' + config.adminDomain().replace(/\./g, ''));
|
||||
|
||||
maildb.add(domain, { dkimSelector }, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, 'Domain already exists'));
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'No such domain'));
|
||||
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
|
||||
@@ -966,9 +1047,11 @@ function sendTestMail(domain, to, callback) {
|
||||
getDomain(domain, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
mailer.sendTestMail(result.domain, to);
|
||||
mailer.sendTestMail(result.domain, to, function (error) {
|
||||
if (error) return callback(new MailError(MailError.EXTERNAL_ERROR, error.message));
|
||||
|
||||
callback();
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ The application '<%= title %>' installed at <%= appFqdn %> is not responding.
|
||||
This is most likely a problem in the application.
|
||||
|
||||
To resolve this, you can try the following:
|
||||
* Restart the app in the app configuration dialog
|
||||
* Restore the app to the latest backup
|
||||
* Contact us via support@cloudron.io or https://forum.cloudron.io
|
||||
* Restart the app by opening the app's web terminal - https://cloudron.io/documentation/apps/#web-terminal
|
||||
* Restore the app to the latest backup - https://cloudron.io/documentation/backups/#restoring-an-app
|
||||
* Contact us via <%= supportEmail %> or https://forum.cloudron.io
|
||||
|
||||
|
||||
Powered by https://cloudron.io
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<%if (format === 'text') { %>
|
||||
|
||||
Dear Cloudron Admin,
|
||||
|
||||
A new version <%= updateInfo.manifest.version %> of the app '<%= app.manifest.title %>' installed at <%= app.fqdn %> is available!
|
||||
|
||||
Changes:
|
||||
<%= updateInfo.manifest.changelog %>
|
||||
|
||||
<% if (!hasSubscription) { -%>
|
||||
*Keep your Cloudron automatically up-to-date and secure by upgrading to a paid plan at* <%= webadminUrl %>/#/settings
|
||||
<% } -%>
|
||||
|
||||
Powered by https://cloudron.io
|
||||
|
||||
Sent at: <%= new Date().toUTCString() %>
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<center>
|
||||
|
||||
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
|
||||
|
||||
<h3>Dear <%= cloudronName %> Admin,</h3>
|
||||
|
||||
<div style="width: 650px; text-align: left;">
|
||||
<p>
|
||||
A new version <%= updateInfo.manifest.version %> of the app '<%= app.manifest.title %>' installed at <%= app.fqdn %> is available!
|
||||
</p>
|
||||
|
||||
<h5>Changelog:</h5>
|
||||
<%- changelogHTML %>
|
||||
|
||||
<br/>
|
||||
|
||||
<% if (!hasSubscription) { %>
|
||||
<p>Keep your Cloudron automatically up-to-date and secure by upgrading to a <a href="<%= webadminUrl %>/#/settings">paid plan</a>.</p>
|
||||
<% } %>
|
||||
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
<div style="font-size: 10px; color: #333333; background: #ffffff;">
|
||||
Powered by <a href="https://cloudron.io">Cloudron</a>.
|
||||
</div>
|
||||
|
||||
</center>
|
||||
|
||||
<% } %>
|
||||
40
src/mail_templates/app_updated.ejs
Normal file
40
src/mail_templates/app_updated.ejs
Normal file
@@ -0,0 +1,40 @@
|
||||
<%if (format === 'text') { %>
|
||||
|
||||
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>
|
||||
<% } %>
|
||||
63
src/mail_templates/app_updates_available.ejs
Normal file
63
src/mail_templates/app_updates_available.ejs
Normal file
@@ -0,0 +1,63 @@
|
||||
<%if (format === 'text') { %>
|
||||
|
||||
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.
|
||||
|
||||
Changes:
|
||||
<%= apps[i].updateInfo.manifest.changelog %>
|
||||
|
||||
<% } -%>
|
||||
|
||||
<% if (!hasSubscription) { -%>
|
||||
*Keep your Cloudron automatically up-to-date and secure by upgrading to a paid plan at* <%= webadminUrl %>/#/settings
|
||||
<% } else { -%>
|
||||
Update now at <%= webadminUrl %>
|
||||
<% } -%>
|
||||
|
||||
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;">
|
||||
<% 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.
|
||||
</p>
|
||||
|
||||
<h5>Changelog:</h5>
|
||||
<%- apps[i].changelogHTML %>
|
||||
|
||||
<br/>
|
||||
<% } -%>
|
||||
|
||||
<% 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 { -%>
|
||||
<p>
|
||||
<br/>
|
||||
<center><a href="<%= webadminUrl %>">Update now</a></center>
|
||||
<br/>
|
||||
</p>
|
||||
<% } -%>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<% } %>
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
Dear <%= cloudronName %> Admin,
|
||||
|
||||
creating a backup has failed.
|
||||
Cloudron failed to create a complete backup. Please see https://cloudron.io/documentation/troubleshooting/#backups
|
||||
for troubleshooting.
|
||||
|
||||
Logs for this failure are available at <%= logUrl %>
|
||||
|
||||
-------------------------------------
|
||||
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
<%if (format === 'text') { %>
|
||||
|
||||
Dear <%= cloudronName %> Admin,
|
||||
|
||||
Version <%= newBoxVersion %> is now available!
|
||||
|
||||
Changelog:
|
||||
<% for (var i = 0; i < changelog.length; i++) { %>
|
||||
* <%- changelog[i] %>
|
||||
<% } %>
|
||||
|
||||
<% if (!hasSubscription) { -%>
|
||||
*Keep your Cloudron automatically up-to-date and secure by upgrading to a paid plan at* <%= webadminUrl %>/#/settings
|
||||
<% } -%>
|
||||
|
||||
Powered by https://cloudron.io
|
||||
|
||||
Sent at: <%= new Date().toUTCString() %>
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<center>
|
||||
|
||||
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
|
||||
|
||||
<h3>Dear <%= cloudronName %> Admin,</h3>
|
||||
|
||||
<div style="width: 650px; text-align: left;">
|
||||
<p>
|
||||
Version <b><%= newBoxVersion %></b> is now available!
|
||||
</p>
|
||||
|
||||
<h5>Changelog:</h5>
|
||||
<ul>
|
||||
<% for (var i = 0; i < changelogHTML.length; i++) { %>
|
||||
<li><%- changelogHTML[i] %></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
|
||||
<br/>
|
||||
|
||||
<% if (!hasSubscription) { %>
|
||||
<p>Keep your Cloudron automatically up-to-date and secure by upgrading to a <a href="<%= webadminUrl %>/#/settings">paid plan</a>.</p>
|
||||
<% } %>
|
||||
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
<div style="font-size: 10px; color: #333333; background: #ffffff;">
|
||||
Powered by <a href="https://cloudron.io">Cloudron</a>.
|
||||
</div>
|
||||
|
||||
</center>
|
||||
|
||||
<% } %>
|
||||
|
||||
@@ -8,6 +8,10 @@ The Cloudron will attempt to renew the certificate every 12 hours
|
||||
until the certificate expires (at which point it will switch to
|
||||
using the fallback certificate).
|
||||
|
||||
See https://cloudron.io/documentation/troubleshooting/#certificates to
|
||||
double check if your server is configured correctly to obtain certificates
|
||||
via Let's Encrypt.
|
||||
|
||||
The error was:
|
||||
|
||||
-------------------------------------
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
<% if (format === 'text') { -%>
|
||||
|
||||
Dear <%= cloudronName %> Admin,
|
||||
|
||||
This is a summary of the activities on your Cloudron.
|
||||
<% if (info.usersAdded.length) { -%>
|
||||
|
||||
The following users were added:
|
||||
<% for (var i = 0; i < info.usersAdded.length; i++) { -%>
|
||||
* <%- info.usersAdded[i].email %>
|
||||
<% }} -%>
|
||||
<% if (info.certRenewals.length) { -%>
|
||||
|
||||
The certificates of the following apps was renewed:
|
||||
<% for (var i = 0; i < info.certRenewals.length; i++) { -%>
|
||||
* <%- info.certRenewals[i].domain %> - <%- info.certRenewals[i].errorMessage || 'Success' %>
|
||||
<% }} -%>
|
||||
<% if (info.pendingBoxUpdate) { -%>
|
||||
|
||||
Cloudron v<%- info.pendingBoxUpdate.version %> is available:
|
||||
<% for (var i = 0; i < info.pendingBoxUpdate.changelog.length; i++) { -%>
|
||||
* <%- info.pendingBoxUpdate.changelog[i] %>
|
||||
<% }} -%>
|
||||
<% if (info.pendingAppUpdates.length) { -%>
|
||||
|
||||
One or more app updates are available:
|
||||
<% for (var i = 0; i < info.pendingAppUpdates.length; i++) { -%>
|
||||
- <%= info.pendingAppUpdates[i].manifest.title %> package v<%= info.pendingAppUpdates[i].manifest.version %>
|
||||
<% for (var j = 0; j < info.pendingAppUpdates[i].manifest.changelog.trim().split('\n').length; j++) { -%>
|
||||
<%= info.pendingAppUpdates[i].manifest.changelog.trim().split('\n')[j] %>
|
||||
<% }}} -%>
|
||||
<% if (info.finishedBoxUpdates.length) { -%>
|
||||
|
||||
Cloudron was updated with the following releases:
|
||||
<% for (var i = 0; i < info.finishedBoxUpdates.length; i++) { -%>
|
||||
- Version <%= info.finishedBoxUpdates[i].boxUpdateInfo.version %>
|
||||
<% for (var j = 0; j < info.finishedBoxUpdates[i].boxUpdateInfo.changelog.length; j++) { -%>
|
||||
* <%= info.finishedBoxUpdates[i].boxUpdateInfo.changelog[j] %>
|
||||
<% }}} -%>
|
||||
<% if (info.finishedAppUpdates.length) { -%>
|
||||
|
||||
The following apps were updated:
|
||||
<% for (var i = 0; i < info.finishedAppUpdates.length; i++) { -%>
|
||||
- <%= info.finishedAppUpdates[i].toManifest.title %> package v<%= info.finishedAppUpdates[i].toManifest.version %>
|
||||
<% for (var j = 0; j < info.finishedAppUpdates[i].toManifest.changelog.trim().split('\n').length; j++) { -%>
|
||||
<%= info.finishedAppUpdates[i].toManifest.changelog.trim().split('\n')[j] %>
|
||||
<% }}} -%>
|
||||
<% if (info.finishedBackups.length) { -%>
|
||||
|
||||
Last successful backup: <%- info.finishedBackups[0].backupId || info.finishedBackups[0].filename %>
|
||||
<% } else { -%>
|
||||
|
||||
This Cloudron did **not** backup successfully in the last week!
|
||||
<% } -%>
|
||||
|
||||
Powered by https://cloudron.io
|
||||
|
||||
Sent at: <%= new Date().toUTCString() %>
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<center>
|
||||
<div style="max-width: 800px; text-align: left; border: 1px solid lightgray; padding: 20px;">
|
||||
<center>
|
||||
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
|
||||
</center>
|
||||
|
||||
<br/>
|
||||
|
||||
<p>This is a summary of the activities on your Cloudron <a href="<%= webadminUrl %>"><%= cloudronName %></a> last week.</p>
|
||||
|
||||
<% if (info.usersAdded.length) { -%>
|
||||
<p><b>The following users were added:</b></p>
|
||||
<ul>
|
||||
<% for (var i = 0; i < info.usersAdded.length; i++) { %>
|
||||
<li><%- info.usersAdded[i].email %></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
<% } %>
|
||||
|
||||
<% if (info.certRenewals.length) { -%>
|
||||
<p><b>The certificates of the following apps were renewed:</b></p>
|
||||
<ul>
|
||||
<% for (var i = 0; i < info.certRenewals.length; i++) { %>
|
||||
<li><%- info.certRenewals[i].domain %> - <%- info.certRenewals[i].errorMessage || 'Success' %></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
<% } %>
|
||||
|
||||
<% if (info.pendingBoxUpdate) { -%>
|
||||
<p><b>Cloudron v<%- info.pendingBoxUpdate.version %> is available:</b></p>
|
||||
<ul>
|
||||
<% for (var i = 0; i < info.pendingBoxUpdate.changelog.length; i++) { %>
|
||||
<li><%- info.pendingBoxUpdate.changelog[i].replace(/^[\*,-] /, '') %></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
<% } %>
|
||||
|
||||
<% if (info.pendingAppUpdates.length) { %>
|
||||
<p><b>Available app updates:</b></p>
|
||||
<ul>
|
||||
<% for (var i = 0; i < info.pendingAppUpdates.length; i++) { %>
|
||||
<li>
|
||||
<b><%= info.pendingAppUpdates[i].manifest.title %></b>
|
||||
<ul>
|
||||
<% for (var j = 0; j < info.pendingAppUpdates[i].manifest.changelog.trim().split('\n').length; j++) { %>
|
||||
<li><%= info.pendingAppUpdates[i].manifest.changelog.trim().split('\n')[j].replace(/^[\*,-] /, '') %></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
<% } %>
|
||||
|
||||
<% if (info.finishedBoxUpdates.length) { %>
|
||||
<p><b>Your Cloudron was updated with the following releases:</b></p>
|
||||
<ul>
|
||||
<% for (var i = 0; i < info.finishedBoxUpdates.length; i++) { %>
|
||||
<li>
|
||||
<b><%= info.finishedBoxUpdates[i].boxUpdateInfo.version %></b>
|
||||
<ul>
|
||||
<% for (var j = 0; j < info.finishedBoxUpdates[i].boxUpdateInfo.changelog.length; j++) { %>
|
||||
<li><%= info.finishedBoxUpdates[i].boxUpdateInfo.changelog[j].replace(/^[\*,-] /, '') %></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
<% } %>
|
||||
|
||||
<% if (info.finishedAppUpdates.length) { %>
|
||||
<p><b>The following apps were updated:</b></p>
|
||||
<ul>
|
||||
<% for (var i = 0; i < info.finishedAppUpdates.length; i++) { %>
|
||||
<li>
|
||||
<b><%= info.finishedAppUpdates[i].toManifest.title %></b> (package v<%= info.finishedAppUpdates[i].toManifest.version %>)
|
||||
<ul>
|
||||
<% for (var j = 0; j < info.finishedAppUpdates[i].toManifest.changelog.trim().split('\n').length; j++) { -%>
|
||||
<li><%= info.finishedAppUpdates[i].toManifest.changelog.trim().split('\n')[j].replace(/^[\*,-] /, '') %></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
<% } %>
|
||||
|
||||
<% if (info.finishedBackups.length) { %>
|
||||
<p><b>Last successful backup : </b> <%= info.finishedBackups[0].backupId || info.finishedBackups[0].filename %> </p>
|
||||
<% } else { %>
|
||||
<p><b>This Cloudron did not backup successfully in the last week!</b></p>
|
||||
<% } %>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<center>
|
||||
<small>
|
||||
Powered by <a href="https://cloudron.io">Cloudron</a><br/>
|
||||
Sent on <%= new Date().toUTCString() %>
|
||||
</small>
|
||||
</center>
|
||||
</div>
|
||||
</center>
|
||||
|
||||
<% } %>
|
||||
@@ -2,22 +2,21 @@
|
||||
|
||||
Dear <%= cloudronName %> Admin,
|
||||
|
||||
<%= program %> exited unexpectedly using too much memory!
|
||||
<%= program %> was restarted now as it ran out of memory.
|
||||
|
||||
The app has been restarted now. Should this message appear repeatedly or
|
||||
undefined behavior is observed, give the app more memory.
|
||||
This can be done in the advanced settings in the app configuration dialog
|
||||
in your Cloudron's web interface.
|
||||
If this message appears repeatedly, give the app more memory.
|
||||
|
||||
Please see some excerpt of the logs below.
|
||||
* To increase an app's memory limit - https://cloudron.io/documentation/apps/#increasing-the-memory-limit-of-an-app
|
||||
* To increase a service's memory limit - https://cloudron.io/documentation/troubleshooting/#services
|
||||
|
||||
Out of memory event:
|
||||
|
||||
-------------------------------------
|
||||
|
||||
<%- context %>
|
||||
<%- event %>
|
||||
|
||||
-------------------------------------
|
||||
|
||||
|
||||
Powered by https://cloudron.io
|
||||
|
||||
Sent at: <%= new Date().toUTCString() %>
|
||||
|
||||
@@ -1,54 +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"
|
||||
}
|
||||
}],
|
||||
"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>"
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
<%if (format === 'text') { %>
|
||||
|
||||
Dear <%= cloudronName %> Admin,
|
||||
|
||||
<%= subject %>
|
||||
|
||||
Please see some excerpt of the logs below:
|
||||
|
||||
-------------------------------------
|
||||
|
||||
<%- context %>
|
||||
|
||||
-------------------------------------
|
||||
|
||||
|
||||
Powered by https://cloudron.io
|
||||
|
||||
Sent at: <%= new Date().toUTCString() %>
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<% } %>
|
||||
@@ -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));
|
||||
|
||||
276
src/mailer.js
276
src/mailer.js
@@ -5,15 +5,13 @@ exports = module.exports = {
|
||||
userRemoved: userRemoved,
|
||||
adminChanged: adminChanged,
|
||||
passwordReset: passwordReset,
|
||||
boxUpdateAvailable: boxUpdateAvailable,
|
||||
appUpdateAvailable: appUpdateAvailable,
|
||||
sendDigest: sendDigest,
|
||||
appUpdatesAvailable: appUpdatesAvailable,
|
||||
|
||||
sendInvite: sendInvite,
|
||||
unexpectedExit: unexpectedExit,
|
||||
|
||||
appUp: appUp,
|
||||
appDied: appDied,
|
||||
appUpdated: appUpdated,
|
||||
oomEvent: oomEvent,
|
||||
|
||||
backupFailed: backupFailed,
|
||||
@@ -22,13 +20,12 @@ exports = module.exports = {
|
||||
|
||||
sendTestMail: sendTestMail,
|
||||
|
||||
_getMailQueue: _getMailQueue,
|
||||
_clearMailQueue: _clearMailQueue
|
||||
_mailQueue: [] // accumulate mails in test mode
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
custom = require('./custom.js'),
|
||||
debug = require('debug')('box:mailer'),
|
||||
docker = require('./docker.js').connection,
|
||||
ejs = require('ejs'),
|
||||
@@ -38,74 +35,39 @@ var assert = require('assert'),
|
||||
settings = require('./settings.js'),
|
||||
showdown = require('showdown'),
|
||||
smtpTransport = require('nodemailer-smtp-transport'),
|
||||
users = require('./users.js'),
|
||||
util = require('util');
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
var MAIL_TEMPLATES_DIR = path.join(__dirname, 'mail_templates');
|
||||
|
||||
var gMailQueue = [ ];
|
||||
|
||||
function splatchError(error) {
|
||||
var result = { };
|
||||
Object.getOwnPropertyNames(error).forEach(function (key) {
|
||||
var value = this[key];
|
||||
if (value instanceof Error) value = splatchError(value);
|
||||
result[key] = value;
|
||||
}, error /* thisArg */);
|
||||
|
||||
return util.inspect(result, { depth: null, showHidden: true });
|
||||
}
|
||||
|
||||
function getAdminEmails(callback) {
|
||||
users.getAllAdmins(function (error, admins) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (admins.length === 0) return callback(new Error('No admins on this cloudron')); // box not activated yet
|
||||
|
||||
var adminEmails = [ ];
|
||||
admins.forEach(function (admin) { adminEmails.push(admin.email); });
|
||||
|
||||
callback(null, adminEmails);
|
||||
});
|
||||
}
|
||||
|
||||
// This will collect the most common details required for notification emails
|
||||
function getMailConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return callback(error);
|
||||
settings.getCloudronName(function (error, cloudronName) {
|
||||
// this is not fatal
|
||||
if (error) {
|
||||
debug(error);
|
||||
cloudronName = 'Cloudron';
|
||||
}
|
||||
|
||||
settings.getCloudronName(function (error, cloudronName) {
|
||||
// this is not fatal
|
||||
if (error) {
|
||||
debug(error);
|
||||
cloudronName = 'Cloudron';
|
||||
}
|
||||
|
||||
callback(null, {
|
||||
adminEmails: adminEmails,
|
||||
cloudronName: cloudronName,
|
||||
notificationFrom: `"${cloudronName}" <no-reply@${config.adminDomain()}>`
|
||||
});
|
||||
callback(null, {
|
||||
cloudronName: cloudronName,
|
||||
notificationFrom: `"${cloudronName}" <no-reply@${config.adminDomain()}>`
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function processQueue() {
|
||||
sendMails(gMailQueue);
|
||||
gMailQueue = [ ];
|
||||
}
|
||||
|
||||
// note : this function should NOT access the database. it is called by the crashnotifier
|
||||
// which does not initialize mailer or the databse
|
||||
function sendMails(queue, callback) {
|
||||
assert(util.isArray(queue));
|
||||
function sendMail(mailOptions, callback) {
|
||||
assert.strictEqual(typeof mailOptions, 'object');
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
if (process.env.BOX_ENV === 'test') {
|
||||
exports._mailQueue.push(mailOptions);
|
||||
return callback();
|
||||
}
|
||||
|
||||
docker.getContainer('mail').inspect(function (error, data) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -124,39 +86,21 @@ function sendMails(queue, callback) {
|
||||
host: mailServerIp,
|
||||
port: config.get('smtpPort'),
|
||||
auth: {
|
||||
user: `no-reply@${config.adminDomain()}`,
|
||||
user: mailOptions.authUser || `no-reply@${config.adminDomain()}`,
|
||||
pass: relayToken
|
||||
}
|
||||
}));
|
||||
|
||||
debug('Processing mail queue of size %d (through %s:2525)', queue.length, mailServerIp);
|
||||
transport.sendMail(mailOptions, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.mapSeries(queue, function iterator(mailOptions, callback) {
|
||||
transport.sendMail(mailOptions, function (error) {
|
||||
if (error) return debug(error); // TODO: requeue?
|
||||
debug('Email sent to ' + mailOptions.to);
|
||||
});
|
||||
callback(null);
|
||||
}, function done() {
|
||||
debug('Done processing mail queue');
|
||||
debug(`Email "${mailOptions.subject}" sent to ${mailOptions.to}`);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function enqueue(mailOptions) {
|
||||
assert.strictEqual(typeof mailOptions, 'object');
|
||||
|
||||
if (!mailOptions.from) debug('sender address is missing');
|
||||
if (!mailOptions.to) debug('recipient address is missing');
|
||||
|
||||
debug('Queued mail for ' + mailOptions.from + ' to ' + mailOptions.to);
|
||||
gMailQueue.push(mailOptions);
|
||||
|
||||
if (process.env.BOX_ENV !== 'test') processQueue();
|
||||
}
|
||||
|
||||
function render(templateFile, params) {
|
||||
assert.strictEqual(typeof templateFile, 'string');
|
||||
assert.strictEqual(typeof params, 'object');
|
||||
@@ -187,7 +131,7 @@ function mailUserEvent(mailTo, user, event) {
|
||||
text: render('user_event.ejs', { user: user, event: event, format: 'text' }),
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
sendMail(mailOptions);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -223,7 +167,7 @@ function sendInvite(user, invitor) {
|
||||
html: render('welcome_user.ejs', templateDataHTML)
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
sendMail(mailOptions);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -256,7 +200,7 @@ function userAdded(mailTo, user) {
|
||||
html: render('user_added.ejs', templateDataHTML)
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
sendMail(mailOptions);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -308,7 +252,7 @@ function passwordReset(user) {
|
||||
html: render('password_reset.ejs', templateDataHTML)
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
sendMail(mailOptions);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -328,7 +272,7 @@ function appUp(mailTo, app) {
|
||||
text: render('app_up.ejs', { title: app.manifest.title, appFqdn: app.fqdn, format: 'text' })
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
sendMail(mailOptions);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -345,67 +289,30 @@ 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, format: 'text' })
|
||||
text: render('app_down.ejs', { title: app.manifest.title, appFqdn: app.fqdn, supportEmail: custom.spec().support.email, format: 'text' })
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
sendMail(mailOptions);
|
||||
});
|
||||
}
|
||||
|
||||
function boxUpdateAvailable(hasSubscription, newBoxVersion, changelog) {
|
||||
assert.strictEqual(typeof hasSubscription, 'boolean');
|
||||
assert.strictEqual(typeof newBoxVersion, 'string');
|
||||
assert(util.isArray(changelog));
|
||||
|
||||
getMailConfig(function (error, mailConfig) {
|
||||
if (error) return debug('Error getting mail details:', error);
|
||||
|
||||
var converter = new showdown.Converter();
|
||||
|
||||
var templateData = {
|
||||
webadminUrl: config.adminOrigin(),
|
||||
newBoxVersion: newBoxVersion,
|
||||
hasSubscription: hasSubscription,
|
||||
changelog: changelog,
|
||||
changelogHTML: changelog.map(function (e) { return converter.makeHtml(e); }),
|
||||
cloudronName: mailConfig.cloudronName,
|
||||
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
};
|
||||
|
||||
var templateDataText = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataText.format = 'text';
|
||||
|
||||
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataHTML.format = 'html';
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: mailConfig.adminEmails.join(', '),
|
||||
subject: util.format('%s has a new update available', mailConfig.cloudronName),
|
||||
text: render('box_update_available.ejs', templateDataText),
|
||||
html: render('box_update_available.ejs', templateDataHTML)
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
});
|
||||
}
|
||||
|
||||
function appUpdateAvailable(app, hasSubscription, info) {
|
||||
function appUpdated(mailTo, app, callback) {
|
||||
assert.strictEqual(typeof mailTo, 'string');
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof hasSubscription, 'boolean');
|
||||
assert.strictEqual(typeof info, 'object');
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
debug('Sending mail for app %s @ %s updated', app.id, app.fqdn);
|
||||
|
||||
getMailConfig(function (error, mailConfig) {
|
||||
if (error) return debug('Error getting mail details:', error);
|
||||
|
||||
var converter = new showdown.Converter();
|
||||
|
||||
var templateData = {
|
||||
webadminUrl: config.adminOrigin(),
|
||||
hasSubscription: hasSubscription,
|
||||
app: app,
|
||||
updateInfo: info,
|
||||
changelogHTML: converter.makeHtml(info.manifest.changelog),
|
||||
title: app.manifest.title,
|
||||
appFqdn: app.fqdn,
|
||||
version: app.manifest.version,
|
||||
changelog: app.manifest.changelog,
|
||||
changelogHTML: converter.makeHtml(app.manifest.changelog),
|
||||
cloudronName: mailConfig.cloudronName,
|
||||
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
};
|
||||
@@ -418,27 +325,36 @@ function appUpdateAvailable(app, hasSubscription, info) {
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: mailConfig.adminEmails.join(', '),
|
||||
subject: util.format('App %s has a new update available', app.fqdn),
|
||||
text: render('app_update_available.ejs', templateDataText),
|
||||
html: render('app_update_available.ejs', templateDataHTML)
|
||||
to: mailTo,
|
||||
subject: `[${mailConfig.cloudronName}] App ${app.fqdn} was updated`,
|
||||
text: render('app_updated.ejs', templateDataText),
|
||||
html: render('app_updated.ejs', templateDataHTML)
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
sendMail(mailOptions, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function sendDigest(info) {
|
||||
assert.strictEqual(typeof info, 'object');
|
||||
function appUpdatesAvailable(mailTo, apps, hasSubscription, callback) {
|
||||
assert.strictEqual(typeof mailTo, 'string');
|
||||
assert.strictEqual(typeof apps, 'object');
|
||||
assert.strictEqual(typeof hasSubscription, 'boolean');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getMailConfig(function (error, mailConfig) {
|
||||
if (error) return debug('Error getting mail details:', error);
|
||||
|
||||
var converter = new showdown.Converter();
|
||||
apps.forEach(function (app) {
|
||||
app.changelogHTML = converter.makeHtml(app.updateInfo.manifest.changelog);
|
||||
});
|
||||
|
||||
var templateData = {
|
||||
webadminUrl: config.adminOrigin(),
|
||||
hasSubscription: hasSubscription,
|
||||
apps: apps,
|
||||
cloudronName: mailConfig.cloudronName,
|
||||
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar',
|
||||
info: info
|
||||
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
};
|
||||
|
||||
var templateDataText = JSON.parse(JSON.stringify(templateData));
|
||||
@@ -449,34 +365,35 @@ function sendDigest(info) {
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: mailConfig.adminEmails.join(', '),
|
||||
subject: util.format('[%s] Weekly activity digest', mailConfig.cloudronName),
|
||||
text: render('digest.ejs', templateDataText),
|
||||
html: render('digest.ejs', templateDataHTML)
|
||||
to: mailTo,
|
||||
subject: `New app updates available for ${mailConfig.cloudronName}`,
|
||||
text: render('app_updates_available.ejs', templateDataText),
|
||||
html: render('app_updates_available.ejs', templateDataHTML)
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
sendMail(mailOptions, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function backupFailed(error) {
|
||||
var message = splatchError(error);
|
||||
function backupFailed(mailTo, errorMessage, logUrl) {
|
||||
assert.strictEqual(typeof mailTo, 'string');
|
||||
|
||||
getMailConfig(function (error, mailConfig) {
|
||||
if (error) return debug('Error getting mail details:', error);
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: config.provider() === 'caas' ? 'support@cloudron.io' : mailConfig.adminEmails.join(', '),
|
||||
to: mailTo,
|
||||
subject: util.format('[%s] Failed to backup', mailConfig.cloudronName),
|
||||
text: render('backup_failed.ejs', { cloudronName: mailConfig.cloudronName, message: message, format: 'text' })
|
||||
text: render('backup_failed.ejs', { cloudronName: mailConfig.cloudronName, message: errorMessage, logUrl: logUrl, format: 'text' })
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
sendMail(mailOptions);
|
||||
});
|
||||
}
|
||||
|
||||
function certificateRenewalError(domain, message) {
|
||||
function certificateRenewalError(mailTo, domain, message) {
|
||||
assert.strictEqual(typeof mailTo, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof message, 'string');
|
||||
|
||||
@@ -485,19 +402,19 @@ function certificateRenewalError(domain, message) {
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: config.provider() === 'caas' ? 'support@cloudron.io' : mailConfig.adminEmails.join(', '),
|
||||
to: mailTo,
|
||||
subject: util.format('[%s] Certificate renewal error', domain),
|
||||
text: render('certificate_renewal_error.ejs', { domain: domain, message: message, format: 'text' })
|
||||
};
|
||||
|
||||
sendMails([ mailOptions ]);
|
||||
sendMail(mailOptions);
|
||||
});
|
||||
}
|
||||
|
||||
function oomEvent(mailTo, program, context) {
|
||||
function oomEvent(mailTo, program, event) {
|
||||
assert.strictEqual(typeof mailTo, 'string');
|
||||
assert.strictEqual(typeof program, 'string');
|
||||
assert.strictEqual(typeof context, 'string');
|
||||
assert.strictEqual(typeof event, 'object');
|
||||
|
||||
getMailConfig(function (error, mailConfig) {
|
||||
if (error) return debug('Error getting mail details:', error);
|
||||
@@ -505,59 +422,30 @@ function oomEvent(mailTo, program, context) {
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: mailTo,
|
||||
subject: util.format('[%s] %s exited unexpectedly', mailConfig.cloudronName, program),
|
||||
text: render('oom_event.ejs', { cloudronName: mailConfig.cloudronName, program: program, context: context, format: 'text' })
|
||||
subject: util.format('[%s] %s was restarted (OOM)', mailConfig.cloudronName, program),
|
||||
text: render('oom_event.ejs', { cloudronName: mailConfig.cloudronName, program: program, event: JSON.stringify(event), format: 'text' })
|
||||
};
|
||||
|
||||
sendMails([ mailOptions ]);
|
||||
sendMail(mailOptions);
|
||||
});
|
||||
}
|
||||
|
||||
// this function bypasses the queue intentionally. it is also expected to work without the mailer module initialized
|
||||
// NOTE: crashnotifier should ideally be able to send mail when there is no db, however we need the 'from' address domain from the db
|
||||
function unexpectedExit(mailTo, subject, context) {
|
||||
assert.strictEqual(typeof mailTo, 'string');
|
||||
assert.strictEqual(typeof subject, 'string');
|
||||
assert.strictEqual(typeof context, 'string');
|
||||
|
||||
getMailConfig(function (error, mailConfig) {
|
||||
if (error) return debug('Error getting mail details:', error);
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: mailTo,
|
||||
subject: `[${mailConfig.cloudronName}] ${subject}`,
|
||||
text: render('unexpected_exit.ejs', { cloudronName: mailConfig.cloudronName, subject: subject, context: context, format: 'text' })
|
||||
};
|
||||
|
||||
sendMails([ mailOptions ]);
|
||||
});
|
||||
}
|
||||
|
||||
function sendTestMail(domain, email) {
|
||||
function sendTestMail(domain, email, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getMailConfig(function (error, mailConfig) {
|
||||
if (error) return debug('Error getting mail details:', error);
|
||||
|
||||
var mailOptions = {
|
||||
authUser: `no-reply@${domain}`,
|
||||
from: `"${mailConfig.cloudronName}" <no-reply@${domain}>`,
|
||||
to: email,
|
||||
subject: util.format('Test Email from %s', mailConfig.cloudronName),
|
||||
text: render('test.ejs', { cloudronName: mailConfig.cloudronName, format: 'text'})
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
sendMail(mailOptions, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function _getMailQueue() {
|
||||
return gMailQueue;
|
||||
}
|
||||
|
||||
function _clearMailQueue(callback) {
|
||||
gMailQueue = [];
|
||||
|
||||
if (callback) callback();
|
||||
}
|
||||
|
||||
@@ -5,7 +5,11 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
dns = require('dns');
|
||||
config = require('./config.js'),
|
||||
dns = require('dns'),
|
||||
_ = require('underscore');
|
||||
|
||||
const DEFAULT_OPTIONS = { server: '127.0.0.1', timeout: 5000 }; // unbound runs on 127.0.0.1
|
||||
|
||||
// a note on TXT records. It doesn't have quotes ("") at the DNS level. Those quotes
|
||||
// are added for DNS server software to enclose spaces. Such quotes may also be returned
|
||||
@@ -17,7 +21,10 @@ function resolve(hostname, rrtype, options, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const resolver = new dns.Resolver();
|
||||
if (options.server) resolver.setServers([ options.server ]);
|
||||
options = _.extend({ }, DEFAULT_OPTIONS, options);
|
||||
|
||||
// Only use unbound on a Cloudron
|
||||
if (config.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);
|
||||
|
||||
@@ -2,18 +2,21 @@
|
||||
|
||||
exports = module.exports = {
|
||||
get: get,
|
||||
getByUserIdAndTitle: getByUserIdAndTitle,
|
||||
add: add,
|
||||
upsert: upsert,
|
||||
update: update,
|
||||
del: del,
|
||||
listByUserIdPaged: listByUserIdPaged
|
||||
listByUserIdPaged: listByUserIdPaged,
|
||||
|
||||
// exported for testing
|
||||
_clear: clear
|
||||
};
|
||||
|
||||
let assert = require('assert'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror');
|
||||
|
||||
const NOTIFICATION_FIELDS = [ 'id', 'userId', 'eventId', 'title', 'message', 'action', 'creationTime', 'acknowledged' ];
|
||||
const NOTIFICATION_FIELDS = [ 'id', 'userId', 'eventId', 'title', 'message', 'creationTime', 'acknowledged' ];
|
||||
|
||||
function postProcess(result) {
|
||||
assert.strictEqual(typeof result, 'object');
|
||||
@@ -27,8 +30,8 @@ function add(notification, callback) {
|
||||
assert.strictEqual(typeof notification, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const query = 'INSERT INTO notifications (userId, eventId, title, message, action) VALUES (?, ?, ?, ?, ?)';
|
||||
const args = [ notification.userId, notification.eventId, notification.title, notification.message, notification.action ];
|
||||
const query = 'INSERT INTO notifications (userId, eventId, title, message, acknowledged) VALUES (?, ?, ?, ?, ?)';
|
||||
const args = [ notification.userId, notification.eventId, notification.title, notification.message, notification.acknowledged ];
|
||||
|
||||
database.query(query, args, function (error, result) {
|
||||
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'no such eventlog entry'));
|
||||
@@ -38,27 +41,18 @@ function add(notification, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
// will clear the ack flag
|
||||
// matches by userId and title
|
||||
function upsert(notification, callback) {
|
||||
assert.strictEqual(typeof notification, 'object');
|
||||
function getByUserIdAndTitle(userId, title, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof title, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT * from notifications WHERE userId = ? AND title = ?', [ notification.userId, notification.title ], function (error, result) {
|
||||
database.query('SELECT ' + NOTIFICATION_FIELDS + ' from notifications WHERE userId = ? AND title = ? ORDER BY creationTime LIMIT 1', [ userId, title ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return add(notification, callback);
|
||||
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
postProcess(result[0]);
|
||||
postProcess(results[0]);
|
||||
|
||||
var data = {
|
||||
acknowledged: false,
|
||||
eventId: notification.eventId,
|
||||
message: notification.message,
|
||||
action: notification.action,
|
||||
creationTime: new Date()
|
||||
};
|
||||
|
||||
update(result[0].id, data, callback);
|
||||
callback(null, results[0]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -79,7 +73,7 @@ function update(id, data, callback) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
return callback(null, id);
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -131,3 +125,14 @@ function listByUserIdPaged(userId, page, perPage, callback) {
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
function clear(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM notifications', function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,34 +3,34 @@
|
||||
exports = module.exports = {
|
||||
NotificationsError: NotificationsError,
|
||||
|
||||
add: add,
|
||||
upsert: upsert,
|
||||
get: get,
|
||||
ack: ack,
|
||||
getAllPaged: getAllPaged,
|
||||
|
||||
// specialized notifications
|
||||
userAdded: userAdded,
|
||||
userRemoved: userRemoved,
|
||||
adminChanged: adminChanged,
|
||||
oomEvent: oomEvent,
|
||||
appUp: appUp,
|
||||
appDied: appDied,
|
||||
processCrash: processCrash,
|
||||
apptaskCrash: apptaskCrash,
|
||||
backupConfigWarning: backupConfigWarning,
|
||||
diskSpaceWarning: diskSpaceWarning,
|
||||
mailStatusWarning: mailStatusWarning
|
||||
onEvent: onEvent,
|
||||
|
||||
// NOTE: if you add an alert, be sure to add title below
|
||||
ALERT_BACKUP_CONFIG: 'backupConfig',
|
||||
ALERT_DISK_SPACE: 'diskSpace',
|
||||
ALERT_MAIL_STATUS: 'mailStatus',
|
||||
ALERT_REBOOT: 'reboot',
|
||||
ALERT_BOX_UPDATE: 'boxUpdate',
|
||||
|
||||
alert: alert,
|
||||
|
||||
// exported for testing
|
||||
_add: add
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
let assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('./config.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'),
|
||||
safe = require('safetydance'),
|
||||
users = require('./users.js'),
|
||||
util = require('util');
|
||||
|
||||
@@ -56,46 +56,21 @@ util.inherits(NotificationsError, Error);
|
||||
NotificationsError.INTERNAL_ERROR = 'Internal Error';
|
||||
NotificationsError.NOT_FOUND = 'Not Found';
|
||||
|
||||
function add(userId, eventId, title, message, action, callback) {
|
||||
function add(userId, eventId, title, message, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert(typeof eventId === 'string' || eventId === null);
|
||||
assert.strictEqual(typeof title, 'string');
|
||||
assert.strictEqual(typeof message, 'string');
|
||||
assert.strictEqual(typeof action, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('add: ', userId, title, action);
|
||||
debug('add: ', userId, title);
|
||||
|
||||
notificationdb.add({
|
||||
userId: userId,
|
||||
eventId: eventId,
|
||||
title: title,
|
||||
message: message,
|
||||
action: action
|
||||
}, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new NotificationsError(NotificationsError.NOT_FOUND, error.message));
|
||||
if (error) return callback(new NotificationsError(NotificationsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, { id: result });
|
||||
});
|
||||
}
|
||||
|
||||
function upsert(userId, eventId, title, message, action, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert(typeof eventId === 'string' || eventId === null);
|
||||
assert.strictEqual(typeof title, 'string');
|
||||
assert.strictEqual(typeof message, 'string');
|
||||
assert.strictEqual(typeof action, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('upsert: ', userId, title, action);
|
||||
|
||||
notificationdb.upsert({
|
||||
userId: userId,
|
||||
eventId: eventId,
|
||||
title: title,
|
||||
message: message,
|
||||
action: action
|
||||
acknowledged: false
|
||||
}, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new NotificationsError(NotificationsError.NOT_FOUND, error.message));
|
||||
if (error) return callback(new NotificationsError(NotificationsError.INTERNAL_ERROR, error));
|
||||
@@ -161,159 +136,229 @@ function actionForAllAdmins(skippingUserIds, iterator, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function userAdded(performedBy, eventId, user) {
|
||||
function userAdded(performedBy, eventId, user, callback) {
|
||||
assert.strictEqual(typeof performedBy, 'string');
|
||||
assert.strictEqual(typeof eventId, 'string');
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
actionForAllAdmins([ performedBy, user.id ], function (admin, callback) {
|
||||
actionForAllAdmins([ performedBy, user.id ], function (admin, done) {
|
||||
mailer.userAdded(admin.email, user);
|
||||
add(admin.id, eventId, 'User added', `User ${user.fallbackEmail} was added`, '/#/users', callback);
|
||||
}, function (error) {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
add(admin.id, eventId, 'User added', `User ${user.fallbackEmail} was added`, done);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function userRemoved(performedBy, eventId, user) {
|
||||
function userRemoved(performedBy, eventId, user, callback) {
|
||||
assert.strictEqual(typeof performedBy, 'string');
|
||||
assert.strictEqual(typeof eventId, 'string');
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
actionForAllAdmins([ performedBy, user.id ], function (admin, 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`, '/#/users', callback);
|
||||
}, function (error) {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
add(admin.id, eventId, 'User removed', `User ${user.username || user.email || user.fallbackEmail} was removed`, done);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function adminChanged(performedBy, eventId, user) {
|
||||
function adminChanged(performedBy, eventId, user, callback) {
|
||||
assert.strictEqual(typeof performedBy, 'string');
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
actionForAllAdmins([ performedBy, user.id ], function (admin, 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'}`, '/#/users', callback);
|
||||
}, function (error) {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
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);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function oomEvent(eventId, program, context) {
|
||||
assert.strictEqual(typeof eventId, 'string');
|
||||
assert.strictEqual(typeof program, 'string');
|
||||
assert.strictEqual(typeof context, 'object');
|
||||
|
||||
// also send us a notification mail
|
||||
if (config.provider() === 'caas') mailer.oomEvent('support@cloudron.io', program, JSON.stringify(context, null, 4));
|
||||
|
||||
actionForAllAdmins([], function (admin, callback) {
|
||||
mailer.oomEvent(admin.email, program, JSON.stringify(context, null, 4));
|
||||
|
||||
var message;
|
||||
if (context.app) message = `The application ${context.app.manifest.title} with id ${context.app.id} ran out of memory.`;
|
||||
else message = `The container with id ${context.details.id} ran out of memory`;
|
||||
|
||||
add(admin.id, eventId, 'Process died out-of-memory', message, context.app ? '/#/apps' : '', callback);
|
||||
}, function (error) {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
function appUp(eventId, app) {
|
||||
function oomEvent(eventId, app, addon, containerId, event, callback) {
|
||||
assert.strictEqual(typeof eventId, 'string');
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof addon, 'object');
|
||||
assert.strictEqual(typeof containerId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// also send us a notification mail
|
||||
if (config.provider() === 'caas') mailer.appDied('support@cloudron.io', app);
|
||||
let title, message, program;
|
||||
if (app) {
|
||||
program = `App ${app.fqdn}`;
|
||||
title = `The application ${app.fqdn} (${app.manifest.title}) ran out of memory.`;
|
||||
message = 'The application has been restarted automatically. If you see this notification often, consider increasing the [memory limit](https://cloudron.io/documentation/apps/#increasing-the-memory-limit-of-an-app)';
|
||||
} else if (addon) {
|
||||
program = `${addon.name} service`;
|
||||
title = `The ${addon.name} service ran out of memory`;
|
||||
message = 'The service has been restarted automatically. If you see this notification often, consider increasing the [memory limit](https://cloudron.io/documentation/troubleshooting/#services)';
|
||||
} else { // this never happens currently
|
||||
program = `Container ${containerId}`;
|
||||
title = `The container ${containerId} ran out of memory`;
|
||||
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)';
|
||||
}
|
||||
|
||||
actionForAllAdmins([], function (admin, 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);
|
||||
|
||||
add(admin.id, eventId, title, message, done);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function appUp(eventId, app, callback) {
|
||||
assert.strictEqual(typeof eventId, 'string');
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
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);
|
||||
add(admin.id, eventId, `App ${app.fqdn} is back online`, `The application ${app.manifest.title} installed at ${app.fqdn} is back online.`, '/#/apps', callback);
|
||||
}, function (error) {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
add(admin.id, eventId, `App ${app.fqdn} is back online`, `The application ${app.manifest.title} installed at ${app.fqdn} is back online.`, done);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function appDied(eventId, app) {
|
||||
function appDied(eventId, app, callback) {
|
||||
assert.strictEqual(typeof eventId, 'string');
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// also send us a notification mail
|
||||
if (config.provider() === 'caas') mailer.appDied('support@cloudron.io', app);
|
||||
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);
|
||||
add(admin.id, eventId, `App ${app.fqdn} is down`, `The application ${app.manifest.title} installed at ${app.fqdn} is not responding.`, '/#/apps', callback);
|
||||
}, function (error) {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
add(admin.id, eventId, `App ${app.fqdn} is down`, `The application ${app.manifest.title} installed at ${app.fqdn} is not responding.`, callback);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function processCrash(eventId, processName, crashLogFile) {
|
||||
function appUpdated(eventId, app, callback) {
|
||||
assert.strictEqual(typeof eventId, 'string');
|
||||
assert.strictEqual(typeof processName, 'string');
|
||||
assert.strictEqual(typeof crashLogFile, 'string');
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var subject = `${processName} exited unexpectedly`;
|
||||
var crashLogs = safe.fs.readFileSync(crashLogFile, 'utf8') || `No logs found at ${crashLogFile}`;
|
||||
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) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// also send us a notification mail
|
||||
if (config.provider() === 'caas') mailer.unexpectedExit('support@cloudron.io', subject, crashLogs);
|
||||
|
||||
actionForAllAdmins([], function (admin, callback) {
|
||||
mailer.unexpectedExit(admin.email, subject, crashLogs);
|
||||
add(admin.id, eventId, subject, 'Detailed logs have been sent to your email address.', '/#/system', callback);
|
||||
}, function (error) {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
mailer.appUpdated(admin.email, app, function (error) {
|
||||
if (error) console.error('Failed to send app updated email', error); // non fatal
|
||||
done();
|
||||
});
|
||||
});
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function apptaskCrash(eventId, appId, crashLogFile) {
|
||||
function certificateRenewalError(eventId, vhost, errorMessage, callback) {
|
||||
assert.strictEqual(typeof eventId, 'string');
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof crashLogFile, 'string');
|
||||
assert.strictEqual(typeof vhost, 'string');
|
||||
assert.strictEqual(typeof errorMessage, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var subject = `Apptask for ${appId} crashed`;
|
||||
var crashLogs = safe.fs.readFileSync(crashLogFile, 'utf8') || `No logs found at ${crashLogFile}`;
|
||||
|
||||
// also send us a notification mail
|
||||
if (config.provider() === 'caas') mailer.unexpectedExit('support@cloudron.io', subject, crashLogs);
|
||||
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.unexpectedExit(admin.email, subject, crashLogs);
|
||||
add(admin.id, eventId, subject, 'Detailed logs have been sent to your email address.', '/#/system', callback);
|
||||
}, function (error) {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
mailer.certificateRenewalError(admin.email, vhost, errorMessage);
|
||||
add(admin.id, eventId, `Certificate renewal of ${vhost} failed`, `Failed to new certs of ${vhost}: ${errorMessage}. Renewal will be retried in 12 hours`, callback);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function backupConfigWarning(message) {
|
||||
function backupFailed(eventId, taskId, errorMessage, callback) {
|
||||
assert.strictEqual(typeof eventId, 'string');
|
||||
assert.strictEqual(typeof taskId, 'string');
|
||||
assert.strictEqual(typeof errorMessage, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (custom.spec().alerts.email) mailer.backupFailed(custom.spec().alerts.email, errorMessage, `${config.adminOrigin()}/logs.html?taskId=${taskId}`);
|
||||
if (!custom.spec().alerts.notifyCloudronAdmins) return callback();
|
||||
|
||||
actionForAllAdmins([], function (admin, callback) {
|
||||
mailer.backupFailed(admin.email, errorMessage, `${config.adminOrigin()}/logs.html?taskId=${taskId}`);
|
||||
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);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function alert(id, title, message, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof title, 'string');
|
||||
assert.strictEqual(typeof message, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`alert: id=${id} title=${title} message=${message}`);
|
||||
|
||||
const acknowledged = !message;
|
||||
|
||||
actionForAllAdmins([], function (admin, callback) {
|
||||
upsert(admin.id, null, 'Backup configuration is unsafe', message, '/#/backups', callback);
|
||||
const data = {
|
||||
userId: admin.id,
|
||||
eventId: null,
|
||||
title: title,
|
||||
message: message,
|
||||
acknowledged: acknowledged,
|
||||
creationTime: new Date()
|
||||
};
|
||||
|
||||
notificationdb.getByUserIdAndTitle(admin.id, title, function (error, result) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new NotificationsError(NotificationsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (!result && acknowledged) return callback(); // do not add acked alerts
|
||||
|
||||
let updateFunc = !result ? notificationdb.add.bind(null, data) : notificationdb.update.bind(null, result.id, data);
|
||||
|
||||
updateFunc(function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new NotificationsError(NotificationsError.NOT_FOUND, error.message));
|
||||
if (error) return callback(new NotificationsError(NotificationsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function mailStatusWarning(message) {
|
||||
assert.strictEqual(typeof message, 'string');
|
||||
function onEvent(id, action, source, data, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof action, 'string');
|
||||
assert.strictEqual(typeof source, 'object');
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
actionForAllAdmins([], function (admin, callback) {
|
||||
upsert(admin.id, null, 'Email is not configured properly', message, '/#/email', callback);
|
||||
}, function (error) {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
function diskSpaceWarning(message) {
|
||||
assert.strictEqual(typeof message, 'string');
|
||||
|
||||
actionForAllAdmins([], function (admin, callback) {
|
||||
upsert(admin.id, null, 'Out of Disk Space', message, '/#/graphs', callback);
|
||||
}, function (error) {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
switch (action) {
|
||||
case eventlog.ACTION_USER_ADD:
|
||||
return userAdded(source.userId, id, data.user, callback);
|
||||
|
||||
case eventlog.ACTION_USER_REMOVE:
|
||||
return userRemoved(source.userId, id, data.user, callback);
|
||||
|
||||
case eventlog.ACTION_USER_UPDATE:
|
||||
if (!data.adminStatusChanged) return callback();
|
||||
return adminChanged(source.userId, id, data.user, callback);
|
||||
|
||||
case eventlog.ACTION_APP_OOM:
|
||||
return oomEvent(id, data.app, data.addon, data.containerId, data.event, callback);
|
||||
|
||||
case eventlog.ACTION_APP_DOWN:
|
||||
return appDied(id, data.app, callback);
|
||||
|
||||
case eventlog.ACTION_APP_UP:
|
||||
return appUp(id, data.app, callback);
|
||||
|
||||
case eventlog.ACTION_APP_UPDATE_FINISH:
|
||||
return appUpdated(id, data.app, callback);
|
||||
|
||||
case eventlog.ACTION_CERTIFICATE_RENEWAL:
|
||||
case eventlog.ACTION_CERTIFICATE_NEW:
|
||||
if (!data.errorMessage) return 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
|
||||
|
||||
default:
|
||||
return callback();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ exports = module.exports = {
|
||||
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
|
||||
INFRA_VERSION_FILE: path.join(config.baseDir(), 'platformdata/INFRA_VERSION'),
|
||||
|
||||
LICENSE_FILE: '/etc/cloudron/LICENSE',
|
||||
CUSTOM_FILE: '/etc/cloudron/custom.yml',
|
||||
|
||||
PLATFORM_DATA_DIR: path.join(config.baseDir(), 'platformdata'),
|
||||
APPS_DATA_DIR: path.join(config.baseDir(), 'appsdata'),
|
||||
BOX_DATA_DIR: path.join(config.baseDir(), 'boxdata'),
|
||||
@@ -34,6 +37,7 @@ exports = module.exports = {
|
||||
|
||||
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'),
|
||||
|
||||
// this pattern is for the cloudron logs API route to work
|
||||
BACKUP_LOG_FILE: path.join(config.baseDir(), 'platformdata/logs/backup/app.log'),
|
||||
|
||||
@@ -4,8 +4,6 @@ exports = module.exports = {
|
||||
start: start,
|
||||
stop: stop,
|
||||
|
||||
handleCertChanged: handleCertChanged,
|
||||
|
||||
// exported for testing
|
||||
_isReady: false
|
||||
};
|
||||
@@ -14,17 +12,16 @@ var addons = require('./addons.js'),
|
||||
apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:platform'),
|
||||
fs = require('fs'),
|
||||
graphs = require('./graphs.js'),
|
||||
infra = require('./infra_version.js'),
|
||||
locker = require('./locker.js'),
|
||||
mail = require('./mail.js'),
|
||||
paths = require('./paths.js'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
sftp = require('./sftp.js'),
|
||||
shell = require('./shell.js'),
|
||||
taskmanager = require('./taskmanager.js'),
|
||||
_ = require('underscore');
|
||||
@@ -63,6 +60,7 @@ function start(callback) {
|
||||
// mark app state before we start addons. this gives the db import logic a chance to mark an app as errored
|
||||
startApps.bind(null, existingInfra),
|
||||
graphs.startGraphite.bind(null, existingInfra),
|
||||
sftp.startSftp.bind(null, existingInfra),
|
||||
addons.startServices.bind(null, existingInfra),
|
||||
fs.writeFile.bind(fs, paths.INFRA_VERSION_FILE, JSON.stringify(infra, null, 4))
|
||||
], function (error) {
|
||||
@@ -121,10 +119,13 @@ function pruneInfraImages(callback) {
|
||||
if (!line) continue;
|
||||
let parts = line.split(' '); // [ ID, Repo:Tag@Digest ]
|
||||
if (image.tag === parts[1]) continue; // keep
|
||||
debug(`pruneInfraImages: removing unused image of ${image.repo}: ${line}`);
|
||||
debug(`pruneInfraImages: removing unused image of ${image.repo}: tag: ${parts[1]} id: ${parts[0]}`);
|
||||
|
||||
shell.exec('pruneInfraImages', `docker rmi ${parts[0]}`, iteratorCallback);
|
||||
let result = safe.child_process.execSync(`docker rmi ${parts[0]}`, { encoding: 'utf8' });
|
||||
if (result === null) debug(`Erroring removing image ${parts[0]}: ${safe.error.mesage}`);
|
||||
}
|
||||
|
||||
iteratorCallback();
|
||||
}, callback);
|
||||
}
|
||||
|
||||
@@ -134,13 +135,13 @@ function stopContainers(existingInfra, callback) {
|
||||
// TODO: only nuke containers with isCloudronManaged=true
|
||||
debug('stopping all containers for infra upgrade');
|
||||
async.series([
|
||||
shell.exec.bind(null, 'stopContainers', 'docker ps -qa | xargs --no-run-if-empty docker stop'),
|
||||
shell.exec.bind(null, 'stopContainers', 'docker ps -qa | xargs --no-run-if-empty docker rm -f')
|
||||
shell.exec.bind(null, 'stopContainers', 'docker ps -qa --filter \'network=cloudron\' | xargs --no-run-if-empty docker stop'),
|
||||
shell.exec.bind(null, 'stopContainers', 'docker ps -qa --filter \'network=cloudron\' | xargs --no-run-if-empty docker rm -f')
|
||||
], callback);
|
||||
} else {
|
||||
assert(typeof infra.images, 'object');
|
||||
var changedAddons = [ ];
|
||||
for (var imageName in infra.images) {
|
||||
for (var imageName in existingInfra.images) { // do not use infra.images because we can only stop things which are existing
|
||||
if (infra.images[imageName].tag !== existingInfra.images[imageName].tag) changedAddons.push(imageName);
|
||||
}
|
||||
|
||||
@@ -148,8 +149,8 @@ function stopContainers(existingInfra, callback) {
|
||||
let filterArg = changedAddons.map(function (c) { return `--filter 'name=${c}'`; }).join(' '); // name=c matches *c*. required for redis-{appid}
|
||||
// ignore error if container not found (and fail later) so that this code works across restarts
|
||||
async.series([
|
||||
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} | xargs --no-run-if-empty docker stop || true`),
|
||||
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} | xargs --no-run-if-empty docker rm -f || true`)
|
||||
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} --filter 'network=cloudron' | xargs --no-run-if-empty docker stop || true`),
|
||||
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} --filter 'network=cloudron' | xargs --no-run-if-empty docker rm -f || true`)
|
||||
], callback);
|
||||
}
|
||||
}
|
||||
@@ -167,14 +168,3 @@ function startApps(existingInfra, callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
function handleCertChanged(cn, callback) {
|
||||
assert.strictEqual(typeof cn, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('handleCertChanged', cn);
|
||||
|
||||
if (cn === '*.' + config.adminDomain() || cn === config.adminFqdn()) return mail.startMail(callback);
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
@@ -6,10 +6,14 @@ exports = module.exports = {
|
||||
activate: activate,
|
||||
getStatus: getStatus,
|
||||
|
||||
autoRegister: autoRegister,
|
||||
|
||||
ProvisionError: ProvisionError
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
var appstore = require('./appstore.js'),
|
||||
AppstoreError = require('./appstore.js').AppstoreError,
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
backups = require('./backups.js'),
|
||||
BackupsError = require('./backups.js').BackupsError,
|
||||
@@ -21,10 +25,11 @@ var assert = require('assert'),
|
||||
domains = require('./domains.js'),
|
||||
DomainsError = domains.DomainsError,
|
||||
eventlog = require('./eventlog.js'),
|
||||
fs = require('fs'),
|
||||
mail = require('./mail.js'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
settingsdb = require('./settingsdb.js'),
|
||||
settings = require('./settings.js'),
|
||||
superagent = require('superagent'),
|
||||
users = require('./users.js'),
|
||||
@@ -73,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) {
|
||||
@@ -81,35 +87,24 @@ function setProgress(task, message, callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
function autoprovision(autoconf, callback) {
|
||||
assert.strictEqual(typeof autoconf, 'object');
|
||||
function autoRegister(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
async.eachSeries(Object.keys(autoconf), function (key, iteratorDone) {
|
||||
debug(`autoprovision: ${key}`);
|
||||
if (!fs.existsSync(paths.LICENSE_FILE)) return callback();
|
||||
|
||||
switch (key) {
|
||||
case 'appstoreConfig':
|
||||
if (config.provider() === 'caas') { // skip registration
|
||||
settingsdb.set(settings.APPSTORE_CONFIG_KEY, JSON.stringify(autoconf[key]), iteratorDone);
|
||||
} else { // register cloudron
|
||||
settings.setAppstoreConfig(autoconf[key], iteratorDone);
|
||||
}
|
||||
break;
|
||||
case 'caasConfig':
|
||||
settingsdb.set(settings.CAAS_CONFIG_KEY, JSON.stringify(autoconf[key]), iteratorDone);
|
||||
break;
|
||||
case 'backupConfig':
|
||||
settings.setBackupConfig(autoconf[key], iteratorDone);
|
||||
break;
|
||||
default:
|
||||
debug(`autoprovision: ${key} ignored`);
|
||||
return iteratorDone();
|
||||
const license = safe.fs.readFileSync(paths.LICENSE_FILE, 'utf8');
|
||||
if (!license) return callback(new ProvisionError(ProvisionError.EXTERNAL_ERROR, 'Cannot read license'));
|
||||
|
||||
debug('Auto-registering cloudron');
|
||||
|
||||
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'));
|
||||
}
|
||||
}, function (error) {
|
||||
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -120,7 +115,6 @@ function unprovision(callback) {
|
||||
|
||||
config.setAdminDomain('');
|
||||
config.setAdminFqdn('');
|
||||
config.setAdminLocation(constants.ADMIN_LOCATION);
|
||||
|
||||
// TODO: also cancel any existing configureWebadmin task
|
||||
async.series([
|
||||
@@ -129,9 +123,10 @@ function unprovision(callback) {
|
||||
], 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');
|
||||
|
||||
@@ -173,11 +168,12 @@ function setup(dnsConfig, autoconf, auditSource, callback) {
|
||||
callback(); // now that args are validated run the task in the background
|
||||
|
||||
async.series([
|
||||
autoRegister.bind(null, domain),
|
||||
domains.prepareDashboardDomain.bind(null, domain, auditSource, (progress) => setProgress('setup', progress.message, NOOP_CALLBACK)),
|
||||
cloudron.setDashboardDomain.bind(null, domain, auditSource), // this sets up the config.fqdn()
|
||||
mail.addDomain.bind(null, domain), // this relies on config.mailFqdn()
|
||||
mail.addDomain.bind(null, domain), // this relies on config.mailFqdn() and config.adminDomain()
|
||||
setProgress.bind(null, 'setup', 'Applying auto-configuration'),
|
||||
autoprovision.bind(null, autoconf),
|
||||
(next) => { if (!backupConfig) return next(); settings.setBackupConfig(backupConfig, next); },
|
||||
setProgress.bind(null, 'setup', 'Done'),
|
||||
eventlog.add.bind(null, eventlog.ACTION_PROVISION, auditSource, { })
|
||||
], function (error) {
|
||||
@@ -248,11 +244,10 @@ function activate(username, password, email, displayName, ip, auditSource, callb
|
||||
});
|
||||
}
|
||||
|
||||
function restore(backupConfig, backupId, version, autoconf, auditSource, callback) {
|
||||
function restore(backupConfig, backupId, version, auditSource, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof version, 'string');
|
||||
assert.strictEqual(typeof autoconf, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -286,11 +281,10 @@ function restore(backupConfig, backupId, version, autoconf, auditSource, callbac
|
||||
setProgress.bind(null, 'restore', 'Downloading backup'),
|
||||
backups.restore.bind(null, backupConfig, backupId, (progress) => setProgress('restore', progress.message, NOOP_CALLBACK)),
|
||||
setProgress.bind(null, 'restore', 'Applying auto-configuration'),
|
||||
autoprovision.bind(null, autoconf),
|
||||
// currently, our suggested restore flow is after a dnsSetup. The dnSetup creates DKIM keys and updates the DNS
|
||||
// for this reason, we have to re-setup DNS after a restore so it has DKIm from the backup
|
||||
// Once we have a 100% IP based restore, we can skip this
|
||||
mail.setDnsRecords.bind(null, config.adminDomain(), config.mailFqdn()),
|
||||
mail.setDnsRecords.bind(null, config.adminDomain()),
|
||||
eventlog.add.bind(null, eventlog.ACTION_RESTORE, auditSource, { backupId }),
|
||||
], function (error) {
|
||||
gProvisionStatus.restore.active = false;
|
||||
@@ -318,7 +312,6 @@ function getStatus(callback) {
|
||||
cloudronName: cloudronName,
|
||||
adminFqdn: config.adminDomain() ? config.adminFqdn() : null,
|
||||
activated: activated,
|
||||
edition: config.edition()
|
||||
}, gProvisionStatus));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,6 @@ exports = module.exports = {
|
||||
getCertificate: getCertificate,
|
||||
ensureCertificate: ensureCertificate,
|
||||
|
||||
renewAll: renewAll,
|
||||
renewCerts: renewCerts,
|
||||
|
||||
// the 'configure' functions always ensure a certificate
|
||||
@@ -46,11 +45,10 @@ var acme2 = require('./cert/acme2.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
fallback = require('./cert/fallback.js'),
|
||||
fs = require('fs'),
|
||||
mailer = require('./mailer.js'),
|
||||
mail = require('./mail.js'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
platform = require('./platform.js'),
|
||||
rimraf = require('rimraf'),
|
||||
safe = require('safetydance'),
|
||||
shell = require('./shell.js'),
|
||||
@@ -245,14 +243,11 @@ function setFallbackCertificate(domain, fallback, callback) {
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.key`), fallback.key)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
|
||||
}
|
||||
|
||||
platform.handleCertChanged('*.' + domain, function (error) {
|
||||
// TODO: maybe the cert is being used by the mail container
|
||||
reload(function (error) {
|
||||
if (error) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, error));
|
||||
|
||||
reload(function (error) {
|
||||
if (error) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -332,6 +327,15 @@ function getCertificate(fqdn, domain, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function notifyCertChanged(vhost, callback) {
|
||||
assert.strictEqual(typeof vhost, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (vhost !== config.mailFqdn()) return callback();
|
||||
|
||||
mail.handleCertChanged(callback);
|
||||
}
|
||||
|
||||
function ensureCertificate(vhost, domain, auditSource, callback) {
|
||||
assert.strictEqual(typeof vhost, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
@@ -358,19 +362,16 @@ 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) {
|
||||
var errorMessage = error ? error.message : '';
|
||||
eventlog.add(currentBundle ? eventlog.ACTION_CERTIFICATE_RENEWAL : eventlog.ACTION_CERTIFICATE_NEW, auditSource, { domain: vhost, errorMessage: error ? error.message : '' });
|
||||
|
||||
if (error) {
|
||||
debug('ensureCertificate: could not get certificate. using fallback certs', error);
|
||||
mailer.certificateRenewalError(vhost, errorMessage);
|
||||
}
|
||||
notifyCertChanged(vhost, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
eventlog.add(currentBundle ? eventlog.ACTION_CERTIFICATE_RENEWAL : eventlog.ACTION_CERTIFICATE_NEW, auditSource, { domain: vhost, errorMessage: errorMessage });
|
||||
// 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 no cert was returned use fallback. the fallback/caas provider will not provide any for example
|
||||
if (!certFilePath || !keyFilePath) return getFallbackCertificate(domain, callback);
|
||||
|
||||
callback(null, { certFilePath, keyFilePath, type: 'new-le' });
|
||||
callback(null, { certFilePath, keyFilePath });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -391,7 +392,6 @@ function writeAdminNginxConfig(bundle, configFileName, vhost, callback) {
|
||||
endpoint: 'admin',
|
||||
certFilePath: bundle.certFilePath,
|
||||
keyFilePath: bundle.keyFilePath,
|
||||
xFrameOptions: 'SAMEORIGIN',
|
||||
robotsTxtQuoted: JSON.stringify('User-agent: *\nDisallow: /\n')
|
||||
};
|
||||
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
|
||||
@@ -456,8 +456,7 @@ function writeAppNginxConfig(app, bundle, callback) {
|
||||
endpoint: endpoint,
|
||||
certFilePath: bundle.certFilePath,
|
||||
keyFilePath: bundle.keyFilePath,
|
||||
robotsTxtQuoted: app.robotsTxt ? JSON.stringify(app.robotsTxt) : null,
|
||||
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN' // once all apps have been updated/
|
||||
robotsTxtQuoted: app.robotsTxt ? JSON.stringify(app.robotsTxt) : null
|
||||
};
|
||||
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
|
||||
|
||||
@@ -486,8 +485,7 @@ function writeAppRedirectNginxConfig(app, fqdn, bundle, callback) {
|
||||
endpoint: 'redirect',
|
||||
certFilePath: bundle.certFilePath,
|
||||
keyFilePath: bundle.keyFilePath,
|
||||
robotsTxtQuoted: null,
|
||||
xFrameOptions: 'SAMEORIGIN'
|
||||
robotsTxtQuoted: null
|
||||
};
|
||||
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
|
||||
|
||||
@@ -584,25 +582,12 @@ function renewCerts(options, auditSource, progressCallback, callback) {
|
||||
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(function (ignoredError) {
|
||||
if (ignoredError) debug('renewAll: error reconfiguring app', ignoredError);
|
||||
|
||||
platform.handleCertChanged(appDomain.fqdn, iteratorCallback);
|
||||
});
|
||||
configureFunc(iteratorCallback);
|
||||
});
|
||||
}, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function renewAll(auditSource, callback) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('renewAll: Checking certificates for renewal');
|
||||
|
||||
renewCerts({}, auditSource, callback);
|
||||
}
|
||||
|
||||
function removeAppConfigs() {
|
||||
for (let appConfigFile of fs.readdirSync(paths.NGINX_APPCONFIG_DIR)) {
|
||||
if (appConfigFile !== constants.NGINX_DEFAULT_CONFIG_FILE_NAME && !appConfigFile.startsWith(constants.ADMIN_LOCATION)) {
|
||||
|
||||
@@ -4,8 +4,6 @@ exports = module.exports = {
|
||||
initialize: initialize,
|
||||
uninitialize: uninitialize,
|
||||
|
||||
isUnmanaged: isUnmanaged,
|
||||
|
||||
scope: scope,
|
||||
websocketAuth: websocketAuth
|
||||
};
|
||||
@@ -17,7 +15,6 @@ var accesscontrol = require('../accesscontrol.js'),
|
||||
clients = require('../clients.js'),
|
||||
ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy,
|
||||
ClientsError = clients.ClientsError,
|
||||
config = require('../config.js'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
LocalStrategy = require('passport-local').Strategy,
|
||||
passport = require('passport'),
|
||||
@@ -141,9 +138,3 @@ function websocketAuth(requiredScopes, req, res, next) {
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
function isUnmanaged(req, res, next) {
|
||||
if (!config.isManaged()) return next();
|
||||
|
||||
next(new HttpError(401, 'Managed instance does not permit this operation'));
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
verifyOwnership: verifyOwnership,
|
||||
|
||||
getApp: getApp,
|
||||
getApps: getApps,
|
||||
getAppIcon: getAppIcon,
|
||||
@@ -32,9 +30,8 @@ exports = module.exports = {
|
||||
var apps = require('../apps.js'),
|
||||
AppsError = apps.AppsError,
|
||||
assert = require('assert'),
|
||||
config = require('../config.js'),
|
||||
auditSource = require('../auditsource.js'),
|
||||
debug = require('debug')('box:routes/apps'),
|
||||
fs = require('fs'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
paths = require('../paths.js'),
|
||||
@@ -42,30 +39,6 @@ var apps = require('../apps.js'),
|
||||
util = require('util'),
|
||||
WebSocket = require('ws');
|
||||
|
||||
function auditSource(req) {
|
||||
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
|
||||
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
|
||||
}
|
||||
|
||||
function verifyOwnership(req, res, next) {
|
||||
if (req.user.admin) return next();
|
||||
|
||||
if (!config.isSpacesEnabled()) return next();
|
||||
|
||||
const appCreate = !('id' in req.params);
|
||||
|
||||
if (appCreate) return next(); // ok to install app
|
||||
|
||||
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 (app.ownerId !== req.user.id) return next(new HttpError(401, 'Unauthorized'));
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
function getApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
@@ -92,11 +65,15 @@ function getApps(req, res, next) {
|
||||
function getAppIcon(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
var iconPath = paths.APP_ICONS_DIR + '/' + req.params.id + '.png';
|
||||
fs.exists(iconPath, function (exists) {
|
||||
if (!exists) return next(new HttpError(404, 'No such icon'));
|
||||
res.sendFile(iconPath);
|
||||
});
|
||||
if (!req.query.original) {
|
||||
const userIconPath = `${paths.APP_ICONS_DIR}/${req.params.id}.user.png`;
|
||||
if (safe.fs.existsSync(userIconPath)) return res.sendFile(userIconPath);
|
||||
}
|
||||
|
||||
const appstoreIconPath = `${paths.APP_ICONS_DIR}/${req.params.id}.png`;
|
||||
if (safe.fs.existsSync(appstoreIconPath)) return res.sendFile(appstoreIconPath);
|
||||
|
||||
return next(new HttpError(404, 'No such icon'));
|
||||
}
|
||||
|
||||
function installApp(req, res, next) {
|
||||
@@ -122,6 +99,8 @@ function installApp(req, res, next) {
|
||||
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 ('label' in data && typeof data.label !== 'string') return next(new HttpError(400, 'label must be a string'));
|
||||
|
||||
// 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'));
|
||||
@@ -130,8 +109,6 @@ function installApp(req, res, next) {
|
||||
|
||||
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
|
||||
|
||||
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
|
||||
|
||||
if ('sso' in data && typeof data.sso !== 'boolean') return next(new HttpError(400, 'sso must be a boolean'));
|
||||
if ('enableBackup' in data && typeof data.enableBackup !== 'boolean') return next(new HttpError(400, 'enableBackup must be a boolean'));
|
||||
if ('enableAutomaticUpdate' in data && typeof data.enableAutomaticUpdate !== 'boolean') return next(new HttpError(400, 'enableAutomaticUpdate must be a boolean'));
|
||||
@@ -152,13 +129,13 @@ function installApp(req, res, next) {
|
||||
|
||||
debug('Installing app :%j', data);
|
||||
|
||||
apps.install(data, req.user, auditSource(req), function (error, app) {
|
||||
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.BILLING_REQUIRED) return next(new HttpError(402, 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));
|
||||
@@ -189,7 +166,6 @@ function configureApp(req, res, next) {
|
||||
if (!data.cert && data.key) return next(new HttpError(400, 'cert must be provided'));
|
||||
|
||||
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
|
||||
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
|
||||
|
||||
if ('enableBackup' in data && typeof data.enableBackup !== 'boolean') return next(new HttpError(400, 'enableBackup must be a boolean'));
|
||||
if ('enableAutomaticUpdate' in data && typeof data.enableAutomaticUpdate !== 'boolean') return next(new HttpError(400, 'enableAutomaticUpdate must be a boolean'));
|
||||
@@ -210,11 +186,13 @@ function configureApp(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 ('label' in data && typeof data.label !== 'string') return next(new HttpError(400, 'label must be a string'));
|
||||
if ('dataDir' in data && typeof data.dataDir !== 'string') return next(new HttpError(400, 'dataDir must be a string'));
|
||||
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
|
||||
|
||||
debug('Configuring app id:%s data:%j', req.params.id, data);
|
||||
|
||||
apps.configure(req.params.id, data, req.user, auditSource(req), function (error) {
|
||||
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.'));
|
||||
@@ -239,7 +217,7 @@ 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(req), function (error) {
|
||||
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));
|
||||
@@ -264,14 +242,14 @@ 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(req), function (error, result) {
|
||||
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.BILLING_REQUIRED) return next(new HttpError(402, 'Billing required'));
|
||||
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));
|
||||
@@ -300,8 +278,8 @@ function uninstallApp(req, res, next) {
|
||||
|
||||
debug('Uninstalling app id:%s', req.params.id);
|
||||
|
||||
apps.uninstall(req.params.id, auditSource(req), function (error) {
|
||||
if (error && error.reason === AppsError.BILLING_REQUIRED) return next(new HttpError(402, 'Billing required'));
|
||||
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));
|
||||
|
||||
@@ -348,12 +326,11 @@ function updateApp(req, res, next) {
|
||||
if ('appStoreId' in data && typeof data.appStoreId !== 'string') return next(new HttpError(400, 'appStoreId must be a string'));
|
||||
if (!data.manifest && !data.appStoreId) return next(new HttpError(400, 'appStoreId or manifest is required'));
|
||||
|
||||
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
|
||||
if ('force' in data && typeof data.force !== 'boolean') return next(new HttpError(400, 'force must be a boolean'));
|
||||
|
||||
debug('Update app id:%s to manifest:%j', req.params.id, data.manifest);
|
||||
|
||||
apps.update(req.params.id, req.body, auditSource(req), function (error) {
|
||||
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));
|
||||
@@ -398,7 +375,7 @@ function getLogStream(req, res, next) {
|
||||
res.on('close', logStream.close);
|
||||
logStream.on('data', function (data) {
|
||||
var obj = JSON.parse(data);
|
||||
res.write(sse(obj.monotonicTimestamp, JSON.stringify(obj))); // send timestamp as id
|
||||
res.write(sse(obj.realtimeTimestamp, JSON.stringify(obj))); // send timestamp as id
|
||||
});
|
||||
logStream.on('end', res.end.bind(res));
|
||||
logStream.on('error', res.end.bind(res, null));
|
||||
@@ -598,7 +575,7 @@ function downloadFile(req, res, next) {
|
||||
|
||||
var headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Disposition': 'attachment; filename="' + info.filename + '"'
|
||||
'Content-Disposition': `attachment; filename*=utf-8''${encodeURIComponent(info.filename)}` // RFC 2184 section 4
|
||||
};
|
||||
if (info.size) headers['Content-Length'] = info.size;
|
||||
|
||||
|
||||
105
src/routes/appstore.js
Normal file
105
src/routes/appstore.js
Normal file
@@ -0,0 +1,105 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
getApps: getApps,
|
||||
getApp: getApp,
|
||||
getAppVersion: getAppVersion,
|
||||
|
||||
registerCloudron: registerCloudron,
|
||||
getSubscription: getSubscription
|
||||
};
|
||||
|
||||
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));
|
||||
if (error && error.reason === AppstoreError.LICENSE_ERROR) return next(new HttpError(402, error.message));
|
||||
if (error && error.reason === AppstoreError.NOT_REGISTERED) return next(new HttpError(412, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
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));
|
||||
if (error && error.reason === AppstoreError.LICENSE_ERROR) return next(new HttpError(402, error.message));
|
||||
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, app));
|
||||
});
|
||||
}
|
||||
|
||||
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));
|
||||
if (error && error.reason === AppstoreError.LICENSE_ERROR) return next(new HttpError(402, error.message));
|
||||
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, manifest));
|
||||
});
|
||||
}
|
||||
|
||||
function registerCloudron(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.email !== 'string' || !req.body.email) return next(new HttpError(400, 'email must be string'));
|
||||
if (typeof req.body.password !== 'string' || !req.body.password) return next(new HttpError(400, 'password must be string'));
|
||||
if ('totpToken' in req.body && typeof req.body.totpToken !== 'string') return next(new HttpError(400, 'totpToken must be string'));
|
||||
if (typeof req.body.signup !== 'boolean') return next(new HttpError(400, 'signup must be a boolean'));
|
||||
|
||||
appstore.registerWithLoginCredentials(req.body, function (error) {
|
||||
if (error && error.reason === AppstoreError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === AppstoreError.ACCESS_DENIED) return next(new HttpError(412, error.message));
|
||||
if (error && error.reason === AppstoreError.ALREADY_REGISTERED) return next(new HttpError(422, error.message));
|
||||
if (error && error.reason === AppstoreError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(201, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function getSubscription(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
appstore.getSubscription(function (error, result) {
|
||||
if (error && error.reason === AppstoreError.INVALID_TOKEN) return next(new HttpError(402, error.message));
|
||||
if (error && error.reason === AppstoreError.NOT_REGISTERED) return next(new HttpError(412, error.message));
|
||||
if (error && error.reason === AppstoreError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, result)); // { email, cloudronId, plan, cancel_at, status }
|
||||
});
|
||||
}
|
||||
@@ -2,20 +2,17 @@
|
||||
|
||||
exports = module.exports = {
|
||||
list: list,
|
||||
startBackup: startBackup
|
||||
startBackup: startBackup,
|
||||
cleanup: cleanup
|
||||
};
|
||||
|
||||
var backupdb = require('../backupdb.js'),
|
||||
let auditSource = require('../auditsource.js'),
|
||||
backupdb = require('../backupdb.js'),
|
||||
backups = require('../backups.js'),
|
||||
BackupsError = require('../backups.js').BackupsError,
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess;
|
||||
|
||||
function auditSource(req) {
|
||||
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
|
||||
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
|
||||
}
|
||||
|
||||
function list(req, res, next) {
|
||||
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
|
||||
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
|
||||
@@ -32,10 +29,18 @@ function list(req, res, next) {
|
||||
}
|
||||
|
||||
function startBackup(req, res, next) {
|
||||
backups.startBackupTask(auditSource(req), function (error, taskId) {
|
||||
backups.startBackupTask(auditSource.fromRequest(req), function (error, taskId) {
|
||||
if (error && error.reason === BackupsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
function cleanup(req, res, next) {
|
||||
backups.startCleanupTask(auditSource.fromRequest(req), function (error, taskId) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,26 +10,23 @@ exports = module.exports = {
|
||||
checkForUpdates: checkForUpdates,
|
||||
getLogs: getLogs,
|
||||
getLogStream: getLogStream,
|
||||
setDashboardDomain: setDashboardDomain,
|
||||
setDashboardAndMailDomain: setDashboardAndMailDomain,
|
||||
prepareDashboardDomain: prepareDashboardDomain,
|
||||
renewCerts: renewCerts
|
||||
};
|
||||
|
||||
let assert = require('assert'),
|
||||
async = require('async'),
|
||||
auditSource = require('../auditsource.js'),
|
||||
cloudron = require('../cloudron.js'),
|
||||
CloudronError = cloudron.CloudronError,
|
||||
custom = require('../custom.js'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
updater = require('../updater.js'),
|
||||
updateChecker = require('../updatechecker.js'),
|
||||
UpdaterError = require('../updater.js').UpdaterError;
|
||||
|
||||
function auditSource(req) {
|
||||
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
|
||||
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
|
||||
}
|
||||
|
||||
function reboot(req, res, next) {
|
||||
// Finish the request, to let the appstore know we triggered the reboot
|
||||
next(new HttpSuccess(202, {}));
|
||||
@@ -61,8 +58,10 @@ function getDisks(req, res, next) {
|
||||
}
|
||||
|
||||
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(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));
|
||||
@@ -151,10 +150,12 @@ function getLogStream(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function setDashboardDomain(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'));
|
||||
|
||||
cloudron.setDashboardDomain(req.body.domain, auditSource(req), function (error) {
|
||||
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));
|
||||
|
||||
@@ -165,8 +166,11 @@ function setDashboardDomain(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'));
|
||||
|
||||
cloudron.prepareDashboardDomain(req.body.domain, auditSource(req), function (error, taskId) {
|
||||
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));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId }));
|
||||
@@ -174,7 +178,7 @@ function prepareDashboardDomain(req, res, next) {
|
||||
}
|
||||
|
||||
function renewCerts(req, res, next) {
|
||||
cloudron.renewCerts({ domain: req.body.domain || null }, auditSource(req), function (error, taskId) {
|
||||
cloudron.renewCerts({ domain: req.body.domain || null }, auditSource.fromRequest(req), function (error, taskId) {
|
||||
if (error && error.reason === CloudronError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
|
||||
@@ -11,16 +11,12 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
auditSource = require('../auditsource.js'),
|
||||
domains = require('../domains.js'),
|
||||
DomainsError = domains.DomainsError,
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess;
|
||||
|
||||
function auditSource(req) {
|
||||
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
|
||||
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
|
||||
}
|
||||
|
||||
function verifyDomainLock(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.domain, 'string');
|
||||
|
||||
@@ -69,7 +65,7 @@ function add(req, res, next) {
|
||||
tlsConfig: req.body.tlsConfig || { provider: 'letsencrypt-prod' }
|
||||
};
|
||||
|
||||
domains.add(req.body.domain, data, auditSource(req), function (error) {
|
||||
domains.add(req.body.domain, data, auditSource.fromRequest(req), function (error) {
|
||||
if (error && error.reason === DomainsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === DomainsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === DomainsError.INVALID_PROVIDER) return next(new HttpError(400, error.message));
|
||||
@@ -135,7 +131,7 @@ function update(req, res, next) {
|
||||
tlsConfig: req.body.tlsConfig || { provider: 'letsencrypt-prod' }
|
||||
};
|
||||
|
||||
domains.update(req.params.domain, data, auditSource(req), function (error) {
|
||||
domains.update(req.params.domain, data, auditSource.fromRequest(req), function (error) {
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error && error.reason === DomainsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === DomainsError.INVALID_PROVIDER) return next(new HttpError(400, error.message));
|
||||
@@ -148,7 +144,7 @@ function update(req, res, next) {
|
||||
function del(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.domain, 'string');
|
||||
|
||||
domains.del(req.params.domain, auditSource(req), function (error) {
|
||||
domains.del(req.params.domain, auditSource.fromRequest(req), function (error) {
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error && error.reason === DomainsError.IN_USE) return next(new HttpError(409, 'Domain is still in use. Remove all apps and mailboxes using this domain'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
@@ -16,6 +16,10 @@ function getGraphs(req, res, next) {
|
||||
delete req.headers['cookies'];
|
||||
req.url = url.format({ pathname: '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);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
exports = module.exports = {
|
||||
accesscontrol: require('./accesscontrol.js'),
|
||||
apps: require('./apps.js'),
|
||||
appstore: require('./appstore.js'),
|
||||
backups: require('./backups.js'),
|
||||
clients: require('./clients.js'),
|
||||
cloudron: require('./cloudron.js'),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user