Compare commits
595 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 42fea6eeef | |||
| 69d9f98190 | |||
| ab255e78c5 | |||
| 2628678d82 | |||
| eaf9b7f049 | |||
| b8df4d0b79 | |||
| eb315f34dc | |||
| af535757a8 | |||
| 600e030c6d | |||
| e86b813551 | |||
| af6653dfeb | |||
| f93e0c868c | |||
| e53aaddc9c | |||
| d3ebb99131 | |||
| 82037b70e4 | |||
| 323dfb1853 | |||
| 59b6d2ef66 | |||
| 7b99167d30 | |||
| c829f190df | |||
| f306f334c0 | |||
| 3fb8243264 | |||
| b8861b9d78 | |||
| 61d98ca4ca | |||
| a3a22604ba | |||
| 1766c3f6d6 | |||
| aae259479f | |||
| b640a053e3 | |||
| a3cc17705d | |||
| 78ac1d2a12 | |||
| 6ec36c9605 | |||
| bd2b03876b | |||
| 0c5cb9c98f | |||
| eddcc6fe27 | |||
| 953b55439c | |||
| e526890e3e | |||
| 71e1e2468b | |||
| fcb7c3e82a | |||
| 0d65635bc4 | |||
| 4aa76a859f | |||
| 003789265c | |||
| 1bb3b5e51b | |||
| 032ac3ed97 | |||
| edc2c25bda | |||
| 2fa44879e9 | |||
| 7e27413b29 | |||
| 77508d180e | |||
| 111f5a7c99 | |||
| d2182559e8 | |||
| 9fe91cf9cb | |||
| de000648dc | |||
| 0958a57c45 | |||
| 9aae0d9d4c | |||
| ccfd385beb | |||
| ee6cca5cdf | |||
| 0093e840c6 | |||
| 7c6e5ac32b | |||
| 15039bf293 | |||
| 89b6cd9d1f | |||
| 60992405d5 | |||
| d96b1cc864 | |||
| 5165cd8f40 | |||
| 9f8b47daa9 | |||
| 9372afad9a | |||
| eef6056174 | |||
| a1dfc758c6 | |||
| 8caf5cc741 | |||
| 7739f8f174 | |||
| 0618431be7 | |||
| 609c4388f0 | |||
| 28243956db | |||
| ff3a4f65dd | |||
| 44da148fd1 | |||
| 0b37479838 | |||
| c09aa2a498 | |||
| 00d032616f | |||
| 041285b187 | |||
| fa9aa50fdf | |||
| e0b1ebba92 | |||
| 581bbafa06 | |||
| ce93518c0a | |||
| 0ba0b009c7 | |||
| eed8f109bc | |||
| 63946509b3 | |||
| 668ff99450 | |||
| 03984a811f | |||
| 7c733ae150 | |||
| 9fe02d3818 | |||
| f10b80d90d | |||
| caf1d18250 | |||
| c700635656 | |||
| d6d2ee7d19 | |||
| f5a5da6731 | |||
| 8f2ce5f4f8 | |||
| 62619d21c0 | |||
| bf7707b70b | |||
| 698f3c833b | |||
| 5996a107ed | |||
| 0307dc5145 | |||
| f9aa09f717 | |||
| 2688a57d46 | |||
| 7ad069fd94 | |||
| 06d043dac4 | |||
| 94be6a9e3c | |||
| 97567b7d2a | |||
| 95d691154d | |||
| 9ac9b49522 | |||
| 6a887c2bba | |||
| 0250508a89 | |||
| f97973626c | |||
| 5cdf9d1c6f | |||
| 009e888686 | |||
| e3478c9d13 | |||
| 2e3ddba7e5 | |||
| 81ac44b7da | |||
| ffe50ff977 | |||
| 73faba3c28 | |||
| c1db52927e | |||
| e7120bd086 | |||
| 91ad94f978 | |||
| ee517da4f4 | |||
| 0d04213199 | |||
| 114f6c596d | |||
| 5dadd083be | |||
| 28d61a4d70 | |||
| a49969f2be | |||
| 65eaf0792e | |||
| 6153f45fd9 | |||
| d5ffb8b118 | |||
| bc283f1485 | |||
| 2d427a86f0 | |||
| 2a6edd53b6 | |||
| cf8bb3da9e | |||
| 7c1325cb34 | |||
| f4ad912cf3 | |||
| 78936a35c5 | |||
| 4ab2f8c153 | |||
| 858f03e02d | |||
| 045cfeeb0d | |||
| bbc121399e | |||
| 03d513a3b1 | |||
| 539447409e | |||
| 4525c6f39e | |||
| a6618c5813 | |||
| 5c1a0c1305 | |||
| 62c9fc90f9 | |||
| 2c60614d4b | |||
| 816fa94555 | |||
| bbdafc6a2f | |||
| 5333db5239 | |||
| 6254fe196a | |||
| 79d3713a4b | |||
| f1da537c80 | |||
| b19fc23cb2 | |||
| 5366524dc0 | |||
| f86d4f0755 | |||
| 3ad495528f | |||
| 5bfb253869 | |||
| 630fbb373c | |||
| 8d09ec5ca6 | |||
| 23af20ddc9 | |||
| 86441bfeb6 | |||
| be51daabf0 | |||
| 86e8db435a | |||
| d8401f9ef9 | |||
| 64f98aca5a | |||
| f660947594 | |||
| 576e22e1a0 | |||
| e004a00073 | |||
| c0fdac5b34 | |||
| a2a035235e | |||
| 85ac2bbe52 | |||
| c26e9bbef7 | |||
| 3e85029ea1 | |||
| 8dd3c55ecf | |||
| 1ee902a541 | |||
| 55cbe46c7c | |||
| 5a8a4e7907 | |||
| 3b5be641f0 | |||
| a34fe120fb | |||
| b9918cb6fb | |||
| 21a86175b4 | |||
| 84150f53e7 | |||
| 42c1f8bb04 | |||
| a504759b95 | |||
| af763eadd4 | |||
| 818735e2c8 | |||
| aefa8ed0d6 | |||
| 78d3aafd7a | |||
| 1fed7335cf | |||
| 34626abdcf | |||
| 477f8a3ca1 | |||
| bd7bd2adae | |||
| d47fd34f66 | |||
| f6ceee7f50 | |||
| a08f05fb44 | |||
| 6214ba7b31 | |||
| e69004548b | |||
| c8216d84ac | |||
| 5cfc3b22fa | |||
| a755aecfc5 | |||
| 64e34e13be | |||
| e455ea987a | |||
| bf87d3fc8b | |||
| cfdb939bff | |||
| 16fab63442 | |||
| fe14bcf155 | |||
| 9732b899b0 | |||
| 23b9854c57 | |||
| 0b3f65c70e | |||
| 83d56f79c6 | |||
| 8632939c6e | |||
| 93cdba8137 | |||
| 2686a1b9e3 | |||
| eec1fe1272 | |||
| f7f26fdf78 | |||
| d980d44833 | |||
| af4b2b075e | |||
| 3fcf6aa339 | |||
| 1b205ac107 | |||
| bcee0aa2ad | |||
| e2bf52b69d | |||
| 3c6dffbbc7 | |||
| 691b876d61 | |||
| ed14115ff1 | |||
| 6d9c6ffba3 | |||
| b8dd01d502 | |||
| 7e16b96abe | |||
| 705d0ba7f9 | |||
| 306c80dd93 | |||
| c8ed1d950b | |||
| 705bf3db98 | |||
| e2b388f721 | |||
| 3f34734933 | |||
| 391ee00db8 | |||
| 64a7b80395 | |||
| 46a00c839b | |||
| 9f6621434f | |||
| 3963eb687f | |||
| 56cd97147a | |||
| 614b3ed5d1 | |||
| 9f622c5e65 | |||
| 4f6a467181 | |||
| a46e208c63 | |||
| a0fd60408b | |||
| ffbbba938a | |||
| a222b3ed58 | |||
| 6ba574432a | |||
| 96075c7c20 | |||
| 64665542bc | |||
| 54d2a4f17b | |||
| 1d829c4af2 | |||
| dc4dc05628 | |||
| b2469de9b0 | |||
| 812d3576a9 | |||
| 6012eb7898 | |||
| a591b5910e | |||
| 1e084e98d1 | |||
| b4224a7f8d | |||
| ab8a010b94 | |||
| 4c164c17cf | |||
| 650f181a07 | |||
| 97ab521038 | |||
| c138c4bb5f | |||
| 1067ff882a | |||
| b6ad6e121b | |||
| a756345138 | |||
| 35f69cfea9 | |||
| 3f0bc6165b | |||
| d0dde04695 | |||
| 2f38a4018c | |||
| 2c76716bc7 | |||
| f38b87c660 | |||
| 9bac2acc37 | |||
| 68536b6d7d | |||
| 1d0a52404a | |||
| 017460b497 | |||
| 8efd496579 | |||
| 229fe0f66f | |||
| efa36ab161 | |||
| 191c85d01f | |||
| 88330ab415 | |||
| 76f5b22c07 | |||
| 0639ca1594 | |||
| e620a26c04 | |||
| 867c3595b2 | |||
| df66d77a68 | |||
| 5e919b90f5 | |||
| 428269f503 | |||
| b03e26a510 | |||
| 1e15b63a4a | |||
| 8d5e70f6aa | |||
| 91a1bc7a01 | |||
| 0e3f9c9569 | |||
| 2ad0a57fc1 | |||
| def3521ee1 | |||
| 3d004b3dcc | |||
| 0439bd8869 | |||
| 10b4043358 | |||
| ac3b0f0082 | |||
| d49a1dea7a | |||
| ec9c96da6f | |||
| 2de630e491 | |||
| 3af358b9bc | |||
| b61478edc9 | |||
| b23afdd32d | |||
| 43055da614 | |||
| 2c3f1ab720 | |||
| 35a31922a5 | |||
| bf432dc26f | |||
| 18cc93799e | |||
| fc3bc48f47 | |||
| fc96f59ecc | |||
| 534a00b3af | |||
| 619d1e44e5 | |||
| 068113bd5d | |||
| ca16072d90 | |||
| 6fac59cf9d | |||
| f953cfc4d5 | |||
| 7a1723d173 | |||
| b6643518f6 | |||
| 91470156c9 | |||
| 40c6ab5615 | |||
| 6cc4e44f22 | |||
| 976cf1740e | |||
| 22cdd3f55e | |||
| e0cd7999eb | |||
| 4f7242fa6a | |||
| 964da5ee52 | |||
| baa99d1a44 | |||
| 6d1cb1bb14 | |||
| f7e6c5cd40 | |||
| ad22df6f71 | |||
| 8e572a7c23 | |||
| e49b57294d | |||
| badb6e4672 | |||
| d09ff985af | |||
| a3130c8aab | |||
| 0843d51c98 | |||
| 9a1b5dd5cc | |||
| 6f398144cb | |||
| d91df50b9f | |||
| 99ead48fd5 | |||
| 922b6d2b18 | |||
| 6a3b45223a | |||
| 93b0f39545 | |||
| dbe86af31e | |||
| 3ede50a141 | |||
| e607fe9a41 | |||
| 43d125b216 | |||
| 9467a2922a | |||
| b35c81e546 | |||
| 59700e455e | |||
| e8fcfc4594 | |||
| 9bac7e8124 | |||
| 210c453508 | |||
| 442d4e5c6f | |||
| af63cb936d | |||
| b4c9f64721 | |||
| c64a29e6fc | |||
| f05df7cfef | |||
| f4a76a26af | |||
| f338e015d5 | |||
| 89cf8167e6 | |||
| d5194cfdc9 | |||
| eb07d3d543 | |||
| 6a1a697820 | |||
| e5cc81d8fa | |||
| 3640b0bd0e | |||
| 4d4ce9b86e | |||
| db385c6770 | |||
| 2925e98d54 | |||
| 75ee40865e | |||
| af58e56732 | |||
| dc3e3f5f4d | |||
| 83304ff66c | |||
| 575e0cea33 | |||
| 0bf3b45ddc | |||
| 826a0e7708 | |||
| 0522d1e3c4 | |||
| 12970bf50a | |||
| 4a739213bf | |||
| 4f336a05fc | |||
| c3dacba894 | |||
| f88c01eea6 | |||
| 15b0dfcb60 | |||
| ebd27b444d | |||
| ee1c587922 | |||
| 4da91ec90d | |||
| 3cf3c36e86 | |||
| 8bd6c9933f | |||
| 2e0a7dcd47 | |||
| 714c205538 | |||
| 00041add55 | |||
| 7f5fe12712 | |||
| 441fdb81f8 | |||
| fb02e8768c | |||
| 14f0f954b7 | |||
| 10f0d48b2a | |||
| 6933184c2e | |||
| a1b983de23 | |||
| b3aa59de19 | |||
| 796ced999f | |||
| 353b5e07bf | |||
| c29eef4c14 | |||
| 8bc7dc9724 | |||
| 60984d18dd | |||
| df1dc80fc1 | |||
| 8e2f0cdf73 | |||
| bf1e19f8e6 | |||
| 9a7214ea07 | |||
| 4499f08357 | |||
| 8671c4c015 | |||
| 2612cc18fa | |||
| 36a21acae8 | |||
| 1ed4710c68 | |||
| 75b6688734 | |||
| f7a7e4e95a | |||
| a8ba0b91f7 | |||
| 95540e8cbc | |||
| 9ebd22d6f7 | |||
| 1cf5807fb9 | |||
| cc7824681b | |||
| a0a523ae71 | |||
| fba70d888b | |||
| ce9fc7b3f7 | |||
| 3d4df8e26c | |||
| d3f9647cd5 | |||
| 2a49569805 | |||
| 47c8700d42 | |||
| d302dbc634 | |||
| eab3cd6b2b | |||
| 92151b1e42 | |||
| 621d00a5c6 | |||
| 5bd7cd6749 | |||
| 5fb525f011 | |||
| d8257c4745 | |||
| ef5dc7311f | |||
| 498642b670 | |||
| daa8514654 | |||
| 608de479fb | |||
| 51f7a47ea6 | |||
| 480aed9f33 | |||
| 74ae0a1787 | |||
| ed8351b0dc | |||
| a1070b7da3 | |||
| 3067d87ca9 | |||
| 56ca6f449f | |||
| 99ad3e499d | |||
| 7182ad4205 | |||
| 0b10e2b332 | |||
| f546d53ca2 | |||
| 2bcc0eef96 | |||
| a5daad2e1a | |||
| b3c8767d79 | |||
| f97f528f05 | |||
| ba8a549235 | |||
| 737541f707 | |||
| 94cb222869 | |||
| df98847535 | |||
| 3d22458f9b | |||
| d76381fa26 | |||
| 606cd4da36 | |||
| 554006683e | |||
| 0966edd8fe | |||
| 78a2176d1d | |||
| 39848a25a8 | |||
| ea946396e7 | |||
| b4d5def56d | |||
| 477abf53f3 | |||
| 0cb03e3789 | |||
| f4d7d4e7f2 | |||
| c09ae963e9 | |||
| fa30312cea | |||
| c063267c72 | |||
| 589602cdb0 | |||
| 6be062f8fd | |||
| 837ec4eb12 | |||
| 4a4166764a | |||
| 7654f36e23 | |||
| 6810c61e58 | |||
| 75f9b19db2 | |||
| 17410c9432 | |||
| 8a1de81284 | |||
| 7b540a1d2d | |||
| 8e8488a8e6 | |||
| b1b843fdd8 | |||
| c13c4d0b28 | |||
| 2371c8053f | |||
| 7dc2f3cb5b | |||
| 163563f400 | |||
| 868ed977b3 | |||
| 262fe18fb2 | |||
| 1eba79660e | |||
| 3088ac098f | |||
| 45a41ea161 | |||
| 6c17709d2a | |||
| 2a52543087 | |||
| f4f6f4e7e0 | |||
| f53c526677 | |||
| 1aa58a3905 | |||
| 2d58a6bdff | |||
| 40c22a1ad7 | |||
| 3d0da34960 | |||
| a6e53e3617 | |||
| 8efab41d37 | |||
| 9af456cc7d | |||
| 9ba78b5b87 | |||
| b1b848de21 | |||
| 5497a7d4d8 | |||
| 18887b27e6 | |||
| fb42b54210 | |||
| 4d2ba2adaa | |||
| c97e8d6bd4 | |||
| b15029de11 | |||
| 9aa74c99fc | |||
| 35c9e99102 | |||
| cab9bc3a61 | |||
| 712c920b86 | |||
| 9978dff627 | |||
| ff5bd42bef | |||
| dfa318e898 | |||
| 38977858aa | |||
| 6510240c0a | |||
| d66dc11f01 | |||
| ce4424d115 | |||
| a958c01974 | |||
| 877f181f8d | |||
| 02c0137dc1 | |||
| d0b34cc43e | |||
| 93a2cab355 | |||
| 6907475f7a | |||
| 9bf93b026b | |||
| f932f8b3d3 | |||
| 7ab5d5e50d | |||
| 5028230354 | |||
| 80e9214f5b | |||
| 5ca64dd642 | |||
| 24d9d3063b | |||
| 74b1df17c0 | |||
| 7880a2f9c3 | |||
| 8a84872704 | |||
| 5d13cc363f | |||
| 987a42b448 | |||
| 3601e4f8a6 | |||
| 60ed290179 | |||
| ff73bc121f | |||
| 6cd0601629 | |||
| b5c8e7a52a | |||
| 7f3114e67d | |||
| 1dbcf2a46a | |||
| 898cbd01b3 | |||
| b6b7d08af3 | |||
| 6a2dacb08a | |||
| 1015b0ad9c | |||
| 106e17f7ff | |||
| 6ca28d9a58 | |||
| ad6bc191f9 | |||
| 682f7a710c | |||
| f24a099e79 | |||
| 156ffb40c9 | |||
| db8b6838bb | |||
| c3631350cf | |||
| 669a1498aa | |||
| 12e55d1fab | |||
| ca9cd2cf0f | |||
| e8d9597345 | |||
| 24b0a96f07 | |||
| 858ffcec72 | |||
| 05a8911cca | |||
| 89b41b11a4 | |||
| 491d1c1273 | |||
| 0a0884bf93 | |||
| a1ac7f2ef9 | |||
| 6aef9213aa | |||
| 2e92172794 | |||
| c210359046 | |||
| 042ea081a0 | |||
| 1c32224a8a | |||
| b3fa5afe3a | |||
| 843fec9dcb | |||
| 35d9cc3c02 | |||
| 02d5d2f808 | |||
| a77d45f5de | |||
| 5e09f3dcb2 | |||
| eb566d28e7 | |||
| 8795da5d20 | |||
| a9ec46c97e | |||
| dc86b0f319 | |||
| f7089c52ff | |||
| 62793ca7b3 | |||
| 92e6909567 | |||
| 55e5c319fe | |||
| 1f8451fedb | |||
| cdc78936b5 | |||
| eaf0b4e56e |
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"env": {
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2017
|
||||
},
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
4
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -6,4 +6,3 @@ installer/src/certs/server.key
|
||||
# vim swap files
|
||||
*.swp
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"node": true,
|
||||
"browser": true,
|
||||
"unused": true,
|
||||
"multistr": true,
|
||||
"globalstrict": true,
|
||||
"predef": [ "angular", "$" ],
|
||||
"esnext": true
|
||||
}
|
||||
@@ -1317,3 +1317,150 @@
|
||||
* Make the mailbox name follow the apps new location, if the user did not set it explicitly
|
||||
* Add backups view
|
||||
|
||||
[3.0.0]
|
||||
* Support alternate app domains with redirects
|
||||
* Allow hyphen in mailbox names
|
||||
* Fix issue where the UI timesout when relay server is not reachable
|
||||
* Add support for personal spaces
|
||||
* Add UI to edit users in the groups dialog
|
||||
* Add UI to set groups when creating a user
|
||||
* Open logs and terminal in a new tab instead of a window
|
||||
* Add button to view backup logs
|
||||
* Add Mailjet mail relay support
|
||||
* Encryption support for incremental backups
|
||||
* Display restore errors in the UI
|
||||
* Update Haraka to 2.8.19
|
||||
* GPG verify releases
|
||||
* Allow subdomains in location field
|
||||
|
||||
[3.0.1]
|
||||
* Support alternate app domains with redirects
|
||||
* Allow hyphen in mailbox names
|
||||
* Fix issue where the UI timesout when relay server is not reachable
|
||||
* Add support for personal spaces
|
||||
* Add UI to edit users in the groups dialog
|
||||
* Add UI to set groups when creating a user
|
||||
* Open logs and terminal in a new tab instead of a window
|
||||
* Add button to view backup logs
|
||||
* Add Mailjet mail relay support
|
||||
* Encryption support for incremental backups
|
||||
* Display restore errors in the UI
|
||||
* Update Haraka to 2.8.19
|
||||
* GPG verify releases
|
||||
* Allow subdomains in location field
|
||||
|
||||
[3.0.2]
|
||||
* Fix issue where normal users are shown apps they don't have access to
|
||||
* Re-configure email apps when email is enabled/disabled
|
||||
|
||||
[3.1.0]
|
||||
* Add UDP support
|
||||
* Clicking invite button does not send an invite immediately
|
||||
* Implement docker addon
|
||||
* Automatically login after password reset and account setup
|
||||
* Make backup interval configurable
|
||||
* Fix alternate domain certificate renewal
|
||||
|
||||
[3.1.1]
|
||||
* Fix caas domain migration
|
||||
|
||||
[3.1.2]
|
||||
* Add UDP support
|
||||
* Clicking invite button does not send an invite immediately
|
||||
* Implement docker addon
|
||||
* Automatically login after password reset and account setup
|
||||
* Make backup interval configurable
|
||||
* Fix alternate domain certificate renewal
|
||||
* API token can now have a name
|
||||
|
||||
[3.1.3]
|
||||
* Prevent dashboard domain from being deleted
|
||||
* Add alternateDomains to app install route
|
||||
* cloudflare: Fix crash when access denied
|
||||
|
||||
[3.1.4]
|
||||
* Fix issue where support tab was redirecting
|
||||
|
||||
[3.2.0]
|
||||
* Add DO Spaces SFO2 region
|
||||
* Wildcard DNS now validates the config
|
||||
* Add ACMEv2 support
|
||||
* Add Wildcard Let's Encrypt provider
|
||||
|
||||
[3.2.1]
|
||||
* Add acme2 support. This provides DNS based validation removing inbound port 80 requirement
|
||||
* Add support for wildcard certificates
|
||||
* Allow mailbox name to be reset to the buit-in '.app' name
|
||||
* Fix permission issue when restoring a Cloudron
|
||||
* Fix a crash when restoring Cloudron
|
||||
* Allow alternate domains to be set in app installation REST API
|
||||
* Add SFO2 region for DigitalOcean Spaces
|
||||
* Show the title in port bindings instead of the long description
|
||||
|
||||
[3.2.2]
|
||||
* Update Haraka to 2.8.20
|
||||
* (mail) Fix issue where LDAP connections where not cleaned up
|
||||
|
||||
[3.3.0]
|
||||
* Use new addons with REST APIs
|
||||
* Ubuntu 18.04 LTS support
|
||||
* Custom env vars can be set per application
|
||||
* Add a button to renew certs
|
||||
* Add better support for private builds
|
||||
* cloudflare: Fix crash when using bad email
|
||||
* cloudflare: HTTP proxying works now
|
||||
* add new exoscale-sos regions
|
||||
* Add UI to toggle dynamic DNS
|
||||
* Add support for hyphenated subdomains
|
||||
|
||||
[3.3.1]
|
||||
* Use new addons with REST APIs
|
||||
* Ubuntu 18.04 LTS support
|
||||
* Custom env vars can be set per application
|
||||
* Add a button to renew certs
|
||||
* Add better support for private builds
|
||||
* cloudflare: Fix crash when using bad email
|
||||
* cloudflare: HTTP proxying works now
|
||||
* add new exoscale-sos regions
|
||||
* Add UI to toggle dynamic DNS
|
||||
* Add support for hyphenated subdomains
|
||||
|
||||
[3.3.2]
|
||||
* Use new addons with REST APIs
|
||||
* Ubuntu 18.04 LTS support
|
||||
* Custom env vars can be set per application
|
||||
* Add a button to renew certs
|
||||
* Add better support for private builds
|
||||
* cloudflare: Fix crash when using bad email
|
||||
* cloudflare: HTTP proxying works now
|
||||
* add new exoscale-sos regions
|
||||
* Add UI to toggle dynamic DNS
|
||||
* Add support for hyphenated subdomains
|
||||
* Add domain, mail events to eventlog
|
||||
|
||||
[3.3.3]
|
||||
* Use new addons with REST APIs
|
||||
* Ubuntu 18.04 LTS support
|
||||
* Custom env vars can be set per application
|
||||
* Add a button to renew certs
|
||||
* Add better support for private builds
|
||||
* cloudflare: Fix crash when using bad email
|
||||
* cloudflare: HTTP proxying works now
|
||||
* add new exoscale-sos regions
|
||||
* Add UI to toggle dynamic DNS
|
||||
* Add support for hyphenated subdomains
|
||||
* Add domain, mail events to eventlog
|
||||
|
||||
[3.3.4]
|
||||
* Use new addons with REST APIs
|
||||
* Ubuntu 18.04 LTS support
|
||||
* Custom env vars can be set per application
|
||||
* Add a button to renew certs
|
||||
* Add better support for private builds
|
||||
* cloudflare: Fix crash when using bad email
|
||||
* cloudflare: HTTP proxying works now
|
||||
* add new exoscale-sos regions
|
||||
* Add UI to toggle dynamic DNS
|
||||
* Add support for hyphenated subdomains
|
||||
* Add domain, mail events to eventlog
|
||||
|
||||
|
||||
@@ -14,8 +14,11 @@ function die {
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# hold grub since updating it breaks on some VPS providers. also, dist-upgrade will trigger it
|
||||
apt-mark hold grub* >/dev/null
|
||||
apt-get -o Dpkg::Options::="--force-confdef" update -y
|
||||
apt-get -o Dpkg::Options::="--force-confdef" dist-upgrade -y
|
||||
apt-get -o Dpkg::Options::="--force-confdef" upgrade -y
|
||||
apt-mark unhold grub* >/dev/null
|
||||
|
||||
echo "==> Installing required packages"
|
||||
|
||||
@@ -23,6 +26,7 @@ debconf-set-selections <<< 'mysql-server mysql-server/root_password password pas
|
||||
debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password password'
|
||||
|
||||
# this enables automatic security upgrades (https://help.ubuntu.com/community/AutomaticSecurityUpdates)
|
||||
# resolvconf is needed for unbound to work property after disabling systemd-resolved in 18.04
|
||||
apt-get -y install \
|
||||
acl \
|
||||
awscli \
|
||||
@@ -31,12 +35,14 @@ apt-get -y install \
|
||||
curl \
|
||||
dmsetup \
|
||||
iptables \
|
||||
libpython2.7 \
|
||||
logrotate \
|
||||
mysql-server-5.7 \
|
||||
nginx-full \
|
||||
openssh-server \
|
||||
pwgen \
|
||||
rcconf \
|
||||
resolvconf \
|
||||
sudo \
|
||||
swaks \
|
||||
unattended-upgrades \
|
||||
unbound \
|
||||
@@ -72,8 +78,9 @@ if [[ "${storage_driver}" != "overlay2" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# do not upgrade grub because it might prompt user and break this script
|
||||
echo "==> Enable memory accounting"
|
||||
apt-get -y install grub2
|
||||
apt-get -y --no-upgrade install grub2-common
|
||||
sed -e 's/^GRUB_CMDLINE_LINUX="\(.*\)"$/GRUB_CMDLINE_LINUX="\1 cgroup_enable=memory swapaccount=1 panic_on_oops=1 panic=5"/' -i /etc/default/grub
|
||||
update-grub
|
||||
|
||||
@@ -83,11 +90,12 @@ if [ ! -f "${arg_infraversionpath}/infra_version.js" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
images=$(node -e "var i = require('${arg_infraversionpath}/infra_version.js'); console.log(i.baseImages.join(' '), Object.keys(i.images).map(function (x) { return i.images[x].tag; }).join(' '));")
|
||||
images=$(node -e "var i = require('${arg_infraversionpath}/infra_version.js'); console.log(i.baseImages.map(function (x) { return x.tag; }).join(' '), Object.keys(i.images).map(function (x) { return i.images[x].tag; }).join(' '));")
|
||||
|
||||
echo -e "\tPulling docker images: ${images}"
|
||||
for image in ${images}; do
|
||||
docker pull "${image}"
|
||||
docker pull "${image%@sha256:*}" # this will tag the image for readability
|
||||
done
|
||||
|
||||
echo "==> Install collectd"
|
||||
@@ -97,6 +105,11 @@ if ! apt-get install -y collectd collectd-utils; then
|
||||
sed -e 's/^FQDNLookup true/FQDNLookup false/' -i /etc/collectd/collectd.conf
|
||||
fi
|
||||
|
||||
echo "==> Configuring host"
|
||||
sed -e 's/^#NTP=/NTP=0.ubuntu.pool.ntp.org 1.ubuntu.pool.ntp.org 2.ubuntu.pool.ntp.org 3.ubuntu.pool.ntp.org/' -i /etc/systemd/timesyncd.conf
|
||||
timedatectl set-ntp 1
|
||||
timedatectl set-timezone UTC
|
||||
|
||||
# Disable bind for good measure (on online.net, kimsufi servers these are pre-installed and conflicts with unbound)
|
||||
systemctl stop bind9 || true
|
||||
systemctl disable bind9 || true
|
||||
@@ -109,3 +122,7 @@ systemctl disable dnsmasq || true
|
||||
systemctl stop postfix || true
|
||||
systemctl disable postfix || true
|
||||
|
||||
# on ubuntu 18.04, this is the default. this requires resolvconf for DNS to work further after the disable
|
||||
systemctl stop systemd-resolved || true
|
||||
systemctl disable systemd-resolved || true
|
||||
|
||||
|
||||
@@ -9,10 +9,10 @@ require('debug').formatArgs = function formatArgs(args) {
|
||||
args[0] = this.namespace + ' ' + args[0];
|
||||
};
|
||||
|
||||
var appHealthMonitor = require('./src/apphealthmonitor.js'),
|
||||
async = require('async'),
|
||||
let async = require('async'),
|
||||
config = require('./src/config.js'),
|
||||
ldap = require('./src/ldap.js'),
|
||||
dockerProxy = require('./src/dockerproxy.js'),
|
||||
server = require('./src/server.js');
|
||||
|
||||
console.log();
|
||||
@@ -25,6 +25,9 @@ console.log(' Version: ', config.version());
|
||||
console.log(' Admin Origin: ', config.adminOrigin());
|
||||
console.log(' Appstore API server origin: ', config.apiServerOrigin());
|
||||
console.log(' Appstore Web server origin: ', config.webServerOrigin());
|
||||
console.log(' SysAdmin Port: ', config.get('sysadminPort'));
|
||||
console.log(' LDAP Server Port: ', config.get('ldapPort'));
|
||||
console.log(' Docker Proxy Port: ', config.get('dockerProxyPort'));
|
||||
console.log();
|
||||
console.log('==========================================');
|
||||
console.log();
|
||||
@@ -32,7 +35,7 @@ console.log();
|
||||
async.series([
|
||||
server.start,
|
||||
ldap.start,
|
||||
appHealthMonitor.start,
|
||||
dockerProxy.start
|
||||
], function (error) {
|
||||
if (error) {
|
||||
console.error('Error starting server', error);
|
||||
@@ -44,13 +47,19 @@ async.series([
|
||||
var NOOP_CALLBACK = function () { };
|
||||
|
||||
process.on('SIGINT', function () {
|
||||
console.log('Received SIGINT. Shutting down.');
|
||||
|
||||
server.stop(NOOP_CALLBACK);
|
||||
ldap.stop(NOOP_CALLBACK);
|
||||
dockerProxy.stop(NOOP_CALLBACK);
|
||||
setTimeout(process.exit.bind(process), 3000);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', function () {
|
||||
console.log('Received SIGTERM. Shutting down.');
|
||||
|
||||
server.stop(NOOP_CALLBACK);
|
||||
ldap.stop(NOOP_CALLBACK);
|
||||
dockerProxy.stop(NOOP_CALLBACK);
|
||||
setTimeout(process.exit.bind(process), 3000);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
db.runSql.bind(db, 'ALTER TABLE apps ADD COLUMN ownerId VARCHAR(128)'),
|
||||
function (next) {
|
||||
db.all('SELECT id FROM users ORDER BY createdAt LIMIT 1', [ ], function (error, results) {
|
||||
if (error || results.length === 0) return next(error);
|
||||
|
||||
var ownerId = results[0].id;
|
||||
db.runSql('UPDATE apps SET ownerId=?', [ ownerId ], next);
|
||||
});
|
||||
},
|
||||
db.runSql.bind(db, 'ALTER TABLE apps MODIFY ownerId VARCHAR(128) NOT NULL'),
|
||||
db.runSql.bind(db, 'ALTER TABLE apps ADD CONSTRAINT apps_owner_constraint FOREIGN KEY(ownerId) REFERENCES users(id)'),
|
||||
db.runSql.bind(db, 'COMMIT'),
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN ownerId', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN ts TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN ts ', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
var cmd = 'CREATE TABLE IF NOT EXISTS subdomains(' +
|
||||
'appId VARCHAR(128) NOT NULL,' +
|
||||
'domain VARCHAR(128) NOT NULL,' +
|
||||
'subdomain VARCHAR(128) NOT NULL,' +
|
||||
'type VARCHAR(128) NOT NULL,' +
|
||||
'dnsRecordId VARCHAR(512),' +
|
||||
'FOREIGN KEY(domain) REFERENCES domains(domain),' +
|
||||
'FOREIGN KEY(appId) REFERENCES apps(id),' +
|
||||
'UNIQUE (subdomain, domain)) CHARACTER SET utf8 COLLATE utf8_bin';
|
||||
|
||||
db.runSql(cmd, function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('DROP TABLE subdomains', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT * from apps', [ ], function (error, results) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var queries = [
|
||||
db.runSql.bind(db, 'START TRANSACTION;')
|
||||
];
|
||||
|
||||
results.forEach(function (app) {
|
||||
queries.push(db.runSql.bind(db, 'INSERT INTO subdomains (appId, domain, subdomain, type, dnsRecordId) VALUES (?, ?, ?, ?, ?)', [ app.id, app.domain, app.location, 'primary', app.dnsRecordId ]));
|
||||
});
|
||||
|
||||
queries.push(db.runSql.bind(db, 'COMMIT'));
|
||||
|
||||
async.series(queries, callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('DELETE FROM subdomains', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP INDEX location_domain_unique_index, DROP FOREIGN KEY apps_domain_constraint, DROP COLUMN domain, DROP COLUMN location, DROP COLUMN dnsRecordId', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.all('SELECT * from subdomains WHERE type = ?', [ 'primary' ], function (error, results) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var cmd = 'ALTER TABLE apps'
|
||||
+ ' ADD COLUMN location VARCHAR(128),'
|
||||
+ ' ADD COLUMN domain VARCHAR(128),'
|
||||
+ ' ADD COLUMN dnsRecordId VARCHAR(512)';
|
||||
|
||||
db.runSql(cmd, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var queries = [ db.runSql.bind(db, 'START TRANSACTION;') ];
|
||||
results.forEach(function (d) {
|
||||
queries.push(db.runSql.bind(db, 'UPDATE apps SET domain = ?, location = ?, dnsRecordId = ? WHERE id = ?', [ d.domain, d.subdomain, d.appId, d.dnsRecordId ]));
|
||||
});
|
||||
queries.push(db.runSql.bind(db, 'COMMIT'));
|
||||
|
||||
async.series(queries, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var cmd = 'ALTER TABLE apps'
|
||||
+ ' ADD CONSTRAINT apps_domain_constraint FOREIGN KEY(domain) REFERENCES domains(domain),'
|
||||
+ ' ADD UNIQUE location_domain_unique_index (location, domain)';
|
||||
|
||||
db.runSql(cmd, callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE subdomains DROP COLUMN dnsRecordId', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE subdomains ADD COLUMN dnsRecordId VARCHAR(512)', function (error) {
|
||||
if (error) return callback(error);
|
||||
callback();
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users ADD COLUMN admin BOOLEAN DEFAULT 0', function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
db.all('SELECT userId FROM groupMembers WHERE groupId=?', [ 'admin' ], function (error, results) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (results.length === 0) return callback();
|
||||
|
||||
async.eachSeries(results, function (result, iteratorDone) {
|
||||
db.runSql('UPDATE users SET admin=1 WHERE id=?', [ result.userId ], iteratorDone);
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.series([
|
||||
db.runSql.bind(db, 'DELETE FROM groupMembers WHERE groupId=?', [ 'admin' ]),
|
||||
db.runSql.bind(db, 'DELETE FROM groups WHERE id=?', [ 'admin' ])
|
||||
], callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users DROP COLUMN admin', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('UPDATE tokens SET expires=? WHERE clientId=?', [ 1525636734905, 'cid-webadmin' ], function (error) { // force webadmin to get a new token
|
||||
if (error) console.error(error);
|
||||
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE appPortBindings ADD COLUMN type VARCHAR(8) NOT NULL DEFAULT "tcp"'),
|
||||
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP INDEX hostPort'), // this drops the unique constraint
|
||||
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP PRIMARY KEY, ADD PRIMARY KEY(hostPort, type)')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP PRIMARY KEY, ADD PRIMARY KEY(hostPort)'),
|
||||
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP COLUMN type')
|
||||
], callback);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
|
||||
if (error || results.length === 0) return callback(error);
|
||||
|
||||
var backupConfig = JSON.parse(results[0].value);
|
||||
backupConfig.intervalSecs = 24 * 60 * 60;
|
||||
db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [ JSON.stringify(backupConfig) ], callback);
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
// first check precondtion of domain entry in settings
|
||||
db.all('SELECT * FROM domains', [ ], function (error, domains) {
|
||||
if (error) return callback(error);
|
||||
|
||||
let caasDomains = domains.filter(function (d) { return d.provider === 'caas'; });
|
||||
|
||||
async.eachSeries(caasDomains, function (domain, iteratorCallback) {
|
||||
let config = JSON.parse(domain.configJson);
|
||||
config.hyphenatedSubdomains = true;
|
||||
|
||||
db.runSql('UPDATE domains SET configJson = ? WHERE domain = ?', [ JSON.stringify(config), domain.domain ], iteratorCallback);
|
||||
}, callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE tokens ADD COLUMN name VARCHAR(64) DEFAULT ""', [], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE tokens DROP COLUMN name', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT * from domains WHERE provider=?', [ 'manual' ], function (error, results) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(results, function (result, iteratorDone) {
|
||||
var config = JSON.parse(result.configJson || '{}');
|
||||
if (!config.wildcard) return iteratorDone();
|
||||
delete config.wildcard;
|
||||
|
||||
db.runSql('UPDATE domains SET provider=?, configJson=? WHERE domain=?', [ 'wildcard', JSON.stringify(config), result.domain ], iteratorDone);
|
||||
}, callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
var cmd = 'CREATE TABLE IF NOT EXISTS appEnvVars(' +
|
||||
'appId VARCHAR(128) NOT NULL,' +
|
||||
'name TEXT NOT NULL,' +
|
||||
'value TEXT NOT NULL,' +
|
||||
'FOREIGN KEY(appId) REFERENCES apps(id)) CHARACTER SET utf8 COLLATE utf8_bin';
|
||||
|
||||
db.runSql(cmd, function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('DROP TABLE appEnvVars', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
+27
-5
@@ -25,6 +25,7 @@ CREATE TABLE IF NOT EXISTS users(
|
||||
fallbackEmail VARCHAR(512) DEFAULT "",
|
||||
twoFactorAuthenticationSecret VARCHAR(128) DEFAULT "",
|
||||
twoFactorAuthenticationEnabled BOOLEAN DEFAULT false,
|
||||
admin BOOLEAN DEFAULT false,
|
||||
|
||||
PRIMARY KEY(id));
|
||||
|
||||
@@ -40,6 +41,7 @@ CREATE TABLE IF NOT EXISTS groupMembers(
|
||||
FOREIGN KEY(userId) REFERENCES users(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tokens(
|
||||
name VARCHAR(64) DEFAULT "", // description
|
||||
accessToken VARCHAR(128) NOT NULL UNIQUE,
|
||||
identifier VARCHAR(128) NOT NULL,
|
||||
clientId VARCHAR(128),
|
||||
@@ -49,7 +51,7 @@ CREATE TABLE IF NOT EXISTS tokens(
|
||||
|
||||
CREATE TABLE IF NOT EXISTS clients(
|
||||
id VARCHAR(128) NOT NULL UNIQUE, // prefixed with cid- to identify token easily in auth routes
|
||||
appId VARCHAR(128) NOT NULL,
|
||||
appId VARCHAR(128) NOT NULL, // name of the client (for external apps) or id of app (for built-in apps)
|
||||
type VARCHAR(16) NOT NULL,
|
||||
clientSecret VARCHAR(512) NOT NULL,
|
||||
redirectURI VARCHAR(512) NOT NULL,
|
||||
@@ -68,10 +70,10 @@ CREATE TABLE IF NOT EXISTS apps(
|
||||
httpPort INTEGER, // this is the nginx proxy port and not manifest.httpPort
|
||||
location VARCHAR(128) NOT NULL,
|
||||
domain VARCHAR(128) NOT NULL,
|
||||
dnsRecordId VARCHAR(512), // tracks any id that we got back to track dns updates
|
||||
accessRestrictionJson TEXT, // { users: [ ], groups: [ ] }
|
||||
createdAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updatedAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
creationTime TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app was installed
|
||||
updateTime TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the last app update was done
|
||||
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, // when this db record was updated (useful for UI caching)
|
||||
memoryLimit BIGINT DEFAULT 0,
|
||||
xFrameOptions VARCHAR(512),
|
||||
sso BOOLEAN DEFAULT 1, // whether user chose to enable SSO
|
||||
@@ -84,11 +86,14 @@ CREATE TABLE IF NOT EXISTS apps(
|
||||
oldConfigJson TEXT, // used to pass old config to apptask (configure, restore)
|
||||
updateConfigJson TEXT, // used to pass new config to apptask (update)
|
||||
|
||||
FOREIGN KEY(domain) REFERENCES domains(domain),
|
||||
ownerId VARCHAR(128),
|
||||
|
||||
FOREIGN KEY(ownerId) REFERENCES users(id),
|
||||
PRIMARY KEY(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS appPortBindings(
|
||||
hostPort INTEGER NOT NULL UNIQUE,
|
||||
type VARCHAR(8) NOT NULL DEFAULT "tcp",
|
||||
environmentVariable VARCHAR(128) NOT NULL,
|
||||
appId VARCHAR(128) NOT NULL,
|
||||
FOREIGN KEY(appId) REFERENCES apps(id),
|
||||
@@ -113,6 +118,12 @@ CREATE TABLE IF NOT EXISTS appAddonConfigs(
|
||||
value VARCHAR(512) NOT NULL,
|
||||
FOREIGN KEY(appId) REFERENCES apps(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS appEnvVars(
|
||||
appId VARCHAR(128) NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
FOREIGN KEY(appId) REFERENCES apps(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS backups(
|
||||
id VARCHAR(128) NOT NULL,
|
||||
creationTime TIMESTAMP,
|
||||
@@ -176,3 +187,14 @@ CREATE TABLE IF NOT EXISTS mailboxes(
|
||||
FOREIGN KEY(domain) REFERENCES mail(domain),
|
||||
UNIQUE (name, domain));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS subdomains(
|
||||
appId VARCHAR(128) NOT NULL,
|
||||
domain VARCHAR(128) NOT NULL,
|
||||
subdomain VARCHAR(128) NOT NULL,
|
||||
type VARCHAR(128) NOT NULL,
|
||||
|
||||
FOREIGN KEY(domain) REFERENCES domains(domain),
|
||||
FOREIGN KEY(appId) REFERENCES apps(id),
|
||||
UNIQUE (subdomain, domain))
|
||||
|
||||
CHARACTER SET utf8 COLLATE utf8_bin;
|
||||
|
||||
Generated
+253
-56
@@ -1171,8 +1171,7 @@
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
|
||||
"dev": true
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
||||
},
|
||||
"base32.js": {
|
||||
"version": "0.0.1",
|
||||
@@ -1633,15 +1632,15 @@
|
||||
}
|
||||
},
|
||||
"cloudron-manifestformat": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-2.11.0.tgz",
|
||||
"integrity": "sha512-t4KR2KmK1JDtxw1n6IVNg0+xxspk/Cpb6m1WimE9hJ0KJYsIgZNnkce47uYiG9/nWrgUSV4xcdzsS91OOvWgig==",
|
||||
"version": "2.14.2",
|
||||
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-2.14.2.tgz",
|
||||
"integrity": "sha512-+VQwlP/2NY0VIjPTkANhlg8DrS62IxkAVS7B7KG6DrzRp+hRCejbDMQjUB8GvyPSVQGerqUoEu5R/tc4/UBiAA==",
|
||||
"requires": {
|
||||
"cron": "1.3.0",
|
||||
"java-packagename-regex": "https://registry.npmjs.org/java-packagename-regex/-/java-packagename-regex-1.0.0.tgz",
|
||||
"java-packagename-regex": "1.0.0",
|
||||
"safetydance": "0.0.15",
|
||||
"semver": "4.3.6",
|
||||
"tv4": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz",
|
||||
"tv4": "1.3.0",
|
||||
"validator": "3.43.0"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -1709,6 +1708,41 @@
|
||||
"typedarray": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz"
|
||||
}
|
||||
},
|
||||
"connect": {
|
||||
"version": "3.6.6",
|
||||
"resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz",
|
||||
"integrity": "sha1-Ce/2xVr3I24TcTWnJXSFi2eG9SQ=",
|
||||
"requires": {
|
||||
"debug": "2.6.9",
|
||||
"finalhandler": "1.1.0",
|
||||
"parseurl": "1.3.2",
|
||||
"utils-merge": "1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"finalhandler": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz",
|
||||
"integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=",
|
||||
"requires": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "1.0.2",
|
||||
"escape-html": "1.0.3",
|
||||
"on-finished": "2.3.0",
|
||||
"parseurl": "1.3.2",
|
||||
"statuses": "1.3.1",
|
||||
"unpipe": "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"connect-ensure-login": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/connect-ensure-login/-/connect-ensure-login-0.1.1.tgz",
|
||||
@@ -2650,7 +2684,6 @@
|
||||
"version": "0.8.6",
|
||||
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.6.tgz",
|
||||
"integrity": "sha1-QooiOv4DQl0s1tY0f99AxmkDVj0=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"nan": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz"
|
||||
}
|
||||
@@ -2684,6 +2717,11 @@
|
||||
"safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz"
|
||||
}
|
||||
},
|
||||
"ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
|
||||
},
|
||||
"ejs": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.1.tgz",
|
||||
@@ -3194,22 +3232,18 @@
|
||||
"dependencies": {
|
||||
"mkdirp": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz",
|
||||
"resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz",
|
||||
"integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=",
|
||||
"dev": true
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz",
|
||||
"resolved": "http://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz",
|
||||
"integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"fs.realpath": {
|
||||
"version": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
||||
},
|
||||
"fstream": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz",
|
||||
@@ -3950,18 +3984,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"glob": {
|
||||
"version": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
|
||||
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
|
||||
"requires": {
|
||||
"fs.realpath": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"inflight": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"once": "1.4.0",
|
||||
"path-is-absolute": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz"
|
||||
}
|
||||
},
|
||||
"globule": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz",
|
||||
@@ -4492,7 +4514,8 @@
|
||||
}
|
||||
},
|
||||
"java-packagename-regex": {
|
||||
"version": "https://registry.npmjs.org/java-packagename-regex/-/java-packagename-regex-1.0.0.tgz",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/java-packagename-regex/-/java-packagename-regex-1.0.0.tgz",
|
||||
"integrity": "sha1-lR9he9WhlCIO0GcLm4KowOxcYiQ="
|
||||
},
|
||||
"js-base64": {
|
||||
@@ -4553,7 +4576,7 @@
|
||||
},
|
||||
"jsonfile": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-1.0.1.tgz",
|
||||
"resolved": "http://registry.npmjs.org/jsonfile/-/jsonfile-1.0.1.tgz",
|
||||
"integrity": "sha1-6l7+QLg2kLmGZ2FKc5L8YOhCwN0=",
|
||||
"dev": true
|
||||
},
|
||||
@@ -5257,6 +5280,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
},
|
||||
"multiparty": {
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.1.4.tgz",
|
||||
@@ -5405,7 +5433,7 @@
|
||||
},
|
||||
"ncp": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/ncp/-/ncp-0.4.2.tgz",
|
||||
"resolved": "http://registry.npmjs.org/ncp/-/ncp-0.4.2.tgz",
|
||||
"integrity": "sha1-q8xsvT7C7Spyn/bnwfqPAXhKhXQ=",
|
||||
"dev": true
|
||||
},
|
||||
@@ -5966,6 +5994,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"on-finished": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
|
||||
"requires": {
|
||||
"ee-first": "1.1.1"
|
||||
}
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
@@ -6115,6 +6151,11 @@
|
||||
"resolved": "https://registry.npmjs.org/parse-links/-/parse-links-0.1.0.tgz",
|
||||
"integrity": "sha1-afpighugBBX+c2MyNVIeRUe36CE="
|
||||
},
|
||||
"parseurl": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
|
||||
"integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M="
|
||||
},
|
||||
"passport": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/passport/-/passport-0.4.0.tgz",
|
||||
@@ -6813,29 +6854,16 @@
|
||||
}
|
||||
},
|
||||
"readdirp": {
|
||||
"version": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz",
|
||||
"integrity": "sha512-LgQ8mdp6hbxJUZz27qxVl7gmFM/0DfHRO52c5RUbKAgMvr81tour7YYWW1JYNmrXyD/o0Myy9/DC3fUYkqnyzg==",
|
||||
"dev": true,
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz",
|
||||
"integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=",
|
||||
"requires": {
|
||||
"graceful-fs": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
|
||||
"minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"readable-stream": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz",
|
||||
"set-immediate-shim": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz"
|
||||
}
|
||||
},
|
||||
"recursive-readdir": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz",
|
||||
"integrity": "sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg==",
|
||||
"requires": {
|
||||
"minimatch": "3.0.4"
|
||||
"graceful-fs": "4.1.11",
|
||||
"minimatch": "3.0.4",
|
||||
"readable-stream": "2.3.6",
|
||||
"set-immediate-shim": "1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
@@ -6850,6 +6878,26 @@
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
|
||||
},
|
||||
"graceful-fs": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
|
||||
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg="
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
|
||||
},
|
||||
"isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
@@ -6857,6 +6905,79 @@
|
||||
"requires": {
|
||||
"brace-expansion": "1.1.11"
|
||||
}
|
||||
},
|
||||
"process-nextick-args": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
|
||||
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw=="
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
||||
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
|
||||
"requires": {
|
||||
"core-util-is": "1.0.2",
|
||||
"inherits": "2.0.3",
|
||||
"isarray": "1.0.0",
|
||||
"process-nextick-args": "2.0.0",
|
||||
"safe-buffer": "5.1.2",
|
||||
"string_decoder": "1.1.1",
|
||||
"util-deprecate": "1.0.2"
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"requires": {
|
||||
"safe-buffer": "5.1.2"
|
||||
}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
}
|
||||
}
|
||||
},
|
||||
"recursive-readdir": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz",
|
||||
"integrity": "sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"minimatch": "3.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
|
||||
"dev": true
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "1.1.11"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -7234,7 +7355,73 @@
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
|
||||
"integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==",
|
||||
"requires": {
|
||||
"glob": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz"
|
||||
"glob": "7.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"requires": {
|
||||
"balanced-match": "1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
},
|
||||
"fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
|
||||
"integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
|
||||
"requires": {
|
||||
"fs.realpath": "1.0.0",
|
||||
"inflight": "1.0.6",
|
||||
"inherits": "2.0.3",
|
||||
"minimatch": "3.0.4",
|
||||
"once": "1.4.0",
|
||||
"path-is-absolute": "1.0.1"
|
||||
}
|
||||
},
|
||||
"inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||
"requires": {
|
||||
"once": "1.4.0",
|
||||
"wrappy": "1.0.2"
|
||||
}
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"requires": {
|
||||
"brace-expansion": "1.1.11"
|
||||
}
|
||||
},
|
||||
"path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
}
|
||||
}
|
||||
},
|
||||
"s3-block-read-stream": {
|
||||
@@ -7249,8 +7436,7 @@
|
||||
"safe-json-stringify": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.1.0.tgz",
|
||||
"integrity": "sha512-EzBtUaFH9bHYPc69wqjp0efJI/DPNHdFbGE3uIMn4sVbO0zx8vZ8cG4WKxQfOpUOKsQyGBiT2mTqnCw+6nLswA==",
|
||||
"optional": true
|
||||
"integrity": "sha512-EzBtUaFH9bHYPc69wqjp0efJI/DPNHdFbGE3uIMn4sVbO0zx8vZ8cG4WKxQfOpUOKsQyGBiT2mTqnCw+6nLswA=="
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
@@ -7641,9 +7827,9 @@
|
||||
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
|
||||
},
|
||||
"set-immediate-shim": {
|
||||
"version": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
|
||||
"integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=",
|
||||
"dev": true
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
|
||||
"integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E="
|
||||
},
|
||||
"shebang-command": {
|
||||
"version": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
|
||||
@@ -7796,6 +7982,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"statuses": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz",
|
||||
"integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4="
|
||||
},
|
||||
"stdout-stream": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.0.tgz",
|
||||
@@ -8402,7 +8593,8 @@
|
||||
"dev": true
|
||||
},
|
||||
"tv4": {
|
||||
"version": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz",
|
||||
"integrity": "sha1-0CDIRvrdUMhVq7JeuuzGj8EPeWM="
|
||||
},
|
||||
"tweetnacl": {
|
||||
@@ -8459,6 +8651,11 @@
|
||||
"version": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
},
|
||||
"utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
|
||||
},
|
||||
"uuid": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz",
|
||||
|
||||
+5
-5
@@ -20,7 +20,8 @@
|
||||
"async": "^2.6.1",
|
||||
"aws-sdk": "^2.253.1",
|
||||
"body-parser": "^1.18.3",
|
||||
"cloudron-manifestformat": "^2.11.0",
|
||||
"cloudron-manifestformat": "^2.14.2",
|
||||
"connect": "^3.6.6",
|
||||
"connect-ensure-login": "^0.1.1",
|
||||
"connect-lastmile": "^1.0.2",
|
||||
"connect-timeout": "^1.9.0",
|
||||
@@ -57,7 +58,7 @@
|
||||
"progress-stream": "^2.0.0",
|
||||
"proxy-middleware": "^0.15.0",
|
||||
"qrcode": "^1.2.0",
|
||||
"recursive-readdir": "^2.2.2",
|
||||
"readdirp": "^2.1.0",
|
||||
"request": "^2.87.0",
|
||||
"rimraf": "^2.6.2",
|
||||
"s3-block-read-stream": "^0.5.0",
|
||||
@@ -86,13 +87,12 @@
|
||||
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
|
||||
"nock": "^9.0.14",
|
||||
"node-sass": "^4.6.1",
|
||||
"readdirp": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz"
|
||||
"recursive-readdir": "^2.2.2"
|
||||
},
|
||||
"scripts": {
|
||||
"migrate_local": "DATABASE_URL=mysql://root:@localhost/box node_modules/.bin/db-migrate up",
|
||||
"migrate_test": "BOX_ENV=test DATABASE_URL=mysql://root:@localhost/boxtest node_modules/.bin/db-migrate up",
|
||||
"test": "npm run migrate_test && src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- --exit -R spec ./src/test ./src/routes/test/[^a]*",
|
||||
"test_all": "npm run migrate_test && src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- --exit -R spec ./src/test ./src/routes/test",
|
||||
"test": "npm run migrate_test && src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- --exit -R spec ./src/test ./src/routes/test",
|
||||
"postmerge": "/bin/true",
|
||||
"precommit": "/bin/true",
|
||||
"prepush": "npm test",
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
|
||||
|
||||
function get_status() {
|
||||
key="$1"
|
||||
if status=$($curl -q -f "http://localhost:3000/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
|
||||
}
|
||||
|
||||
domain=""
|
||||
domainProvider=""
|
||||
domainConfigJson="{}"
|
||||
domainTlsProvider="letsencrypt-prod"
|
||||
adminUsername="superadmin"
|
||||
adminPassword="Secret123#"
|
||||
adminEmail="admin@server.local"
|
||||
appstoreUserId=""
|
||||
appstoreToken=""
|
||||
backupDir="/var/backups"
|
||||
|
||||
args=$(getopt -o "" -l "domain:,domain-provider:,domain-tls-provider:,admin-username:,admin-password:,admin-email:,appstore-user:,appstore-token:,backup-dir:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--domain) domain="$2"; shift 2;;
|
||||
--domain-provider) domainProvider="$2"; shift 2;;
|
||||
--domain-tls-provider) domainTlsProvider="$2"; shift 2;;
|
||||
--admin-username) adminUsername="$2"; shift 2;;
|
||||
--admin-password) adminPassword="$2"; shift 2;;
|
||||
--admin-email) adminEmail="$2"; shift 2;;
|
||||
--appstore-user) appstoreUser="$2"; shift 2;;
|
||||
--appstore-token) appstoreToken="$2"; shift 2;;
|
||||
--backup-dir) backupDir="$2"; shift 2;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "=> Waiting for cloudron to be ready"
|
||||
wait_for_status "version" '*'
|
||||
|
||||
if [[ $(get_status "webadminStatus") != *'"tls": true'* ]]; then
|
||||
echo "=> Domain setup"
|
||||
dnsSetupData=$(printf '{ "domain": "%s", "adminFqdn": "%s", "provider": "%s", "config": %s, "tlsConfig": { "provider": "%s" } }' "${domain}" "my.${domain}" "${domainProvider}" "$domainConfigJson" "${domainTlsProvider}")
|
||||
|
||||
if ! $curl -X POST -H "Content-Type: application/json" -d "${dnsSetupData}" http://localhost:3000/api/v1/cloudron/dns_setup; then
|
||||
echo "DNS Setup Failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
wait_for_status "webadminStatus" '*"tls": true*'
|
||||
else
|
||||
echo "=> Skipping Domain setup"
|
||||
fi
|
||||
|
||||
activationData=$(printf '{"username": "%s", "password":"%s", "email": "%s" }' "${adminUsername}" "${adminPassword}" "${adminEmail}")
|
||||
if [[ $(get_status "activated") == "false" ]]; then
|
||||
echo "=> Activating"
|
||||
|
||||
if ! activationResult=$($curl -X POST -H "Content-Type: application/json" -d "${activationData}" http://localhost:3000/api/v1/cloudron/activate); then
|
||||
echo "Failed to activate with ${activationData}: ${activationResult}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
wait_for_status "activated" "true"
|
||||
else
|
||||
echo "=> Skipping Activation"
|
||||
fi
|
||||
|
||||
echo "=> Getting token"
|
||||
if ! activationResult=$($curl -X POST -H "Content-Type: application/json" -d "${activationData}" http://localhost:3000/api/v1/developer/login); then
|
||||
echo "Failed to login with ${activationData}: ${activationResult}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
accessToken=$(echo "${activationResult}" | python3 -c 'import sys, json; print(json.load(sys.stdin)[sys.argv[1]])' "accessToken")
|
||||
|
||||
echo "=> Setting up App Store account with accessToken ${accessToken}"
|
||||
appstoreData=$(printf '{"userId":"%s", "token":"%s" }' "${appstoreUser}" "${appstoreToken}")
|
||||
|
||||
if ! appstoreResult=$($curl -X POST -H "Content-Type: application/json" -d "${appstoreData}" "http://localhost:3000/api/v1/settings/appstore_config?access_token=${accessToken}"); then
|
||||
echo "Failed to setup Appstore account with ${appstoreData}: ${appstoreResult}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=> Setting up Backup Directory with accessToken ${accessToken}"
|
||||
backupData=$(printf '{"provider":"filesystem", "key":"", "backupFolder":"%s", "retentionSecs": 864000, "format": "tgz", "externalDisk": true}' "${backupDir}")
|
||||
|
||||
chown -R yellowtent:yellowtent "${backupDir}"
|
||||
|
||||
if ! backupResult=$($curl -X POST -H "Content-Type: application/json" -d "${backupData}" "http://localhost:3000/api/v1/settings/backup_config?access_token=${accessToken}"); then
|
||||
echo "Failed to setup backup configuration with ${backupDir}: ${backupResult}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=> Done!"
|
||||
|
||||
Executable
+106
@@ -0,0 +1,106 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
|
||||
|
||||
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}"
|
||||
|
||||
+57
-31
@@ -4,7 +4,6 @@ set -eu -o pipefail
|
||||
|
||||
# change this to a hash when we make a upgrade release
|
||||
readonly LOG_FILE="/var/log/cloudron-setup.log"
|
||||
readonly DATA_FILE="/root/cloudron-install-data.json"
|
||||
readonly MINIMUM_DISK_SIZE_GB="18" # this is the size of "/" and required to fit in docker images 18 is a safe bet for different reporting on 20GB min
|
||||
readonly MINIMUM_MEMORY="974" # this is mostly reported for 1GB main memory (DO 992, EC2 990, Linode 989, Serverdiscounter.com 974)
|
||||
|
||||
@@ -36,40 +35,42 @@ if [[ "${disk_size_gb}" -lt "${MINIMUM_DISK_SIZE_GB}" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if systemctl -q is-active box; then
|
||||
echo "Error: Cloudron is already installed. To reinstall, start afresh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
initBaseImage="true"
|
||||
# provisioning data
|
||||
provider=""
|
||||
edition=""
|
||||
requestedVersion=""
|
||||
apiServerOrigin="https://api.cloudron.io"
|
||||
webServerOrigin="https://cloudron.io"
|
||||
prerelease="false"
|
||||
sourceTarballUrl=""
|
||||
rebootServer="true"
|
||||
baseDataDir=""
|
||||
|
||||
args=$(getopt -o "" -l "help,skip-baseimage-init,data-dir:,provider:,version:,env:,prerelease,skip-reboot" -n "$0" -- "$@")
|
||||
args=$(getopt -o "" -l "help,skip-baseimage-init,provider:,version:,env:,edition:,skip-reboot" -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
|
||||
apiServerOrigin="https://api.dev.cloudron.io"
|
||||
webServerOrigin="https://dev.cloudron.io"
|
||||
prerelease="true"
|
||||
elif [[ "$2" == "staging" ]]; then
|
||||
apiServerOrigin="https://api.staging.cloudron.io"
|
||||
webServerOrigin="https://staging.cloudron.io"
|
||||
prerelease="true"
|
||||
fi
|
||||
shift 2;;
|
||||
--skip-baseimage-init) initBaseImage="false"; shift;;
|
||||
--skip-reboot) rebootServer="false"; shift;;
|
||||
--prerelease) prerelease="true"; shift;;
|
||||
--data-dir) baseDataDir=$(realpath "$2"); shift 2;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
@@ -82,14 +83,18 @@ if [[ ${EUID} -ne 0 ]]; then
|
||||
fi
|
||||
|
||||
# Only --help works with mismatched ubuntu
|
||||
if [[ $(lsb_release -rs) != "16.04" ]]; then
|
||||
echo "Cloudron requires Ubuntu 16.04" > /dev/stderr
|
||||
ubuntu_version=$(lsb_release -rs)
|
||||
if [[ "${ubuntu_version}" != "16.04" && "${ubuntu_version}" != "18.04" ]]; then
|
||||
echo "Cloudron requires Ubuntu 16.04 or 18.04" > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Can only write after we have confirmed script has root access
|
||||
echo "Running cloudron-setup with args : $@" > "${LOG_FILE}"
|
||||
|
||||
# validate arguments in the absence of data
|
||||
if [[ -z "${provider}" ]]; then
|
||||
echo "--provider is required (azure, cloudscale, digitalocean, ec2, exoscale, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
|
||||
echo "--provider is required (azure, digitalocean, ec2, exoscale, gce, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
|
||||
exit 1
|
||||
elif [[ \
|
||||
"${provider}" != "ami" && \
|
||||
@@ -99,6 +104,8 @@ elif [[ \
|
||||
"${provider}" != "digitalocean" && \
|
||||
"${provider}" != "ec2" && \
|
||||
"${provider}" != "exoscale" && \
|
||||
"${provider}" != "galaxygate" && \
|
||||
"${provider}" != "digitalocean" && \
|
||||
"${provider}" != "gce" && \
|
||||
"${provider}" != "hetzner" && \
|
||||
"${provider}" != "lightsail" && \
|
||||
@@ -109,7 +116,7 @@ elif [[ \
|
||||
"${provider}" != "vultr" && \
|
||||
"${provider}" != "generic" \
|
||||
]]; then
|
||||
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, gce, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
|
||||
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -132,18 +139,18 @@ echo ""
|
||||
if [[ "${initBaseImage}" == "true" ]]; then
|
||||
echo "=> Updating apt and installing script dependencies"
|
||||
if ! apt-get update &>> "${LOG_FILE}"; then
|
||||
echo "Could not update package repositories"
|
||||
echo "Could not update package repositories. See ${LOG_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! apt-get install curl python3 ubuntu-standard -y &>> "${LOG_FILE}"; then
|
||||
echo "Could not install setup dependencies (curl)"
|
||||
echo "Could not install setup dependencies (curl). See ${LOG_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "=> Checking version"
|
||||
if ! releaseJson=$($curl -s "${apiServerOrigin}/api/v1/releases?prerelease=${prerelease}&boxVersion=${requestedVersion}"); then
|
||||
if ! releaseJson=$($curl -s "${apiServerOrigin}/api/v1/releases?boxVersion=${requestedVersion}"); then
|
||||
echo "Failed to get release information"
|
||||
exit 1
|
||||
fi
|
||||
@@ -159,18 +166,6 @@ if ! sourceTarballUrl=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build data
|
||||
# from 1.9, we use autoprovision.json
|
||||
data=$(cat <<EOF
|
||||
{
|
||||
"provider": "${provider}",
|
||||
"apiServerOrigin": "${apiServerOrigin}",
|
||||
"webServerOrigin": "${webServerOrigin}",
|
||||
"version": "${version}"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "=> Downloading version ${version} ..."
|
||||
box_src_tmp_dir=$(mktemp -dt box-src-XXXXXX)
|
||||
|
||||
@@ -188,13 +183,44 @@ if [[ "${initBaseImage}" == "true" ]]; then
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# NOTE: this install script only supports 3.x and above
|
||||
echo "=> Installing version ${version} (this takes some time) ..."
|
||||
echo "${data}" > "${DATA_FILE}"
|
||||
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" --data-dir "${baseDataDir}" &>> "${LOG_FILE}"; then
|
||||
echo "Failed to install cloudron. See ${LOG_FILE} for details"
|
||||
exit 1
|
||||
if [[ "${version}" =~ 3\.[0-2]+\.[0-9]+ ]]; then
|
||||
readonly DATA_FILE="/root/cloudron-install-data.json"
|
||||
data=$(cat <<EOF
|
||||
{
|
||||
"provider": "${provider}",
|
||||
"edition": "${edition}",
|
||||
"apiServerOrigin": "${apiServerOrigin}",
|
||||
"webServerOrigin": "${webServerOrigin}",
|
||||
"version": "${version}"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
echo "${data}" > "${DATA_FILE}"
|
||||
|
||||
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" &>> "${LOG_FILE}"; then
|
||||
echo "Failed to install cloudron. See ${LOG_FILE} for details"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm "${DATA_FILE}"
|
||||
else
|
||||
mkdir -p /etc/cloudron
|
||||
cat > "/etc/cloudron/cloudron.conf" <<CONF_END
|
||||
{
|
||||
"apiServerOrigin": "${apiServerOrigin}",
|
||||
"webServerOrigin": "${webServerOrigin}",
|
||||
"provider": "${provider}",
|
||||
"edition": "${edition}"
|
||||
}
|
||||
CONF_END
|
||||
|
||||
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" &>> "${LOG_FILE}"; then
|
||||
echo "Failed to install cloudron. See ${LOG_FILE} for details"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
rm "${DATA_FILE}"
|
||||
|
||||
echo -n "=> Waiting for cloudron to be ready (this takes some time) ..."
|
||||
while true; do
|
||||
|
||||
Executable
+89
@@ -0,0 +1,89 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script collects diagnostic information to help debug server related issues
|
||||
# It also enables SSH access for the cloudron support team
|
||||
|
||||
PASTEBIN="https://paste.cloudron.io"
|
||||
OUT="/tmp/cloudron-support.log"
|
||||
LINE="\n========================================================\n"
|
||||
CLOUDRON_SUPPORT_PUBLIC_KEY="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQVilclYAIu+ioDp/sgzzFz6YU0hPcRYY7ze/LiF/lC7uQqK062O54BFXTvQ3ehtFZCx3bNckjlT2e6gB8Qq07OM66De4/S/g+HJW4TReY2ppSPMVNag0TNGxDzVH8pPHOysAm33LqT2b6L/wEXwC6zWFXhOhHjcMqXvi8Ejaj20H1HVVcf/j8qs5Thkp9nAaFTgQTPu8pgwD8wDeYX1hc9d0PYGesTADvo6HF4hLEoEnefLw7PaStEbzk2fD3j7/g5r5HcgQQXBe74xYZ/1gWOX2pFNuRYOBSEIrNfJEjFJsqk3NR1+ZoMGK7j+AZBR4k0xbrmncQLcQzl6MMDzkp support@cloudron.io"
|
||||
|
||||
# We require root
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# check if at least 10mb root partition space is available
|
||||
if [[ "`df --output="avail" / | sed -n 2p`" -lt "10240" ]]; then
|
||||
echo "No more space left on /"
|
||||
echo "This is likely the root case of the issue. Free up some space and also check other partitions below:"
|
||||
echo ""
|
||||
df -h
|
||||
echo ""
|
||||
echo "To recover from a full disk, follow the guide at https://cloudron.io/documentation/server/#recovery-after-disk-full"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# check for at least 5mb free /tmp space for the log file
|
||||
if [[ "`df --output="avail" /tmp | sed -n 2p`" -lt "5120" ]]; then
|
||||
echo "Not enough space left on /tmp"
|
||||
echo "Free up some space first by deleting files from /tmp"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -n "Generating Cloudron Support stats..."
|
||||
|
||||
# clear file
|
||||
rm -rf $OUT
|
||||
|
||||
ssh_port=$(cat /etc/ssh/sshd_config | grep "Port " | sed -e "s/.*Port //")
|
||||
if [[ $SUDO_USER == "" ]]; then
|
||||
ssh_user="root"
|
||||
ssh_folder="/root/.ssh/"
|
||||
authorized_key_file="${ssh_folder}/authorized_keys"
|
||||
else
|
||||
ssh_user="$SUDO_USER"
|
||||
ssh_folder="/home/$SUDO_USER/.ssh/"
|
||||
authorized_key_file="${ssh_folder}/authorized_keys"
|
||||
fi
|
||||
|
||||
echo -e $LINE"SSH"$LINE >> $OUT
|
||||
echo "Username: ${ssh_user}" >> $OUT
|
||||
echo "Port: ${ssh_port}" >> $OUT
|
||||
|
||||
echo -e $LINE"cloudron.conf"$LINE >> $OUT
|
||||
cat /etc/cloudron/cloudron.conf &>> $OUT
|
||||
|
||||
echo -e $LINE"Docker container"$LINE >> $OUT
|
||||
if ! timeout --kill-after 10s 15s docker ps -a &>> $OUT 2>&1; then
|
||||
echo -e "Docker is not responding" >> $OUT
|
||||
fi
|
||||
|
||||
echo -e $LINE"Filesystem stats"$LINE >> $OUT
|
||||
df -h &>> $OUT
|
||||
|
||||
echo -e $LINE"System daemon status"$LINE >> $OUT
|
||||
systemctl status --lines=100 cloudron.target box mysql unbound cloudron-syslog nginx collectd docker &>> $OUT
|
||||
|
||||
echo -e $LINE"Firewall chains"$LINE >> $OUT
|
||||
iptables -L &>> $OUT
|
||||
|
||||
echo "Done"
|
||||
|
||||
echo -n "Uploading information..."
|
||||
# for some reason not using $(cat $OUT) will not contain newlines!?
|
||||
paste_key=$(curl -X POST ${PASTEBIN}/documents --silent -d "$(cat $OUT)" | python3 -c "import sys, json; print(json.load(sys.stdin)['key'])")
|
||||
echo "Done"
|
||||
|
||||
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"
|
||||
|
||||
echo ""
|
||||
echo "Please email the following link to support@cloudron.io"
|
||||
echo ""
|
||||
echo "${PASTEBIN}/${paste_key}"
|
||||
@@ -7,21 +7,28 @@ set -eu
|
||||
[[ $(uname -s) == "Darwin" ]] && GNU_GETOPT="/usr/local/opt/gnu-getopt/bin/getopt" || GNU_GETOPT="getopt"
|
||||
readonly GNU_GETOPT
|
||||
|
||||
args=$(${GNU_GETOPT} -o "" -l "output:" -n "$0" -- "$@")
|
||||
args=$(${GNU_GETOPT} -o "" -l "output:,version:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
bundle_file=""
|
||||
version=""
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--output) bundle_file="$2"; shift 2;;
|
||||
--version) version="$2"; shift 2;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "${version}" ]]; then
|
||||
echo "--version is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
readonly TMPDIR=${TMPDIR:-/tmp} # why is this not set on mint?
|
||||
|
||||
if ! $(cd "${SOURCE_DIR}" && git diff --exit-code >/dev/null); then
|
||||
@@ -41,19 +48,16 @@ fi
|
||||
|
||||
box_version=$(cd "${SOURCE_DIR}" && git rev-parse "HEAD")
|
||||
branch=$(git rev-parse --abbrev-ref HEAD)
|
||||
if [[ "${branch}" == "master" ]]; then
|
||||
dashboard_version=$(cd "${SOURCE_DIR}/../dashboard" && git rev-parse "${branch}")
|
||||
else
|
||||
dashboard_version=$(cd "${SOURCE_DIR}/../dashboard" && git fetch && git rev-parse "origin/${branch}")
|
||||
fi
|
||||
dashboard_version=$(cd "${SOURCE_DIR}/../dashboard" && git fetch && git rev-parse "${branch}")
|
||||
bundle_dir=$(mktemp -d -t box 2>/dev/null || mktemp -d box-XXXXXXXXXX --tmpdir=$TMPDIR)
|
||||
[[ -z "$bundle_file" ]] && bundle_file="${TMPDIR}/box-${box_version:0:10}-${dashboard_version:0:10}.tar.gz"
|
||||
[[ -z "$bundle_file" ]] && bundle_file="${TMPDIR}/box-${box_version:0:10}-${dashboard_version:0:10}-${version}.tar.gz"
|
||||
|
||||
chmod "o+rx,g+rx" "${bundle_dir}" # otherwise extracted tarball director won't be readable by others/group
|
||||
echo "==> Checking out code box version [${box_version}] and dashboard version [${dashboard_version}] into ${bundle_dir}"
|
||||
(cd "${SOURCE_DIR}" && git archive --format=tar ${box_version} | (cd "${bundle_dir}" && tar xf -))
|
||||
(cd "${SOURCE_DIR}/../dashboard" && git archive --format=tar ${dashboard_version} | (mkdir -p "${bundle_dir}/dashboard.build" && cd "${bundle_dir}/dashboard.build" && tar xf -))
|
||||
(cp "${SOURCE_DIR}/../dashboard/LICENSE" "${bundle_dir}")
|
||||
echo "${version}" > "${bundle_dir}/VERSION"
|
||||
|
||||
echo "==> Installing modules for dashboard asset generation"
|
||||
(cd "${bundle_dir}/dashboard.build" && npm install --production)
|
||||
|
||||
+21
-31
@@ -1,5 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script is run before the box code is switched. This means that we can
|
||||
# put network related/curl downloads here. If the script fails, the old code
|
||||
# will continue to run
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
@@ -10,29 +14,12 @@ fi
|
||||
readonly USER=yellowtent
|
||||
readonly BOX_SRC_DIR=/home/${USER}/box
|
||||
readonly BASE_DATA_DIR=/home/${USER}
|
||||
readonly CLOUDRON_CONF=/home/yellowtent/configs/cloudron.conf
|
||||
|
||||
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
|
||||
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly box_src_tmp_dir="$(realpath ${script_dir}/..)"
|
||||
|
||||
readonly is_update=$([[ -f "${CLOUDRON_CONF}" ]] && echo "yes" || echo "no")
|
||||
|
||||
arg_data=""
|
||||
arg_data_dir=""
|
||||
|
||||
args=$(getopt -o "" -l "data:,data-file:,data-dir:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--data) arg_data="$2"; shift 2;;
|
||||
--data-file) arg_data=$(cat $2); shift 2;;
|
||||
--data-dir) arg_data_dir="$2"; shift 2;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
done
|
||||
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
|
||||
@@ -77,6 +64,7 @@ if [[ "$(node --version)" != "v8.11.2" ]]; then
|
||||
rm -rf /usr/local/node-6.11.5
|
||||
fi
|
||||
|
||||
# this is here (and not in updater.js) because rebuild requires the above node
|
||||
for try in `seq 1 10`; do
|
||||
# for reasons unknown, the dtrace package will fail. but rebuilding second time will work
|
||||
|
||||
@@ -93,15 +81,26 @@ if [[ ${try} -eq 10 ]]; then
|
||||
exit 4
|
||||
fi
|
||||
|
||||
echo "==> installer: downloading new addon images"
|
||||
images=$(node -e "var i = require('${box_src_tmp_dir}/src/infra_version.js'); console.log(i.baseImages.map(function (x) { return x.tag; }).join(' '), Object.keys(i.images).map(function (x) { return i.images[x].tag; }).join(' '));")
|
||||
|
||||
echo -e "\tPulling docker images: ${images}"
|
||||
for image in ${images}; do
|
||||
docker pull "${image}" # this pulls the image using the sha256
|
||||
docker pull "${image%@sha256:*}" # this will tag the image for readability
|
||||
done
|
||||
|
||||
echo "==> installer: update cloudron-syslog"
|
||||
CLOUDRON_SYSLOG_DIR=/usr/local/cloudron-syslog
|
||||
if [[ "$($CLOUDRON_SYSLOG_DIR/bin/cloudron-syslog --version)" != "1.0.1" ]]; then
|
||||
CLOUDRON_SYSLOG="${CLOUDRON_SYSLOG_DIR}/bin/cloudron-syslog"
|
||||
CLOUDRON_SYSLOG_VERSION="1.0.3"
|
||||
while [[ ! -f "${CLOUDRON_SYSLOG}" || "$(${CLOUDRON_SYSLOG} --version)" != ${CLOUDRON_SYSLOG_VERSION} ]]; do
|
||||
rm -rf "${CLOUDRON_SYSLOG_DIR}"
|
||||
mkdir -p "${CLOUDRON_SYSLOG_DIR}"
|
||||
if npm install --unsafe-perm -g --prefix "${CLOUDRON_SYSLOG_DIR}" cloudron-syslog@1.0.1; then break; fi
|
||||
if npm install --unsafe-perm -g --prefix "${CLOUDRON_SYSLOG_DIR}" cloudron-syslog@${CLOUDRON_SYSLOG_VERSION}; then break; fi
|
||||
echo "===> installer: Failed to install cloudron-syslog, trying again"
|
||||
sleep 5
|
||||
fi
|
||||
done
|
||||
|
||||
if ! id "${USER}" 2>/dev/null; then
|
||||
useradd "${USER}" -m
|
||||
@@ -112,15 +111,6 @@ if [[ "${is_update}" == "yes" ]]; then
|
||||
${BOX_SRC_DIR}/setup/stop.sh
|
||||
fi
|
||||
|
||||
# setup links to data directory
|
||||
if [[ -n "${arg_data_dir}" ]]; then
|
||||
echo "==> installer: setting up links to data directory"
|
||||
mkdir "${arg_data_dir}/appsdata"
|
||||
ln -s "${arg_data_dir}/appsdata" "${BASE_DATA_DIR}/appsdata"
|
||||
mkdir "${arg_data_dir}/platformdata"
|
||||
ln -s "${arg_data_dir}/platformdata" "${BASE_DATA_DIR}/platformdata"
|
||||
fi
|
||||
|
||||
# ensure we are not inside the source directory, which we will remove now
|
||||
cd /root
|
||||
|
||||
@@ -130,4 +120,4 @@ mv "${box_src_tmp_dir}" "${BOX_SRC_DIR}"
|
||||
chown -R "${USER}:${USER}" "${BOX_SRC_DIR}"
|
||||
|
||||
echo "==> installer: calling box setup script"
|
||||
"${BOX_SRC_DIR}/setup/start.sh" --data "${arg_data}"
|
||||
"${BOX_SRC_DIR}/setup/start.sh"
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
source_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
json="${source_dir}/../node_modules/.bin/json"
|
||||
|
||||
arg_api_server_origin=""
|
||||
arg_fqdn="" # remove after 1.10
|
||||
arg_admin_domain=""
|
||||
arg_admin_location=""
|
||||
arg_admin_fqdn=""
|
||||
arg_retire_reason=""
|
||||
arg_retire_info=""
|
||||
arg_version=""
|
||||
arg_web_server_origin=""
|
||||
arg_provider=""
|
||||
arg_is_demo="false"
|
||||
|
||||
args=$(getopt -o "" -l "data:,retire-reason:,retire-info:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--retire-reason)
|
||||
arg_retire_reason="$2"
|
||||
shift 2
|
||||
;;
|
||||
--retire-info)
|
||||
arg_retire_info="$2"
|
||||
shift 2
|
||||
;;
|
||||
--data)
|
||||
# these params must be valid in all cases
|
||||
arg_fqdn=$(echo "$2" | $json fqdn)
|
||||
arg_admin_fqdn=$(echo "$2" | $json adminFqdn)
|
||||
|
||||
arg_admin_location=$(echo "$2" | $json adminLocation)
|
||||
[[ "${arg_admin_location}" == "" ]] && arg_admin_location="my"
|
||||
|
||||
arg_admin_domain=$(echo "$2" | $json adminDomain)
|
||||
[[ "${arg_admin_domain}" == "" ]] && arg_admin_domain="${arg_fqdn}"
|
||||
|
||||
# only update/restore have this valid (but not migrate)
|
||||
arg_api_server_origin=$(echo "$2" | $json apiServerOrigin)
|
||||
[[ "${arg_api_server_origin}" == "" ]] && arg_api_server_origin="https://api.cloudron.io"
|
||||
arg_web_server_origin=$(echo "$2" | $json webServerOrigin)
|
||||
[[ "${arg_web_server_origin}" == "" ]] && arg_web_server_origin="https://cloudron.io"
|
||||
|
||||
# TODO check if and where this is used
|
||||
arg_version=$(echo "$2" | $json version)
|
||||
|
||||
# read possibly empty parameters here
|
||||
arg_is_demo=$(echo "$2" | $json isDemo)
|
||||
[[ "${arg_is_demo}" == "" ]] && arg_is_demo="false"
|
||||
|
||||
arg_provider=$(echo "$2" | $json provider)
|
||||
[[ "${arg_provider}" == "" ]] && arg_provider="generic"
|
||||
|
||||
shift 2
|
||||
;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "Parsed arguments:"
|
||||
echo "api server: ${arg_api_server_origin}"
|
||||
echo "admin fqdn: ${arg_admin_fqdn}"
|
||||
echo "fqdn: ${arg_fqdn}"
|
||||
echo "version: ${arg_version}"
|
||||
echo "web server: ${arg_web_server_origin}"
|
||||
echo "provider: ${arg_provider}"
|
||||
+18
-51
@@ -2,6 +2,9 @@
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
# This script is run after the box code is switched. This means that this script
|
||||
# should pretty much always succeed. No network logic/download code here.
|
||||
|
||||
echo "==> Cloudron Start"
|
||||
|
||||
readonly USER="yellowtent"
|
||||
@@ -12,17 +15,8 @@ readonly APPS_DATA_DIR="${HOME_DIR}/appsdata" # app data
|
||||
readonly BOX_DATA_DIR="${HOME_DIR}/boxdata" # box data
|
||||
readonly CONFIG_DIR="${HOME_DIR}/configs"
|
||||
|
||||
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
|
||||
|
||||
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
|
||||
|
||||
echo "==> Configuring host"
|
||||
sed -e 's/^#NTP=/NTP=0.ubuntu.pool.ntp.org 1.ubuntu.pool.ntp.org 2.ubuntu.pool.ntp.org 3.ubuntu.pool.ntp.org/' -i /etc/systemd/timesyncd.conf
|
||||
timedatectl set-ntp 1
|
||||
timedatectl set-timezone UTC
|
||||
hostnamectl set-hostname "${arg_admin_fqdn}"
|
||||
readonly get_config="$(realpath ${script_dir}/../node_modules/.bin/json) -f /etc/cloudron/cloudron.conf"
|
||||
|
||||
echo "==> Configuring docker"
|
||||
cp "${script_dir}/start/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
|
||||
@@ -48,19 +42,6 @@ if [[ ! -f /etc/systemd/system/docker.service.d/cloudron.conf ]] || ! diff -q /e
|
||||
fi
|
||||
docker network create --subnet=172.18.0.0/16 cloudron || true
|
||||
|
||||
# caas has ssh on port 202 and we disable password login
|
||||
if [[ "${arg_provider}" == "caas" ]]; then
|
||||
# https://stackoverflow.com/questions/4348166/using-with-sed on why ? must be escaped
|
||||
sed -e 's/^#\?PermitRootLogin .*/PermitRootLogin without-password/g' \
|
||||
-e 's/^#\?PermitEmptyPasswords .*/PermitEmptyPasswords no/g' \
|
||||
-e 's/^#\?PasswordAuthentication .*/PasswordAuthentication no/g' \
|
||||
-e 's/^#\?Port .*/Port 202/g' \
|
||||
-i /etc/ssh/sshd_config
|
||||
|
||||
# required so we can connect to this machine since port 22 is blocked by iptables by now
|
||||
systemctl reload sshd
|
||||
fi
|
||||
|
||||
mkdir -p "${BOX_DATA_DIR}"
|
||||
mkdir -p "${APPS_DATA_DIR}"
|
||||
|
||||
@@ -71,11 +52,14 @@ mkdir -p "${PLATFORM_DATA_DIR}/graphite"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/mysql"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/postgresql"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/mongodb"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/redis"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/addons/mail"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/collectd/collectd.conf.d"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/logrotate.d"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/acme"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/backup"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/logs/backup"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/update"
|
||||
|
||||
mkdir -p "${BOX_DATA_DIR}/appicons"
|
||||
mkdir -p "${BOX_DATA_DIR}/certs"
|
||||
@@ -107,6 +91,14 @@ setfacl -n -m u:${USER}:r /var/log/journal/*/system.journal
|
||||
echo "==> Creating config directory"
|
||||
mkdir -p "${CONFIG_DIR}"
|
||||
|
||||
# migration for cloudron.conf file. Can be removed after 3.3
|
||||
if [[ ! -d /etc/cloudron ]]; then
|
||||
echo "==> Migrating existing cloudron.conf to new location"
|
||||
mkdir -p /etc/cloudron
|
||||
cp "${CONFIG_DIR}/cloudron.conf" /etc/cloudron/cloudron.conf
|
||||
fi
|
||||
chown -R "${USER}" /etc/cloudron
|
||||
|
||||
echo "==> Setting up unbound"
|
||||
# DO uses Google nameservers by default. This causes RBL queries to fail (host 2.0.0.127.zen.spamhaus.org)
|
||||
# We do not use dnsmasq because it is not a recursive resolver and defaults to the value in the interfaces file (which is Google DNS!)
|
||||
@@ -169,14 +161,8 @@ if ! grep -q "^Restart=" /etc/systemd/system/multi-user.target.wants/nginx.servi
|
||||
echo -e "\n[Service]\nRestart=always\n" >> /etc/systemd/system/multi-user.target.wants/nginx.service
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
# remove this migration after 1.10
|
||||
[[ -f /etc/nginx/cert/host.cert ]] && cp /etc/nginx/cert/host.cert "/etc/nginx/cert/${arg_admin_domain}.host.cert"
|
||||
[[ -f /etc/nginx/cert/host.key ]] && cp /etc/nginx/cert/host.key "/etc/nginx/cert/${arg_admin_domain}.host.key"
|
||||
systemctl start nginx
|
||||
|
||||
# bookkeep the version as part of data
|
||||
echo "{ \"version\": \"${arg_version}\", \"apiServerOrigin\": \"${arg_api_server_origin}\" }" > "${BOX_DATA_DIR}/version"
|
||||
|
||||
# restart mysql to make sure it has latest config
|
||||
if [[ ! -f /etc/mysql/mysql.cnf ]] || ! diff -q "${script_dir}/start/mysql.cnf" /etc/mysql/mysql.cnf >/dev/null; then
|
||||
# wait for all running mysql jobs
|
||||
@@ -206,27 +192,6 @@ cd "${BOX_SRC_DIR}"
|
||||
BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up
|
||||
EOF
|
||||
|
||||
echo "==> Creating cloudron.conf"
|
||||
cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
|
||||
{
|
||||
"version": "${arg_version}",
|
||||
"apiServerOrigin": "${arg_api_server_origin}",
|
||||
"webServerOrigin": "${arg_web_server_origin}",
|
||||
"adminDomain": "${arg_admin_domain}",
|
||||
"adminFqdn": "${arg_admin_fqdn}",
|
||||
"adminLocation": "${arg_admin_location}",
|
||||
"provider": "${arg_provider}",
|
||||
"isDemo": ${arg_is_demo}
|
||||
}
|
||||
CONF_END
|
||||
|
||||
echo "==> Creating config.json for dashboard"
|
||||
cat > "${BOX_SRC_DIR}/dashboard/dist/config.json" <<CONF_END
|
||||
{
|
||||
"webServerOrigin": "${arg_web_server_origin}"
|
||||
}
|
||||
CONF_END
|
||||
|
||||
if [[ ! -f "${BOX_DATA_DIR}/dhparams.pem" ]]; then
|
||||
echo "==> Generating dhparams (takes forever)"
|
||||
openssl dhparam -out "${BOX_DATA_DIR}/dhparams.pem" 2048
|
||||
@@ -237,9 +202,11 @@ fi
|
||||
|
||||
echo "==> Changing ownership"
|
||||
chown "${USER}:${USER}" -R "${CONFIG_DIR}"
|
||||
chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/nginx" "${PLATFORM_DATA_DIR}/collectd" "${PLATFORM_DATA_DIR}/addons" "${PLATFORM_DATA_DIR}/acme" "${PLATFORM_DATA_DIR}/backup"
|
||||
# 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 "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/nginx" "${PLATFORM_DATA_DIR}/collectd" "${PLATFORM_DATA_DIR}/addons" "${PLATFORM_DATA_DIR}/acme" "${PLATFORM_DATA_DIR}/backup" "${PLATFORM_DATA_DIR}/logs" "${PLATFORM_DATA_DIR}/update"
|
||||
chown "${USER}:${USER}" "${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"
|
||||
|
||||
@@ -90,8 +90,8 @@ server {
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade";
|
||||
proxy_hide_header Referrer-Policy;
|
||||
|
||||
# CSP headers for the admin/dashboard resources
|
||||
<% 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';";
|
||||
<% } -%>
|
||||
|
||||
@@ -160,6 +160,7 @@ server {
|
||||
}
|
||||
|
||||
# graphite paths (uncomment block below and visit /graphite/index.html)
|
||||
# remember to comment out the CSP policy as well to access the graphite dashboard
|
||||
# location ~ ^/(graphite|content|metrics|dashboard|render|browser|composer)/ {
|
||||
# proxy_pass http://127.0.0.1:8000;
|
||||
# client_max_body_size 1m;
|
||||
@@ -171,6 +172,10 @@ server {
|
||||
}
|
||||
<% } else if ( endpoint === 'app' ) { %>
|
||||
proxy_pass http://127.0.0.1:<%= port %>;
|
||||
<% } else if ( endpoint === 'redirect' ) { %>
|
||||
# redirect everything to the app. this is temporary because there is no way
|
||||
# to clear a permanent redirect on the browser
|
||||
return 302 https://<%= redirectTo %>$request_uri;
|
||||
<% } %>
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,11 +1,11 @@
|
||||
# sudo logging breaks journalctl output with very long urls (systemd bug)
|
||||
Defaults !syslog
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/createappdir.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/createappdir.sh
|
||||
Defaults!/home/yellowtent/box/src/scripts/rmvolume.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmvolume.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/rmappdir.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmappdir.sh
|
||||
Defaults!/home/yellowtent/box/src/scripts/rmaddon.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmaddon.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/reloadnginx.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reloadnginx.sh
|
||||
|
||||
+97
-144
@@ -1,190 +1,143 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
SCOPE_APPS: 'apps',
|
||||
SCOPE_APPS_READ: 'apps:read',
|
||||
SCOPE_APPS_MANAGE: 'apps:manage',
|
||||
SCOPE_CLIENTS: 'clients',
|
||||
SCOPE_CLOUDRON: 'cloudron',
|
||||
SCOPE_DOMAINS: 'domains',
|
||||
SCOPE_DOMAINS_READ: 'domains:read',
|
||||
SCOPE_DOMAINS_MANAGE: 'domains:manage',
|
||||
SCOPE_MAIL: 'mail',
|
||||
SCOPE_PROFILE: 'profile',
|
||||
SCOPE_SETTINGS: 'settings',
|
||||
SCOPE_USERS: 'users',
|
||||
VALID_SCOPES: [ 'apps', 'clients', 'cloudron', 'domains', 'mail', 'profile', 'settings', 'users' ],
|
||||
SCOPE_USERS_READ: 'users:read',
|
||||
SCOPE_USERS_MANAGE: 'users:manage',
|
||||
SCOPE_APPSTORE: 'appstore',
|
||||
VALID_SCOPES: [ 'apps', 'appstore', 'clients', 'cloudron', 'domains', 'mail', 'profile', 'settings', 'users' ], // keep this sorted
|
||||
|
||||
SCOPE_ANY: '*',
|
||||
|
||||
initialize: initialize,
|
||||
uninitialize: uninitialize,
|
||||
|
||||
accessTokenAuth: accessTokenAuth,
|
||||
|
||||
validateScope: validateScope,
|
||||
validateRequestedScopes: validateRequestedScopes,
|
||||
normalizeScope: normalizeScope,
|
||||
canonicalScope: canonicalScope
|
||||
validateScopeString: validateScopeString,
|
||||
hasScopes: hasScopes,
|
||||
canonicalScopeString: canonicalScopeString,
|
||||
intersectScopes: intersectScopes,
|
||||
validateToken: validateToken,
|
||||
scopesForUser: scopesForUser
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
BasicStrategy = require('passport-http').BasicStrategy,
|
||||
BearerStrategy = require('passport-http-bearer').Strategy,
|
||||
clients = require('./clients'),
|
||||
ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy,
|
||||
ClientsError = clients.ClientsError,
|
||||
DatabaseError = require('./databaseerror'),
|
||||
config = require('./config.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:accesscontrol'),
|
||||
LocalStrategy = require('passport-local').Strategy,
|
||||
passport = require('passport'),
|
||||
tokendb = require('./tokendb'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
users = require('./users.js'),
|
||||
UsersError = users.UsersError,
|
||||
_ = require('underscore');
|
||||
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
// returns scopes that does not have wildcards and is sorted
|
||||
function canonicalScopeString(scope) {
|
||||
if (scope === exports.SCOPE_ANY) return exports.VALID_SCOPES.join(',');
|
||||
|
||||
passport.serializeUser(function (user, callback) {
|
||||
callback(null, user.id);
|
||||
});
|
||||
return scope.split(',').sort().join(',');
|
||||
}
|
||||
|
||||
passport.deserializeUser(function(userId, callback) {
|
||||
users.get(userId, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
function intersectScopes(allowedScopes, wantedScopes) {
|
||||
assert(Array.isArray(allowedScopes), 'Expecting sorted array');
|
||||
assert(Array.isArray(wantedScopes), 'Expecting sorted array');
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
});
|
||||
if (_.isEqual(allowedScopes, wantedScopes)) return allowedScopes; // quick path
|
||||
|
||||
passport.use(new LocalStrategy(function (username, password, callback) {
|
||||
if (username.indexOf('@') === -1) {
|
||||
users.verifyWithUsername(username, password, function (error, result) {
|
||||
if (error && error.reason === UsersError.NOT_FOUND) return callback(null, false);
|
||||
if (error && error.reason === UsersError.WRONG_PASSWORD) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
if (!result) return callback(null, false);
|
||||
callback(null, result);
|
||||
});
|
||||
} else {
|
||||
users.verifyWithEmail(username, password, function (error, result) {
|
||||
if (error && error.reason === UsersError.NOT_FOUND) return callback(null, false);
|
||||
if (error && error.reason === UsersError.WRONG_PASSWORD) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
if (!result) return callback(null, false);
|
||||
callback(null, result);
|
||||
});
|
||||
let wantedScopesMap = new Map();
|
||||
let results = [];
|
||||
|
||||
// make a map of scope -> [ subscopes ]
|
||||
for (let w of wantedScopes) {
|
||||
let parts = w.split(':');
|
||||
let subscopes = wantedScopesMap.get(parts[0]) || new Set();
|
||||
subscopes.add(parts[1] || '*');
|
||||
wantedScopesMap.set(parts[0], subscopes);
|
||||
}
|
||||
|
||||
for (let a of allowedScopes) {
|
||||
let parts = a.split(':');
|
||||
let as = parts[1] || '*';
|
||||
|
||||
let subscopes = wantedScopesMap.get(parts[0]);
|
||||
if (!subscopes) continue;
|
||||
|
||||
if (subscopes.has('*') || subscopes.has(as)) {
|
||||
results.push(a);
|
||||
} else if (as === '*') {
|
||||
results = results.concat(Array.from(subscopes).map(function (ss) { return `${a}:${ss}`; }));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
passport.use(new BasicStrategy(function (username, password, callback) {
|
||||
if (username.indexOf('cid-') === 0) {
|
||||
debug('BasicStrategy: detected client id %s instead of username:password', username);
|
||||
// username is actually client id here
|
||||
// password is client secret
|
||||
clients.get(username, function (error, client) {
|
||||
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
if (client.clientSecret != password) return callback(null, false);
|
||||
return callback(null, client);
|
||||
});
|
||||
} else {
|
||||
users.verifyWithUsername(username, password, function (error, result) {
|
||||
if (error && error.reason === UsersError.NOT_FOUND) return callback(null, false);
|
||||
if (error && error.reason === UsersError.WRONG_PASSWORD) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
if (!result) return callback(null, false);
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
passport.use(new ClientPasswordStrategy(function (clientId, clientSecret, callback) {
|
||||
clients.get(clientId, function(error, client) {
|
||||
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
|
||||
if (error) { return callback(error); }
|
||||
if (client.clientSecret != clientSecret) { return callback(null, false); }
|
||||
return callback(null, client);
|
||||
});
|
||||
}));
|
||||
|
||||
passport.use(new BearerStrategy(accessTokenAuth));
|
||||
|
||||
callback(null);
|
||||
return results;
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function canonicalScope(scope) {
|
||||
return scope.replace(exports.SCOPE_ANY, exports.VALID_SCOPES.join(','));
|
||||
}
|
||||
|
||||
function normalizeScope(allowedScope, wantedScope) {
|
||||
assert.strictEqual(typeof allowedScope, 'string');
|
||||
assert.strictEqual(typeof wantedScope, 'string');
|
||||
|
||||
const allowedScopes = allowedScope.split(',');
|
||||
const wantedScopes = wantedScope.split(',');
|
||||
|
||||
if (allowedScopes.indexOf(exports.SCOPE_ANY) !== -1) return canonicalScope(wantedScope);
|
||||
if (wantedScopes.indexOf(exports.SCOPE_ANY) !== -1) return canonicalScope(allowedScope);
|
||||
|
||||
return _.intersection(allowedScopes, wantedScopes).join(',');
|
||||
}
|
||||
|
||||
function accessTokenAuth(accessToken, callback) {
|
||||
assert.strictEqual(typeof accessToken, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
tokendb.get(accessToken, function (error, token) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
|
||||
users.get(token.identifier, function (error, user) {
|
||||
if (error && error.reason === UsersError.NOT_FOUND) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
|
||||
// scopes here can define what capabilities that token carries
|
||||
// passport put the 'info' object into req.authInfo, where we can further validate the scopes
|
||||
var scope = normalizeScope(user.scope, token.scope);
|
||||
var info = { scope: scope, clientId: token.clientId };
|
||||
|
||||
callback(null, user, info);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function validateScope(scope) {
|
||||
function validateScopeString(scope) {
|
||||
assert.strictEqual(typeof scope, 'string');
|
||||
|
||||
if (scope === '') return new Error('Empty scope not allowed');
|
||||
|
||||
// NOTE: this function intentionally does not allow '*'. This is only allowed in the db to allow
|
||||
// us not write a migration script every time we add a new scope
|
||||
var allValid = scope.split(',').every(function (s) { return exports.VALID_SCOPES.indexOf(s) !== -1; });
|
||||
var allValid = scope.split(',').every(function (s) { return exports.VALID_SCOPES.indexOf(s.split(':')[0]) !== -1; });
|
||||
if (!allValid) return new Error('Invalid scope. Available scopes are ' + exports.VALID_SCOPES.join(', '));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// tests if all requestedScopes are attached to the request
|
||||
function validateRequestedScopes(authInfo, requestedScopes) {
|
||||
assert.strictEqual(typeof authInfo, 'object');
|
||||
assert(Array.isArray(requestedScopes));
|
||||
// tests if all requiredScopes are attached to the request
|
||||
function hasScopes(authorizedScopes, requiredScopes) {
|
||||
assert(Array.isArray(authorizedScopes), 'Expecting array');
|
||||
assert(Array.isArray(requiredScopes), 'Expecting array');
|
||||
|
||||
if (!authInfo || !authInfo.scope) return new Error('No scope found');
|
||||
if (authorizedScopes.indexOf(exports.SCOPE_ANY) !== -1) return null;
|
||||
|
||||
var scopes = authInfo.scope.split(',');
|
||||
for (var i = 0; i < requiredScopes.length; ++i) {
|
||||
const scopeParts = requiredScopes[i].split(':');
|
||||
|
||||
if (scopes.indexOf(exports.SCOPE_ANY) !== -1) return null;
|
||||
|
||||
for (var i = 0; i < requestedScopes.length; ++i) {
|
||||
if (scopes.indexOf(requestedScopes[i]) === -1) {
|
||||
debug('scope: missing scope "%s".', requestedScopes[i]);
|
||||
return new Error('Missing required scope "' + requestedScopes[i] + '"');
|
||||
// this allows apps:write if the token has a higher apps scope
|
||||
if (authorizedScopes.indexOf(requiredScopes[i]) === -1 && authorizedScopes.indexOf(scopeParts[0]) === -1) {
|
||||
debug('scope: missing scope "%s".', requiredScopes[i]);
|
||||
return new Error('Missing required scope "' + requiredScopes[i] + '"');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function scopesForUser(user, callback) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (user.admin) return callback(null, exports.VALID_SCOPES);
|
||||
|
||||
callback(null, config.isSpacesEnabled() ? [ 'profile', 'apps', 'domains:read', 'users:read' ] : [ 'profile', 'apps:read' ]);
|
||||
}
|
||||
|
||||
function validateToken(accessToken, callback) {
|
||||
assert.strictEqual(typeof accessToken, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
tokendb.get(accessToken, function (error, token) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
|
||||
if (error) return callback(error); // this triggers 'internal error' in passport
|
||||
|
||||
users.get(token.identifier, function (error, user) {
|
||||
if (error && error.reason === UsersError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
|
||||
if (error) return callback(error);
|
||||
|
||||
scopesForUser(user, function (error, userScopes) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var authorizedScopes = intersectScopes(userScopes, token.scope.split(','));
|
||||
const skipPasswordVerification = token.clientId === 'cid-sdk' || token.clientId === 'cid-cli'; // these clients do not require password checks unlike UI
|
||||
var info = { authorizedScopes: authorizedScopes, skipPasswordVerification: skipPasswordVerification }; // ends up in req.authInfo
|
||||
|
||||
callback(null, user, info);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+779
-199
File diff suppressed because it is too large
Load Diff
+194
-37
@@ -24,6 +24,9 @@ exports = module.exports = {
|
||||
setRunCommand: setRunCommand,
|
||||
getAppStoreIds: getAppStoreIds,
|
||||
|
||||
setOwner: setOwner,
|
||||
transferOwnership: transferOwnership,
|
||||
|
||||
// installation codes (keep in sync in UI)
|
||||
ISTATE_PENDING_INSTALL: 'pending_install', // installs and fresh reinstalls
|
||||
ISTATE_PENDING_CLONE: 'pending_clone', // clone
|
||||
@@ -47,6 +50,10 @@ exports = module.exports = {
|
||||
HEALTH_ERROR: 'error',
|
||||
HEALTH_DEAD: 'dead',
|
||||
|
||||
// subdomain table types
|
||||
SUBDOMAIN_TYPE_PRIMARY: 'primary',
|
||||
SUBDOMAIN_TYPE_REDIRECT: 'redirect',
|
||||
|
||||
_clear: clear
|
||||
};
|
||||
|
||||
@@ -59,12 +66,14 @@ var assert = require('assert'),
|
||||
util = require('util');
|
||||
|
||||
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.installationProgress', 'apps.runState',
|
||||
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'apps.location', 'apps.domain', 'apps.dnsRecordId',
|
||||
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'subdomains.subdomain AS location', 'subdomains.domain',
|
||||
'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit',
|
||||
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
|
||||
'apps.creationTime', 'apps.updateTime' ].join(',');
|
||||
'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.ts' ].join(',');
|
||||
|
||||
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
|
||||
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(',');
|
||||
|
||||
const SUBDOMAIN_FIELDS = [ 'appId', 'domain', 'subdomain', 'type' ].join(',');
|
||||
|
||||
function postProcess(result) {
|
||||
assert.strictEqual(typeof result, 'object');
|
||||
@@ -89,14 +98,16 @@ function postProcess(result) {
|
||||
assert(result.environmentVariables === null || typeof result.environmentVariables === 'string');
|
||||
|
||||
result.portBindings = { };
|
||||
var hostPorts = result.hostPorts === null ? [ ] : result.hostPorts.split(',');
|
||||
var environmentVariables = result.environmentVariables === null ? [ ] : result.environmentVariables.split(',');
|
||||
let hostPorts = result.hostPorts === null ? [ ] : result.hostPorts.split(',');
|
||||
let environmentVariables = result.environmentVariables === null ? [ ] : result.environmentVariables.split(',');
|
||||
let portTypes = result.portTypes === null ? [ ] : result.portTypes.split(',');
|
||||
|
||||
delete result.hostPorts;
|
||||
delete result.environmentVariables;
|
||||
delete result.portTypes;
|
||||
|
||||
for (var i = 0; i < environmentVariables.length; i++) {
|
||||
result.portBindings[environmentVariables[i]] = parseInt(hostPorts[i], 10);
|
||||
for (let i = 0; i < environmentVariables.length; i++) {
|
||||
result.portBindings[environmentVariables[i]] = { hostPort: parseInt(hostPorts[i], 10), type: portTypes[i] };
|
||||
}
|
||||
|
||||
assert(result.accessRestrictionJson === null || typeof result.accessRestrictionJson === 'string');
|
||||
@@ -113,6 +124,20 @@ function postProcess(result) {
|
||||
assert(result.debugModeJson === null || typeof result.debugModeJson === 'string');
|
||||
result.debugMode = safe.JSON.parse(result.debugModeJson);
|
||||
delete result.debugModeJson;
|
||||
|
||||
result.alternateDomains = result.alternateDomains || [];
|
||||
result.alternateDomains.forEach(function (d) {
|
||||
delete d.appId;
|
||||
delete d.type;
|
||||
});
|
||||
|
||||
let envNames = JSON.parse(result.envNames), envValues = JSON.parse(result.envValues);
|
||||
delete result.envNames;
|
||||
delete result.envValues;
|
||||
result.env = {};
|
||||
for (let i = 0; i < envNames.length; i++) { // NOTE: envNames is [ null ] when env of an app is empty
|
||||
if (envNames[i]) result.env[envNames[i]] = envValues[i];
|
||||
}
|
||||
}
|
||||
|
||||
function get(id, callback) {
|
||||
@@ -120,14 +145,25 @@ function get(id, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
|
||||
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId WHERE apps.id = ? GROUP BY apps.id', [ id ], function (error, result) {
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes, '
|
||||
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
|
||||
+ ' FROM apps'
|
||||
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
|
||||
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' WHERE apps.id = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, id ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
postProcess(result[0]);
|
||||
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, result[0]);
|
||||
result[0].alternateDomains = alternateDomains;
|
||||
|
||||
postProcess(result[0]);
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -136,14 +172,24 @@ function getByHttpPort(httpPort, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
|
||||
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId WHERE httpPort = ? GROUP BY apps.id', [ httpPort ], function (error, result) {
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
|
||||
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
|
||||
+ ' FROM apps'
|
||||
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
|
||||
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' WHERE httpPort = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, httpPort ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
postProcess(result[0]);
|
||||
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, result[0]);
|
||||
result[0].alternateDomains = alternateDomains;
|
||||
postProcess(result[0]);
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -152,14 +198,24 @@ function getByContainerId(containerId, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
|
||||
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId WHERE containerId = ? GROUP BY apps.id', [ containerId ], function (error, result) {
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
|
||||
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
|
||||
+ ' FROM apps'
|
||||
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
|
||||
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' WHERE containerId = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, containerId ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
postProcess(result[0]);
|
||||
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, result[0]);
|
||||
result[0].alternateDomains = alternateDomains;
|
||||
postProcess(result[0]);
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -167,24 +223,41 @@ function getAll(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
|
||||
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
|
||||
+ ' GROUP BY apps.id ORDER BY apps.id', function (error, results) {
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
|
||||
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
|
||||
+ ' FROM apps'
|
||||
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
|
||||
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' GROUP BY apps.id ORDER BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
results.forEach(postProcess);
|
||||
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE type = ?', [ exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
alternateDomains.forEach(function (d) {
|
||||
var domain = results.find(function (a) { return d.appId === a.id; });
|
||||
if (!domain) return;
|
||||
|
||||
domain.alternateDomains = domain.alternateDomains || [];
|
||||
domain.alternateDomains.push(d);
|
||||
});
|
||||
|
||||
results.forEach(postProcess);
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function add(id, appStoreId, manifest, location, domain, portBindings, data, callback) {
|
||||
function add(id, appStoreId, manifest, location, domain, ownerId, portBindings, data, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof appStoreId, 'string');
|
||||
assert(manifest && typeof manifest === 'object');
|
||||
assert.strictEqual(typeof manifest.version, 'string');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof ownerId, 'string');
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
assert(data && typeof data === 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -202,18 +275,32 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal
|
||||
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 || {};
|
||||
|
||||
var queries = [];
|
||||
|
||||
queries.push({
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, domain, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt) ' +
|
||||
' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, installationState, location, domain, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt ]
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId) ' +
|
||||
' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId ]
|
||||
});
|
||||
|
||||
queries.push({
|
||||
query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)',
|
||||
args: [ id, domain, location, exports.SUBDOMAIN_TYPE_PRIMARY ]
|
||||
});
|
||||
|
||||
Object.keys(portBindings).forEach(function (env) {
|
||||
queries.push({
|
||||
query: 'INSERT INTO appPortBindings (environmentVariable, hostPort, appId) VALUES (?, ?, ?)',
|
||||
args: [ env, portBindings[env], id ]
|
||||
query: 'INSERT INTO appPortBindings (environmentVariable, hostPort, type, appId) VALUES (?, ?, ?, ?)',
|
||||
args: [ env, portBindings[env].hostPort, portBindings[env].type, id ]
|
||||
});
|
||||
});
|
||||
|
||||
Object.keys(env).forEach(function (name) {
|
||||
queries.push({
|
||||
query: 'INSERT INTO appEnvVars (appId, name, value) VALUES (?, ?, ?)',
|
||||
args: [ id, name, env[name] ]
|
||||
});
|
||||
});
|
||||
|
||||
@@ -225,6 +312,15 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal
|
||||
});
|
||||
}
|
||||
|
||||
if (data.alternateDomains) {
|
||||
data.alternateDomains.forEach(function (d) {
|
||||
queries.push({
|
||||
query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)',
|
||||
args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_REDIRECT ]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
database.transaction(queries, function (error) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error.message));
|
||||
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'no such domain'));
|
||||
@@ -254,18 +350,19 @@ function getPortBindings(id, callback) {
|
||||
|
||||
var portBindings = { };
|
||||
for (var i = 0; i < results.length; i++) {
|
||||
portBindings[results[i].environmentVariable] = results[i].hostPort;
|
||||
portBindings[results[i].environmentVariable] = { hostPort: results[i].hostPort, type: results[i].type };
|
||||
}
|
||||
|
||||
callback(null, portBindings);
|
||||
});
|
||||
}
|
||||
|
||||
function delPortBinding(hostPort, callback) {
|
||||
function delPortBinding(hostPort, type, callback) {
|
||||
assert.strictEqual(typeof hostPort, 'number');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM appPortBindings WHERE hostPort=?', [ hostPort ], function (error, result) {
|
||||
database.query('DELETE FROM appPortBindings WHERE hostPort=? AND type=?', [ hostPort, type ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
@@ -278,14 +375,16 @@ function del(id, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var queries = [
|
||||
{ query: 'DELETE FROM subdomains WHERE appId = ?', args: [ id ] },
|
||||
{ query: 'DELETE FROM mailboxes WHERE ownerId=?', args: [ id ] },
|
||||
{ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] },
|
||||
{ query: 'DELETE FROM appEnvVars WHERE appId = ?', args: [ id ] },
|
||||
{ query: 'DELETE FROM apps WHERE id = ?', args: [ id ] }
|
||||
];
|
||||
|
||||
database.transaction(queries, function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (results[2].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
if (results[4].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -295,8 +394,10 @@ function clear(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
async.series([
|
||||
database.query.bind(null, 'DELETE FROM subdomains'),
|
||||
database.query.bind(null, 'DELETE FROM appPortBindings'),
|
||||
database.query.bind(null, 'DELETE FROM appAddonConfigs'),
|
||||
database.query.bind(null, 'DELETE FROM appEnvVars'),
|
||||
database.query.bind(null, 'DELETE FROM apps')
|
||||
], function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
@@ -315,6 +416,8 @@ function updateWithConstraints(id, app, constraints, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
assert(!('portBindings' in app) || typeof app.portBindings === 'object');
|
||||
assert(!('accessRestriction' in app) || typeof app.accessRestriction === 'object' || app.accessRestriction === '');
|
||||
assert(!('alternateDomains' in app) || Array.isArray(app.alternateDomains));
|
||||
assert(!('env' in app) || typeof app.env === 'object');
|
||||
|
||||
var queries = [ ];
|
||||
|
||||
@@ -323,8 +426,34 @@ function updateWithConstraints(id, app, constraints, callback) {
|
||||
// replace entries by app id
|
||||
queries.push({ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] });
|
||||
Object.keys(portBindings).forEach(function (env) {
|
||||
var values = [ portBindings[env], env, id ];
|
||||
queries.push({ query: 'INSERT INTO appPortBindings (hostPort, environmentVariable, appId) VALUES(?, ?, ?)', args: values });
|
||||
var values = [ portBindings[env].hostPort, portBindings[env].type, env, id ];
|
||||
queries.push({ query: 'INSERT INTO appPortBindings (hostPort, type, environmentVariable, appId) VALUES(?, ?, ?, ?)', args: values });
|
||||
});
|
||||
}
|
||||
|
||||
if ('env' in app) {
|
||||
queries.push({ query: 'DELETE FROM appEnvVars WHERE appId = ?', args: [ id ] });
|
||||
|
||||
Object.keys(app.env).forEach(function (name) {
|
||||
queries.push({
|
||||
query: 'INSERT INTO appEnvVars (appId, name, value) VALUES (?, ?, ?)',
|
||||
args: [ id, name, app.env[name] ]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if ('location' in app) {
|
||||
queries.push({ query: 'UPDATE subdomains SET subdomain = ? WHERE appId = ? AND type = ?', args: [ app.location, id, exports.SUBDOMAIN_TYPE_PRIMARY ]});
|
||||
}
|
||||
|
||||
if ('domain' in app) {
|
||||
queries.push({ query: 'UPDATE subdomains SET domain = ? WHERE appId = ? AND type = ?', args: [ app.domain, id, exports.SUBDOMAIN_TYPE_PRIMARY ]});
|
||||
}
|
||||
|
||||
if ('alternateDomains' in app) {
|
||||
queries.push({ query: 'DELETE FROM subdomains WHERE appId = ? AND type = ?', args: [ id, exports.SUBDOMAIN_TYPE_REDIRECT ]});
|
||||
app.alternateDomains.forEach(function (d) {
|
||||
queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_REDIRECT ]});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -333,7 +462,7 @@ function updateWithConstraints(id, app, constraints, callback) {
|
||||
if (p === 'manifest' || p === 'oldConfig' || p === 'updateConfig' || p === 'restoreConfig' || p === 'accessRestriction' || p === 'debugMode') {
|
||||
fields.push(`${p}Json = ?`);
|
||||
values.push(JSON.stringify(app[p]));
|
||||
} else if (p !== 'portBindings') {
|
||||
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'env') {
|
||||
fields.push(p + ' = ?');
|
||||
values.push(app[p]);
|
||||
}
|
||||
@@ -505,3 +634,31 @@ function getAddonConfigByName(appId, addonId, name, callback) {
|
||||
callback(null, results[0].value);
|
||||
});
|
||||
}
|
||||
|
||||
function setOwner(appId, ownerId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof ownerId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('UPDATE apps SET ownerId=? WHERE appId=?', [ ownerId, appId ], function (error, results) {
|
||||
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such user'));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such app'));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function transferOwnership(oldOwnerId, newOwnerId, callback) {
|
||||
assert.strictEqual(typeof oldOwnerId, 'string');
|
||||
assert.strictEqual(typeof newOwnerId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('UPDATE apps SET ownerId=? WHERE ownerId=?', [ newOwnerId, oldOwnerId ], function (error) {
|
||||
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such user'));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
+50
-59
@@ -12,15 +12,14 @@ var appdb = require('./appdb.js'),
|
||||
util = require('util');
|
||||
|
||||
exports = module.exports = {
|
||||
start: start,
|
||||
stop: stop
|
||||
run: run
|
||||
};
|
||||
|
||||
var HEALTHCHECK_INTERVAL = 10 * 1000; // every 10 seconds. this needs to be small since the UI makes only healthy apps clickable
|
||||
var UNHEALTHY_THRESHOLD = 10 * 60 * 1000; // 10 minutes
|
||||
var gHealthInfo = { }; // { time, emailSent }
|
||||
var gRunTimeout = null;
|
||||
var gDockerEventStream = null;
|
||||
|
||||
const NOOP_CALLBACK = function (error) { if (error) console.error(error); };
|
||||
|
||||
function debugApp(app) {
|
||||
assert(typeof app === 'object');
|
||||
@@ -88,6 +87,9 @@ function checkAppHealth(app, callback) {
|
||||
return setHealth(app, appdb.HEALTH_DEAD, callback);
|
||||
}
|
||||
|
||||
// non-appstore apps may not have healthCheckPath
|
||||
if (!manifest.healthCheckPath) return setHealth(app, appdb.HEALTH_HEALTHY, callback);
|
||||
|
||||
// poll through docker network instead of nginx to bypass any potential oauth proxy
|
||||
var healthCheckUrl = 'http://127.0.0.1:' + app.httpPort + manifest.healthCheckPath;
|
||||
superagent
|
||||
@@ -110,48 +112,23 @@ function checkAppHealth(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function processApps(callback) {
|
||||
apps.getAll(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.each(result, checkAppHealth, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
var alive = result
|
||||
.filter(function (a) { return a.installationState === appdb.ISTATE_INSTALLED && a.runState === appdb.RSTATE_RUNNING && a.health === appdb.HEALTH_HEALTHY; })
|
||||
.map(function (a) { return (a.location || 'naked_domain') + '|' + a.manifest.id; }).join(', ');
|
||||
|
||||
debug('apps alive: [%s]', alive);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function run() {
|
||||
processApps(function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
gRunTimeout = setTimeout(run, HEALTHCHECK_INTERVAL);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
OOM can be tested using stress tool like so:
|
||||
docker run -ti -m 100M cloudron/base:0.10.0 /bin/bash
|
||||
apt-get update && apt-get install stress
|
||||
stress --vm 1 --vm-bytes 200M --vm-hang 0
|
||||
*/
|
||||
function processDockerEvents() {
|
||||
// note that for some reason, the callback is called only on the first event
|
||||
debug('Listening for docker events');
|
||||
function processDockerEvents(interval, callback) {
|
||||
assert.strictEqual(typeof interval, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const OOM_MAIL_LIMIT = 60 * 60 * 1000; // 60 minutes
|
||||
var lastOomMailTime = new Date(new Date() - OOM_MAIL_LIMIT);
|
||||
let lastOomMailTime = new Date(new Date() - OOM_MAIL_LIMIT);
|
||||
const since = ((new Date().getTime() / 1000) - interval).toFixed(0);
|
||||
const until = ((new Date().getTime() / 1000) - 1).toFixed(0);
|
||||
|
||||
docker.getEvents({ filters: JSON.stringify({ event: [ 'oom' ] }) }, function (error, stream) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
gDockerEventStream = stream;
|
||||
docker.getEvents({ since: since, until: until, filters: JSON.stringify({ event: [ 'oom' ] }) }, function (error, stream) {
|
||||
if (error) return callback(error);
|
||||
|
||||
stream.setEncoding('utf8');
|
||||
stream.on('data', function (data) {
|
||||
@@ -173,34 +150,48 @@ function processDockerEvents() {
|
||||
});
|
||||
|
||||
stream.on('error', function (error) {
|
||||
console.error('Error reading docker events', error);
|
||||
gDockerEventStream = null; // TODO: reconnect?
|
||||
debug('Error reading docker events', error);
|
||||
callback();
|
||||
});
|
||||
|
||||
stream.on('end', function () {
|
||||
console.error('Docker event stream ended');
|
||||
gDockerEventStream = null; // TODO: reconnect?
|
||||
stream.on('end', callback);
|
||||
|
||||
// safety hatch if 'until' doesn't work (there are cases where docker is working with a different time)
|
||||
setTimeout(stream.destroy.bind(stream), 3000); // https://github.com/apocas/dockerode/issues/179
|
||||
});
|
||||
}
|
||||
|
||||
function processApp(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
apps.getAll(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.each(result, checkAppHealth, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
var alive = result
|
||||
.filter(function (a) { return a.installationState === appdb.ISTATE_INSTALLED && a.runState === appdb.RSTATE_RUNNING && a.health === appdb.HEALTH_HEALTHY; })
|
||||
.map(function (a) { return (a.location || 'naked_domain') + '|' + a.manifest.id; }).join(', ');
|
||||
|
||||
debug('apps alive: [%s]', alive);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function start(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
function run(interval, callback) {
|
||||
assert.strictEqual(typeof interval, 'number');
|
||||
|
||||
debug('Starting apphealthmonitor');
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
processDockerEvents();
|
||||
async.series([
|
||||
processDockerEvents.bind(null, interval),
|
||||
processApp
|
||||
], function (error) {
|
||||
if (error) debug(error);
|
||||
|
||||
run();
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
function stop(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
clearTimeout(gRunTimeout);
|
||||
if (gDockerEventStream) gDockerEventStream.end();
|
||||
|
||||
callback();
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
+245
-138
@@ -4,7 +4,8 @@ exports = module.exports = {
|
||||
AppsError: AppsError,
|
||||
|
||||
hasAccessTo: hasAccessTo,
|
||||
removeInternalAppFields: removeInternalAppFields,
|
||||
removeInternalFields: removeInternalFields,
|
||||
removeRestrictedFields: removeRestrictedFields,
|
||||
|
||||
get: get,
|
||||
getByIpAddress: getByIpAddress,
|
||||
@@ -41,10 +42,16 @@ exports = module.exports = {
|
||||
downloadFile: downloadFile,
|
||||
uploadFile: uploadFile,
|
||||
|
||||
setOwner: setOwner,
|
||||
transferOwnership: transferOwnership,
|
||||
|
||||
PORT_TYPE_TCP: 'tcp',
|
||||
PORT_TYPE_UDP: 'udp',
|
||||
|
||||
// exported for testing
|
||||
_validateHostname: validateHostname,
|
||||
_validatePortBindings: validatePortBindings,
|
||||
_validateAccessRestriction: validateAccessRestriction
|
||||
_validateAccessRestriction: validateAccessRestriction,
|
||||
_translatePortBindings: translatePortBindings
|
||||
};
|
||||
|
||||
var appdb = require('./appdb.js'),
|
||||
@@ -64,7 +71,6 @@ var appdb = require('./appdb.js'),
|
||||
DomainsError = require('./domains.js').DomainsError,
|
||||
eventlog = require('./eventlog.js'),
|
||||
fs = require('fs'),
|
||||
groups = require('./groups.js'),
|
||||
mail = require('./mail.js'),
|
||||
mailboxdb = require('./mailboxdb.js'),
|
||||
manifestFormat = require('cloudron-manifestformat'),
|
||||
@@ -78,7 +84,6 @@ var appdb = require('./appdb.js'),
|
||||
split = require('split'),
|
||||
superagent = require('superagent'),
|
||||
taskmanager = require('./taskmanager.js'),
|
||||
tld = require('tldjs'),
|
||||
TransformStream = require('stream').Transform,
|
||||
updateChecker = require('./updatechecker.js'),
|
||||
url = require('url'),
|
||||
@@ -120,43 +125,10 @@ AppsError.BILLING_REQUIRED = 'Billing Required';
|
||||
AppsError.ACCESS_DENIED = 'Access denied';
|
||||
AppsError.BAD_CERTIFICATE = 'Invalid certificate';
|
||||
|
||||
// Hostname validation comes from RFC 1123 (section 2.1)
|
||||
// Domain name validation comes from RFC 2181 (Name syntax)
|
||||
// https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
|
||||
// We are validating the validity of the location-fqdn as host name (and not dns name)
|
||||
function validateHostname(location, domain, hostname) {
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
|
||||
const RESERVED_LOCATIONS = [
|
||||
constants.API_LOCATION,
|
||||
constants.SMTP_LOCATION,
|
||||
constants.IMAP_LOCATION
|
||||
];
|
||||
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new AppsError(AppsError.BAD_FIELD, location + ' is reserved');
|
||||
|
||||
if (hostname === config.adminFqdn()) return new AppsError(AppsError.BAD_FIELD, location + ' is reserved');
|
||||
|
||||
// workaround https://github.com/oncletom/tld.js/issues/73
|
||||
var tmp = hostname.replace('_', '-');
|
||||
if (!tld.isValid(tmp)) return new AppsError(AppsError.BAD_FIELD, 'Hostname is not a valid domain name');
|
||||
|
||||
if (hostname.length > 253) return new AppsError(AppsError.BAD_FIELD, 'Hostname length exceeds 253 characters');
|
||||
|
||||
if (location) {
|
||||
// label validation
|
||||
if (location.length > 63) return new AppsError(AppsError.BAD_FIELD, 'Subdomain exceeds 63 characters');
|
||||
if (location.match(/^[A-Za-z0-9-]+$/) === null) return new AppsError(AppsError.BAD_FIELD, 'Subdomain can only contain alphanumerics and hyphen');
|
||||
if (location.startsWith('-') || location.endsWith('-')) return new AppsError(AppsError.BAD_FIELD, 'Subdomain cannot start or end with hyphen');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// validate the port bindings
|
||||
function validatePortBindings(portBindings, tcpPorts) {
|
||||
function validatePortBindings(portBindings, manifest) {
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
assert.strictEqual(typeof manifest, 'object');
|
||||
|
||||
// keep the public ports in sync with firewall rules in setup/start/cloudron-firewall.sh
|
||||
// these ports are reserved even if we listen only on 127.0.0.1 because we setup HostIp to be 127.0.0.1
|
||||
@@ -187,26 +159,58 @@ function validatePortBindings(portBindings, tcpPorts) {
|
||||
|
||||
if (!portBindings) return null;
|
||||
|
||||
var env;
|
||||
for (env in portBindings) {
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(env)) return new AppsError(AppsError.BAD_FIELD, env + ' is not valid environment variable');
|
||||
for (let portName in portBindings) {
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(portName)) return new AppsError(AppsError.BAD_FIELD, `${portName} is not a valid environment variable`);
|
||||
|
||||
if (!Number.isInteger(portBindings[env])) return new AppsError(AppsError.BAD_FIELD, portBindings[env] + ' is not an integer');
|
||||
if (RESERVED_PORTS.indexOf(portBindings[env]) !== -1) return new AppsError(AppsError.PORT_RESERVED, String(portBindings[env]));
|
||||
if (portBindings[env] <= 1023 || portBindings[env] > 65535) return new AppsError(AppsError.BAD_FIELD, portBindings[env] + ' is not in permitted range');
|
||||
const hostPort = portBindings[portName];
|
||||
if (!Number.isInteger(hostPort)) return new AppsError(AppsError.BAD_FIELD, `${hostPort} is not an integer`);
|
||||
if (RESERVED_PORTS.indexOf(hostPort) !== -1) return new AppsError(AppsError.PORT_RESERVED, String(hostPort));
|
||||
if (hostPort <= 1023 || hostPort > 65535) return new AppsError(AppsError.BAD_FIELD, `${hostPort} is not in permitted range`);
|
||||
|
||||
}
|
||||
|
||||
// it is OK if there is no 1-1 mapping between values in manifest.tcpPorts and portBindings. missing values implies
|
||||
// that the user wants the service disabled
|
||||
tcpPorts = tcpPorts || { };
|
||||
for (env in portBindings) {
|
||||
if (!(env in tcpPorts)) return new AppsError(AppsError.BAD_FIELD, 'Invalid portBindings ' + env);
|
||||
const tcpPorts = manifest.tcpPorts || { };
|
||||
const udpPorts = manifest.udpPorts || { };
|
||||
for (let portName in portBindings) {
|
||||
if (!(portName in tcpPorts) && !(portName in udpPorts)) return new AppsError(AppsError.BAD_FIELD, `Invalid portBindings ${portName}`);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function translatePortBindings(portBindings, manifest) {
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
assert.strictEqual(typeof manifest, 'object');
|
||||
|
||||
if (!portBindings) return null;
|
||||
|
||||
let result = {};
|
||||
const tcpPorts = manifest.tcpPorts || { };
|
||||
|
||||
for (let portName in portBindings) {
|
||||
const portType = portName in tcpPorts ? exports.PORT_TYPE_TCP : exports.PORT_TYPE_UDP;
|
||||
result[portName] = { hostPort: portBindings[portName], type: portType };
|
||||
}
|
||||
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');
|
||||
|
||||
@@ -287,6 +291,16 @@ function validateBackupFormat(format) {
|
||||
return new AppsError(AppsError.BAD_FIELD, 'Invalid backup format');
|
||||
}
|
||||
|
||||
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');
|
||||
// http://pubs.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap08.html
|
||||
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) return new AppsError(AppsError.BAD_FIELD, `"${key}" is not a valid environment variable`);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getDuplicateErrorDetails(location, portBindings, error) {
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
@@ -302,8 +316,8 @@ function getDuplicateErrorDetails(location, portBindings, error) {
|
||||
if (match[1] === location) return new AppsError(AppsError.ALREADY_EXISTS);
|
||||
|
||||
// check if any of the port bindings conflict
|
||||
for (var env in portBindings) {
|
||||
if (portBindings[env] === parseInt(match[1])) return new AppsError(AppsError.PORT_CONFLICT, match[1]);
|
||||
for (let portName in portBindings) {
|
||||
if (portBindings[portName] === parseInt(match[1])) return new AppsError(AppsError.PORT_CONFLICT, match[1]);
|
||||
}
|
||||
|
||||
return new AppsError(AppsError.ALREADY_EXISTS);
|
||||
@@ -320,16 +334,25 @@ function getAppConfig(app) {
|
||||
memoryLimit: app.memoryLimit,
|
||||
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN',
|
||||
robotsTxt: app.robotsTxt,
|
||||
sso: app.sso
|
||||
sso: app.sso,
|
||||
alternateDomains: app.alternateDomains || [],
|
||||
env: app.env
|
||||
};
|
||||
}
|
||||
|
||||
function removeInternalAppFields(app) {
|
||||
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');
|
||||
'sso', 'debugMode', 'robotsTxt', 'enableBackup', 'creationTime', 'updateTime', 'ts',
|
||||
'alternateDomains', 'ownerId', 'env');
|
||||
}
|
||||
|
||||
function removeRestrictedFields(app) {
|
||||
return _.pick(app,
|
||||
'id', 'appStoreId', 'installationState', 'installationProgress', 'runState', 'health', 'ownerId',
|
||||
'location', 'domain', 'fqdn', 'manifest', 'portBindings', 'iconUrl', 'creationTime', 'ts');
|
||||
}
|
||||
|
||||
function getIconUrlSync(app) {
|
||||
@@ -347,35 +370,34 @@ function hasAccessTo(app, user, callback) {
|
||||
// check user access
|
||||
if (app.accessRestriction.users.some(function (e) { return e === user.id; })) return callback(null, true);
|
||||
|
||||
// check group access
|
||||
groups.getGroups(user.id, function (error, groupIds) {
|
||||
if (error) return callback(null, false);
|
||||
if (user.admin) return callback(null, true); // admins can always access any app
|
||||
|
||||
const isAdmin = groupIds.indexOf(constants.ADMIN_GROUP_ID) !== -1;
|
||||
if (!app.accessRestriction.groups) return callback(null, false);
|
||||
|
||||
if (isAdmin) return callback(null, true); // admins can always access any app
|
||||
if (app.accessRestriction.groups.some(function (gid) { return user.groupIds.indexOf(gid) !== -1; })) return callback(null, true);
|
||||
|
||||
if (!app.accessRestriction.groups) return callback(null, false);
|
||||
|
||||
if (app.accessRestriction.groups.some(function (gid) { return groupIds.indexOf(gid) !== -1; })) return callback(null, true);
|
||||
|
||||
callback(null, false);
|
||||
});
|
||||
callback(null, false);
|
||||
}
|
||||
|
||||
function get(appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
appdb.get(appId, function (error, app) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) 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));
|
||||
|
||||
domaindb.get(app.domain, function (error, result) {
|
||||
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);
|
||||
|
||||
let domainObjectMap = {};
|
||||
for (let d of domainObjects) { domainObjectMap[d.domain] = d; }
|
||||
|
||||
app.iconUrl = getIconUrlSync(app);
|
||||
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
|
||||
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
|
||||
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
|
||||
|
||||
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
@@ -392,25 +414,38 @@ function getByIpAddress(ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
docker.getContainerIdByIp(ip, function (error, containerId) {
|
||||
domaindb.getAll(function (error, domainObjects) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
appdb.getByContainerId(containerId, function (error, app) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
let domainObjectMap = {};
|
||||
for (let d of domainObjects) { domainObjectMap[d.domain] = d; }
|
||||
|
||||
docker.getContainerIdByIp(ip, function (error, containerId) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
domaindb.get(app.domain, function (error, result) {
|
||||
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));
|
||||
|
||||
app.iconUrl = getIconUrlSync(app);
|
||||
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
|
||||
postProcess(app);
|
||||
|
||||
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
domaindb.getAll(function (error, domainObjects) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (!error) app.mailboxName = mailboxes[0].name;
|
||||
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]); });
|
||||
|
||||
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (!error) app.mailboxName = mailboxes[0].name;
|
||||
|
||||
callback(null, app);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -420,28 +455,34 @@ function getByIpAddress(ip, callback) {
|
||||
function getAll(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
appdb.getAll(function (error, apps) {
|
||||
domaindb.getAll(function (error, domainObjects) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
async.eachSeries(apps, function (app, iteratorDone) {
|
||||
domaindb.get(app.domain, function (error, result) {
|
||||
if (error) return iteratorDone(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
let domainObjectMap = {};
|
||||
for (let d of domainObjects) { domainObjectMap[d.domain] = d; }
|
||||
|
||||
appdb.getAll(function (error, apps) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
apps.forEach(postProcess);
|
||||
|
||||
async.eachSeries(apps, function (app, iteratorDone) {
|
||||
app.iconUrl = getIconUrlSync(app);
|
||||
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
|
||||
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
|
||||
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
|
||||
|
||||
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return iteratorDone(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (!error) app.mailboxName = mailboxes[0].name;
|
||||
|
||||
iteratorDone(null, app);
|
||||
});
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, apps);
|
||||
callback(null, apps);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -483,8 +524,9 @@ function mailboxNameForLocation(location, manifest) {
|
||||
return (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
|
||||
}
|
||||
|
||||
function install(data, auditSource, callback) {
|
||||
function install(data, user, auditSource, callback) {
|
||||
assert(data && typeof data === 'object');
|
||||
assert(user && typeof user === 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -502,7 +544,10 @@ function install(data, auditSource, callback) {
|
||||
robotsTxt = data.robotsTxt || null,
|
||||
enableBackup = 'enableBackup' in data ? data.enableBackup : true,
|
||||
backupId = data.backupId || null,
|
||||
backupFormat = data.backupFormat || 'tgz';
|
||||
backupFormat = data.backupFormat || 'tgz',
|
||||
ownerId = data.ownerId,
|
||||
alternateDomains = data.alternateDomains || [],
|
||||
env = data.env || {};
|
||||
|
||||
assert(data.appStoreId || data.manifest); // atleast one of them is required
|
||||
|
||||
@@ -515,7 +560,7 @@ function install(data, auditSource, callback) {
|
||||
error = checkManifestConstraints(manifest);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validatePortBindings(portBindings, manifest.tcpPorts);
|
||||
error = validatePortBindings(portBindings, manifest);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validateAccessRestriction(accessRestriction);
|
||||
@@ -540,6 +585,9 @@ function install(data, auditSource, callback) {
|
||||
// if sso was unspecified, enable it by default if possible
|
||||
if (sso === null) sso = !!manifest.addons['ldap'] || !!manifest.addons['oauth'];
|
||||
|
||||
error = validateEnv(env);
|
||||
if (error) return callback(error);
|
||||
|
||||
var appId = uuid.v4();
|
||||
|
||||
if (icon) {
|
||||
@@ -554,13 +602,14 @@ function install(data, 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));
|
||||
|
||||
var fqdn = domains.fqdn(location, domain, domainObject.provider);
|
||||
location = addSpacesSuffix(location, user);
|
||||
alternateDomains.forEach(function (ad) { ad.subdomain = addSpacesSuffix(ad.subdomain, user); }); // TODO: validate these
|
||||
|
||||
error = validateHostname(location, domain, fqdn);
|
||||
if (error) return callback(error);
|
||||
error = domains.validateHostname(location, domainObject);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message));
|
||||
|
||||
if (cert && key) {
|
||||
error = reverseProxy.validateCertificate(fqdn, cert, key);
|
||||
error = reverseProxy.validateCertificate(location, domainObject, { cert, key });
|
||||
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
|
||||
}
|
||||
|
||||
@@ -575,19 +624,21 @@ function install(data, auditSource, callback) {
|
||||
mailboxName: mailboxNameForLocation(location, manifest),
|
||||
restoreConfig: backupId ? { backupId: backupId, backupFormat: backupFormat } : null,
|
||||
enableBackup: enableBackup,
|
||||
robotsTxt: robotsTxt
|
||||
robotsTxt: robotsTxt,
|
||||
alternateDomains: alternateDomains,
|
||||
env: env
|
||||
};
|
||||
|
||||
appdb.add(appId, appStoreId, manifest, location, domain, portBindings, data, function (error) {
|
||||
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.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, function (appstoreError) {
|
||||
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) console.error('Failed to rollback app installation.', 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));
|
||||
@@ -601,8 +652,8 @@ function install(data, auditSource, callback) {
|
||||
|
||||
// save cert to boxdata/certs
|
||||
if (cert && key) {
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.user.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.user.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
|
||||
let error = reverseProxy.setAppCertificateSync(location, domainObject, { cert, key });
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error setting cert: ' + error.message));
|
||||
}
|
||||
|
||||
taskmanager.restartAppTask(appId);
|
||||
@@ -621,16 +672,17 @@ function install(data, auditSource, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function configure(appId, data, auditSource, callback) {
|
||||
function configure(appId, data, user, auditSource, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert(data && typeof data === 'object');
|
||||
assert(user && typeof user === 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
get(appId, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var domain, location, portBindings, values = { };
|
||||
let domain, location, portBindings, values = { }, mailboxName;
|
||||
if ('location' in data) location = values.location = data.location.toLowerCase();
|
||||
else location = app.location;
|
||||
|
||||
@@ -644,9 +696,10 @@ function configure(appId, data, auditSource, callback) {
|
||||
}
|
||||
|
||||
if ('portBindings' in data) {
|
||||
portBindings = values.portBindings = data.portBindings;
|
||||
error = validatePortBindings(values.portBindings, app.manifest.tcpPorts);
|
||||
error = validatePortBindings(data.portBindings, app.manifest);
|
||||
if (error) return callback(error);
|
||||
values.portBindings = translatePortBindings(data.portBindings, app.manifest);
|
||||
portBindings = data.portBindings;
|
||||
} else {
|
||||
portBindings = app.portBindings;
|
||||
}
|
||||
@@ -676,7 +729,26 @@ function configure(appId, data, auditSource, callback) {
|
||||
}
|
||||
|
||||
if ('mailboxName' in data) {
|
||||
error = mail.validateName(data.mailboxName);
|
||||
if (data.mailboxName === '') { // special case to reset back to .app
|
||||
mailboxName = mailboxNameForLocation(location, app.manifest);
|
||||
} else {
|
||||
error = mail.validateName(data.mailboxName);
|
||||
if (error) return callback(error);
|
||||
mailboxName = data.mailboxName;
|
||||
}
|
||||
} else { // keep existing name or follow the new location
|
||||
mailboxName = app.mailboxName.endsWith('.app') ? mailboxNameForLocation(location, app.manifest) : app.mailboxName;
|
||||
}
|
||||
|
||||
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) {
|
||||
values.env = data.env;
|
||||
error = validateEnv(data.env);
|
||||
if (error) return callback(error);
|
||||
}
|
||||
|
||||
@@ -684,23 +756,20 @@ function configure(appId, data, 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));
|
||||
|
||||
var fqdn = domains.fqdn(location, domain, domainObject.provider);
|
||||
location = addSpacesSuffix(location, user);
|
||||
|
||||
error = validateHostname(location, domain, fqdn);
|
||||
if (error) return callback(error);
|
||||
error = domains.validateHostname(location, domainObject);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message));
|
||||
|
||||
// save cert to boxdata/certs. TODO: move this to apptask when we have a real task queue
|
||||
if ('cert' in data && 'key' in data) {
|
||||
if (data.cert && data.key) {
|
||||
error = reverseProxy.validateCertificate(fqdn, data.cert, data.key);
|
||||
error = reverseProxy.validateCertificate(location, domainObject, { cert: data.cert, key: data.key });
|
||||
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`), data.cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.key`), data.key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
|
||||
} else { // remove existing cert/key
|
||||
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`))) debug('Error removing cert: ' + safe.error.message);
|
||||
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}..user.key`))) debug('Error removing key: ' + safe.error.message);
|
||||
}
|
||||
|
||||
error = reverseProxy.setAppCertificateSync(location, domainObject, { cert: data.cert, key: data.key });
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error setting cert: ' + error.message));
|
||||
}
|
||||
|
||||
if ('enableBackup' in data) values.enableBackup = data.enableBackup;
|
||||
@@ -710,9 +779,9 @@ function configure(appId, data, auditSource, callback) {
|
||||
debug('Will configure app with id:%s values:%j', appId, values);
|
||||
|
||||
// make the mailbox name follow the apps new location, if the user did not set it explicitly
|
||||
var oldName = app.mailboxName;
|
||||
var newName = data.mailboxName || (app.mailboxName.endsWith('.app') ? mailboxNameForLocation(location, app.manifest) : app.mailboxName);
|
||||
mailboxdb.updateName(oldName, values.oldConfig.domain, newName, domain, function (error) {
|
||||
mailboxdb.updateName(app.mailboxName /* old */, values.oldConfig.domain, mailboxName, domain, function (error) {
|
||||
if (mailboxName.endsWith('.app')) error = null; // ignore internal mailbox conflict errors since we want to show location conflict errors in the UI
|
||||
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new AppsError(AppsError.ALREADY_EXISTS, 'This mailbox is already taken'));
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
@@ -815,7 +884,7 @@ function getLogs(appId, options, callback) {
|
||||
|
||||
debug('Getting logs for %s', appId);
|
||||
|
||||
get(appId, function (error /*, app */) {
|
||||
get(appId, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var lines = options.lines || 100,
|
||||
@@ -826,9 +895,10 @@ function getLogs(appId, options, callback) {
|
||||
assert.strictEqual(typeof format, 'string');
|
||||
|
||||
var args = [ '--lines=' + lines ];
|
||||
if (follow) args.push('--follow', '--retry'); // same as -F. to make it work if file doesn't exist
|
||||
args.push(path.join(paths.LOG_DIR, appId, 'app.log'));
|
||||
if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs
|
||||
args.push(path.join(paths.LOG_DIR, appId, 'apptask.log'));
|
||||
args.push(path.join(paths.LOG_DIR, appId, 'app.log'));
|
||||
if (app.manifest.addons && app.manifest.addons.redis) args.push(path.join(paths.LOG_DIR, `redis-${appId}/app.log`));
|
||||
|
||||
var cp = spawn('/usr/bin/tail', args);
|
||||
|
||||
@@ -838,10 +908,14 @@ function getLogs(appId, options, callback) {
|
||||
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
|
||||
var timestamp = (new Date(data[0])).getTime();
|
||||
if (isNaN(timestamp)) timestamp = 0;
|
||||
var message = line.slice(data[0].length+1);
|
||||
|
||||
// ignore faulty empty logs
|
||||
if (!timestamp && !message) return;
|
||||
|
||||
return JSON.stringify({
|
||||
realtimeTimestamp: timestamp * 1000,
|
||||
message: line.slice(data[0].length+1),
|
||||
message: message,
|
||||
source: appId
|
||||
}) + '\n';
|
||||
});
|
||||
@@ -900,9 +974,10 @@ function restore(appId, data, auditSource, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function clone(appId, data, auditSource, callback) {
|
||||
function clone(appId, data, user, auditSource, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
assert(user && typeof user === 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -911,12 +986,14 @@ function clone(appId, data, auditSource, callback) {
|
||||
var location = data.location.toLowerCase(),
|
||||
domain = data.domain.toLowerCase(),
|
||||
portBindings = data.portBindings || null,
|
||||
backupId = data.backupId;
|
||||
backupId = data.backupId,
|
||||
ownerId = data.ownerId;
|
||||
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
assert(ownerId === null || typeof ownerId === 'string');
|
||||
|
||||
get(appId, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
@@ -932,15 +1009,16 @@ function clone(appId, data, auditSource, callback) {
|
||||
error = checkManifestConstraints(backupInfo.manifest);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validatePortBindings(portBindings, backupInfo.manifest.tcpPorts);
|
||||
error = validatePortBindings(portBindings, backupInfo.manifest);
|
||||
if (error) return callback(error);
|
||||
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'No such domain'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
|
||||
|
||||
error = validateHostname(location, domain, domains.fqdn(location, domain, domainObject.provider));
|
||||
if (error) return callback(error);
|
||||
location = addSpacesSuffix(location, user);
|
||||
error = domains.validateHostname(location, domainObject);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message));
|
||||
|
||||
var newAppId = uuid.v4(), manifest = backupInfo.manifest;
|
||||
|
||||
@@ -953,18 +1031,19 @@ function clone(appId, data, auditSource, callback) {
|
||||
sso: !!app.sso,
|
||||
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app',
|
||||
enableBackup: app.enableBackup,
|
||||
robotsTxt: app.robotsTxt
|
||||
robotsTxt: app.robotsTxt,
|
||||
env: app.env
|
||||
};
|
||||
|
||||
appdb.add(newAppId, app.appStoreId, manifest, location, domain, portBindings, data, function (error) {
|
||||
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) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
appstore.purchase(newAppId, app.appStoreId, function (appstoreError) {
|
||||
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) console.error('Failed to rollback app installation.', 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));
|
||||
@@ -1003,7 +1082,7 @@ function uninstall(appId, auditSource, callback) {
|
||||
get(appId, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
|
||||
appstore.unpurchase(appId, app.appStoreId, function (error) {
|
||||
appstore.unpurchase(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.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
|
||||
@@ -1101,6 +1180,8 @@ function exec(appId, options, callback) {
|
||||
};
|
||||
|
||||
container.exec(execOptions, function (error, exec) {
|
||||
if (error && error.statusCode === 409) return callback(new AppsError(AppsError.BAD_STATE, error.message)); // container restarting/not running
|
||||
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
var startOptions = {
|
||||
Detach: false,
|
||||
@@ -1141,11 +1222,12 @@ function autoupdateApps(updateInfo, auditSource, callback) { // updateInfo is {
|
||||
function canAutoupdateApp(app, newManifest) {
|
||||
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
|
||||
|
||||
var newTcpPorts = newManifest.tcpPorts || { };
|
||||
var portBindings = app.portBindings; // this is never null
|
||||
const newTcpPorts = newManifest.tcpPorts || { };
|
||||
const newUdpPorts = newManifest.udpPorts || { };
|
||||
const portBindings = app.portBindings; // this is never null
|
||||
|
||||
for (var env in portBindings) {
|
||||
if (!(env in newTcpPorts)) return new Error(env + ' was in use but new update removes it');
|
||||
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
|
||||
@@ -1341,3 +1423,28 @@ function uploadFile(appId, sourceFilePath, destFilePath, callback) {
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function setOwner(appId, ownerId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
appdb.setOwner(appId, ownerId, function (error) {
|
||||
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));
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function transferOwnership(oldOwnerId, newOwnerId, callback) {
|
||||
assert.strictEqual(typeof oldOwnerId, 'string');
|
||||
assert.strictEqual(typeof newOwnerId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
appdb.transferOwnership(oldOwnerId, newOwnerId, function (error) {
|
||||
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));
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
+21
-45
@@ -19,8 +19,7 @@ exports = module.exports = {
|
||||
AppstoreError: AppstoreError
|
||||
};
|
||||
|
||||
var appdb = require('./appdb.js'),
|
||||
apps = require('./apps.js'),
|
||||
var apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
@@ -95,59 +94,36 @@ function isFreePlan(subscription) {
|
||||
return !subscription || subscription.plan.id === 'free';
|
||||
}
|
||||
|
||||
function purchase(appId, appstoreId, callback) {
|
||||
// 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 appstoreId, 'string');
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
assert(data.appstoreId || data.manifestId);
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (appstoreId === '') return callback(null);
|
||||
|
||||
function doThePurchase() {
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/apps/' + appId;
|
||||
var data = { appstoreId: appstoreId };
|
||||
|
||||
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
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 !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getSubscription(function (error, result) {
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// only check for app install count if on the free plan
|
||||
if (result.id !== 'free') return doThePurchase();
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/apps/' + appId;
|
||||
|
||||
appdb.getAppStoreIds(function (error, result) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
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 !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
|
||||
|
||||
var count = result.filter(function (a) { return !!a.appStoreId; }).length;
|
||||
|
||||
// we only allow max of 2 app installations without a subscription
|
||||
// WARNING install and clone in apps.js will first add the db record and then call purchase() so we test for more than 2 here
|
||||
if (count > 2) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED, 'Too many apps installed'));
|
||||
|
||||
doThePurchase();
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function unpurchase(appId, appstoreId, callback) {
|
||||
function unpurchase(appId, data, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof appstoreId, 'string');
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
assert(data.appstoreId || data.manifestId);
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (appstoreId === '') return callback(null);
|
||||
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -159,7 +135,7 @@ function unpurchase(appId, appstoreId, callback) {
|
||||
if (result.statusCode === 404) return callback(null); // was never purchased
|
||||
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
|
||||
|
||||
superagent.del(url).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
superagent.del(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
|
||||
if (result.statusCode !== 204) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
|
||||
@@ -265,17 +241,17 @@ function getBoxUpdate(callback) {
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/boxupdate';
|
||||
|
||||
superagent.get(url).query({ accessToken: appstoreConfig.token, boxVersion: config.version() }).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.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)));
|
||||
|
||||
var updateInfo = result.body;
|
||||
|
||||
if (!semver.valid(updateInfo.version) || semver.gt(config.version(), updateInfo.version)) {
|
||||
return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Invalid update version: %s %s', result.statusCode, result.text)));
|
||||
}
|
||||
|
||||
// { version, changelog, upgrade, sourceTarballUrl}
|
||||
// updateInfo: { version, changelog, upgrade, sourceTarballUrl, sourceTarballSigUrl, boxVersionsUrl, boxVersionsSigUrl }
|
||||
callback(null, updateInfo);
|
||||
});
|
||||
});
|
||||
|
||||
+166
-41
@@ -10,8 +10,8 @@ exports = module.exports = {
|
||||
_reserveHttpPort: reserveHttpPort,
|
||||
_configureReverseProxy: configureReverseProxy,
|
||||
_unconfigureReverseProxy: unconfigureReverseProxy,
|
||||
_createVolume: createVolume,
|
||||
_deleteVolume: deleteVolume,
|
||||
_createAppDir: createAppDir,
|
||||
_deleteAppDir: deleteAppDir,
|
||||
_verifyManifest: verifyManifest,
|
||||
_registerSubdomain: registerSubdomain,
|
||||
_unregisterSubdomain: unregisterSubdomain,
|
||||
@@ -36,6 +36,7 @@ var addons = require('./addons.js'),
|
||||
ejs = require('ejs'),
|
||||
fs = require('fs'),
|
||||
manifestFormat = require('cloudron-manifestformat'),
|
||||
mkdirp = require('mkdirp'),
|
||||
net = require('net'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
@@ -46,16 +47,13 @@ var addons = require('./addons.js'),
|
||||
shell = require('./shell.js'),
|
||||
superagent = require('superagent'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
tld = require('tldjs'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
var COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd.config.ejs', { encoding: 'utf8' }),
|
||||
CONFIGURE_COLLECTD_CMD = path.join(__dirname, 'scripts/configurecollectd.sh'),
|
||||
LOGROTATE_CONFIG_EJS = fs.readFileSync(__dirname + '/logrotate.ejs', { encoding: 'utf8' }),
|
||||
CONFIGURE_LOGROTATE_CMD = path.join(__dirname, 'scripts/configurelogrotate.sh'),
|
||||
RMAPPDIR_CMD = path.join(__dirname, 'scripts/rmappdir.sh'),
|
||||
CREATEAPPDIR_CMD = path.join(__dirname, 'scripts/createappdir.sh');
|
||||
CONFIGURE_LOGROTATE_CMD = path.join(__dirname, 'scripts/configurelogrotate.sh');
|
||||
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -135,6 +133,20 @@ function createContainer(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
// Only delete the main container of the app, not destroy any docker addon created ones
|
||||
function deleteMainContainer(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'deleting main app container');
|
||||
|
||||
docker.deleteContainer(app.containerId, function (error) {
|
||||
if (error) return callback(new Error('Error deleting container: ' + error));
|
||||
|
||||
updateApp(app, { containerId: null }, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function deleteContainers(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -148,19 +160,47 @@ function deleteContainers(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function createVolume(app, callback) {
|
||||
function createAppDir(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
shell.sudo('createVolume', [ CREATEAPPDIR_CMD, app.id ], callback);
|
||||
mkdirp(path.join(paths.APPS_DATA_DIR, app.id), callback);
|
||||
}
|
||||
|
||||
function deleteVolume(app, options, callback) {
|
||||
function deleteAppDir(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
shell.sudo('deleteVolume', [ RMAPPDIR_CMD, app.id, !!options.removeDirectory ], callback);
|
||||
const appDataDir = path.join(paths.APPS_DATA_DIR, app.id);
|
||||
|
||||
// resolve any symlinked data dir
|
||||
const stat = safe.fs.lstatSync(appDataDir);
|
||||
if (!stat) return callback(null);
|
||||
|
||||
const resolvedAppDataDir = stat.isSymbolicLink() ? safe.fs.readlinkSync(appDataDir) : appDataDir;
|
||||
|
||||
if (safe.fs.existsSync(resolvedAppDataDir)) {
|
||||
const entries = safe.fs.readdirSync(resolvedAppDataDir);
|
||||
if (!entries) return callback(`Error listing ${resolvedAppDataDir}: ${safe.error.message}`);
|
||||
|
||||
// remove only files. directories inside app dir are currently volumes managed by the addons
|
||||
entries.forEach(function (entry) {
|
||||
let stat = safe.fs.statSync(path.join(resolvedAppDataDir, entry));
|
||||
if (stat && !stat.isDirectory()) safe.fs.unlinkSync(path.join(resolvedAppDataDir, entry));
|
||||
});
|
||||
}
|
||||
|
||||
// if this fails, it's probably because the localstorage/redis addons have not cleaned up properly
|
||||
if (options.removeDirectory) {
|
||||
if (stat.isSymbolicLink()) {
|
||||
if (!safe.fs.unlinkSync(appDataDir)) return callback(safe.error.code === 'ENOENT' ? null : safe.error);
|
||||
} else {
|
||||
if (!safe.fs.rmdirSync(appDataDir)) return callback(safe.error.code === 'ENOENT' ? null : safe.error);
|
||||
}
|
||||
}
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function addCollectdProfile(app, callback) {
|
||||
@@ -269,17 +309,18 @@ function registerSubdomain(app, overwrite, callback) {
|
||||
// refuse to update any existing DNS record for custom domains that we did not create
|
||||
if (values.length !== 0 && !overwrite) return retryCallback(null, new Error('DNS Record already exists'));
|
||||
|
||||
domains.upsertDnsRecords(app.location, app.domain, 'A', [ ip ], function (error, changeId) {
|
||||
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) return retryCallback(error); // try again
|
||||
domains.upsertDnsRecords(app.location, app.domain, 'A', [ ip ], function (error) {
|
||||
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) {
|
||||
debug('Upsert error. Will retry.', error.message);
|
||||
return retryCallback(error); // try again
|
||||
}
|
||||
|
||||
retryCallback(null, error || changeId);
|
||||
retryCallback(null, error);
|
||||
});
|
||||
});
|
||||
}, function (error, result) {
|
||||
if (error || result instanceof Error) return callback(error || result);
|
||||
|
||||
// dnsRecordId tracks whether we created this DNS record so that we can unregister later
|
||||
updateApp(app, { dnsRecordId: result }, callback);
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -290,11 +331,6 @@ function unregisterSubdomain(app, location, domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!app.dnsRecordId) {
|
||||
debugApp(app, 'Skip unregister of record not created by cloudron');
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -309,12 +345,83 @@ function unregisterSubdomain(app, location, domain, callback) {
|
||||
});
|
||||
}, function (error, result) {
|
||||
if (error || result instanceof Error) return callback(error || result);
|
||||
|
||||
updateApp(app, { dnsRecordId: null }, callback);
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function registerAlternateDomains(app, overwrite, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof overwrite, 'boolean');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(app.alternateDomains, function (domain, callback) {
|
||||
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
|
||||
debugApp(app, 'Registering alternate subdomain [%s] overwrite: %s', (domain.subdomain ? (domain.subdomain + '.') : '') + domain.domain, overwrite);
|
||||
|
||||
// get the current record before updating it
|
||||
domains.getDnsRecords(domain.subdomain, domain.domain, 'A', function (error, values) {
|
||||
if (error) return retryCallback(error);
|
||||
|
||||
// refuse to update any existing DNS record for custom domains that we did not create
|
||||
if (values.length !== 0 && !overwrite) return retryCallback(null, new Error('DNS Record already exists'));
|
||||
|
||||
domains.upsertDnsRecords(domain.subdomain, domain.domain, 'A', [ ip ], function (error) {
|
||||
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) {
|
||||
debug('Upsert error. Will retry.', error.message);
|
||||
return retryCallback(error); // try again
|
||||
}
|
||||
retryCallback(null, error);
|
||||
});
|
||||
});
|
||||
}, function (error, result) {
|
||||
if (error || result instanceof Error) return callback(error || result);
|
||||
callback();
|
||||
});
|
||||
}, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function unregisterAlternateDomains(app, all, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof all, 'boolean');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let obsoleteDomains = [];
|
||||
if (all) {
|
||||
obsoleteDomains = app.alternateDomains;
|
||||
} else if (app.oldConfig) { // oldConfig can be null during an infra update
|
||||
obsoleteDomains = app.oldConfig.alternateDomains.filter(function (o) {
|
||||
return !app.alternateDomains.some(function (n) { return n.subdomain === o.subdomain && n.domain === o.domain; });
|
||||
});
|
||||
}
|
||||
|
||||
if (obsoleteDomains.length === 0) return callback();
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(obsoleteDomains, function (domain, callback) {
|
||||
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
|
||||
debugApp(app, 'Unregistering subdomain: %s%s', domain.subdomain ? (domain.subdomain + '.') : '', domain.domain);
|
||||
|
||||
domains.removeDnsRecords(domain.subdomain, domain.domain, 'A', [ ip ], function (error) {
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return retryCallback(null, null);
|
||||
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) return retryCallback(error); // try again
|
||||
|
||||
retryCallback(null, error);
|
||||
});
|
||||
}, function (error, result) {
|
||||
if (error || result instanceof Error) return callback(error || result);
|
||||
callback();
|
||||
});
|
||||
}, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function removeIcon(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -329,8 +436,10 @@ function cleanupLogs(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// note that redis container logs are cleaned up by the addon
|
||||
rimraf(path.join(paths.LOG_DIR, app.id), function (error) {
|
||||
if (error) debugApp(app, 'cannot cleanup logs: %s', error);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
@@ -347,7 +456,14 @@ function waitForDnsPropagation(app, callback) {
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
domains.waitForDnsRecord(app.fqdn, app.domain, ip, { interval: 5000, times: 240 }, callback);
|
||||
domains.waitForDnsRecord(app.location, app.domain, 'A', ip, { interval: 5000, times: 240 }, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// now wait for alternateDomains, if any
|
||||
async.eachSeries(app.alternateDomains, function (domain, iteratorCallback) {
|
||||
domains.waitForDnsRecord(domain.subdomain, domain.domain, 'A', ip, { interval: 5000, times: 240 }, iteratorCallback);
|
||||
}, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -379,16 +495,14 @@ function install(app, callback) {
|
||||
removeCollectdProfile.bind(null, app),
|
||||
removeLogrotateConfig.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
deleteContainers.bind(null, app),
|
||||
deleteMainContainer.bind(null, app),
|
||||
function teardownAddons(next) {
|
||||
// when restoring, app does not require these addons anymore. remove carefully to preserve the db passwords
|
||||
var addonsToRemove = !isRestoring
|
||||
? app.manifest.addons
|
||||
: _.omit(app.oldConfig.manifest.addons, Object.keys(app.manifest.addons));
|
||||
var addonsToRemove = !isRestoring ? app.manifest.addons : _.omit(app.oldConfig.manifest.addons, Object.keys(app.manifest.addons));
|
||||
|
||||
addons.teardownAddons(app, addonsToRemove, next);
|
||||
},
|
||||
deleteVolume.bind(null, app, { removeDirectory: false }), // do not remove any symlinked volume
|
||||
deleteAppDir.bind(null, app, { removeDirectory: false }), // do not remove any symlinked volume
|
||||
|
||||
// for restore case
|
||||
function deleteImageIfChanged(done) {
|
||||
@@ -405,11 +519,14 @@ function install(app, callback) {
|
||||
updateApp.bind(null, app, { installationProgress: '30, Registering subdomain' }),
|
||||
registerSubdomain.bind(null, app, isRestoring /* overwrite */),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '35, Registering alternate domains'}),
|
||||
registerAlternateDomains.bind(null, app, isRestoring /* overwrite */),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '40, Downloading image' }),
|
||||
docker.downloadImage.bind(null, app.manifest),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '50, Creating volume' }),
|
||||
createVolume.bind(null, app),
|
||||
createAppDir.bind(null, app),
|
||||
|
||||
function restoreFromBackup(next) {
|
||||
if (!restoreConfig) {
|
||||
@@ -420,6 +537,8 @@ function install(app, callback) {
|
||||
} else {
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '60, Download backup and restoring addons' }),
|
||||
addons.setupAddons.bind(null, app, app.manifest.addons),
|
||||
addons.clearAddons.bind(null, app, app.manifest.addons),
|
||||
backups.restoreApp.bind(null, app, app.manifest.addons, restoreConfig),
|
||||
], next);
|
||||
}
|
||||
@@ -492,26 +611,31 @@ function configure(app, callback) {
|
||||
removeCollectdProfile.bind(null, app),
|
||||
removeLogrotateConfig.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
deleteContainers.bind(null, app),
|
||||
deleteMainContainer.bind(null, app),
|
||||
unregisterAlternateDomains.bind(null, app, false /* all */),
|
||||
function (next) {
|
||||
if (!locationChanged) return next();
|
||||
|
||||
unregisterSubdomain(app, app.oldConfig.location, app.oldConfig.domain, next);
|
||||
},
|
||||
|
||||
|
||||
reserveHttpPort.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '20, Downloading icon' }),
|
||||
downloadIcon.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '35, Registering subdomain' }),
|
||||
updateApp.bind(null, app, { installationProgress: '30, Registering subdomain' }),
|
||||
registerSubdomain.bind(null, app, !locationChanged /* overwrite */), // if location changed, do not overwrite to detect conflicts
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '35, Registering alternate domains'}),
|
||||
registerAlternateDomains.bind(null, app, true /* overwrite */), // figure out when to overwrite
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '40, Downloading image' }),
|
||||
docker.downloadImage.bind(null, app.manifest),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '45, Ensuring volume' }),
|
||||
createVolume.bind(null, app),
|
||||
createAppDir.bind(null, app),
|
||||
|
||||
// re-setup addons since they rely on the app's fqdn (e.g oauth)
|
||||
updateApp.bind(null, app, { installationProgress: '50, Setting up addons' }),
|
||||
@@ -588,7 +712,7 @@ function update(app, callback) {
|
||||
removeCollectdProfile.bind(null, app),
|
||||
removeLogrotateConfig.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
deleteContainers.bind(null, app),
|
||||
deleteMainContainer.bind(null, app),
|
||||
function deleteImageIfChanged(done) {
|
||||
if (app.manifest.dockerImage === app.updateConfig.manifest.dockerImage) return done();
|
||||
|
||||
@@ -600,14 +724,14 @@ function update(app, callback) {
|
||||
|
||||
// free unused ports
|
||||
function (next) {
|
||||
// make sure we always have objects
|
||||
var currentPorts = app.portBindings || {};
|
||||
var newPorts = app.updateConfig.manifest.tcpPorts || {};
|
||||
const currentPorts = app.portBindings || {};
|
||||
const newTcpPorts = app.updateConfig.manifest.tcpPorts || {};
|
||||
const newUdpPorts = app.updateConfig.manifest.udpPorts || {};
|
||||
|
||||
async.each(Object.keys(currentPorts), function (portName, callback) {
|
||||
if (newPorts[portName]) return callback(); // port still in use
|
||||
if (newTcpPorts[portName] || newUdpPorts[portName]) return callback(); // port still in use
|
||||
|
||||
appdb.delPortBinding(currentPorts[portName], function (error) {
|
||||
appdb.delPortBinding(currentPorts[portName], apps.PORT_TYPE_TCP, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) console.error('Portbinding does not exist in database.');
|
||||
else if (error) return next(error);
|
||||
|
||||
@@ -680,12 +804,13 @@ function uninstall(app, callback) {
|
||||
addons.teardownAddons.bind(null, app, app.manifest.addons),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '40, Deleting volume' }),
|
||||
deleteVolume.bind(null, app, { removeDirectory: true }),
|
||||
deleteAppDir.bind(null, app, { removeDirectory: true }),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '50, Deleting image' }),
|
||||
docker.deleteImage.bind(null, app.manifest),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '60, Unregistering subdomain' }),
|
||||
updateApp.bind(null, app, { installationProgress: '60, Unregistering domains' }),
|
||||
unregisterAlternateDomains.bind(null, app, true /* all */),
|
||||
unregisterSubdomain.bind(null, app, app.location, app.domain),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '70, Cleanup icon' }),
|
||||
|
||||
+187
-18
@@ -58,6 +58,7 @@ 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'),
|
||||
util = require('util'),
|
||||
@@ -123,6 +124,9 @@ function testConfig(backupConfig, callback) {
|
||||
|
||||
if (backupConfig.format !== 'tgz' && backupConfig.format !== 'rsync') return callback(new BackupsError(BackupsError.BAD_FIELD, 'unknown format'));
|
||||
|
||||
// remember to adjust the cron ensureBackup task interval accordingly
|
||||
if (backupConfig.intervalSecs < 6 * 60 * 60) return callback(new BackupsError(BackupsError.BAD_FIELD, 'Interval must be atleast 6 hours'));
|
||||
|
||||
api(backupConfig.provider).testConfig(backupConfig, callback);
|
||||
}
|
||||
|
||||
@@ -157,7 +161,7 @@ function get(backupId, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
backupdb.get(backupId, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new BackupsError(BackupsError.NOT_FOUND, error));
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new BackupsError(BackupsError.NOT_FOUND));
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, result);
|
||||
@@ -182,6 +186,92 @@ function log(detail) {
|
||||
progress.setDetail(progress.BACKUP, detail);
|
||||
}
|
||||
|
||||
function encryptFilePath(filePath, key) {
|
||||
assert.strictEqual(typeof filePath, 'string');
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
|
||||
var encryptedParts = filePath.split('/').map(function (part) {
|
||||
const cipher = crypto.createCipher('aes-256-cbc', key);
|
||||
let crypt = cipher.update(part);
|
||||
crypt = Buffer.concat([ crypt, cipher.final() ]);
|
||||
|
||||
return crypt.toString('base64') // ensures path is valid
|
||||
.replace(/\//g, '-') // replace '/' of base64 since it conflicts with path separator
|
||||
.replace(/=/g,''); // strip trailing = padding. this is only needed if we concat base64 strings, which we don't
|
||||
});
|
||||
|
||||
return encryptedParts.join('/');
|
||||
}
|
||||
|
||||
function decryptFilePath(filePath, key) {
|
||||
assert.strictEqual(typeof filePath, 'string');
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
|
||||
let decryptedParts = [];
|
||||
for (let part of filePath.split('/')) {
|
||||
part = part + Array(part.length % 4).join('='); // add back = padding
|
||||
part = part.replace(/-/g, '/'); // replace with '/'
|
||||
|
||||
try {
|
||||
let decrypt = crypto.createDecipher('aes-256-cbc', key);
|
||||
let text = decrypt.update(Buffer.from(part, 'base64'));
|
||||
text = Buffer.concat([ text, decrypt.final() ]);
|
||||
decryptedParts.push(text.toString('utf8'));
|
||||
} catch (error) {
|
||||
debug(`Error decrypting file ${filePath} part ${part}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return decryptedParts.join('/');
|
||||
}
|
||||
|
||||
function createReadStream(sourceFile, key) {
|
||||
assert.strictEqual(typeof sourceFile, 'string');
|
||||
assert(key === null || typeof key === 'string');
|
||||
|
||||
var stream = fs.createReadStream(sourceFile);
|
||||
var ps = progressStream({ time: 10000 }); // display a progress every 10 seconds
|
||||
|
||||
stream.on('error', function (error) {
|
||||
debug('createReadStream: tar stream error.', error);
|
||||
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||
});
|
||||
|
||||
ps.on('progress', function(progress) {
|
||||
debug('createReadStream: %s@%s (%s)', Math.round(progress.transferred/1024/1024) + 'M', Math.round(progress.speed/1024/1024) + 'Mbps', sourceFile);
|
||||
});
|
||||
|
||||
if (key !== null) {
|
||||
var encrypt = crypto.createCipher('aes-256-cbc', key);
|
||||
encrypt.on('error', function (error) {
|
||||
debug('createReadStream: encrypt stream error.', error);
|
||||
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||
});
|
||||
return stream.pipe(encrypt).pipe(ps);
|
||||
} else {
|
||||
return stream.pipe(ps);
|
||||
}
|
||||
}
|
||||
|
||||
function createWriteStream(destFile, key) {
|
||||
assert.strictEqual(typeof destFile, 'string');
|
||||
assert(key === null || typeof key === 'string');
|
||||
|
||||
var stream = fs.createWriteStream(destFile);
|
||||
|
||||
if (key !== null) {
|
||||
var decrypt = crypto.createDecipher('aes-256-cbc', key);
|
||||
decrypt.on('error', function (error) {
|
||||
debug('createWriteStream: decrypt stream error.', error);
|
||||
});
|
||||
decrypt.pipe(stream);
|
||||
return decrypt;
|
||||
} else {
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
|
||||
function createTarPackStream(sourceDir, key) {
|
||||
assert.strictEqual(typeof sourceDir, 'string');
|
||||
assert(key === null || typeof key === 'string');
|
||||
@@ -232,21 +322,23 @@ function sync(backupConfig, backupId, dataDir, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
function setBackupProgress(message) {
|
||||
debug('%s: %s', (new Date()).toISOString(), message);
|
||||
debug('%s', message);
|
||||
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, message);
|
||||
}
|
||||
|
||||
syncer.sync(dataDir, function processTask(task, iteratorCallback) {
|
||||
debug('sync: processing task: %j', task);
|
||||
var backupFilePath = path.join(getBackupFilePath(backupConfig, backupId, backupConfig.format), task.path);
|
||||
// the empty task.path is special to signify the directory
|
||||
const destPath = task.path && backupConfig.key ? encryptFilePath(task.path, backupConfig.key) : task.path;
|
||||
const backupFilePath = path.join(getBackupFilePath(backupConfig, backupId, backupConfig.format), destPath);
|
||||
|
||||
if (task.operation === 'removedir') {
|
||||
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, `Removing directory ${task.path}`);
|
||||
setBackupProgress(`Removing directory ${backupFilePath}`);
|
||||
return api(backupConfig.provider).removeDir(backupConfig, backupFilePath)
|
||||
.on('progress', setBackupProgress)
|
||||
.on('done', iteratorCallback);
|
||||
} else if (task.operation === 'remove') {
|
||||
setBackupProgress(`Removing ${task.path}`);
|
||||
setBackupProgress(`Removing ${backupFilePath}`);
|
||||
return api(backupConfig.provider).remove(backupConfig, backupFilePath, iteratorCallback);
|
||||
}
|
||||
|
||||
@@ -258,7 +350,7 @@ function sync(backupConfig, backupId, dataDir, callback) {
|
||||
debug(`${task.operation} ${task.path} try ${retryCount}`);
|
||||
if (task.operation === 'add') {
|
||||
setBackupProgress(`Adding ${task.path} position ${task.position} try ${retryCount}`);
|
||||
var stream = fs.createReadStream(path.join(dataDir, task.path));
|
||||
var stream = createReadStream(path.join(dataDir, task.path), backupConfig.key || null);
|
||||
stream.on('error', function (error) {
|
||||
setBackupProgress(`read stream error for ${task.path}: ${error.message}`);
|
||||
retryCallback();
|
||||
@@ -366,7 +458,7 @@ function tarExtract(inStream, destination, key, callback) {
|
||||
var decrypt = crypto.createDecipher('aes-256-cbc', key);
|
||||
decrypt.on('error', function (error) {
|
||||
debug('tarExtract: decrypt stream error.', error);
|
||||
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, `Failed to decrypt: ${error.message}`));
|
||||
});
|
||||
inStream.pipe(ps).pipe(decrypt).pipe(gunzip).pipe(extract);
|
||||
} else {
|
||||
@@ -400,6 +492,50 @@ function restoreFsMetadata(appDataDir, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function downloadDir(backupConfig, backupFilePath, destDir, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof backupFilePath, 'string');
|
||||
assert.strictEqual(typeof destDir, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`downloadDir: ${backupFilePath} to ${destDir}`);
|
||||
|
||||
function downloadFile(entry, callback) {
|
||||
let relativePath = path.relative(backupFilePath, entry.fullPath);
|
||||
if (backupConfig.key) {
|
||||
relativePath = decryptFilePath(relativePath, backupConfig.key);
|
||||
if (!relativePath) return callback(new BackupsError(BackupsError.BAD_STATE, 'Unable to decrypt file'));
|
||||
}
|
||||
const destFilePath = path.join(destDir, relativePath);
|
||||
|
||||
mkdirp(path.dirname(destFilePath), function (error) {
|
||||
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||
|
||||
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
|
||||
let destStream = createWriteStream(destFilePath, backupConfig.key || null);
|
||||
|
||||
// protect against multiple errors. must destroy the write stream so that a previous retry does not write
|
||||
let closeAndRetry = once((error) => { destStream.destroy(); retryCallback(error); });
|
||||
|
||||
api(backupConfig.provider).download(backupConfig, entry.fullPath, function (error, sourceStream) {
|
||||
if (error) return closeAndRetry(error);
|
||||
|
||||
sourceStream.on('error', closeAndRetry);
|
||||
destStream.on('error', closeAndRetry);
|
||||
|
||||
debug(`Downloading ${entry.fullPath} to ${destFilePath}`);
|
||||
|
||||
sourceStream.pipe(destStream, { end: true }).on('finish', closeAndRetry);
|
||||
});
|
||||
}, callback);
|
||||
});
|
||||
}
|
||||
|
||||
api(backupConfig.provider).listDir(backupConfig, backupFilePath, 1000, function (entries, done) {
|
||||
async.each(entries, downloadFile, done);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function download(backupConfig, backupId, format, dataDir, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
@@ -418,9 +554,7 @@ function download(backupConfig, backupId, format, dataDir, callback) {
|
||||
tarExtract(sourceStream, dataDir, backupConfig.key || null, callback);
|
||||
});
|
||||
} else {
|
||||
var events = api(backupConfig.provider).downloadDir(backupConfig, getBackupFilePath(backupConfig, backupId, format), dataDir);
|
||||
events.on('progress', log);
|
||||
events.on('done', function (error) {
|
||||
downloadDir(backupConfig, getBackupFilePath(backupConfig, backupId, format), dataDir, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
restoreFsMetadata(dataDir, callback);
|
||||
@@ -436,9 +570,13 @@ function restore(backupConfig, backupId, callback) {
|
||||
download(backupConfig, backupId, backupConfig.format, paths.BOX_DATA_DIR, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('restore: download completed, importing database');
|
||||
|
||||
database.importFromFile(`${paths.BOX_DATA_DIR}/box.mysqldump`, function (error) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
debug('restore: database imported');
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
@@ -562,6 +700,34 @@ function uploadBoxSnapshot(backupConfig, 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, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof timestamp, 'string');
|
||||
@@ -591,8 +757,7 @@ function rotateBoxBackup(backupConfig, timestamp, appBackupIds, callback) {
|
||||
|
||||
log(`Rotated box backup successfully as id ${backupId}`);
|
||||
|
||||
// FIXME this is only needed for caas, hopefully we can remove that in the future
|
||||
api(backupConfig.provider).backupDone(backupConfig, backupId, appBackupIds, function (error) {
|
||||
backupDone(backupConfig, backupId, appBackupIds, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, backupId);
|
||||
@@ -828,15 +993,19 @@ function ensureBackup(auditSource, callback) {
|
||||
getByStatePaged(backupdb.BACKUP_STATE_NORMAL, 1, 1, function (error, backups) {
|
||||
if (error) {
|
||||
debug('Unable to list backups', error);
|
||||
return callback(error); // no point trying to backup if appstore is down
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
if (backups.length !== 0 && (new Date() - new Date(backups[0].creationTime) < 23 * 60 * 60 * 1000)) { // ~1 day ago
|
||||
debug('Previous backup was %j, no need to backup now', backups[0]);
|
||||
return callback(null);
|
||||
}
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
backup(auditSource, callback);
|
||||
if (backups.length !== 0 && (new Date() - new Date(backups[0].creationTime) < (backupConfig.intervalSecs - 3600) * 1000)) { // adjust 1 hour
|
||||
debug('Previous backup was %j, no need to backup now', backups[0]);
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
backup(auditSource, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
crypto = require('crypto'),
|
||||
debug = require('debug')('box:cert/acme'),
|
||||
debug = require('debug')('box:cert/acme1'),
|
||||
execSync = require('safetydance').child_process.execSync,
|
||||
fs = require('fs'),
|
||||
parseLinks = require('parse-links'),
|
||||
@@ -25,7 +25,7 @@ exports = module.exports = {
|
||||
_name: 'acme'
|
||||
};
|
||||
|
||||
function AcmeError(reason, errorOrMessage) {
|
||||
function Acme1Error(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
@@ -43,18 +43,18 @@ function AcmeError(reason, errorOrMessage) {
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(AcmeError, Error);
|
||||
AcmeError.INTERNAL_ERROR = 'Internal Error';
|
||||
AcmeError.EXTERNAL_ERROR = 'External Error';
|
||||
AcmeError.ALREADY_EXISTS = 'Already Exists';
|
||||
AcmeError.NOT_COMPLETED = 'Not Completed';
|
||||
AcmeError.FORBIDDEN = 'Forbidden';
|
||||
util.inherits(Acme1Error, Error);
|
||||
Acme1Error.INTERNAL_ERROR = 'Internal Error';
|
||||
Acme1Error.EXTERNAL_ERROR = 'External Error';
|
||||
Acme1Error.ALREADY_EXISTS = 'Already Exists';
|
||||
Acme1Error.NOT_COMPLETED = 'Not Completed';
|
||||
Acme1Error.FORBIDDEN = 'Forbidden';
|
||||
|
||||
// http://jose.readthedocs.org/en/latest/
|
||||
// https://www.ietf.org/proceedings/92/slides/slides-92-acme-1.pdf
|
||||
// https://community.letsencrypt.org/t/list-of-client-implementations/2103
|
||||
|
||||
function Acme(options) {
|
||||
function Acme1(options) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
|
||||
this.caOrigin = options.prod ? CA_PROD : CA_STAGING;
|
||||
@@ -62,7 +62,7 @@ function Acme(options) {
|
||||
this.email = options.email;
|
||||
}
|
||||
|
||||
Acme.prototype.getNonce = function (callback) {
|
||||
Acme1.prototype.getNonce = function (callback) {
|
||||
superagent.get(this.caOrigin + '/directory').timeout(30 * 1000).end(function (error, response) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (response.statusCode !== 200) return callback(new Error('Invalid response code when fetching nonce : ' + response.statusCode));
|
||||
@@ -91,7 +91,7 @@ function getModulus(pem) {
|
||||
return Buffer.from(match[1], 'hex');
|
||||
}
|
||||
|
||||
Acme.prototype.sendSignedRequest = function (url, payload, callback) {
|
||||
Acme1.prototype.sendSignedRequest = function (url, payload, callback) {
|
||||
assert.strictEqual(typeof url, 'string');
|
||||
assert.strictEqual(typeof payload, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -136,7 +136,7 @@ Acme.prototype.sendSignedRequest = function (url, payload, callback) {
|
||||
});
|
||||
};
|
||||
|
||||
Acme.prototype.updateContact = function (registrationUri, callback) {
|
||||
Acme1.prototype.updateContact = function (registrationUri, callback) {
|
||||
assert.strictEqual(typeof registrationUri, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -151,8 +151,8 @@ Acme.prototype.updateContact = function (registrationUri, callback) {
|
||||
|
||||
var that = this;
|
||||
this.sendSignedRequest(registrationUri, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
|
||||
if (result.statusCode !== 202) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to update contact. Expecting 202, got %s %s', result.statusCode, result.text)));
|
||||
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
|
||||
if (result.statusCode !== 202) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to update contact. Expecting 202, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
debug('updateContact: contact of user updated to %s', that.email);
|
||||
|
||||
@@ -160,7 +160,7 @@ Acme.prototype.updateContact = function (registrationUri, callback) {
|
||||
});
|
||||
};
|
||||
|
||||
Acme.prototype.registerUser = function (callback) {
|
||||
Acme1.prototype.registerUser = function (callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var payload = {
|
||||
@@ -173,9 +173,9 @@ Acme.prototype.registerUser = function (callback) {
|
||||
|
||||
var that = this;
|
||||
this.sendSignedRequest(this.caOrigin + '/acme/new-reg', JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
|
||||
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
|
||||
if (result.statusCode === 409) return that.updateContact(result.headers.location, callback); // already exists
|
||||
if (result.statusCode !== 201) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
if (result.statusCode !== 201) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
debug('registerUser: registered user %s', that.email);
|
||||
|
||||
@@ -183,7 +183,7 @@ Acme.prototype.registerUser = function (callback) {
|
||||
});
|
||||
};
|
||||
|
||||
Acme.prototype.registerDomain = function (domain, callback) {
|
||||
Acme1.prototype.registerDomain = function (domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -198,9 +198,9 @@ Acme.prototype.registerDomain = function (domain, callback) {
|
||||
debug('registerDomain: %s', domain);
|
||||
|
||||
this.sendSignedRequest(this.caOrigin + '/acme/new-authz', JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when registering domain: ' + error.message));
|
||||
if (result.statusCode === 403) return callback(new AcmeError(AcmeError.FORBIDDEN, result.body.detail));
|
||||
if (result.statusCode !== 201) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when registering domain: ' + error.message));
|
||||
if (result.statusCode === 403) return callback(new Acme1Error(Acme1Error.FORBIDDEN, result.body.detail));
|
||||
if (result.statusCode !== 201) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
debug('registerDomain: registered %s', domain);
|
||||
|
||||
@@ -208,7 +208,7 @@ Acme.prototype.registerDomain = function (domain, callback) {
|
||||
});
|
||||
};
|
||||
|
||||
Acme.prototype.prepareHttpChallenge = function (challenge, callback) {
|
||||
Acme1.prototype.prepareHttpChallenge = function (challenge, callback) {
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -232,13 +232,13 @@ Acme.prototype.prepareHttpChallenge = function (challenge, callback) {
|
||||
debug('prepareHttpChallenge: writing %s to %s', keyAuthorization, path.join(paths.ACME_CHALLENGES_DIR, token));
|
||||
|
||||
fs.writeFile(path.join(paths.ACME_CHALLENGES_DIR, token), token + '.' + thumbprint, function (error) {
|
||||
if (error) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, error));
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
Acme.prototype.notifyChallengeReady = function (challenge, callback) {
|
||||
Acme1.prototype.notifyChallengeReady = function (challenge, callback) {
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -252,14 +252,14 @@ Acme.prototype.notifyChallengeReady = function (challenge, callback) {
|
||||
};
|
||||
|
||||
this.sendSignedRequest(challenge.uri, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when notifying challenge: ' + error.message));
|
||||
if (result.statusCode !== 202) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 202, got %s %s', result.statusCode, result.text)));
|
||||
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when notifying challenge: ' + error.message));
|
||||
if (result.statusCode !== 202) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 202, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
Acme.prototype.waitForChallenge = function (challenge, callback) {
|
||||
Acme1.prototype.waitForChallenge = function (challenge, callback) {
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -271,18 +271,18 @@ Acme.prototype.waitForChallenge = function (challenge, callback) {
|
||||
superagent.get(challenge.uri).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) {
|
||||
debug('waitForChallenge: network error getting uri %s', challenge.uri);
|
||||
return retryCallback(new AcmeError(AcmeError.EXTERNAL_ERROR, error.message)); // network error
|
||||
return retryCallback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, error.message)); // network error
|
||||
}
|
||||
if (result.statusCode !== 202) {
|
||||
debug('waitForChallenge: invalid response code getting uri %s', result.statusCode);
|
||||
return retryCallback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
|
||||
return retryCallback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
|
||||
}
|
||||
|
||||
debug('waitForChallenge: status is "%s %j', result.body.status, result.body);
|
||||
|
||||
if (result.body.status === 'pending') return retryCallback(new AcmeError(AcmeError.NOT_COMPLETED));
|
||||
if (result.body.status === 'pending') return retryCallback(new Acme1Error(Acme1Error.NOT_COMPLETED));
|
||||
else if (result.body.status === 'valid') return retryCallback();
|
||||
else return retryCallback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Unexpected status: ' + result.body.status));
|
||||
else return retryCallback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Unexpected status: ' + result.body.status));
|
||||
});
|
||||
}, function retryFinished(error) {
|
||||
// async.retry will pass 'undefined' as second arg making it unusable with async.waterfall()
|
||||
@@ -291,7 +291,7 @@ Acme.prototype.waitForChallenge = function (challenge, callback) {
|
||||
};
|
||||
|
||||
// https://community.letsencrypt.org/t/public-beta-rate-limits/4772 for rate limits
|
||||
Acme.prototype.signCertificate = function (domain, csrDer, callback) {
|
||||
Acme1.prototype.signCertificate = function (domain, csrDer, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert(util.isBuffer(csrDer));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -306,13 +306,13 @@ Acme.prototype.signCertificate = function (domain, csrDer, callback) {
|
||||
debug('signCertificate: sending new-cert request');
|
||||
|
||||
this.sendSignedRequest(this.caOrigin + '/acme/new-cert', JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when signing certificate: ' + error.message));
|
||||
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when signing certificate: ' + error.message));
|
||||
// 429 means we reached the cert limit for this domain
|
||||
if (result.statusCode !== 201) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
if (result.statusCode !== 201) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
var certUrl = result.headers.location;
|
||||
|
||||
if (!certUrl) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Missing location in downloadCertificate'));
|
||||
if (!certUrl) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Missing location in downloadCertificate'));
|
||||
|
||||
safe.fs.writeFileSync(path.join(outdir, domain + '.url'), certUrl, 'utf8'); // maybe use for renewal
|
||||
|
||||
@@ -320,7 +320,7 @@ Acme.prototype.signCertificate = function (domain, csrDer, callback) {
|
||||
});
|
||||
};
|
||||
|
||||
Acme.prototype.createKeyAndCsr = function (domain, callback) {
|
||||
Acme1.prototype.createKeyAndCsr = function (domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -333,15 +333,15 @@ Acme.prototype.createKeyAndCsr = function (domain, callback) {
|
||||
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
|
||||
} else {
|
||||
var key = execSync('openssl genrsa 4096');
|
||||
if (!key) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||
if (!key) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
|
||||
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
|
||||
|
||||
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
|
||||
}
|
||||
|
||||
var csrDer = execSync(util.format('openssl req -new -key %s -outform DER -subj /CN=%s', privateKeyFile, domain));
|
||||
if (!csrDer) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error)); // bookkeeping
|
||||
if (!csrDer) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
|
||||
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error)); // bookkeeping
|
||||
|
||||
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFile);
|
||||
|
||||
@@ -349,13 +349,13 @@ Acme.prototype.createKeyAndCsr = function (domain, callback) {
|
||||
};
|
||||
|
||||
// TODO: download the chain in a loop following 'up' header
|
||||
Acme.prototype.downloadChain = function (linkHeader, callback) {
|
||||
if (!linkHeader) return new AcmeError(AcmeError.EXTERNAL_ERROR, 'Empty link header when downloading certificate chain');
|
||||
Acme1.prototype.downloadChain = function (linkHeader, callback) {
|
||||
if (!linkHeader) return new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Empty link header when downloading certificate chain');
|
||||
|
||||
debug('downloadChain: linkHeader %s', linkHeader);
|
||||
|
||||
var linkInfo = parseLinks(linkHeader);
|
||||
if (!linkInfo || !linkInfo.up) return new AcmeError(AcmeError.EXTERNAL_ERROR, 'Failed to parse link header when downloading certificate chain');
|
||||
if (!linkInfo || !linkInfo.up) return new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Failed to parse link header when downloading certificate chain');
|
||||
|
||||
var intermediateCertUrl = linkInfo.up.startsWith('https://') ? linkInfo.up : (this.caOrigin + linkInfo.up);
|
||||
|
||||
@@ -366,18 +366,18 @@ Acme.prototype.downloadChain = function (linkHeader, callback) {
|
||||
res.on('data', function(chunk) { data.push(chunk); });
|
||||
res.on('end', function () { res.text = Buffer.concat(data); done(); });
|
||||
}).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when downloading certificate'));
|
||||
if (result.statusCode !== 200) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
if (error && !error.response) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when downloading certificate'));
|
||||
if (result.statusCode !== 200) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
var chainDer = result.text;
|
||||
var chainPem = execSync('openssl x509 -inform DER -outform PEM', { input: chainDer }); // this is really just base64 encoding with header
|
||||
if (!chainPem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||
if (!chainPem) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
|
||||
|
||||
callback(null, chainPem);
|
||||
});
|
||||
};
|
||||
|
||||
Acme.prototype.downloadCertificate = function (domain, certUrl, callback) {
|
||||
Acme1.prototype.downloadCertificate = function (domain, certUrl, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof certUrl, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -390,9 +390,9 @@ Acme.prototype.downloadCertificate = function (domain, certUrl, callback) {
|
||||
res.on('data', function(chunk) { data.push(chunk); });
|
||||
res.on('end', function () { res.text = Buffer.concat(data); done(); });
|
||||
}).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when downloading certificate'));
|
||||
if (result.statusCode === 202) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, 'Retry not implemented yet'));
|
||||
if (result.statusCode !== 200) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
if (error && !error.response) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when downloading certificate'));
|
||||
if (result.statusCode === 202) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, 'Retry not implemented yet'));
|
||||
if (result.statusCode !== 200) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
var certificateDer = result.text;
|
||||
|
||||
@@ -400,14 +400,14 @@ Acme.prototype.downloadCertificate = function (domain, certUrl, callback) {
|
||||
debug('downloadCertificate: cert der file for %s saved', domain);
|
||||
|
||||
var certificatePem = execSync('openssl x509 -inform DER -outform PEM', { input: certificateDer }); // this is really just base64 encoding with header
|
||||
if (!certificatePem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||
if (!certificatePem) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
|
||||
|
||||
that.downloadChain(result.header['link'], function (error, chainPem) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var certificateFile = path.join(outdir, domain + '.cert');
|
||||
var fullChainPem = Buffer.concat([certificatePem, chainPem]);
|
||||
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
|
||||
|
||||
debug('downloadCertificate: cert file for %s saved at %s', domain, certificateFile);
|
||||
|
||||
@@ -416,14 +416,14 @@ Acme.prototype.downloadCertificate = function (domain, certUrl, callback) {
|
||||
});
|
||||
};
|
||||
|
||||
Acme.prototype.acmeFlow = function (domain, callback) {
|
||||
Acme1.prototype.acmeFlow = function (domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!fs.existsSync(paths.ACME_ACCOUNT_KEY_FILE)) {
|
||||
debug('getCertificate: generating acme account key on first run');
|
||||
this.accountKeyPem = safe.child_process.execSync('openssl genrsa 4096');
|
||||
if (!this.accountKeyPem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||
if (!this.accountKeyPem) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
|
||||
|
||||
safe.fs.writeFileSync(paths.ACME_ACCOUNT_KEY_FILE, this.accountKeyPem);
|
||||
} else {
|
||||
@@ -441,7 +441,7 @@ Acme.prototype.acmeFlow = function (domain, callback) {
|
||||
debug('acmeFlow: challenges: %j', result);
|
||||
|
||||
var httpChallenges = result.challenges.filter(function(x) { return x.type === 'http-01'; });
|
||||
if (httpChallenges.length === 0) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'no http challenges'));
|
||||
if (httpChallenges.length === 0) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'no http challenges'));
|
||||
var challenge = httpChallenges[0];
|
||||
|
||||
async.waterfall([
|
||||
@@ -456,24 +456,26 @@ Acme.prototype.acmeFlow = function (domain, callback) {
|
||||
});
|
||||
};
|
||||
|
||||
Acme.prototype.getCertificate = function (domain, callback) {
|
||||
Acme1.prototype.getCertificate = function (hostname, domain, callback) {
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('getCertificate: start acme flow for %s from %s', domain, this.caOrigin);
|
||||
this.acmeFlow(domain, function (error) {
|
||||
debug('getCertificate: start acme flow for %s from %s', hostname, this.caOrigin);
|
||||
this.acmeFlow(hostname, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var outdir = paths.APP_CERTS_DIR;
|
||||
callback(null, path.join(outdir, domain + '.cert'), path.join(outdir, domain + '.key'));
|
||||
callback(null, path.join(outdir, hostname + '.cert'), path.join(outdir, hostname + '.key'));
|
||||
});
|
||||
};
|
||||
|
||||
function getCertificate(domain, options, callback) {
|
||||
function getCertificate(hostname, domain, options, callback) {
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var acme = new Acme(options || { });
|
||||
acme.getCertificate(domain, callback);
|
||||
var acme = new Acme1(options || { });
|
||||
acme.getCertificate(hostname, domain, callback);
|
||||
}
|
||||
@@ -0,0 +1,637 @@
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
crypto = require('crypto'),
|
||||
debug = require('debug')('box:cert/acme2'),
|
||||
domains = require('../domains.js'),
|
||||
execSync = require('safetydance').child_process.execSync,
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
paths = require('../paths.js'),
|
||||
safe = require('safetydance'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
const CA_PROD_DIRECTORY_URL = 'https://acme-v02.api.letsencrypt.org/directory',
|
||||
CA_STAGING_DIRECTORY_URL = 'https://acme-staging-v02.api.letsencrypt.org/directory';
|
||||
|
||||
exports = module.exports = {
|
||||
getCertificate: getCertificate,
|
||||
|
||||
// testing
|
||||
_name: 'acme',
|
||||
_getChallengeSubdomain: getChallengeSubdomain
|
||||
};
|
||||
|
||||
function Acme2Error(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(Acme2Error, Error);
|
||||
Acme2Error.INTERNAL_ERROR = 'Internal Error';
|
||||
Acme2Error.EXTERNAL_ERROR = 'External Error';
|
||||
Acme2Error.ALREADY_EXISTS = 'Already Exists';
|
||||
Acme2Error.NOT_COMPLETED = 'Not Completed';
|
||||
Acme2Error.FORBIDDEN = 'Forbidden';
|
||||
|
||||
// http://jose.readthedocs.org/en/latest/
|
||||
// https://www.ietf.org/proceedings/92/slides/slides-92-acme-1.pdf
|
||||
// https://community.letsencrypt.org/t/list-of-client-implementations/2103
|
||||
|
||||
function Acme2(options) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
|
||||
this.accountKeyPem = null; // Buffer
|
||||
this.email = options.email;
|
||||
this.keyId = null;
|
||||
this.caDirectory = options.prod ? CA_PROD_DIRECTORY_URL : CA_STAGING_DIRECTORY_URL;
|
||||
this.directory = {};
|
||||
this.performHttpAuthorization = !!options.performHttpAuthorization;
|
||||
this.wildcard = !!options.wildcard;
|
||||
}
|
||||
|
||||
Acme2.prototype.getNonce = function (callback) {
|
||||
superagent.get(this.directory.newNonce).timeout(30 * 1000).end(function (error, response) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (response.statusCode !== 204) return callback(new Error('Invalid response code when fetching nonce : ' + response.statusCode));
|
||||
|
||||
return callback(null, response.headers['Replay-Nonce'.toLowerCase()]);
|
||||
});
|
||||
};
|
||||
|
||||
// urlsafe base64 encoding (jose)
|
||||
function urlBase64Encode(string) {
|
||||
return string.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
||||
}
|
||||
|
||||
function b64(str) {
|
||||
var buf = util.isBuffer(str) ? str : new Buffer(str);
|
||||
return urlBase64Encode(buf.toString('base64'));
|
||||
}
|
||||
|
||||
function getModulus(pem) {
|
||||
assert(util.isBuffer(pem));
|
||||
|
||||
var stdout = execSync('openssl rsa -modulus -noout', { input: pem, encoding: 'utf8' });
|
||||
if (!stdout) return null;
|
||||
var match = stdout.match(/Modulus=([0-9a-fA-F]+)$/m);
|
||||
if (!match) return null;
|
||||
return Buffer.from(match[1], 'hex');
|
||||
}
|
||||
|
||||
Acme2.prototype.sendSignedRequest = function (url, payload, callback) {
|
||||
assert.strictEqual(typeof url, 'string');
|
||||
assert.strictEqual(typeof payload, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
assert(util.isBuffer(this.accountKeyPem));
|
||||
|
||||
const that = this;
|
||||
let header = {
|
||||
url: url,
|
||||
alg: 'RS256'
|
||||
};
|
||||
|
||||
// keyId is null when registering account
|
||||
if (this.keyId) {
|
||||
header.kid = this.keyId;
|
||||
} else {
|
||||
header.jwk = {
|
||||
e: b64(Buffer.from([0x01, 0x00, 0x01])), // exponent - 65537
|
||||
kty: 'RSA',
|
||||
n: b64(getModulus(this.accountKeyPem))
|
||||
};
|
||||
}
|
||||
|
||||
var payload64 = b64(payload);
|
||||
|
||||
this.getNonce(function (error, nonce) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('sendSignedRequest: using nonce %s for url %s', nonce, url);
|
||||
|
||||
var protected64 = b64(JSON.stringify(_.extend({ }, header, { nonce: nonce })));
|
||||
|
||||
var signer = crypto.createSign('RSA-SHA256');
|
||||
signer.update(protected64 + '.' + payload64, 'utf8');
|
||||
var signature64 = urlBase64Encode(signer.sign(that.accountKeyPem, 'base64'));
|
||||
|
||||
var data = {
|
||||
protected: protected64,
|
||||
payload: payload64,
|
||||
signature: signature64
|
||||
};
|
||||
|
||||
superagent.post(url).set('Content-Type', 'application/jose+json').set('User-Agent', 'acme-cloudron').send(JSON.stringify(data)).timeout(30 * 1000).end(function (error, res) {
|
||||
if (error && !error.response) return callback(error); // network errors
|
||||
|
||||
callback(null, res);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Acme2.prototype.updateContact = function (registrationUri, callback) {
|
||||
assert.strictEqual(typeof registrationUri, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`updateContact: registrationUri: ${registrationUri} email: ${this.email}`);
|
||||
|
||||
// https://github.com/ietf-wg-acme/acme/issues/30
|
||||
const payload = {
|
||||
contact: [ 'mailto:' + this.email ]
|
||||
};
|
||||
|
||||
const that = this;
|
||||
this.sendSignedRequest(registrationUri, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
|
||||
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to update contact. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
debug(`updateContact: contact of user updated to ${that.email}`);
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
Acme2.prototype.registerUser = function (callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var payload = {
|
||||
termsOfServiceAgreed: true
|
||||
};
|
||||
|
||||
debug('registerUser: registering user');
|
||||
|
||||
var that = this;
|
||||
this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when registering new account: ' + error.message));
|
||||
// 200 if already exists. 201 for new accounts
|
||||
if (result.statusCode !== 200 && result.statusCode !== 201) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to register new account. Expecting 200 or 201, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
debug(`registerUser: user registered keyid: ${result.headers.location}`);
|
||||
|
||||
that.keyId = result.headers.location;
|
||||
|
||||
that.updateContact(result.headers.location, callback);
|
||||
});
|
||||
};
|
||||
|
||||
Acme2.prototype.newOrder = function (domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var payload = {
|
||||
identifiers: [{
|
||||
type: 'dns',
|
||||
value: domain
|
||||
}]
|
||||
};
|
||||
|
||||
debug('newOrder: %s', domain);
|
||||
|
||||
this.sendSignedRequest(this.directory.newOrder, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when registering domain: ' + error.message));
|
||||
if (result.statusCode === 403) return callback(new Acme2Error(Acme2Error.FORBIDDEN, result.body.detail));
|
||||
if (result.statusCode !== 201) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
debug('newOrder: created order %s %j', domain, result.body);
|
||||
|
||||
const order = result.body, orderUrl = result.headers.location;
|
||||
|
||||
if (!Array.isArray(order.authorizations)) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'invalid authorizations in order'));
|
||||
if (typeof order.finalize !== 'string') return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'invalid finalize in order'));
|
||||
if (typeof orderUrl !== 'string') return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'invalid order location in order header'));
|
||||
|
||||
callback(null, order, orderUrl);
|
||||
});
|
||||
};
|
||||
|
||||
Acme2.prototype.waitForOrder = function (orderUrl, callback) {
|
||||
assert.strictEqual(typeof orderUrl, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`waitForOrder: ${orderUrl}`);
|
||||
|
||||
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
|
||||
debug('waitForOrder: getting status');
|
||||
|
||||
superagent.get(orderUrl).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) {
|
||||
debug('waitForOrder: network error getting uri %s', orderUrl);
|
||||
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message)); // network error
|
||||
}
|
||||
if (result.statusCode !== 200) {
|
||||
debug('waitForOrder: invalid response code getting uri %s', result.statusCode);
|
||||
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
|
||||
}
|
||||
|
||||
debug('waitForOrder: status is "%s %j', result.body.status, result.body);
|
||||
|
||||
if (result.body.status === 'pending' || result.body.status === 'processing') return retryCallback(new Acme2Error(Acme2Error.NOT_COMPLETED));
|
||||
else if (result.body.status === 'valid' && result.body.certificate) return retryCallback(null, result.body.certificate);
|
||||
else return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Unexpected status or invalid response: ' + result.body));
|
||||
});
|
||||
}, callback);
|
||||
};
|
||||
|
||||
Acme2.prototype.getKeyAuthorization = function (token) {
|
||||
assert(util.isBuffer(this.accountKeyPem));
|
||||
|
||||
let jwk = {
|
||||
e: b64(Buffer.from([0x01, 0x00, 0x01])), // Exponent - 65537
|
||||
kty: 'RSA',
|
||||
n: b64(getModulus(this.accountKeyPem))
|
||||
};
|
||||
|
||||
let shasum = crypto.createHash('sha256');
|
||||
shasum.update(JSON.stringify(jwk));
|
||||
let thumbprint = urlBase64Encode(shasum.digest('base64'));
|
||||
return token + '.' + thumbprint;
|
||||
};
|
||||
|
||||
Acme2.prototype.notifyChallengeReady = function (challenge, callback) {
|
||||
assert.strictEqual(typeof challenge, 'object'); // { type, status, url, token }
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('notifyChallengeReady: %s was met', challenge.url);
|
||||
|
||||
const keyAuthorization = this.getKeyAuthorization(challenge.token);
|
||||
|
||||
var payload = {
|
||||
resource: 'challenge',
|
||||
keyAuthorization: keyAuthorization
|
||||
};
|
||||
|
||||
this.sendSignedRequest(challenge.url, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when notifying challenge: ' + error.message));
|
||||
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
Acme2.prototype.waitForChallenge = function (challenge, callback) {
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('waitingForChallenge: %j', challenge);
|
||||
|
||||
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
|
||||
debug('waitingForChallenge: getting status');
|
||||
|
||||
superagent.get(challenge.url).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) {
|
||||
debug('waitForChallenge: network error getting uri %s', challenge.url);
|
||||
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message)); // network error
|
||||
}
|
||||
if (result.statusCode !== 200) {
|
||||
debug('waitForChallenge: invalid response code getting uri %s', result.statusCode);
|
||||
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
|
||||
}
|
||||
|
||||
debug('waitForChallenge: status is "%s %j', result.body.status, result.body);
|
||||
|
||||
if (result.body.status === 'pending') return retryCallback(new Acme2Error(Acme2Error.NOT_COMPLETED));
|
||||
else if (result.body.status === 'valid') return retryCallback();
|
||||
else return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Unexpected status: ' + result.body.status));
|
||||
});
|
||||
}, function retryFinished(error) {
|
||||
// async.retry will pass 'undefined' as second arg making it unusable with async.waterfall()
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
// https://community.letsencrypt.org/t/public-beta-rate-limits/4772 for rate limits
|
||||
Acme2.prototype.signCertificate = function (domain, finalizationUrl, csrDer, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof finalizationUrl, 'string');
|
||||
assert(util.isBuffer(csrDer));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const payload = {
|
||||
csr: b64(csrDer)
|
||||
};
|
||||
|
||||
debug('signCertificate: sending sign request');
|
||||
|
||||
this.sendSignedRequest(finalizationUrl, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when signing certificate: ' + error.message));
|
||||
// 429 means we reached the cert limit for this domain
|
||||
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
};
|
||||
|
||||
Acme2.prototype.createKeyAndCsr = function (hostname, callback) {
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var outdir = paths.APP_CERTS_DIR;
|
||||
const certName = hostname.replace('*.', '_.');
|
||||
var csrFile = path.join(outdir, `${certName}.csr`);
|
||||
var privateKeyFile = path.join(outdir, `${certName}.key`);
|
||||
|
||||
if (safe.fs.existsSync(privateKeyFile)) {
|
||||
// in some old releases, csr file was corrupt. so always regenerate it
|
||||
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
|
||||
} else {
|
||||
var key = execSync('openssl genrsa 4096');
|
||||
if (!key) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
|
||||
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
|
||||
|
||||
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
|
||||
}
|
||||
|
||||
var csrDer = execSync(`openssl req -new -key ${privateKeyFile} -outform DER -subj /CN=${hostname}`);
|
||||
if (!csrDer) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
|
||||
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error)); // bookkeeping
|
||||
|
||||
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFile);
|
||||
|
||||
callback(null, csrDer);
|
||||
};
|
||||
|
||||
Acme2.prototype.downloadCertificate = function (hostname, certUrl, callback) {
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
assert.strictEqual(typeof certUrl, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var outdir = paths.APP_CERTS_DIR;
|
||||
|
||||
superagent.get(certUrl).buffer().parse(function (res, done) {
|
||||
var data = [ ];
|
||||
res.on('data', function(chunk) { data.push(chunk); });
|
||||
res.on('end', function () { res.text = Buffer.concat(data); done(); });
|
||||
}).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when downloading certificate'));
|
||||
if (result.statusCode === 202) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, 'Retry not implemented yet'));
|
||||
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
const fullChainPem = result.text;
|
||||
|
||||
const certName = hostname.replace('*.', '_.');
|
||||
var certificateFile = path.join(outdir, `${certName}.cert`);
|
||||
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
|
||||
|
||||
debug('downloadCertificate: cert file for %s saved at %s', hostname, certificateFile);
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
Acme2.prototype.prepareHttpChallenge = function (hostname, domain, authorization, callback) {
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof authorization, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('acmeFlow: challenges: %j', authorization);
|
||||
let httpChallenges = authorization.challenges.filter(function(x) { return x.type === 'http-01'; });
|
||||
if (httpChallenges.length === 0) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'no http challenges'));
|
||||
let challenge = httpChallenges[0];
|
||||
|
||||
debug('prepareHttpChallenge: preparing for challenge %j', challenge);
|
||||
|
||||
let keyAuthorization = this.getKeyAuthorization(challenge.token);
|
||||
|
||||
debug('prepareHttpChallenge: writing %s to %s', keyAuthorization, path.join(paths.ACME_CHALLENGES_DIR, challenge.token));
|
||||
|
||||
fs.writeFile(path.join(paths.ACME_CHALLENGES_DIR, challenge.token), keyAuthorization, function (error) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, challenge);
|
||||
});
|
||||
};
|
||||
|
||||
Acme2.prototype.cleanupHttpChallenge = function (hostname, domain, challenge, callback) {
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('cleanupHttpChallenge: unlinking %s', path.join(paths.ACME_CHALLENGES_DIR, challenge.token));
|
||||
|
||||
fs.unlink(path.join(paths.ACME_CHALLENGES_DIR, challenge.token), callback);
|
||||
};
|
||||
|
||||
function getChallengeSubdomain(hostname, domain) {
|
||||
let challengeSubdomain;
|
||||
|
||||
if (hostname === domain) {
|
||||
challengeSubdomain = '_acme-challenge';
|
||||
} else if (hostname.includes('*')) { // wildcard
|
||||
let subdomain = hostname.slice(0, -domain.length - 1);
|
||||
challengeSubdomain = subdomain ? subdomain.replace('*', '_acme-challenge') : '_acme-challenge';
|
||||
} else {
|
||||
challengeSubdomain = '_acme-challenge.' + hostname.slice(0, -domain.length - 1);
|
||||
}
|
||||
|
||||
debug(`getChallengeSubdomain: challenge subdomain for hostname ${hostname} at domain ${domain} is ${challengeSubdomain}`);
|
||||
|
||||
return challengeSubdomain;
|
||||
}
|
||||
|
||||
Acme2.prototype.prepareDnsChallenge = function (hostname, domain, authorization, callback) {
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof authorization, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('acmeFlow: challenges: %j', authorization);
|
||||
let dnsChallenges = authorization.challenges.filter(function(x) { return x.type === 'dns-01'; });
|
||||
if (dnsChallenges.length === 0) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'no dns challenges'));
|
||||
let challenge = dnsChallenges[0];
|
||||
|
||||
const keyAuthorization = this.getKeyAuthorization(challenge.token);
|
||||
let shasum = crypto.createHash('sha256');
|
||||
shasum.update(keyAuthorization);
|
||||
|
||||
const txtValue = urlBase64Encode(shasum.digest('base64'));
|
||||
let challengeSubdomain = getChallengeSubdomain(hostname, domain);
|
||||
|
||||
debug(`prepareDnsChallenge: update ${challengeSubdomain} with ${txtValue}`);
|
||||
|
||||
domains.upsertDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message));
|
||||
|
||||
domains.waitForDnsRecord(challengeSubdomain, domain, 'TXT', txtValue, { interval: 5000, times: 200 }, function (error) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message));
|
||||
|
||||
callback(null, challenge);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Acme2.prototype.cleanupDnsChallenge = function (hostname, domain, challenge, callback) {
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const keyAuthorization = this.getKeyAuthorization(challenge.token);
|
||||
let shasum = crypto.createHash('sha256');
|
||||
shasum.update(keyAuthorization);
|
||||
|
||||
const txtValue = urlBase64Encode(shasum.digest('base64'));
|
||||
let challengeSubdomain = getChallengeSubdomain(hostname, domain);
|
||||
|
||||
debug(`cleanupDnsChallenge: remove ${challengeSubdomain} with ${txtValue}`);
|
||||
|
||||
domains.removeDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
};
|
||||
|
||||
Acme2.prototype.prepareChallenge = function (hostname, domain, authorizationUrl, callback) {
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof authorizationUrl, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const that = this;
|
||||
superagent.get(authorizationUrl).timeout(30 * 1000).end(function (error, response) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (response.statusCode !== 200) return callback(new Error('Invalid response code getting authorization : ' + response.statusCode));
|
||||
|
||||
const authorization = response.body;
|
||||
|
||||
if (that.performHttpAuthorization) {
|
||||
that.prepareHttpChallenge(hostname, domain, authorization, callback);
|
||||
} else {
|
||||
that.prepareDnsChallenge(hostname, domain, authorization, callback);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Acme2.prototype.cleanupChallenge = function (hostname, domain, challenge, callback) {
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (this.performHttpAuthorization) {
|
||||
this.cleanupHttpChallenge(hostname, domain, challenge, callback);
|
||||
} else {
|
||||
this.cleanupDnsChallenge(hostname, domain, challenge, callback);
|
||||
}
|
||||
};
|
||||
|
||||
Acme2.prototype.acmeFlow = function (hostname, domain, callback) {
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!fs.existsSync(paths.ACME_ACCOUNT_KEY_FILE)) {
|
||||
debug('getCertificate: generating acme account key on first run');
|
||||
this.accountKeyPem = safe.child_process.execSync('openssl genrsa 4096');
|
||||
if (!this.accountKeyPem) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
|
||||
|
||||
safe.fs.writeFileSync(paths.ACME_ACCOUNT_KEY_FILE, this.accountKeyPem);
|
||||
} else {
|
||||
debug('getCertificate: using existing acme account key');
|
||||
this.accountKeyPem = fs.readFileSync(paths.ACME_ACCOUNT_KEY_FILE);
|
||||
}
|
||||
|
||||
var that = this;
|
||||
this.registerUser(function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
that.newOrder(hostname, function (error, order, orderUrl) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(order.authorizations, function (authorizationUrl, iteratorCallback) {
|
||||
debug(`acmeFlow: authorizing ${authorizationUrl}`);
|
||||
|
||||
that.prepareChallenge(hostname, domain, authorizationUrl, function (error, challenge) {
|
||||
if (error) return iteratorCallback(error);
|
||||
|
||||
async.waterfall([
|
||||
that.notifyChallengeReady.bind(that, challenge),
|
||||
that.waitForChallenge.bind(that, challenge),
|
||||
that.createKeyAndCsr.bind(that, hostname),
|
||||
that.signCertificate.bind(that, hostname, order.finalize),
|
||||
that.waitForOrder.bind(that, orderUrl),
|
||||
that.downloadCertificate.bind(that, hostname)
|
||||
], function (error) {
|
||||
that.cleanupChallenge(hostname, domain, challenge, function (cleanupError) {
|
||||
if (cleanupError) debug('acmeFlow: ignoring error when cleaning up challenge:', cleanupError);
|
||||
|
||||
iteratorCallback(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
}, callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Acme2.prototype.getDirectory = function (callback) {
|
||||
const that = this;
|
||||
|
||||
superagent.get(this.caDirectory).timeout(30 * 1000).end(function (error, response) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (response.statusCode !== 200) return callback(new Error('Invalid response code when fetching directory : ' + response.statusCode));
|
||||
|
||||
if (typeof response.body.newNonce !== 'string' ||
|
||||
typeof response.body.newOrder !== 'string' ||
|
||||
typeof response.body.newAccount !== 'string') return callback(new Error(`Invalid response body : ${response.body}`));
|
||||
|
||||
that.directory = response.body;
|
||||
|
||||
callback(null);
|
||||
});
|
||||
};
|
||||
|
||||
Acme2.prototype.getCertificate = function (hostname, domain, callback) {
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`getCertificate: start acme flow for ${hostname} from ${this.caDirectory}`);
|
||||
|
||||
if (hostname !== domain && this.wildcard) { // bare domain is not part of wildcard SAN
|
||||
hostname = domains.makeWildcard(hostname);
|
||||
debug(`getCertificate: will get wildcard cert for ${hostname}`);
|
||||
}
|
||||
|
||||
const that = this;
|
||||
this.getDirectory(function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
that.acmeFlow(hostname, domain, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var outdir = paths.APP_CERTS_DIR;
|
||||
const certName = hostname.replace('*.', '_.');
|
||||
callback(null, path.join(outdir, `${certName}.cert`), path.join(outdir, `${certName}.key`));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function getCertificate(hostname, domain, options, callback) {
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var acme = new Acme2(options || { });
|
||||
acme.getCertificate(hostname, domain, callback);
|
||||
}
|
||||
+4
-3
@@ -10,12 +10,13 @@ exports = module.exports = {
|
||||
var assert = require('assert'),
|
||||
debug = require('debug')('box:cert/caas.js');
|
||||
|
||||
function getCertificate(vhost, options, callback) {
|
||||
assert.strictEqual(typeof vhost, 'string');
|
||||
function getCertificate(hostname, domain, options, callback) {
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('getCertificate: using fallback certificate', vhost);
|
||||
debug('getCertificate: using fallback certificate', hostname);
|
||||
|
||||
return callback(null, '', '');
|
||||
}
|
||||
|
||||
@@ -10,12 +10,13 @@ exports = module.exports = {
|
||||
var assert = require('assert'),
|
||||
debug = require('debug')('box:cert/fallback.js');
|
||||
|
||||
function getCertificate(vhost, options, callback) {
|
||||
assert.strictEqual(typeof vhost, 'string');
|
||||
function getCertificate(hostname, domain, options, callback) {
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('getCertificate: using fallback certificate', vhost);
|
||||
debug('getCertificate: using fallback certificate', hostname);
|
||||
|
||||
return callback(null, '', '');
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ exports = module.exports = {
|
||||
|
||||
var assert = require('assert');
|
||||
|
||||
function getCertificate(domain, options, callback) {
|
||||
function getCertificate(hostname, domain, options, callback) {
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
+43
-18
@@ -37,6 +37,7 @@ var apps = require('./apps.js'),
|
||||
accesscontrol = require('./accesscontrol.js'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
users = require('./users.js'),
|
||||
UsersError = users.UsersError,
|
||||
util = require('util'),
|
||||
uuid = require('uuid');
|
||||
|
||||
@@ -67,7 +68,7 @@ ClientsError.NOT_FOUND = 'Not found';
|
||||
ClientsError.INTERNAL_ERROR = 'Internal Error';
|
||||
ClientsError.NOT_ALLOWED = 'Not allowed to remove this client';
|
||||
|
||||
function validateName(name) {
|
||||
function validateClientName(name) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
|
||||
if (name.length < 1) return new ClientsError(ClientsError.BAD_FIELD, 'Name must be atleast 1 character');
|
||||
@@ -78,6 +79,14 @@ function validateName(name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateTokenName(name) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
|
||||
if (name.length > 64) return new ClientsError(ClientsError.BAD_FIELD, 'Name too long');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function add(appId, type, redirectURI, scope, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
@@ -85,10 +94,10 @@ function add(appId, type, redirectURI, scope, callback) {
|
||||
assert.strictEqual(typeof scope, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = accesscontrol.validateScope(scope);
|
||||
var error = accesscontrol.validateScopeString(scope);
|
||||
if (error) return callback(new ClientsError(ClientsError.INVALID_SCOPE, error.message));
|
||||
|
||||
error = validateName(appId);
|
||||
error = validateClientName(appId);
|
||||
if (error) return callback(error);
|
||||
|
||||
var id = 'cid-' + uuid.v4();
|
||||
@@ -243,44 +252,60 @@ function delByAppIdAndType(appId, type, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function addTokenByUserId(clientId, userId, expiresAt, callback) {
|
||||
function addTokenByUserId(clientId, userId, expiresAt, options, callback) {
|
||||
assert.strictEqual(typeof clientId, 'string');
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof expiresAt, 'number');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const name = options.name || '';
|
||||
let error = validateTokenName(name);
|
||||
if (error) return callback(error);
|
||||
|
||||
get(clientId, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var token = tokendb.generateToken();
|
||||
var scope = accesscontrol.canonicalScope(result.scope);
|
||||
|
||||
tokendb.add(token, userId, result.id, expiresAt, scope, function (error) {
|
||||
users.get(userId, function (error, user) {
|
||||
if (error && error.reason === UsersError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such user'));
|
||||
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, {
|
||||
accessToken: token,
|
||||
identifier: userId,
|
||||
clientId: result.id,
|
||||
scope: result.scope,
|
||||
expires: expiresAt
|
||||
accesscontrol.scopesForUser(user, function (error, userScopes) {
|
||||
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
|
||||
|
||||
var scope = accesscontrol.canonicalScopeString(result.scope);
|
||||
var authorizedScopes = accesscontrol.intersectScopes(userScopes, scope.split(','));
|
||||
|
||||
var token = tokendb.generateToken();
|
||||
|
||||
tokendb.add(token, userId, result.id, expiresAt, authorizedScopes.join(','), name, function (error) {
|
||||
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, {
|
||||
accessToken: token,
|
||||
tokenScopes: authorizedScopes,
|
||||
identifier: userId,
|
||||
clientId: result.id,
|
||||
expires: expiresAt
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// this issues a cid-cli token that does not require a password in various routes
|
||||
function issueDeveloperToken(userObject, ip, callback) {
|
||||
function issueDeveloperToken(userObject, auditSource, callback) {
|
||||
assert.strictEqual(typeof userObject, 'object');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const expiresAt = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
|
||||
|
||||
addTokenByUserId('cid-cli', userObject.id, expiresAt, function (error, result) {
|
||||
addTokenByUserId('cid-cli', userObject.id, expiresAt, {}, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'cli', ip: ip }, { userId: userObject.id, user: users.removePrivateFields(userObject) });
|
||||
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { userId: userObject.id, user: users.removePrivateFields(userObject) });
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
|
||||
+133
-162
@@ -8,25 +8,26 @@ exports = module.exports = {
|
||||
getConfig: getConfig,
|
||||
getDisks: getDisks,
|
||||
getLogs: getLogs,
|
||||
getStatus: getStatus,
|
||||
|
||||
updateToLatest: updateToLatest,
|
||||
reboot: reboot,
|
||||
|
||||
onActivated: onActivated,
|
||||
|
||||
checkDiskSpace: checkDiskSpace
|
||||
|
||||
checkDiskSpace: checkDiskSpace,
|
||||
|
||||
configureWebadmin: configureWebadmin,
|
||||
getWebadminStatus: getWebadminStatus
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
backups = require('./backups.js'),
|
||||
caas = require('./caas.js'),
|
||||
config = require('./config.js'),
|
||||
cron = require('./cron.js'),
|
||||
debug = require('debug')('box:cloudron'),
|
||||
domains = require('./domains.js'),
|
||||
df = require('@sindresorhus/df'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
locker = require('./locker.js'),
|
||||
mailer = require('./mailer.js'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
@@ -39,16 +40,24 @@ var assert = require('assert'),
|
||||
shell = require('./shell.js'),
|
||||
spawn = require('child_process').spawn,
|
||||
split = require('split'),
|
||||
updateChecker = require('./updatechecker.js'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
users = require('./users.js'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
util = require('util');
|
||||
|
||||
var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'),
|
||||
UPDATE_CMD = path.join(__dirname, 'scripts/update.sh');
|
||||
var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh');
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
let gWebadminStatus = {
|
||||
dns: false,
|
||||
tls: false,
|
||||
configuring: false,
|
||||
restore: {
|
||||
active: false,
|
||||
error: null
|
||||
}
|
||||
};
|
||||
|
||||
function CloudronError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
@@ -79,35 +88,46 @@ CloudronError.SELF_UPGRADE_NOT_SUPPORTED = 'Self upgrade not supported';
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
async.series([
|
||||
settings.initialize,
|
||||
reverseProxy.configureDefaultServer,
|
||||
cron.initialize, // required for caas heartbeat before activation
|
||||
onActivated
|
||||
], callback);
|
||||
cron.startPreActivationJobs(callback);
|
||||
|
||||
runStartupTasks();
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
async.series([
|
||||
cron.uninitialize,
|
||||
platform.stop,
|
||||
settings.uninitialize
|
||||
cron.stopJobs,
|
||||
platform.stop
|
||||
], callback);
|
||||
}
|
||||
|
||||
function onActivated(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// Starting the platform after a user is available means:
|
||||
// 1. mail bounces can now be sent to the cloudron owner
|
||||
// 2. the restore code path can run without sudo (since mail/ is non-root)
|
||||
users.count(function (error, count) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
if (!count) return callback(); // not activated
|
||||
async.series([
|
||||
platform.start,
|
||||
cron.startPostActivationJobs
|
||||
], callback);
|
||||
}
|
||||
|
||||
platform.start(callback);
|
||||
// each of these tasks can fail. we will add some routes to fix/re-run them
|
||||
function runStartupTasks() {
|
||||
// configure nginx to be reachable by IP
|
||||
reverseProxy.configureDefaultServer(NOOP_CALLBACK);
|
||||
|
||||
// always generate webadmin config since we have no versioning mechanism for the ejs
|
||||
configureWebadmin(NOOP_CALLBACK);
|
||||
|
||||
// check activation state and start the platform
|
||||
users.isActivated(function (error, activated) {
|
||||
if (error) return debug(error);
|
||||
if (!activated) return debug('initialize: not activated yet'); // not activated
|
||||
|
||||
onActivated(NOOP_CALLBACK);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -140,47 +160,23 @@ function getDisks(callback) {
|
||||
function getConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// result to not depend on the appstore
|
||||
const BOX_AND_USER_TEMPLATE = {
|
||||
box: {
|
||||
region: null,
|
||||
size: null,
|
||||
plan: 'Custom Plan'
|
||||
},
|
||||
user: {
|
||||
billing: false,
|
||||
currency: ''
|
||||
}
|
||||
};
|
||||
settings.getAll(function (error, allSettings) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
caas.getBoxAndUserDetails(function (error, result) {
|
||||
if (error) debug('Failed to fetch cloudron details.', error.reason, error.message);
|
||||
|
||||
result = _.extend(BOX_AND_USER_TEMPLATE, result || {});
|
||||
|
||||
settings.getCloudronName(function (error, cloudronName) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, {
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin(),
|
||||
adminDomain: config.adminDomain(),
|
||||
adminLocation: config.adminLocation(),
|
||||
adminFqdn: config.adminFqdn(),
|
||||
mailFqdn: config.mailFqdn(),
|
||||
version: config.version(),
|
||||
update: updateChecker.getUpdateInfo(),
|
||||
progress: progress.getAll(),
|
||||
isDemo: config.isDemo(),
|
||||
region: result.box.region,
|
||||
size: result.box.size,
|
||||
billing: !!result.user.billing,
|
||||
plan: result.box.plan,
|
||||
currency: result.user.currency,
|
||||
memory: os.totalmem(),
|
||||
provider: config.provider(),
|
||||
cloudronName: cloudronName
|
||||
});
|
||||
// be picky about what we send out here since this is sent for 'normal' users as well
|
||||
callback(null, {
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin(),
|
||||
adminDomain: config.adminDomain(),
|
||||
adminFqdn: config.adminFqdn(),
|
||||
mailFqdn: config.mailFqdn(),
|
||||
version: config.version(),
|
||||
progress: progress.getAll(),
|
||||
isDemo: config.isDemo(),
|
||||
edition: config.edition(),
|
||||
memory: os.totalmem(),
|
||||
provider: config.provider(),
|
||||
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY]
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -189,102 +185,6 @@ function reboot(callback) {
|
||||
shell.sudo('reboot', [ REBOOT_CMD ], callback);
|
||||
}
|
||||
|
||||
function update(boxUpdateInfo, auditSource, callback) {
|
||||
assert.strictEqual(typeof boxUpdateInfo, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!boxUpdateInfo) return callback(null);
|
||||
|
||||
var error = locker.lock(locker.OP_BOX_UPDATE);
|
||||
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
|
||||
|
||||
eventlog.add(eventlog.ACTION_UPDATE, auditSource, { boxUpdateInfo: boxUpdateInfo });
|
||||
|
||||
// ensure tools can 'wait' on progress
|
||||
progress.set(progress.UPDATE, 0, 'Starting');
|
||||
|
||||
// initiate the update/upgrade but do not wait for it
|
||||
if (boxUpdateInfo.upgrade) {
|
||||
debug('Starting upgrade');
|
||||
caas.upgrade(boxUpdateInfo, function (error) {
|
||||
if (error) {
|
||||
debug('Upgrade failed with error:', error);
|
||||
locker.unlock(locker.OP_BOX_UPDATE);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
debug('Starting update');
|
||||
doUpdate(boxUpdateInfo, function (error) {
|
||||
if (error) {
|
||||
debug('Update failed with error:', error);
|
||||
locker.unlock(locker.OP_BOX_UPDATE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function updateToLatest(auditSource, callback) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var boxUpdateInfo = updateChecker.getUpdateInfo().box;
|
||||
if (!boxUpdateInfo) return callback(new CloudronError(CloudronError.ALREADY_UPTODATE, 'No update available'));
|
||||
if (!boxUpdateInfo.sourceTarballUrl) return callback(new CloudronError(CloudronError.BAD_STATE, 'No automatic update available'));
|
||||
|
||||
if (boxUpdateInfo.upgrade && config.provider() !== 'caas') return callback(new CloudronError(CloudronError.SELF_UPGRADE_NOT_SUPPORTED));
|
||||
|
||||
update(boxUpdateInfo, auditSource, callback);
|
||||
}
|
||||
|
||||
function doUpdate(boxUpdateInfo, callback) {
|
||||
assert(boxUpdateInfo && typeof boxUpdateInfo === 'object');
|
||||
|
||||
function updateError(e) {
|
||||
progress.set(progress.UPDATE, -1, e.message);
|
||||
callback(e);
|
||||
}
|
||||
|
||||
progress.set(progress.UPDATE, 5, 'Backing up for update');
|
||||
|
||||
backups.backupBoxAndApps({ userId: null, username: 'updater' }, function (error) {
|
||||
if (error) return updateError(error);
|
||||
|
||||
// NOTE: this data is opaque and will be passed through the installer.sh
|
||||
var data= {
|
||||
provider: config.provider(),
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin(),
|
||||
adminDomain: config.adminDomain(),
|
||||
adminFqdn: config.adminFqdn(),
|
||||
adminLocation: config.adminLocation(),
|
||||
isDemo: config.isDemo(),
|
||||
|
||||
appstore: {
|
||||
apiServerOrigin: config.apiServerOrigin()
|
||||
},
|
||||
caas: {
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin()
|
||||
},
|
||||
|
||||
version: boxUpdateInfo.version
|
||||
};
|
||||
|
||||
debug('updating box %s %j', boxUpdateInfo.sourceTarballUrl, _.omit(data, 'tlsCert', 'tlsKey', 'token', 'appstore', 'caas'));
|
||||
|
||||
progress.set(progress.UPDATE, 5, 'Downloading and installing new version');
|
||||
|
||||
shell.sudo('update', [ UPDATE_CMD, boxUpdateInfo.sourceTarballUrl, JSON.stringify(data) ], function (error) {
|
||||
if (error) return updateError(error);
|
||||
|
||||
// Do not add any code here. The installer script will stop the box code any instant
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function checkDiskSpace(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
@@ -366,7 +266,7 @@ function getLogs(unit, options, callback) {
|
||||
source: obj.SYSLOG_IDENTIFIER || ''
|
||||
}) + '\n';
|
||||
});
|
||||
} else { // mail, mongodb, mysql, postgresql
|
||||
} else { // mail, mongodb, mysql, postgresql, backup
|
||||
let args = [ '--lines=' + lines ];
|
||||
if (follow) args.push('--follow');
|
||||
args.push(path.join(paths.LOG_DIR, unit, 'app.log'));
|
||||
@@ -394,3 +294,74 @@ function getLogs(unit, options, callback) {
|
||||
|
||||
return callback(null, transformStream);
|
||||
}
|
||||
|
||||
function configureWebadmin(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('configureWebadmin: adminDomain:%s status:%j', config.adminDomain(), gWebadminStatus);
|
||||
|
||||
if (process.env.BOX_ENV === 'test' || !config.adminDomain() || gWebadminStatus.configuring) return callback();
|
||||
|
||||
gWebadminStatus.configuring = true; // re-entracy guard
|
||||
|
||||
function configureReverseProxy(error) {
|
||||
debug('configureReverseProxy: error %j', error || null);
|
||||
|
||||
reverseProxy.configureAdmin({ userId: null, username: 'setup' }, function (error) {
|
||||
debug('configureWebadmin: done error: %j', error || {});
|
||||
gWebadminStatus.configuring = false;
|
||||
|
||||
if (error) return callback(error);
|
||||
|
||||
gWebadminStatus.tls = true;
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
// update the DNS. configure nginx regardless of whether it succeeded so that
|
||||
// box is accessible even if dns creds are invalid
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return configureReverseProxy(error);
|
||||
|
||||
domains.upsertDnsRecords(config.adminLocation(), config.adminDomain(), 'A', [ ip ], function (error) {
|
||||
debug('addWebadminDnsRecord: updated records with error:', error);
|
||||
if (error) return configureReverseProxy(error);
|
||||
|
||||
domains.waitForDnsRecord(config.adminLocation(), config.adminDomain(), 'A', ip, { interval: 30000, times: 50000 }, function (error) {
|
||||
if (error) return configureReverseProxy(error);
|
||||
|
||||
gWebadminStatus.dns = true;
|
||||
|
||||
configureReverseProxy();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getWebadminStatus() {
|
||||
return gWebadminStatus;
|
||||
}
|
||||
|
||||
function getStatus(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
users.isActivated(function (error, activated) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
settings.getCloudronName(function (error, cloudronName) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, {
|
||||
version: config.version(),
|
||||
apiServerOrigin: config.apiServerOrigin(), // used by CaaS tool
|
||||
provider: config.provider(),
|
||||
cloudronName: cloudronName,
|
||||
adminFqdn: config.adminDomain() ? config.adminFqdn() : null,
|
||||
activated: activated,
|
||||
edition: config.edition(),
|
||||
webadminStatus: gWebadminStatus // only valid when !activated
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+25
-16
@@ -22,8 +22,8 @@ exports = module.exports = {
|
||||
setAdminFqdn: setAdminFqdn,
|
||||
setAdminLocation: setAdminLocation,
|
||||
version: version,
|
||||
setVersion: setVersion,
|
||||
database: database,
|
||||
edition: edition,
|
||||
|
||||
// these values are derived
|
||||
adminOrigin: adminOrigin,
|
||||
@@ -36,8 +36,12 @@ exports = module.exports = {
|
||||
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
|
||||
};
|
||||
@@ -52,31 +56,28 @@ var assert = require('assert'),
|
||||
// assert on unknown environment can't proceed
|
||||
assert(exports.CLOUDRON || exports.TEST, 'Unknown environment. This should not happen!');
|
||||
|
||||
var homeDir = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
|
||||
|
||||
var data = { };
|
||||
|
||||
function baseDir() {
|
||||
const homeDir = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
|
||||
if (exports.CLOUDRON) return homeDir;
|
||||
if (exports.TEST) return path.join(homeDir, '.cloudron_test');
|
||||
// cannot reach
|
||||
}
|
||||
|
||||
var cloudronConfigFileName = path.join(baseDir(), 'configs/cloudron.conf');
|
||||
|
||||
// only tests can run without a config file on disk, they use the defaults with runtime overrides
|
||||
if (exports.CLOUDRON) assert(fs.existsSync(cloudronConfigFileName), 'No cloudron.conf found, cannot proceed');
|
||||
const cloudronConfigFileName = exports.CLOUDRON ? '/etc/cloudron/cloudron.conf' : path.join(baseDir(), 'cloudron.conf');
|
||||
|
||||
function saveSync() {
|
||||
// only save values we want to have in the cloudron.conf, see start.sh
|
||||
var conf = {
|
||||
version: data.version,
|
||||
apiServerOrigin: data.apiServerOrigin,
|
||||
webServerOrigin: data.webServerOrigin,
|
||||
adminDomain: data.adminDomain,
|
||||
adminFqdn: data.adminFqdn,
|
||||
adminLocation: data.adminLocation,
|
||||
provider: data.provider,
|
||||
isDemo: data.isDemo
|
||||
isDemo: data.isDemo,
|
||||
edition: data.edition
|
||||
};
|
||||
|
||||
fs.writeFileSync(cloudronConfigFileName, JSON.stringify(conf, null, 4)); // functions are ignored by JSON.stringify
|
||||
@@ -96,13 +97,14 @@ function initConfig() {
|
||||
data.adminDomain = '';
|
||||
data.adminLocation = 'my';
|
||||
data.port = 3000;
|
||||
data.version = null;
|
||||
data.apiServerOrigin = null;
|
||||
data.webServerOrigin = null;
|
||||
data.provider = 'generic';
|
||||
data.smtpPort = 2525; // this value comes from mail container
|
||||
data.sysadminPort = 3001;
|
||||
data.ldapPort = 3002;
|
||||
data.dockerProxyPort = 3003;
|
||||
data.edition = '';
|
||||
|
||||
// keep in sync with start.sh
|
||||
data.database = {
|
||||
@@ -115,7 +117,6 @@ function initConfig() {
|
||||
|
||||
// overrides for local testings
|
||||
if (exports.TEST) {
|
||||
data.version = '1.1.1-test';
|
||||
data.port = 5454;
|
||||
data.apiServerOrigin = 'http://localhost:6060'; // hock doesn't support https
|
||||
data.database.password = '';
|
||||
@@ -204,11 +205,8 @@ function sysadminOrigin() {
|
||||
}
|
||||
|
||||
function version() {
|
||||
return get('version');
|
||||
}
|
||||
|
||||
function setVersion(version) {
|
||||
set('version', version);
|
||||
if (exports.TEST) return '3.0.0-test';
|
||||
return fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim();
|
||||
}
|
||||
|
||||
function database() {
|
||||
@@ -219,10 +217,18 @@ 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';
|
||||
return fs.existsSync(IPV6_PROC_FILE);
|
||||
@@ -235,3 +241,6 @@ function dkimSelector() {
|
||||
return loc === 'my' ? 'cloudron' : `cloudron-${loc.replace(/\./g, '')}`;
|
||||
}
|
||||
|
||||
function edition() {
|
||||
return get('edition');
|
||||
}
|
||||
|
||||
+1
-5
@@ -19,12 +19,8 @@ exports = module.exports = {
|
||||
|
||||
ADMIN_NAME: 'Settings',
|
||||
|
||||
ADMIN_CLIENT_ID: 'webadmin', // oauth client id
|
||||
|
||||
ADMIN_GROUP_NAME: 'admin',
|
||||
ADMIN_GROUP_ID: 'admin',
|
||||
|
||||
NGINX_ADMIN_CONFIG_FILE_NAME: 'admin.conf',
|
||||
NGINX_DEFAULT_CONFIG_FILE_NAME: 'default.conf',
|
||||
|
||||
GHOST_USER_FILE: '/tmp/cloudron_ghost.json',
|
||||
|
||||
|
||||
+39
-22
@@ -1,11 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
initialize: initialize,
|
||||
uninitialize: uninitialize
|
||||
startPostActivationJobs: startPostActivationJobs,
|
||||
startPreActivationJobs: startPreActivationJobs,
|
||||
|
||||
stopJobs: stopJobs
|
||||
};
|
||||
|
||||
var apps = require('./apps.js'),
|
||||
var appHealthMonitor = require('./apphealthmonitor.js'),
|
||||
apps = require('./apps.js'),
|
||||
appstore = require('./appstore.js'),
|
||||
assert = require('assert'),
|
||||
backups = require('./backups.js'),
|
||||
@@ -22,6 +25,7 @@ var apps = require('./apps.js'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
scheduler = require('./scheduler.js'),
|
||||
settings = require('./settings.js'),
|
||||
updater = require('./updater.js'),
|
||||
updateChecker = require('./updatechecker.js');
|
||||
|
||||
var gJobs = {
|
||||
@@ -39,8 +43,9 @@ var gJobs = {
|
||||
cleanupTokens: null,
|
||||
digestEmail: null,
|
||||
dockerVolumeCleaner: null,
|
||||
dynamicDNS: null,
|
||||
schedulerSync: null
|
||||
dynamicDns: null,
|
||||
schedulerSync: null,
|
||||
appHealthMonitor: null
|
||||
};
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
|
||||
@@ -54,9 +59,7 @@ var AUDIT_SOURCE = { userId: null, username: 'cron' };
|
||||
// Months: 0-11
|
||||
// Day of Week: 0-6
|
||||
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
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
|
||||
@@ -70,6 +73,12 @@ function initialize(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
function startPostActivationJobs(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var randomHourMinute = Math.floor(60*Math.random());
|
||||
gJobs.alive = new CronJob({
|
||||
cronTime: '00 ' + randomHourMinute + ' * * * *', // every hour on a random minute
|
||||
@@ -77,10 +86,10 @@ function initialize(callback) {
|
||||
start: true
|
||||
});
|
||||
|
||||
settings.events.on(settings.TIME_ZONE_KEY, recreateJobs);
|
||||
settings.events.on(settings.APP_AUTOUPDATE_PATTERN_KEY, appAutoupdatePatternChanged);
|
||||
settings.events.on(settings.BOX_AUTOUPDATE_PATTERN_KEY, boxAutoupdatePatternChanged);
|
||||
settings.events.on(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
|
||||
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);
|
||||
@@ -101,7 +110,7 @@ function recreateJobs(tz) {
|
||||
|
||||
if (gJobs.backup) gJobs.backup.stop();
|
||||
gJobs.backup = new CronJob({
|
||||
cronTime: '00 00 */6 * * *', // every 6 hours. backups.ensureBackup() will only trigger a backup once per day
|
||||
cronTime: '00 00 */6 * * *', // check every 6 hours
|
||||
onTick: backups.ensureBackup.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
|
||||
start: true,
|
||||
timeZone: tz
|
||||
@@ -189,6 +198,14 @@ function recreateJobs(tz) {
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
|
||||
if (gJobs.appHealthMonitor) gJobs.appHealthMonitor.stop();
|
||||
gJobs.appHealthMonitor = new CronJob({
|
||||
cronTime: '*/10 * * * * *', // every 10 seconds
|
||||
onTick: appHealthMonitor.run.bind(null, 10),
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
}
|
||||
|
||||
function boxAutoupdatePatternChanged(pattern) {
|
||||
@@ -207,7 +224,7 @@ function boxAutoupdatePatternChanged(pattern) {
|
||||
var updateInfo = updateChecker.getUpdateInfo();
|
||||
if (updateInfo.box) {
|
||||
debug('Starting autoupdate to %j', updateInfo.box);
|
||||
cloudron.updateToLatest(AUDIT_SOURCE, NOOP_CALLBACK);
|
||||
updater.updateToLatest(AUDIT_SOURCE, NOOP_CALLBACK);
|
||||
} else {
|
||||
debug('No box auto updates available');
|
||||
}
|
||||
@@ -250,25 +267,25 @@ function dynamicDnsChanged(enabled) {
|
||||
debug('Dynamic DNS setting changed to %s', enabled);
|
||||
|
||||
if (enabled) {
|
||||
gJobs.dynamicDNS = new CronJob({
|
||||
gJobs.dynamicDns = new CronJob({
|
||||
cronTime: '00 */10 * * * *',
|
||||
onTick: dyndns.sync,
|
||||
start: true,
|
||||
timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack
|
||||
});
|
||||
} else {
|
||||
if (gJobs.dynamicDNS) gJobs.dynamicDNS.stop();
|
||||
gJobs.dynamicDNS = null;
|
||||
if (gJobs.dynamicDns) gJobs.dynamicDns.stop();
|
||||
gJobs.dynamicDns = null;
|
||||
}
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
function stopJobs(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.events.removeListener(settings.TIME_ZONE_KEY, recreateJobs);
|
||||
settings.events.removeListener(settings.APP_AUTOUPDATE_PATTERN_KEY, appAutoupdatePatternChanged);
|
||||
settings.events.removeListener(settings.BOX_AUTOUPDATE_PATTERN_KEY, boxAutoupdatePatternChanged);
|
||||
settings.events.removeListener(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
|
||||
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;
|
||||
|
||||
+1
-2
@@ -91,8 +91,7 @@ function clear(callback) {
|
||||
|
||||
async.series([
|
||||
child_process.exec.bind(null, cmd),
|
||||
require('./clientdb.js')._addDefaultClients,
|
||||
require('./groupdb.js')._addDefaultGroups
|
||||
require('./clientdb.js')._addDefaultClients
|
||||
], callback);
|
||||
}
|
||||
|
||||
|
||||
+23
-29
@@ -28,42 +28,36 @@ function maybeSend(callback) {
|
||||
var pendingAppUpdates = updateInfo.apps || {};
|
||||
pendingAppUpdates = Object.keys(pendingAppUpdates).map(function (key) { return pendingAppUpdates[key]; });
|
||||
|
||||
appstore.getSubscription(function (error, subscription) {
|
||||
if (error) debug('Error getting subscription:', error);
|
||||
eventlog.getByCreationTime(new Date(new Date() - 7*86400000), function (error, events) {
|
||||
if (error) return callback(error);
|
||||
|
||||
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; });
|
||||
|
||||
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);
|
||||
|
||||
if (error) return callback(error);
|
||||
var info = {
|
||||
pendingAppUpdates: pendingAppUpdates,
|
||||
pendingBoxUpdate: updateInfo.box || null,
|
||||
|
||||
var info = {
|
||||
hasSubscription: appstore.isFreePlan(subscription),
|
||||
finishedAppUpdates: appUpdates,
|
||||
finishedBoxUpdates: boxUpdates,
|
||||
|
||||
pendingAppUpdates: pendingAppUpdates,
|
||||
pendingBoxUpdate: updateInfo.box || null,
|
||||
certRenewals: certRenewals,
|
||||
finishedBackups: finishedBackups, // only the successful backups
|
||||
usersAdded: usersAdded,
|
||||
usersRemoved: usersRemoved // unused because we don't have username to work with
|
||||
};
|
||||
|
||||
finishedAppUpdates: appUpdates,
|
||||
finishedBoxUpdates: boxUpdates,
|
||||
// always send digest for backup failure notification
|
||||
debug('maybeSend: sending digest email', info);
|
||||
mailer.sendDigest(info);
|
||||
|
||||
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();
|
||||
});
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+20
-3
@@ -50,7 +50,7 @@ function add(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
if (result.statusCode === 420) return callback(new DomainsError(DomainsError.STILL_BUSY));
|
||||
if (result.statusCode !== 201) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||
|
||||
return callback(null, result.body.changeId);
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -126,10 +126,27 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
|
||||
|
||||
var credentials = {
|
||||
token: dnsConfig.token,
|
||||
fqdn: domain
|
||||
fqdn: domain,
|
||||
hyphenatedSubdomains: true // this will ensure we always use them, regardless of passed-in configs
|
||||
};
|
||||
|
||||
return callback(null, credentials);
|
||||
const testSubdomain = 'cloudrontestdns';
|
||||
|
||||
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
|
||||
|
||||
del(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record removed again');
|
||||
|
||||
callback(null, credentials);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+3
-12
@@ -28,12 +28,7 @@ function translateRequestError(result, callback) {
|
||||
if (result.statusCode === 422) return callback(new DomainsError(DomainsError.BAD_FIELD, result.body.message));
|
||||
if ((result.statusCode === 400 || result.statusCode === 401 || result.statusCode === 403) && result.body.errors.length > 0) {
|
||||
let error = result.body.errors[0];
|
||||
let message = error.message;
|
||||
if (error.code === 6003) {
|
||||
if (error.error_chain[0] && error.error_chain[0].code === 6103) message = 'Invalid API Key';
|
||||
else message = 'Invalid credentials';
|
||||
}
|
||||
|
||||
let message = `message: ${error.message} statusCode: ${result.statusCode} code:${error.code}`;
|
||||
return callback(new DomainsError(DomainsError.ACCESS_DENIED, message));
|
||||
}
|
||||
|
||||
@@ -112,7 +107,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
var priority = null;
|
||||
|
||||
if (type === 'MX') {
|
||||
priority = value.split(' ')[0];
|
||||
priority = parseInt(value.split(' ')[0], 10);
|
||||
value = value.split(' ')[1];
|
||||
}
|
||||
|
||||
@@ -152,11 +147,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, 'unused');
|
||||
});
|
||||
}, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -129,11 +129,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
return iteratorCallback(null);
|
||||
});
|
||||
}
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, '' + recordIds[0]); // DO ids are integers
|
||||
});
|
||||
}, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -204,6 +200,8 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
|
||||
|
||||
var credentials = {
|
||||
token: dnsConfig.token
|
||||
};
|
||||
|
||||
+3
-1
@@ -48,7 +48,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
if (result.statusCode === 400) return callback(new DomainsError(DomainsError.BAD_FIELD, formatError(result)));
|
||||
if (result.statusCode !== 201) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
return callback(null, 'unused-id');
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -112,6 +112,8 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
|
||||
|
||||
var credentials = {
|
||||
token: dnsConfig.token
|
||||
};
|
||||
|
||||
+10
-11
@@ -19,19 +19,13 @@ var assert = require('assert'),
|
||||
function getDnsCredentials(dnsConfig) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
|
||||
var config = {
|
||||
return {
|
||||
projectId: dnsConfig.projectId,
|
||||
keyFilename: dnsConfig.keyFilename,
|
||||
email: dnsConfig.email
|
||||
};
|
||||
|
||||
if (dnsConfig.credentials) {
|
||||
config.credentials = {
|
||||
credentials: {
|
||||
client_email: dnsConfig.credentials.client_email,
|
||||
private_key: dnsConfig.credentials.private_key
|
||||
};
|
||||
}
|
||||
return config;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getZoneByName(dnsConfig, zoneName, callback) {
|
||||
@@ -97,7 +91,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
|
||||
}
|
||||
|
||||
callback(null, change.id);
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -169,6 +163,11 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (typeof dnsConfig.projectId !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'projectId must be a string'));
|
||||
if (!dnsConfig.credentials || typeof dnsConfig.credentials !== 'object') return callback(new DomainsError(DomainsError.BAD_FIELD, 'credentials must be an object'));
|
||||
if (typeof dnsConfig.credentials.client_email !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'credentials.client_email must be a string'));
|
||||
if (typeof dnsConfig.credentials.private_key !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'credentials.private_key must be a string'));
|
||||
|
||||
var credentials = getDnsCredentials(dnsConfig);
|
||||
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
|
||||
|
||||
|
||||
+7
-3
@@ -21,6 +21,7 @@ const GODADDY_API = 'https://api.godaddy.com/v1/domains';
|
||||
// this is a workaround for godaddy not having a delete API
|
||||
// https://stackoverflow.com/questions/39347464/delete-record-libcloud-godaddy-api
|
||||
const GODADDY_INVALID_IP = '0.0.0.0';
|
||||
const GODADDY_INVALID_TXT = '""';
|
||||
|
||||
function formatError(response) {
|
||||
return util.format(`GoDaddy DNS error [${response.statusCode}] ${response.body.message}`);
|
||||
@@ -63,7 +64,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
if (result.statusCode === 422) return callback(new DomainsError(DomainsError.BAD_FIELD, formatError(result))); // conflict
|
||||
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
return callback(null, 'unused-id');
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -109,7 +110,7 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
|
||||
debug(`get: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
|
||||
if (type !== 'A') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, new Error('Not supported by GoDaddy API'))); // can never happen
|
||||
if (type !== 'A' && type !== 'TXT') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, new Error('Record deletion is not supported by GoDaddy API')));
|
||||
|
||||
// check if the record exists at all so that we don't insert the "Dead" record for no reason
|
||||
get(dnsConfig, zoneName, subdomain, type, function (error, values) {
|
||||
@@ -119,7 +120,7 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
// godaddy does not have a delete API. so fill it up with an invalid IP that we can ignore in future get()
|
||||
var records = [{
|
||||
ttl: 600,
|
||||
data: GODADDY_INVALID_IP
|
||||
data: type === 'A' ? GODADDY_INVALID_IP : GODADDY_INVALID_TXT
|
||||
}];
|
||||
|
||||
superagent.put(`${GODADDY_API}/${zoneName}/records/${type}/${subdomain}`)
|
||||
@@ -146,6 +147,9 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!dnsConfig.apiKey || typeof dnsConfig.apiKey !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'apiKey must be a non-empty string'));
|
||||
if (!dnsConfig.apiSecret || typeof dnsConfig.apiSecret !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'apiSecret must be a non-empty string'));
|
||||
|
||||
var credentials = {
|
||||
apiKey: dnsConfig.apiKey,
|
||||
apiSecret: dnsConfig.apiSecret
|
||||
|
||||
@@ -26,7 +26,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// Result: backend specific change id, to be passed into getChangeStatus()
|
||||
// Result: none
|
||||
|
||||
callback(new Error('not implemented'));
|
||||
}
|
||||
|
||||
+2
-2
@@ -24,7 +24,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
|
||||
debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
|
||||
|
||||
return callback(null, 'noop-record-id');
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
@@ -60,6 +60,6 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
|
||||
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
|
||||
|
||||
callback(null, { wildcard: !!dnsConfig.wildcard });
|
||||
callback(null, {});
|
||||
});
|
||||
}
|
||||
|
||||
+4
-1
@@ -90,7 +90,7 @@ function updateRecord(dnsConfig, zoneName, recordId, subdomain, type, values, ca
|
||||
if (result.statusCode === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
return callback(null, 'unused-id');
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -208,6 +208,9 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (typeof dnsConfig.username !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'username must be a string'));
|
||||
if (typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a string'));
|
||||
|
||||
var credentials = {
|
||||
username: dnsConfig.username,
|
||||
token: dnsConfig.token
|
||||
|
||||
+3
-2
@@ -22,7 +22,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
|
||||
debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
|
||||
|
||||
return callback(null, 'noop-record-id');
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
@@ -46,9 +46,10 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
function waitForDns(domain, zoneName, value, options, callback) {
|
||||
function waitForDns(domain, zoneName, type, value, options, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
+6
-3
@@ -114,14 +114,14 @@ function add(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
};
|
||||
|
||||
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
|
||||
route53.changeResourceRecordSets(params, function(error, result) {
|
||||
route53.changeResourceRecordSets(params, function(error) {
|
||||
if (error && error.code === 'AccessDenied') return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
|
||||
if (error && error.code === 'InvalidClientTokenId') return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
|
||||
if (error && error.code === 'PriorRequestNotComplete') return callback(new DomainsError(DomainsError.STILL_BUSY, error.message));
|
||||
if (error && error.code === 'InvalidChangeBatch') return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
|
||||
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
|
||||
|
||||
callback(null, result.ChangeInfo.Id);
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -233,12 +233,15 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!dnsConfig.accessKeyId || typeof dnsConfig.accessKeyId !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'accessKeyId must be a non-empty string'));
|
||||
if (!dnsConfig.secretAccessKey || typeof dnsConfig.secretAccessKey !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'secretAccessKey must be a non-empty string'));
|
||||
|
||||
var credentials = {
|
||||
accessKeyId: dnsConfig.accessKeyId,
|
||||
secretAccessKey: dnsConfig.secretAccessKey,
|
||||
region: dnsConfig.region || 'us-east-1',
|
||||
endpoint: dnsConfig.endpoint || null,
|
||||
listHostedZonesByName: true // new/updated creds require this perm
|
||||
listHostedZonesByName: true, // new/updated creds require this perm
|
||||
};
|
||||
|
||||
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
|
||||
|
||||
+20
-8
@@ -30,8 +30,9 @@ function resolveIp(hostname, options, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function isChangeSynced(domain, value, nameserver, callback) {
|
||||
function isChangeSynced(domain, type, value, nameserver, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert.strictEqual(typeof nameserver, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -44,20 +45,30 @@ function isChangeSynced(domain, value, nameserver, callback) {
|
||||
}
|
||||
|
||||
async.every(nsIps, function (nsIp, iteratorCallback) {
|
||||
resolveIp(domain, { server: nsIp, timeout: 5000 }, function (error, answer) {
|
||||
const resolveOptions = { server: nsIp, timeout: 5000 };
|
||||
const resolver = type === 'A' ? resolveIp.bind(null, domain) : dns.resolve.bind(null, domain, 'TXT');
|
||||
|
||||
resolver(resolveOptions, function (error, answer) {
|
||||
if (error && error.code === 'TIMEOUT') {
|
||||
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) timed out when resolving ${domain}`);
|
||||
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) timed out when resolving ${domain} (${type})`);
|
||||
return iteratorCallback(null, true); // should be ok if dns server is down
|
||||
}
|
||||
|
||||
if (error) {
|
||||
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) errored when resolve ${domain}: ${error}`);
|
||||
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) errored when resolve ${domain} (${type}): ${error}`);
|
||||
return iteratorCallback(null, false);
|
||||
}
|
||||
|
||||
debug(`isChangeSynced: ${domain} was resolved to ${answer} at NS ${nameserver} (${nsIp}). Expecting ${value}`);
|
||||
let match;
|
||||
if (type === 'A') {
|
||||
match = answer.length === 1 && answer[0] === value;
|
||||
} else if (type === 'TXT') { // answer is a 2d array of strings
|
||||
match = answer.some(function (a) { return value === a.join(''); });
|
||||
}
|
||||
|
||||
iteratorCallback(null, answer.length === 1 && answer[0] === value);
|
||||
debug(`isChangeSynced: ${domain} (${type}) was resolved to ${answer} at NS ${nameserver} (${nsIp}). Expecting ${value}. Match ${match}`);
|
||||
|
||||
iteratorCallback(null, match);
|
||||
});
|
||||
}, callback);
|
||||
|
||||
@@ -65,9 +76,10 @@ function isChangeSynced(domain, value, nameserver, callback) {
|
||||
}
|
||||
|
||||
// check if IP change has propagated to every nameserver
|
||||
function waitForDns(domain, zoneName, value, options, callback) {
|
||||
function waitForDns(domain, zoneName, type, value, options, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert(type === 'A' || type === 'TXT');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -82,7 +94,7 @@ function waitForDns(domain, zoneName, value, options, callback) {
|
||||
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
|
||||
if (error || !nameservers) return retryCallback(error || new DomainsError(DomainsError.EXTERNAL_ERROR, 'Unable to get nameservers'));
|
||||
|
||||
async.every(nameservers, isChangeSynced.bind(null, domain, value), function (error, synced) {
|
||||
async.every(nameservers, isChangeSynced.bind(null, domain, type, value), function (error, synced) {
|
||||
debug('waitForDns: %s %s ns: %j', domain, synced ? 'done' : 'not done', nameservers);
|
||||
|
||||
retryCallback(synced ? null : new DomainsError(DomainsError.EXTERNAL_ERROR, 'ETRYAGAIN'));
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
upsert: upsert,
|
||||
get: get,
|
||||
del: del,
|
||||
waitForDns: require('./waitfordns.js'),
|
||||
verifyDnsConfig: verifyDnsConfig
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
debug = require('debug')('box:dns/manual'),
|
||||
dns = require('../native-dns.js'),
|
||||
DomainsError = require('../domains.js').DomainsError,
|
||||
sysinfo = require('../sysinfo.js'),
|
||||
util = require('util');
|
||||
|
||||
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
|
||||
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
callback(null, [ ]); // returning ip confuses apptask into thinking the entry already exists
|
||||
}
|
||||
|
||||
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
return callback();
|
||||
}
|
||||
|
||||
function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// Very basic check if the nameservers can be fetched
|
||||
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
|
||||
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
|
||||
|
||||
const separator = dnsConfig.hyphenatedSubdomains ? '-' : '.';
|
||||
const fqdn = `cloudrontest${separator}${domain}`;
|
||||
dns.resolve(fqdn, 'A', { server: '127.0.0.1', timeout: 5000 }, function (error, result) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, `Unable to resolve ${fqdn}`));
|
||||
if (error || !result) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : `Unable to resolve ${fqdn}`));
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, `Failed to detect IP of this server: ${error.message}`));
|
||||
|
||||
if (result.length !== 1 || ip !== result[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, `Domain resolves to ${JSON.stringify(result)} instead of ${ip}`));
|
||||
|
||||
callback(null, {});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
+131
-11
@@ -2,6 +2,8 @@
|
||||
|
||||
exports = module.exports = {
|
||||
connection: connectionInstance(),
|
||||
setRegistryConfig: setRegistryConfig,
|
||||
|
||||
downloadImage: downloadImage,
|
||||
createContainer: createContainer,
|
||||
startContainer: startContainer,
|
||||
@@ -16,7 +18,10 @@ exports = module.exports = {
|
||||
getContainerIdByIp: getContainerIdByIp,
|
||||
inspect: inspect,
|
||||
inspectByName: inspect,
|
||||
execContainer: execContainer
|
||||
execContainer: execContainer,
|
||||
createVolume: createVolume,
|
||||
removeVolume: removeVolume,
|
||||
clearVolume: clearVolume
|
||||
};
|
||||
|
||||
function connectionInstance() {
|
||||
@@ -43,19 +48,62 @@ var addons = require('./addons.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:docker.js'),
|
||||
mkdirp = require('mkdirp'),
|
||||
once = require('once'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
shell = require('./shell.js'),
|
||||
spawn = child_process.spawn,
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
const RMVOLUME_CMD = path.join(__dirname, 'scripts/rmvolume.sh');
|
||||
|
||||
function DockerError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(DockerError, Error);
|
||||
DockerError.INTERNAL_ERROR = 'Internal Error';
|
||||
DockerError.BAD_FIELD = 'Bad field';
|
||||
|
||||
function debugApp(app, args) {
|
||||
assert(typeof app === 'object');
|
||||
|
||||
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
||||
}
|
||||
|
||||
function setRegistryConfig(auth, callback) {
|
||||
assert.strictEqual(typeof auth, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const isLogin = !!auth.password;
|
||||
|
||||
// currently, auth info is not stashed in the db but maybe it should for restore to work?
|
||||
const cmd = isLogin ? `docker login ${auth.serveraddress} --username ${auth.username} --password ${auth.password}` : `docker logout ${auth.serveraddress}`;
|
||||
|
||||
child_process.exec(cmd, { }, function (error, stdout, stderr) {
|
||||
if (error) return callback(new DockerError(DockerError.BAD_FIELD, stderr));
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function pullImage(manifest, callback) {
|
||||
var docker = exports.connection;
|
||||
|
||||
@@ -127,16 +175,22 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
dockerPortBindings[manifest.httpPort + '/tcp'] = [ { HostIp: '127.0.0.1', HostPort: app.httpPort + '' } ];
|
||||
|
||||
var portEnv = [];
|
||||
for (var e in app.portBindings) {
|
||||
var hostPort = app.portBindings[e];
|
||||
var containerPort = manifest.tcpPorts[e].containerPort || hostPort;
|
||||
for (let portName in app.portBindings) {
|
||||
const hostPort = app.portBindings[portName];
|
||||
const portType = portName in manifest.tcpPorts ? 'tcp' : 'udp';
|
||||
const ports = portType == 'tcp' ? manifest.tcpPorts : manifest.udpPorts;
|
||||
|
||||
exposedPorts[containerPort + '/tcp'] = {};
|
||||
portEnv.push(e + '=' + hostPort);
|
||||
var containerPort = ports[portName].containerPort || hostPort;
|
||||
|
||||
dockerPortBindings[containerPort + '/tcp'] = [ { HostIp: '0.0.0.0', HostPort: hostPort + '' } ];
|
||||
exposedPorts[`${containerPort}/${portType}`] = {};
|
||||
portEnv.push(`${portName}=${hostPort}`);
|
||||
|
||||
dockerPortBindings[`${containerPort}/${portType}`] = [ { HostIp: '0.0.0.0', HostPort: hostPort + '' } ];
|
||||
}
|
||||
|
||||
let appEnv = [];
|
||||
Object.keys(app.env).forEach(function (name) { appEnv.push(`${name}=${app.env[name]}`); });
|
||||
|
||||
// first check db record, then manifest
|
||||
var memoryLimit = app.memoryLimit || manifest.memoryLimit || 0;
|
||||
|
||||
@@ -164,9 +218,10 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
var containerOptions = {
|
||||
name: name, // used for filtering logs
|
||||
Tty: isAppContainer,
|
||||
Hostname: app.id, // set to something 'constant' so app containers can use this to communicate (across app updates)
|
||||
Image: app.manifest.dockerImage,
|
||||
Cmd: (isAppContainer && app.debugMode && app.debugMode.cmd) ? app.debugMode.cmd : cmd,
|
||||
Env: stdEnv.concat(addonEnv).concat(portEnv),
|
||||
Env: stdEnv.concat(addonEnv).concat(portEnv).concat(appEnv),
|
||||
ExposedPorts: isAppContainer ? exposedPorts : { },
|
||||
Volumes: { // see also ReadonlyRootfs
|
||||
'/tmp': {},
|
||||
@@ -175,10 +230,11 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
Labels: {
|
||||
'fqdn': app.fqdn,
|
||||
'appId': app.id,
|
||||
'isSubcontainer': String(!isAppContainer)
|
||||
'isSubcontainer': String(!isAppContainer),
|
||||
'isCloudronManaged': String(true)
|
||||
},
|
||||
HostConfig: {
|
||||
Binds: addons.getBindsSync(app, app.manifest.addons),
|
||||
Mounts: addons.getMountsSync(app, app.manifest.addons),
|
||||
LogConfig: {
|
||||
Type: 'syslog',
|
||||
Config: {
|
||||
@@ -349,7 +405,8 @@ function deleteImage(manifest, callback) {
|
||||
// just removes the tag). we used to remove the image by id. this is not required anymore because aliases are
|
||||
// not created anymore after https://github.com/docker/docker/pull/10571
|
||||
docker.getImage(dockerImage).remove(removeOptions, function (error) {
|
||||
if (error && error.statusCode === 404) return callback(null);
|
||||
if (error && error.statusCode === 400) return callback(null); // invalid image format. this can happen if user installed with a bad --docker-image
|
||||
if (error && error.statusCode === 404) return callback(null); // not found
|
||||
if (error && error.statusCode === 409) return callback(null); // another container using the image
|
||||
|
||||
if (error) debug('Error removing image %s : %j', dockerImage, error);
|
||||
@@ -423,3 +480,66 @@ function execContainer(containerId, cmd, options, callback) {
|
||||
|
||||
if (options.stdin) options.stdin.pipe(cp.stdin).on('error', callback);
|
||||
}
|
||||
|
||||
function createVolume(app, name, subdir, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof subdir, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let docker = exports.connection;
|
||||
|
||||
const volumeDataDir = path.join(paths.APPS_DATA_DIR, app.id, subdir);
|
||||
|
||||
const volumeOptions = {
|
||||
Name: name,
|
||||
Driver: 'local',
|
||||
DriverOpts: { // https://github.com/moby/moby/issues/19990#issuecomment-248955005
|
||||
type: 'none',
|
||||
device: volumeDataDir,
|
||||
o: 'bind'
|
||||
},
|
||||
Labels: {
|
||||
'fqdn': app.fqdn,
|
||||
'appId': app.id
|
||||
},
|
||||
};
|
||||
|
||||
mkdirp(volumeDataDir, function (error) {
|
||||
if (error) return callback(new Error(`Error creating app data dir: ${error.message}`));
|
||||
|
||||
docker.createVolume(volumeOptions, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function clearVolume(app, name, subdir, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof subdir, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
shell.sudo('removeVolume', [ RMVOLUME_CMD, app.id, subdir ], callback);
|
||||
}
|
||||
|
||||
function removeVolume(app, name, subdir, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof subdir, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let docker = exports.connection;
|
||||
|
||||
let volume = docker.getVolume(name);
|
||||
volume.remove(function (error) {
|
||||
if (error && error.statusCode !== 404) {
|
||||
debug(`removeVolume: Error removing volume of ${app.id} ${error}`);
|
||||
callback(error);
|
||||
}
|
||||
|
||||
shell.sudo('removeVolume', [ RMVOLUME_CMD, app.id, subdir ], callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
start: start,
|
||||
stop: stop
|
||||
};
|
||||
|
||||
var apps = require('./apps.js'),
|
||||
AppsError = apps.AppsError,
|
||||
assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
express = require('express'),
|
||||
debug = require('debug')('box:dockerproxy'),
|
||||
http = require('http'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
middleware = require('./middleware'),
|
||||
net = require('net'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
_ = require('underscore');
|
||||
|
||||
var gHttpServer = null;
|
||||
|
||||
function authorizeApp(req, res, next) {
|
||||
// TODO add here some authorization
|
||||
// - block apps not using the docker addon
|
||||
// - block calls regarding platform containers
|
||||
// - only allow managing and inspection of containers belonging to the app
|
||||
|
||||
// make the tests pass for now
|
||||
if (config.TEST) {
|
||||
req.app = { id: 'testappid' };
|
||||
return next();
|
||||
}
|
||||
|
||||
apps.getByIpAddress(req.connection.remoteAddress, function (error, app) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(401, 'Unauthorized'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
if (!('docker' in app.manifest.addons)) return next(new HttpError(401, 'Unauthorized'));
|
||||
|
||||
req.app = app;
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
function attachDockerRequest(req, res, next) {
|
||||
var options = {
|
||||
socketPath: '/var/run/docker.sock',
|
||||
method: req.method,
|
||||
path: req.url,
|
||||
headers: req.headers
|
||||
};
|
||||
|
||||
req.dockerRequest = http.request(options, function (dockerResponse) {
|
||||
res.writeHead(dockerResponse.statusCode, dockerResponse.headers);
|
||||
|
||||
// Force node to send out the headers, this is required for the /container/wait api to make the docker cli proceed
|
||||
res.write(' ');
|
||||
|
||||
dockerResponse.on('error', function (error) { console.error('dockerResponse error:', error); });
|
||||
dockerResponse.pipe(res, { end: true });
|
||||
});
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
function containersCreate(req, res, next) {
|
||||
safe.set(req.body, 'HostConfig.NetworkMode', 'cloudron'); // overwrite the network the container lives in
|
||||
safe.set(req.body, 'NetworkingConfig', {}); // drop any custom network configs
|
||||
safe.set(req.body, 'Labels', _.extend({ }, safe.query(req.body, 'Labels'), { appId: req.app.id })); // overwrite the app id to track containers of an app
|
||||
safe.set(req.body, 'HostConfig.LogConfig', { Type: 'syslog', Config: { 'tag': req.app.id, 'syslog-address': 'udp://127.0.0.1:2514', 'syslog-format': 'rfc5424' }});
|
||||
|
||||
const appDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'data'),
|
||||
dockerDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'docker');
|
||||
|
||||
debug('Original volume binds:', req.body.HostConfig.Binds);
|
||||
|
||||
let binds = [];
|
||||
for (let bind of (req.body.HostConfig.Binds || [])) {
|
||||
if (bind.startsWith(appDataDir)) binds.push(bind); // eclipse will inspect docker to find out the host folders and pass that to child containers
|
||||
else if (bind.startsWith('/app/data')) binds.push(bind.replace(new RegExp('^/app/data'), appDataDir));
|
||||
else binds.push(`${dockerDataDir}/${bind}`);
|
||||
}
|
||||
|
||||
// cleanup the paths from potential double slashes
|
||||
binds = binds.map(function (bind) { return bind.replace(/\/+/g, '/'); });
|
||||
|
||||
debug('Rewritten volume binds:', binds);
|
||||
safe.set(req.body, 'HostConfig.Binds', binds);
|
||||
|
||||
let plainBody = JSON.stringify(req.body);
|
||||
|
||||
req.dockerRequest.setHeader('Content-Length', Buffer.byteLength(plainBody));
|
||||
req.dockerRequest.end(plainBody);
|
||||
}
|
||||
|
||||
function process(req, res, next) {
|
||||
// we have to rebuild the body since we consumed in in the parser
|
||||
if (Object.keys(req.body).length !== 0) {
|
||||
let plainBody = JSON.stringify(req.body);
|
||||
req.dockerRequest.setHeader('Content-Length', Buffer.byteLength(plainBody));
|
||||
req.dockerRequest.end(plainBody);
|
||||
} else if (!req.readable) {
|
||||
req.dockerRequest.end();
|
||||
} else {
|
||||
req.pipe(req.dockerRequest, { end: true });
|
||||
}
|
||||
}
|
||||
|
||||
function start(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
assert(gHttpServer === null, 'Already started');
|
||||
|
||||
let json = middleware.json({ strict: true });
|
||||
let router = new express.Router();
|
||||
router.post('/:version/containers/create', containersCreate);
|
||||
|
||||
let proxyServer = express();
|
||||
|
||||
if (config.TEST) {
|
||||
proxyServer.use(function (req, res, next) {
|
||||
console.log('Proxying: ' + req.method, req.url);
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
proxyServer.use(authorizeApp)
|
||||
.use(attachDockerRequest)
|
||||
.use(json)
|
||||
.use(router)
|
||||
.use(process)
|
||||
.use(middleware.lastMile());
|
||||
|
||||
gHttpServer = http.createServer(proxyServer);
|
||||
gHttpServer.listen(config.get('dockerProxyPort'), '0.0.0.0', callback);
|
||||
|
||||
debug(`startDockerProxy: started proxy on port ${config.get('dockerProxyPort')}`);
|
||||
|
||||
gHttpServer.on('upgrade', function (req, client, head) {
|
||||
// Create a new tcp connection to the TCP server
|
||||
var remote = net.connect('/var/run/docker.sock', function () {
|
||||
var upgradeMessage = req.method + ' ' + req.url + ' HTTP/1.1\r\n' +
|
||||
`Host: ${req.headers.host}\r\n` +
|
||||
'Connection: Upgrade\r\n' +
|
||||
'Upgrade: tcp\r\n';
|
||||
|
||||
if (req.headers['content-type'] === 'application/json') {
|
||||
// TODO we have to parse the immediate upgrade request body, but I don't know how
|
||||
let plainBody = '{"Detach":false,"Tty":false}\r\n';
|
||||
upgradeMessage += `Content-Type: application/json\r\n`;
|
||||
upgradeMessage += `Content-Length: ${Buffer.byteLength(plainBody)}\r\n`;
|
||||
upgradeMessage += '\r\n';
|
||||
upgradeMessage += plainBody;
|
||||
}
|
||||
|
||||
upgradeMessage += '\r\n';
|
||||
|
||||
// resend the upgrade event to the docker daemon, so it responds with the proper message through the pipes
|
||||
remote.write(upgradeMessage);
|
||||
|
||||
// two-way pipes between client and docker daemon
|
||||
client.pipe(remote).pipe(client);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function stop(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (gHttpServer) gHttpServer.close();
|
||||
|
||||
gHttpServer = null;
|
||||
|
||||
callback();
|
||||
}
|
||||
+218
-65
@@ -6,6 +6,9 @@ module.exports = exports = {
|
||||
getAll: getAll,
|
||||
update: update,
|
||||
del: del,
|
||||
isLocked: isLocked,
|
||||
|
||||
renewCerts: renewCerts,
|
||||
|
||||
fqdn: fqdn,
|
||||
setAdmin: setAdmin,
|
||||
@@ -17,16 +20,28 @@ module.exports = exports = {
|
||||
waitForDnsRecord: waitForDnsRecord,
|
||||
|
||||
removePrivateFields: removePrivateFields,
|
||||
removeRestrictedFields: removeRestrictedFields,
|
||||
|
||||
DomainsError: DomainsError
|
||||
validateHostname: validateHostname,
|
||||
|
||||
makeWildcard: makeWildcard,
|
||||
|
||||
parentDomain: parentDomain,
|
||||
|
||||
DomainsError: DomainsError,
|
||||
|
||||
// exported for testing
|
||||
_getName: getName
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
caas = require('./caas.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:domains'),
|
||||
domaindb = require('./domaindb.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
path = require('path'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
ReverseProxyError = reverseProxy.ReverseProxyError,
|
||||
@@ -68,7 +83,7 @@ DomainsError.STILL_BUSY = 'Still busy';
|
||||
DomainsError.IN_USE = 'In Use';
|
||||
DomainsError.INTERNAL_ERROR = 'Internal error';
|
||||
DomainsError.ACCESS_DENIED = 'Access denied';
|
||||
DomainsError.INVALID_PROVIDER = 'provider must be route53, gcdns, digitalocean, gandi, cloudflare, namecom, noop, manual or caas';
|
||||
DomainsError.INVALID_PROVIDER = 'provider must be route53, gcdns, digitalocean, gandi, cloudflare, namecom, noop, wildcard, manual or caas';
|
||||
|
||||
// choose which subdomain backend we use for test purpose we use route53
|
||||
function api(provider) {
|
||||
@@ -85,12 +100,18 @@ function api(provider) {
|
||||
case 'namecom': return require('./dns/namecom.js');
|
||||
case 'noop': return require('./dns/noop.js');
|
||||
case 'manual': return require('./dns/manual.js');
|
||||
case 'wildcard': return require('./dns/wildcard.js');
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
function verifyDnsConfig(config, domain, zoneName, provider, ip, callback) {
|
||||
assert(config && typeof config === 'object'); // the dns config to test with
|
||||
function parentDomain(domain) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
return domain.replace(/^\S+?\./, ''); // +? means non-greedy
|
||||
}
|
||||
|
||||
function verifyDnsConfig(dnsConfig, domain, zoneName, provider, ip, callback) {
|
||||
assert(dnsConfig && typeof dnsConfig === 'object'); // the dns config to test with
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof provider, 'string');
|
||||
@@ -98,21 +119,98 @@ function verifyDnsConfig(config, domain, zoneName, provider, ip, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var backend = api(provider);
|
||||
if (!backend) return callback(new DomainsError(DomainsError.INVALID_PROVIDER));
|
||||
if (!backend) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid provider'));
|
||||
|
||||
api(provider).verifyDnsConfig(config, domain, zoneName, ip, callback);
|
||||
api(provider).verifyDnsConfig(dnsConfig, domain, zoneName, ip, function (error, result) {
|
||||
if (error && error.reason === DomainsError.ACCESS_DENIED) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Incorrect configuration. Access denied'));
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Zone not found'));
|
||||
if (error && error.reason === DomainsError.EXTERNAL_ERROR) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Configuration error: ' + error.message));
|
||||
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
|
||||
if (error && error.reason === DomainsError.INVALID_PROVIDER) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
result.hyphenatedSubdomains = !!dnsConfig.hyphenatedSubdomains;
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function fqdn(location, domainObject) {
|
||||
return location + (location ? (domainObject.config.hyphenatedSubdomains ? '-' : '.') : '') + domainObject.domain;
|
||||
}
|
||||
|
||||
function add(domain, zoneName, provider, config, fallbackCertificate, tlsConfig, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof provider, 'string');
|
||||
assert.strictEqual(typeof config, 'object');
|
||||
assert.strictEqual(typeof fallbackCertificate, 'object');
|
||||
// Hostname validation comes from RFC 1123 (section 2.1)
|
||||
// Domain name validation comes from RFC 2181 (Name syntax)
|
||||
// https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
|
||||
// We are validating the validity of the location-fqdn as host name (and not dns name)
|
||||
function validateHostname(location, domainObject) {
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
|
||||
const hostname = fqdn(location, domainObject);
|
||||
|
||||
const RESERVED_LOCATIONS = [
|
||||
constants.API_LOCATION,
|
||||
constants.SMTP_LOCATION,
|
||||
constants.IMAP_LOCATION
|
||||
];
|
||||
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new DomainsError(DomainsError.BAD_FIELD, location + ' is reserved');
|
||||
|
||||
if (hostname === config.adminFqdn()) return new DomainsError(DomainsError.BAD_FIELD, location + ' is reserved');
|
||||
|
||||
// workaround https://github.com/oncletom/tld.js/issues/73
|
||||
var tmp = hostname.replace('_', '-');
|
||||
if (!tld.isValid(tmp)) return new DomainsError(DomainsError.BAD_FIELD, 'Hostname is not a valid domain name');
|
||||
|
||||
if (hostname.length > 253) return new DomainsError(DomainsError.BAD_FIELD, 'Hostname length exceeds 253 characters');
|
||||
|
||||
if (location) {
|
||||
// label validation
|
||||
if (location.split('.').some(function (p) { return p.length > 63 || p.length < 1; })) return new DomainsError(DomainsError.BAD_FIELD, 'Invalid subdomain length');
|
||||
if (location.match(/^[A-Za-z0-9-.]+$/) === null) return new DomainsError(DomainsError.BAD_FIELD, 'Subdomain can only contain alphanumeric, hyphen and dot');
|
||||
if (/^[-.]/.test(location)) return new DomainsError(DomainsError.BAD_FIELD, 'Subdomain cannot start or end with hyphen or dot');
|
||||
}
|
||||
|
||||
if (domainObject.config.hyphenatedSubdomains) {
|
||||
if (location.indexOf('.') !== -1) return new DomainsError(DomainsError.BAD_FIELD, 'Subdomain cannot contain a dot');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateTlsConfig(tlsConfig, dnsProvider) {
|
||||
assert.strictEqual(typeof tlsConfig, 'object');
|
||||
assert.strictEqual(typeof dnsProvider, 'string');
|
||||
|
||||
switch (tlsConfig.provider) {
|
||||
case 'letsencrypt-prod':
|
||||
case 'letsencrypt-staging':
|
||||
case 'fallback':
|
||||
case 'caas':
|
||||
break;
|
||||
default:
|
||||
return new DomainsError(DomainsError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback, letsencrypt-prod/staging');
|
||||
}
|
||||
|
||||
if (tlsConfig.wildcard) {
|
||||
if (!tlsConfig.provider.startsWith('letsencrypt')) return new DomainsError(DomainsError.BAD_FIELD, 'wildcard can only be set with letsencrypt');
|
||||
if (dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard') return new DomainsError(DomainsError.BAD_FIELD, 'wildcard cert requires a programmable DNS backend');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function add(domain, data, auditSource, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof data.zoneName, 'string');
|
||||
assert.strictEqual(typeof data.provider, 'string');
|
||||
assert.strictEqual(typeof data.config, 'object');
|
||||
assert.strictEqual(typeof data.fallbackCertificate, 'object');
|
||||
assert.strictEqual(typeof data.tlsConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let { zoneName, provider, config, fallbackCertificate, tlsConfig } = data;
|
||||
|
||||
if (!tld.isValid(domain)) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid domain'));
|
||||
if (domain.endsWith('.')) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid domain'));
|
||||
|
||||
@@ -124,32 +222,31 @@ function add(domain, zoneName, provider, config, fallbackCertificate, tlsConfig,
|
||||
}
|
||||
|
||||
if (fallbackCertificate) {
|
||||
let error = reverseProxy.validateCertificate(`test.${domain}`, fallbackCertificate.cert, fallbackCertificate.key);
|
||||
let error = reverseProxy.validateCertificate('test', { domain, config }, fallbackCertificate);
|
||||
if (error) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
|
||||
} else {
|
||||
fallbackCertificate = reverseProxy.generateFallbackCertificateSync({ domain, config });
|
||||
if (fallbackCertificate.error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, fallbackCertificate.error));
|
||||
}
|
||||
|
||||
if (tlsConfig.provider !== 'fallback' && tlsConfig.provider !== 'caas' && tlsConfig.provider.indexOf('letsencrypt-') !== 0) {
|
||||
return callback(new DomainsError(DomainsError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback or le-*'));
|
||||
}
|
||||
let error = validateTlsConfig(tlsConfig, provider);
|
||||
if (error) return callback(error);
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
|
||||
|
||||
verifyDnsConfig(config, domain, zoneName, provider, ip, function (error, result) {
|
||||
if (error && error.reason === DomainsError.ACCESS_DENIED) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record. Access denied'));
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Zone not found'));
|
||||
if (error && error.reason === DomainsError.EXTERNAL_ERROR) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record: ' + error.message));
|
||||
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
|
||||
if (error && error.reason === DomainsError.INVALID_PROVIDER) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
verifyDnsConfig(config, domain, zoneName, provider, ip, function (error, sanitizedConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
domaindb.add(domain, { zoneName: zoneName, provider: provider, config: result, tlsConfig: tlsConfig }, function (error) {
|
||||
domaindb.add(domain, { zoneName: zoneName, provider: provider, config: sanitizedConfig, tlsConfig: tlsConfig }, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new DomainsError(DomainsError.ALREADY_EXISTS));
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
eventlog.add(eventlog.ACTION_DOMAIN_ADD, auditSource, { domain, zoneName, provider });
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
@@ -157,6 +254,10 @@ function add(domain, zoneName, provider, config, fallbackCertificate, tlsConfig,
|
||||
});
|
||||
}
|
||||
|
||||
function isLocked(domain) {
|
||||
return domain === config.adminDomain() && config.edition() === 'hostingprovider';
|
||||
}
|
||||
|
||||
function get(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -166,6 +267,8 @@ function get(domain, callback) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
result.locked = isLocked(domain);
|
||||
|
||||
reverseProxy.getFallbackCertificate(domain, function (error, bundle) {
|
||||
if (error && error.reason !== ReverseProxyError.NOT_FOUND) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -187,50 +290,49 @@ function getAll(callback) {
|
||||
domaindb.getAll(function (error, result) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
result.forEach(function (r) { r.locked = isLocked(r.domain); });
|
||||
|
||||
return callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function update(domain, zoneName, provider, config, fallbackCertificate, tlsConfig, callback) {
|
||||
function update(domain, data, auditSource, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof provider, 'string');
|
||||
assert.strictEqual(typeof config, 'object');
|
||||
assert.strictEqual(typeof fallbackCertificate, 'object');
|
||||
assert.strictEqual(typeof tlsConfig, 'object');
|
||||
assert.strictEqual(typeof data.zoneName, 'string');
|
||||
assert.strictEqual(typeof data.provider, 'string');
|
||||
assert.strictEqual(typeof data.config, 'object');
|
||||
assert.strictEqual(typeof data.fallbackCertificate, 'object');
|
||||
assert.strictEqual(typeof data.tlsConfig, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
domaindb.get(domain, function (error, result) {
|
||||
let { zoneName, provider, config, fallbackCertificate, tlsConfig } = data;
|
||||
|
||||
domaindb.get(domain, function (error, domainObject) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (zoneName) {
|
||||
if (!tld.isValid(zoneName)) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid zoneName'));
|
||||
} else {
|
||||
zoneName = result.zoneName;
|
||||
zoneName = domainObject.zoneName;
|
||||
}
|
||||
|
||||
if (fallbackCertificate) {
|
||||
let error = reverseProxy.validateCertificate(`test.${domain}`, fallbackCertificate.cert, fallbackCertificate.key);
|
||||
let error = reverseProxy.validateCertificate('test', domainObject, fallbackCertificate);
|
||||
if (error) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
|
||||
}
|
||||
|
||||
if (tlsConfig.provider !== 'fallback' && tlsConfig.provider !== 'caas' && tlsConfig.provider.indexOf('letsencrypt-') !== 0) {
|
||||
return callback(new DomainsError(DomainsError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback or letsencrypt-*'));
|
||||
}
|
||||
error = validateTlsConfig(tlsConfig, provider);
|
||||
if (error) return callback(error);
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
|
||||
|
||||
verifyDnsConfig(config, domain, zoneName, provider, ip, function (error, result) {
|
||||
if (error && error.reason === DomainsError.ACCESS_DENIED) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record. Access denied'));
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Zone not found'));
|
||||
if (error && error.reason === DomainsError.EXTERNAL_ERROR) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record:' + error.message));
|
||||
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
|
||||
if (error && error.reason === DomainsError.INVALID_PROVIDER) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
verifyDnsConfig(config, domain, zoneName, provider, ip, function (error, sanitizedConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
domaindb.update(domain, { zoneName: zoneName, provider: provider, config: result, tlsConfig: tlsConfig }, function (error) {
|
||||
domaindb.update(domain, { zoneName: zoneName, provider: provider, config: sanitizedConfig, tlsConfig: tlsConfig }, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -239,6 +341,8 @@ function update(domain, zoneName, provider, config, fallbackCertificate, tlsConf
|
||||
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
eventlog.add(eventlog.ACTION_DOMAIN_UPDATE, auditSource, { domain, zoneName, provider });
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
@@ -247,28 +351,46 @@ function update(domain, zoneName, provider, config, fallbackCertificate, tlsConf
|
||||
});
|
||||
}
|
||||
|
||||
function del(domain, callback) {
|
||||
function del(domain, auditSource, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (domain === config.adminDomain()) return callback(new DomainsError(DomainsError.IN_USE));
|
||||
|
||||
domaindb.del(domain, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
|
||||
if (error && error.reason === DatabaseError.IN_USE) return callback(new DomainsError(DomainsError.IN_USE));
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
eventlog.add(eventlog.ACTION_DOMAIN_REMOVE, auditSource, { domain });
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function getName(domain, subdomain) {
|
||||
// support special caas domains
|
||||
// returns the 'name' that needs to be inserted into zone
|
||||
function getName(domain, subdomain, type) {
|
||||
// hack for supporting special caas domains. if we want to remove this, we have to fix the appstore domain API first
|
||||
if (domain.provider === 'caas') return subdomain;
|
||||
|
||||
if (domain.domain === domain.zoneName) return subdomain;
|
||||
const part = domain.domain.slice(0, -domain.zoneName.length - 1);
|
||||
|
||||
var part = domain.domain.slice(0, -domain.zoneName.length - 1);
|
||||
if (subdomain === '') return part;
|
||||
|
||||
return subdomain === '' ? part : subdomain + '.' + part;
|
||||
if (!domain.config.hyphenatedSubdomains) return part ? `${subdomain}.${part}` : subdomain;
|
||||
|
||||
// hyphenatedSubdomains
|
||||
if (type !== 'TXT') return `${subdomain}-${part}`;
|
||||
|
||||
if (subdomain.startsWith('_acme-challenge.')) {
|
||||
return `${subdomain}-${part}`;
|
||||
} else if (subdomain === '_acme-challenge') {
|
||||
const up = part.replace(/^[^.]*\.?/, ''); // this gets the domain one level up
|
||||
return up ? `${subdomain}.${up}` : subdomain;
|
||||
} else {
|
||||
return `${subdomain}.${part}`;
|
||||
}
|
||||
}
|
||||
|
||||
function getDnsRecords(subdomain, domain, type, callback) {
|
||||
@@ -280,7 +402,7 @@ function getDnsRecords(subdomain, domain, type, callback) {
|
||||
get(domain, function (error, result) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
api(result.provider).get(result.config, result.zoneName, getName(result, subdomain), type, function (error, values) {
|
||||
api(result.provider).get(result.config, result.zoneName, getName(result, subdomain, type), type, function (error, values) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, values);
|
||||
@@ -288,6 +410,7 @@ function getDnsRecords(subdomain, domain, type, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
// note: for TXT records the values must be quoted
|
||||
function upsertDnsRecords(subdomain, domain, type, values, callback) {
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
@@ -300,10 +423,10 @@ function upsertDnsRecords(subdomain, domain, type, values, callback) {
|
||||
get(domain, function (error, result) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
api(result.provider).upsert(result.config, result.zoneName, getName(result, subdomain), type, values, function (error, changeId) {
|
||||
api(result.provider).upsert(result.config, result.zoneName, getName(result, subdomain, type), type, values, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, changeId);
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -320,7 +443,7 @@ function removeDnsRecords(subdomain, domain, type, values, callback) {
|
||||
get(domain, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
api(result.provider).del(result.config, result.zoneName, getName(result, subdomain), type, values, function (error) {
|
||||
api(result.provider).del(result.config, result.zoneName, getName(result, subdomain, type), type, values, function (error) {
|
||||
if (error && error.reason !== DomainsError.NOT_FOUND) return callback(error);
|
||||
|
||||
callback(null);
|
||||
@@ -328,18 +451,20 @@ function removeDnsRecords(subdomain, domain, type, values, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
// only wait for A record
|
||||
function waitForDnsRecord(fqdn, domain, value, options, callback) {
|
||||
assert.strictEqual(typeof fqdn, 'string');
|
||||
function waitForDnsRecord(subdomain, domain, type, value, options, callback) {
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert(type === 'A' || type === 'TXT');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
get(domain, function (error, result) {
|
||||
get(domain, function (error, domainObject) {
|
||||
if (error) return callback(error);
|
||||
|
||||
api(result.provider).waitForDns(fqdn, result ? result.zoneName : domain, value, options, callback);
|
||||
const hostname = fqdn(subdomain, domainObject);
|
||||
|
||||
api(domainObject.provider).waitForDns(hostname, domainObject.zoneName, type, value, options, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -359,7 +484,7 @@ function setAdmin(domain, callback) {
|
||||
|
||||
config.setAdminDomain(result.domain);
|
||||
config.setAdminLocation('my');
|
||||
config.setAdminFqdn('my' + (result.provider === 'caas' ? '-' : '.') + result.domain);
|
||||
config.setAdminFqdn('my' + (result.config.hyphenatedSubdomains ? '-' : '.') + result.domain);
|
||||
|
||||
callback();
|
||||
|
||||
@@ -368,12 +493,40 @@ function setAdmin(domain, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function fqdn(location, domain, provider) {
|
||||
return location + (location ? (provider !== 'caas' ? '.' : '-') : '') + domain;
|
||||
}
|
||||
|
||||
// removes all fields that are strictly private and should never be returned by API calls
|
||||
function removePrivateFields(domain) {
|
||||
var result = _.pick(domain, 'domain', 'zoneName', 'provider', 'config', 'tlsConfig', 'fallbackCertificate');
|
||||
var result = _.pick(domain, 'domain', 'zoneName', 'provider', 'config', 'tlsConfig', 'fallbackCertificate', 'locked');
|
||||
if (result.fallbackCertificate) delete result.fallbackCertificate.key; // do not return the 'key'. in caas, this is private
|
||||
return result;
|
||||
}
|
||||
|
||||
// removes all fields that are not accessible by a normal user
|
||||
function removeRestrictedFields(domain) {
|
||||
var result = _.pick(domain, 'domain', 'zoneName', 'provider', 'locked');
|
||||
|
||||
// always ensure config object
|
||||
result.config = { hyphenatedSubdomains: !!domain.config.hyphenatedSubdomains };
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function makeWildcard(hostname) {
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
|
||||
let parts = hostname.split('.');
|
||||
parts[0] = '*';
|
||||
return parts.join('.');
|
||||
}
|
||||
|
||||
function renewCerts(domain, auditSource, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// trigger renewal in the background
|
||||
reverseProxy.renewCerts({ domain: domain }, auditSource, function (error) {
|
||||
debug('renewCerts', error);
|
||||
});
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
+20
-1
@@ -18,16 +18,35 @@ exports = module.exports = {
|
||||
ACTION_APP_UNINSTALL: 'app.uninstall',
|
||||
ACTION_APP_UPDATE: 'app.update',
|
||||
ACTION_APP_LOGIN: 'app.login',
|
||||
|
||||
ACTION_BACKUP_FINISH: 'backup.finish',
|
||||
ACTION_BACKUP_START: 'backup.start',
|
||||
ACTION_BACKUP_CLEANUP: 'backup.cleanup',
|
||||
|
||||
ACTION_CERTIFICATE_NEW: 'certificate.new',
|
||||
ACTION_CERTIFICATE_RENEWAL: 'certificate.renew',
|
||||
|
||||
ACTION_DOMAIN_ADD: 'domain.add',
|
||||
ACTION_DOMAIN_UPDATE: 'domain.update',
|
||||
ACTION_DOMAIN_REMOVE: 'domain.remove',
|
||||
|
||||
ACTION_MAIL_ENABLED: 'mail.enabled',
|
||||
ACTION_MAIL_DISABLED: 'mail.disabled',
|
||||
ACTION_MAIL_MAILBOX_ADD: 'mail.box.add',
|
||||
ACTION_MAIL_MAILBOX_REMOVE: 'mail.box.remove',
|
||||
ACTION_MAIL_LIST_ADD: 'mail.list.add',
|
||||
ACTION_MAIL_LIST_REMOVE: 'mail.list.remove',
|
||||
|
||||
ACTION_PROVISION: 'cloudron.provision',
|
||||
ACTION_RESTORE: 'cloudron.restore', // unused
|
||||
ACTION_START: 'cloudron.start',
|
||||
ACTION_UPDATE: 'cloudron.update',
|
||||
|
||||
ACTION_USER_ADD: 'user.add',
|
||||
ACTION_USER_LOGIN: 'user.login',
|
||||
ACTION_USER_REMOVE: 'user.remove',
|
||||
ACTION_USER_UPDATE: 'user.update'
|
||||
ACTION_USER_UPDATE: 'user.update',
|
||||
ACTION_USER_TRANSFER: 'user.transfer',
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
startGraphite: startGraphite
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
infra = require('./infra_version.js'),
|
||||
paths = require('./paths.js'),
|
||||
shell = require('./shell.js');
|
||||
|
||||
function startGraphite(existingInfra, callback) {
|
||||
assert.strictEqual(typeof existingInfra, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const tag = infra.images.graphite.tag;
|
||||
const dataDir = paths.PLATFORM_DATA_DIR;
|
||||
|
||||
if (existingInfra.version === infra.version && infra.images.graphite.tag === existingInfra.images.graphite.tag) return callback();
|
||||
|
||||
const cmd = `docker run --restart=always -d --name="graphite" \
|
||||
--net cloudron \
|
||||
--net-alias graphite \
|
||||
--log-driver syslog \
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag=graphite \
|
||||
-m 75m \
|
||||
--memory-swap 150m \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
-p 127.0.0.1:2003:2003 \
|
||||
-p 127.0.0.1:2004:2004 \
|
||||
-p 127.0.0.1:8000:8000 \
|
||||
-v "${dataDir}/graphite:/var/lib/graphite" \
|
||||
--label isCloudronManaged=true \
|
||||
--read-only -v /tmp -v /run "${tag}"`;
|
||||
|
||||
shell.execSync('startGraphite', cmd);
|
||||
|
||||
callback();
|
||||
}
|
||||
+46
-12
@@ -6,6 +6,7 @@ exports = module.exports = {
|
||||
getAll: getAll,
|
||||
getAllWithMembers: getAllWithMembers,
|
||||
add: add,
|
||||
update: update,
|
||||
del: del,
|
||||
count: count,
|
||||
|
||||
@@ -15,15 +16,15 @@ exports = module.exports = {
|
||||
setMembers: setMembers,
|
||||
isMember: isMember,
|
||||
|
||||
getGroups: getGroups,
|
||||
setGroups: setGroups,
|
||||
getMembership: getMembership,
|
||||
setMembership: setMembership,
|
||||
|
||||
_clear: clear,
|
||||
_addDefaultGroups: addDefaultGroups
|
||||
getGroups: getGroups,
|
||||
|
||||
_clear: clear
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
constants = require('./constants.js'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror');
|
||||
|
||||
@@ -62,10 +63,10 @@ function getWithMembers(groupId, callback) {
|
||||
function getAll(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + GROUPS_FIELDS + ' FROM groups', function (error, result) {
|
||||
database.query('SELECT ' + GROUPS_FIELDS + ' FROM groups', function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, result);
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -95,6 +96,31 @@ function add(id, name, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function update(id, data, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert(data && typeof data === 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var args = [ ];
|
||||
var fields = [ ];
|
||||
for (var k in data) {
|
||||
if (k === 'name') {
|
||||
assert.strictEqual(typeof data.name, 'string');
|
||||
fields.push(k + ' = ?');
|
||||
args.push(data.name);
|
||||
}
|
||||
}
|
||||
args.push(id);
|
||||
|
||||
database.query('UPDATE groups SET ' + fields.join(', ') + ' WHERE id = ?', args, function (error, result) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('groups_name') !== -1) return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'name already exists'));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function del(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -126,7 +152,7 @@ function clear(callback) {
|
||||
database.query('DELETE FROM groupMembers', function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
database.query('DELETE FROM groups WHERE id != ?', [ 'admin' ], function (error) {
|
||||
database.query('DELETE FROM groups', function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(error);
|
||||
@@ -165,7 +191,7 @@ function setMembers(groupId, userIds, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getGroups(userId, callback) {
|
||||
function getMembership(userId, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -177,7 +203,7 @@ function getGroups(userId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function setGroups(userId, groupIds, callback) {
|
||||
function setMembership(userId, groupIds, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert(Array.isArray(groupIds));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -235,6 +261,14 @@ function isMember(groupId, userId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function addDefaultGroups(callback) {
|
||||
add(constants.ADMIN_GROUP_ID, 'admin', callback);
|
||||
function getGroups(userId, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + GROUPS_FIELDS + ' ' +
|
||||
' FROM groups INNER JOIN groupMembers ON groups.id = groupMembers.groupId AND groupMembers.userId = ?', [ userId ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
+43
-14
@@ -6,6 +6,7 @@ exports = module.exports = {
|
||||
create: create,
|
||||
remove: remove,
|
||||
get: get,
|
||||
update: update,
|
||||
getWithMembers: getWithMembers,
|
||||
getAll: getAll,
|
||||
getAllWithMembers: getAllWithMembers,
|
||||
@@ -17,7 +18,9 @@ exports = module.exports = {
|
||||
isMember: isMember,
|
||||
|
||||
getGroups: getGroups,
|
||||
setGroups: setGroups
|
||||
|
||||
setMembership: setMembership,
|
||||
getMembership: getMembership
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
@@ -25,7 +28,8 @@ var assert = require('assert'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
groupdb = require('./groupdb.js'),
|
||||
util = require('util'),
|
||||
uuid = require('uuid');
|
||||
uuid = require('uuid'),
|
||||
_ = require('underscore');
|
||||
|
||||
// http://dustinsenos.com/articles/customErrorsInNode
|
||||
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
|
||||
@@ -64,11 +68,8 @@ function validateGroupname(name) {
|
||||
|
||||
if (constants.RESERVED_NAMES.indexOf(name) !== -1) return new GroupsError(GroupsError.BAD_FIELD, 'name is reserved');
|
||||
|
||||
// +/- can be tricky in emails. also need to consider valid LDAP characters here (e.g '+' is reserved)
|
||||
if (/[^a-zA-Z0-9.]/.test(name)) return new GroupsError(GroupsError.BAD_FIELD, 'name can only contain alphanumerals and dot');
|
||||
|
||||
// app emails are sent using the .app suffix
|
||||
if (name.indexOf('.app') !== -1) return new GroupsError(GroupsError.BAD_FIELD, 'name pattern is reserved for apps');
|
||||
// need to consider valid LDAP characters here (e.g '+' is reserved)
|
||||
if (/[^a-zA-Z0-9.-]/.test(name)) return new GroupsError(GroupsError.BAD_FIELD, 'name can only contain alphanumerals, hyphen and dot');
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -96,9 +97,6 @@ function remove(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// never allow admin group to be deleted
|
||||
if (id === constants.ADMIN_GROUP_ID) return callback(new GroupsError(GroupsError.NOT_ALLOWED));
|
||||
|
||||
groupdb.del(id, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupsError(GroupsError.NOT_FOUND));
|
||||
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
|
||||
@@ -163,11 +161,11 @@ function getMembers(groupId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getGroups(userId, callback) {
|
||||
function getMembership(userId, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
groupdb.getGroups(userId, function (error, result) {
|
||||
groupdb.getMembership(userId, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupsError(GroupsError.NOT_FOUND));
|
||||
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -175,12 +173,12 @@ function getGroups(userId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function setGroups(userId, groupIds, callback) {
|
||||
function setMembership(userId, groupIds, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert(Array.isArray(groupIds));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
groupdb.setGroups(userId, groupIds, function (error) {
|
||||
groupdb.setMembership(userId, groupIds, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupsError(GroupsError.NOT_FOUND));
|
||||
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -239,3 +237,34 @@ function isMember(groupId, userId, callback) {
|
||||
return callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function update(groupId, data, callback) {
|
||||
assert.strictEqual(typeof groupId, 'string');
|
||||
assert(data && typeof data === 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let error;
|
||||
if ('name' in data) {
|
||||
assert.strictEqual(typeof data.name, 'string');
|
||||
error = validateGroupname(data.name);
|
||||
if (error) return callback(error);
|
||||
}
|
||||
|
||||
groupdb.update(groupId, _.pick(data, 'name'), function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupsError(GroupsError.NOT_FOUND));
|
||||
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function getGroups(userId, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
groupdb.getGroups(userId, function (error, results) {
|
||||
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
+13
-12
@@ -5,20 +5,21 @@
|
||||
// Do not require anything here!
|
||||
|
||||
exports = module.exports = {
|
||||
// a major version makes all apps restore from backup. #451 must be fixed before we do this.
|
||||
// a minor version makes all apps re-configure themselves
|
||||
'version': '48.11.0',
|
||||
// a version change recreates all containers with latest docker config
|
||||
'version': '48.12.1',
|
||||
|
||||
'baseImages': [ 'cloudron/base:0.10.0' ],
|
||||
'baseImages': [
|
||||
{ repo: 'cloudron/base', tag: 'cloudron/base:1.0.0@sha256:147a648a068a2e746644746bbfb42eb7a50d682437cead3c67c933c546357617' }
|
||||
],
|
||||
|
||||
// Note that if any of the databases include an upgrade, bump the infra version above
|
||||
// This is because we upgrade using dumps instead of mysql_upgrade, pg_upgrade etc
|
||||
// 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:1.1.0' },
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:1.1.0' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:1.1.0' },
|
||||
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:1.0.0' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:1.3.0' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:1.0.0' }
|
||||
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.0.1@sha256:5a13360da4a2085c7d474bea6b1090c5eb24732d4f73459942af7612d4993d7f' },
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.0.2@sha256:6dcee0731dfb9b013ed94d56205eee219040ee806c7e251db3b3886eaa4947ff' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.0.2@sha256:95e006390ddce7db637e1672eb6f3c257d3c2652747424f529b1dee3cbe6728c' },
|
||||
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.0.0@sha256:8a88dd334b62b578530a014ca1a2425a54cb9df1e475f5d3a36806e5cfa22121' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.0.0@sha256:3c0fbb2a042ac471940ac3e9f6ffa900c8a294941fb7de509b2e3309b09fbffd' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.0@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' }
|
||||
}
|
||||
};
|
||||
|
||||
+30
-37
@@ -28,37 +28,32 @@ var NOOP = function () {};
|
||||
var GROUP_USERS_DN = 'cn=users,ou=groups,dc=cloudron';
|
||||
var GROUP_ADMINS_DN = 'cn=admins,ou=groups,dc=cloudron';
|
||||
|
||||
function getAppByRequest(req, callback) {
|
||||
assert.strictEqual(typeof req, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// Will attach req.app if successful
|
||||
function authenticateApp(req, res, next) {
|
||||
var sourceIp = req.connection.ldap.id.split(':')[0];
|
||||
if (sourceIp.split('.').length !== 4) return callback(new ldap.InsufficientAccessRightsError('Missing source identifier'));
|
||||
if (sourceIp.split('.').length !== 4) return next(new ldap.InsufficientAccessRightsError('Missing source identifier'));
|
||||
|
||||
apps.getByIpAddress(sourceIp, function (error, app) {
|
||||
if (error) return callback(new ldap.OperationsError(error.message));
|
||||
if (error) return next(new ldap.OperationsError(error.message));
|
||||
if (!app) return next(new ldap.OperationsError('Could not detect app source'));
|
||||
|
||||
if (!app) return callback(new ldap.OperationsError('Could not detect app source'));
|
||||
req.app = app;
|
||||
|
||||
callback(null, app);
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
function getUsersWithAccessToApp(req, callback) {
|
||||
assert.strictEqual(typeof req, 'object');
|
||||
assert.strictEqual(typeof req.app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getAppByRequest(req, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
users.list(function (error, result) {
|
||||
if (error) return callback(new ldap.OperationsError(error.toString()));
|
||||
|
||||
users.list(function (error, result){
|
||||
async.filter(result, apps.hasAccessTo.bind(null, req.app), function (error, allowedUsers) {
|
||||
if (error) return callback(new ldap.OperationsError(error.toString()));
|
||||
|
||||
async.filter(result, apps.hasAccessTo.bind(null, app), function (error, result) {
|
||||
if (error) return callback(new ldap.OperationsError(error.toString()));
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
callback(null, allowedUsers);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -139,7 +134,7 @@ function userSearch(req, res, next) {
|
||||
var dn = ldap.parseDN('cn=' + entry.id + ',ou=users,dc=cloudron');
|
||||
|
||||
var groups = [ GROUP_USERS_DN ];
|
||||
if (entry.admin) groups.push(GROUP_ADMINS_DN);
|
||||
if (entry.admin || req.app.ownerId === entry.id) groups.push(GROUP_ADMINS_DN);
|
||||
|
||||
var displayName = entry.displayName || entry.username || ''; // displayName can be empty and username can be null
|
||||
var nameParts = displayName.split(' ');
|
||||
@@ -159,7 +154,7 @@ function userSearch(req, res, next) {
|
||||
givenName: firstName,
|
||||
username: entry.username,
|
||||
samaccountname: entry.username, // to support ActiveDirectory clients
|
||||
isadmin: entry.admin ? 1 : 0,
|
||||
isadmin: (entry.admin || req.app.ownerId === entry.id) ? 1 : 0,
|
||||
memberof: groups
|
||||
}
|
||||
};
|
||||
@@ -199,7 +194,7 @@ function groupSearch(req, res, next) {
|
||||
|
||||
groups.forEach(function (group) {
|
||||
var dn = ldap.parseDN('cn=' + group.name + ',ou=groups,dc=cloudron');
|
||||
var members = group.admin ? result.filter(function (entry) { return entry.admin; }) : result;
|
||||
var members = group.admin ? result.filter(function (entry) { return entry.admin || req.app.ownerId === entry.id; }) : result;
|
||||
|
||||
var obj = {
|
||||
dn: dn.toString(),
|
||||
@@ -248,7 +243,7 @@ function groupAdminsCompare(req, res, next) {
|
||||
// we only support memberuid here, if we add new group attributes later add them here
|
||||
if (req.attribute === 'memberuid') {
|
||||
var found = result.find(function (u) { return u.id === req.value; });
|
||||
if (found && found.admin) return res.end(true);
|
||||
if (found && (found.admin || req.app.ownerId == found.id)) return res.end(true);
|
||||
}
|
||||
|
||||
res.end(false);
|
||||
@@ -413,6 +408,7 @@ function mailingListSearch(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
// Will attach req.user if successful
|
||||
function authenticateUser(req, res, next) {
|
||||
debug('user bind: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
|
||||
|
||||
@@ -444,21 +440,18 @@ function authenticateUser(req, res, next) {
|
||||
}
|
||||
|
||||
function authorizeUserForApp(req, res, next) {
|
||||
assert(req.user);
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
assert.strictEqual(typeof req.app, 'object');
|
||||
|
||||
getAppByRequest(req, function (error, app) {
|
||||
if (error) return next(error);
|
||||
apps.hasAccessTo(req.app, req.user, function (error, result) {
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
|
||||
apps.hasAccessTo(app, req.user, function (error, result) {
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
// we return no such object, to avoid leakage of a users existence
|
||||
if (!result) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
// we return no such object, to avoid leakage of a users existence
|
||||
if (!result) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: req.app.id, app: req.app }, { userId: req.user.id, user: users.removePrivateFields(req.user) });
|
||||
|
||||
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: app.id, app: app }, { userId: req.user.id, user: users.removePrivateFields(req.user) });
|
||||
|
||||
res.end();
|
||||
});
|
||||
res.end();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -525,9 +518,9 @@ function start(callback) {
|
||||
|
||||
gServer = ldap.createServer({ log: logger });
|
||||
|
||||
gServer.search('ou=users,dc=cloudron', userSearch);
|
||||
gServer.search('ou=groups,dc=cloudron', groupSearch);
|
||||
gServer.bind('ou=users,dc=cloudron', authenticateUser, authorizeUserForApp);
|
||||
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);
|
||||
|
||||
// http://www.ietf.org/proceedings/43/I-D/draft-srivastava-ldap-mail-00.txt
|
||||
gServer.search('ou=mailboxes,dc=cloudron', mailboxSearch);
|
||||
@@ -538,8 +531,8 @@ function start(callback) {
|
||||
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailbox);
|
||||
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailbox);
|
||||
|
||||
gServer.compare('cn=users,ou=groups,dc=cloudron', groupUsersCompare);
|
||||
gServer.compare('cn=admins,ou=groups,dc=cloudron', groupAdminsCompare);
|
||||
gServer.compare('cn=users,ou=groups,dc=cloudron', authenticateApp, groupUsersCompare);
|
||||
gServer.compare('cn=admins,ou=groups,dc=cloudron', authenticateApp, groupAdminsCompare);
|
||||
|
||||
// this is the bind for addons (after bind, they might search and authenticate)
|
||||
gServer.bind('ou=addons,dc=cloudron', function(req, res /*, next */) {
|
||||
|
||||
+104
-67
@@ -8,9 +8,8 @@ exports = module.exports = {
|
||||
getDomain: getDomain,
|
||||
addDomain: addDomain,
|
||||
removeDomain: removeDomain,
|
||||
updateDomain: updateDomain,
|
||||
|
||||
addDnsRecords: addDnsRecords,
|
||||
setDnsRecords: setDnsRecords,
|
||||
|
||||
validateName: validateName,
|
||||
|
||||
@@ -52,6 +51,7 @@ var assert = require('assert'),
|
||||
debug = require('debug')('box:mail'),
|
||||
dns = require('./native-dns.js'),
|
||||
domains = require('./domains.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
infra = require('./infra_version.js'),
|
||||
mailboxdb = require('./mailboxdb.js'),
|
||||
maildb = require('./maildb.js'),
|
||||
@@ -106,8 +106,8 @@ function validateName(name) {
|
||||
if (name.length < 1) return new MailError(MailError.BAD_FIELD, 'mailbox name must be atleast 1 char');
|
||||
if (name.length >= 200) return new MailError(MailError.BAD_FIELD, 'mailbox name too long');
|
||||
|
||||
// +/- can be tricky in emails. also need to consider valid LDAP characters here (e.g '+' is reserved)
|
||||
if (/[^a-zA-Z0-9.]/.test(name)) return new MailError(MailError.BAD_FIELD, 'mailbox name can only contain alphanumerals and dot');
|
||||
// also need to consider valid LDAP characters here (e.g '+' is reserved)
|
||||
if (/[^a-zA-Z0-9.-]/.test(name)) return new MailError(MailError.BAD_FIELD, 'mailbox name can only contain alphanumerals and dot');
|
||||
|
||||
// app emails are sent using the .app suffix
|
||||
if (name.indexOf('.app') !== -1) return new MailError(MailError.BAD_FIELD, 'mailbox name pattern is reserved for apps');
|
||||
@@ -162,6 +162,8 @@ function checkSmtpRelay(relay, callback) {
|
||||
};
|
||||
|
||||
var transporter = nodemailer.createTransport(smtpTransport({
|
||||
connectionTimeout: 5000,
|
||||
greetingTimeout: 5000,
|
||||
host: relay.host,
|
||||
port: relay.port,
|
||||
auth: {
|
||||
@@ -199,6 +201,7 @@ function verifyRelay(relay, callback) {
|
||||
function checkDkim(domain, callback) {
|
||||
var dkim = {
|
||||
domain: config.dkimSelector() + '._domainkey.' + domain,
|
||||
name: config.dkimSelector() + '._domainkey',
|
||||
type: 'TXT',
|
||||
expected: null,
|
||||
value: null,
|
||||
@@ -225,6 +228,7 @@ function checkDkim(domain, callback) {
|
||||
function checkSpf(domain, callback) {
|
||||
var spf = {
|
||||
domain: domain,
|
||||
name: '@',
|
||||
type: 'TXT',
|
||||
value: null,
|
||||
expected: 'v=spf1 a:' + config.mailFqdn() + ' ~all',
|
||||
@@ -256,6 +260,7 @@ function checkSpf(domain, callback) {
|
||||
function checkMx(domain, callback) {
|
||||
var mx = {
|
||||
domain: domain,
|
||||
name: '@',
|
||||
type: 'MX',
|
||||
value: null,
|
||||
expected: '10 ' + config.mailFqdn() + '.',
|
||||
@@ -274,9 +279,19 @@ function checkMx(domain, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function txtToDict(txt) {
|
||||
var dict = {};
|
||||
txt.split(';').forEach(function(v) {
|
||||
var p = v.trim().split('=');
|
||||
dict[p[0]]=p[1];
|
||||
});
|
||||
return dict;
|
||||
}
|
||||
|
||||
function checkDmarc(domain, callback) {
|
||||
var dmarc = {
|
||||
domain: '_dmarc.' + domain,
|
||||
name: '_dmarc',
|
||||
type: 'TXT',
|
||||
value: null,
|
||||
expected: 'v=DMARC1; p=reject; pct=100',
|
||||
@@ -288,7 +303,9 @@ function checkDmarc(domain, callback) {
|
||||
|
||||
if (txtRecords.length !== 0) {
|
||||
dmarc.value = txtRecords[0].join('');
|
||||
dmarc.status = (dmarc.value === dmarc.expected);
|
||||
// allow extra fields in dmarc like rua
|
||||
const actual = txtToDict(dmarc.value), expected = txtToDict(dmarc.expected);
|
||||
dmarc.status = Object.keys(expected).every(k => expected[k] === actual[k]);
|
||||
}
|
||||
|
||||
callback(null, dmarc);
|
||||
@@ -537,7 +554,7 @@ function restartMail(callback) {
|
||||
if (process.env.BOX_ENV === 'test' && !process.env.TEST_CREATE_INFRA) return callback();
|
||||
|
||||
const tag = infra.images.mail.tag;
|
||||
const memoryLimit = Math.max((1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 128, 256);
|
||||
const memoryLimit = 4 * 256;
|
||||
|
||||
// admin and mail share the same certificate
|
||||
reverseProxy.getCertificate({ fqdn: config.adminFqdn(), domain: config.adminDomain() }, function (error, bundle) {
|
||||
@@ -572,6 +589,7 @@ function restartMail(callback) {
|
||||
-v "${paths.PLATFORM_DATA_DIR}/addons/mail:/etc/mail" \
|
||||
${ports} \
|
||||
-p 127.0.0.1:2020:2020 \
|
||||
--label isCloudronManaged=true \
|
||||
--read-only -v /run -v /tmp ${tag}`;
|
||||
|
||||
shell.execSync('startMail', cmd);
|
||||
@@ -581,6 +599,20 @@ function restartMail(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function restartMailIfActivated(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
users.isActivated(function (error, activated) {
|
||||
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
|
||||
if (!activated) {
|
||||
debug('restartMailIfActivated: skipping restart of mail container since Cloudron is not activated yet');
|
||||
return callback(); // not provisioned yet, do not restart container after dns setup
|
||||
}
|
||||
|
||||
restartMail(callback);
|
||||
});
|
||||
}
|
||||
|
||||
function getDomain(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -609,7 +641,7 @@ function txtRecordsWithSpf(domain, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
domains.getDnsRecords('', domain, 'TXT', function (error, txtRecords) {
|
||||
if (error) return callback(error);
|
||||
if (error) return new MailError(MailError.EXTERNAL_ERROR, error.message);
|
||||
|
||||
debug('txtRecordsWithSpf: current txt records - %j', txtRecords);
|
||||
|
||||
@@ -687,40 +719,53 @@ function readDkimPublicKeySync(domain) {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
function addDnsRecords(domain, callback) {
|
||||
function setDnsRecords(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = ensureDkimKeySync(domain);
|
||||
if (error) return callback(error);
|
||||
maildb.get(domain, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND));
|
||||
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
|
||||
|
||||
if (process.env.BOX_ENV === 'test') return callback();
|
||||
|
||||
var dkimKey = readDkimPublicKeySync(domain);
|
||||
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 records = [ ];
|
||||
records.push(dkimRecord);
|
||||
|
||||
debug('addDnsRecords: %j', records);
|
||||
|
||||
txtRecordsWithSpf(domain, function (error, txtRecords) {
|
||||
error = ensureDkimKeySync(domain);
|
||||
if (error) return callback(error);
|
||||
|
||||
if (txtRecords) records.push({ subdomain: '', domain: domain, type: 'TXT', values: txtRecords });
|
||||
if (process.env.BOX_ENV === 'test') return callback();
|
||||
|
||||
debug('addDnsRecords: will update %j', records);
|
||||
var dkimKey = readDkimPublicKeySync(domain);
|
||||
if (!dkimKey) return callback(new MailError(MailError.INTERNAL_ERROR, new Error('Failed to read dkim public key')));
|
||||
|
||||
async.mapSeries(records, function (record, iteratorCallback) {
|
||||
domains.upsertDnsRecords(record.subdomain, record.domain, record.type, record.values, iteratorCallback);
|
||||
}, function (error, changeIds) {
|
||||
if (error) debug('addDnsRecords: failed to update : %s. will retry', error);
|
||||
else debug('addDnsRecords: records %j added with changeIds %j', records, changeIds);
|
||||
// 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 + '"' ] };
|
||||
|
||||
callback(error);
|
||||
var records = [ ];
|
||||
records.push(dkimRecord);
|
||||
if (result.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 ' + config.mailFqdn() + '.' ] });
|
||||
}
|
||||
|
||||
debug('setDnsRecords: %j', records);
|
||||
|
||||
txtRecordsWithSpf(domain, function (error, txtRecords) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (txtRecords) records.push({ subdomain: '', domain: domain, type: 'TXT', values: txtRecords });
|
||||
|
||||
debug('setDnsRecords: will update %j', records);
|
||||
|
||||
async.mapSeries(records, function (record, iteratorCallback) {
|
||||
domains.upsertDnsRecords(record.subdomain, record.domain, record.type, record.values, iteratorCallback);
|
||||
}, function (error, changeIds) {
|
||||
if (error) {
|
||||
debug(`setDnsRecords: failed to update: ${error}`);
|
||||
return callback(new MailError(MailError.EXTERNAL_ERROR, error.message));
|
||||
}
|
||||
|
||||
debug('setDnsRecords: records %j added with changeIds %j', records, changeIds);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -735,32 +780,20 @@ function addDomain(domain, callback) {
|
||||
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
|
||||
|
||||
async.series([
|
||||
addDnsRecords.bind(null, domain), // do this first to ensure DKIM keys
|
||||
restartMail
|
||||
setDnsRecords.bind(null, domain), // do this first to ensure DKIM keys
|
||||
restartMailIfActivated
|
||||
], NOOP_CALLBACK); // do these asynchronously
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
// this is just a way to resync the mail "dns" records via the UI
|
||||
function updateDomain(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getDomain(domain, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
addDnsRecords(domain, NOOP_CALLBACK);
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function removeDomain(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (domain === config.adminDomain()) return callback(new MailError(MailError.IN_USE));
|
||||
|
||||
maildb.del(domain, function (error) {
|
||||
if (error && error.reason === DatabaseError.IN_USE) return callback(new MailError(MailError.IN_USE));
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, error.message));
|
||||
@@ -811,6 +844,7 @@ function setMailRelay(domain, relay, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
maildb.update(domain, { relay: relay }, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND));
|
||||
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
|
||||
|
||||
restartMail(NOOP_CALLBACK);
|
||||
@@ -820,28 +854,19 @@ function setMailRelay(domain, relay, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function setMailEnabled(domain, enabled, callback) {
|
||||
function setMailEnabled(domain, enabled, auditSource, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof enabled, 'boolean');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
maildb.update(domain, { enabled: enabled }, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND));
|
||||
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
|
||||
|
||||
restartMail(NOOP_CALLBACK);
|
||||
|
||||
if (!enabled || process.env.BOX_ENV === 'test') return callback(null);
|
||||
|
||||
// Add MX and DMARC record. Note that DMARC policy depends on DKIM signing and thus works
|
||||
// only if we use our internal mail server.
|
||||
var records = [
|
||||
{ subdomain: '_dmarc', type: 'TXT', values: [ '"v=DMARC1; p=reject; pct=100"' ] },
|
||||
{ subdomain: '', type: 'MX', values: [ '10 ' + config.mailFqdn() + '.' ] }
|
||||
];
|
||||
|
||||
async.mapSeries(records, function (record, iteratorCallback) {
|
||||
domains.upsertDnsRecords(record.subdomain, domain, record.type, record.values, iteratorCallback);
|
||||
}, NOOP_CALLBACK);
|
||||
eventlog.add(enabled ? eventlog.ACTION_MAIL_ENABLED : eventlog.ACTION_MAIL_DISABLED, auditSource, { domain });
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -896,10 +921,11 @@ function getMailbox(name, domain, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function addMailbox(name, domain, userId, callback) {
|
||||
function addMailbox(name, domain, userId, auditSource, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
name = name.toLowerCase();
|
||||
@@ -911,6 +937,8 @@ function addMailbox(name, domain, userId, callback) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, `mailbox ${name} already exists`));
|
||||
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
|
||||
|
||||
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_ADD, auditSource, { name, domain, userId });
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
@@ -934,15 +962,18 @@ function updateMailbox(name, domain, userId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function removeMailbox(name, domain, callback) {
|
||||
function removeMailbox(name, domain, auditSource, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
mailboxdb.del(name, domain, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such mailbox'));
|
||||
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
|
||||
|
||||
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_REMOVE, auditSource, { name, domain });
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
@@ -1027,10 +1058,11 @@ function getList(domain, listName, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function addList(name, domain, members, callback) {
|
||||
function addList(name, domain, members, auditSource, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert(Array.isArray(members));
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
name = name.toLowerCase();
|
||||
@@ -1049,6 +1081,8 @@ function addList(name, domain, members, callback) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, 'list already exits'));
|
||||
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
|
||||
|
||||
eventlog.add(eventlog.ACTION_MAIL_LIST_ADD, auditSource, { name, domain });
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
@@ -1079,15 +1113,18 @@ function updateList(name, domain, members, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function removeList(domain, listName, callback) {
|
||||
function removeList(name, domain, auditSource, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof listName, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
mailboxdb.del(listName, domain, function (error) {
|
||||
mailboxdb.del(name, domain, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such list'));
|
||||
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
|
||||
|
||||
eventlog.add(eventlog.ACTION_MAIL_LIST_ADD, auditSource, { name, domain });
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -53,11 +53,6 @@ Last successful backup: <%- info.finishedBackups[0].backupId || info.finishedBac
|
||||
This Cloudron did **not** backup successfully in the last week!
|
||||
<% } -%>
|
||||
|
||||
<% if (!info.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() %>
|
||||
@@ -156,12 +151,6 @@ Sent at: <%= new Date().toUTCString() %>
|
||||
<% } %>
|
||||
|
||||
<br/>
|
||||
|
||||
<% if (!info.hasSubscription) { %>
|
||||
Keep your Cloudron automatically up-to-date and secure by upgrading to a <a href="<%= webadminUrl %>/#/settings">paid plan</a>.
|
||||
<% } %>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
|
||||
@@ -4,15 +4,6 @@ Dear <%= cloudronName %> Admin,
|
||||
|
||||
A new user with email <%= user.email %> was added to <%= cloudronName %>.
|
||||
|
||||
<% if (inviteLink) { %>
|
||||
As requested, this user has not been sent an invitation email.
|
||||
|
||||
To set a password and perform any configuration on behalf of the user, please use this link:
|
||||
<%= inviteLink %>
|
||||
|
||||
<% } %>
|
||||
|
||||
|
||||
Powered by https://cloudron.io
|
||||
|
||||
<% } else { %>
|
||||
@@ -27,14 +18,6 @@ Powered by https://cloudron.io
|
||||
A new user with email <%= user.email %> was added to <%= cloudronName %>.
|
||||
</p>
|
||||
|
||||
<% if (inviteLink) { %>
|
||||
<p>
|
||||
As requested, this user has not been sent an invitation email.<br/>
|
||||
<br/>
|
||||
<a href="<%= inviteLink %>">Set a password and perform any configuration on behalf of the user</a>
|
||||
</p>
|
||||
<% } %>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
|
||||
+5
-7
@@ -202,7 +202,7 @@ function sendInvite(user, invitor) {
|
||||
var templateData = {
|
||||
user: user,
|
||||
webadminUrl: config.adminOrigin(),
|
||||
setupLink: `${config.adminOrigin()}/api/v1/session/account/setup.html?reset_token=${user.resetToken}&email=${user.email}`,
|
||||
setupLink: `${config.adminOrigin()}/api/v1/session/account/setup.html?reset_token=${user.resetToken}&email=${encodeURIComponent(user.email)}`,
|
||||
invitor: invitor,
|
||||
cloudronName: mailConfig.cloudronName,
|
||||
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
@@ -226,11 +226,10 @@ function sendInvite(user, invitor) {
|
||||
});
|
||||
}
|
||||
|
||||
function userAdded(user, inviteSent) {
|
||||
function userAdded(user) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof inviteSent, 'boolean');
|
||||
|
||||
debug('Sending mail for userAdded %s including invite link', inviteSent ? 'not' : '');
|
||||
debug('Sending mail for userAdded');
|
||||
|
||||
getMailConfig(function (error, mailConfig) {
|
||||
if (error) return debug('Error getting mail details:', error);
|
||||
@@ -239,7 +238,6 @@ function userAdded(user, inviteSent) {
|
||||
|
||||
var templateData = {
|
||||
user: user,
|
||||
inviteLink: inviteSent ? null : `${config.adminOrigin()}/api/v1/session/account/setup.html?reset_token=${user.resetToken}&email=${user.email}`,
|
||||
cloudronName: mailConfig.cloudronName,
|
||||
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
};
|
||||
@@ -289,7 +287,7 @@ function passwordReset(user) {
|
||||
|
||||
var templateData = {
|
||||
user: user,
|
||||
resetLink: `${config.adminOrigin()}/api/v1/session/password/reset.html?reset_token=${user.resetToken}&email=${user.email}`,
|
||||
resetLink: `${config.adminOrigin()}/api/v1/session/password/reset.html?reset_token=${user.resetToken}&email=${encodeURIComponent(user.email)}`,
|
||||
cloudronName: mailConfig.cloudronName,
|
||||
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
};
|
||||
@@ -429,7 +427,7 @@ function sendDigest(info) {
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: mailConfig.adminEmails.join(', '),
|
||||
subject: util.format('[%s] Cloudron - Weekly activity digest', mailConfig.cloudronName),
|
||||
subject: util.format('[%s] Weekly activity digest', mailConfig.cloudronName),
|
||||
text: render('digest.ejs', templateDataText),
|
||||
html: render('digest.ejs', templateDataHTML)
|
||||
};
|
||||
|
||||
@@ -30,6 +30,7 @@ function resolve(hostname, rrtype, options, callback) {
|
||||
// result is an empty array if there was no error but there is no record. when you query a random
|
||||
// domain, it errors with ENOTFOUND. But if you query an existing domain (A record) but with different
|
||||
// type (CNAME) it is not an error and empty array
|
||||
// for TXT records, result is 2d array of strings
|
||||
callback(error, result);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ app.controller('Controller', ['$scope', function ($scope) {
|
||||
<% } %>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">Display Name</label>
|
||||
<label class="control-label">Full Name</label>
|
||||
<input type="displayName" class="form-control" ng-model="displayName" name="displayName" required>
|
||||
</div>
|
||||
|
||||
@@ -72,7 +72,7 @@ app.controller('Controller', ['$scope', function ($scope) {
|
||||
<input type="password" class="form-control" ng-model="passwordRepeat" name="passwordRepeat" required>
|
||||
</div>
|
||||
|
||||
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Create" ng-disabled="setupForm.$invalid || password !== passwordRepeat"/>
|
||||
<center><input class="btn btn-primary btn-outline" type="submit" value="Setup" ng-disabled="setupForm.$invalid || password !== passwordRepeat"/></center>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+4
-4
@@ -8,7 +8,6 @@ exports = module.exports = {
|
||||
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
|
||||
INFRA_VERSION_FILE: path.join(config.baseDir(), 'platformdata/INFRA_VERSION'),
|
||||
BACKUP_RESULT_FILE: path.join(config.baseDir(), 'platformdata/backup/result.txt'),
|
||||
BACKUP_LOG_FILE: path.join(config.baseDir(), 'platformdata/backup/logs.txt'),
|
||||
|
||||
OLD_DATA_DIR: path.join(config.baseDir(), 'data'),
|
||||
PLATFORM_DATA_DIR: path.join(config.baseDir(), 'platformdata'),
|
||||
@@ -23,6 +22,7 @@ exports = module.exports = {
|
||||
NGINX_APPCONFIG_DIR: path.join(config.baseDir(), 'platformdata/nginx/applications'),
|
||||
NGINX_CERT_DIR: path.join(config.baseDir(), 'platformdata/nginx/cert'),
|
||||
BACKUP_INFO_DIR: path.join(config.baseDir(), 'platformdata/backup'),
|
||||
UPDATE_DIR: path.join(config.baseDir(), 'platformdata/update'),
|
||||
SNAPSHOT_INFO_FILE: path.join(config.baseDir(), 'platformdata/backup/snapshot-info.json'),
|
||||
|
||||
// this is not part of appdata because an icon may be set before install
|
||||
@@ -33,7 +33,7 @@ exports = module.exports = {
|
||||
CLOUDRON_AVATAR_FILE: path.join(config.baseDir(), 'boxdata/avatar.png'),
|
||||
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'boxdata/updatechecker.json'),
|
||||
|
||||
AUTO_PROVISION_FILE: path.join(config.baseDir(), 'configs/autoprovision.json'),
|
||||
|
||||
LOG_DIR: path.join(config.baseDir(), 'platformdata/logs')
|
||||
LOG_DIR: path.join(config.baseDir(), 'platformdata/logs'),
|
||||
// this pattern is for the cloudron logs API route to work
|
||||
BACKUP_LOG_FILE: path.join(config.baseDir(), 'platformdata/logs/backup/app.log'),
|
||||
};
|
||||
|
||||
+65
-211
@@ -4,32 +4,32 @@ exports = module.exports = {
|
||||
start: start,
|
||||
stop: stop,
|
||||
|
||||
handleCertChanged: handleCertChanged
|
||||
handleCertChanged: handleCertChanged,
|
||||
|
||||
// exported for testing
|
||||
_isReady: false
|
||||
};
|
||||
|
||||
var apps = require('./apps.js'),
|
||||
var addons = require('./addons.js'),
|
||||
apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:platform'),
|
||||
execSync = require('child_process').execSync,
|
||||
fs = require('fs'),
|
||||
hat = require('./hat.js'),
|
||||
graphs = require('./graphs.js'),
|
||||
infra = require('./infra_version.js'),
|
||||
locker = require('./locker.js'),
|
||||
mail = require('./mail.js'),
|
||||
os = require('os'),
|
||||
paths = require('./paths.js'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
settings = require('./settings.js'),
|
||||
shell = require('./shell.js'),
|
||||
taskmanager = require('./taskmanager.js'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
var gPlatformReadyTimer = null;
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
function start(callback) {
|
||||
@@ -49,14 +49,9 @@ function start(callback) {
|
||||
if (_.isEqual(infra, existingInfra)) {
|
||||
debug('platform is uptodate at version %s', infra.version);
|
||||
|
||||
updateAddons(function (error) {
|
||||
if (error) return callback(error);
|
||||
onPlatformReady();
|
||||
|
||||
emitPlatformReady();
|
||||
|
||||
callback();
|
||||
});
|
||||
return;
|
||||
return callback();
|
||||
}
|
||||
|
||||
debug('Updating infrastructure from %s to %s', existingInfra.version, infra.version);
|
||||
@@ -66,256 +61,115 @@ function start(callback) {
|
||||
|
||||
async.series([
|
||||
stopContainers.bind(null, existingInfra),
|
||||
startAddons.bind(null, existingInfra),
|
||||
removeOldImages,
|
||||
// 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),
|
||||
fs.writeFile.bind(fs, paths.INFRA_VERSION_FILE, JSON.stringify(infra, null, 4)),
|
||||
updateAddons
|
||||
graphs.startGraphite.bind(null, existingInfra),
|
||||
addons.startAddons.bind(null, existingInfra),
|
||||
pruneInfraImages,
|
||||
fs.writeFile.bind(fs, paths.INFRA_VERSION_FILE, JSON.stringify(infra, null, 4))
|
||||
], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
locker.unlock(locker.OP_PLATFORM_START);
|
||||
|
||||
emitPlatformReady();
|
||||
onPlatformReady();
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function stop(callback) {
|
||||
clearTimeout(gPlatformReadyTimer);
|
||||
gPlatformReadyTimer = null;
|
||||
exports.events = null;
|
||||
taskmanager.pauseTasks(callback);
|
||||
}
|
||||
|
||||
function updateAddons(callback) {
|
||||
settings.getPlatformConfig(function (error, platformConfig) {
|
||||
if (error) return callback(error);
|
||||
function onPlatformReady() {
|
||||
debug('onPlatformReady: platform is ready');
|
||||
exports._isReady = true;
|
||||
taskmanager.resumeTasks();
|
||||
applyPlatformConfig(NOOP_CALLBACK);
|
||||
}
|
||||
|
||||
for (var containerName of [ 'mysql', 'postgresql', 'mail', 'mongodb' ]) {
|
||||
const containerConfig = platformConfig[containerName];
|
||||
if (!containerConfig) continue;
|
||||
function applyPlatformConfig(callback) {
|
||||
// scale back db containers, if possible. this is retried because updating memory constraints can fail
|
||||
// with failed to write to memory.memsw.limit_in_bytes: write /sys/fs/cgroup/memory/docker/xx/memory.memsw.limit_in_bytes: device or resource busy
|
||||
|
||||
if (!containerConfig.memory || !containerConfig.memorySwap) continue;
|
||||
async.retry({ times: 10, interval: 5 * 60 * 1000 }, function (retryCallback) {
|
||||
settings.getPlatformConfig(function (error, platformConfig) {
|
||||
if (error) return retryCallback(error);
|
||||
|
||||
const cmd = `docker update --memory ${containerConfig.memory} --memory-swap ${containerConfig.memorySwap} ${containerName}`;
|
||||
shell.execSync(`update${containerName}`, cmd);
|
||||
addons.updateAddonConfig(platformConfig, function (error) {
|
||||
if (error) debug('Error updating addons. Will rety in 5 minutes', platformConfig, error);
|
||||
|
||||
retryCallback(error);
|
||||
});
|
||||
});
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function pruneInfraImages(callback) {
|
||||
debug('pruneInfraImages: checking existing images');
|
||||
|
||||
// cannot blindly remove all unused images since redis image may not be used
|
||||
let images = infra.baseImages.concat(Object.keys(infra.images).map(function (addon) { return infra.images[addon]; }));
|
||||
|
||||
for (let image of images) {
|
||||
let output = execSync(`docker images --digests ${image.repo} --format "{{.ID}} {{.Repository}}:{{.Tag}}@{{.Digest}}"`, { encoding: 'utf8' });
|
||||
let lines = output.trim().split('\n');
|
||||
for (let line of lines) {
|
||||
if (!line) continue;
|
||||
let parts = line.split(' '); // [ ID, Repo:Tag@Digest ]
|
||||
if (image.tag === parts[1]) continue; // keep
|
||||
debug(`pruneInfraImages: removing unused image of ${image.repo}: ${line}`);
|
||||
shell.execSync('pruneInfraImages', `docker rmi ${parts[0]}`);
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function emitPlatformReady() {
|
||||
// give some time for the platform to "settle". For example, mysql might still be initing the
|
||||
// database dir and we cannot call service scripts until that's done.
|
||||
// TODO: make this smarter to not wait for 15secs for the crash-restart case
|
||||
gPlatformReadyTimer = setTimeout(function () {
|
||||
debug('emitting platform ready');
|
||||
gPlatformReadyTimer = null;
|
||||
taskmanager.resumeTasks();
|
||||
}, 15000);
|
||||
}
|
||||
|
||||
function removeOldImages(callback) {
|
||||
debug('removing old addon images');
|
||||
|
||||
for (var imageName in infra.images) {
|
||||
if (imageName === 'redis') continue; // see #223
|
||||
var image = infra.images[imageName];
|
||||
debug('cleaning up images of %j', image);
|
||||
var cmd = 'docker images "%s" | tail -n +2 | awk \'{ print $1 ":" $2 }\' | grep -v "%s" | xargs --no-run-if-empty docker rmi';
|
||||
shell.execSync('removeOldImagesSync', util.format(cmd, image.repo, image.tag));
|
||||
}
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
function stopContainers(existingInfra, callback) {
|
||||
// TODO: be nice and stop addons cleanly (example, shutdown commands)
|
||||
|
||||
// always stop addons to restart them on any infra change, regardless of minor or major update
|
||||
if (existingInfra.version !== infra.version) {
|
||||
// TODO: only nuke containers with isCloudronManaged=true
|
||||
debug('stopping all containers for infra upgrade');
|
||||
shell.execSync('stopContainers', 'docker ps -qa | xargs --no-run-if-empty docker stop');
|
||||
shell.execSync('stopContainers', 'docker ps -qa | xargs --no-run-if-empty docker rm -f');
|
||||
} else {
|
||||
assert(typeof infra.images, 'object');
|
||||
var changedAddons = [ ];
|
||||
for (var imageName in infra.images) {
|
||||
if (imageName === 'redis') continue; // see #223
|
||||
if (infra.images[imageName].tag !== existingInfra.images[imageName].tag) changedAddons.push(imageName);
|
||||
}
|
||||
|
||||
debug('stopping addons for incremental infra update: %j', changedAddons);
|
||||
debug('stopContainer: stopping addons for incremental infra update: %j', changedAddons);
|
||||
let filterArg = changedAddons.map(function (c) { return `--filter 'name=${c}'`; }).join(' '); // name=c matches *c*. required for redis-{appid}
|
||||
// ignore error if container not found (and fail later) so that this code works across restarts
|
||||
shell.execSync('stopContainers', 'docker rm -f ' + changedAddons.join(' ') + ' || true');
|
||||
shell.execSync('stopContainers', `docker ps -qa ${filterArg} | xargs --no-run-if-empty docker stop || true`);
|
||||
shell.execSync('stopContainers', `docker ps -qa ${filterArg} | xargs --no-run-if-empty docker rm -f || true`);
|
||||
}
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
function startGraphite(callback) {
|
||||
const tag = infra.images.graphite.tag;
|
||||
const dataDir = paths.PLATFORM_DATA_DIR;
|
||||
|
||||
const cmd = `docker run --restart=always -d --name="graphite" \
|
||||
--net cloudron \
|
||||
--net-alias graphite \
|
||||
--log-driver syslog \
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag=graphite \
|
||||
-m 75m \
|
||||
--memory-swap 150m \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
-p 127.0.0.1:2003:2003 \
|
||||
-p 127.0.0.1:2004:2004 \
|
||||
-p 127.0.0.1:8000:8000 \
|
||||
-v "${dataDir}/graphite:/app/data" \
|
||||
--read-only -v /tmp -v /run "${tag}"`;
|
||||
|
||||
shell.execSync('startGraphite', cmd);
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
function startMysql(callback) {
|
||||
const tag = infra.images.mysql.tag;
|
||||
const dataDir = paths.PLATFORM_DATA_DIR;
|
||||
const rootPassword = hat(8 * 128);
|
||||
const memoryLimit = (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256;
|
||||
|
||||
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mysql_vars.sh',
|
||||
'MYSQL_ROOT_PASSWORD=' + rootPassword +'\nMYSQL_ROOT_HOST=172.18.0.1', 'utf8')) {
|
||||
return callback(new Error('Could not create mysql var file:' + safe.error.message));
|
||||
}
|
||||
|
||||
const cmd = `docker run --restart=always -d --name="mysql" \
|
||||
--net cloudron \
|
||||
--net-alias mysql \
|
||||
--log-driver syslog \
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag=mysql \
|
||||
-m ${memoryLimit}m \
|
||||
--memory-swap ${memoryLimit * 2}m \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
-v "${dataDir}/mysql:/var/lib/mysql" \
|
||||
-v "${dataDir}/addons/mysql_vars.sh:/etc/mysql/mysql_vars.sh:ro" \
|
||||
--read-only -v /tmp -v /run "${tag}"`;
|
||||
|
||||
shell.execSync('startMysql', cmd);
|
||||
|
||||
setTimeout(callback, 5000);
|
||||
}
|
||||
|
||||
function startPostgresql(callback) {
|
||||
const tag = infra.images.postgresql.tag;
|
||||
const dataDir = paths.PLATFORM_DATA_DIR;
|
||||
const rootPassword = hat(8 * 128);
|
||||
const memoryLimit = (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256;
|
||||
|
||||
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/postgresql_vars.sh', 'POSTGRESQL_ROOT_PASSWORD=' + rootPassword, 'utf8')) {
|
||||
return callback(new Error('Could not create postgresql var file:' + safe.error.message));
|
||||
}
|
||||
|
||||
const cmd = `docker run --restart=always -d --name="postgresql" \
|
||||
--net cloudron \
|
||||
--net-alias postgresql \
|
||||
--log-driver syslog \
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag=postgresql \
|
||||
-m ${memoryLimit}m \
|
||||
--memory-swap ${memoryLimit * 2}m \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
-v "${dataDir}/postgresql:/var/lib/postgresql" \
|
||||
-v "${dataDir}/addons/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh:ro" \
|
||||
--read-only -v /tmp -v /run "${tag}"`;
|
||||
|
||||
shell.execSync('startPostgresql', cmd);
|
||||
|
||||
setTimeout(callback, 5000);
|
||||
}
|
||||
|
||||
function startMongodb(callback) {
|
||||
const tag = infra.images.mongodb.tag;
|
||||
const dataDir = paths.PLATFORM_DATA_DIR;
|
||||
const rootPassword = hat(8 * 128);
|
||||
const memoryLimit = (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 200;
|
||||
|
||||
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mongodb_vars.sh', 'MONGODB_ROOT_PASSWORD=' + rootPassword, 'utf8')) {
|
||||
return callback(new Error('Could not create mongodb var file:' + safe.error.message));
|
||||
}
|
||||
|
||||
const cmd = `docker run --restart=always -d --name="mongodb" \
|
||||
--net cloudron \
|
||||
--net-alias mongodb \
|
||||
--log-driver syslog \
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag=mongodb \
|
||||
-m ${memoryLimit}m \
|
||||
--memory-swap ${memoryLimit * 2}m \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
-v "${dataDir}/mongodb:/var/lib/mongodb" \
|
||||
-v "${dataDir}/addons/mongodb_vars.sh:/etc/mongodb_vars.sh:ro" \
|
||||
--read-only -v /tmp -v /run "${tag}"`;
|
||||
|
||||
shell.execSync('startMongodb', cmd);
|
||||
|
||||
setTimeout(callback, 5000);
|
||||
}
|
||||
|
||||
function startAddons(existingInfra, callback) {
|
||||
var startFuncs = [ ];
|
||||
|
||||
// always start addons on any infra change, regardless of minor or major update
|
||||
if (existingInfra.version !== infra.version) {
|
||||
debug('startAddons: no existing infra or infra upgrade. starting all addons');
|
||||
startFuncs.push(startGraphite, startMysql, startPostgresql, startMongodb, mail.startMail);
|
||||
} else {
|
||||
assert.strictEqual(typeof existingInfra.images, 'object');
|
||||
|
||||
if (infra.images.graphite.tag !== existingInfra.images.graphite.tag) startFuncs.push(startGraphite);
|
||||
if (infra.images.mysql.tag !== existingInfra.images.mysql.tag) startFuncs.push(startMysql);
|
||||
if (infra.images.postgresql.tag !== existingInfra.images.postgresql.tag) startFuncs.push(startPostgresql);
|
||||
if (infra.images.mongodb.tag !== existingInfra.images.mongodb.tag) startFuncs.push(startMongodb);
|
||||
if (infra.images.mail.tag !== existingInfra.images.mail.tag) startFuncs.push(mail.startMail);
|
||||
|
||||
debug('startAddons: existing infra. incremental addon create %j', startFuncs.map(function (f) { return f.name; }));
|
||||
}
|
||||
|
||||
async.series(startFuncs, callback);
|
||||
}
|
||||
|
||||
function startApps(existingInfra, callback) {
|
||||
// Infra version change strategy:
|
||||
// * no existing version - restore apps
|
||||
// * major versions - restore apps
|
||||
// * minor versions - reconfigure apps
|
||||
|
||||
if (existingInfra.version === infra.version) {
|
||||
debug('startApp: apps are already uptodate');
|
||||
callback();
|
||||
} else if (existingInfra.version === 'none' || !semver.valid(existingInfra.version) || semver.major(existingInfra.version) !== semver.major(infra.version)) {
|
||||
if (existingInfra.version === 'none') { // cloudron is being restored from backup
|
||||
debug('startApps: restoring installed apps');
|
||||
apps.restoreInstalledApps(callback);
|
||||
} else {
|
||||
} else if (existingInfra.version !== infra.version) {
|
||||
debug('startApps: reconfiguring installed apps');
|
||||
reverseProxy.removeAppConfigs(); // should we change the cert location, nginx will not start
|
||||
apps.configureInstalledApps(callback);
|
||||
} else {
|
||||
debug('startApps: apps are already uptodate');
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
function handleCertChanged(cn) {
|
||||
assert.strictEqual(typeof cn, 'string');
|
||||
|
||||
debug('handleCertChanged', cn);
|
||||
|
||||
if (cn === '*.' + config.adminDomain() || cn === config.adminFqdn()) {
|
||||
mail.startMail(NOOP_CALLBACK);
|
||||
}
|
||||
|
||||
Binary file not shown.
+310
-153
@@ -6,11 +6,15 @@ exports = module.exports = {
|
||||
setFallbackCertificate: setFallbackCertificate,
|
||||
getFallbackCertificate: getFallbackCertificate,
|
||||
|
||||
generateFallbackCertificateSync: generateFallbackCertificateSync,
|
||||
setAppCertificateSync: setAppCertificateSync,
|
||||
|
||||
validateCertificate: validateCertificate,
|
||||
|
||||
getCertificate: getCertificate,
|
||||
|
||||
renewAll: renewAll,
|
||||
renewCerts: renewCerts,
|
||||
|
||||
configureDefaultServer: configureDefaultServer,
|
||||
|
||||
@@ -22,10 +26,10 @@ exports = module.exports = {
|
||||
removeAppConfigs: removeAppConfigs,
|
||||
|
||||
// exported for testing
|
||||
_getApi: getApi
|
||||
_getCertApi: getCertApi
|
||||
};
|
||||
|
||||
var acme = require('./cert/acme.js'),
|
||||
var acme2 = require('./cert/acme2.js'),
|
||||
apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
@@ -33,7 +37,7 @@ var acme = require('./cert/acme.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
crypto = require('crypto'),
|
||||
debug = require('debug')('box:certificates'),
|
||||
debug = require('debug')('box:reverseproxy'),
|
||||
domains = require('./domains.js'),
|
||||
ejs = require('ejs'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
@@ -44,14 +48,14 @@ var acme = require('./cert/acme.js'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
platform = require('./platform.js'),
|
||||
rimraf = require('rimraf'),
|
||||
safe = require('safetydance'),
|
||||
shell = require('./shell.js'),
|
||||
users = require('./users.js'),
|
||||
util = require('util');
|
||||
|
||||
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/../setup/start/nginx/appconfig.ejs', { encoding: 'utf8' }),
|
||||
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh'),
|
||||
NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh');
|
||||
|
||||
function ReverseProxyError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
@@ -76,33 +80,29 @@ ReverseProxyError.INTERNAL_ERROR = 'Internal Error';
|
||||
ReverseProxyError.INVALID_CERT = 'Invalid certificate';
|
||||
ReverseProxyError.NOT_FOUND = 'Not Found';
|
||||
|
||||
function getApi(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
function getCertApi(domainObject, callback) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
domains.get(app.domain, function (error, domain) {
|
||||
if (error) return callback(error);
|
||||
if (domainObject.tlsConfig.provider === 'fallback') return callback(null, fallback, { fallback: true });
|
||||
|
||||
if (domain.tlsConfig.provider === 'fallback') return callback(null, fallback, {});
|
||||
var api = domainObject.tlsConfig.provider === 'caas' ? caas : acme2;
|
||||
|
||||
var api = domain.tlsConfig.provider === 'caas' ? caas : acme;
|
||||
var options = { prod: false, performHttpAuthorization: false, wildcard: false, email: '' };
|
||||
if (domainObject.tlsConfig.provider !== 'caas') { // matches 'le-prod' or 'letsencrypt-prod'
|
||||
options.prod = domainObject.tlsConfig.provider.match(/.*-prod/) !== null;
|
||||
options.performHttpAuthorization = domainObject.provider.match(/noop|manual|wildcard/) !== null;
|
||||
options.wildcard = !!domainObject.tlsConfig.wildcard;
|
||||
}
|
||||
|
||||
var options = { };
|
||||
if (domain.tlsConfig.provider === 'caas') {
|
||||
options.prod = true;
|
||||
} else { // acme
|
||||
options.prod = domain.tlsConfig.provider.match(/.*-prod/) !== null; // matches 'le-prod' or 'letsencrypt-prod'
|
||||
}
|
||||
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
|
||||
// we cannot use admin@fqdn because the user might not have set it up.
|
||||
// we simply update the account with the latest email we have each time when getting letsencrypt certs
|
||||
// https://github.com/ietf-wg-acme/acme/issues/30
|
||||
users.getOwner(function (error, owner) {
|
||||
options.email = error ? 'support@cloudron.io' : (owner.fallbackEmail || owner.email); // can error if not activated yet
|
||||
|
||||
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
|
||||
// we cannot use admin@fqdn because the user might not have set it up.
|
||||
// we simply update the account with the latest email we have each time when getting letsencrypt certs
|
||||
// https://github.com/ietf-wg-acme/acme/issues/30
|
||||
users.getOwner(function (error, owner) {
|
||||
options.email = error ? 'support@cloudron.io' : (owner.fallbackEmail || owner.email); // can error if not activated yet
|
||||
|
||||
callback(null, api, options);
|
||||
});
|
||||
callback(null, api, options);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -119,30 +119,55 @@ function isExpiringSync(certFilePath, hours) {
|
||||
return result.status === 1; // 1 - expired 0 - not expired
|
||||
}
|
||||
|
||||
// checks if the certificate matches the options provided by user (like wildcard, le-staging etc)
|
||||
function providerMatchesSync(domainObject, certFilePath, apiOptions) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof certFilePath, 'string');
|
||||
assert.strictEqual(typeof apiOptions, 'object');
|
||||
|
||||
if (!fs.existsSync(certFilePath)) return false; // not found
|
||||
|
||||
if (apiOptions.fallback) return certFilePath.includes('.host.cert');
|
||||
|
||||
const subjectAndIssuer = safe.child_process.execSync(`/usr/bin/openssl x509 -noout -subject -issuer -in "${certFilePath}"`, { encoding: 'utf8' });
|
||||
|
||||
const subject = subjectAndIssuer.match(/^subject=(.*)$/m)[1];
|
||||
const domain = subject.substr(subject.indexOf('=') + 1).trim(); // subject can be /CN=, CN=, CN = and other forms
|
||||
const issuer = subjectAndIssuer.match(/^issuer=(.*)$/m)[1];
|
||||
const isWildcardCert = domain.includes('*');
|
||||
const isLetsEncryptProd = issuer.includes('Let\'s Encrypt Authority');
|
||||
|
||||
const issuerMismatch = (apiOptions.prod && !isLetsEncryptProd) || (!apiOptions.prod && isLetsEncryptProd);
|
||||
// bare domain is not part of wildcard SAN
|
||||
const wildcardMismatch = (domain !== domainObject.domain) && (apiOptions.wildcard && !isWildcardCert) || (!apiOptions.wildcard && isWildcardCert);
|
||||
|
||||
const mismatch = issuerMismatch || wildcardMismatch;
|
||||
|
||||
debug(`providerMatchesSync: ${certFilePath} subject=${subject} domain=${domain} issuer=${issuer} wildcard=${isWildcardCert}/${apiOptions.wildcard} prod=${isLetsEncryptProd}/${apiOptions.prod} match=${!mismatch}`);
|
||||
|
||||
return !mismatch;
|
||||
}
|
||||
|
||||
// note: https://tools.ietf.org/html/rfc4346#section-7.4.2 (certificate_list) requires that the
|
||||
// servers certificate appears first (and not the intermediate cert)
|
||||
function validateCertificate(domain, cert, key) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof cert, 'string');
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
function validateCertificate(location, domainObject, certificate) {
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert(certificate && typeof certificate, 'object');
|
||||
|
||||
function matchesDomain(candidate) {
|
||||
if (typeof candidate !== 'string') return false;
|
||||
if (candidate === domain) return true;
|
||||
if (candidate.indexOf('*') === 0 && candidate.slice(2) === domain.slice(domain.indexOf('.') + 1)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
const cert = certificate.cert, key = certificate.key;
|
||||
|
||||
// check for empty cert and key strings
|
||||
if (!cert && key) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'missing cert');
|
||||
if (cert && !key) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'missing key');
|
||||
|
||||
// -checkhost checks for SAN or CN exclusively. SAN takes precedence and if present, ignores the CN.
|
||||
var result = safe.child_process.execSync(`openssl x509 -noout -checkhost "${domain}"`, { encoding: 'utf8', input: cert });
|
||||
const fqdn = domains.fqdn(location, domainObject);
|
||||
|
||||
var result = safe.child_process.execSync(`openssl x509 -noout -checkhost "${fqdn}"`, { encoding: 'utf8', input: cert });
|
||||
if (!result) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Unable to get certificate subject.');
|
||||
|
||||
if (result.indexOf('does match certificate') === -1) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, `Certificate is not valid for this domain. Expecting ${domain}`);
|
||||
if (result.indexOf('does match certificate') === -1) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, `Certificate is not valid for this domain. Expecting ${fqdn}`);
|
||||
|
||||
// http://httpd.apache.org/docs/2.0/ssl/ssl_faq.html#verify
|
||||
var certModulus = safe.child_process.execSync('openssl x509 -noout -modulus', { encoding: 'utf8', input: cert });
|
||||
@@ -162,27 +187,52 @@ function reload(callback) {
|
||||
shell.sudo('reload', [ RELOAD_NGINX_CMD ], callback);
|
||||
}
|
||||
|
||||
function generateFallbackCertificateSync(domainObject) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
|
||||
const domain = domainObject.domain;
|
||||
const certFilePath = path.join(os.tmpdir(), `${domain}-${crypto.randomBytes(4).readUInt32LE(0)}.cert`);
|
||||
const keyFilePath = path.join(os.tmpdir(), `${domain}-${crypto.randomBytes(4).readUInt32LE(0)}.key`);
|
||||
|
||||
let opensslConf = safe.fs.readFileSync('/etc/ssl/openssl.cnf', 'utf8');
|
||||
// SAN must contain all the domains since CN check is based on implementation if SAN is found. -checkhost also checks only SAN if present!
|
||||
let opensslConfWithSan;
|
||||
let cn = domainObject.config.hyphenatedSubdomains ? domains.parentDomain(domain) : domain;
|
||||
|
||||
debug(`generateFallbackCertificateSync: domain=${domainObject.domain} cn=${cn} hyphenated=${domainObject.config.hyphenatedSubdomains}`);
|
||||
|
||||
opensslConfWithSan = `${opensslConf}\n[SAN]\nsubjectAltName=DNS:${domain},DNS:*.${cn}\n`;
|
||||
let configFile = path.join(os.tmpdir(), 'openssl-' + crypto.randomBytes(4).readUInt32LE(0) + '.conf');
|
||||
safe.fs.writeFileSync(configFile, opensslConfWithSan, 'utf8');
|
||||
let certCommand = util.format(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=*.${cn} -extensions SAN -config ${configFile} -nodes`);
|
||||
if (!safe.child_process.execSync(certCommand)) return { error: new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message) };
|
||||
safe.fs.unlinkSync(configFile);
|
||||
|
||||
const cert = safe.fs.readFileSync(certFilePath, 'utf8');
|
||||
if (!cert) return { error: safe.error };
|
||||
safe.fs.unlinkSync(certFilePath);
|
||||
|
||||
const key = safe.fs.readFileSync(keyFilePath, 'utf8');
|
||||
if (!key) return { error: safe.error };
|
||||
safe.fs.unlinkSync(keyFilePath);
|
||||
|
||||
return { cert: cert, key: key, error: null };
|
||||
}
|
||||
|
||||
function setFallbackCertificate(domain, fallback, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert(fallback && typeof fallback === 'object');
|
||||
assert.strictEqual(typeof fallback, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const certFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`);
|
||||
const keyFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.key`);
|
||||
|
||||
if (fallback) {
|
||||
// backup the cert
|
||||
if (fallback.restricted) { // restricted certs are not backed up
|
||||
debug(`setFallbackCertificate: setting restricted certs for domain ${domain}`);
|
||||
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`), fallback.cert)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
|
||||
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`), fallback.key)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
|
||||
} else {
|
||||
debug(`setFallbackCertificate: setting certs for domain ${domain}`);
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`), fallback.cert)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
|
||||
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));
|
||||
} else if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) { // generate it
|
||||
let opensslConf = safe.fs.readFileSync('/etc/ssl/openssl.cnf', 'utf8');
|
||||
// SAN must contain all the domains since CN check is based on implementation if SAN is found. -checkhost also checks only SAN if present!
|
||||
let opensslConfWithSan = `${opensslConf}\n[SAN]\nsubjectAltName=DNS:${domain},DNS:*.${domain}\n`;
|
||||
let configFile = path.join(os.tmpdir(), 'openssl-' + crypto.randomBytes(4).readUInt32LE(0) + '.conf');
|
||||
safe.fs.writeFileSync(configFile, opensslConfWithSan, 'utf8');
|
||||
let certCommand = util.format(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=*.${domain} -extensions SAN -config ${configFile} -nodes`);
|
||||
if (!safe.child_process.execSync(certCommand)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
|
||||
safe.fs.unlinkSync(configFile);
|
||||
}
|
||||
|
||||
platform.handleCertChanged('*.' + domain);
|
||||
@@ -202,83 +252,119 @@ function getFallbackCertificate(domain, callback) {
|
||||
var certFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`);
|
||||
var keyFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`);
|
||||
|
||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
|
||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath, type: 'provisioned' });
|
||||
|
||||
// check for auto-generated or user set fallback certs
|
||||
certFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`);
|
||||
keyFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.key`);
|
||||
|
||||
callback(null, { certFilePath, keyFilePath });
|
||||
callback(null, { certFilePath, keyFilePath, type: 'fallback' });
|
||||
}
|
||||
|
||||
function setAppCertificateSync(location, domainObject, certificate) {
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof certificate, 'object');
|
||||
|
||||
let fqdn = domains.fqdn(location, domainObject);
|
||||
if (certificate.cert && certificate.key) {
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`), certificate.cert)) return safe.error;
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.key`), certificate.key)) return safe.error;
|
||||
} else { // remove existing cert/key
|
||||
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`))) debug('Error removing cert: ' + safe.error.message);
|
||||
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.key`))) debug('Error removing key: ' + safe.error.message);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getCertificateByHostname(hostname, domainObject, callback) {
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let certFilePath = path.join(paths.APP_CERTS_DIR, `${hostname}.user.cert`);
|
||||
let keyFilePath = path.join(paths.APP_CERTS_DIR, `${hostname}.user.key`);
|
||||
|
||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
|
||||
|
||||
if (hostname !== domainObject.domain && domainObject.tlsConfig.wildcard) { // bare domain is not part of wildcard SAN
|
||||
let certName = domains.makeWildcard(hostname).replace('*.', '_.');
|
||||
certFilePath = path.join(paths.APP_CERTS_DIR, `${certName}.cert`);
|
||||
keyFilePath = path.join(paths.APP_CERTS_DIR, `${certName}.key`);
|
||||
|
||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
|
||||
} else {
|
||||
certFilePath = path.join(paths.APP_CERTS_DIR, `${hostname}.cert`);
|
||||
keyFilePath = path.join(paths.APP_CERTS_DIR, `${hostname}.key`);
|
||||
|
||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
|
||||
}
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function getCertificate(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var certFilePath = path.join(paths.APP_CERTS_DIR, `${app.fqdn}.user.cert`);
|
||||
var keyFilePath = path.join(paths.APP_CERTS_DIR, `${app.fqdn}.user.key`);
|
||||
|
||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
|
||||
|
||||
certFilePath = path.join(paths.APP_CERTS_DIR, `${app.fqdn}.cert`);
|
||||
keyFilePath = path.join(paths.APP_CERTS_DIR, `${app.fqdn}.key`);
|
||||
|
||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
|
||||
|
||||
return getFallbackCertificate(app.domain, callback);
|
||||
}
|
||||
|
||||
function ensureCertificate(app, auditSource, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const vhost = app.fqdn;
|
||||
|
||||
var certFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.user.cert`);
|
||||
var keyFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.user.key`);
|
||||
|
||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) {
|
||||
debug('ensureCertificate: %s. user certificate already exists at %s', vhost, keyFilePath);
|
||||
return callback(null, { certFilePath, keyFilePath, reason: 'user' });
|
||||
}
|
||||
|
||||
certFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.cert`);
|
||||
keyFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.key`);
|
||||
|
||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) {
|
||||
debug('ensureCertificate: %s. certificate already exists at %s', vhost, keyFilePath);
|
||||
|
||||
if (!isExpiringSync(certFilePath, 24 * 30)) return callback(null, { certFilePath, keyFilePath, reason: 'existing-le' });
|
||||
debug('ensureCertificate: %s cert require renewal', vhost);
|
||||
} else {
|
||||
debug('ensureCertificate: %s cert does not exist', vhost);
|
||||
}
|
||||
|
||||
getApi(app, function (error, api, apiOptions) {
|
||||
domains.get(app.domain, function (error, domainObject) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('ensureCertificate: getting certificate for %s with options %j', vhost, apiOptions);
|
||||
getCertificateByHostname(app.fqdn, domainObject, function (error, result) {
|
||||
if (error || result) return callback(error, result);
|
||||
|
||||
api.getCertificate(vhost, apiOptions, function (error, certFilePath, keyFilePath) {
|
||||
var errorMessage = error ? error.message : '';
|
||||
|
||||
if (error) {
|
||||
debug('ensureCertificate: could not get certificate. using fallback certs', error);
|
||||
mailer.certificateRenewalError(vhost, errorMessage);
|
||||
}
|
||||
|
||||
eventlog.add(eventlog.ACTION_CERTIFICATE_RENEWAL, 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(app.domain, callback);
|
||||
|
||||
callback(null, { certFilePath, keyFilePath, reason: 'new-le' });
|
||||
return getFallbackCertificate(app.domain, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function configureAdminInternal(bundle, configFileName, vhost, callback) {
|
||||
function ensureCertificate(vhost, domain, auditSource, callback) {
|
||||
assert.strictEqual(typeof vhost, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
if (error) return callback(error);
|
||||
|
||||
getCertApi(domainObject, function (error, api, apiOptions) {
|
||||
if (error) return callback(error);
|
||||
|
||||
getCertificateByHostname(vhost, domainObject, function (error, currentBundle) {
|
||||
if (currentBundle) {
|
||||
debug(`ensureCertificate: ${vhost} certificate already exists at ${currentBundle.keyFilePath}`);
|
||||
|
||||
if (currentBundle.certFilePath.endsWith('.user.cert')) return callback(null, currentBundle); // user certs cannot be renewed
|
||||
if (!isExpiringSync(currentBundle.certFilePath, 24 * 30) && providerMatchesSync(domainObject, currentBundle.certFilePath, apiOptions)) return callback(null, currentBundle);
|
||||
debug(`ensureCertificate: ${vhost} cert require renewal`);
|
||||
} else {
|
||||
debug(`ensureCertificate: ${vhost} cert does not exist`);
|
||||
}
|
||||
|
||||
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 : '';
|
||||
|
||||
if (error) {
|
||||
debug('ensureCertificate: could not get certificate. using fallback certs', error);
|
||||
mailer.certificateRenewalError(vhost, errorMessage);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
callback(null, { certFilePath, keyFilePath, type: 'new-le' });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function writeAdminConfig(bundle, configFileName, vhost, callback) {
|
||||
assert.strictEqual(typeof bundle, 'object');
|
||||
assert.strictEqual(typeof configFileName, 'string');
|
||||
assert.strictEqual(typeof vhost, 'string');
|
||||
@@ -307,15 +393,14 @@ function configureAdmin(auditSource, callback) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var adminApp = { domain: config.adminDomain(), fqdn: config.adminFqdn() };
|
||||
ensureCertificate(adminApp, auditSource, function (error, bundle) {
|
||||
ensureCertificate(config.adminFqdn(), config.adminDomain(), auditSource, function (error, bundle) {
|
||||
if (error) return callback(error);
|
||||
|
||||
configureAdminInternal(bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), callback);
|
||||
writeAdminConfig(bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), callback);
|
||||
});
|
||||
}
|
||||
|
||||
function configureAppInternal(app, bundle, callback) {
|
||||
function writeAppConfig(app, bundle, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof bundle, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -348,15 +433,56 @@ function configureAppInternal(app, bundle, callback) {
|
||||
reload(callback);
|
||||
}
|
||||
|
||||
function writeAppRedirectConfig(app, fqdn, bundle, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof fqdn, 'string');
|
||||
assert.strictEqual(typeof bundle, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var data = {
|
||||
sourceDir: path.resolve(__dirname, '..'),
|
||||
vhost: fqdn,
|
||||
redirectTo: app.fqdn,
|
||||
hasIPv6: config.hasIPv6(),
|
||||
endpoint: 'redirect',
|
||||
certFilePath: bundle.certFilePath,
|
||||
keyFilePath: bundle.keyFilePath,
|
||||
robotsTxtQuoted: null,
|
||||
xFrameOptions: 'SAMEORIGIN'
|
||||
};
|
||||
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
|
||||
|
||||
// if we change the filename, also change it in unconfigureApp()
|
||||
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, `${app.id}-redirect-${fqdn}.conf`);
|
||||
debug('writing config for "%s" redirecting to "%s" to %s with options %j', app.fqdn, fqdn, nginxConfigFilename, data);
|
||||
|
||||
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) {
|
||||
debug('Error creating nginx redirect config for "%s" : %s', app.fqdn, safe.error.message);
|
||||
return callback(safe.error);
|
||||
}
|
||||
|
||||
reload(callback);
|
||||
}
|
||||
|
||||
function configureApp(app, auditSource, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
ensureCertificate(app, auditSource, function (error, bundle) {
|
||||
ensureCertificate(app.fqdn, app.domain, auditSource, function (error, bundle) {
|
||||
if (error) return callback(error);
|
||||
|
||||
configureAppInternal(app, bundle, callback);
|
||||
writeAppConfig(app, bundle, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(app.alternateDomains, function (alternateDomain, callback) {
|
||||
ensureCertificate(alternateDomain.fqdn, alternateDomain.domain, auditSource, function (error, bundle) {
|
||||
if (error) return callback(error);
|
||||
|
||||
writeAppRedirectConfig(app, alternateDomain.fqdn, bundle, callback);
|
||||
});
|
||||
}, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -364,13 +490,66 @@ function unconfigureApp(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
|
||||
if (!safe.fs.unlinkSync(nginxConfigFilename)) {
|
||||
if (safe.error.code !== 'ENOENT') debug('Error removing nginx configuration of "%s": %s', app.fqdn, safe.error.message);
|
||||
return callback(null);
|
||||
}
|
||||
// we use globbing to find all nginx configs for an app
|
||||
rimraf(path.join(paths.NGINX_APPCONFIG_DIR, `${app.id}*.conf`), function (error) {
|
||||
if (error) debug('Error removing nginx configurations of "%s":', app.fqdn, error);
|
||||
|
||||
reload(callback);
|
||||
reload(callback);
|
||||
});
|
||||
}
|
||||
|
||||
function renewCerts(options, auditSource, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
apps.getAll(function (error, allApps) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var appDomains = [];
|
||||
|
||||
// add webadmin domain
|
||||
appDomains.push({ domain: config.adminDomain(), fqdn: config.adminFqdn(), type: 'webadmin', nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, constants.NGINX_ADMIN_CONFIG_FILE_NAME) });
|
||||
|
||||
// add app main
|
||||
allApps.forEach(function (app) {
|
||||
appDomains.push({ domain: app.domain, fqdn: app.fqdn, type: 'main', app: app, nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf') });
|
||||
|
||||
app.alternateDomains.forEach(function (alternateDomain) {
|
||||
let nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, `${app.id}-redirect-${alternateDomain.fqdn}.conf`);
|
||||
appDomains.push({ domain: alternateDomain.domain, fqdn: alternateDomain.fqdn, type: 'alternate', app: app, nginxConfigFilename: nginxConfigFilename });
|
||||
});
|
||||
});
|
||||
|
||||
if (options.domain) appDomains = appDomains.filter(function (appDomain) { return appDomain.domain === options.domain; });
|
||||
|
||||
async.eachSeries(appDomains, function (appDomain, iteratorCallback) {
|
||||
ensureCertificate(appDomain.fqdn, appDomain.domain, auditSource, function (error, bundle) {
|
||||
if (error) return iteratorCallback(error); // this can happen if cloudron is not setup yet
|
||||
|
||||
// hack to check if the app's cert changed or not. this doesn't handle prod/staging le change since they use same file name
|
||||
let currentNginxConfig = safe.fs.readFileSync(appDomain.nginxConfigFilename, 'utf8') || '';
|
||||
if (currentNginxConfig.includes(bundle.certFilePath)) return iteratorCallback();
|
||||
|
||||
debug(`renewCerts: creating new nginx config since ${appDomain.nginxConfigFilename} does not have ${bundle.certFilePath}`);
|
||||
|
||||
// reconfigure since the cert changed
|
||||
var configureFunc;
|
||||
if (appDomain.type === 'webadmin') configureFunc = writeAdminConfig.bind(null, bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn());
|
||||
else if (appDomain.type === 'main') configureFunc = writeAppConfig.bind(null, appDomain.app, bundle);
|
||||
else if (appDomain.type === 'alternate') configureFunc = writeAppRedirectConfig.bind(null, appDomain.app, appDomain.fqdn, bundle);
|
||||
else return callback(new Error(`Unknown domain type for ${appDomain.fqdn}. This should never happen`));
|
||||
|
||||
configureFunc(function (ignoredError) {
|
||||
if (ignoredError) debug('renewAll: error reconfiguring app', ignoredError);
|
||||
|
||||
platform.handleCertChanged(appDomain.fqdn);
|
||||
|
||||
iteratorCallback(); // move to next domain
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renewAll(auditSource, callback) {
|
||||
@@ -379,41 +558,19 @@ function renewAll(auditSource, callback) {
|
||||
|
||||
debug('renewAll: Checking certificates for renewal');
|
||||
|
||||
apps.getAll(function (error, allApps) {
|
||||
if (error) return callback(error);
|
||||
|
||||
allApps.push({ domain: config.adminDomain(), fqdn: config.adminFqdn() }); // inject fake webadmin app
|
||||
|
||||
async.eachSeries(allApps, function (app, iteratorCallback) {
|
||||
ensureCertificate(app, auditSource, function (error, bundle) {
|
||||
if (error) return iteratorCallback(error); // this can happen if cloudron is not setup yet
|
||||
if (bundle.reason !== 'new-le' && bundle.reason !== 'fallback') return iteratorCallback();
|
||||
|
||||
// reconfigure for the case where we got a renewed cert after fallback
|
||||
var configureFunc = app.fqdn === config.adminFqdn() ?
|
||||
configureAdminInternal.bind(null, bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn())
|
||||
: configureAppInternal.bind(null, app, bundle);
|
||||
|
||||
configureFunc(function (ignoredError) {
|
||||
if (ignoredError) debug('fallbackExpiredCertificates: error reconfiguring app', ignoredError);
|
||||
|
||||
platform.handleCertChanged(app.fqdn);
|
||||
|
||||
iteratorCallback(); // move to next app
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
renewCerts({}, auditSource, callback);
|
||||
}
|
||||
|
||||
function removeAppConfigs() {
|
||||
for (var appConfigFile of fs.readdirSync(paths.NGINX_APPCONFIG_DIR)) {
|
||||
fs.unlinkSync(path.join(paths.NGINX_APPCONFIG_DIR, appConfigFile));
|
||||
for (let appConfigFile of fs.readdirSync(paths.NGINX_APPCONFIG_DIR)) {
|
||||
if (appConfigFile !== constants.NGINX_DEFAULT_CONFIG_FILE_NAME && appConfigFile !== constants.NGINX_ADMIN_CONFIG_FILE_NAME) {
|
||||
fs.unlinkSync(path.join(paths.NGINX_APPCONFIG_DIR, appConfigFile));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function configureDefaultServer(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var certFilePath = path.join(paths.NGINX_CERT_DIR, 'default.cert');
|
||||
var keyFilePath = path.join(paths.NGINX_CERT_DIR, 'default.key');
|
||||
@@ -426,7 +583,7 @@ function configureDefaultServer(callback) {
|
||||
safe.child_process.execSync(certCommand);
|
||||
}
|
||||
|
||||
configureAdminInternal({ certFilePath, keyFilePath }, 'default.conf', '', function (error) {
|
||||
writeAdminConfig({ certFilePath, keyFilePath }, constants.NGINX_DEFAULT_CONFIG_FILE_NAME, '', function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('configureDefaultServer: done');
|
||||
|
||||
+104
-11
@@ -1,14 +1,100 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
initialize: initialize,
|
||||
uninitialize: uninitialize,
|
||||
|
||||
isUnmanaged: isUnmanaged,
|
||||
|
||||
scope: scope,
|
||||
websocketAuth: websocketAuth
|
||||
};
|
||||
|
||||
var accesscontrol = require('../accesscontrol.js'),
|
||||
assert = require('assert'),
|
||||
BasicStrategy = require('passport-http').BasicStrategy,
|
||||
BearerStrategy = require('passport-http-bearer').Strategy,
|
||||
clients = require('../clients.js'),
|
||||
ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy,
|
||||
ClientsError = clients.ClientsError,
|
||||
config = require('../config.js'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
passport = require('passport');
|
||||
LocalStrategy = require('passport-local').Strategy,
|
||||
passport = require('passport'),
|
||||
users = require('../users.js'),
|
||||
UsersError = users.UsersError;
|
||||
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// serialize user into session
|
||||
passport.serializeUser(function (user, callback) {
|
||||
callback(null, user.id);
|
||||
});
|
||||
|
||||
// deserialize user from session
|
||||
passport.deserializeUser(function(userId, callback) {
|
||||
users.get(userId, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
});
|
||||
|
||||
// used when username/password is sent in request body. used in CLI tool login route
|
||||
passport.use(new LocalStrategy(function (username, password, callback) {
|
||||
if (username.indexOf('@') === -1) {
|
||||
users.verifyWithUsername(username, password, function (error, result) {
|
||||
if (error && error.reason === UsersError.NOT_FOUND) return callback(null, false);
|
||||
if (error && error.reason === UsersError.WRONG_PASSWORD) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
if (!result) return callback(null, false);
|
||||
callback(null, result);
|
||||
});
|
||||
} else {
|
||||
users.verifyWithEmail(username, password, function (error, result) {
|
||||
if (error && error.reason === UsersError.NOT_FOUND) return callback(null, false);
|
||||
if (error && error.reason === UsersError.WRONG_PASSWORD) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
if (!result) return callback(null, false);
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
// Used to authenticate a OAuth2 client which uses clientId and clientSecret in the Authorization header
|
||||
passport.use(new BasicStrategy(function (clientId, clientSecret, callback) {
|
||||
clients.get(clientId, function (error, client) {
|
||||
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
if (client.clientSecret !== clientSecret) return callback(null, false);
|
||||
callback(null, client);
|
||||
});
|
||||
}));
|
||||
|
||||
// Used to authenticate a OAuth2 client which uses clientId and clientSecret in the request body (client_id, client_secret)
|
||||
passport.use(new ClientPasswordStrategy(function (clientId, clientSecret, callback) {
|
||||
clients.get(clientId, function(error, client) {
|
||||
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
|
||||
if (error) { return callback(error); }
|
||||
if (client.clientSecret !== clientSecret) { return callback(null, false); }
|
||||
callback(null, client);
|
||||
});
|
||||
}));
|
||||
|
||||
// used for "Authorization: Bearer token" or access_token query param authentication
|
||||
passport.use(new BearerStrategy(function (token, callback) {
|
||||
accesscontrol.validateToken(token, callback);
|
||||
}));
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
// The scope middleware provides an auth middleware for routes.
|
||||
//
|
||||
@@ -19,16 +105,18 @@ var accesscontrol = require('../accesscontrol.js'),
|
||||
// See server.js:
|
||||
// var profileScope = routes.oauth2.scope('profile');
|
||||
//
|
||||
function scope(requestedScope) {
|
||||
assert.strictEqual(typeof requestedScope, 'string');
|
||||
function scope(requiredScope) {
|
||||
assert.strictEqual(typeof requiredScope, 'string');
|
||||
|
||||
var requestedScopes = requestedScope.split(',');
|
||||
var requiredScopes = requiredScope.split(',');
|
||||
|
||||
return [
|
||||
passport.authenticate(['bearer'], { session: false }),
|
||||
|
||||
function (req, res, next) {
|
||||
var error = accesscontrol.validateRequestedScopes(req.authInfo || null, requestedScopes);
|
||||
assert(req.authInfo && typeof req.authInfo === 'object');
|
||||
|
||||
var error = accesscontrol.hasScopes(req.authInfo.authorizedScopes, requiredScopes);
|
||||
if (error) return next(new HttpError(403, error.message));
|
||||
|
||||
next();
|
||||
@@ -36,21 +124,26 @@ function scope(requestedScope) {
|
||||
];
|
||||
}
|
||||
|
||||
function websocketAuth(requestedScopes, req, res, next) {
|
||||
assert(Array.isArray(requestedScopes));
|
||||
function websocketAuth(requiredScopes, req, res, next) {
|
||||
assert(Array.isArray(requiredScopes));
|
||||
|
||||
if (typeof req.query.access_token !== 'string') return next(new HttpError(401, 'Unauthorized'));
|
||||
|
||||
accesscontrol.accessTokenAuth(req.query.access_token, function (error, user, info) {
|
||||
accesscontrol.validateToken(req.query.access_token, function (error, user, info) {
|
||||
if (error) return next(new HttpError(500, error.message));
|
||||
if (!user) return next(new HttpError(401, 'Unauthorized'));
|
||||
|
||||
req.user = user;
|
||||
req.authInfo = info;
|
||||
|
||||
var e = accesscontrol.validateRequestedScopes(req.authInfo, requestedScopes);
|
||||
if (e) return next(new HttpError(401, e.message));
|
||||
var e = accesscontrol.hasScopes(info.authorizedScopes, requiredScopes);
|
||||
if (e) return next(new HttpError(403, e.message));
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
function isUnmanaged(req, res, next) {
|
||||
if (!config.isManaged()) return next();
|
||||
|
||||
next(new HttpError(401, 'Managed instance does not permit this operation'));
|
||||
}
|
||||
|
||||
+70
-23
@@ -1,9 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
verifyOwnership: verifyOwnership,
|
||||
|
||||
getApp: getApp,
|
||||
getApps: getApps,
|
||||
getAllByUser: getAllByUser,
|
||||
getAppIcon: getAppIcon,
|
||||
installApp: installApp,
|
||||
configureApp: configureApp,
|
||||
@@ -22,6 +23,8 @@ exports = module.exports = {
|
||||
|
||||
cloneApp: cloneApp,
|
||||
|
||||
setOwner: setOwner,
|
||||
|
||||
uploadFile: uploadFile,
|
||||
downloadFile: downloadFile
|
||||
};
|
||||
@@ -29,6 +32,7 @@ exports = module.exports = {
|
||||
var apps = require('../apps.js'),
|
||||
AppsError = apps.AppsError,
|
||||
assert = require('assert'),
|
||||
config = require('../config.js'),
|
||||
debug = require('debug')('box:routes/apps'),
|
||||
fs = require('fs'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
@@ -43,6 +47,25 @@ function auditSource(req) {
|
||||
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');
|
||||
|
||||
@@ -50,29 +73,17 @@ function getApp(req, res, next) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, apps.removeInternalAppFields(app)));
|
||||
next(new HttpSuccess(200, apps.removeInternalFields(app)));
|
||||
});
|
||||
}
|
||||
|
||||
function getApps(req, res, next) {
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
|
||||
apps.getAll(function (error, allApps) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
allApps = allApps.map(apps.removeInternalAppFields);
|
||||
|
||||
next(new HttpSuccess(200, { apps: allApps }));
|
||||
});
|
||||
}
|
||||
|
||||
function getAllByUser(req, res, next) {
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
|
||||
apps.getAllByUser(req.user, function (error, allApps) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
allApps = allApps.map(apps.removeInternalAppFields);
|
||||
allApps = allApps.map(apps.removeRestrictedFields);
|
||||
|
||||
next(new HttpSuccess(200, { apps: allApps }));
|
||||
});
|
||||
@@ -92,6 +103,7 @@ function installApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
var data = req.body;
|
||||
data.ownerId = req.user.id;
|
||||
|
||||
// atleast one
|
||||
if ('manifest' in data && typeof data.manifest !== 'object') return next(new HttpError(400, 'manifest must be an object'));
|
||||
@@ -127,9 +139,19 @@ function installApp(req, res, next) {
|
||||
|
||||
if (data.robotsTxt && typeof data.robotsTxt !== 'string') return next(new HttpError(400, 'robotsTxt must be a string'));
|
||||
|
||||
if ('alternateDomains' in data) {
|
||||
if (!Array.isArray(data.alternateDomains)) return next(new HttpError(400, 'alternateDomains must be an array'));
|
||||
if (data.alternateDomains.some(function (d) { return (typeof d.domain !== 'string' || typeof d.subdomain !== 'string'); })) return next(new HttpError(400, 'alternateDomains array must contain objects with domain and subdomain strings'));
|
||||
}
|
||||
|
||||
if ('env' in data) {
|
||||
if (!data.env || typeof data.env !== 'object') return next(new HttpError(400, 'env must be an object'));
|
||||
if (Object.keys(data.env).some(function (key) { return typeof data.env[key] !== 'string'; })) return next(new HttpError(400, 'env must contain values as strings'));
|
||||
}
|
||||
|
||||
debug('Installing app :%j', data);
|
||||
|
||||
apps.install(data, auditSource(req), function (error, app) {
|
||||
apps.install(data, req.user, auditSource(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.'));
|
||||
@@ -137,7 +159,7 @@ function installApp(req, res, next) {
|
||||
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.BAD_CERTIFICATE) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(503, error.message));
|
||||
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, app));
|
||||
@@ -172,9 +194,19 @@ function configureApp(req, res, next) {
|
||||
|
||||
if ('mailboxName' in data && typeof data.mailboxName !== 'string') return next(new HttpError(400, 'mailboxName must be a string'));
|
||||
|
||||
if ('alternateDomains' in data) {
|
||||
if (!Array.isArray(data.alternateDomains)) return next(new HttpError(400, 'alternateDomains must be an array'));
|
||||
if (data.alternateDomains.some(function (d) { return (typeof d.domain !== 'string' || typeof d.subdomain !== 'string'); })) return next(new HttpError(400, 'alternateDomains array must contain objects with domain and subdomain strings'));
|
||||
}
|
||||
|
||||
if ('env' in data) {
|
||||
if (!data.env || typeof data.env !== 'object') return next(new HttpError(400, 'env must be an object'));
|
||||
if (Object.keys(data.env).some(function (key) { return typeof data.env[key] !== 'string'; })) return next(new HttpError(400, 'env must contain values as strings'));
|
||||
}
|
||||
|
||||
debug('Configuring app id:%s data:%j', req.params.id, data);
|
||||
|
||||
apps.configure(req.params.id, data, auditSource(req), function (error) {
|
||||
apps.configure(req.params.id, data, req.user, auditSource(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.'));
|
||||
@@ -215,6 +247,7 @@ function cloneApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
var data = req.body;
|
||||
data.ownerId = req.user.id;
|
||||
|
||||
debug('Clone app id:%s', req.params.id);
|
||||
|
||||
@@ -223,7 +256,7 @@ 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, auditSource(req), function (error, result) {
|
||||
apps.clone(req.params.id, data, req.user, auditSource(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.'));
|
||||
@@ -247,7 +280,7 @@ function backupApp(req, res, next) {
|
||||
apps.backup(req.params.id, function (error) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(503, error));
|
||||
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, { }));
|
||||
@@ -480,7 +513,7 @@ function execWebSocket(req, res, next) {
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
console.log('Connected to terminal');
|
||||
debug('Connected to terminal');
|
||||
|
||||
req.clearTimeout();
|
||||
|
||||
@@ -488,7 +521,7 @@ function execWebSocket(req, res, next) {
|
||||
duplexStream.on('end', function () { ws.close(); });
|
||||
duplexStream.on('close', function () { ws.close(); });
|
||||
duplexStream.on('error', function (error) {
|
||||
console.error('duplexStream error:', error);
|
||||
debug('duplexStream error:', error);
|
||||
});
|
||||
duplexStream.on('data', function (data) {
|
||||
if (ws.readyState !== WebSocket.OPEN) return;
|
||||
@@ -496,7 +529,7 @@ function execWebSocket(req, res, next) {
|
||||
});
|
||||
|
||||
ws.on('error', function (error) {
|
||||
console.error('websocket error:', error);
|
||||
debug('websocket error:', error);
|
||||
});
|
||||
ws.on('message', function (msg) {
|
||||
duplexStream.write(msg);
|
||||
@@ -565,3 +598,17 @@ function downloadFile(req, res, next) {
|
||||
stream.pipe(res);
|
||||
});
|
||||
}
|
||||
|
||||
function setOwner(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.ownerId !== 'string') return next(new HttpError(400, 'ownerId must be a string'));
|
||||
|
||||
apps.setOwner(req.params.id, req.body.ownerId, function (error) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, { }));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ function get(req, res, next) {
|
||||
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
|
||||
|
||||
backups.getByStatePaged(backupdb.BACKUP_STATE_NORMAL, page, perPage, function (error, result) {
|
||||
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return next(new HttpError(503, error.message));
|
||||
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, { backups: result }));
|
||||
|
||||
+19
-1
@@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
getConfig: getConfig,
|
||||
changePlan: changePlan
|
||||
};
|
||||
|
||||
@@ -12,8 +13,25 @@ var caas = require('../caas.js'),
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
_ = require('underscore');
|
||||
|
||||
function getConfig(req, res, next) {
|
||||
if (config.provider() !== 'caas') return next(new HttpError(422, 'Cannot use this API with this provider'));
|
||||
|
||||
caas.getBoxAndUserDetails(function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
// the result is { box: { region, size, plan }, user: { billing, currency } }
|
||||
next(new HttpSuccess(200, {
|
||||
region: result.box.region,
|
||||
size: result.box.size,
|
||||
billing: !!result.user.billing,
|
||||
plan: result.box.plan,
|
||||
currency: result.user.currency
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
function changePlan(req, res, next) {
|
||||
if (config.provider() !== 'caas') return next(new HttpError(422, 'Cannot use migrate API with this provider'));
|
||||
if (config.provider() !== 'caas') return next(new HttpError(422, 'Cannot use this API with this provider'));
|
||||
|
||||
if ('size' in req.body && typeof req.body.size !== 'string') return next(new HttpError(400, 'size must be string'));
|
||||
if ('region' in req.body && typeof req.body.region !== 'string') return next(new HttpError(400, 'region must be string'));
|
||||
|
||||
@@ -80,8 +80,9 @@ function addToken(req, res, next) {
|
||||
var data = req.body;
|
||||
var expiresAt = data.expiresAt ? parseInt(data.expiresAt, 10) : Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
|
||||
if (isNaN(expiresAt) || expiresAt <= Date.now()) return next(new HttpError(400, 'expiresAt must be a timestamp in the future'));
|
||||
if ('name' in req.body && typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be a string'));
|
||||
|
||||
clients.addTokenByUserId(req.params.clientId, req.user.id, expiresAt, function (error, result) {
|
||||
clients.addTokenByUserId(req.params.clientId, req.user.id, expiresAt, { name: req.body.name || '' }, function (error, result) {
|
||||
if (error && error.reason === ClientsError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
next(new HttpSuccess(201, { token: result }));
|
||||
|
||||
+24
-5
@@ -5,11 +5,13 @@ exports = module.exports = {
|
||||
getProgress: getProgress,
|
||||
getConfig: getConfig,
|
||||
getDisks: getDisks,
|
||||
getUpdateInfo: getUpdateInfo,
|
||||
update: update,
|
||||
feedback: feedback,
|
||||
checkForUpdates: checkForUpdates,
|
||||
getLogs: getLogs,
|
||||
getLogStream: getLogStream
|
||||
getLogStream: getLogStream,
|
||||
getStatus: getStatus,
|
||||
};
|
||||
|
||||
var appstore = require('../appstore.js'),
|
||||
@@ -21,7 +23,9 @@ var appstore = require('../appstore.js'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
progress = require('../progress.js'),
|
||||
updater = require('../updater.js'),
|
||||
updateChecker = require('../updatechecker.js'),
|
||||
UpdaterError = require('../updater.js').UpdaterError,
|
||||
_ = require('underscore');
|
||||
|
||||
function auditSource(req) {
|
||||
@@ -57,17 +61,24 @@ function getDisks(req, res, next) {
|
||||
|
||||
function update(req, res, next) {
|
||||
// this only initiates the update, progress can be checked via the progress route
|
||||
cloudron.updateToLatest(auditSource(req), function (error) {
|
||||
if (error && error.reason === CloudronError.ALREADY_UPTODATE) return next(new HttpError(422, error.message));
|
||||
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === CloudronError.SELF_UPGRADE_NOT_SUPPORTED) return next(new HttpError(412, error.message));
|
||||
updater.updateToLatest(auditSource(req), function (error) {
|
||||
if (error && error.reason === UpdaterError.ALREADY_UPTODATE) return next(new HttpError(422, error.message));
|
||||
if (error && error.reason === UpdaterError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === UpdaterError.SELF_UPGRADE_NOT_SUPPORTED) return next(new HttpError(412, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function getUpdateInfo(req, res, next) {
|
||||
next(new HttpSuccess(200, { update: updateChecker.getUpdateInfo() }));
|
||||
}
|
||||
|
||||
function checkForUpdates(req, res, next) {
|
||||
// it can take a while sometimes to get all the app updates one by one
|
||||
req.clearTimeout();
|
||||
|
||||
async.series([
|
||||
updateChecker.checkAppUpdates,
|
||||
updateChecker.checkBoxUpdates
|
||||
@@ -159,3 +170,11 @@ function getLogStream(req, res, next) {
|
||||
logStream.on('error', res.end.bind(res, null));
|
||||
});
|
||||
}
|
||||
|
||||
function getStatus(req, res, next) {
|
||||
cloudron.getStatus(function (error, status) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, status));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@ function login(req, res, next) {
|
||||
if (!verified) return next(new HttpError(401, 'Invalid totpToken'));
|
||||
}
|
||||
|
||||
clients.issueDeveloperToken(user, ip, function (error, result) {
|
||||
const auditSource = { authType: 'cli', ip: ip };
|
||||
clients.issueDeveloperToken(user, auditSource, function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, result));
|
||||
|
||||
+83
-15
@@ -5,7 +5,11 @@ exports = module.exports = {
|
||||
get: get,
|
||||
getAll: getAll,
|
||||
update: update,
|
||||
del: del
|
||||
del: del,
|
||||
|
||||
renewCerts: renewCerts,
|
||||
|
||||
verifyDomainLock: verifyDomainLock
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
@@ -14,23 +18,56 @@ var assert = require('assert'),
|
||||
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 };
|
||||
}
|
||||
|
||||
// this code exists for the hosting provider edition
|
||||
function verifyDomainLock(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.domain, 'string');
|
||||
|
||||
if (domains.isLocked(req.params.domain)) return next(new HttpError(423, 'This domain is locked'));
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
function add(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
|
||||
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider must be a string'));
|
||||
if (typeof req.body.config !== 'object') return next(new HttpError(400, 'config must be an object'));
|
||||
|
||||
if (!req.body.config || typeof req.body.config !== 'object') return next(new HttpError(400, 'config must be an object'));
|
||||
if ('hyphenatedSubdomains' in req.body.config && typeof req.body.config.hyphenatedSubdomains !== 'boolean') return next(new HttpError(400, 'hyphenatedSubdomains must be a boolean'));
|
||||
if ('wildcard' in req.body.config && typeof req.body.config.wildcard !== 'boolean') return next(new HttpError(400, 'wildcard must be a boolean'));
|
||||
|
||||
if ('zoneName' in req.body && typeof req.body.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be a string'));
|
||||
if ('fallbackCertificate' in req.body && typeof req.body.fallbackCertificate !== 'object') return next(new HttpError(400, 'fallbackCertificate must be a object with cert and key strings'));
|
||||
if (req.body.fallbackCertificate && (!req.body.cert || typeof req.body.cert !== 'string')) return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
|
||||
if (req.body.fallbackCertificate && (!req.body.key || typeof req.body.key !== 'string')) return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
|
||||
if ('tlsConfig' in req.body && typeof req.body.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be a object with a provider string property'));
|
||||
if (req.body.tlsConfig && (!req.body.tlsConfig.provider || typeof req.body.tlsConfig.provider !== 'string')) return next(new HttpError(400, 'tlsConfig.provider must be a string'));
|
||||
if (req.body.fallbackCertificate) {
|
||||
let fallbackCertificate = req.body.fallbackCertificate;
|
||||
if (!fallbackCertificate.cert || typeof fallbackCertificate.cert !== 'string') return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
|
||||
if (!fallbackCertificate.key || typeof fallbackCertificate.key !== 'string') return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
|
||||
if ('restricted' in fallbackCertificate && typeof fallbackCertificate.restricted !== 'boolean') return next(new HttpError(400, 'fallbackCertificate.restricted must be a boolean'));
|
||||
}
|
||||
|
||||
if ('tlsConfig' in req.body) {
|
||||
if (!req.body.tlsConfig || typeof req.body.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be a object with a provider string property'));
|
||||
if (!req.body.tlsConfig.provider || typeof req.body.tlsConfig.provider !== 'string') return next(new HttpError(400, 'tlsConfig.provider must be a string'));
|
||||
}
|
||||
|
||||
// some DNS providers like DigitalOcean take a really long time to verify credentials (https://github.com/expressjs/timeout/issues/26)
|
||||
req.clearTimeout();
|
||||
|
||||
domains.add(req.body.domain, req.body.zoneName || '', req.body.provider, req.body.config, req.body.fallbackCertificate || null, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
|
||||
let data = {
|
||||
zoneName: req.body.zoneName || '',
|
||||
provider: req.body.provider,
|
||||
config: req.body.config,
|
||||
fallbackCertificate: req.body.fallbackCertificate || null,
|
||||
tlsConfig: req.body.tlsConfig || { provider: 'letsencrypt-prod' }
|
||||
};
|
||||
|
||||
domains.add(req.body.domain, data, auditSource(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));
|
||||
@@ -55,7 +92,7 @@ function getAll(req, res, next) {
|
||||
domains.getAll(function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
result = result.map(domains.removePrivateFields);
|
||||
result = result.map(domains.removeRestrictedFields);
|
||||
|
||||
next(new HttpSuccess(200, { domains: result }));
|
||||
});
|
||||
@@ -66,18 +103,37 @@ function update(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider must be an object'));
|
||||
if (typeof req.body.config !== 'object') return next(new HttpError(400, 'config must be an object'));
|
||||
|
||||
if (!req.body.config || typeof req.body.config !== 'object') return next(new HttpError(400, 'config must be an object'));
|
||||
if ('hyphenatedSubdomains' in req.body.config && typeof req.body.config.hyphenatedSubdomains !== 'boolean') return next(new HttpError(400, 'hyphenatedSubdomains must be a boolean'));
|
||||
if ('wildcard' in req.body.config && typeof req.body.config.wildcard !== 'boolean') return next(new HttpError(400, 'wildcard must be a boolean'));
|
||||
|
||||
if ('zoneName' in req.body && typeof req.body.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be a string'));
|
||||
if ('fallbackCertificate' in req.body && typeof req.body.fallbackCertificate !== 'object') return next(new HttpError(400, 'fallbackCertificate must be a object with cert and key strings'));
|
||||
if (req.body.fallbackCertificate && (!req.body.fallbackCertificate.cert || typeof req.body.fallbackCertificate.cert !== 'string')) return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
|
||||
if (req.body.fallbackCertificate && (!req.body.fallbackCertificate.key || typeof req.body.fallbackCertificate.key !== 'string')) return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
|
||||
if ('tlsConfig' in req.body && typeof req.body.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be a object with a provider string property'));
|
||||
if (req.body.tlsConfig && (!req.body.tlsConfig.provider || typeof req.body.tlsConfig.provider !== 'string')) return next(new HttpError(400, 'tlsConfig.provider must be a string'));
|
||||
if (req.body.fallbackCertificate) {
|
||||
let fallbackCertificate = req.body.fallbackCertificate;
|
||||
if (!fallbackCertificate.cert || typeof fallbackCertificate.cert !== 'string') return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
|
||||
if (!fallbackCertificate.key || typeof fallbackCertificate.key !== 'string') return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
|
||||
if ('restricted' in fallbackCertificate && typeof fallbackCertificate.restricted !== 'boolean') return next(new HttpError(400, 'fallbackCertificate.restricted must be a boolean'));
|
||||
}
|
||||
|
||||
if ('tlsConfig' in req.body) {
|
||||
if (!req.body.tlsConfig || typeof req.body.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be a object with a provider string property'));
|
||||
if (!req.body.tlsConfig.provider || typeof req.body.tlsConfig.provider !== 'string') return next(new HttpError(400, 'tlsConfig.provider must be a string'));
|
||||
}
|
||||
|
||||
// some DNS providers like DigitalOcean take a really long time to verify credentials (https://github.com/expressjs/timeout/issues/26)
|
||||
req.clearTimeout();
|
||||
|
||||
domains.update(req.params.domain, req.body.zoneName || '', req.body.provider, req.body.config, req.body.fallbackCertificate || null, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
|
||||
let data = {
|
||||
zoneName: req.body.zoneName || '',
|
||||
provider: req.body.provider,
|
||||
config: req.body.config,
|
||||
fallbackCertificate: req.body.fallbackCertificate || null,
|
||||
tlsConfig: req.body.tlsConfig || { provider: 'letsencrypt-prod' }
|
||||
};
|
||||
|
||||
domains.update(req.params.domain, data, auditSource(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));
|
||||
@@ -90,7 +146,7 @@ function update(req, res, next) {
|
||||
function del(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.domain, 'string');
|
||||
|
||||
domains.del(req.params.domain, function (error) {
|
||||
domains.del(req.params.domain, auditSource(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));
|
||||
@@ -99,3 +155,15 @@ function del(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function renewCerts(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.domain, 'string');
|
||||
|
||||
domains.renewCerts(req.params.domain, auditSource(req), function (error) {
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
}
|
||||
|
||||
+17
-3
@@ -4,6 +4,7 @@ exports = module.exports = {
|
||||
get: get,
|
||||
list: list,
|
||||
create: create,
|
||||
update: update,
|
||||
remove: remove,
|
||||
updateMembers: updateMembers
|
||||
};
|
||||
@@ -12,7 +13,6 @@ var assert = require('assert'),
|
||||
groups = require('../groups.js'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
groups = require('../groups.js'),
|
||||
GroupsError = groups.GroupsError;
|
||||
|
||||
function create(req, res, next) {
|
||||
@@ -45,6 +45,20 @@ function get(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function update(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.groupId, 'string');
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if ('name' in req.body && typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be a string'));
|
||||
|
||||
groups.update(req.params.groupId, req.body, function (error) {
|
||||
if (error && error.reason === GroupsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, { }));
|
||||
});
|
||||
}
|
||||
|
||||
function updateMembers(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.groupId, 'string');
|
||||
|
||||
@@ -55,12 +69,12 @@ function updateMembers(req, res, next) {
|
||||
if (error && error.reason === GroupsError.NOT_FOUND) return next(new HttpError(404, 'Invalid group or user id'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200));
|
||||
next(new HttpSuccess(200, { }));
|
||||
});
|
||||
}
|
||||
|
||||
function list(req, res, next) {
|
||||
groups.getAllWithMembers(function (error, result) {
|
||||
groups.getAll(function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, { groups: result }));
|
||||
|
||||
@@ -19,6 +19,5 @@ exports = module.exports = {
|
||||
sysadmin: require('./sysadmin.js'),
|
||||
settings: require('./settings.js'),
|
||||
ssh: require('./ssh.js'),
|
||||
user: require('./user.js'),
|
||||
users: require('./users.js')
|
||||
};
|
||||
|
||||
+20
-9
@@ -4,9 +4,10 @@ exports = module.exports = {
|
||||
getDomain: getDomain,
|
||||
addDomain: addDomain,
|
||||
getDomainStats: getDomainStats,
|
||||
updateDomain: updateDomain,
|
||||
removeDomain: removeDomain,
|
||||
|
||||
setDnsRecords: setDnsRecords,
|
||||
|
||||
getStatus: getStatus,
|
||||
|
||||
setMailFromValidation: setMailFromValidation,
|
||||
@@ -43,6 +44,11 @@ var assert = require('assert'),
|
||||
|
||||
var mailProxy = middleware.proxy(url.parse('http://127.0.0.1:2020'));
|
||||
|
||||
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 getDomain(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.domain, 'string');
|
||||
|
||||
@@ -81,15 +87,20 @@ function getDomainStats(req, res, next) {
|
||||
mailProxy(req, res, next);
|
||||
}
|
||||
|
||||
function updateDomain(req, res, next) {
|
||||
function setDnsRecords(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.domain, 'string');
|
||||
|
||||
mail.updateDomain(req.params.domain, function (error) {
|
||||
// can take a setup all the DNS entries. this is mostly because some backends try to list DNS entries (DO)
|
||||
// for upsert and this takes a lot of time
|
||||
req.clearTimeout();
|
||||
|
||||
mail.setDnsRecords(req.params.domain, function (error) {
|
||||
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error && error.reason === MailError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202));
|
||||
next(new HttpSuccess(201));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -179,7 +190,7 @@ function setMailEnabled(req, res, next) {
|
||||
|
||||
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled is required'));
|
||||
|
||||
mail.setMailEnabled(req.params.domain, !!req.body.enabled, function (error) {
|
||||
mail.setMailEnabled(req.params.domain, !!req.body.enabled, auditSource(req), function (error) {
|
||||
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === MailError.BILLING_REQUIRED) return next(new HttpError(402, error.message));
|
||||
@@ -232,7 +243,7 @@ function addMailbox(req, res, next) {
|
||||
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be a string'));
|
||||
if (typeof req.body.userId !== 'string') return next(new HttpError(400, 'userId must be a string'));
|
||||
|
||||
mail.addMailbox(req.body.name, req.params.domain, req.body.userId, function (error) {
|
||||
mail.addMailbox(req.body.name, req.params.domain, req.body.userId, auditSource(req), function (error) {
|
||||
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error && error.reason === MailError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
@@ -261,7 +272,7 @@ function removeMailbox(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.domain, 'string');
|
||||
assert.strictEqual(typeof req.params.name, 'string');
|
||||
|
||||
mail.removeMailbox(req.params.name, req.params.domain, function (error) {
|
||||
mail.removeMailbox(req.params.name, req.params.domain, auditSource(req), function (error) {
|
||||
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
@@ -347,7 +358,7 @@ function addList(req, res, next) {
|
||||
if (typeof req.body.members[i] !== 'string') return next(new HttpError(400, 'member must be a string'));
|
||||
}
|
||||
|
||||
mail.addList(req.body.name, req.params.domain, req.body.members, function (error) {
|
||||
mail.addList(req.body.name, req.params.domain, req.body.members, auditSource(req), function (error) {
|
||||
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error && error.reason === MailError.ALREADY_EXISTS) return next(new HttpError(409, 'list already exists'));
|
||||
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
@@ -380,7 +391,7 @@ function removeList(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.domain, 'string');
|
||||
assert.strictEqual(typeof req.params.name, 'string');
|
||||
|
||||
mail.removeList(req.params.domain, req.params.name, function (error) {
|
||||
mail.removeList(req.params.name, req.params.domain, auditSource(req), function (error) {
|
||||
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
|
||||
+31
-59
@@ -19,8 +19,7 @@ exports = module.exports = {
|
||||
csrf: csrf
|
||||
};
|
||||
|
||||
var accesscontrol = require('../accesscontrol.js'),
|
||||
apps = require('../apps.js'),
|
||||
var apps = require('../apps.js'),
|
||||
assert = require('assert'),
|
||||
authcodedb = require('../authcodedb.js'),
|
||||
clients = require('../clients'),
|
||||
@@ -39,7 +38,6 @@ var accesscontrol = require('../accesscontrol.js'),
|
||||
session = require('connect-ensure-login'),
|
||||
settings = require('../settings.js'),
|
||||
speakeasy = require('speakeasy'),
|
||||
tokendb = require('../tokendb.js'),
|
||||
url = require('url'),
|
||||
users = require('../users.js'),
|
||||
UsersError = users.UsersError,
|
||||
@@ -52,19 +50,13 @@ function auditSource(req, appId, appObject) {
|
||||
return { authType: 'oauth', ip: ip, appId: appId, app: appObject };
|
||||
}
|
||||
|
||||
// create OAuth 2.0 server
|
||||
var gServer = null;
|
||||
|
||||
function initialize() {
|
||||
assert(gServer === null);
|
||||
assert.strictEqual(gServer, null);
|
||||
|
||||
gServer = oauth2orize.createServer();
|
||||
|
||||
// Register serialialization and deserialization functions.
|
||||
//
|
||||
// The client id is stored in the session and can thus be retrieved for each
|
||||
// step in the oauth flow transaction, which involves multiple http requests.
|
||||
|
||||
gServer.serializeClient(function (client, callback) {
|
||||
return callback(null, client.id);
|
||||
});
|
||||
@@ -73,16 +65,7 @@ function initialize() {
|
||||
clients.get(id, callback);
|
||||
});
|
||||
|
||||
|
||||
// Register supported grant types.
|
||||
|
||||
// Grant authorization codes. The callback takes the `client` requesting
|
||||
// authorization, the `redirectURI` (which is used as a verifier in the
|
||||
// subsequent exchange), the authenticated `user` granting access, and
|
||||
// their response, which contains approved scope, duration, etc. as parsed by
|
||||
// the application. The application issues a code, which is bound to these
|
||||
// values, and will be exchanged for an access token.
|
||||
|
||||
// grant authorization code that can be exchanged for access tokens. this is used by external oauth clients
|
||||
gServer.grant(oauth2orize.grant.code({ scopeSeparator: ',' }, function (client, redirectURI, user, ares, callback) {
|
||||
debug('grant code:', client.id, redirectURI, user.id, ares);
|
||||
|
||||
@@ -98,33 +81,8 @@ function initialize() {
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
gServer.grant(oauth2orize.grant.token({ scopeSeparator: ',' }, function (client, user, ares, callback) {
|
||||
debug('grant token:', client.id, user.id, ares);
|
||||
|
||||
var token = tokendb.generateToken();
|
||||
var expires = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
|
||||
var scope = accesscontrol.normalizeScope(user.scope, client.scope);
|
||||
|
||||
tokendb.add(token, user.id, client.id, expires, scope, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('grant token: new access token for client %s token %s', client.id, token);
|
||||
|
||||
callback(null, token);
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
// Exchange authorization codes for access tokens. The callback accepts the
|
||||
// `client`, which is exchanging `code` and any `redirectURI` from the
|
||||
// authorization request for verification. If these values are validated, the
|
||||
// application issues an access token on behalf of the user who authorized the
|
||||
// code.
|
||||
|
||||
// exchange authorization codes for access tokens. this is used by external oauth clients
|
||||
gServer.exchange(oauth2orize.exchange.code(function (client, code, redirectURI, callback) {
|
||||
debug('exchange:', client, code, redirectURI);
|
||||
|
||||
authcodedb.get(code, function (error, authCode) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
@@ -133,21 +91,28 @@ function initialize() {
|
||||
authcodedb.del(code, function (error) {
|
||||
if(error) return callback(error);
|
||||
|
||||
var token = tokendb.generateToken();
|
||||
var expires = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
|
||||
var scope = accesscontrol.canonicalScope(client.scope);
|
||||
|
||||
tokendb.add(token, authCode.userId, authCode.clientId, expires, client.scope, function (error) {
|
||||
clients.addTokenByUserId(client.id, authCode.userId, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('exchange: new access token for client %s token %s', client.id, token);
|
||||
debug('exchange: new access token for client %s user %s token %s', client.id, authCode.userId, result.accessToken.slice(0, 6)); // partial token for security
|
||||
|
||||
callback(null, token);
|
||||
callback(null, result.accessToken);
|
||||
});
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
// implicit token grant that skips issuing auth codes. this is used by our webadmin
|
||||
gServer.grant(oauth2orize.grant.token({ scopeSeparator: ',' }, function (client, user, ares, callback) {
|
||||
clients.addTokenByUserId(client.id, user.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('grant token: new access token for client %s user %s token %s', client.id, user.id, result.accessToken.slice(0, 6)); // partial token for security
|
||||
|
||||
callback(null, result.accessToken);
|
||||
});
|
||||
}));
|
||||
|
||||
// overwrite the session.ensureLoggedIn to not use res.redirect() due to a chrome bug not sending cookies on redirects
|
||||
session.ensureLoggedIn = function (redirectTo) {
|
||||
assert.strictEqual(typeof redirectTo, 'string');
|
||||
@@ -353,7 +318,7 @@ function renderAccountSetupSite(res, req, userObject, error) {
|
||||
csrf: req.csrfToken(),
|
||||
resetToken: req.query.reset_token || req.body.resetToken,
|
||||
email: req.query.email || req.body.email,
|
||||
title: 'Password Setup'
|
||||
title: 'Account Setup'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -395,12 +360,15 @@ function accountSetup(req, res, next) {
|
||||
userObject.displayName = req.body.displayName;
|
||||
|
||||
// setPassword clears the resetToken
|
||||
users.setPassword(userObject.id, req.body.password, function (error, result) {
|
||||
users.setPassword(userObject.id, req.body.password, function (error) {
|
||||
if (error && error.reason === UsersError.BAD_FIELD) return renderAccountSetupSite(res, req, userObject, error.message);
|
||||
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
res.redirect(config.adminOrigin());
|
||||
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
res.redirect(`${config.adminOrigin()}?accessToken=${result.accessToken}&expiresAt=${result.expires}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -444,7 +412,11 @@ function passwordReset(req, res, next) {
|
||||
if (error && error.reason === UsersError.BAD_FIELD) return next(new HttpError(406, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
res.redirect(config.adminOrigin());
|
||||
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
res.redirect(`${config.adminOrigin()}?accessToken=${result.accessToken}&expiresAt=${result.expires}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -532,7 +504,7 @@ function authorization() {
|
||||
function token() {
|
||||
return [
|
||||
passport.authenticate(['basic', 'oauth2-client-password'], { session: false }),
|
||||
gServer.token(),
|
||||
gServer.token(), // will call the token grant callback registered in initialize()
|
||||
gServer.errorHandler()
|
||||
];
|
||||
}
|
||||
|
||||
@@ -9,8 +9,7 @@ exports = module.exports = {
|
||||
disableTwoFactorAuthentication: disableTwoFactorAuthentication
|
||||
};
|
||||
|
||||
var accesscontrol = require('../accesscontrol.js'),
|
||||
assert = require('assert'),
|
||||
var assert = require('assert'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
users = require('../users.js'),
|
||||
@@ -30,10 +29,9 @@ function get(req, res, next) {
|
||||
username: req.user.username,
|
||||
email: req.user.email,
|
||||
fallbackEmail: req.user.fallbackEmail,
|
||||
admin: req.user.admin,
|
||||
scope: accesscontrol.canonicalScope(req.authInfo.scope), // this returns the token scope and not the user's scope
|
||||
displayName: req.user.displayName,
|
||||
twoFactorAuthenticationEnabled: req.user.twoFactorAuthenticationEnabled
|
||||
twoFactorAuthenticationEnabled: req.user.twoFactorAuthenticationEnabled,
|
||||
admin: req.user.admin
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -65,7 +63,7 @@ function changePassword(req, res, next) {
|
||||
|
||||
users.setPassword(req.user.id, req.body.newPassword, function (error) {
|
||||
if (error && error.reason === UsersError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(403, 'Wrong password'));
|
||||
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(204));
|
||||
@@ -91,7 +89,7 @@ function enableTwoFactorAuthentication(req, res, next) {
|
||||
|
||||
users.enableTwoFactorAuthentication(req.user.id, req.body.totpToken, function (error) {
|
||||
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
|
||||
if (error && error.reason === UsersError.BAD_TOKEN) return next(new HttpError(403, 'Invalid token'));
|
||||
if (error && error.reason === UsersError.BAD_TOKEN) return next(new HttpError(401, 'Invalid token'));
|
||||
if (error && error.reason === UsersError.ALREADY_EXISTS) return next(new HttpError(409, 'TwoFactor Authentication is already enabled'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
|
||||
+82
-3
@@ -20,10 +20,20 @@ exports = module.exports = {
|
||||
setTimeZone: setTimeZone,
|
||||
|
||||
getAppstoreConfig: getAppstoreConfig,
|
||||
setAppstoreConfig: setAppstoreConfig
|
||||
setAppstoreConfig: setAppstoreConfig,
|
||||
|
||||
getPlatformConfig: getPlatformConfig,
|
||||
setPlatformConfig: setPlatformConfig,
|
||||
|
||||
getDynamicDnsConfig: getDynamicDnsConfig,
|
||||
setDynamicDnsConfig: setDynamicDnsConfig,
|
||||
|
||||
setRegistryConfig: setRegistryConfig
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
docker = require('../docker.js'),
|
||||
DockerError = docker.DockerError,
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
safe = require('safetydance'),
|
||||
@@ -152,6 +162,7 @@ function setBackupConfig(req, res, next) {
|
||||
|
||||
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
|
||||
if (typeof req.body.retentionSecs !== 'number') return next(new HttpError(400, 'retentionSecs is required'));
|
||||
if (typeof req.body.intervalSecs !== 'number') return next(new HttpError(400, 'intervalSecs is required'));
|
||||
if ('key' in req.body && typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
|
||||
if ('syncConcurrency' in req.body) {
|
||||
if (typeof req.body.syncConcurrency !== 'number') return next(new HttpError(400, 'syncConcurrency must be a positive integer'));
|
||||
@@ -160,15 +171,68 @@ function setBackupConfig(req, res, next) {
|
||||
if (typeof req.body.format !== 'string') return next(new HttpError(400, 'format must be a string'));
|
||||
if ('acceptSelfSignedCerts' in req.body && typeof req.body.acceptSelfSignedCerts !== 'boolean') return next(new HttpError(400, 'format must be a boolean'));
|
||||
|
||||
// testing the backup using put/del takes a bit of time at times
|
||||
req.clearTimeout();
|
||||
|
||||
settings.setBackupConfig(req.body, function (error) {
|
||||
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === SettingsError.EXTERNAL_ERROR) return next(new HttpError(402, error.message));
|
||||
if (error && error.reason === SettingsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function getPlatformConfig(req, res, next) {
|
||||
settings.getPlatformConfig(function (error, config) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, config));
|
||||
});
|
||||
}
|
||||
|
||||
function setPlatformConfig(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
for (let addon of [ 'mysql', 'postgresql', 'mail', 'mongodb' ]) {
|
||||
if (!(addon in req.body)) continue;
|
||||
if (typeof req.body[addon] !== 'object') return next(new HttpError(400, 'addon config must be an object'));
|
||||
|
||||
if (typeof req.body[addon].memory !== 'number') return next(new HttpError(400, 'memory must be a number'));
|
||||
if (typeof req.body[addon].memorySwap !== 'number') return next(new HttpError(400, 'memorySwap must be a number'));
|
||||
}
|
||||
|
||||
settings.setPlatformConfig(req.body, function (error) {
|
||||
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === SettingsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function getDynamicDnsConfig(req, res, next) {
|
||||
settings.getDynamicDnsConfig(function (error, enabled) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, { enabled: enabled }));
|
||||
});
|
||||
}
|
||||
|
||||
function setDynamicDnsConfig(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled boolean is required'));
|
||||
|
||||
settings.setDynamicDnsConfig(req.body.enabled, function (error) {
|
||||
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, {}));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function getAppstoreConfig(req, res, next) {
|
||||
settings.getAppstoreConfig(function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
@@ -190,7 +254,7 @@ function setAppstoreConfig(req, res, next) {
|
||||
|
||||
settings.setAppstoreConfig(options, function (error) {
|
||||
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === SettingsError.EXTERNAL_ERROR) return next(new HttpError(406, error.message));
|
||||
if (error && error.reason === SettingsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
settings.getAppstoreConfig(function (error, result) {
|
||||
@@ -200,3 +264,18 @@ function setAppstoreConfig(req, res, next) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setRegistryConfig(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.serveraddress !== 'string') return next(new HttpError(400, 'serveraddress is required'));
|
||||
if ('username' in req.body && typeof req.body.username !== 'string') return next(new HttpError(400, 'username is required'));
|
||||
if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password is required'));
|
||||
|
||||
docker.setRegistryConfig(req.body, function (error) {
|
||||
if (error && error.reason === DockerError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200));
|
||||
});
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user