Compare commits
1578 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e90a211820 | |||
| 8529485837 | |||
| 6810d823f5 | |||
| 3e62f1913a | |||
| d23662c464 | |||
| 922c1ea317 | |||
| 258d81d7e9 | |||
| 1363e02603 | |||
| ccc65127f1 | |||
| 3b38bb5d33 | |||
| 59c51c5747 | |||
| ca17afc734 | |||
| 0b537fe163 | |||
| 2a32bf3fc7 | |||
| 57c4d47657 | |||
| 0371fe19ab | |||
| 3de8fd5d92 | |||
| ce86cb892d | |||
| 9789ae3374 | |||
| e508893dcc | |||
| 699f04c9ff | |||
| 89c82fb001 | |||
| b7fed04c12 | |||
| 0ec5714271 | |||
| 5e483e4f3a | |||
| 84374b955e | |||
| 3a25c8da9f | |||
| 5a5983cf96 | |||
| 71c44a4c44 | |||
| 41053d6857 | |||
| 4287642308 | |||
| 3934e59bd3 | |||
| 9080e5c3ab | |||
| 3d5599cdd9 | |||
| 138d01e755 | |||
| 213ce114e3 | |||
| ad8b9cfc9f | |||
| de400dd652 | |||
| 6218ee30a7 | |||
| 976f072ef4 | |||
| f4762be58b | |||
| 1b92ce08aa | |||
| 1d3d8288a9 | |||
| eec54e93bf | |||
| 77b965cada | |||
| bcc9eda66c | |||
| 3a0b9d7b3b | |||
| e511b70d8f | |||
| 25cc60e648 | |||
| d1e05dcb6f | |||
| 8cfd859711 | |||
| 7b3b826f87 | |||
| 195c9bd81f | |||
| a8928d26d1 | |||
| ef287d4436 | |||
| 6ae1de6989 | |||
| 9c810ce837 | |||
| ba913bb949 | |||
| 58487b729a | |||
| bf73cbaf97 | |||
| 1db868bf9c | |||
| d331597bff | |||
| 71648d92ae | |||
| 735485b539 | |||
| 09c8248e31 | |||
| c0b0029935 | |||
| 64af278f39 | |||
| 57dabbfc69 | |||
| 279f7a80c5 | |||
| b66fdb10f2 | |||
| 84c1703c1a | |||
| f324d50cef | |||
| 93a1e6fca8 | |||
| 4d55783ed8 | |||
| aad50fb5b2 | |||
| fb4ba5855b | |||
| fbe5f42536 | |||
| 7663360ce6 | |||
| 0a3aad0205 | |||
| cde42e5f92 | |||
| fd965072c5 | |||
| d703d1cd13 | |||
| bd9c664b1a | |||
| ae94ff1432 | |||
| b64acb412e | |||
| cbc5ec7d89 | |||
| 5401dc9e18 | |||
| 9b37597ac8 | |||
| 784c8b2bd2 | |||
| 2388fe5047 | |||
| 064eff0ac1 | |||
| b5c933494a | |||
| 8c0bd97064 | |||
| 2ca9534715 | |||
| 641704a741 | |||
| 82d88d375e | |||
| 751caa7b3b | |||
| 7e16128b11 | |||
| 008fa09877 | |||
| 045963afe5 | |||
| b799df3626 | |||
| 772df6f9af | |||
| 72cb383f2c | |||
| 625dc7c49b | |||
| 86916a94de | |||
| 71666a028b | |||
| 01e6301332 | |||
| 13050f7bff | |||
| bedcd6fccf | |||
| df8a71cd8b | |||
| a113ece22b | |||
| a63c2cfdf2 | |||
| 8f78a9dcde | |||
| 02eb362f37 | |||
| f79263a92a | |||
| cd95da6d35 | |||
| 5ab2c9afaa | |||
| e77201099d | |||
| 30a4c00f35 | |||
| e68db4ce57 | |||
| b5a83ab902 | |||
| 2c9efea733 | |||
| 9615dc1458 | |||
| f50a8482c3 | |||
| cd3dc00f2f | |||
| 65eae30a48 | |||
| fa4392df09 | |||
| f8d6fd80d5 | |||
| 88ed545830 | |||
| 4388f6e87c | |||
| 6157364e20 | |||
| 96999e399d | |||
| 6a3df679fa | |||
| 03e49c59e2 | |||
| b525b6e4fa | |||
| 5541b89cf7 | |||
| aaeed5d18b | |||
| d6c3c8a294 | |||
| d337fc6d47 | |||
| 2d897d8537 | |||
| 12b101e04f | |||
| d69af56c90 | |||
| 0cac5610c8 | |||
| d0afcf6628 | |||
| 37fa27d54f | |||
| be4fed2c19 | |||
| 47d02d8c4f | |||
| 4881d8e3a1 | |||
| cc618abf58 | |||
| 546e381325 | |||
| 9d1bb29a00 | |||
| 876d0d5873 | |||
| 2aa5c387c7 | |||
| 9ca8e49a4e | |||
| 6ceed03f6b | |||
| 4836b16030 | |||
| f9f44b18ad | |||
| d4f5b7ca34 | |||
| 9b57329f56 | |||
| 0064ac5ead | |||
| f2489c0845 | |||
| dca345b135 | |||
| 645c1b9151 | |||
| 678fca6704 | |||
| b74fae3762 | |||
| 2817ea833a | |||
| b7ed6d8463 | |||
| 005c33dbb5 | |||
| 4176317250 | |||
| bbd562f711 | |||
| a19505a708 | |||
| 1eed16bc97 | |||
| d9f88985fe | |||
| a57e33e8d1 | |||
| b4552ddb5f | |||
| 1da2450b10 | |||
| 9536b42244 | |||
| dd75cdb37e | |||
| 3b3e537797 | |||
| 0f9168052a | |||
| eb47476c83 | |||
| 7b04817874 | |||
| c7a7456ec9 | |||
| e422dd1198 | |||
| a75928d805 | |||
| fb2c5a85b6 | |||
| 4de2e381ff | |||
| 4da8c8d6db | |||
| 3c565defca | |||
| 191be658d5 | |||
| 1f209d0fb4 | |||
| ba91e1dfb2 | |||
| 6766884cd8 | |||
| b075140e76 | |||
| aa8586d273 | |||
| 9b2a3d23b2 | |||
| 6a43a4bd20 | |||
| 8c78889e88 | |||
| 873159b793 | |||
| b5823d3210 | |||
| cd99c22f64 | |||
| baa5122fcb | |||
| 5447aa7c80 | |||
| 933918ea27 | |||
| cbbcdc5df1 | |||
| 4dfa7b132d | |||
| fb5bfaa2bd | |||
| 20e206fa43 | |||
| 467fa59023 | |||
| 166c06c628 | |||
| 5ff3c8961c | |||
| 08f33f0e78 | |||
| 0c5a637203 | |||
| e3b4fdb6b1 | |||
| e730a6e282 | |||
| 722808a0e4 | |||
| eae33161c1 | |||
| f14df141f7 | |||
| f7a4330cd1 | |||
| 23474c9752 | |||
| fc08f9823e | |||
| 639bddb4b7 | |||
| f87b32fc7b | |||
| 468ad6d578 | |||
| 8b5c7d3d87 | |||
| e791084793 | |||
| 316a1ae2c5 | |||
| 71beca68dc | |||
| aae79db27a | |||
| 6f188da2a6 | |||
| 9ae4ce82a7 | |||
| 5adfa722d4 | |||
| c26dda7cc9 | |||
| b7440ee516 | |||
| e4b06b16a9 | |||
| 491af5bd9a | |||
| 9b67ab9713 | |||
| f0a62600af | |||
| dd5dfd98b7 | |||
| d5ec38c4db | |||
| f945463dbe | |||
| cf9439fb3b | |||
| 6901847c49 | |||
| c54c25c35e | |||
| 5728bce6bc | |||
| d752403ed6 | |||
| a48c08bd23 | |||
| e46bbe8546 | |||
| f5c8f18980 | |||
| 2d2270a337 | |||
| d315c53ff8 | |||
| d36b06acf7 | |||
| 2299af1dba | |||
| e25ccc5e9a | |||
| 3ea6610923 | |||
| 2d50f10fd6 | |||
| 81d0637483 | |||
| 6c4df5abf0 | |||
| 2eb0b5eedd | |||
| 0e00492f54 | |||
| b84a62eb5d | |||
| c41ed95afe | |||
| fe07013383 | |||
| 4f9cb9a8a1 | |||
| ec5129d25b | |||
| 6a781c62ec | |||
| c01ee83cd7 | |||
| cc591e399d | |||
| 7462c703f3 | |||
| 879a6b4202 | |||
| 0ae8dc1040 | |||
| 242548b36a | |||
| 252aedda25 | |||
| 3507269321 | |||
| 9a5dce33db | |||
| c4101a62ed | |||
| f52037f305 | |||
| 03bd67c4e7 | |||
| 1eef239392 | |||
| d1e14ed691 | |||
| 60a787ce3d | |||
| f96bc6d5f4 | |||
| 5d439d9e79 | |||
| 1453178693 | |||
| 510121bf54 | |||
| 2d607b394c | |||
| bd12b0e441 | |||
| 738b4e60fa | |||
| 1ae2f55c04 | |||
| 2ebdf9673d | |||
| 0427d790e5 | |||
| 90add7cf47 | |||
| 26b1f8dfdb | |||
| ba29889f54 | |||
| 9d2284add7 | |||
| dd44edde0a | |||
| 885e90e810 | |||
| 9cdf5dd0f3 | |||
| df6e3eb1e6 | |||
| 05026771e1 | |||
| 7039108438 | |||
| 02ee13cfb2 | |||
| 096e244252 | |||
| bf5b7294a0 | |||
| a5da266643 | |||
| cf7bb49e15 | |||
| 208b732bda | |||
| c73d93b8bd | |||
| 98a96eae2b | |||
| 2f9fe30c9d | |||
| aeee8afc02 | |||
| e85f0a4f52 | |||
| da98649667 | |||
| 5ac08cc06b | |||
| da72597dd3 | |||
| 1f1c94de70 | |||
| 60b3fceea6 | |||
| 5073809486 | |||
| debd779cfd | |||
| 6b9454100e | |||
| 779ad24542 | |||
| b94dbf5fa3 | |||
| 45c49c9757 | |||
| 91288c96b1 | |||
| f8e22a0730 | |||
| 114b45882a | |||
| b1b6f70118 | |||
| 648d42dfe4 | |||
| 99f989c384 | |||
| 2112c7d096 | |||
| ac63d00c93 | |||
| e04871f79f | |||
| 182c162dc4 | |||
| 822b38cc89 | |||
| d564003c87 | |||
| 1b307632ab | |||
| aa747cea85 | |||
| f4a322478d | |||
| d2882433a5 | |||
| a94b175805 | |||
| 37d81da806 | |||
| d089444441 | |||
| b0d65a1bae | |||
| 16288cf277 | |||
| 7ddbabf781 | |||
| fe35f4497b | |||
| 625463f6ab | |||
| ff632b6816 | |||
| fbc666f178 | |||
| d89bbdd50c | |||
| 96f9aa39b2 | |||
| 7330814d0f | |||
| 312efdcd94 | |||
| 5db78ae359 | |||
| 97967e60e8 | |||
| 9106b5d182 | |||
| 74bdb6cb9d | |||
| 0a44d426fa | |||
| e1718c4e8d | |||
| f511a610b5 | |||
| 4d5715188d | |||
| 2ea21be5bd | |||
| 5bb0419699 | |||
| a8131eed71 | |||
| ed09c06ba4 | |||
| 3c59a0ff31 | |||
| a6d24b3e48 | |||
| 060135eecb | |||
| ef296c24fe | |||
| 707aaf25ec | |||
| 7edeb0c358 | |||
| e516af14b2 | |||
| 4086f2671d | |||
| 23c4550430 | |||
| 31d25cd6be | |||
| 07b3c7a245 | |||
| a00b7281a7 | |||
| ddeee0c970 | |||
| 8aad71efd0 | |||
| 2028f6b984 | |||
| bff4999d27 | |||
| d429015f83 | |||
| e2628e2d43 | |||
| 05dcbee7e3 | |||
| a81919262e | |||
| b14b5f141b | |||
| 1259d11173 | |||
| 0a7b132be8 | |||
| ed9210eede | |||
| 9ee6aa54c6 | |||
| 7cfc455cd3 | |||
| a481ceac8c | |||
| 8c7eff4e24 | |||
| c6c584ff74 | |||
| ba50eb121d | |||
| aa8ebbd7ea | |||
| 64bc9c6dbe | |||
| bba9963b7c | |||
| 6ea2aa4a54 | |||
| 3c3f81365b | |||
| 3adeed381b | |||
| 0f5b7278b8 | |||
| f94ff49fb9 | |||
| d512a9c30d | |||
| 0c5113ed5b | |||
| 2469f4cdff | |||
| 9c53bfb7fb | |||
| 8b8144588d | |||
| 77553da4c1 | |||
| cbcf943691 | |||
| 725a19e5b5 | |||
| f9115f902a | |||
| e4faf26d74 | |||
| 1c96fbb533 | |||
| 3dc163c33d | |||
| edae94cf2e | |||
| d1ff8e9d6b | |||
| 70743bd285 | |||
| 493f1505f0 | |||
| 007e3b5eef | |||
| d9bf6c0933 | |||
| 324344d118 | |||
| 5cb71e9443 | |||
| cca19f00c5 | |||
| 6648f41f3d | |||
| c1e6b47fd6 | |||
| 0f103ccce1 | |||
| bc6e652293 | |||
| 85b4f2dbdd | |||
| d47b83a63b | |||
| b2e9fa7e0d | |||
| a9fb444622 | |||
| 33ba22a021 | |||
| 57de0282cd | |||
| 8568fd26d8 | |||
| 84f41e08cf | |||
| a96da20536 | |||
| 5199a9342e | |||
| 893ecec0fa | |||
| e3da6419f5 | |||
| 0750d2ba50 | |||
| f1fcb65fbe | |||
| 215aa65d5a | |||
| 85f67c13da | |||
| 6dcc478aeb | |||
| 3f2496db6f | |||
| 612f79f9e0 | |||
| 90fb1cd735 | |||
| 7c24d9c6c6 | |||
| 60f1b2356a | |||
| 0b8f21508f | |||
| ae128c0fa4 | |||
| 1b4ec9ecf9 | |||
| b0ce0b61d6 | |||
| e1ffdaddfa | |||
| af8344f482 | |||
| 7dc2596b3b | |||
| 0109956fc2 | |||
| 945fe3f3ec | |||
| 9c868135f3 | |||
| 5be288023b | |||
| a03f97186c | |||
| 0aab891980 | |||
| 5268d3f57d | |||
| 129cbb5beb | |||
| 2601d2945d | |||
| e3829eb24b | |||
| f6cb1a0863 | |||
| 4f964101a0 | |||
| f6dcba025f | |||
| d6ec65d456 | |||
| 65d8074a07 | |||
| e3af61ca4a | |||
| a58f1268f0 | |||
| 41eacc4bc5 | |||
| aabb9dee13 | |||
| c855d75f35 | |||
| 8f5cdcf439 | |||
| 984559427e | |||
| 89494ced41 | |||
| ef764c2393 | |||
| 8624e2260d | |||
| aa011f4add | |||
| 3df61c9ab8 | |||
| a4516776d6 | |||
| 54d0ade997 | |||
| 3557fcd129 | |||
| 330b4a613c | |||
| 7ba3412aae | |||
| 6f60495d4d | |||
| 0b2eb8fb9e | |||
| 48af17e052 | |||
| b7b1055530 | |||
| e7029c0afd | |||
| cba3674ac0 | |||
| 865a549885 | |||
| 50dcf827a5 | |||
| f5fb582f83 | |||
| dbba502f83 | |||
| aae49f16a2 | |||
| 45d5f8c74d | |||
| 6cfd64e536 | |||
| c5cc404b3e | |||
| 42cbcc6ce3 | |||
| 812bdcd462 | |||
| f275409ee8 | |||
| 8994ac3727 | |||
| 7c5ff5e4d5 | |||
| c5e84d5469 | |||
| c143450dc6 | |||
| 07b95c2c4b | |||
| c30734f7f3 | |||
| 91f506c17b | |||
| 7a17695ad5 | |||
| f5076c87d4 | |||
| a47d6e1f3a | |||
| f6ff1abb00 | |||
| 386aaf6470 | |||
| 2b3c4cf0ff | |||
| b602e921d0 | |||
| 2fc3cdc2a2 | |||
| e2cadbfc30 | |||
| 3ffa935da7 | |||
| 5f539e331a | |||
| 356d0fabda | |||
| 122ec75cb6 | |||
| a3a48e1a49 | |||
| 4ede765e1f | |||
| 4fa181b346 | |||
| 4f76d91ae9 | |||
| 20d1759fa5 | |||
| 433e783ede | |||
| 47f47d916d | |||
| b31ac7d1fd | |||
| ea47fb7305 | |||
| 82170f8f1b | |||
| acb2655f58 | |||
| b1464517e6 | |||
| 151e6351f6 | |||
| 154f768281 | |||
| 90c857e8fc | |||
| 7a3efa2631 | |||
| 38cc767f27 | |||
| e1a718c78f | |||
| 32a4450e5e | |||
| fca3f606d2 | |||
| 4a0a934a76 | |||
| f7c406bec9 | |||
| f4807a6354 | |||
| 0960008b7b | |||
| 04a1aa38b4 | |||
| f84622efa1 | |||
| f6c4614275 | |||
| 7d36533524 | |||
| 5cd3df4869 | |||
| b0480f48f3 | |||
| 2e820c343a | |||
| ce927a2247 | |||
| ae810d59e9 | |||
| 1438ee52a1 | |||
| de4b3e55fa | |||
| d2cd78c5cb | |||
| d000719fa2 | |||
| efea4ed615 | |||
| 67a931c4b8 | |||
| bdcc5c0629 | |||
| d113cfc0ba | |||
| 4a3ab50878 | |||
| b39261c8cf | |||
| 7efb57c8da | |||
| 90c24cf356 | |||
| 54abada561 | |||
| f1922660be | |||
| 795e3c57da | |||
| 3f201464a5 | |||
| 8ac0be6bb5 | |||
| 130805e7bd | |||
| b8c7357fea | |||
| 819f8e338f | |||
| 9569e46ff8 | |||
| b7baab2d0f | |||
| e2d284797d | |||
| a3ac343fe2 | |||
| dadde96e41 | |||
| 99475c51e8 | |||
| cc9b4e26b5 | |||
| 32f232d3c0 | |||
| 235047ad0b | |||
| 228f75de0b | |||
| 2f89e7e2b4 | |||
| 437f39deb3 | |||
| 59582f16c4 | |||
| af9e3e38ce | |||
| d992702b87 | |||
| 6a9fe1128f | |||
| 573da29a4d | |||
| 00cff1a728 | |||
| 9bdeff0a39 | |||
| a1f263c048 | |||
| 346eac389c | |||
| f52c16b209 | |||
| 4faf880aa4 | |||
| f417a49b34 | |||
| 66fd713d12 | |||
| 2e7630f97e | |||
| 3f10524532 | |||
| 51f9826918 | |||
| f5bb76333b | |||
| 4947faa5ca | |||
| 101dc3a93c | |||
| bd3ee0fa24 | |||
| 2c52668a74 | |||
| 03edd8c96b | |||
| 37dfa41e01 | |||
| ea8a3d798e | |||
| 1df94fd84d | |||
| 5af957dc9c | |||
| 21073c627e | |||
| 66cdba9c1a | |||
| 56d3b38ce6 | |||
| 15d0275045 | |||
| 991c1a0137 | |||
| 7d549dbbd5 | |||
| e27c5583bb | |||
| 650c49637f | |||
| eb5dcf1c3e | |||
| ed2b61b709 | |||
| 41466a3018 | |||
| 2e130ef99d | |||
| a96fb39a82 | |||
| c9923c8d4b | |||
| 74b0ff338b | |||
| dcaccc2d7a | |||
| d60714e4e6 | |||
| d513d5d887 | |||
| 386566fd4b | |||
| 3357ca76fe | |||
| a183ce13ee | |||
| e9d0ed8e1e | |||
| 66f66fd14f | |||
| b49d30b477 | |||
| 73d83ec57e | |||
| efb39fb24b | |||
| 73623f2e92 | |||
| fbcc4cfa50 | |||
| 474a3548e0 | |||
| 2cdf68379b | |||
| cc8509f8eb | |||
| a520c1b1cb | |||
| 75fc2cbcfb | |||
| b8bb69f730 | |||
| b46d3e74d6 | |||
| 77a1613107 | |||
| 62fab7b09f | |||
| 5d87352b28 | |||
| ff60f5a381 | |||
| 7f666d9369 | |||
| 442f16dbd0 | |||
| 2dcab77ed1 | |||
| 13be04a169 | |||
| e3767c3a54 | |||
| ce957c8dd5 | |||
| 0606b2994c | |||
| 33acccbaaa | |||
| 1e097abe86 | |||
| e51705c41d | |||
| 7eafa661fe | |||
| 2fe323e587 | |||
| 4e608d04dc | |||
| 531d314e25 | |||
| 1ab23d2902 | |||
| b3496e1354 | |||
| 2efa0aaca4 | |||
| ef9aeb0772 | |||
| 924a0136eb | |||
| c382fc375e | |||
| 2544acddfa | |||
| 58072892d6 | |||
| 85a897c78c | |||
| 6adf5772d8 | |||
| f98e3b1960 | |||
| 671a967e35 | |||
| 950ef0074f | |||
| 5515324fd4 | |||
| e72622ed4f | |||
| e821733a58 | |||
| a03c0e4475 | |||
| 3203821546 | |||
| 16f3cee5c5 | |||
| 57afb46cbd | |||
| 91dde5147a | |||
| d0692f7379 | |||
| e360658c6e | |||
| e7dc77e6de | |||
| e240a8b58f | |||
| 38d4f2c27b | |||
| 552e2a036c | |||
| 2d4b978032 | |||
| 36e00f0c84 | |||
| ef64b2b945 | |||
| f6cd33ae24 | |||
| dd109f149f | |||
| 5b62d63463 | |||
| 3fec599c0c | |||
| e30ea9f143 | |||
| 7cb0c31c59 | |||
| b00a7e3cbb | |||
| e63446ffa2 | |||
| 580da19bc2 | |||
| 936f456cec | |||
| 5d6a02f73c | |||
| b345195ea9 | |||
| 3e6b66751c | |||
| f78571e46d | |||
| f52000958c | |||
| 5ac9c6ce02 | |||
| 1110a67483 | |||
| 57bb1280f8 | |||
| 25c000599f | |||
| 86f45e2769 | |||
| 7110240e73 | |||
| 1da37b66d8 | |||
| f1975d8f2b | |||
| f407ce734a | |||
| f813cfa8db | |||
| d5880cb953 | |||
| 95da9744c1 | |||
| 85c3e45cde | |||
| 520a396ded | |||
| 13ad611c96 | |||
| 85f58d9681 | |||
| c1de62acef | |||
| 7e47e36773 | |||
| 00b6217cab | |||
| acc2b5a1a3 | |||
| b06feaa36b | |||
| 89cf8a455a | |||
| 710046a94f | |||
| b366b0fa6a | |||
| f9e7a8207a | |||
| 6178bf3d4b | |||
| f3b979f112 | |||
| 9faae96d61 | |||
| 2135fe5dd0 | |||
| 007a8d248d | |||
| 58d4a3455b | |||
| 8e3c14f245 | |||
| 91af2495a6 | |||
| 7d7df5247b | |||
| f99450d264 | |||
| d3eeb5f48a | |||
| 1e8a02f91a | |||
| 97c3bd8b8e | |||
| 09ce27d74b | |||
| 2447e91a9f | |||
| e6d881b75d | |||
| 36f963dce8 | |||
| 1b15d28212 | |||
| 4e0c15e102 | |||
| c9e40f59de | |||
| 38cf31885c | |||
| 4420470242 | |||
| 9b05786615 | |||
| 725b2c81ee | |||
| 661965f2e0 | |||
| 7e0ef60305 | |||
| 2ac0fe21c6 | |||
| b997f2329d | |||
| 23ee758ac9 | |||
| 9ea12e71f0 | |||
| d3594c2dd6 | |||
| 6ee4b0da27 | |||
| 3e66feb514 | |||
| cd91a5ef64 | |||
| cf89609633 | |||
| 67c24c1282 | |||
| 7d3df3c55f | |||
| dfe5cec46f | |||
| 17c881da47 | |||
| 6e30c4917c | |||
| c6d4f0d2f0 | |||
| b32128bebf | |||
| a3f3d86908 | |||
| b29c82087a | |||
| 657beda7c9 | |||
| b4f5ecb304 | |||
| 3dabad5e91 | |||
| 890b46836b | |||
| 835b3224c6 | |||
| f8d27f3139 | |||
| 33f263ebb9 | |||
| 027925c0ba | |||
| 17c4819d41 | |||
| 017d19a8c8 | |||
| 46b6e319f5 | |||
| 8f087e1c30 | |||
| c3fc0e83a8 | |||
| 7ed0ef7b37 | |||
| 46ede3d60d | |||
| 7a63fd4711 | |||
| 65f573b773 | |||
| afa2fe8177 | |||
| ad72a8a929 | |||
| a7b00bad63 | |||
| 85fd74135c | |||
| 970ccf1ddb | |||
| b237eb03f6 | |||
| a569294f86 | |||
| 16f85a23d2 | |||
| fcee8aa5f3 | |||
| d85eabce02 | |||
| de23d1aa03 | |||
| 1766bc6ee3 | |||
| c1801d6e71 | |||
| 64844045ca | |||
| e90da46967 | |||
| d10957d6df | |||
| 50dc90d7ae | |||
| 663bedfe39 | |||
| ce9834757e | |||
| cc932328ff | |||
| 4ebe143a98 | |||
| 82aff74fc2 | |||
| 6adc099455 | |||
| 35efc8c650 | |||
| 3f63d79905 | |||
| 00096f4dcd | |||
| c3e0d9086e | |||
| f1dfe3c7e8 | |||
| 6f96ff790f | |||
| ccb218f243 | |||
| 9ac194bbea | |||
| 0191907ce2 | |||
| e80069625b | |||
| 0e156b9376 | |||
| a8f1b0241e | |||
| 6715cf23d7 | |||
| 82a173f7d8 | |||
| 857504c409 | |||
| 4b4586c1e5 | |||
| 6679fe47df | |||
| e7a98025a2 | |||
| 2870f24bec | |||
| 037440034b | |||
| 15cc1f92e3 | |||
| 00c6ad675e | |||
| 655a740b0c | |||
| 028852740d | |||
| c8000fdf90 | |||
| 995e56d7e4 | |||
| c20d3b62b0 | |||
| c537dfabb2 | |||
| 11b5304cb9 | |||
| fd8abbe2ab | |||
| 25d871860d | |||
| d1911be28c | |||
| 938ca6402c | |||
| 0aaecf6e46 | |||
| b06d84984b | |||
| 51b50688e4 | |||
| 066d7ab972 | |||
| e092074d77 | |||
| 83bdcb8cc4 | |||
| f80f40cbcd | |||
| 4b93b31c3d | |||
| 4d050725b7 | |||
| 57597bd103 | |||
| fb52c2b684 | |||
| de547df9bd | |||
| a05342eaa0 | |||
| fb931b7a3a | |||
| d1c07b6d30 | |||
| 7f0ad2afa0 | |||
| d8e0639db4 | |||
| 4d91351845 | |||
| d3f08ef580 | |||
| 5e11a9c8ed | |||
| 957e1a7708 | |||
| 7c86ed9783 | |||
| 799b588693 | |||
| 596f4c01a4 | |||
| f155de0f17 | |||
| 476ba1ad69 | |||
| ac4aa4bd3d | |||
| 237f2c5112 | |||
| cbc6785eb5 | |||
| 26c4cdbf17 | |||
| fb78f31891 | |||
| 2b6bf8d195 | |||
| 2854462e0e | |||
| b4e4b11ab3 | |||
| 7c5a258af3 | |||
| 12aa8ac0ad | |||
| 58d8f688e5 | |||
| 7efb9e817e | |||
| 5145ea3530 | |||
| 2f6933102c | |||
| 25ef5ab636 | |||
| 4ae12ac10b | |||
| bfffde5f89 | |||
| aa7ec53257 | |||
| 1f41e6dc0f | |||
| 1fbbaa82ab | |||
| 68b1d1dde1 | |||
| d773cb4873 | |||
| d3c7616120 | |||
| 6a92af3db3 | |||
| 763e14f55d | |||
| 4f57d97fff | |||
| 3153fb5cbe | |||
| c9e96cd97a | |||
| c41042635f | |||
| 141b2d2691 | |||
| e71e8043cf | |||
| 49d80dbfc4 | |||
| 8d6eca2349 | |||
| 13d0491759 | |||
| 37e2d78d6a | |||
| 6745221e0f | |||
| 18bbe70364 | |||
| eec8d4bdac | |||
| 86029c1068 | |||
| 0ae9be4de9 | |||
| 57e3180737 | |||
| a84cdc3d09 | |||
| a5f35f39fe | |||
| 3427db3983 | |||
| e3878fa381 | |||
| e1ded9f7b5 | |||
| 1981493398 | |||
| dece7319cc | |||
| 5c4e163709 | |||
| d1acc6c466 | |||
| f879d6f529 | |||
| 1ac38d4921 | |||
| 4818e9a8e4 | |||
| c4ed471d1c | |||
| 83c0b2986a | |||
| b8cddf559a | |||
| 4ba9f80d44 | |||
| d1d3309e91 | |||
| 84cffe8888 | |||
| 3929b3ca0a | |||
| d649a470f9 | |||
| db330b23cb | |||
| cda649884e | |||
| 45053205db | |||
| 3f1533896e | |||
| e20dfe1b26 | |||
| 946d9db296 | |||
| 6dc2e1aa14 | |||
| 001749564d | |||
| ffcba4646c | |||
| 01d0c8eb9c | |||
| 0cf40bd207 | |||
| 4a283e9f35 | |||
| 5ab37bcf7e | |||
| 9151965cd6 | |||
| c5cd71f9e3 | |||
| 602b335c0e | |||
| 837c8b85c2 | |||
| 7d16396e72 | |||
| 66d3d07148 | |||
| b5c1161caa | |||
| b0420889ad | |||
| 527819d886 | |||
| 1ad0cff28e | |||
| 783ec03ac9 | |||
| 6cd395d494 | |||
| 681079e01c | |||
| aabbc43769 | |||
| 2692f6ef4e | |||
| 887cbb0b22 | |||
| ca4fdc1be8 | |||
| 93199c7f5b | |||
| 4c6566f42f | |||
| c38f7d7f93 | |||
| da85cea329 | |||
| d5c70a2b11 | |||
| fe355b4bac | |||
| a7dee6be51 | |||
| 2817dc0603 | |||
| 6f36c72e88 | |||
| 45e806c455 | |||
| bbdd76dd37 | |||
| 09921e86c0 | |||
| d6e4b64103 | |||
| 9dd3e4537a | |||
| a5f31e8724 | |||
| 72ac00b69a | |||
| ae5722a7d4 | |||
| 4e3192d450 | |||
| ccca3aca04 | |||
| e4dd5d6434 | |||
| 9a77fb6306 | |||
| 3ec5c713bf | |||
| 837fc27e94 | |||
| 9ad6025310 | |||
| d765e4c619 | |||
| f5217236d6 | |||
| 8f8d099faf | |||
| 16660e083f | |||
| 4e35020a1c | |||
| 111e0bcb5f | |||
| d7f9a547fc | |||
| 6a64f24e98 | |||
| 37d7be93b5 | |||
| 9c809aa6e1 | |||
| 7ab9f3fa2f | |||
| ffeb484a10 | |||
| 2ffb32ae60 | |||
| 905bb92bad | |||
| 3926efd153 | |||
| c5e5bb90e3 | |||
| cea543cba5 | |||
| a8b489624d | |||
| 49d3bddb62 | |||
| c0ff3cbd22 | |||
| 1de97d6967 | |||
| a44a82083e | |||
| d57681ff21 | |||
| e3de2f81d3 | |||
| e8c5f8164c | |||
| c07e215148 | |||
| 4bb676fb5c | |||
| dbdf86edfc | |||
| 2c8e6330ce | |||
| 1b563854a7 | |||
| 80b890101b | |||
| c3696469ff | |||
| 3e08e7c653 | |||
| 53e39f571c | |||
| c992853cca | |||
| 85e17b570b | |||
| 30eccfb54b | |||
| 3623831390 | |||
| d0a3d00492 | |||
| 0b6fbfd910 | |||
| 8cfb27fdcd | |||
| 841ab54565 | |||
| a2e9254343 | |||
| 43cb03a292 | |||
| f2fca33309 | |||
| 14d26fe064 | |||
| 9cc968e790 | |||
| 831e22b4ff | |||
| 6774514bd2 | |||
| f543b98764 | |||
| 2e94600afe | |||
| 9295ce783a | |||
| 134f8a28bf | |||
| ab5e4e998c | |||
| a98551f99c | |||
| 42fe84152a | |||
| 8a3d212bd4 | |||
| af51ddc347 | |||
| b582e549c2 | |||
| 5efbccd974 | |||
| 82f5cd6075 | |||
| 0d8820c247 | |||
| 37c6a96a3a | |||
| c53b54bda3 | |||
| 808753ad3a | |||
| f919570cea | |||
| 9acf49a99e | |||
| 239883d01f | |||
| e3cee37527 | |||
| 8fd0461c62 | |||
| 4d2b5c83ca | |||
| bc314c1119 | |||
| d01749a2c2 | |||
| b46154676a | |||
| fd2d60dca3 | |||
| ed17bdc7c3 | |||
| ac05399cda | |||
| 1af5c6a418 | |||
| e2bb668fe4 | |||
| d255466417 | |||
| 5509406395 | |||
| 97333474c4 | |||
| 38928d63d6 | |||
| 05c64dcbf2 | |||
| e39b081567 | |||
| 62174658cf | |||
| 3d26e8a666 | |||
| 3d337640ef | |||
| 985eaf8ca9 | |||
| e0bee13812 | |||
| 7c6922d228 | |||
| bf68c2d321 | |||
| fd51320fb7 | |||
| 815392ba38 | |||
| f8c110f75c | |||
| 70f9ceb1b8 | |||
| 2353a8b5fa | |||
| cf1c2dc1ee | |||
| 467283d5e0 | |||
| a887e19d46 | |||
| 2ab941660e | |||
| a75769071c | |||
| 7f2af067cf | |||
| 88454e7d6c | |||
| 5c920fd200 | |||
| ab650c7a95 | |||
| 1e776bbbe0 | |||
| cd0294129f | |||
| d1c6e786c2 | |||
| 58d66b5293 | |||
| 1942a7ecf4 | |||
| 22c2add55e | |||
| 60c5cccfc2 | |||
| b4874ec1f4 | |||
| d7b326bf2b | |||
| b9d8b5f973 | |||
| 64fd6e0dac | |||
| 868103e7e4 | |||
| 3354cb8ebe | |||
| 4fc012dea0 | |||
| 947cb786d6 | |||
| 689f2791ba | |||
| a5ec5b0ed9 | |||
| 8e5916b785 | |||
| 563f846eba | |||
| 7781ea3205 | |||
| 2f5ece8f1d | |||
| ec46dab754 | |||
| d5d27d512c | |||
| 0a695190c4 | |||
| 59deca76a1 | |||
| a829ab44f1 | |||
| 82a7befb92 | |||
| 331d0ee717 | |||
| addafa529f | |||
| 8232d471a3 | |||
| 813454ca82 | |||
| 7d987d7c79 | |||
| 7a25187bee | |||
| f97cbb5fd5 | |||
| 12d233c5f9 | |||
| 09fce1978e | |||
| 8ed2f98d1d | |||
| 13262d014b | |||
| ade1187fc8 | |||
| 2404e79928 | |||
| d68ed91b17 | |||
| 1a21423401 | |||
| a478134759 | |||
| c639746211 | |||
| 7a96e4858a | |||
| 02339d503c | |||
| c3a5360a88 | |||
| ad9097d212 | |||
| 6e57f8cc03 | |||
| d6365ff27f | |||
| 4793eb9ef5 | |||
| 03175aa8de | |||
| bc3169deb3 | |||
| 9b4d43075e | |||
| d2c12297dc | |||
| 1a8496d61e | |||
| a017af41c5 | |||
| ec216d9828 | |||
| bce1efb77c | |||
| b078d37f37 | |||
| 8d944f74c0 | |||
| dc10b8a07f | |||
| 7b9f741522 | |||
| 51cb3b0ba8 | |||
| 4db4834c90 | |||
| e1f0d12251 | |||
| e2388b7d88 | |||
| d0e6b6bfe4 | |||
| b6f2c94464 | |||
| 8cdddef077 | |||
| e82ac5ecc5 | |||
| db6c07f86a | |||
| 2df642000d | |||
| 11d80cec7d | |||
| 8c9ce30d29 | |||
| df142994a8 | |||
| 2d115d3d0f | |||
| 1b594d3e50 | |||
| 332f2e7c10 | |||
| a7614cef2e | |||
| 9842b6d4a1 | |||
| 88818a1ec2 | |||
| 812f5cce99 | |||
| fdf7da9111 | |||
| ed9e1772ea | |||
| 657a2cac2f | |||
| d15aa2744d | |||
| 29ab3e91b3 | |||
| f6377fd1c6 | |||
| 122a987d61 | |||
| 4610e78d91 | |||
| 351bd46cb7 | |||
| 8878bc4bf9 | |||
| 61b6bee946 | |||
| 9997cbddb8 | |||
| 7115498f32 | |||
| 0f05c243aa | |||
| 9c12f1fe15 | |||
| 7383cc4e90 | |||
| 6466b47ada | |||
| 1856fc05d9 | |||
| a19662bdfa | |||
| 488763fc42 | |||
| 7cbe60a484 | |||
| ded9a6e377 | |||
| ea205363a0 | |||
| ad13445c93 | |||
| eb5c2ed30b | |||
| bd3080a6b3 | |||
| be5290c5ca | |||
| 43fd207164 | |||
| 34c53694a0 | |||
| 927f8483ce | |||
| a19205e3ad | |||
| 49e5c60422 | |||
| 57b623ee44 | |||
| 0c904af927 | |||
| 9cd025972c | |||
| 21111eccc4 | |||
| 917079f341 | |||
| 4d6d768be1 | |||
| c54cd992ca | |||
| d5ec599dd1 | |||
| 0542ab16d4 | |||
| 7e75ef7685 | |||
| f296265461 | |||
| fb4eade215 | |||
| 8b3e85907c | |||
| ca4876649d | |||
| 7ebc2abe5d | |||
| 37e132319b | |||
| b2728118e9 | |||
| c428f649aa | |||
| 7baf979a59 | |||
| ccecaca047 | |||
| c7ee684f25 | |||
| 52156c9a35 | |||
| 4fba216af9 | |||
| 1d00c788d1 | |||
| d891d39587 | |||
| cfde6e31ad | |||
| 243772d1f5 | |||
| 1c36b8eaf7 | |||
| 120fa4924a | |||
| c3c9c2f39a | |||
| fc90829ba2 | |||
| ce9224c690 | |||
| 18a2107247 | |||
| f13d05dad7 | |||
| 86586444a9 | |||
| 4e47d0595d | |||
| 45e85e4d53 | |||
| a3420f885d | |||
| a266fe13d0 | |||
| 44aba5d6e1 | |||
| 3fe5307ae3 | |||
| d03fb0e71f | |||
| d9723b72e4 | |||
| 6ba61f1bda | |||
| d1df647ddd | |||
| 95c4a1f90c | |||
| e00325e694 | |||
| 85c13cae58 | |||
| 00fd9e5b7f | |||
| dde81ee847 | |||
| c46fc96500 | |||
| 1914a9a703 | |||
| 1a061e4446 | |||
| 29ce80cebe | |||
| 4b6ac538ac | |||
| 70b9000b0e | |||
| 24dcb1b79c | |||
| 384915883f | |||
| 4cfc75f1d1 | |||
| c49cbb524d | |||
| b401c3d930 | |||
| 890a7cfb37 | |||
| 70a1ef1af3 | |||
| 38a0cdc0be | |||
| 93344a5a4a | |||
| 9f792fc04b | |||
| 7cb95faacb | |||
| bf122f0f56 | |||
| 78e9446a05 | |||
| 138e1595fa | |||
| 37b02ad36a | |||
| 02f0055594 | |||
| ec1f0f9320 | |||
| bfe6389f62 | |||
| 30db3e8973 | |||
| 5b67f2cf29 | |||
| a007b74b1c | |||
| a89482d4fa | |||
| 0cd4f133aa | |||
| e5ba4ff973 | |||
| ce133b997d | |||
| 217632354f | |||
| 9841351190 | |||
| f3341f4b7f | |||
| ff1f448860 | |||
| 37f28746fc | |||
| 9a22ba3af7 | |||
| 2942da78de | |||
| 89ff6be971 | |||
| be0d7bcce1 | |||
| 851b257678 | |||
| 579eacb644 | |||
| f52c5b584e | |||
| 8980c18deb | |||
| b05a9ce064 | |||
| 1974314c1f | |||
| 2bde023d4d | |||
| 3a10003246 | |||
| 1b08710b7e | |||
| 101d09eeb3 | |||
| 00f949f156 | |||
| adbe46d369 | |||
| 3198926cd6 | |||
| 957a6a20fe | |||
| 94f75bb0d7 | |||
| 0f442755e5 | |||
| cd2e782d48 | |||
| e97606ca87 | |||
| 00ada80230 | |||
| 34db98c489 | |||
| 110695355c | |||
| 021fb4bb94 | |||
| dea033e4b0 | |||
| 7dfe40739e | |||
| 9f0d1b515c | |||
| 2691d46d50 | |||
| 78c8f1de71 | |||
| d27ee4bfbc | |||
| cc5daa428d | |||
| 3e2189aeed | |||
| 79f9963792 | |||
| 6f53723169 | |||
| d8cb100fc0 | |||
| 5f9b2f1159 | |||
| 801ca7eda1 | |||
| 45a2d3745c | |||
| 551fe4d846 | |||
| 791981c2f2 | |||
| a18a620847 | |||
| 99e63ffc3f | |||
| e10a6d9de5 | |||
| 147f16571a | |||
| bd1fbc4a05 | |||
| 0843f78ec8 | |||
| 9769fbfcf2 | |||
| 7e73197eb9 | |||
| e3964fd710 | |||
| e66961b814 | |||
| 4176e5a98e | |||
| 45cf8a62d1 | |||
| b1380819ba | |||
| 57fa457596 | |||
| de1e218ce9 | |||
| e117ee2bef | |||
| a9e101d9f4 | |||
| a2f8203a42 | |||
| b9ee127775 | |||
| 6668bb3e8a | |||
| 5fd129e509 | |||
| d59c1f53b9 | |||
| d2f38c1abc | |||
| c0a1db6941 | |||
| fc10b4a79b | |||
| 9da2117e99 | |||
| 7e030b149b | |||
| bd23abd265 | |||
| dd0fb8292c | |||
| b4cbf63519 | |||
| 4fd04fa349 | |||
| c22cdb8d81 | |||
| eb963b2eb4 | |||
| 7d299908c9 | |||
| 2585282f86 | |||
| f25d5b3304 | |||
| 6e878faa8b | |||
| 15a6cbe62b | |||
| 76b0b214ec | |||
| f5c643c960 | |||
| ca8e0613fb | |||
| 0c9334d0d2 | |||
| 712dc97e9b | |||
| 4df48c97ec | |||
| fe3ea53cda | |||
| d385c80882 | |||
| b823213c94 | |||
| 4b86311ab9 | |||
| b9efa8f445 | |||
| f8db12346d | |||
| 4d3948f81f | |||
| 5431d50206 | |||
| 6db078c26a | |||
| f61e9c7f27 | |||
| 567d92ce00 | |||
| 7a6d26c5da | |||
| 046ac85177 | |||
| f0fd088247 | |||
| 5ec0d1e691 | |||
| 9391a934c3 | |||
| bb62e6a318 | |||
| 0da6539c48 | |||
| 9cf833dab2 | |||
| ed57260fcf | |||
| c98f625c4c | |||
| f3008064e4 | |||
| 1faee00764 | |||
| a40505e2ee | |||
| 484202b4c6 | |||
| 6a7fc17c60 | |||
| 05d3897ae2 | |||
| 9f1210202a | |||
| be6b172d6f | |||
| fef9e0a5c1 | |||
| b84b033bf3 | |||
| b30ff1f55a | |||
| c6be0b290b | |||
| 33cfd7a629 | |||
| 5952a5c69d | |||
| 20de563925 | |||
| 7da80b4c62 | |||
| 15d765be6d | |||
| bfe2f116a7 | |||
| f535b3de2f | |||
| e560c18b57 | |||
| aecb99b6a3 | |||
| 7da17f8190 | |||
| 1964270a4f | |||
| f45b61d95c | |||
| ff11c38169 | |||
| 3e67067431 | |||
| 824f00d1e8 | |||
| 96d19f59a4 | |||
| 42c6fe50d2 | |||
| 9242f7095a | |||
| 99c9fbc38f | |||
| 0d31207ad7 | |||
| 8af7dbc35a | |||
| d0a373cb15 | |||
| 3dc87bbca8 | |||
| a55c399585 | |||
| f74aa24dd2 | |||
| 1aa7eb4478 | |||
| 0c7002ba59 | |||
| fd6dd1ea18 | |||
| aa74d5cd82 | |||
| 8fc10a0bdd | |||
| 809ed0f0dc | |||
| b8a4e1c4a3 | |||
| d9e45f732b | |||
| ca025b36f7 | |||
| bfb719d35e | |||
| 2a1b61107f | |||
| 969cee7c90 | |||
| 7a3f579d3e | |||
| 288d5efa88 | |||
| 7be821963c | |||
| a236f8992a | |||
| a5c2257f39 | |||
| 9d3b4ba816 | |||
| 43bf0767f1 | |||
| b301e5b151 | |||
| 2b484c0382 | |||
| f40ab4e2d5 | |||
| c0a27380e9 | |||
| 0d7a3f43c4 | |||
| 8195e439f3 | |||
| b5edbf716c | |||
| 466265fde1 | |||
| 40033e09cd | |||
| 573663412c | |||
| 17599417f7 | |||
| 0ece6d8b0e | |||
| e0ac0393fe | |||
| 6d38b3255c | |||
| 477ff424d6 | |||
| a843104348 | |||
| 0f4bc0981a | |||
| 07f6351465 | |||
| 1b26e86365 | |||
| 94b4bf94c0 | |||
| d5de05b633 | |||
| 0ab6cad048 | |||
| 9833ad548b | |||
| aa1ba3b226 | |||
| 3774d4de28 | |||
| e4961726bc | |||
| 77cf7d0da6 | |||
| a993e0b228 | |||
| 43671a9fd6 | |||
| 49cfd1e9b7 | |||
| 58d4a4f54f | |||
| e4e328ba6a | |||
| fd6bc955ff | |||
| 511a18e0ed | |||
| e29d224a92 | |||
| bb48ffb01f | |||
| 31fd3411f7 | |||
| a737d2675e | |||
| fd462659cd | |||
| cb10d0d465 | |||
| 61f1c4884c | |||
| 2cd00de6e3 | |||
| d3c5d53eae | |||
| 6dfafae342 | |||
| 2f861c3309 | |||
| af388f0f16 | |||
| c36cc86c5f | |||
| 02f195b25c | |||
| 18623fd9b7 | |||
| 9b74bb73aa | |||
| ee9636b496 | |||
| 5c2cbd7840 | |||
| 7fbac6cc17 | |||
| 9e7e9d66bf | |||
| 7fe66aa7fa | |||
| 2dda0efe83 | |||
| 59620ca473 | |||
| 12eae1eff2 | |||
| b03bf87b7d | |||
| c32718b164 | |||
| a6ea12fedc | |||
| 2d260eb0d5 | |||
| d7dd069ae0 | |||
| 6a77a58489 | |||
| c30ac5f927 | |||
| 437f7ef890 | |||
| 1f7347e8de | |||
| 96f59d7cfe | |||
| d55f65c7c9 | |||
| 9a0d5b918f | |||
| 3553fbc7b6 | |||
| 55d53f13d9 | |||
| 27369a650c | |||
| 913f0d5d97 | |||
| ada63ec697 | |||
| 117f06e971 | |||
| 9f03a9a6e2 | |||
| ce406c7088 | |||
| e7127df30d | |||
| 10e2817257 | |||
| 337a47c62b | |||
| 14bdac20ef | |||
| 88e2b3f9aa | |||
| 22d731f06d | |||
| e3d288ef7d | |||
| 455f597543 | |||
| 8c9e626920 | |||
| 5a000c1ff4 | |||
| ddf634bfb2 | |||
| 89d3b8cc6a | |||
| 49af6d09a2 | |||
| e5b0cac284 | |||
| 6f33900f85 | |||
| 514823af7d | |||
| 65b058f563 | |||
| 7c8560deff | |||
| 6bbe2613b4 | |||
| 5771478e4b | |||
| e13030bc89 | |||
| 0a0ac93a55 | |||
| 214fb50e74 | |||
| 959f8ee31e | |||
| cb0d75be37 | |||
| 11353e9e3a | |||
| 8cd5c15c2b | |||
| b86b8b8ee1 | |||
| c5f6e6b028 | |||
| 592d8abc58 | |||
| d93068fc62 | |||
| a864af52df |
@@ -1631,3 +1631,521 @@
|
||||
[4.1.4]
|
||||
* Add CLOUDRON_ prefix to MySQL addon variables
|
||||
|
||||
[4.1.5]
|
||||
* Make the terminal addon button inject variables based on manifest version
|
||||
* Preserve addon passwords correctly when using v2 manifest
|
||||
* Show error message instead of logging out user when invalid 2FA token is provided
|
||||
* Ensure redis vars are renamed with manifest v2
|
||||
* Add missing Scaleway Object Storage to restore UI
|
||||
* Fix Exoscale endpoints in restore UI
|
||||
* Reset the app icon when showing the configure UI
|
||||
|
||||
[4.1.6]
|
||||
* Fix issue where CLOUDRON_APP_HOSTNAME was incorrectly set
|
||||
* Remove chat link from the footer of login screen
|
||||
* Add support for oplog tailing in mongodb
|
||||
* Fix LDAP not accessible via scheduler containers
|
||||
|
||||
[4.1.7]
|
||||
* Fix issue where login looped when admin bit was removed
|
||||
|
||||
[4.2.0]
|
||||
* Fix issue where tar backups with files > 8GB was corrupt
|
||||
* Add SparkPost as mail relay backend
|
||||
* Add Wasabi storage backend
|
||||
* TOTP tokens are now checked for with +- 60 seconds
|
||||
* IP based restore
|
||||
* Fix issue where task logs were not getting rotated correctly
|
||||
* Add notification for box update
|
||||
* User enable/disable flag
|
||||
* Check disk space before various operations like install, update, backup etc
|
||||
* Collect per app du information
|
||||
* Set Cloudron specific UA for healthchecks
|
||||
* Show message why an app task is 'pending'
|
||||
* Rework app task system so that we can now pass dynamic arguments
|
||||
* Add external LDAP server integration
|
||||
|
||||
[4.2.1]
|
||||
* Rework the app configuration routes & UI
|
||||
* Fine grained eventlog for app configuration
|
||||
* Update Haraka to 2.8.24
|
||||
* Set sieve_max_redirects to 64
|
||||
* SRS support for mail forwarding
|
||||
* Fix issue where sieve responses were not sent via the relay
|
||||
* File based session store
|
||||
* Fix API token error reporting for namecheap backend
|
||||
|
||||
[4.2.2]
|
||||
* Fix typos in migration
|
||||
|
||||
[4.2.3]
|
||||
* Remove flicker of custom icon
|
||||
* Preserve PROVIDER setting from cloudron.conf
|
||||
* Add Skip backup option when updating an app
|
||||
* Fix bug where nginx was not reloaded on cert renewal
|
||||
|
||||
[4.2.4]
|
||||
* Fix demo settings state regression
|
||||
|
||||
[4.2.5]
|
||||
* Fix the demo settins fix
|
||||
|
||||
[4.2.6]
|
||||
* Fix configuration of empty app location (subdomain)
|
||||
|
||||
[4.2.7]
|
||||
* Fix issue where the icon for normal users was displayed incorrectly
|
||||
* Kill stuck backup processes after 12 hours and notify admins
|
||||
* Reconfigure email apps when mail domain is added/removed
|
||||
* Fix crash when only udp ports are defined
|
||||
|
||||
[4.3.0]
|
||||
* Add timeout to kill long running tasks in case they get stuck
|
||||
* email: Auto-subscribe to Spam folder
|
||||
* Allow setting a custom CSP policy
|
||||
* ticket: when email is down, add a field to provide alternate contact email
|
||||
* Re-work app import flow
|
||||
* Add pagination and search to mailbox and mail alias listing
|
||||
* Add UI and workflow to add a private registry
|
||||
* Show external LDAP connector
|
||||
* Network view: Allow IP address detection to be configurable
|
||||
* Add support for custom docker registry
|
||||
* Resolve any lists and aliases in a mailing list
|
||||
* Rename Accounts view to Profile
|
||||
* Add search for groups and user association UI
|
||||
|
||||
[4.3.1]
|
||||
* Make logout from all button logout from all sessions
|
||||
* List unstable apps by default
|
||||
* Fix crash when listing mailboxes
|
||||
|
||||
[4.3.2]
|
||||
* Update manifestformat module
|
||||
|
||||
[4.3.3]
|
||||
* Fix bug where stopped containers got started on server restart
|
||||
* Fix external LDAP UI and syncing
|
||||
* Fix timeout being too low in docker proxy
|
||||
* Make manifest.id optional for custom apps
|
||||
* Fix registry detection in private images
|
||||
* Make mailbox domain configurable for apps
|
||||
|
||||
[4.3.4]
|
||||
* Do not error if fallback certs went missing
|
||||
* Add 'New Apps' section to Appstore view
|
||||
* Fix issue where graphs of some apps were not appearing
|
||||
|
||||
[4.4.0]
|
||||
* Show swap in graphs
|
||||
* Make avatars customizable
|
||||
* Hide access tokens from logs
|
||||
* Add missing '@' sign for email address in app mailbox
|
||||
* Add app fqdn to backup progress message
|
||||
* import: add option to import app in-place
|
||||
* import: add option to import app from arbitrary backup config
|
||||
* Show download progress for rsync backups
|
||||
* Fix various repair workflows
|
||||
* acme2: Implement post-as-get
|
||||
|
||||
[4.4.1]
|
||||
* ami: fix AWS provider validation
|
||||
|
||||
[4.4.2]
|
||||
* Fix crash when reporting that DKIM is not setup correctly
|
||||
* Stopped apps cannot be updated or auto-updated
|
||||
* eventlog: track support ticket creation and remote support status
|
||||
|
||||
[4.4.3]
|
||||
* Add restart button in recovery section
|
||||
* Fix issue where memory usage was not computed correctly
|
||||
* cloudflare: support API tokens
|
||||
|
||||
[4.4.4]
|
||||
* Fix bug where restart button in terminal was not working
|
||||
* Add search field in apps view
|
||||
* Make app view tags and domain filter persistent
|
||||
* Add timezone UI
|
||||
|
||||
[4.4.5]
|
||||
* Fix user listing regression in group edit dialog
|
||||
* Do not show error page for 503
|
||||
* Add mail list and mail box update events
|
||||
* Certs of stopped apps are not renewed anymore
|
||||
* Fix broken memory sliders in the services UI
|
||||
* Set CPU Shares
|
||||
* Update nodejs to 12.14.1
|
||||
* Update MySQL addon packet size to 64M
|
||||
|
||||
[5.0.0]
|
||||
* Show backup disk usage in graphs
|
||||
* Add per-user app passwords
|
||||
* Make app not responding page customizable
|
||||
* Make footer customizable
|
||||
* Add UI to import backups
|
||||
* Display timestamps in browser timezone in the UI
|
||||
* Mail eventlog and usage
|
||||
* Add user roles - owner, admin, user manager and user
|
||||
* Setup logrotate configs for collectd since upstream does not set it up
|
||||
* mail: Add X-Envelope-To and X-Envelope-From headers for incoming mails
|
||||
* linode: add object storage backend
|
||||
* restore: carefully replace backup config
|
||||
* spam: add default corpus and global db
|
||||
|
||||
[5.0.1]
|
||||
* Show backup disk usage in graphs
|
||||
* Add per-user app passwords
|
||||
* Make app not responding page customizable
|
||||
* Make footer customizable
|
||||
* Add UI to import backups
|
||||
* Display timestamps in browser timezone in the UI
|
||||
* Mail eventlog and usage
|
||||
* Add user roles - owner, admin, user manager and user
|
||||
* Setup logrotate configs for collectd since upstream does not set it up
|
||||
* mail: Add X-Envelope-To and X-Envelope-From headers for incoming mails
|
||||
* linode: add object storage backend
|
||||
* restore: carefully replace backup config
|
||||
* spam: add default corpus and global db
|
||||
|
||||
[5.0.2]
|
||||
* Show backup disk usage in graphs
|
||||
* Add per-user app passwords
|
||||
* Make app not responding page customizable
|
||||
* Make footer customizable
|
||||
* Add UI to import backups
|
||||
* Display timestamps in browser timezone in the UI
|
||||
* Mail eventlog and usage
|
||||
* Add user roles - owner, admin, user manager and user
|
||||
* Setup logrotate configs for collectd since upstream does not set it up
|
||||
* mail: Add X-Envelope-To and X-Envelope-From headers for incoming mails
|
||||
* linode: add object storage backend
|
||||
* restore: carefully replace backup config
|
||||
* spam: per mailbox bayes db and training
|
||||
|
||||
[5.0.3]
|
||||
* Show backup disk usage in graphs
|
||||
* Add per-user app passwords
|
||||
* Make app not responding page customizable
|
||||
* Make footer customizable
|
||||
* Add UI to import backups
|
||||
* Display timestamps in browser timezone in the UI
|
||||
* Mail eventlog and usage
|
||||
* Add user roles - owner, admin, user manager and user
|
||||
* Setup logrotate configs for collectd since upstream does not set it up
|
||||
* mail: Add X-Envelope-To and X-Envelope-From headers for incoming mails
|
||||
* linode: add object storage backend
|
||||
* restore: carefully replace backup config
|
||||
* spam: per mailbox bayes db and training
|
||||
|
||||
[5.0.4]
|
||||
* Fix potential previlige escalation because of ghost file
|
||||
* linode: dns backend
|
||||
* make branding routes owner only
|
||||
* add branding API
|
||||
* Add app start/stop/restart events
|
||||
* Use the primary email for LE account
|
||||
* make mail eventlog more descriptive
|
||||
|
||||
[5.0.5]
|
||||
* Fix bug where incoming mail from dynamic hostnames was rejected
|
||||
* Increase token expiry
|
||||
* Fix bug in tag UI where tag removal did not work
|
||||
|
||||
[5.0.6]
|
||||
* Make mail eventlog only visible to owners
|
||||
* Make app password work with sftp
|
||||
|
||||
[5.1.0]
|
||||
* Add turn addon
|
||||
* Fix disk usage display
|
||||
* Drop support for TLSv1 and TLSv1.1
|
||||
* Make cert validation work for ECC certs
|
||||
* Add type filter to mail eventlog
|
||||
* mail: Fix listing of mailboxes and aliases in the UI
|
||||
* branding: fix login page title
|
||||
* Only a Cloudron owner can install/update/exec apps with the docker addon
|
||||
* security: reset tokens are only valid for a day
|
||||
* mail: fix eventlog db perms
|
||||
* Fix various bugs in the disk graphs
|
||||
|
||||
[5.1.1]
|
||||
* Add turn addon
|
||||
* Fix disk usage display
|
||||
* Drop support for TLSv1 and TLSv1.1
|
||||
* Make cert validation work for ECC certs
|
||||
* Add type filter to mail eventlog
|
||||
* mail: Fix listing of mailboxes and aliases in the UI
|
||||
* branding: fix login page title
|
||||
* Only a Cloudron owner can install/update/exec apps with the docker addon
|
||||
* security: reset tokens are only valid for a day
|
||||
* mail: fix eventlog db perms
|
||||
* Fix various bugs in the disk graphs
|
||||
* Fix collectd installation
|
||||
* graphs: sort disk contents by usage
|
||||
* backups: show apps that are not automatically backed up in backup view
|
||||
|
||||
[5.1.2]
|
||||
* Add turn addon
|
||||
* Fix disk usage display
|
||||
* Drop support for TLSv1 and TLSv1.1
|
||||
* Make cert validation work for ECC certs
|
||||
* Add type filter to mail eventlog
|
||||
* mail: Fix listing of mailboxes and aliases in the UI
|
||||
* branding: fix login page title
|
||||
* Only a Cloudron owner can install/update/exec apps with the docker addon
|
||||
* security: reset tokens are only valid for a day
|
||||
* mail: fix eventlog db perms
|
||||
* Fix various bugs in the disk graphs
|
||||
* Fix collectd installation
|
||||
* graphs: sort disk contents by usage
|
||||
* backups: show apps that are not automatically backed up in backup view
|
||||
* turn: deny local address peers https://www.rtcsec.com/2020/04/01-slack-webrtc-turn-compromise/
|
||||
|
||||
[5.1.3]
|
||||
* Fix crash with misconfigured reverse proxy
|
||||
* Fix issue where invitation links are not working anymore
|
||||
|
||||
[5.1.4]
|
||||
* Add support for custom .well-known documents to be served
|
||||
* Add ECDHE-RSA-AES128-SHA256 to cipher list
|
||||
* Fix GPG signature verification
|
||||
|
||||
[5.1.5]
|
||||
* Check for .well-known routes upstream as fallback. This broke nextcloud's caldav/carddav
|
||||
|
||||
[5.2.0]
|
||||
* acme: request ECC certs
|
||||
* less-strict DKIM check to allow users to set a stronger DKIM key
|
||||
* Add members only flag to mailing list
|
||||
* oauth: add backward compat layer for backup and uninstall
|
||||
* fix bug in disk usage sorting
|
||||
* mail: aliases can be across domains
|
||||
* mail: allow an external MX to be set
|
||||
* Add UI to download backup config as JSON (and import it)
|
||||
* Ensure stopped apps are getting backed up
|
||||
* Add OVH Object Storage backend
|
||||
* Add per-app redis status and configuration to Services
|
||||
* spam: large emails were not scanned
|
||||
* mail relay: fix delivery event log
|
||||
* manual update check always gets the latest updates
|
||||
* graphs: fix issue where large number of apps would crash the box code (query param limit exceeded)
|
||||
* backups: fix various security issues in encypted backups (thanks @mehdi)
|
||||
* graphs: add app graphs
|
||||
* older encrypted backups cannot be used in this version
|
||||
* Add backup listing UI
|
||||
* stopping an app will stop dependent services
|
||||
* Add new wasabi s3 storage region us-east-2
|
||||
* mail: Fix bug where SRS translation was done on the main domain instead of mailing list domain
|
||||
* backups: add retention policy
|
||||
* Drop `NET_RAW` caps from container preventing sniffing of network traffic
|
||||
|
||||
[5.2.1]
|
||||
* Fix app disk graphs
|
||||
* restart apps on addon container change
|
||||
|
||||
[5.2.2]
|
||||
* regression: import UI
|
||||
* Mbps -> MBps
|
||||
* Remove verbose logs
|
||||
* Set dmode in tar extract
|
||||
* mail: fix crash in audit logs
|
||||
* import: fix crash because encryption is unset
|
||||
* create redis with the correct label
|
||||
|
||||
[5.2.3]
|
||||
* Do not restart stopped apps
|
||||
|
||||
[5.2.4]
|
||||
* mail: enable/disable incoming mail was showing an error
|
||||
* Do not trigger backup of stopped apps. Instead, we will just retain it's existing backups
|
||||
based on retention policy
|
||||
* remove broken disk graphs
|
||||
* fix OVH backups
|
||||
|
||||
[5.3.0]
|
||||
* better nginx config for higher loads
|
||||
* backups: add CIFS storage provider
|
||||
* backups: add SSHFS storage provider
|
||||
* backups: add NFS storage provider
|
||||
* s3: use vhost style
|
||||
* Fix crash when redis config was set
|
||||
* Update schedule was unselected in the UI
|
||||
* cloudron-setup: --provider is now optional
|
||||
* show warning for unstable updates
|
||||
* add forumUrl to app manifest
|
||||
* postgresql: add unaccent extension for peertube
|
||||
* mail: Add Auto-Submitted header to NDRs
|
||||
* backups: ensure that the latest backup of installed apps is always preserved
|
||||
* add nginx logs
|
||||
* mail: make authentication case insensitive
|
||||
* Fix timeout issues in postgresql and mysql addon
|
||||
* Do not count stopped apps for memory use
|
||||
* LDAP group synchronization
|
||||
|
||||
[5.3.1]
|
||||
* better nginx config for higher loads
|
||||
* backups: add CIFS storage provider
|
||||
* backups: add SSHFS storage provider
|
||||
* backups: add NFS storage provider
|
||||
* s3: use vhost style
|
||||
* Fix crash when redis config was set
|
||||
* Update schedule was unselected in the UI
|
||||
* cloudron-setup: --provider is now optional
|
||||
* show warning for unstable updates
|
||||
* add forumUrl to app manifest
|
||||
* postgresql: add unaccent extension for peertube
|
||||
* mail: Add Auto-Submitted header to NDRs
|
||||
* backups: ensure that the latest backup of installed apps is always preserved
|
||||
* add nginx logs
|
||||
* mail: make authentication case insensitive
|
||||
* Fix timeout issues in postgresql and mysql addon
|
||||
* Do not count stopped apps for memory use
|
||||
* LDAP group synchronization
|
||||
|
||||
[5.3.2]
|
||||
* Do not install sshfs package
|
||||
* 'provider' is not required anymore in various API calls
|
||||
* redis: Set maxmemory and maxmemory-policy
|
||||
* Add mlock capability to manifest (for vault app)
|
||||
|
||||
[5.3.3]
|
||||
* Fix issue where some postinstall messages where causing angular to infinite loop
|
||||
|
||||
[5.3.4]
|
||||
* Fix issue in database error handling
|
||||
|
||||
[5.4.0]
|
||||
* Update nginx to 1.18 for various security fixes
|
||||
* Add ping capability (for statping app)
|
||||
* Fix bug where aliases were displayed incorrectly in SOGo
|
||||
* Add univention as LDAP provider
|
||||
* Bump max_connection for postgres addon to 200
|
||||
* mail: Add pagination to mailing list API
|
||||
* Allow admin to lock email and display name of users
|
||||
* Allow admin to ensure all users have 2FA setup
|
||||
* ami: fix regression where we didn't send provider as part of get status call
|
||||
* nginx: hide version
|
||||
* backups: add b2 provider
|
||||
* Add filemanager webinterface
|
||||
* Add darkmode
|
||||
* Add note that password reset and invite links expire in 24 hours
|
||||
|
||||
[5.4.1]
|
||||
* Update nginx to 1.18 for various security fixes
|
||||
* Add ping capability (for statping app)
|
||||
* Fix bug where aliases were displayed incorrectly in SOGo
|
||||
* Add univention as LDAP provider
|
||||
* Bump max_connection for postgres addon to 200
|
||||
* mail: Add pagination to mailing list API
|
||||
* Allow admin to lock email and display name of users
|
||||
* Allow admin to ensure all users have 2FA setup
|
||||
* ami: fix regression where we didn't send provider as part of get status call
|
||||
* nginx: hide version
|
||||
* backups: add b2 provider
|
||||
* Add filemanager webinterface
|
||||
* Add darkmode
|
||||
* Add note that password reset and invite links expire in 24 hours
|
||||
|
||||
[5.5.0]
|
||||
* postgresql: update to PostgreSQL 11
|
||||
* postgresql: add citext extension to whitelist for loomio
|
||||
* postgresql: add btree_gist,postgres_fdw,pg_stat_statements,plpgsql extensions for gitlab
|
||||
* SFTP/Filebrowser: fix access of external data directories
|
||||
* Fix contrast issues in dark mode
|
||||
* Add option to delete mailbox data when mailbox is delete
|
||||
* Allow days/hours of backups and updates to be configurable
|
||||
* backup cleaner: fix issue where referenced backups where not counted against time periods
|
||||
* route53: fix issue where verification failed if user had more than 100 zones
|
||||
* rework task workers to run them in a separate cgroup
|
||||
* backups: now much faster thanks to reworking of task worker
|
||||
* When custom fallback cert is set, make sure it's used over LE certs
|
||||
* mongodb: update to MongoDB 4.0.19
|
||||
* List groups ordered by name
|
||||
* Invite links are now valid for a week
|
||||
* Update release GPG key
|
||||
* Add pre-defined variables ($CLOUDRON_APPID) for better post install messages
|
||||
* filemanager: show folder first
|
||||
|
||||
[5.6.0]
|
||||
* Remove IP nginx configuration that redirects to dashboard after activation
|
||||
* dashboard: looks for search string in app title as well
|
||||
* Add vaapi caps for transcoding
|
||||
* Fix issue where the long mongodb database names where causing app indices of rocket.chat to overflow (> 127)
|
||||
* Do not resize swap if swap file exists. This means that users can now control how swap is allocated on their own.
|
||||
* SFTP: fix issue where parallel rebuilds would cause an error
|
||||
* backups: make part size configurable
|
||||
* mail: set max email size
|
||||
* mail: allow mail server location to be set
|
||||
* spamassassin: custom configs and wl/bl
|
||||
* Do not automatically update to unstable release
|
||||
* scheduler: reduce container churn
|
||||
* mail: add API to set banner
|
||||
* Fix bug where systemd 237 ignores --nice value in systemd-run
|
||||
* postgresql: enable uuid-ossp extension
|
||||
* firewall: add blocklist
|
||||
* HTTP URLs now redirect directly to the HTTPS of the final domain
|
||||
* linode: Add singapore region
|
||||
* ovh: add sydney region
|
||||
* s3: makes multi-part copies in parallel
|
||||
|
||||
[5.6.1]
|
||||
* Blocklists are now stored in a text file instead of json
|
||||
* regenerate nginx configs
|
||||
|
||||
[5.6.2]
|
||||
* Update docker to 19.03.12
|
||||
* Fix sorting of user listing in the UI
|
||||
* namecheap: fix crash when server returns invalid response
|
||||
* unlink ghost file automatically on successful login
|
||||
* Bump mysql addon connection limit to 200
|
||||
* Fix install issue where `/dev/dri` may not be present
|
||||
* import: when importing filesystem backups, the input box is a path
|
||||
* firewall: fix race condition where blocklist was not added in correct position in the FORWARD chain
|
||||
* services: fix issue where services where scaled up/down too fast
|
||||
* turn: realm variable was not updated properly on dashboard change
|
||||
* nginx: add splash pages for IP based browser access
|
||||
* Give services panel a separate top-level view
|
||||
* Add app state filter
|
||||
* gcs: copy concurrency was not used
|
||||
* Mention why an app update cannot be applied and provide shortcut to start the app if stopped
|
||||
* Remove version from footer into the setting view
|
||||
* Give services panel a separate top-level view
|
||||
* postgresql: set collation order explicity when creating database to C.UTF-8 (for confluence)
|
||||
* rsync: fix error while goes missing when syncing
|
||||
* Pre-select app domain by default in the redirection drop down
|
||||
* robots: preseve leading and trailing whitespaces/newlines
|
||||
|
||||
[5.6.3]
|
||||
* Fix postgres locale issue
|
||||
|
||||
[6.0.0]
|
||||
* Focal support
|
||||
* Reduce duration of self-signed certs to 800 days
|
||||
* Better backup config filename when downloading
|
||||
* branding: footer can have template variables like %YEAR% and %VERSION%
|
||||
* sftp: secure the API with a token
|
||||
* filemanager: Add extract context menu item
|
||||
* Do not download docker images if present locally
|
||||
* sftp: disable access to non-admins by default
|
||||
* postgresql: whitelist pgcrypto extension for loomio
|
||||
* filemanager: Add new file creation action and collapse new and upload actions
|
||||
* rsync: add warning to remove lifecycle rules
|
||||
* Add volume management
|
||||
* backups: adjust node's heap size based on memory limit
|
||||
* s3: diasble per-chunk timeout
|
||||
* logs: more descriptive log file names on download
|
||||
* collectd: remove collectd config when app stopped (and add it back when started)
|
||||
* Apps can optionally request an authwall to be installed in front of them
|
||||
* mailbox can now owned by a group
|
||||
* linode: enable dns provider in setup view
|
||||
* dns: apps can now use the dns port
|
||||
* httpPaths: allow apps to specify forwarding from custom paths to container ports (for OLS)
|
||||
* add elasticemail smtp relay option
|
||||
* mail: add option to fts using solr
|
||||
* mail: change the namespace separator of new installations to /
|
||||
* mail: enable acl
|
||||
* Disable THP
|
||||
* filemanager: allow download dirs as zip files
|
||||
* aws: add china region
|
||||
* security: fix issue where apps could send with any username (but valid password)
|
||||
* i18n support
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
The Cloudron Subscription license
|
||||
Copyright (c) 2019 Cloudron UG
|
||||
Copyright (c) 2020 Cloudron UG
|
||||
|
||||
With regard to the Cloudron Software:
|
||||
|
||||
|
||||
@@ -29,9 +29,9 @@ anyone to effortlessly host web applications on their server on their own terms.
|
||||
* Trivially migrate to another server keeping your apps and data (for example, switch your
|
||||
infrastructure provider or move to a bigger server).
|
||||
|
||||
* Comprehensive [REST API](https://cloudron.io/documentation/developer/api/).
|
||||
* Comprehensive [REST API](https://docs.cloudron.io/api/).
|
||||
|
||||
* [CLI](https://cloudron.io/documentation/cli/) to configure apps.
|
||||
* [CLI](https://docs.cloudron.io/custom-apps/cli/) to configure apps.
|
||||
|
||||
* Alerts, audit logs, graphs, dns management ... and much more
|
||||
|
||||
@@ -41,25 +41,37 @@ Try our demo at https://my.demo.cloudron.io (username: cloudron password: cloudr
|
||||
|
||||
## Installing
|
||||
|
||||
[Install script](https://cloudron.io/documentation/installation/) - [Pricing](https://cloudron.io/pricing.html)
|
||||
[Install script](https://docs.cloudron.io/installation/) - [Pricing](https://cloudron.io/pricing.html)
|
||||
|
||||
**Note:** This repo is a small part of what gets installed on your server - there is
|
||||
the dashboard, database addons, graph container, base image etc. Cloudron also relies
|
||||
on external services such as the App Store for apps to be installed. As such, don't
|
||||
clone this repo and npm install and expect something to work.
|
||||
|
||||
## Documentation
|
||||
## Development
|
||||
|
||||
* [Documentation](https://cloudron.io/documentation/)
|
||||
This is the backend code of Cloudron. The frontend code is [here](https://git.cloudron.io/cloudron/dashboard).
|
||||
|
||||
## Related repos
|
||||
The way to develop is to first install a full instance of Cloudron in a VM. Then you can use the [hotfix](https://git.cloudron.io/cloudron/cloudron-machine)
|
||||
tool to patch the VM with the latest code.
|
||||
|
||||
The [base image repo](https://git.cloudron.io/cloudron/docker-base-image) is the parent image of all
|
||||
the containers in the Cloudron.
|
||||
```
|
||||
SSH_PASSPHRASE=sshkeypassword cloudron-machine hotfix --cloudron my.example.com --release 6.0.0 --ssh-key keyname
|
||||
```
|
||||
|
||||
## Community
|
||||
## License
|
||||
|
||||
* [Chat](https://chat.cloudron.io)
|
||||
Please note that the Cloudron code is under a source-available license. This is not the same as an
|
||||
open source license but ensures the code is available for introspection (and hacking!).
|
||||
|
||||
## Contributions
|
||||
|
||||
Just to give some heads up, we are a bit restrictive in merging changes. We are a small team and
|
||||
would like to keep our maintenance burden low. It might be best to discuss features first in the [forum](https://forum.cloudron.io),
|
||||
to also figure out how many other people will use it to justify maintenance for a feature.
|
||||
|
||||
## Support
|
||||
|
||||
* [Documentation](https://docs.cloudron.io/)
|
||||
* [Forum](https://forum.cloudron.io/)
|
||||
* [Support](mailto:support@cloudron.io)
|
||||
|
||||
|
||||
@@ -4,8 +4,7 @@ set -euv -o pipefail
|
||||
|
||||
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
readonly arg_provider="${1:-generic}"
|
||||
readonly arg_infraversionpath="${SOURCE_DIR}/${2:-}"
|
||||
readonly arg_infraversionpath="${SOURCE_DIR}/../src"
|
||||
|
||||
function die {
|
||||
echo $1
|
||||
@@ -14,6 +13,9 @@ function die {
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
readonly ubuntu_codename=$(lsb_release -cs)
|
||||
readonly ubuntu_version=$(lsb_release -rs)
|
||||
|
||||
# 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
|
||||
@@ -27,22 +29,24 @@ debconf-set-selections <<< 'mysql-server mysql-server/root_password_again passwo
|
||||
|
||||
# this enables automatic security upgrades (https://help.ubuntu.com/community/AutomaticSecurityUpdates)
|
||||
# resolvconf is needed for unbound to work property after disabling systemd-resolved in 18.04
|
||||
ubuntu_version=$(lsb_release -rs)
|
||||
ubuntu_codename=$(lsb_release -cs)
|
||||
|
||||
gpg_package=$([[ "${ubuntu_version}" == "16.04" ]] && echo "gnupg" || echo "gpg")
|
||||
mysql_package=$([[ "${ubuntu_version}" == "20.04" ]] && echo "mysql-server-8.0" || echo "mysql-server-5.7")
|
||||
apt-get -y install \
|
||||
acl \
|
||||
build-essential \
|
||||
cifs-utils \
|
||||
cron \
|
||||
curl \
|
||||
debconf-utils \
|
||||
dmsetup \
|
||||
$gpg_package \
|
||||
ipset \
|
||||
iptables \
|
||||
libpython2.7 \
|
||||
linux-generic \
|
||||
logrotate \
|
||||
mysql-server-5.7 \
|
||||
nginx-full \
|
||||
$mysql_package \
|
||||
openssh-server \
|
||||
pwgen \
|
||||
resolvconf \
|
||||
@@ -52,6 +56,12 @@ apt-get -y install \
|
||||
unbound \
|
||||
xfsprogs
|
||||
|
||||
echo "==> installing nginx for xenial for TLSv3 support"
|
||||
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.18.0-2~${ubuntu_codename}_amd64.deb -o /tmp/nginx.deb
|
||||
# apt install with install deps (as opposed to dpkg -i)
|
||||
apt install -y /tmp/nginx.deb
|
||||
rm /tmp/nginx.deb
|
||||
|
||||
# on some providers like scaleway the sudo file is changed and we want to keep the old one
|
||||
apt-get -o Dpkg::Options::="--force-confold" install -y sudo
|
||||
|
||||
@@ -60,10 +70,10 @@ apt-get -o Dpkg::Options::="--force-confold" install -y sudo
|
||||
cp /usr/share/unattended-upgrades/20auto-upgrades /etc/apt/apt.conf.d/20auto-upgrades
|
||||
|
||||
echo "==> Installing node.js"
|
||||
mkdir -p /usr/local/node-10.15.1
|
||||
curl -sL https://nodejs.org/dist/v10.15.1/node-v10.15.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-10.15.1
|
||||
ln -sf /usr/local/node-10.15.1/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-10.15.1/bin/npm /usr/bin/npm
|
||||
mkdir -p /usr/local/node-10.18.1
|
||||
curl -sL https://nodejs.org/dist/v10.18.1/node-v10.18.1-linux-x64.tar.gz | tar zxf - --strip-components=1 -C /usr/local/node-10.18.1
|
||||
ln -sf /usr/local/node-10.18.1/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-10.18.1/bin/npm /usr/bin/npm
|
||||
apt-get install -y python # Install python which is required for npm rebuild
|
||||
[[ "$(python --version 2>&1)" == "Python 2.7."* ]] || die "Expecting python version to be 2.7.x"
|
||||
|
||||
@@ -75,9 +85,9 @@ mkdir -p /etc/systemd/system/docker.service.d
|
||||
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=overlay2" > /etc/systemd/system/docker.service.d/cloudron.conf
|
||||
|
||||
# there are 3 packages for docker - containerd, CLI and the daemon
|
||||
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_1.2.2-3_amd64.deb" -o /tmp/containerd.deb
|
||||
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce-cli_18.09.2~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker-ce-cli.deb
|
||||
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce_18.09.2~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker.deb
|
||||
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_1.2.13-2_amd64.deb" -o /tmp/containerd.deb
|
||||
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce-cli_19.03.12~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker-ce-cli.deb
|
||||
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce_19.03.12~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker.deb
|
||||
# apt install with install deps (as opposed to dpkg -i)
|
||||
apt install -y /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb
|
||||
rm /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb
|
||||
@@ -109,11 +119,13 @@ for image in ${images}; do
|
||||
done
|
||||
|
||||
echo "==> Install collectd"
|
||||
if ! apt-get install -y collectd collectd-utils; then
|
||||
if ! apt-get install -y libcurl3-gnutls collectd collectd-utils; then
|
||||
# FQDNLookup is true in default debian config. The box code has a custom collectd.conf that fixes this
|
||||
echo "Failed to install collectd. Presumably because of http://mailman.verplant.org/pipermail/collectd/2015-March/006491.html"
|
||||
sed -e 's/^FQDNLookup true/FQDNLookup false/' -i /etc/collectd/collectd.conf
|
||||
fi
|
||||
# https://bugs.launchpad.net/ubuntu/+source/collectd/+bug/1872281
|
||||
[[ "${ubuntu_version}" == "20.04" ]] && echo -e "\nLD_PRELOAD=/usr/lib/python3.8/config-3.8-x86_64-linux-gnu/libpython3.8.so" >> /etc/default/collectd
|
||||
|
||||
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
|
||||
@@ -121,7 +133,16 @@ timedatectl set-ntp 1
|
||||
# mysql follows the system timezone
|
||||
timedatectl set-timezone UTC
|
||||
|
||||
# Disable bind for good measure (on online.net, kimsufi servers these are pre-installed and conflicts with unbound)
|
||||
echo "==> Adding sshd configuration warning"
|
||||
sed -e '/Port 22/ i # NOTE: Cloudron only supports moving SSH to port 202. See https://docs.cloudron.io/security/#securing-ssh-access' -i /etc/ssh/sshd_config
|
||||
|
||||
# https://bugs.launchpad.net/ubuntu/+source/base-files/+bug/1701068
|
||||
echo "==> Disabling motd news"
|
||||
if [ -f "/etc/default/motd-news" ]; then
|
||||
sed -i 's/^ENABLED=.*/ENABLED=0/' /etc/default/motd-news
|
||||
fi
|
||||
|
||||
# Disable bind for good measure (on online.net, kimsufi servers these are pre-installed)
|
||||
systemctl stop bind9 || true
|
||||
systemctl disable bind9 || true
|
||||
|
||||
@@ -133,7 +154,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
|
||||
# on ubuntu 18.04 and 20.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
|
||||
|
||||
@@ -142,4 +163,3 @@ systemctl disable systemd-resolved || true
|
||||
ip6=$([[ -s /proc/net/if_inet6 ]] && echo "yes" || echo "no")
|
||||
echo -e "server:\n\tinterface: 127.0.0.1\n\tdo-ip6: ${ip6}" > /etc/unbound/unbound.conf.d/cloudron-network.conf
|
||||
systemctl restart unbound
|
||||
|
||||
|
||||
@@ -2,68 +2,65 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
// prefix all output with a timestamp
|
||||
// debug() already prefixes and uses process.stderr NOT console.*
|
||||
['log', 'info', 'warn', 'debug', 'error'].forEach(function (log) {
|
||||
var orig = console[log];
|
||||
console[log] = function () {
|
||||
orig.apply(console, [new Date().toISOString()].concat(Array.prototype.slice.call(arguments)));
|
||||
};
|
||||
});
|
||||
|
||||
require('supererror')({ splatchError: true });
|
||||
|
||||
let async = require('async'),
|
||||
config = require('./src/config.js'),
|
||||
ldap = require('./src/ldap.js'),
|
||||
dockerProxy = require('./src/dockerproxy.js'),
|
||||
fs = require('fs'),
|
||||
ldap = require('./src/ldap.js'),
|
||||
paths = require('./src/paths.js'),
|
||||
proxyAuth = require('./src/proxyauth.js'),
|
||||
server = require('./src/server.js');
|
||||
|
||||
console.log();
|
||||
console.log('==========================================');
|
||||
console.log(' Cloudron will use the following settings ');
|
||||
console.log('==========================================');
|
||||
console.log();
|
||||
console.log(' Environment: ', config.CLOUDRON ? 'CLOUDRON' : 'TEST');
|
||||
console.log(' Version: ', config.version());
|
||||
console.log(' Admin Origin: ', config.adminOrigin());
|
||||
console.log(' Appstore API server origin: ', config.apiServerOrigin());
|
||||
console.log(' Appstore Web server origin: ', config.webServerOrigin());
|
||||
console.log(' SysAdmin Port: ', config.get('sysadminPort'));
|
||||
console.log(' LDAP Server Port: ', config.get('ldapPort'));
|
||||
console.log(' Docker Proxy Port: ', config.get('dockerProxyPort'));
|
||||
console.log();
|
||||
console.log('==========================================');
|
||||
console.log();
|
||||
const NOOP_CALLBACK = function () { };
|
||||
|
||||
function setupLogging(callback) {
|
||||
if (process.env.BOX_ENV === 'test') return callback();
|
||||
|
||||
var logfileStream = fs.createWriteStream(paths.BOX_LOG_FILE, { flags:'a' });
|
||||
process.stdout.write = process.stderr.write = logfileStream.write.bind(logfileStream);
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
async.series([
|
||||
server.start,
|
||||
setupLogging,
|
||||
server.start, // do this first since it also inits the database
|
||||
proxyAuth.start,
|
||||
ldap.start,
|
||||
dockerProxy.start
|
||||
], function (error) {
|
||||
if (error) {
|
||||
console.error('Error starting server', error);
|
||||
console.log('Error starting server', error);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('Cloudron is up and running');
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
// require those here so that logging handler is already setup
|
||||
require('supererror');
|
||||
const debug = require('debug')('box:box');
|
||||
|
||||
process.on('SIGINT', function () {
|
||||
debug('Received SIGINT. Shutting down.');
|
||||
|
||||
proxyAuth.stop(NOOP_CALLBACK);
|
||||
server.stop(NOOP_CALLBACK);
|
||||
ldap.stop(NOOP_CALLBACK);
|
||||
dockerProxy.stop(NOOP_CALLBACK);
|
||||
setTimeout(process.exit.bind(process), 3000);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', function () {
|
||||
debug('Received SIGTERM. Shutting down.');
|
||||
|
||||
proxyAuth.stop(NOOP_CALLBACK);
|
||||
server.stop(NOOP_CALLBACK);
|
||||
ldap.stop(NOOP_CALLBACK);
|
||||
dockerProxy.stop(NOOP_CALLBACK);
|
||||
setTimeout(process.exit.bind(process), 3000);
|
||||
});
|
||||
|
||||
process.on('uncaughtException', function (error) {
|
||||
console.error((error && error.stack) ? error.stack : error);
|
||||
setTimeout(process.exit.bind(process, 1), 3000);
|
||||
});
|
||||
|
||||
console.log(`Cloudron is up and running. Logs are at ${paths.BOX_LOG_FILE}`); // this goes to journalctl
|
||||
});
|
||||
|
||||
@@ -12,8 +12,6 @@ exports.up = function(db, callback) {
|
||||
db.all('SELECT * FROM users WHERE admin=1', function (error, results) {
|
||||
if (error) return done(error);
|
||||
|
||||
console.dir(results);
|
||||
|
||||
async.eachSeries(results, function (r, next) {
|
||||
db.runSql('INSERT INTO groupMembers (groupId, userId) VALUES (?, ?)', [ ADMIN_GROUP_ID, r.id ], next);
|
||||
}, done);
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async'),
|
||||
crypto = require('crypto'),
|
||||
fs = require('fs'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
safe = require('safetydance'),
|
||||
tldjs = require('tldjs');
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT * FROM apps, subdomains WHERE apps.id=subdomains.appId AND type="primary"', function (error, apps) {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE apps DROP FOREIGN KEY apps_owner_constraint'),
|
||||
db.runSql.bind(db, 'ALTER TABLE apps DROP COLUMN ownerId')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async'),
|
||||
fs = require('fs');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
if (!fs.existsSync('/etc/cloudron/cloudron.conf')) {
|
||||
console.log('Unable to locate cloudron.conf');
|
||||
return callback();
|
||||
}
|
||||
|
||||
const config = JSON.parse(fs.readFileSync('/etc/cloudron/cloudron.conf', 'utf8'));
|
||||
|
||||
async.series([
|
||||
fs.writeFile.bind(null, '/etc/cloudron/PROVIDER', config.provider, 'utf8'),
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
// we use replace instead of insert because the cloudron-setup adds api/web_server_origin even for legacy setups
|
||||
db.runSql.bind(db, 'REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'api_server_origin', config.apiServerOrigin ]),
|
||||
db.runSql.bind(db, 'REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'web_server_origin', config.webServerOrigin ]),
|
||||
db.runSql.bind(db, 'REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'admin_domain', config.adminDomain ]),
|
||||
db.runSql.bind(db, 'REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'admin_fqdn', config.adminFqdn ]),
|
||||
db.runSql.bind(db, 'REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'demo', config.isDemo ]),
|
||||
db.runSql.bind(db, 'COMMIT')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users ADD COLUMN active BOOLEAN DEFAULT 1', function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users DROP COLUMN active', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE apps ADD COLUMN taskId INTEGER'),
|
||||
db.runSql.bind(db, 'ALTER TABLE apps ADD CONSTRAINT apps_task_constraint FOREIGN KEY(taskId) REFERENCES tasks(id)')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE app DROP FOREIGN KEY apps_task_constraint'),
|
||||
db.runSql.bind(db, 'ALTER TABLE apps DROP COLUMN taskId'),
|
||||
], callback);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP updateConfigJson, DROP restoreConfigJson, DROP oldConfigJson', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps CHANGE installationProgress errorJson TEXT', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps CHANGE errorJson installationProgress TEXT', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users ADD COLUMN source VARCHAR(128) DEFAULT ""', function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users DROP COLUMN source', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
'use strict';
|
||||
|
||||
let async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE tasks CHANGE errorMessage errorJson TEXT', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
// convert error messages into json
|
||||
db.all('SELECT id, errorJson FROM apps', function (error, apps) {
|
||||
async.eachSeries(apps, function (app, iteratorDone) {
|
||||
if (app.errorJson === 'null') return iteratorDone();
|
||||
if (app.errorJson === null) return iteratorDone();
|
||||
|
||||
db.runSql('UPDATE apps SET errorJson = ? WHERE id = ?', [ JSON.stringify({ message: app.errorJson }), app.id ], iteratorDone);
|
||||
}, callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE tasks CHANGE errorJson errorMessage TEXT', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
// imports mailbox entries for existing users
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT * FROM mailboxes', function (error, mailboxes) {
|
||||
async.eachSeries(mailboxes, function (mailbox, iteratorDone) {
|
||||
if (!mailbox.membersJson) return iteratorDone();
|
||||
|
||||
let members = JSON.parse(mailbox.membersJson);
|
||||
members = members.map((m) => m && m.indexOf('@') === -1 ? `${m}@${mailbox.domain}` : m); // only because we don't do things in a xction
|
||||
|
||||
db.runSql('UPDATE mailboxes SET membersJson=? WHERE name=? AND domain=?', [ JSON.stringify(members), mailbox.name, mailbox.domain ], iteratorDone);
|
||||
}, callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('UPDATE apps SET runState=? WHERE runState IS NULL', [ 'running' ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
db.runSql('ALTER TABLE apps MODIFY runState VARCHAR(512) NOT NULL', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE app MODIFY runState VARCHAR(512)', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
// We clear all demo state in the Cloudron...the demo cloudron needs manual intervention afterwards
|
||||
db.runSql('REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'demo', '' ], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN reverseProxyConfigJson TEXT', function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
db.all('SELECT id, robotsTxt FROM apps', function (error, apps) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(apps, function (app, iteratorDone) {
|
||||
if (!app.robotsTxt) return iteratorDone();
|
||||
|
||||
db.runSql('UPDATE apps SET reverseProxyConfigJson=? WHERE id=?', [ JSON.stringify({ robotsTxt: JSON.stringify(app.robotsTxt) }), app.id ], iteratorDone);
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN robotsTxt', callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE apps DROP COLUMN reverseProxyConfigJson'),
|
||||
], callback);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
var fs = require('fs');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
let sysinfoConfig = { provider: 'generic' };
|
||||
|
||||
db.runSql('REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'sysinfo_config', JSON.stringify(sysinfoConfig) ], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE apps ADD COLUMN mailboxDomain VARCHAR(128)'),
|
||||
function setDefaultMailboxDomain(done) {
|
||||
db.all('SELECT * FROM apps, subdomains WHERE apps.id=subdomains.appId AND type="primary"', function (error, apps) {
|
||||
if (error) return done(error);
|
||||
|
||||
async.eachSeries(apps, function (app, iteratorDone) {
|
||||
db.runSql('UPDATE apps SET mailboxDomain=? WHERE id=?', [ app.domain, app.id ], iteratorDone);
|
||||
}, done);
|
||||
});
|
||||
},
|
||||
db.runSql.bind(db, 'ALTER TABLE apps MODIFY COLUMN mailboxDomain VARCHAR(128) NOT NULL'),
|
||||
db.runSql.bind(db, 'ALTER TABLE apps ADD CONSTRAINT apps_mailDomain_constraint FOREIGN KEY(mailboxDomain) REFERENCES domains(domain)'),
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE app DROP FOREIGN KEY apps_mailDomain_constraint'),
|
||||
db.runSql.bind(db, 'ALTER TABLE apps DROP COLUMN mailboxDomain'),
|
||||
], callback);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
let async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('SELECT * FROM domains', function (error, domains) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(domains, function (domain, iteratorCallback) {
|
||||
if (domain.provider !== 'cloudflare') return iteratorCallback();
|
||||
|
||||
let config = JSON.parse(domain.configJson);
|
||||
config.tokenType = 'GlobalApiKey';
|
||||
|
||||
db.runSql('UPDATE domains SET configJson = ? WHERE domain = ?', [ JSON.stringify(config), domain.domain ], iteratorCallback);
|
||||
}, callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN cpuShares INTEGER DEFAULT 512', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN cpuShares', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
var cmd = 'CREATE TABLE appPasswords(' +
|
||||
'id VARCHAR(128) NOT NULL UNIQUE,' +
|
||||
'name VARCHAR(128) NOT NULL,' +
|
||||
'userId VARCHAR(128) NOT NULL,' +
|
||||
'identifier VARCHAR(128) NOT NULL,' +
|
||||
'hashedPassword VARCHAR(1024) NOT NULL,' +
|
||||
'creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,' +
|
||||
'FOREIGN KEY(userId) REFERENCES users(id),' +
|
||||
'UNIQUE (name, userId),' +
|
||||
'PRIMARY KEY (id)) CHARACTER SET utf8 COLLATE utf8_bin';
|
||||
|
||||
db.runSql(cmd, function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('DROP TABLE appPasswords', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('DROP TABLE authcodes', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
var cmd = `CREATE TABLE IF NOT EXISTS authcodes(
|
||||
authCode VARCHAR(128) NOT NULL UNIQUE,
|
||||
userId VARCHAR(128) NOT NULL,
|
||||
clientId VARCHAR(128) NOT NULL,
|
||||
expiresAt BIGINT NOT NULL,
|
||||
PRIMARY KEY(authCode)) CHARACTER SET utf8 COLLATE utf8_bin`;
|
||||
|
||||
db.runSql(cmd, function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('DROP TABLE clients', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
var cmd = `CREATE TABLE IF NOT EXISTS clients(
|
||||
id VARCHAR(128) NOT NULL UNIQUE,
|
||||
appId VARCHAR(128) NOT NULL,
|
||||
type VARCHAR(16) NOT NULL,
|
||||
clientSecret VARCHAR(512) NOT NULL,
|
||||
redirectURI VARCHAR(512) NOT NULL,
|
||||
scope VARCHAR(512) NOT NULL,
|
||||
PRIMARY KEY(id)) CHARACTER SET utf8 COLLATE utf8_bin`;
|
||||
|
||||
db.runSql(cmd, function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE domains DROP COLUMN locked', function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE domains ADD COLUMN locked BOOLEAN DEFAULT 0', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
'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 users ADD COLUMN role VARCHAR(32)'),
|
||||
function migrateAdminFlag(done) {
|
||||
db.all('SELECT * FROM users ORDER BY createdAt', function (error, results) {
|
||||
if (error) return done(error);
|
||||
let ownerFound = false;
|
||||
|
||||
async.eachSeries(results, function (user, next) {
|
||||
let role;
|
||||
if (!ownerFound && user.admin) {
|
||||
role = 'owner';
|
||||
ownerFound = true;
|
||||
console.log(`Designating ${user.username} ${user.email} ${user.id} as the owner of this cloudron`);
|
||||
} else {
|
||||
role = user.admin ? 'admin' : 'user';
|
||||
}
|
||||
db.runSql('UPDATE users SET role=? WHERE id=?', [ role, user.id ], next);
|
||||
}, done);
|
||||
});
|
||||
},
|
||||
db.runSql.bind(db, 'ALTER TABLE users DROP COLUMN admin'),
|
||||
db.runSql.bind(db, 'ALTER TABLE users MODIFY role VARCHAR(32) NOT NULL'),
|
||||
db.runSql.bind(db, 'COMMIT')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users DROP COLUMN role', function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users ADD COLUMN resetTokenCreationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users DROP COLUMN resetTokenCreationTime', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
let async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps MODIFY mailboxDomain VARCHAR(128)', [], function (error) { // make it nullable
|
||||
if (error) console.error(error);
|
||||
|
||||
// clear mailboxName/Domain for apps that do not use mail addons
|
||||
db.all('SELECT * FROM apps', function (error, apps) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(apps, function (app, iteratorDone) {
|
||||
var manifest = JSON.parse(app.manifestJson);
|
||||
if (manifest.addons['sendmail'] || manifest.addons['recvmail']) return iteratorDone();
|
||||
|
||||
db.runSql('UPDATE apps SET mailboxName=?, mailboxDomain=? WHERE id=?', [ null, null, app.id ], iteratorDone);
|
||||
}, callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps MODIFY manifestJson VARCHAR(128) NOT NULL', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE mailboxes ADD COLUMN membersOnly BOOLEAN DEFAULT 0', function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users DROP COLUMN membersOnly', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD COLUMN aliasDomain VARCHAR(128)'),
|
||||
function setAliasDomain(done) {
|
||||
db.all('SELECT * FROM mailboxes', function (error, mailboxes) {
|
||||
async.eachSeries(mailboxes, function (mailbox, iteratorDone) {
|
||||
if (!mailbox.aliasTarget) return iteratorDone();
|
||||
|
||||
db.runSql('UPDATE mailboxes SET aliasDomain=? WHERE name=? AND domain=?', [ mailbox.domain, mailbox.name, mailbox.domain ], iteratorDone);
|
||||
}, done);
|
||||
});
|
||||
},
|
||||
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD CONSTRAINT mailboxes_aliasDomain_constraint FOREIGN KEY(aliasDomain) REFERENCES mail(domain)'),
|
||||
db.runSql.bind(db, 'ALTER TABLE mailboxes CHANGE aliasTarget aliasName VARCHAR(128)')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE mailboxes DROP FOREIGN KEY mailboxes_aliasDomain_constraint'),
|
||||
db.runSql.bind(db, 'ALTER TABLE mailboxes DROP COLUMN aliasDomain'),
|
||||
db.runSql.bind(db, 'ALTER TABLE mailboxes CHANGE aliasName aliasTarget VARCHAR(128)')
|
||||
], callback);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN servicesConfigJson TEXT', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN servicesConfigJson', 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 bindsJson TEXT', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN bindsJson', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
'use strict';
|
||||
|
||||
const backups = require('../src/backups.js'),
|
||||
fs = require('fs');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
|
||||
if (error || results.length === 0) return callback(error);
|
||||
|
||||
var backupConfig = JSON.parse(results[0].value);
|
||||
if (backupConfig.key) {
|
||||
backupConfig.encryption = backups.generateEncryptionKeysSync(backupConfig.key);
|
||||
backups.cleanupCacheFilesSync();
|
||||
|
||||
fs.writeFileSync('/home/yellowtent/platformdata/BACKUP_PASSWORD',
|
||||
'This file contains your Cloudron backup password.\nBefore Cloudron v5.2, this was saved in the database.' +
|
||||
'From Cloudron 5.2, this password is not required anymore. We generate strong keys based off this password and use those keys to encrypt the backups.\n' +
|
||||
'This means that the password is only required at decryption/restore time.\n\n' +
|
||||
'This file can be safely removed and only exists for the off-chance that you do not remember your backup password.\n\n' +
|
||||
`Password: ${backupConfig.key}\n`,
|
||||
'utf8');
|
||||
|
||||
} else {
|
||||
backupConfig.encryption = null;
|
||||
}
|
||||
|
||||
delete backupConfig.key;
|
||||
|
||||
db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [ JSON.stringify(backupConfig) ], callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE backups CHANGE version packageVersion VARCHAR(128) NOT NULL', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE backups CHANGE packageVersion version VARCHAR(128) NOT NULL', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE backups ADD COLUMN encryptionVersion INTEGER', function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
|
||||
if (error || results.length === 0) return callback(error);
|
||||
|
||||
var backupConfig = JSON.parse(results[0].value);
|
||||
if (!backupConfig.encryption) return callback(null);
|
||||
|
||||
// mark old encrypted backups as v1
|
||||
db.runSql('UPDATE backups SET encryptionVersion=1', callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE backups DROP COLUMN encryptionVersion', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
'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.retentionPolicy = { keepWithinSecs: backupConfig.retentionSecs };
|
||||
delete backupConfig.retentionSecs;
|
||||
|
||||
// mark old encrypted backups as v1
|
||||
db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [ JSON.stringify(backupConfig) ], callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
|
||||
if (error || results.length === 0) return callback(error);
|
||||
|
||||
var backupConfig = JSON.parse(results[0].value);
|
||||
if (backupConfig.provider !== 'minio' && backupConfig.provider !== 's3-v4-compat') return callback();
|
||||
|
||||
backupConfig.s3ForcePathStyle = true; // usually minio is self-hosted. s3 v4 compat, we don't know
|
||||
|
||||
db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [ JSON.stringify(backupConfig) ], callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
|
||||
// http://stackoverflow.com/questions/386294/what-is-the-maximum-length-of-a-valid-email-address
|
||||
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE appPasswords DROP INDEX name'),
|
||||
db.runSql.bind(db, 'ALTER TABLE appPasswords ADD CONSTRAINT appPasswords_name_userId_identifier UNIQUE (name, userId, identifier)'),
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE userGroups ADD COLUMN source VARCHAR(128) DEFAULT ""', function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE userGroups DROP COLUMN source', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
'use strict';
|
||||
|
||||
const async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE backups ADD COLUMN identifier VARCHAR(128)', function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
|
||||
db.all('SELECT * FROM backups', function (error, backups) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(backups, function (backup, next) {
|
||||
let identifier = 'unknown';
|
||||
|
||||
if (backup.type === 'box') {
|
||||
identifier = 'box';
|
||||
} else {
|
||||
const match = backup.id.match(/app_(.+?)_.+/);
|
||||
if (match) identifier = match[1];
|
||||
}
|
||||
|
||||
db.runSql('UPDATE backups SET identifier=? WHERE id=?', [ identifier, backup.id ], next);
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
db.runSql('ALTER TABLE backups MODIFY COLUMN identifier VARCHAR(128) NOT NULL', callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE backups DROP COLUMN identifier', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users ADD COLUMN ts TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP', function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
db.runSql('ALTER TABLE users DROP COLUMN modifiedAt', callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users DROP COLUMN ts', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
|
||||
if (error || results.length === 0) return callback(error);
|
||||
|
||||
var backupConfig = JSON.parse(results[0].value);
|
||||
if (backupConfig.intervalSecs === 6 * 60 * 60) { // every 6 hours
|
||||
backupConfig.schedulePattern = '00 00 5,11,17,23 * * *';
|
||||
} else if (backupConfig.intervalSecs === 12 * 60 * 60) { // every 12 hours
|
||||
backupConfig.schedulePattern = '00 00 5,17 * * *';
|
||||
} else if (backupConfig.intervalSecs === 24 * 60 * 60) { // every day
|
||||
backupConfig.schedulePattern = '00 00 23 * * *';
|
||||
} else if (backupConfig.intervalSecs === 3 * 24 * 60 * 60) { // every 3 days (based on day)
|
||||
backupConfig.schedulePattern = '00 00 23 * * 1,3,5';
|
||||
} else if (backupConfig.intervalSecs === 7 * 24 * 60 * 60) { // every week (saturday)
|
||||
backupConfig.schedulePattern = '00 00 23 * * 6';
|
||||
} else { // default to everyday
|
||||
backupConfig.schedulePattern = '00 00 23 * * *';
|
||||
}
|
||||
|
||||
delete backupConfig.intervalSecs;
|
||||
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';
|
||||
|
||||
const async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT value FROM settings WHERE name="admin_domain"', function (error, results) {
|
||||
if (error || results.length === 0) return callback(error);
|
||||
|
||||
const adminDomain = results[0].value;
|
||||
|
||||
async.series([
|
||||
db.runSql.bind(db, 'INSERT INTO settings (name, value) VALUES (?, ?)', [ 'mail_domain', adminDomain ]),
|
||||
db.runSql.bind(db, 'INSERT INTO settings (name, value) VALUES (?, ?)', [ 'mail_fqdn', `my.${adminDomain}` ])
|
||||
], callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'DELETE FROM settings WHERE name="mail_domain"'),
|
||||
db.runSql.bind(db, 'DELETE FROM settings WHERE name="mail_fqdn"'),
|
||||
], callback);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('SELECT * FROM settings WHERE name=?', ['app_autoupdate_pattern'], function (error, results) {
|
||||
if (error || results.length === 0) return callback(error); // will use defaults from box code
|
||||
|
||||
var updatePattern = results[0].value; // use app auto update patter for the box as well
|
||||
|
||||
async.series([
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
db.runSql.bind(db, 'DELETE FROM settings WHERE name=? OR name=?', ['app_autoupdate_pattern', 'box_autoupdate_pattern']),
|
||||
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', ['autoupdate_pattern', updatePattern]),
|
||||
db.runSql.bind(db, 'COMMIT')
|
||||
], callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE mail ADD COLUMN bannerJson TEXT', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE mail DROP COLUMN bannerJson', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
'use strict';
|
||||
|
||||
const OLD_FIREWALL_CONFIG_JSON = '/home/yellowtent/boxdata/firewall-config.json';
|
||||
const PORTS_FILE = '/home/yellowtent/boxdata/firewall/ports.json';
|
||||
const BLOCKLIST_FILE = '/home/yellowtent/boxdata/firewall/blocklist.txt';
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
exports.up = function (db, callback) {
|
||||
if (!fs.existsSync(OLD_FIREWALL_CONFIG_JSON)) return callback();
|
||||
|
||||
try {
|
||||
const dataJson = fs.readFileSync(OLD_FIREWALL_CONFIG_JSON, 'utf8');
|
||||
const data = JSON.parse(dataJson);
|
||||
fs.writeFileSync(BLOCKLIST_FILE, data.blocklist.join('\n') + '\n', 'utf8');
|
||||
fs.writeFileSync(PORTS_FILE, JSON.stringify({ allowed_tcp_ports: data.allowed_tcp_ports }, null, 4), 'utf8');
|
||||
fs.unlinkSync(OLD_FIREWALL_CONFIG_JSON);
|
||||
} catch (error) {
|
||||
console.log('Error migrating old firewall config', error);
|
||||
}
|
||||
|
||||
callback();
|
||||
};
|
||||
|
||||
exports.down = function (db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
var cmd1 = 'CREATE TABLE volumes(' +
|
||||
'id VARCHAR(128) NOT NULL UNIQUE,' +
|
||||
'name VARCHAR(256) NOT NULL UNIQUE,' +
|
||||
'hostPath VARCHAR(1024) NOT NULL UNIQUE,' +
|
||||
'creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,' +
|
||||
'PRIMARY KEY (id)) CHARACTER SET utf8 COLLATE utf8_bin';
|
||||
|
||||
var cmd2 = 'CREATE TABLE appMounts(' +
|
||||
'appId VARCHAR(128) NOT NULL,' +
|
||||
'volumeId VARCHAR(128) NOT NULL,' +
|
||||
'readOnly BOOLEAN DEFAULT 1,' +
|
||||
'UNIQUE KEY appMounts_appId_volumeId (appId, volumeId),' +
|
||||
'FOREIGN KEY(appId) REFERENCES apps(id),' +
|
||||
'FOREIGN KEY(volumeId) REFERENCES volumes(id)) CHARACTER SET utf8 COLLATE utf8_bin;';
|
||||
|
||||
db.runSql(cmd1, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
db.runSql(cmd2, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN bindsJson', callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('DROP TABLE appMounts', function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
db.runSql('DROP TABLE volumes', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN proxyAuth BOOLEAN DEFAULT 0', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN proxyAuth', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD COLUMN ownerType VARCHAR(16)'),
|
||||
db.runSql.bind(db, 'UPDATE mailboxes SET ownerType=?', [ 'user' ]),
|
||||
db.runSql.bind(db, 'ALTER TABLE mailboxes MODIFY ownerType VARCHAR(16) NOT NULL'),
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE mailboxes DROP COLUMN ownerType', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE apps DROP COLUMN httpPort')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
const async = require('async'),
|
||||
iputils = require('../src/iputils.js');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN containerIp VARCHAR(16) UNIQUE', function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
let baseIp = iputils.intFromIp('172.18.16.0');
|
||||
|
||||
db.all('SELECT * FROM apps', function (error, apps) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(apps, function (app, iteratorDone) {
|
||||
const nextIp = iputils.ipFromInt(++baseIp);
|
||||
db.runSql('UPDATE apps SET containerIp=? WHERE id=?', [ nextIp, app.id ], iteratorDone);
|
||||
}, callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN containerIp', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT * FROM settings WHERE name=?', ['platform_config'], function (error, results) {
|
||||
let value;
|
||||
if (error || results.length === 0) {
|
||||
value = { sftp: { requireAdmin: true } };
|
||||
} else {
|
||||
value = JSON.parse(results[0].value);
|
||||
if (!value.sftp) value.sftp = {};
|
||||
value.sftp.requireAdmin = true;
|
||||
}
|
||||
|
||||
// existing installations may not even have the key. so use REPLACE instead of UPDATE
|
||||
db.runSql('REPLACE INTO settings (name, value) VALUES (?, ?)', [ 'platform_config', JSON.stringify(value) ], callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
+62
-42
@@ -21,18 +21,23 @@ CREATE TABLE IF NOT EXISTS users(
|
||||
password VARCHAR(1024) NOT NULL,
|
||||
salt VARCHAR(512) NOT NULL,
|
||||
createdAt VARCHAR(512) NOT NULL,
|
||||
modifiedAt VARCHAR(512) NOT NULL,
|
||||
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
displayName VARCHAR(512) DEFAULT "",
|
||||
fallbackEmail VARCHAR(512) DEFAULT "",
|
||||
twoFactorAuthenticationSecret VARCHAR(128) DEFAULT "",
|
||||
twoFactorAuthenticationEnabled BOOLEAN DEFAULT false,
|
||||
admin BOOLEAN DEFAULT false,
|
||||
source VARCHAR(128) DEFAULT "",
|
||||
role VARCHAR(32),
|
||||
resetToken VARCHAR(128) DEFAULT "",
|
||||
resetTokenCreationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
active BOOLEAN DEFAULT 1,
|
||||
|
||||
PRIMARY KEY(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS userGroups(
|
||||
id VARCHAR(128) NOT NULL UNIQUE,
|
||||
name VARCHAR(254) NOT NULL UNIQUE,
|
||||
source VARCHAR(128) DEFAULT "",
|
||||
PRIMARY KEY(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS groupMembers(
|
||||
@@ -51,51 +56,39 @@ CREATE TABLE IF NOT EXISTS tokens(
|
||||
expires BIGINT NOT NULL, // FIXME: make this a timestamp
|
||||
PRIMARY KEY(accessToken));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS clients(
|
||||
id VARCHAR(128) NOT NULL UNIQUE, // prefixed with cid- to identify token easily in auth routes
|
||||
appId VARCHAR(128) NOT NULL, // name of the client (for external apps) or id of app (for built-in apps)
|
||||
type VARCHAR(16) NOT NULL,
|
||||
clientSecret VARCHAR(512) NOT NULL,
|
||||
redirectURI VARCHAR(512) NOT NULL,
|
||||
scope VARCHAR(512) NOT NULL,
|
||||
PRIMARY KEY(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS apps(
|
||||
id VARCHAR(128) NOT NULL UNIQUE,
|
||||
appStoreId VARCHAR(128) NOT NULL,
|
||||
installationState VARCHAR(512) NOT NULL,
|
||||
installationProgress TEXT,
|
||||
runState VARCHAR(512),
|
||||
appStoreId VARCHAR(128) NOT NULL, // empty for custom apps
|
||||
installationState VARCHAR(512) NOT NULL, // the active task on the app
|
||||
runState VARCHAR(512) NOT NULL, // if the app is stopped
|
||||
health VARCHAR(128),
|
||||
healthTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app last responded
|
||||
containerId VARCHAR(128),
|
||||
manifestJson TEXT,
|
||||
httpPort INTEGER, // this is the nginx proxy port and not manifest.httpPort
|
||||
location VARCHAR(128) NOT NULL,
|
||||
domain VARCHAR(128) NOT NULL,
|
||||
accessRestrictionJson TEXT, // { users: [ ], groups: [ ] }
|
||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app was installed
|
||||
updateTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the last app update was done
|
||||
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, // when this db record was updated (useful for UI caching)
|
||||
memoryLimit BIGINT DEFAULT 0,
|
||||
cpuShares INTEGER DEFAULT 512,
|
||||
xFrameOptions VARCHAR(512),
|
||||
sso BOOLEAN DEFAULT 1, // whether user chose to enable SSO
|
||||
debugModeJson TEXT, // options for development mode
|
||||
robotsTxt TEXT,
|
||||
reverseProxyConfigJson TEXT, // { robotsTxt, csp }
|
||||
enableBackup BOOLEAN DEFAULT 1, // misnomer: controls automatic daily backups
|
||||
enableAutomaticUpdate BOOLEAN DEFAULT 1,
|
||||
mailboxName VARCHAR(128), // mailbox of this app. default allocated as '.app'
|
||||
mailboxName VARCHAR(128), // mailbox of this app
|
||||
mailboxDomain VARCHAR(128), // mailbox domain of this apps
|
||||
label VARCHAR(128), // display name
|
||||
tagsJson VARCHAR(2048), // array of tags
|
||||
dataDir VARCHAR(256) UNIQUE,
|
||||
taskId INTEGER, // current task
|
||||
errorJson TEXT,
|
||||
servicesConfigJson TEXT, // app services configuration
|
||||
containerIp VARCHAR(16) UNIQUE, // this is not-null because of ip allocation fails, user can 'repair'
|
||||
|
||||
// the following fields do not belong here, they can be removed when we use a queue for apptask
|
||||
restoreConfigJson VARCHAR(256), // used to pass backupId to restore from to apptask
|
||||
oldConfigJson TEXT, // used to pass old config to apptask (configure, restore)
|
||||
updateConfigJson TEXT, // used to pass new config to apptask (update)
|
||||
|
||||
ownerId VARCHAR(128),
|
||||
|
||||
FOREIGN KEY(ownerId) REFERENCES users(id),
|
||||
FOREIGN KEY(mailboxDomain) REFERENCES domains(domain),
|
||||
FOREIGN KEY(taskId) REFERENCES tasks(id),
|
||||
PRIMARY KEY(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS appPortBindings(
|
||||
@@ -106,13 +99,6 @@ CREATE TABLE IF NOT EXISTS appPortBindings(
|
||||
FOREIGN KEY(appId) REFERENCES apps(id),
|
||||
PRIMARY KEY(hostPort));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS authcodes(
|
||||
authCode VARCHAR(128) NOT NULL UNIQUE,
|
||||
userId VARCHAR(128) NOT NULL,
|
||||
clientId VARCHAR(128) NOT NULL,
|
||||
expiresAt BIGINT NOT NULL, // ## FIXME: make this a timestamp
|
||||
PRIMARY KEY(authCode));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings(
|
||||
name VARCHAR(128) NOT NULL UNIQUE,
|
||||
value TEXT,
|
||||
@@ -134,8 +120,10 @@ CREATE TABLE IF NOT EXISTS appEnvVars(
|
||||
CREATE TABLE IF NOT EXISTS backups(
|
||||
id VARCHAR(128) NOT NULL,
|
||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
version VARCHAR(128) NOT NULL, /* app version or box version */
|
||||
packageVersion VARCHAR(128) NOT NULL, /* app version or box version */
|
||||
encryptionVersion INTEGER, /* when null, unencrypted backup */
|
||||
type VARCHAR(16) NOT NULL, /* 'box' or 'app' */
|
||||
identifier VARCHAR(128) NOT NULL, /* 'box' or the app id */
|
||||
dependsOn TEXT, /* comma separate list of objects this backup depends on */
|
||||
state VARCHAR(16) NOT NULL,
|
||||
manifestJson TEXT, /* to validate if the app can be installed in this version of box */
|
||||
@@ -159,7 +147,6 @@ CREATE TABLE IF NOT EXISTS domains(
|
||||
provider VARCHAR(16) NOT NULL,
|
||||
configJson TEXT, /* JSON containing the dns backend provider config */
|
||||
tlsConfigJson TEXT, /* JSON containing the tls provider config */
|
||||
locked BOOLEAN,
|
||||
|
||||
PRIMARY KEY (domain))
|
||||
|
||||
@@ -173,6 +160,7 @@ CREATE TABLE IF NOT EXISTS mail(
|
||||
mailFromValidation BOOLEAN DEFAULT 1,
|
||||
catchAllJson TEXT,
|
||||
relayJson TEXT,
|
||||
bannerJson TEXT,
|
||||
|
||||
dkimSelector VARCHAR(128) NOT NULL DEFAULT "cloudron",
|
||||
|
||||
@@ -192,19 +180,23 @@ CREATE TABLE IF NOT EXISTS mailboxes(
|
||||
name VARCHAR(128) NOT NULL,
|
||||
type VARCHAR(16) NOT NULL, /* 'mailbox', 'alias', 'list' */
|
||||
ownerId VARCHAR(128) NOT NULL, /* user id */
|
||||
aliasTarget VARCHAR(128), /* the target name type is an alias */
|
||||
membersJson TEXT, /* members of a group */
|
||||
ownerType VARCHAR(16) NOT NULL,
|
||||
aliasName VARCHAR(128), /* the target name type is an alias */
|
||||
aliasDomain VARCHAR(128), /* the target domain */
|
||||
membersJson TEXT, /* members of a group. fully qualified */
|
||||
membersOnly BOOLEAN DEFAULT false,
|
||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
domain VARCHAR(128),
|
||||
|
||||
FOREIGN KEY(domain) REFERENCES mail(domain),
|
||||
FOREIGN KEY(aliasDomain) 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,
|
||||
type VARCHAR(128) NOT NULL, /* primary or redirect */
|
||||
|
||||
FOREIGN KEY(domain) REFERENCES domains(domain),
|
||||
FOREIGN KEY(appId) REFERENCES apps(id),
|
||||
@@ -215,8 +207,8 @@ CREATE TABLE IF NOT EXISTS tasks(
|
||||
type VARCHAR(32) NOT NULL,
|
||||
percent INTEGER DEFAULT 0,
|
||||
message TEXT,
|
||||
errorMessage TEXT,
|
||||
result TEXT,
|
||||
errorJson TEXT,
|
||||
resultJson TEXT,
|
||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id));
|
||||
@@ -229,8 +221,36 @@ CREATE TABLE IF NOT EXISTS notifications(
|
||||
message TEXT,
|
||||
acknowledged BOOLEAN DEFAULT false,
|
||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY appPasswords_name_appId_identifier (name, userId, identifier),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS appPasswords(
|
||||
id VARCHAR(128) NOT NULL UNIQUE,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
userId VARCHAR(128) NOT NULL,
|
||||
identifier VARCHAR(128) NOT NULL, // resourceId: app id or mail or webadmin
|
||||
hashedPassword VARCHAR(1024) NOT NULL,
|
||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY(userId) REFERENCES users(id),
|
||||
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS volumes(
|
||||
id VARCHAR(128) NOT NULL UNIQUE,
|
||||
name VARCHAR(256) NOT NULL UNIQUE,
|
||||
hostPath VARCHAR(1024) NOT NULL UNIQUE,
|
||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS appMounts(
|
||||
appId VARCHAR(128) NOT NULL,
|
||||
volumeId VARCHAR(128) NOT NULL,
|
||||
readOnly BOOLEAN DEFAULT 1,
|
||||
UNIQUE KEY appMounts_appId_volumeId (appId, volumeId),
|
||||
FOREIGN KEY(appId) REFERENCES apps(id),
|
||||
FOREIGN KEY(volumeId) REFERENCES volumes(id));
|
||||
|
||||
CHARACTER SET utf8 COLLATE utf8_bin;
|
||||
|
||||
Generated
+1759
-1690
File diff suppressed because it is too large
Load Diff
+48
-53
@@ -14,81 +14,76 @@
|
||||
"node": ">=4.0.0 <=4.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google-cloud/dns": "^0.9.2",
|
||||
"@google-cloud/dns": "^1.2.9",
|
||||
"@google-cloud/storage": "^2.5.0",
|
||||
"@sindresorhus/df": "^3.1.0",
|
||||
"async": "^2.6.2",
|
||||
"aws-sdk": "^2.441.0",
|
||||
"body-parser": "^1.18.3",
|
||||
"cloudron-manifestformat": "^2.15.0",
|
||||
"connect": "^3.6.6",
|
||||
"connect-ensure-login": "^0.1.1",
|
||||
"connect-lastmile": "^1.0.2",
|
||||
"@sindresorhus/df": "git+https://github.com/cloudron-io/df.git#type",
|
||||
"async": "^2.6.3",
|
||||
"aws-sdk": "^2.759.0",
|
||||
"basic-auth": "^2.0.1",
|
||||
"body-parser": "^1.19.0",
|
||||
"cloudron-manifestformat": "^5.9.0",
|
||||
"connect": "^3.7.0",
|
||||
"connect-lastmile": "^2.0.0",
|
||||
"connect-timeout": "^1.9.0",
|
||||
"cookie-parser": "^1.4.4",
|
||||
"cookie-session": "^1.3.3",
|
||||
"cron": "^1.7.0",
|
||||
"csurf": "^1.9.0",
|
||||
"db-migrate": "^0.11.5",
|
||||
"db-migrate-mysql": "^1.1.10",
|
||||
"debug": "^4.1.1",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"cookie-session": "^1.4.0",
|
||||
"cron": "^1.8.2",
|
||||
"db-migrate": "^0.11.11",
|
||||
"db-migrate-mysql": "^2.1.1",
|
||||
"debug": "^4.2.0",
|
||||
"dockerode": "^2.5.8",
|
||||
"ejs": "^2.6.1",
|
||||
"ejs-cli": "^2.0.1",
|
||||
"express": "^4.16.4",
|
||||
"express-session": "^1.16.1",
|
||||
"js-yaml": "^3.13.1",
|
||||
"ejs-cli": "^2.2.1",
|
||||
"express": "^4.17.1",
|
||||
"ipaddr.js": "^2.0.0",
|
||||
"js-yaml": "^3.14.0",
|
||||
"json": "^9.0.6",
|
||||
"ldapjs": "^1.0.2",
|
||||
"lodash": "^4.17.11",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"ldapjs": "^2.2.0",
|
||||
"lodash": "^4.17.20",
|
||||
"lodash.chunk": "^4.2.0",
|
||||
"mime": "^2.4.2",
|
||||
"moment-timezone": "^0.5.25",
|
||||
"morgan": "^1.9.1",
|
||||
"multiparty": "^4.2.1",
|
||||
"mysql": "^2.17.1",
|
||||
"nodemailer": "^6.1.1",
|
||||
"mime": "^2.4.6",
|
||||
"moment": "^2.29.0",
|
||||
"moment-timezone": "^0.5.31",
|
||||
"morgan": "^1.10.0",
|
||||
"multiparty": "^4.2.2",
|
||||
"mustache-express": "^1.3.0",
|
||||
"mysql": "^2.18.1",
|
||||
"nodemailer": "^6.4.11",
|
||||
"nodemailer-smtp-transport": "^2.7.4",
|
||||
"oauth2orize": "^1.11.0",
|
||||
"once": "^1.4.0",
|
||||
"parse-links": "^0.1.0",
|
||||
"passport": "^0.4.0",
|
||||
"passport-http": "^0.3.0",
|
||||
"passport-http-bearer": "^1.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"passport-oauth2-client-password": "^0.1.2",
|
||||
"pretty-bytes": "^5.4.1",
|
||||
"progress-stream": "^2.0.0",
|
||||
"proxy-middleware": "^0.15.0",
|
||||
"qrcode": "^1.3.3",
|
||||
"readdirp": "^3.0.0",
|
||||
"request": "^2.88.0",
|
||||
"qrcode": "^1.4.4",
|
||||
"readdirp": "^3.4.0",
|
||||
"request": "^2.88.2",
|
||||
"rimraf": "^2.6.3",
|
||||
"s3-block-read-stream": "^0.5.0",
|
||||
"safetydance": "^0.7.1",
|
||||
"semver": "^6.0.0",
|
||||
"showdown": "^1.9.0",
|
||||
"safetydance": "^1.1.1",
|
||||
"semver": "^6.1.1",
|
||||
"showdown": "^1.9.1",
|
||||
"speakeasy": "^2.0.0",
|
||||
"split": "^1.0.1",
|
||||
"superagent": "^5.0.2",
|
||||
"superagent": "^5.3.1",
|
||||
"supererror": "^0.7.2",
|
||||
"tar-fs": "github:cloudron-io/tar-fs#ignore_stat_error",
|
||||
"tar-stream": "^2.0.1",
|
||||
"tar-stream": "^2.1.4",
|
||||
"tldjs": "^2.3.1",
|
||||
"underscore": "^1.9.1",
|
||||
"uuid": "^3.3.2",
|
||||
"valid-url": "^1.0.9",
|
||||
"validator": "^10.11.0",
|
||||
"ws": "^6.2.1",
|
||||
"xml2js": "^0.4.19"
|
||||
"underscore": "^1.11.0",
|
||||
"uuid": "^3.4.0",
|
||||
"validator": "^11.0.0",
|
||||
"ws": "^7.3.1",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
"expect.js": "*",
|
||||
"hock": "^1.3.3",
|
||||
"js2xmlparser": "^4.0.0",
|
||||
"mocha": "^6.1.4",
|
||||
"hock": "^1.4.1",
|
||||
"js2xmlparser": "^4.0.1",
|
||||
"mocha": "^6.2.3",
|
||||
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
|
||||
"nock": "^10.0.6",
|
||||
"node-sass": "^4.11.0",
|
||||
"node-sass": "^4.14.1",
|
||||
"recursive-readdir": "^2.2.2"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
set -eu
|
||||
|
||||
readonly SOURCE_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly source_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly DATA_DIR="${HOME}/.cloudron_test"
|
||||
readonly DEFAULT_TESTS="./src/test/*-test.js ./src/routes/test/*-test.js"
|
||||
|
||||
! "${SOURCE_dir}/src/test/checkInstall" && exit 1
|
||||
! "${source_dir}/src/test/checkInstall" && exit 1
|
||||
|
||||
# cleanup old data dirs some of those docker container data requires sudo to be removed
|
||||
echo "=> Provide root password to purge any leftover data in ${DATA_DIR} and load apparmor profile:"
|
||||
@@ -22,19 +22,26 @@ fi
|
||||
mkdir -p ${DATA_DIR}
|
||||
cd ${DATA_DIR}
|
||||
mkdir -p appsdata
|
||||
mkdir -p boxdata/appicons boxdata/mail boxdata/certs boxdata/mail/dkim/localhost boxdata/mail/dkim/foobar.com
|
||||
mkdir -p platformdata/addons/mail platformdata/nginx/cert platformdata/nginx/applications platformdata/collectd/collectd.conf.d platformdata/addons platformdata/logrotate.d platformdata/backup platformdata/logs/tasks
|
||||
mkdir -p boxdata/profileicons boxdata/appicons boxdata/mail boxdata/certs boxdata/mail/dkim/localhost boxdata/mail/dkim/foobar.com boxdata/sftp/ssh
|
||||
mkdir -p platformdata/addons/mail/banner platformdata/nginx/cert platformdata/nginx/applications platformdata/collectd/collectd.conf.d platformdata/addons platformdata/logrotate.d platformdata/backup platformdata/logs/tasks
|
||||
|
||||
# translations
|
||||
mkdir -p box/dashboard/dist/translation
|
||||
cp -r ${source_dir}/../dashboard/dist/translation/* box/dashboard/dist/translation
|
||||
|
||||
# put cert
|
||||
echo "=> Generating a localhost selfsigned cert"
|
||||
openssl req -x509 -newkey rsa:2048 -keyout platformdata/nginx/cert/host.key -out platformdata/nginx/cert/host.cert -days 3650 -subj '/CN=localhost' -nodes -config <(cat /etc/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.localhost"))
|
||||
|
||||
# generate legacy key format for sftp
|
||||
ssh-keygen -m PEM -t rsa -f boxdata/sftp/ssh/ssh_host_rsa_key -q -N ""
|
||||
|
||||
# clear out any containers
|
||||
echo "=> Delete all docker containers first"
|
||||
docker ps -qa | xargs --no-run-if-empty docker rm -f
|
||||
|
||||
# create docker network (while the infra code does this, most tests skip infra setup)
|
||||
docker network create --subnet=172.18.0.0/16 cloudron || true
|
||||
docker network create --subnet=172.18.0.0/16 --ip-range=172.18.0.0/20 cloudron || true
|
||||
|
||||
# create the same mysql server version to test with
|
||||
OUT=`docker inspect mysql-server` || true
|
||||
@@ -59,7 +66,7 @@ echo "=> Ensure database"
|
||||
mysql -h"${MYSQL_IP}" -uroot -ppassword -e 'CREATE DATABASE IF NOT EXISTS box'
|
||||
|
||||
echo "=> Run database migrations"
|
||||
cd "${SOURCE_dir}"
|
||||
cd "${source_dir}"
|
||||
BOX_ENV=test DATABASE_URL=mysql://root:password@${MYSQL_IP}/box node_modules/.bin/db-migrate up
|
||||
|
||||
echo "=> Run tests with mocha"
|
||||
|
||||
+18
-63
@@ -41,21 +41,19 @@ if systemctl -q is-active box; then
|
||||
fi
|
||||
|
||||
initBaseImage="true"
|
||||
# provisioning data
|
||||
provider=""
|
||||
provider="generic"
|
||||
requestedVersion=""
|
||||
apiServerOrigin="https://api.cloudron.io"
|
||||
webServerOrigin="https://cloudron.io"
|
||||
sourceTarballUrl=""
|
||||
rebootServer="true"
|
||||
license=""
|
||||
|
||||
args=$(getopt -o "" -l "help,skip-baseimage-init,provider:,version:,env:,skip-reboot,license:" -n "$0" -- "$@")
|
||||
args=$(getopt -o "" -l "help,skip-baseimage-init,provider:,version:,env:,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;;
|
||||
--help) echo "See https://docs.cloudron.io/installation/ on how to install Cloudron"; exit 0;;
|
||||
--provider) provider="$2"; shift 2;;
|
||||
--version) requestedVersion="$2"; shift 2;;
|
||||
--env)
|
||||
@@ -67,7 +65,6 @@ while true; do
|
||||
webServerOrigin="https://staging.cloudron.io"
|
||||
fi
|
||||
shift 2;;
|
||||
--license) license="$2"; shift 2;;
|
||||
--skip-baseimage-init) initBaseImage="false"; shift;;
|
||||
--skip-reboot) rebootServer="false"; shift;;
|
||||
--) break;;
|
||||
@@ -83,49 +80,14 @@ fi
|
||||
|
||||
# Only --help works with mismatched ubuntu
|
||||
ubuntu_version=$(lsb_release -rs)
|
||||
if [[ "${ubuntu_version}" != "16.04" && "${ubuntu_version}" != "18.04" ]]; then
|
||||
echo "Cloudron requires Ubuntu 16.04 or 18.04" > /dev/stderr
|
||||
if [[ "${ubuntu_version}" != "16.04" && "${ubuntu_version}" != "18.04" && "${ubuntu_version}" != "20.04" ]]; then
|
||||
echo "Cloudron requires Ubuntu 16.04, 18.04 or 20.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, contabo, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, lightsail, linode, netcup, ovh, rosehosting, scaleway, upcloud, vultr or generic)"
|
||||
exit 1
|
||||
elif [[ \
|
||||
"${provider}" != "ami" && \
|
||||
"${provider}" != "azure" && \
|
||||
"${provider}" != "caas" && \
|
||||
"${provider}" != "cloudscale" && \
|
||||
"${provider}" != "contabo" && \
|
||||
"${provider}" != "digitalocean" && \
|
||||
"${provider}" != "digitalocean-mp" && \
|
||||
"${provider}" != "ec2" && \
|
||||
"${provider}" != "exoscale" && \
|
||||
"${provider}" != "galaxygate" && \
|
||||
"${provider}" != "digitalocean" && \
|
||||
"${provider}" != "gce" && \
|
||||
"${provider}" != "hetzner" && \
|
||||
"${provider}" != "lightsail" && \
|
||||
"${provider}" != "linode" && \
|
||||
"${provider}" != "linode-stackscript" && \
|
||||
"${provider}" != "netcup" && \
|
||||
"${provider}" != "netcup-image" && \
|
||||
"${provider}" != "ovh" && \
|
||||
"${provider}" != "rosehosting" && \
|
||||
"${provider}" != "scaleway" && \
|
||||
"${provider}" != "upcloud" && \
|
||||
"${provider}" != "upcloud-image" && \
|
||||
"${provider}" != "vultr" && \
|
||||
"${provider}" != "generic" \
|
||||
]]; then
|
||||
echo "--provider must be one of: azure, cloudscale.ch, contabo, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, lightsail, linode, netcup, ovh, rosehosting, scaleway, upcloud, vultr or generic"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "##############################################"
|
||||
echo " Cloudron Setup (${requestedVersion:-latest})"
|
||||
@@ -144,19 +106,13 @@ if [[ "${initBaseImage}" == "true" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=> Ensure required apt sources"
|
||||
if ! add-apt-repository universe &>> "${LOG_FILE}"; then
|
||||
echo "Could not add required apt sources (for nginx-full). See ${LOG_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=> Updating apt and installing script dependencies"
|
||||
if ! apt-get update &>> "${LOG_FILE}"; then
|
||||
echo "Could not update package repositories. See ${LOG_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! apt-get install curl python3 ubuntu-standard -y &>> "${LOG_FILE}"; then
|
||||
if ! DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" -y install curl python3 ubuntu-standard -y &>> "${LOG_FILE}"; then
|
||||
echo "Could not install setup dependencies (curl). See ${LOG_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
@@ -189,41 +145,40 @@ fi
|
||||
|
||||
if [[ "${initBaseImage}" == "true" ]]; then
|
||||
echo -n "=> Installing base dependencies and downloading docker images (this takes some time) ..."
|
||||
if ! /bin/bash "${box_src_tmp_dir}/baseimage/initializeBaseUbuntuImage.sh" "${provider}" "../src" &>> "${LOG_FILE}"; then
|
||||
# initializeBaseUbuntuImage.sh args (provider, infraversion path) are only to support installation of pre 5.3 Cloudrons
|
||||
if ! /bin/bash "${box_src_tmp_dir}/baseimage/initializeBaseUbuntuImage.sh" "generic" "../src" &>> "${LOG_FILE}"; then
|
||||
echo "Init script failed. See ${LOG_FILE} for details"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# NOTE: this install script only supports 3.x and above
|
||||
# The provider flag is still used for marketplace images
|
||||
echo "=> Installing version ${version} (this takes some time) ..."
|
||||
mkdir -p /etc/cloudron
|
||||
cat > "/etc/cloudron/cloudron.conf" <<CONF_END
|
||||
{
|
||||
"apiServerOrigin": "${apiServerOrigin}",
|
||||
"webServerOrigin": "${webServerOrigin}",
|
||||
"provider": "${provider}"
|
||||
}
|
||||
CONF_END
|
||||
|
||||
[[ -n "${license}" ]] && echo -n "$license" > /etc/cloudron/LICENSE
|
||||
echo "${provider}" > /etc/cloudron/PROVIDER
|
||||
|
||||
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
|
||||
|
||||
mysql -uroot -ppassword -e "REPLACE INTO box.settings (name, value) VALUES ('api_server_origin', '${apiServerOrigin}');" 2>/dev/null
|
||||
mysql -uroot -ppassword -e "REPLACE INTO box.settings (name, value) VALUES ('web_server_origin', '${webServerOrigin}');" 2>/dev/null
|
||||
|
||||
echo -n "=> Waiting for cloudron to be ready (this takes some time) ..."
|
||||
while true; do
|
||||
echo -n "."
|
||||
if status=$($curl -q -f "http://localhost:3000/api/v1/cloudron/status" 2>/dev/null); then
|
||||
if status=$($curl -s -f "http://localhost:3000/api/v1/cloudron/status" 2>/dev/null); then
|
||||
break # we are up and running
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
|
||||
echo -e "\n\n${GREEN}Visit https://<IP> and accept the self-signed certificate to finish setup.${DONE}\n"
|
||||
if ! ip=$(curl -s --fail --connect-timeout 2 --max-time 2 https://api.cloudron.io/api/v1/helper/public_ip | sed -n -e 's/.*"ip": "\(.*\)"/\1/p'); then
|
||||
ip='<IP>'
|
||||
fi
|
||||
echo -e "\n\n${GREEN}After reboot, visit https://${ip} and accept the self-signed certificate to finish setup.${DONE}\n"
|
||||
|
||||
if [[ "${rebootServer}" == "true" ]]; then
|
||||
systemctl stop box mysql # sometimes mysql ends up having corrupt privilege tables
|
||||
|
||||
@@ -13,6 +13,7 @@ HELP_MESSAGE="
|
||||
This script collects diagnostic information to help debug server related issues
|
||||
|
||||
Options:
|
||||
--owner-login Login as owner
|
||||
--enable-ssh Enable SSH access for the Cloudron support team
|
||||
--help Show this message
|
||||
"
|
||||
@@ -25,13 +26,25 @@ fi
|
||||
|
||||
enableSSH="false"
|
||||
|
||||
args=$(getopt -o "" -l "help,enable-ssh" -n "$0" -- "$@")
|
||||
args=$(getopt -o "" -l "help,enable-ssh,admin-login,owner-login" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--help) echo -e "${HELP_MESSAGE}"; exit 0;;
|
||||
--enable-ssh) enableSSH="true"; shift;;
|
||||
--admin-login)
|
||||
# fall through
|
||||
;&
|
||||
--owner-login)
|
||||
admin_username=$(mysql -NB -uroot -ppassword -e "SELECT username FROM box.users WHERE role='owner' AND username IS NOT NULL ORDER BY createdAt LIMIT 1" 2>/dev/null)
|
||||
admin_password=$(pwgen -1s 12)
|
||||
ghost_file=/home/yellowtent/platformdata/cloudron_ghost.json
|
||||
printf '{"%s":"%s"}\n' "${admin_username}" "${admin_password}" > "${ghost_file}"
|
||||
chown yellowtent:yellowtent "${ghost_file}" && chmod o-r,g-r "${ghost_file}"
|
||||
echo "Login as ${admin_username} / ${admin_password} . This password may only be used once. ${ghost_file} will be automatically removed after use."
|
||||
exit 0
|
||||
;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
@@ -44,7 +57,7 @@ if [[ "`df --output="avail" / | sed -n 2p`" -lt "10240" ]]; then
|
||||
echo ""
|
||||
df -h
|
||||
echo ""
|
||||
echo "To recover from a full disk, follow the guide at https://cloudron.io/documentation/server/#recovery-after-disk-full"
|
||||
echo "To recover from a full disk, follow the guide at https://docs.cloudron.io/troubleshooting/#recovery-after-disk-full"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -60,8 +73,8 @@ echo -n "Generating Cloudron Support stats..."
|
||||
# clear file
|
||||
rm -rf $OUT
|
||||
|
||||
echo -e $LINE"cloudron.conf"$LINE >> $OUT
|
||||
cat /etc/cloudron/cloudron.conf &>> $OUT
|
||||
echo -e $LINE"PROVIDER"$LINE >> $OUT
|
||||
cat /etc/cloudron/PROVIDER &>> $OUT || true
|
||||
|
||||
echo -e $LINE"Docker container"$LINE >> $OUT
|
||||
if ! timeout --kill-after 10s 15s docker ps -a &>> $OUT 2>&1; then
|
||||
@@ -72,16 +85,16 @@ echo -e $LINE"Filesystem stats"$LINE >> $OUT
|
||||
df -h &>> $OUT
|
||||
|
||||
echo -e $LINE"Appsdata stats"$LINE >> $OUT
|
||||
du -hcsL /home/yellowtent/appsdata/* &>> $OUT
|
||||
du -hcsL /home/yellowtent/appsdata/* &>> $OUT || true
|
||||
|
||||
echo -e $LINE"Boxdata stats"$LINE >> $OUT
|
||||
du -hcsL /home/yellowtent/boxdata/* &>> $OUT
|
||||
|
||||
echo -e $LINE"Backup stats (possibly misleading)"$LINE >> $OUT
|
||||
du -hcsL /var/backups/* &>> $OUT
|
||||
du -hcsL /var/backups/* &>> $OUT || true
|
||||
|
||||
echo -e $LINE"System daemon status"$LINE >> $OUT
|
||||
systemctl status --lines=100 cloudron.target box mysql unbound cloudron-syslog nginx collectd docker &>> $OUT
|
||||
systemctl status --lines=100 box mysql unbound cloudron-syslog nginx collectd docker &>> $OUT
|
||||
|
||||
echo -e $LINE"Box logs"$LINE >> $OUT
|
||||
tail -n 100 /home/yellowtent/platformdata/logs/box.log &>> $OUT
|
||||
@@ -99,7 +112,7 @@ if [[ "${enableSSH}" == "true" ]]; then
|
||||
permit_root_login=$(grep -q ^PermitRootLogin.*yes /etc/ssh/sshd_config && echo "yes" || echo "no")
|
||||
|
||||
# support.js uses similar logic
|
||||
if $(grep -q "ec2\|lightsail\|ami" /etc/cloudron/cloudron.conf); then
|
||||
if [[ -d /home/ubuntu ]]; then
|
||||
ssh_user="ubuntu"
|
||||
keys_file="/home/ubuntu/.ssh/authorized_keys"
|
||||
else
|
||||
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
# This script downloads new translation data from weblate at https://translate.cloudron.io
|
||||
|
||||
OUT="/home/yellowtent/box/dashboard/dist/translation"
|
||||
|
||||
# We require root
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root. Run with sudo"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=> Downloading new translation files..."
|
||||
curl https://translate.cloudron.io/download/cloudron/dashboard/?format=zip -o /tmp/lang.zip
|
||||
|
||||
echo "=> Unpacking..."
|
||||
unzip -jo /tmp/lang.zip -d $OUT
|
||||
chown -R yellowtent:yellowtent $OUT
|
||||
# unzip put very restrictive permissions
|
||||
chmod ua+r $OUT/*
|
||||
|
||||
echo "=> Cleanup..."
|
||||
rm /tmp/lang.zip
|
||||
|
||||
echo "=> Done"
|
||||
|
||||
echo ""
|
||||
echo "Reload the dashboard to see the new translations"
|
||||
echo ""
|
||||
@@ -41,8 +41,8 @@ if ! $(cd "${SOURCE_DIR}/../dashboard" && git diff --exit-code >/dev/null); then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$(node --version)" != "v10.15.1" ]]; then
|
||||
echo "This script requires node 10.15.1"
|
||||
if [[ "$(node --version)" != "v10.18.1" ]]; then
|
||||
echo "This script requires node 10.18.1"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
+42
-21
@@ -11,9 +11,8 @@ if [[ ${EUID} -ne 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
readonly USER=yellowtent
|
||||
readonly BOX_SRC_DIR=/home/${USER}/box
|
||||
readonly BASE_DATA_DIR=/home/${USER}
|
||||
readonly user=yellowtent
|
||||
readonly box_src_dir=/home/${user}/box
|
||||
|
||||
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
|
||||
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
@@ -24,13 +23,15 @@ readonly ubuntu_codename=$(lsb_release -cs)
|
||||
|
||||
readonly is_update=$(systemctl is-active box && echo "yes" || echo "no")
|
||||
|
||||
echo "==> installer: Updating from $(cat $box_src_dir/VERSION) to $(cat $box_src_tmp_dir/VERSION) <=="
|
||||
|
||||
echo "==> installer: updating docker"
|
||||
|
||||
if [[ $(docker version --format {{.Client.Version}}) != "18.09.2" ]]; then
|
||||
if [[ $(docker version --format {{.Client.Version}}) != "19.03.12" ]]; then
|
||||
# there are 3 packages for docker - containerd, CLI and the daemon
|
||||
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_1.2.2-3_amd64.deb" -o /tmp/containerd.deb
|
||||
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce-cli_18.09.2~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker-ce-cli.deb
|
||||
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce_18.09.2~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker.deb
|
||||
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_1.2.13-2_amd64.deb" -o /tmp/containerd.deb
|
||||
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce-cli_19.03.12~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker-ce-cli.deb
|
||||
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce_19.03.12~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker.deb
|
||||
|
||||
echo "==> installer: Waiting for all dpkg tasks to finish..."
|
||||
while fuser /var/lib/dpkg/lock; do
|
||||
@@ -56,13 +57,33 @@ if [[ $(docker version --format {{.Client.Version}}) != "18.09.2" ]]; then
|
||||
rm /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb
|
||||
fi
|
||||
|
||||
readonly nginx_version=$(nginx -v 2>&1)
|
||||
if [[ "${nginx_version}" != *"1.18."* ]]; then
|
||||
echo "==> installer: installing nginx 1.18"
|
||||
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.18.0-2~${ubuntu_codename}_amd64.deb -o /tmp/nginx.deb
|
||||
# apt install with install deps (as opposed to dpkg -i)
|
||||
apt install -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" --force-yes /tmp/nginx.deb
|
||||
rm /tmp/nginx.deb
|
||||
fi
|
||||
|
||||
if ! which ipset; then
|
||||
echo "==> installer: installing ipset"
|
||||
apt install -y ipset
|
||||
fi
|
||||
|
||||
# Only used for the cloudron-translation-update script
|
||||
if ! which unzip; then
|
||||
echo "==> installer: installing unzip"
|
||||
apt install -y unzip
|
||||
fi
|
||||
|
||||
echo "==> installer: updating node"
|
||||
if [[ "$(node --version)" != "v10.15.1" ]]; then
|
||||
mkdir -p /usr/local/node-10.15.1
|
||||
$curl -sL https://nodejs.org/dist/v10.15.1/node-v10.15.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-10.15.1
|
||||
ln -sf /usr/local/node-10.15.1/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-10.15.1/bin/npm /usr/bin/npm
|
||||
rm -rf /usr/local/node-8.11.2 /usr/local/node-8.9.3
|
||||
if [[ "$(node --version)" != "v10.18.1" ]]; then
|
||||
mkdir -p /usr/local/node-10.18.1
|
||||
$curl -sL https://nodejs.org/dist/v10.18.1/node-v10.18.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-10.18.1
|
||||
ln -sf /usr/local/node-10.18.1/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-10.18.1/bin/npm /usr/bin/npm
|
||||
rm -rf /usr/local/node-10.15.1
|
||||
fi
|
||||
|
||||
# this is here (and not in updater.js) because rebuild requires the above node
|
||||
@@ -109,22 +130,22 @@ while [[ ! -f "${CLOUDRON_SYSLOG}" || "$(${CLOUDRON_SYSLOG} --version)" != ${CLO
|
||||
sleep 5
|
||||
done
|
||||
|
||||
if ! id "${USER}" 2>/dev/null; then
|
||||
useradd "${USER}" -m
|
||||
if ! id "${user}" 2>/dev/null; then
|
||||
useradd "${user}" -m
|
||||
fi
|
||||
|
||||
if [[ "${is_update}" == "yes" ]]; then
|
||||
echo "==> installer: stop cloudron.target service for update"
|
||||
${BOX_SRC_DIR}/setup/stop.sh
|
||||
echo "==> installer: stop box service for update"
|
||||
${box_src_dir}/setup/stop.sh
|
||||
fi
|
||||
|
||||
# ensure we are not inside the source directory, which we will remove now
|
||||
cd /root
|
||||
|
||||
echo "==> installer: switching the box code"
|
||||
rm -rf "${BOX_SRC_DIR}"
|
||||
mv "${box_src_tmp_dir}" "${BOX_SRC_DIR}"
|
||||
chown -R "${USER}:${USER}" "${BOX_SRC_DIR}"
|
||||
rm -rf "${box_src_dir}"
|
||||
mv "${box_src_tmp_dir}" "${box_src_dir}"
|
||||
chown -R "${user}:${user}" "${box_src_dir}"
|
||||
|
||||
echo "==> installer: calling box setup script"
|
||||
"${BOX_SRC_DIR}/setup/start.sh"
|
||||
"${box_src_dir}/setup/start.sh"
|
||||
|
||||
+62
-13
@@ -19,6 +19,12 @@ readonly json="$(realpath ${script_dir}/../node_modules/.bin/json)"
|
||||
readonly ubuntu_version=$(lsb_release -rs)
|
||||
|
||||
cp -f "${script_dir}/../scripts/cloudron-support" /usr/bin/cloudron-support
|
||||
cp -f "${script_dir}/../scripts/cloudron-translation-update" /usr/bin/cloudron-translation-update
|
||||
|
||||
# this needs to match the cloudron/base:2.0.0 gid
|
||||
if ! getent group media; then
|
||||
addgroup --gid 500 --system media
|
||||
fi
|
||||
|
||||
echo "==> Configuring docker"
|
||||
cp "${script_dir}/start/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
|
||||
@@ -26,7 +32,8 @@ systemctl enable apparmor
|
||||
systemctl restart apparmor
|
||||
|
||||
usermod ${USER} -a -G docker
|
||||
docker network create --subnet=172.18.0.0/16 cloudron || true
|
||||
# unbound (which starts after box code) relies on this interface to exist. dockerproxy also relies on this.
|
||||
docker network create --subnet=172.18.0.0/16 --ip-range=172.18.0.0/20 cloudron || true
|
||||
|
||||
mkdir -p "${BOX_DATA_DIR}"
|
||||
mkdir -p "${APPS_DATA_DIR}"
|
||||
@@ -39,7 +46,7 @@ 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}/addons/mail/banner"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/collectd/collectd.conf.d"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/logrotate.d"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/acme"
|
||||
@@ -47,13 +54,18 @@ mkdir -p "${PLATFORM_DATA_DIR}/backup"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/logs/backup" \
|
||||
"${PLATFORM_DATA_DIR}/logs/updater" \
|
||||
"${PLATFORM_DATA_DIR}/logs/tasks" \
|
||||
"${PLATFORM_DATA_DIR}/logs/crash"
|
||||
"${PLATFORM_DATA_DIR}/logs/crash" \
|
||||
"${PLATFORM_DATA_DIR}/logs/collectd"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/update"
|
||||
|
||||
mkdir -p "${BOX_DATA_DIR}/appicons"
|
||||
mkdir -p "${BOX_DATA_DIR}/firewall"
|
||||
mkdir -p "${BOX_DATA_DIR}/profileicons"
|
||||
mkdir -p "${BOX_DATA_DIR}/certs"
|
||||
mkdir -p "${BOX_DATA_DIR}/acme" # acme keys
|
||||
mkdir -p "${BOX_DATA_DIR}/mail/dkim"
|
||||
mkdir -p "${BOX_DATA_DIR}/well-known" # .well-known documents
|
||||
mkdir -p "${BOX_DATA_DIR}/sftp/ssh" # sftp keys
|
||||
|
||||
# ensure backups folder exists and is writeable
|
||||
mkdir -p /var/backups
|
||||
@@ -77,13 +89,16 @@ systemctl daemon-reload
|
||||
systemctl restart systemd-journald
|
||||
setfacl -n -m u:${USER}:r /var/log/journal/*/system.journal
|
||||
|
||||
# Give user access to nginx logs (uses adm group)
|
||||
usermod -a -G adm ${USER}
|
||||
|
||||
echo "==> Setting up unbound"
|
||||
# DO uses Google nameservers by default. This causes RBL queries to fail (host 2.0.0.127.zen.spamhaus.org)
|
||||
# We do not use dnsmasq because it is not a recursive resolver and defaults to the value in the interfaces file (which is Google DNS!)
|
||||
# We listen on 0.0.0.0 because there is no way control ordering of docker (which creates the 172.18.0.0/16) and unbound
|
||||
# If IP6 is not enabled, dns queries seem to fail on some hosts. -s returns false if file missing or 0 size
|
||||
ip6=$([[ -s /proc/net/if_inet6 ]] && echo "yes" || echo "no")
|
||||
echo -e "server:\n\tinterface: 0.0.0.0\n\tdo-ip6: ${ip6}\n\taccess-control: 127.0.0.1 allow\n\taccess-control: 172.18.0.1/16 allow\n\tcache-max-negative-ttl: 30\n\tcache-max-ttl: 300\n\t#logfile: /var/log/unbound.log\n\t#verbosity: 10" > /etc/unbound/unbound.conf.d/cloudron-network.conf
|
||||
cp -f "${script_dir}/start/unbound.conf" /etc/unbound/unbound.conf.d/cloudron-network.conf
|
||||
# update the root anchor after a out-of-disk-space situation (see #269)
|
||||
unbound-anchor -a /var/lib/unbound/root.key
|
||||
|
||||
@@ -91,10 +106,11 @@ echo "==> Adding systemd services"
|
||||
cp -r "${script_dir}/start/systemd/." /etc/systemd/system/
|
||||
[[ "${ubuntu_version}" == "16.04" ]] && sed -e 's/MemoryMax/MemoryLimit/g' -i /etc/systemd/system/box.service
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now cloudron-syslog
|
||||
systemctl enable unbound
|
||||
systemctl enable cloudron-syslog
|
||||
systemctl enable cloudron.target
|
||||
systemctl enable box
|
||||
systemctl enable cloudron-firewall
|
||||
systemctl enable --now cloudron-disable-thp
|
||||
|
||||
# update firewall rules
|
||||
systemctl restart cloudron-firewall
|
||||
@@ -108,24 +124,28 @@ systemctl restart unbound
|
||||
# ensure cloudron-syslog runs
|
||||
systemctl restart cloudron-syslog
|
||||
|
||||
$json -f /etc/cloudron/cloudron.conf -I -e "delete this.edition" # can be removed after 4.0
|
||||
|
||||
echo "==> Configuring sudoers"
|
||||
rm -f /etc/sudoers.d/${USER}
|
||||
cp "${script_dir}/start/sudoers" /etc/sudoers.d/${USER}
|
||||
|
||||
echo "==> Configuring collectd"
|
||||
rm -rf /etc/collectd
|
||||
rm -rf /etc/collectd /var/log/collectd.log
|
||||
ln -sfF "${PLATFORM_DATA_DIR}/collectd" /etc/collectd
|
||||
cp "${script_dir}/start/collectd/collectd.conf" "${PLATFORM_DATA_DIR}/collectd/collectd.conf"
|
||||
if [[ "${ubuntu_version}" == "20.04" ]]; then
|
||||
# https://bugs.launchpad.net/ubuntu/+source/collectd/+bug/1872281
|
||||
if ! grep -q LD_PRELOAD /etc/default/collectd; then
|
||||
echo -e "\nLD_PRELOAD=/usr/lib/python3.8/config-3.8-x86_64-linux-gnu/libpython3.8.so" >> /etc/default/collectd
|
||||
fi
|
||||
fi
|
||||
systemctl restart collectd
|
||||
|
||||
echo "==> Configuring logrotate"
|
||||
if ! grep -q "^include ${PLATFORM_DATA_DIR}/logrotate.d" /etc/logrotate.conf; then
|
||||
echo -e "\ninclude ${PLATFORM_DATA_DIR}/logrotate.d\n" >> /etc/logrotate.conf
|
||||
fi
|
||||
rm -f "${PLATFORM_DATA_DIR}/logrotate.d/"*
|
||||
cp "${script_dir}/start/logrotate/"* "${PLATFORM_DATA_DIR}/logrotate.d/"
|
||||
rm -f "${PLATFORM_DATA_DIR}/logrotate.d/box-logrotate" "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate" # remove pre 3.6 config files
|
||||
|
||||
# logrotate files have to be owned by root, this is here to fixup existing installations where we were resetting the owner to yellowtent
|
||||
chown root:root "${PLATFORM_DATA_DIR}/logrotate.d/"
|
||||
@@ -144,8 +164,15 @@ cp "${script_dir}/start/nginx/mime.types" "${PLATFORM_DATA_DIR}/nginx/mime.types
|
||||
if ! grep -q "^Restart=" /etc/systemd/system/multi-user.target.wants/nginx.service; then
|
||||
# default nginx service file does not restart on crash
|
||||
echo -e "\n[Service]\nRestart=always\n" >> /etc/systemd/system/multi-user.target.wants/nginx.service
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
|
||||
# worker_rlimit_nofile in nginx config can be max this number
|
||||
mkdir -p /etc/systemd/system/nginx.service.d
|
||||
if ! grep -q "^LimitNOFILE=" /etc/systemd/system/nginx.service.d/cloudron.conf; then
|
||||
echo -e "[Service]\nLimitNOFILE=16384\n" > /etc/systemd/system/nginx.service.d/cloudron.conf
|
||||
fi
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl start nginx
|
||||
|
||||
# restart mysql to make sure it has latest config
|
||||
@@ -168,10 +195,22 @@ fi
|
||||
|
||||
readonly mysql_root_password="password"
|
||||
mysqladmin -u root -ppassword password password # reset default root password
|
||||
if [[ "${ubuntu_version}" == "20.04" ]]; then
|
||||
# mysql 8 added a new caching_sha2_password scheme which mysqljs does not support
|
||||
mysql -u root -p${mysql_root_password} -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '${mysql_root_password}';"
|
||||
fi
|
||||
mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
|
||||
|
||||
# set HOME explicity, because it's not set when the installer calls it. this is done because
|
||||
# paths.js uses this env var and some of the migrate code requires box code
|
||||
echo "==> Migrating data"
|
||||
(cd "${BOX_SRC_DIR}" && BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up)
|
||||
cd "${BOX_SRC_DIR}"
|
||||
if ! HOME=${HOME_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; then
|
||||
echo "DB migration failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -f /etc/cloudron/cloudron.conf
|
||||
|
||||
if [[ ! -f "${BOX_DATA_DIR}/dhparams.pem" ]]; then
|
||||
echo "==> Generating dhparams (takes forever)"
|
||||
@@ -181,6 +220,16 @@ else
|
||||
cp "${BOX_DATA_DIR}/dhparams.pem" "${PLATFORM_DATA_DIR}/addons/mail/dhparams.pem"
|
||||
fi
|
||||
|
||||
if [[ ! -f "${BOX_DATA_DIR}/sftp/ssh/ssh_host_rsa_key" ]]; then
|
||||
# the key format in Ubuntu 20 changed, so we create keys in legacy format. for older ubuntu, just re-use the host keys
|
||||
# see https://github.com/proftpd/proftpd/issues/793
|
||||
if [[ "${ubuntu_version}" == "20.04" ]]; then
|
||||
ssh-keygen -m PEM -t rsa -f "${BOX_DATA_DIR}/sftp/ssh/ssh_host_rsa_key" -q -N ""
|
||||
else
|
||||
cp /etc/ssh/ssh_host_rsa_key* ${BOX_DATA_DIR}/sftp/ssh
|
||||
fi
|
||||
fi
|
||||
|
||||
# old installations used to create appdata/<app>/redis which is now part of old backups and prevents restore
|
||||
echo "==> Cleaning up stale redis directories"
|
||||
find "${APPS_DATA_DIR}" -maxdepth 2 -type d -name redis -exec rm -rf {} +
|
||||
@@ -200,7 +249,7 @@ chown "${USER}:${USER}" "${BOX_DATA_DIR}/mail"
|
||||
chown "${USER}:${USER}" -R "${BOX_DATA_DIR}/mail/dkim" # this is owned by box currently since it generates the keys
|
||||
|
||||
echo "==> Starting Cloudron"
|
||||
systemctl start cloudron.target
|
||||
systemctl start box
|
||||
|
||||
sleep 2 # give systemd sometime to start the processes
|
||||
|
||||
|
||||
@@ -6,11 +6,34 @@ echo "==> Setting up firewall"
|
||||
iptables -t filter -N CLOUDRON || true
|
||||
iptables -t filter -F CLOUDRON # empty any existing rules
|
||||
|
||||
# NOTE: keep these in sync with src/apps.js validatePortBindings
|
||||
# allow ssh, http, https, ping, dns
|
||||
iptables -t filter -I CLOUDRON -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
# ssh is allowed alternately on port 202
|
||||
iptables -A CLOUDRON -p tcp -m tcp -m multiport --dports 22,25,80,202,443,587,993,4190 -j ACCEPT
|
||||
# first setup any user IP block lists
|
||||
ipset create cloudron_blocklist hash:net || true
|
||||
/home/yellowtent/box/src/scripts/setblocklist.sh
|
||||
|
||||
iptables -t filter -A CLOUDRON -m set --match-set cloudron_blocklist src -j DROP
|
||||
# the DOCKER-USER chain is not cleared on docker restart
|
||||
if ! iptables -t filter -C DOCKER-USER -m set --match-set cloudron_blocklist src -j DROP; then
|
||||
iptables -t filter -I DOCKER-USER 1 -m set --match-set cloudron_blocklist src -j DROP
|
||||
fi
|
||||
|
||||
# allow related and establisted connections
|
||||
iptables -t filter -A CLOUDRON -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
iptables -t filter -A CLOUDRON -p tcp -m tcp -m multiport --dports 22,25,80,202,443 -j ACCEPT # 202 is the alternate ssh port
|
||||
|
||||
# whitelist any user ports
|
||||
ports_json="/home/yellowtent/boxdata/firewall/ports.json"
|
||||
if allowed_tcp_ports=$(node -e "console.log(JSON.parse(fs.readFileSync('${ports_json}', 'utf8')).allowed_tcp_ports.join(','))" 2>/dev/null); then
|
||||
[[ -n "${allowed_tcp_ports}" ]] && iptables -A CLOUDRON -p tcp -m tcp -m multiport --dports "${allowed_tcp_ports}" -j ACCEPT
|
||||
fi
|
||||
|
||||
if allowed_udp_ports=$(node -e "console.log(JSON.parse(fs.readFileSync('${ports_json}', 'utf8')).allowed_udp_ports.join(','))" 2>/dev/null); then
|
||||
[[ -n "${allowed_tcp_ports}" ]] && iptables -A CLOUDRON -p udp -m udp -m multiport --dports "${allowed_tcp_ports}" -j ACCEPT
|
||||
fi
|
||||
|
||||
# turn and stun service
|
||||
iptables -t filter -A CLOUDRON -p tcp -m multiport --dports 3478,5349 -j ACCEPT
|
||||
iptables -t filter -A CLOUDRON -p udp -m multiport --dports 3478,5349 -j ACCEPT
|
||||
iptables -t filter -A CLOUDRON -p udp -m multiport --dports 50000:51000 -j ACCEPT
|
||||
|
||||
iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-request -j ACCEPT
|
||||
iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-reply -j ACCEPT
|
||||
@@ -47,8 +70,6 @@ for port in 22 202; do
|
||||
iptables -A CLOUDRON_RATELIMIT -p tcp --dport ${port} -m state --state NEW -m recent --update --name "public-${port}" --seconds 10 --hitcount 5 -j CLOUDRON_RATELIMIT_LOG
|
||||
done
|
||||
|
||||
# TODO: move docker platform rules to platform.js so it can be specialized to rate limit only when destination is the mail container
|
||||
|
||||
# docker translates (dnat) 25, 587, 993, 4190 in the PREROUTING step
|
||||
for port in 2525 4190 9993; do
|
||||
iptables -A CLOUDRON_RATELIMIT -p tcp --syn ! -s 172.18.0.0/16 -d 172.18.0.0/16 --dport ${port} -m connlimit --connlimit-above 50 -j CLOUDRON_RATELIMIT_LOG
|
||||
@@ -64,12 +85,10 @@ for port in 3306 5432 6379 27017; do
|
||||
iptables -A CLOUDRON_RATELIMIT -p tcp --syn -s 172.18.0.0/16 -d 172.18.0.0/16 --dport ${port} -m connlimit --connlimit-above 5000 -j CLOUDRON_RATELIMIT_LOG
|
||||
done
|
||||
|
||||
# For ssh, http, https
|
||||
if ! iptables -t filter -C INPUT -j CLOUDRON_RATELIMIT 2>/dev/null; then
|
||||
iptables -t filter -I INPUT 1 -j CLOUDRON_RATELIMIT
|
||||
fi
|
||||
|
||||
# For smtp, imap etc routed via docker/nat
|
||||
# Workaroud issue where Docker insists on adding itself first in FORWARD table
|
||||
# Workaround issue where Docker insists on adding itself first in FORWARD table
|
||||
iptables -D FORWARD -j CLOUDRON_RATELIMIT || true
|
||||
iptables -I FORWARD 1 -j CLOUDRON_RATELIMIT
|
||||
|
||||
@@ -3,12 +3,19 @@
|
||||
printf "**********************************************************************\n\n"
|
||||
|
||||
if [[ -z "$(ls -A /home/yellowtent/boxdata/mail/dkim)" ]]; then
|
||||
if [[ -f /tmp/.cloudron-motd-cache ]]; then
|
||||
ip=$(cat /tmp/.cloudron-motd-cache)
|
||||
elif ! ip=$(curl --fail --connect-timeout 2 --max-time 2 -q https://api.cloudron.io/api/v1/helper/public_ip | sed -n -e 's/.*"ip": "\(.*\)"/\1/p'); then
|
||||
ip='<IP>'
|
||||
fi
|
||||
echo "${ip}" > /tmp/.cloudron-motd-cache
|
||||
|
||||
printf "\t\t\tWELCOME TO CLOUDRON\n"
|
||||
printf "\t\t\t-------------------\n"
|
||||
|
||||
printf '\n\e[1;32m%-6s\e[m\n\n' "Visit https://<IP> on your browser and accept the self-signed certificate to finish setup."
|
||||
printf "Cloudron overview - https://cloudron.io/documentation/ \n"
|
||||
printf "Cloudron setup - https://cloudron.io/documentation/installation/#setup \n"
|
||||
printf '\n\e[1;32m%-6s\e[m\n\n' "Visit https://${ip} on your browser and accept the self-signed certificate to finish setup."
|
||||
printf "Cloudron overview - https://docs.cloudron.io/ \n"
|
||||
printf "Cloudron setup - https://docs.cloudron.io/installation/#setup \n"
|
||||
else
|
||||
printf "\t\t\tNOTE TO CLOUDRON ADMINS\n"
|
||||
printf "\t\t\t-----------------------\n"
|
||||
@@ -16,7 +23,7 @@ else
|
||||
printf "Cloudron relies on and may break your installation. Ubuntu security updates\n"
|
||||
printf "are automatically installed on this server every night.\n"
|
||||
printf "\n"
|
||||
printf "Read more at https://cloudron.io/documentation/security/#os-updates\n"
|
||||
printf "Read more at https://docs.cloudron.io/security/#os-updates\n"
|
||||
fi
|
||||
|
||||
printf "\nFor help and more information, visit https://forum.cloudron.io\n\n"
|
||||
|
||||
@@ -4,6 +4,11 @@ set -eu -o pipefail
|
||||
|
||||
readonly APPS_SWAP_FILE="/apps.swap"
|
||||
|
||||
if [[ -f "${APPS_SWAP_FILE}" ]]; then
|
||||
echo "Swap file already exists at /apps.swap . Skipping"
|
||||
exit
|
||||
fi
|
||||
|
||||
# all sizes are in mb
|
||||
readonly physical_memory=$(LC_ALL=C free -m | awk '/Mem:/ { print $2 }')
|
||||
readonly swap_size=$((${physical_memory} > 4096 ? 4096 : ${physical_memory})) # min(RAM, 4GB) if you change this, fix enoughResourcesAvailable() in client.js
|
||||
|
||||
@@ -57,7 +57,7 @@ LoadPlugin logfile
|
||||
|
||||
<Plugin logfile>
|
||||
LogLevel "info"
|
||||
File "/var/log/collectd.log"
|
||||
File "/home/yellowtent/platformdata/logs/collectd/collectd.log"
|
||||
Timestamp true
|
||||
PrintSeverity false
|
||||
</Plugin>
|
||||
@@ -121,7 +121,7 @@ LoadPlugin memory
|
||||
#LoadPlugin netlink
|
||||
#LoadPlugin network
|
||||
#LoadPlugin nfs
|
||||
LoadPlugin nginx
|
||||
#LoadPlugin nginx
|
||||
#LoadPlugin notify_desktop
|
||||
#LoadPlugin notify_email
|
||||
#LoadPlugin ntpd
|
||||
@@ -149,7 +149,7 @@ LoadPlugin nginx
|
||||
#LoadPlugin statsd
|
||||
LoadPlugin swap
|
||||
#LoadPlugin table
|
||||
LoadPlugin tail
|
||||
#LoadPlugin tail
|
||||
#LoadPlugin tail_csv
|
||||
#LoadPlugin tcpconns
|
||||
#LoadPlugin teamspeak2
|
||||
@@ -197,42 +197,11 @@ LoadPlugin write_graphite
|
||||
IgnoreSelected false
|
||||
</Plugin>
|
||||
|
||||
<Plugin nginx>
|
||||
URL "http://127.0.0.1/nginx_status"
|
||||
</Plugin>
|
||||
|
||||
<Plugin swap>
|
||||
ReportByDevice false
|
||||
ReportBytes true
|
||||
</Plugin>
|
||||
|
||||
<Plugin "tail">
|
||||
<File "/var/log/nginx/error.log">
|
||||
Instance "nginx"
|
||||
<Match>
|
||||
Regex ".*"
|
||||
DSType "CounterInc"
|
||||
Type counter
|
||||
Instance "errors"
|
||||
</Match>
|
||||
</File>
|
||||
<File "/var/log/nginx/access.log">
|
||||
Instance "nginx"
|
||||
<Match>
|
||||
Regex ".*"
|
||||
DSType "CounterInc"
|
||||
Type counter
|
||||
Instance "requests"
|
||||
</Match>
|
||||
<Match>
|
||||
Regex " \".*\" [0-9]+ [0-9]+ ([0-9]+)"
|
||||
DSType GaugeAverage
|
||||
Type delay
|
||||
Instance "response"
|
||||
</Match>
|
||||
</File>
|
||||
</Plugin>
|
||||
|
||||
<Plugin python>
|
||||
# https://blog.dbrgn.ch/2017/3/10/write-a-collectd-python-plugin/
|
||||
ModulePath "/home/yellowtent/box/setup/start/collectd/"
|
||||
@@ -240,8 +209,23 @@ LoadPlugin write_graphite
|
||||
Interactive false
|
||||
|
||||
Import "df"
|
||||
# <Module df>
|
||||
# </Module>
|
||||
|
||||
Import "du"
|
||||
<Module du>
|
||||
<Path>
|
||||
Instance maildata
|
||||
Dir "/home/yellowtent/boxdata/mail"
|
||||
</Path>
|
||||
<Path>
|
||||
Instance boxdata
|
||||
Dir "/home/yellowtent/boxdata"
|
||||
Exclude "mail"
|
||||
</Path>
|
||||
<Path>
|
||||
Instance platformdata
|
||||
Dir "/home/yellowtent/platformdata"
|
||||
</Path>
|
||||
</Module>
|
||||
</Plugin>
|
||||
|
||||
<Plugin write_graphite>
|
||||
|
||||
@@ -6,7 +6,7 @@ disks = []
|
||||
|
||||
def init():
|
||||
global disks
|
||||
lines = [s.split() for s in subprocess.check_output(["df", "--type=ext4", "--output=source,target,size,used,avail"]).splitlines()]
|
||||
lines = [s.split() for s in subprocess.check_output(["df", "--type=ext4", "--output=source,target,size,used,avail"]).decode('utf-8').splitlines()]
|
||||
disks = lines[1:] # strip header
|
||||
collectd.info('custom df plugin initialized with %s' % disks)
|
||||
|
||||
@@ -21,6 +21,7 @@ def read():
|
||||
except:
|
||||
continue
|
||||
|
||||
# type comes from https://github.com/collectd/collectd/blob/master/src/types.db
|
||||
val = collectd.Values(type='df_complex', plugin='df', plugin_instance=instance)
|
||||
|
||||
free = st.f_bavail * st.f_frsize # bavail is for non-root user. bfree is total
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import collectd,os,subprocess,sys,re,time
|
||||
|
||||
# https://www.programcreek.com/python/example/106897/collectd.register_read
|
||||
|
||||
PATHS = [] # { name, dir, exclude }
|
||||
# there is a pattern in carbon/storage-schemas.conf which stores values every 12h for a year
|
||||
INTERVAL = 60 * 60 * 12 # twice a day. change values in docker-graphite if you change this
|
||||
|
||||
def du(pathinfo):
|
||||
# -B1 makes du print block sizes and not apparent sizes (to match df which also uses block sizes)
|
||||
cmd = 'timeout 1800 du -DsB1 "{}"'.format(pathinfo['dir'])
|
||||
if pathinfo['exclude'] != '':
|
||||
cmd += ' --exclude "{}"'.format(pathinfo['exclude'])
|
||||
|
||||
collectd.info('computing size with command: %s' % cmd);
|
||||
try:
|
||||
size = subprocess.check_output(cmd, shell=True).split()[0].decode('utf-8')
|
||||
collectd.info('\tsize of %s is %s (time: %i)' % (pathinfo['dir'], size, int(time.time())))
|
||||
return size
|
||||
except Exception as e:
|
||||
collectd.info('\terror getting the size of %s: %s' % (pathinfo['dir'], str(e)))
|
||||
return 0
|
||||
|
||||
def parseSize(size):
|
||||
units = {"B": 1, "KB": 10**3, "MB": 10**6, "GB": 10**9, "TB": 10**12}
|
||||
number, unit, _ = re.split('([a-zA-Z]+)', size.upper())
|
||||
return int(float(number)*units[unit])
|
||||
|
||||
def dockerSize():
|
||||
# use --format '{{json .}}' to dump the string. '{{if eq .Type "Images"}}{{.Size}}{{end}}' still creates newlines
|
||||
# https://godoc.org/github.com/docker/go-units#HumanSize is used. so it's 1000 (KB) and not 1024 (KiB)
|
||||
cmd = 'timeout 1800 docker system df --format "{{.Size}}" | head -n1'
|
||||
try:
|
||||
size = subprocess.check_output(cmd, shell=True).strip().decode('utf-8')
|
||||
collectd.info('size of docker images is %s (%s) (time: %i)' % (size, parseSize(size), int(time.time())))
|
||||
return parseSize(size)
|
||||
except Exception as e:
|
||||
collectd.info('error getting docker images size : %s' % str(e))
|
||||
return 0
|
||||
|
||||
# configure is called for each module block. this is called before init
|
||||
def configure(config):
|
||||
global PATHS
|
||||
|
||||
for child in config.children:
|
||||
if child.key != 'Path':
|
||||
collectd.info('du plugin: Unknown config key "%s"' % key)
|
||||
continue
|
||||
|
||||
pathinfo = { 'name': '', 'dir': '', 'exclude': '' }
|
||||
for node in child.children:
|
||||
if node.key == 'Instance':
|
||||
pathinfo['name'] = node.values[0]
|
||||
elif node.key == 'Dir':
|
||||
pathinfo['dir'] = node.values[0]
|
||||
elif node.key == 'Exclude':
|
||||
pathinfo['exclude'] = node.values[0]
|
||||
|
||||
PATHS.append(pathinfo);
|
||||
collectd.info('du plugin: monitoring %s' % pathinfo['dir']);
|
||||
|
||||
def init():
|
||||
global PATHS
|
||||
collectd.info('custom du plugin initialized with %s %s' % (PATHS, sys.version))
|
||||
|
||||
def read():
|
||||
for pathinfo in PATHS:
|
||||
size = du(pathinfo)
|
||||
|
||||
# type comes from https://github.com/collectd/collectd/blob/master/src/types.db
|
||||
val = collectd.Values(type='capacity', plugin='du', plugin_instance=pathinfo['name'])
|
||||
val.dispatch(values=[size], type_instance='usage')
|
||||
|
||||
size = dockerSize()
|
||||
val = collectd.Values(type='capacity', plugin='du', plugin_instance='docker')
|
||||
val.dispatch(values=[size], type_instance='usage')
|
||||
|
||||
|
||||
|
||||
collectd.register_init(init)
|
||||
collectd.register_config(configure)
|
||||
collectd.register_read(read, INTERVAL)
|
||||
@@ -1,40 +0,0 @@
|
||||
# add customizations here
|
||||
# after making changes run "sudo systemctl restart box"
|
||||
|
||||
# appstore:
|
||||
# blacklist:
|
||||
# - io.wekan.cloudronapp
|
||||
# - io.cloudron.openvpn
|
||||
# whitelist:
|
||||
# org.wordpress.cloudronapp: {}
|
||||
# chat.rocket.cloudronapp: {}
|
||||
# com.nextcloud.cloudronapp: {}
|
||||
#
|
||||
# backups:
|
||||
# configurable: true
|
||||
#
|
||||
# domains:
|
||||
# dynamicDns: true
|
||||
# changeDashboardDomain: true
|
||||
#
|
||||
# subscription:
|
||||
# configurable: true
|
||||
#
|
||||
# support:
|
||||
# email: support@cloudron.io
|
||||
# remoteSupport: true
|
||||
#
|
||||
# ticketFormBody: |
|
||||
# Use this form to open support tickets. You can also write directly to [support@cloudron.io](mailto:support@cloudron.io).
|
||||
# * [Knowledge Base & App Docs](https://cloudron.io/documentation/apps/?support_view)
|
||||
# * [Custom App Packaging & API](https://cloudron.io/developer/packaging/?support_view)
|
||||
# * [Forum](https://forum.cloudron.io/)
|
||||
#
|
||||
# submitTickets: true
|
||||
#
|
||||
# alerts:
|
||||
# email: support@cloudron.io
|
||||
# notifyCloudronAdmins: false
|
||||
#
|
||||
# footer:
|
||||
# body: '© 2019 [Cloudron](https://cloudron.io) [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)'
|
||||
@@ -1,18 +0,0 @@
|
||||
# logrotate config for app, crash, addon and task logs
|
||||
|
||||
# man 7 glob
|
||||
/home/yellowtent/platformdata/logs/[!t][!a][!s][!k][!s]/*.log {
|
||||
# only keep one rotated file, we currently do not send that over the api
|
||||
rotate 1
|
||||
size 10M
|
||||
# we never compress so we can simply tail the files
|
||||
nocompress
|
||||
copytruncate
|
||||
}
|
||||
|
||||
/home/yellowtent/platformdata/logs/tasks/*.log {
|
||||
monthly
|
||||
rotate 0
|
||||
missingok
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# logrotate config for box logs
|
||||
|
||||
# keep upto 5 logs of size 10M each
|
||||
/home/yellowtent/platformdata/logs/box.log {
|
||||
rotate 10
|
||||
rotate 5
|
||||
size 10M
|
||||
# we never compress so we can simply tail the files
|
||||
nocompress
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# logrotate config for app, crash, addon and task logs
|
||||
|
||||
# man 7 glob
|
||||
/home/yellowtent/platformdata/logs/graphite/*.log
|
||||
/home/yellowtent/platformdata/logs/mail/*.log
|
||||
/home/yellowtent/platformdata/logs/mysql/*.log
|
||||
/home/yellowtent/platformdata/logs/mongodb/*.log
|
||||
/home/yellowtent/platformdata/logs/postgresql/*.log
|
||||
/home/yellowtent/platformdata/logs/sftp/*.log
|
||||
/home/yellowtent/platformdata/logs/redis-*/*.log
|
||||
/home/yellowtent/platformdata/logs/crash/*.log
|
||||
/home/yellowtent/platformdata/logs/collectd/*.log
|
||||
/home/yellowtent/platformdata/logs/turn/*.log
|
||||
/home/yellowtent/platformdata/logs/updater/*.log {
|
||||
# only keep one rotated file, we currently do not send that over the api
|
||||
rotate 1
|
||||
size 10M
|
||||
missingok
|
||||
# we never compress so we can simply tail the files
|
||||
nocompress
|
||||
# this truncates the original log file and not the rotated one
|
||||
copytruncate
|
||||
}
|
||||
|
||||
# keep task logs for a week. the 'nocreate' option ensures empty log files are not
|
||||
# created post rotation
|
||||
/home/yellowtent/platformdata/logs/tasks/*.log {
|
||||
minage 7
|
||||
daily
|
||||
rotate 0
|
||||
missingok
|
||||
nocreate
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
performance_schema=OFF
|
||||
max_connections=50
|
||||
# on ec2, without this we get a sporadic connection drop when doing the initial migration
|
||||
max_allowed_packet=32M
|
||||
max_allowed_packet=64M
|
||||
|
||||
# https://mathiasbynens.be/notes/mysql-utf8mb4
|
||||
character-set-server = utf8mb4
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
user www-data;
|
||||
|
||||
worker_processes 1;
|
||||
# detect based on available CPU cores
|
||||
worker_processes auto;
|
||||
|
||||
# this is 4096 by default. See /proc/<PID>/limits and /etc/security/limits.conf
|
||||
# usually twice the worker_connections (one for uptsream, one for downstream)
|
||||
# see also LimitNOFILE=16384 in systemd drop-in
|
||||
worker_rlimit_nofile 8192;
|
||||
|
||||
pid /run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
# a single worker has these many simultaneous connections max
|
||||
worker_connections 4096;
|
||||
}
|
||||
|
||||
http {
|
||||
@@ -36,23 +43,5 @@ http {
|
||||
# zones for rate limiting
|
||||
limit_req_zone $binary_remote_addr zone=admin_login:10m rate=10r/s; # 10 request a second
|
||||
|
||||
|
||||
# default http server that returns 404 for any domain we are not listening on
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
server_name does_not_match_anything;
|
||||
|
||||
# acme challenges (for app installation and re-configure when the vhost config does not exist)
|
||||
location /.well-known/acme-challenge/ {
|
||||
default_type text/plain;
|
||||
alias /home/yellowtent/platformdata/acme/;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
|
||||
include applications/*.conf;
|
||||
}
|
||||
|
||||
@@ -50,3 +50,15 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartdocker.s
|
||||
Defaults!/home/yellowtent/box/src/scripts/restartunbound.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartunbound.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/rmmailbox.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmmailbox.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/starttask.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD:SETENV: /home/yellowtent/box/src/scripts/starttask.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/stoptask.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/stoptask.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/setblocklist.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/setblocklist.sh
|
||||
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
[Unit]
|
||||
Description=Cloudron Admin
|
||||
OnFailure=crashnotifier@%n.service
|
||||
StopWhenUnneeded=true
|
||||
; journald crashes result in a EPIPE in node. Cannot ignore it as it results in loss of logs.
|
||||
BindsTo=systemd-journald.service
|
||||
After=mysql.service nginx.service
|
||||
; As cloudron-resize-fs is a one-shot, the Wants= automatically ensures that the service *finishes*
|
||||
Wants=cloudron-resize-fs.service
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
[Service]
|
||||
Type=idle
|
||||
WorkingDirectory=/home/yellowtent/box
|
||||
Restart=always
|
||||
; Systemd does not append logs when logging to files, we spawn a shell first and exec to replace it after setting up the pipes
|
||||
ExecStart=/bin/sh -c 'echo "Logging to /home/yellowtent/platformdata/logs/box.log"; exec /usr/bin/node --max_old_space_size=150 /home/yellowtent/box/box.js >> /home/yellowtent/platformdata/logs/box.log 2>&1'
|
||||
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
|
||||
ExecStart=/home/yellowtent/box/box.js
|
||||
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box:*,connect-lastmile,-box:ldap" "BOX_ENV=cloudron" "NODE_ENV=production"
|
||||
; kill apptask processes as well
|
||||
KillMode=control-group
|
||||
; Do not kill this process on OOM. Children inherit this score. Do not set it to -1000 so that MemoryMax can keep working
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# https://docs.mongodb.com/manual/tutorial/transparent-huge-pages/
|
||||
[Unit]
|
||||
Description=Disable Transparent Huge Pages (THP)
|
||||
DefaultDependencies=no
|
||||
After=sysinit.target local-fs.target
|
||||
Before=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/sh -c 'echo never | tee /sys/kernel/mm/transparent_hugepage/enabled > /dev/null'
|
||||
|
||||
[Install]
|
||||
WantedBy=basic.target
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
[Unit]
|
||||
Description=Cloudron Smartserver
|
||||
Documentation=https://cloudron.io/documentation.html
|
||||
StopWhenUnneeded=true
|
||||
Requires=box.service
|
||||
After=box.service
|
||||
# AllowIsolate=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[Unit]
|
||||
Description=Unbound DNS Resolver
|
||||
After=network.target
|
||||
After=network.target docker.service
|
||||
|
||||
[Service]
|
||||
PIDFile=/run/unbound.pid
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
server:
|
||||
port: 53
|
||||
interface: 127.0.0.1
|
||||
interface: 172.18.0.1
|
||||
do-ip6: no
|
||||
access-control: 127.0.0.1 allow
|
||||
access-control: 172.18.0.1/16 allow
|
||||
cache-max-negative-ttl: 30
|
||||
cache-max-ttl: 300
|
||||
# enable below for logging to journalctl -u unbound
|
||||
# verbosity: 5
|
||||
# log-queries: yes
|
||||
|
||||
+1
-1
@@ -4,4 +4,4 @@ set -eu -o pipefail
|
||||
|
||||
echo "Stopping cloudron"
|
||||
|
||||
systemctl stop cloudron.target
|
||||
systemctl stop box
|
||||
|
||||
+9
-124
@@ -1,144 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
SCOPE_APPS_READ: 'apps:read',
|
||||
SCOPE_APPS_MANAGE: 'apps:manage',
|
||||
SCOPE_APPSTORE: 'appstore',
|
||||
SCOPE_CLIENTS: 'clients',
|
||||
SCOPE_CLOUDRON: 'cloudron',
|
||||
SCOPE_DOMAINS_READ: 'domains:read',
|
||||
SCOPE_DOMAINS_MANAGE: 'domains:manage',
|
||||
SCOPE_MAIL: 'mail',
|
||||
SCOPE_PROFILE: 'profile',
|
||||
SCOPE_SETTINGS: 'settings',
|
||||
SCOPE_SUBSCRIPTION: 'subscription',
|
||||
SCOPE_USERS_READ: 'users:read',
|
||||
SCOPE_USERS_MANAGE: 'users:manage',
|
||||
VALID_SCOPES: [ 'apps', 'appstore', 'clients', 'cloudron', 'domains', 'mail', 'profile', 'settings', 'subscription', 'users' ], // keep this sorted
|
||||
|
||||
SCOPE_ANY: '*',
|
||||
|
||||
validateScopeString: validateScopeString,
|
||||
hasScopes: hasScopes,
|
||||
canonicalScopeString: canonicalScopeString,
|
||||
intersectScopes: intersectScopes,
|
||||
validateToken: validateToken,
|
||||
scopesForUser: scopesForUser
|
||||
verifyToken: verifyToken
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:accesscontrol'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
users = require('./users.js'),
|
||||
UsersError = users.UsersError,
|
||||
_ = require('underscore');
|
||||
users = require('./users.js');
|
||||
|
||||
// returns scopes that does not have wildcards and is sorted
|
||||
function canonicalScopeString(scope) {
|
||||
if (scope === exports.SCOPE_ANY) return exports.VALID_SCOPES.join(',');
|
||||
|
||||
return scope.split(',').sort().join(',');
|
||||
}
|
||||
|
||||
function intersectScopes(allowedScopes, wantedScopes) {
|
||||
assert(Array.isArray(allowedScopes), 'Expecting sorted array');
|
||||
assert(Array.isArray(wantedScopes), 'Expecting sorted array');
|
||||
|
||||
if (_.isEqual(allowedScopes, wantedScopes)) return allowedScopes; // quick path
|
||||
|
||||
let wantedScopesMap = new Map();
|
||||
let results = [];
|
||||
|
||||
// make a map of scope -> [ subscopes ]
|
||||
for (let w of wantedScopes) {
|
||||
let parts = w.split(':');
|
||||
let subscopes = wantedScopesMap.get(parts[0]) || new Set();
|
||||
subscopes.add(parts[1] || '*');
|
||||
wantedScopesMap.set(parts[0], subscopes);
|
||||
}
|
||||
|
||||
for (let a of allowedScopes) {
|
||||
let parts = a.split(':');
|
||||
let as = parts[1] || '*';
|
||||
|
||||
let subscopes = wantedScopesMap.get(parts[0]);
|
||||
if (!subscopes) continue;
|
||||
|
||||
if (subscopes.has('*') || subscopes.has(as)) {
|
||||
results.push(a);
|
||||
} else if (as === '*') {
|
||||
results = results.concat(Array.from(subscopes).map(function (ss) { return `${a}:${ss}`; }));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function validateScopeString(scope) {
|
||||
assert.strictEqual(typeof scope, 'string');
|
||||
|
||||
if (scope === '') return new Error('Empty scope not allowed');
|
||||
|
||||
// NOTE: this function intentionally does not allow '*'. This is only allowed in the db to allow
|
||||
// us not write a migration script every time we add a new scope
|
||||
var allValid = scope.split(',').every(function (s) { return exports.VALID_SCOPES.indexOf(s.split(':')[0]) !== -1; });
|
||||
if (!allValid) return new Error('Invalid scope. Available scopes are ' + exports.VALID_SCOPES.join(', '));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// tests if all requiredScopes are attached to the request
|
||||
function hasScopes(authorizedScopes, requiredScopes) {
|
||||
assert(Array.isArray(authorizedScopes), 'Expecting array');
|
||||
assert(Array.isArray(requiredScopes), 'Expecting array');
|
||||
|
||||
if (authorizedScopes.indexOf(exports.SCOPE_ANY) !== -1) return null;
|
||||
|
||||
for (var i = 0; i < requiredScopes.length; ++i) {
|
||||
const scopeParts = requiredScopes[i].split(':');
|
||||
|
||||
// this allows apps:write if the token has a higher apps scope
|
||||
if (authorizedScopes.indexOf(requiredScopes[i]) === -1 && authorizedScopes.indexOf(scopeParts[0]) === -1) {
|
||||
debug('scope: missing scope "%s".', requiredScopes[i]);
|
||||
return new Error('Missing required scope "' + requiredScopes[i] + '"');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function scopesForUser(user, callback) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (user.admin) return callback(null, exports.VALID_SCOPES);
|
||||
|
||||
callback(null, [ 'profile', 'apps:read' ]);
|
||||
}
|
||||
|
||||
function validateToken(accessToken, callback) {
|
||||
function verifyToken(accessToken, callback) {
|
||||
assert.strictEqual(typeof accessToken, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
tokendb.getByAccessToken(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
|
||||
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (error) return callback(error);
|
||||
|
||||
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 && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (error) return callback(error);
|
||||
|
||||
scopesForUser(user, function (error, userScopes) {
|
||||
if (error) return callback(error);
|
||||
if (!user.active) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
|
||||
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);
|
||||
});
|
||||
callback(null, user);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+730
-405
File diff suppressed because it is too large
Load Diff
@@ -1,185 +0,0 @@
|
||||
# http://nginx.org/en/docs/http/websocket.html
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
# http server
|
||||
server {
|
||||
listen 80;
|
||||
<% if (hasIPv6) { -%>
|
||||
listen [::]:80;
|
||||
<% } -%>
|
||||
|
||||
<% if (vhost) { -%>
|
||||
server_name <%= vhost %>;
|
||||
<% } else { -%>
|
||||
# IP based access from collectd or initial cloudron setup. TODO: match the IPv6 address
|
||||
server_name "~^\d+\.\d+\.\d+\.\d+$";
|
||||
|
||||
# collectd
|
||||
location /nginx_status {
|
||||
stub_status on;
|
||||
access_log off;
|
||||
allow 127.0.0.1;
|
||||
deny all;
|
||||
}
|
||||
<% } -%>
|
||||
|
||||
# acme challenges (for cert renewal where the vhost config exists)
|
||||
location /.well-known/acme-challenge/ {
|
||||
default_type text/plain;
|
||||
alias /home/yellowtent/platformdata/acme/;
|
||||
}
|
||||
|
||||
location / {
|
||||
# redirect everything to HTTPS
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
# https server
|
||||
server {
|
||||
<% if (vhost) { -%>
|
||||
server_name <%= vhost %>;
|
||||
listen 443 http2;
|
||||
<% if (hasIPv6) { -%>
|
||||
listen [::]:443 http2;
|
||||
<% } -%>
|
||||
<% } else { -%>
|
||||
listen 443 http2 default_server;
|
||||
<% if (hasIPv6) { -%>
|
||||
listen [::]:443 http2 default_server;
|
||||
<% } -%>
|
||||
<% } -%>
|
||||
|
||||
ssl on;
|
||||
# paths are relative to prefix and not to this file
|
||||
ssl_certificate <%= certFilePath %>;
|
||||
ssl_certificate_key <%= keyFilePath %>;
|
||||
ssl_session_timeout 5m;
|
||||
ssl_session_cache shared:SSL:50m;
|
||||
|
||||
# https://bettercrypto.org/static/applied-crypto-hardening.pdf
|
||||
# https://mozilla.github.io/server-side-tls/ssl-config-generator/
|
||||
# https://cipherli.st/
|
||||
# https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # don't use SSLv3 ref: POODLE
|
||||
|
||||
# ciphers according to https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=nginx-1.10.3&openssl=1.0.2g&hsts=yes&profile=modern
|
||||
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
|
||||
ssl_dhparam /home/yellowtent/boxdata/dhparams.pem;
|
||||
add_header Strict-Transport-Security "max-age=15768000";
|
||||
|
||||
# https://github.com/twitter/secureheaders
|
||||
# https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#tab=Compatibility_Matrix
|
||||
# https://wiki.mozilla.org/Security/Guidelines/Web_Security
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
proxy_hide_header X-XSS-Protection;
|
||||
add_header X-Download-Options "noopen";
|
||||
proxy_hide_header X-Download-Options;
|
||||
add_header X-Content-Type-Options "nosniff";
|
||||
proxy_hide_header X-Content-Type-Options;
|
||||
add_header X-Permitted-Cross-Domain-Policies "none";
|
||||
proxy_hide_header X-Permitted-Cross-Domain-Policies;
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade";
|
||||
proxy_hide_header Referrer-Policy;
|
||||
|
||||
# gzip responses that are > 50k and not images
|
||||
gzip on;
|
||||
gzip_min_length 50k;
|
||||
gzip_types text/css text/javascript text/xml text/plain application/javascript application/x-javascript application/json;
|
||||
|
||||
# enable for proxied requests as well
|
||||
gzip_proxied any;
|
||||
|
||||
<% if ( endpoint === 'admin' ) { -%>
|
||||
# CSP headers for the admin/dashboard resources
|
||||
add_header Content-Security-Policy "default-src 'none'; connect-src wss: https: 'self' *.cloudron.io; script-src https: 'self' 'unsafe-inline' 'unsafe-eval'; img-src * data:; style-src https: 'unsafe-inline'; object-src 'none'; font-src https: 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self';";
|
||||
<% } -%>
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_intercept_errors on;
|
||||
proxy_read_timeout 3500;
|
||||
proxy_connect_timeout 3250;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Ssl on;
|
||||
|
||||
# upgrade is a hop-by-hop header (http://nginx.org/en/docs/http/websocket.html)
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
|
||||
# only serve up the status page if we get proxy gateway errors
|
||||
root <%= sourceDir %>/dashboard/dist;
|
||||
error_page 502 503 504 /appstatus.html;
|
||||
location /appstatus.html {
|
||||
internal;
|
||||
}
|
||||
|
||||
location / {
|
||||
# increase the proxy buffer sizes to not run into buffer issues (http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffers)
|
||||
proxy_buffer_size 128k;
|
||||
proxy_buffers 4 256k;
|
||||
proxy_busy_buffers_size 256k;
|
||||
|
||||
# No buffering to temp files, it fails for large downloads
|
||||
proxy_max_temp_file_size 0;
|
||||
|
||||
# Disable check to allow unlimited body sizes. this allows apps to accept whatever size they want
|
||||
client_max_body_size 0;
|
||||
|
||||
<% if (robotsTxtQuoted) { %>
|
||||
location = /robots.txt {
|
||||
return 200 <%- robotsTxtQuoted %>;
|
||||
}
|
||||
<% } %>
|
||||
|
||||
<% if ( endpoint === 'admin' ) { %>
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
client_max_body_size 1m;
|
||||
}
|
||||
|
||||
location ~ ^/api/v1/(developer|session)/login$ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
client_max_body_size 1m;
|
||||
limit_req zone=admin_login burst=5;
|
||||
}
|
||||
|
||||
# the read timeout is between successive reads and not the whole connection
|
||||
location ~ ^/api/v1/apps/.*/exec$ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_read_timeout 30m;
|
||||
}
|
||||
|
||||
location ~ ^/api/v1/apps/.*/upload$ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
client_max_body_size 0;
|
||||
}
|
||||
|
||||
# graphite paths (uncomment block below and visit /graphite/index.html)
|
||||
# remember to comment out the CSP policy as well to access the graphite dashboard
|
||||
# location ~ ^/(graphite|content|metrics|dashboard|render|browser|composer)/ {
|
||||
# proxy_pass http://127.0.0.1:8417;
|
||||
# client_max_body_size 1m;
|
||||
# }
|
||||
|
||||
location / {
|
||||
root <%= sourceDir %>/dashboard/dist;
|
||||
index index.html index.htm;
|
||||
}
|
||||
<% } else if ( endpoint === 'app' ) { %>
|
||||
proxy_pass http://127.0.0.1:<%= port %>;
|
||||
<% } else if ( endpoint === 'redirect' ) { %>
|
||||
# redirect everything to the app. this is temporary because there is no way
|
||||
# to clear a permanent redirect on the browser
|
||||
return 302 https://<%= redirectTo %>$request_uri;
|
||||
<% } %>
|
||||
}
|
||||
}
|
||||
+133
-241
@@ -1,55 +1,27 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
get: get,
|
||||
getByHttpPort: getByHttpPort,
|
||||
getByContainerId: getByContainerId,
|
||||
add: add,
|
||||
exists: exists,
|
||||
del: del,
|
||||
update: update,
|
||||
getAll: getAll,
|
||||
getPortBindings: getPortBindings,
|
||||
delPortBinding: delPortBinding,
|
||||
get,
|
||||
add,
|
||||
exists,
|
||||
del,
|
||||
update,
|
||||
getAll,
|
||||
getPortBindings,
|
||||
delPortBinding,
|
||||
|
||||
setAddonConfig: setAddonConfig,
|
||||
getAddonConfig: getAddonConfig,
|
||||
getAddonConfigByAppId: getAddonConfigByAppId,
|
||||
getAddonConfigByName: getAddonConfigByName,
|
||||
unsetAddonConfig: unsetAddonConfig,
|
||||
unsetAddonConfigByAppId: unsetAddonConfigByAppId,
|
||||
getAppIdByAddonConfigValue: getAppIdByAddonConfigValue,
|
||||
setAddonConfig,
|
||||
getAddonConfig,
|
||||
getAddonConfigByAppId,
|
||||
getAddonConfigByName,
|
||||
unsetAddonConfig,
|
||||
unsetAddonConfigByAppId,
|
||||
getAppIdByAddonConfigValue,
|
||||
getByIpAddress,
|
||||
|
||||
setHealth: setHealth,
|
||||
setInstallationCommand: setInstallationCommand,
|
||||
setRunCommand: setRunCommand,
|
||||
getAppStoreIds: getAppStoreIds,
|
||||
|
||||
setOwner: setOwner,
|
||||
transferOwnership: transferOwnership,
|
||||
|
||||
// installation codes (keep in sync in UI)
|
||||
ISTATE_PENDING_INSTALL: 'pending_install', // installs and fresh reinstalls
|
||||
ISTATE_PENDING_CLONE: 'pending_clone', // clone
|
||||
ISTATE_PENDING_CONFIGURE: 'pending_configure', // config (location, port) changes and on infra update
|
||||
ISTATE_PENDING_UNINSTALL: 'pending_uninstall', // uninstallation
|
||||
ISTATE_PENDING_RESTORE: 'pending_restore', // restore to previous backup or on upgrade
|
||||
ISTATE_PENDING_UPDATE: 'pending_update', // update from installed state preserving data
|
||||
ISTATE_PENDING_FORCE_UPDATE: 'pending_force_update', // update from any state preserving data
|
||||
ISTATE_PENDING_BACKUP: 'pending_backup', // backup the app
|
||||
ISTATE_ERROR: 'error', // error executing last pending_* command
|
||||
ISTATE_INSTALLED: 'installed', // app is installed
|
||||
|
||||
RSTATE_RUNNING: 'running',
|
||||
RSTATE_PENDING_START: 'pending_start',
|
||||
RSTATE_PENDING_STOP: 'pending_stop',
|
||||
RSTATE_STOPPED: 'stopped', // app stopped by us
|
||||
|
||||
// run codes (keep in sync in UI)
|
||||
HEALTH_HEALTHY: 'healthy',
|
||||
HEALTH_UNHEALTHY: 'unhealthy',
|
||||
HEALTH_ERROR: 'error',
|
||||
HEALTH_DEAD: 'dead',
|
||||
setHealth,
|
||||
setTask,
|
||||
getAppStoreIds,
|
||||
|
||||
// subdomain table types
|
||||
SUBDOMAIN_TYPE_PRIMARY: 'primary',
|
||||
@@ -60,17 +32,16 @@ exports = module.exports = {
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror'),
|
||||
safe = require('safetydance'),
|
||||
util = require('util');
|
||||
|
||||
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.installationProgress', 'apps.runState',
|
||||
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'subdomains.subdomain AS location', 'subdomains.domain',
|
||||
'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit',
|
||||
'apps.label', 'apps.tagsJson',
|
||||
'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
|
||||
'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.mailboxName', 'apps.enableAutomaticUpdate',
|
||||
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.errorJson', 'apps.runState',
|
||||
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.accessRestrictionJson', 'apps.memoryLimit', 'apps.cpuShares',
|
||||
'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.servicesConfigJson',
|
||||
'apps.sso', 'apps.debugModeJson', 'apps.enableBackup', 'apps.proxyAuth', 'apps.containerIp',
|
||||
'apps.creationTime', 'apps.updateTime', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableAutomaticUpdate',
|
||||
'apps.dataDir', 'apps.ts', 'apps.healthTime' ].join(',');
|
||||
|
||||
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(',');
|
||||
@@ -84,22 +55,14 @@ function postProcess(result) {
|
||||
result.manifest = safe.JSON.parse(result.manifestJson);
|
||||
delete result.manifestJson;
|
||||
|
||||
assert(result.oldConfigJson === null || typeof result.oldConfigJson === 'string');
|
||||
result.oldConfig = safe.JSON.parse(result.oldConfigJson);
|
||||
delete result.oldConfigJson;
|
||||
|
||||
assert(result.updateConfigJson === null || typeof result.updateConfigJson === 'string');
|
||||
result.updateConfig = safe.JSON.parse(result.updateConfigJson);
|
||||
delete result.updateConfigJson;
|
||||
|
||||
assert(result.restoreConfigJson === null || typeof result.restoreConfigJson === 'string');
|
||||
result.restoreConfig = safe.JSON.parse(result.restoreConfigJson);
|
||||
delete result.restoreConfigJson;
|
||||
|
||||
assert(result.tagsJson === null || typeof result.tagsJson === 'string');
|
||||
result.tags = safe.JSON.parse(result.tagsJson) || [];
|
||||
delete result.tagsJson;
|
||||
|
||||
assert(result.reverseProxyConfigJson === null || typeof result.reverseProxyConfigJson === 'string');
|
||||
result.reverseProxyConfig = safe.JSON.parse(result.reverseProxyConfigJson) || {};
|
||||
delete result.reverseProxyConfigJson;
|
||||
|
||||
assert(result.hostPorts === null || typeof result.hostPorts === 'string');
|
||||
assert(result.environmentVariables === null || typeof result.environmentVariables === 'string');
|
||||
|
||||
@@ -124,11 +87,16 @@ function postProcess(result) {
|
||||
result.sso = !!result.sso; // make it bool
|
||||
result.enableBackup = !!result.enableBackup; // make it bool
|
||||
result.enableAutomaticUpdate = !!result.enableAutomaticUpdate; // make it bool
|
||||
result.proxyAuth = !!result.proxyAuth;
|
||||
|
||||
assert(result.debugModeJson === null || typeof result.debugModeJson === 'string');
|
||||
result.debugMode = safe.JSON.parse(result.debugModeJson);
|
||||
delete result.debugModeJson;
|
||||
|
||||
assert(result.servicesConfigJson === null || typeof result.servicesConfigJson === 'string');
|
||||
result.servicesConfig = safe.JSON.parse(result.servicesConfigJson) || {};
|
||||
delete result.servicesConfigJson;
|
||||
|
||||
result.alternateDomains = result.alternateDomains || [];
|
||||
result.alternateDomains.forEach(function (d) {
|
||||
delete d.appId;
|
||||
@@ -143,27 +111,40 @@ function postProcess(result) {
|
||||
if (envNames[i]) result.env[envNames[i]] = envValues[i];
|
||||
}
|
||||
|
||||
// in the db, we store dataDir as unique/nullable
|
||||
result.dataDir = result.dataDir || '';
|
||||
let volumeIds = JSON.parse(result.volumeIds);
|
||||
delete result.volumeIds;
|
||||
let volumeReadOnlys = JSON.parse(result.volumeReadOnlys);
|
||||
delete result.volumeReadOnlys;
|
||||
|
||||
result.mounts = volumeIds[0] === null ? [] : volumeIds.map((v, idx) => { return { volumeId: v, readOnly: !!volumeReadOnlys[idx] }; }); // NOTE: volumeIds is [null] when volumes of an app is empty
|
||||
|
||||
result.error = safe.JSON.parse(result.errorJson);
|
||||
delete result.errorJson;
|
||||
|
||||
result.taskId = result.taskId ? String(result.taskId) : null;
|
||||
}
|
||||
|
||||
// each query simply join apps table with another table by id. we then join the full result together
|
||||
const PB_QUERY = 'SELECT id, GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes FROM apps LEFT JOIN appPortBindings ON apps.id = appPortBindings.appId GROUP BY apps.id';
|
||||
const ENV_QUERY = 'SELECT id, JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues FROM apps LEFT JOIN appEnvVars ON apps.id = appEnvVars.appId GROUP BY apps.id';
|
||||
const SUBDOMAIN_QUERY = `SELECT id, subdomains.subdomain AS location, subdomains.domain AS domain FROM apps LEFT JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = '${exports.SUBDOMAIN_TYPE_PRIMARY}' GROUP BY apps.id`;
|
||||
const MOUNTS_QUERY = 'SELECT id, JSON_ARRAYAGG(appMounts.volumeId) AS volumeIds, JSON_ARRAYAGG(appMounts.readOnly) AS volumeReadOnlys FROM apps LEFT JOIN appMounts ON apps.id = appMounts.appId GROUP BY apps.id';
|
||||
const APPS_QUERY = `SELECT ${APPS_FIELDS_PREFIXED}, hostPorts, environmentVariables, portTypes, envNames, envValues, location, domain, volumeIds, volumeReadOnlys FROM apps`
|
||||
+ ` LEFT JOIN (${PB_QUERY}) AS q1 on q1.id = apps.id`
|
||||
+ ` LEFT JOIN (${ENV_QUERY}) AS q2 on q2.id = apps.id`
|
||||
+ ` LEFT JOIN (${SUBDOMAIN_QUERY}) AS q3 on q3.id = apps.id`
|
||||
+ ` LEFT JOIN (${MOUNTS_QUERY}) AS q4 on q4.id = apps.id`;
|
||||
|
||||
function get(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes, '
|
||||
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
|
||||
+ ' FROM apps'
|
||||
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
|
||||
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' WHERE apps.id = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, id ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
database.query(`${APPS_QUERY} WHERE apps.id = ?`, [ id ], function (error, result) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
|
||||
|
||||
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
result[0].alternateDomains = alternateDomains;
|
||||
|
||||
@@ -174,51 +155,19 @@ function get(id, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getByHttpPort(httpPort, callback) {
|
||||
assert.strictEqual(typeof httpPort, 'number');
|
||||
function getByIpAddress(ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
|
||||
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
|
||||
+ ' FROM apps'
|
||||
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
|
||||
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' WHERE httpPort = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, httpPort ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
database.query(`${APPS_QUERY} WHERE apps.containerIp = ?`, [ ip ], function (error, result) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
|
||||
|
||||
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
result[0].alternateDomains = alternateDomains;
|
||||
postProcess(result[0]);
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getByContainerId(containerId, callback) {
|
||||
assert.strictEqual(typeof containerId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
|
||||
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
|
||||
+ ' FROM apps'
|
||||
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
|
||||
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' WHERE containerId = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, containerId ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
result[0].alternateDomains = alternateDomains;
|
||||
postProcess(result[0]);
|
||||
|
||||
callback(null, result[0]);
|
||||
@@ -229,18 +178,11 @@ function getByContainerId(containerId, callback) {
|
||||
function getAll(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
|
||||
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
|
||||
+ ' FROM apps'
|
||||
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
|
||||
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' GROUP BY apps.id ORDER BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
database.query(`${APPS_QUERY} ORDER BY apps.id`, [ ], function (error, results) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE type = ?', [ exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
alternateDomains.forEach(function (d) {
|
||||
var domain = results.find(function (a) { return d.appId === a.id; });
|
||||
@@ -257,14 +199,13 @@ function getAll(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function add(id, appStoreId, manifest, location, domain, ownerId, portBindings, data, callback) {
|
||||
function add(id, appStoreId, manifest, location, domain, portBindings, data, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof appStoreId, 'string');
|
||||
assert(manifest && typeof manifest === 'object');
|
||||
assert.strictEqual(typeof manifest.version, 'string');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof ownerId, 'string');
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
assert(data && typeof data === 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -276,24 +217,26 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
|
||||
const accessRestriction = data.accessRestriction || null;
|
||||
const accessRestrictionJson = JSON.stringify(accessRestriction);
|
||||
const memoryLimit = data.memoryLimit || 0;
|
||||
const installationState = data.installationState || exports.ISTATE_PENDING_INSTALL;
|
||||
const restoreConfigJson = data.restoreConfig ? JSON.stringify(data.restoreConfig) : null; // used when cloning
|
||||
const cpuShares = data.cpuShares || 512;
|
||||
const installationState = data.installationState;
|
||||
const runState = data.runState;
|
||||
const sso = 'sso' in data ? data.sso : null;
|
||||
const robotsTxt = 'robotsTxt' in data ? data.robotsTxt : null;
|
||||
const debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null;
|
||||
const env = data.env || {};
|
||||
const label = data.label || null;
|
||||
const tagsJson = data.tags ? JSON.stringify(data.tags) : null;
|
||||
const mailboxName = data.mailboxName || null;
|
||||
const mailboxDomain = data.mailboxDomain || null;
|
||||
const reverseProxyConfigJson = data.reverseProxyConfig ? JSON.stringify(data.reverseProxyConfig) : null;
|
||||
|
||||
var queries = [];
|
||||
|
||||
queries.push({
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, '
|
||||
+ 'restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId, mailboxName, label, tagsJson) '
|
||||
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, restoreConfigJson,
|
||||
sso, debugModeJson, robotsTxt, ownerId, mailboxName, label, tagsJson ]
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, cpuShares, '
|
||||
+ 'sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson) '
|
||||
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, cpuShares,
|
||||
sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson ]
|
||||
});
|
||||
|
||||
queries.push({
|
||||
@@ -325,9 +268,9 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
|
||||
}
|
||||
|
||||
database.transaction(queries, function (error) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error.message));
|
||||
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'no such domain'));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, error.message));
|
||||
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new BoxError(BoxError.NOT_FOUND, 'no such domain'));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -338,7 +281,7 @@ function exists(id, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT 1 FROM apps WHERE id=?', [ id ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
return callback(null, result.length !== 0);
|
||||
});
|
||||
@@ -349,7 +292,7 @@ function getPortBindings(id, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + PORT_BINDINGS_FIELDS + ' FROM appPortBindings WHERE appId = ?', [ id ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
var portBindings = { };
|
||||
for (var i = 0; i < results.length; i++) {
|
||||
@@ -366,8 +309,8 @@ function delPortBinding(hostPort, type, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
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));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -381,12 +324,14 @@ function del(id, callback) {
|
||||
{ query: 'DELETE FROM subdomains WHERE appId = ?', args: [ id ] },
|
||||
{ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] },
|
||||
{ query: 'DELETE FROM appEnvVars WHERE appId = ?', args: [ id ] },
|
||||
{ query: 'DELETE FROM appPasswords WHERE identifier = ?', args: [ id ] },
|
||||
{ query: 'DELETE FROM appMounts WHERE appId = ?', args: [ id ] },
|
||||
{ query: 'DELETE FROM apps WHERE id = ?', args: [ id ] }
|
||||
];
|
||||
|
||||
database.transaction(queries, function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (results[3].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (results[5].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -402,7 +347,7 @@ function clear(callback) {
|
||||
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));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
@@ -446,22 +391,29 @@ function updateWithConstraints(id, app, constraints, callback) {
|
||||
}
|
||||
|
||||
if ('location' in app && 'domain' in app) { // must be updated together as they are unique together
|
||||
queries.push({ query: 'UPDATE subdomains SET subdomain = ?, domain = ? WHERE appId = ? AND type = ?', args: [ app.location, app.domain, id, exports.SUBDOMAIN_TYPE_PRIMARY ]});
|
||||
queries.push({ query: 'DELETE FROM subdomains WHERE appId = ?', args: [ id ]}); // all locations of an app must be updated together
|
||||
queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, app.domain, app.location, exports.SUBDOMAIN_TYPE_PRIMARY ]});
|
||||
|
||||
if ('alternateDomains' in app) {
|
||||
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 ]});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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 ]});
|
||||
if ('mounts' in app) {
|
||||
queries.push({ query: 'DELETE FROM appMounts WHERE appId = ?', args: [ id ]});
|
||||
app.mounts.forEach(function (m) {
|
||||
queries.push({ query: 'INSERT INTO appMounts (appId, volumeId, readOnly) VALUES (?, ?, ?)', args: [ id, m.volumeId, m.readOnly ]});
|
||||
});
|
||||
}
|
||||
|
||||
var fields = [ ], values = [ ];
|
||||
for (var p in app) {
|
||||
if (p === 'manifest' || p === 'oldConfig' || p === 'updateConfig' || p === 'restoreConfig' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode') {
|
||||
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig' || p === 'servicesConfig') {
|
||||
fields.push(`${p}Json = ?`);
|
||||
values.push(JSON.stringify(app[p]));
|
||||
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'env') {
|
||||
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'env' && p !== 'mounts') {
|
||||
fields.push(p + ' = ?');
|
||||
values.push(app[p]);
|
||||
}
|
||||
@@ -473,15 +425,14 @@ function updateWithConstraints(id, app, constraints, callback) {
|
||||
}
|
||||
|
||||
database.transaction(queries, function (error, results) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error.message));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (results[results.length - 1].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, error.message));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (results[results.length - 1].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
// not sure if health should influence runState
|
||||
function setHealth(appId, health, healthTime, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof health, 'string');
|
||||
@@ -490,60 +441,29 @@ function setHealth(appId, health, healthTime, callback) {
|
||||
|
||||
var values = { health, healthTime };
|
||||
|
||||
var constraints = 'AND runState NOT LIKE "pending_%" AND installationState = "installed"';
|
||||
|
||||
updateWithConstraints(appId, values, constraints, callback);
|
||||
updateWithConstraints(appId, values, '', callback);
|
||||
}
|
||||
|
||||
function setInstallationCommand(appId, installationState, values, callback) {
|
||||
function setTask(appId, values, options, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof installationState, 'string');
|
||||
|
||||
if (typeof values === 'function') {
|
||||
callback = values;
|
||||
values = { };
|
||||
} else {
|
||||
assert.strictEqual(typeof values, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
}
|
||||
|
||||
values.installationState = installationState;
|
||||
values.installationProgress = '';
|
||||
|
||||
// Rules are:
|
||||
// uninstall is allowed in any state
|
||||
// force update is allowed in any state including pending_uninstall! (for better or worse)
|
||||
// restore is allowed from installed or error state or currently restoring
|
||||
// configure is allowed in installed state or currently configuring or in error state
|
||||
// update and backup are allowed only in installed state
|
||||
|
||||
if (installationState === exports.ISTATE_PENDING_UNINSTALL || installationState === exports.ISTATE_PENDING_FORCE_UPDATE) {
|
||||
updateWithConstraints(appId, values, '', callback);
|
||||
} else if (installationState === exports.ISTATE_PENDING_RESTORE) {
|
||||
updateWithConstraints(appId, values, 'AND (installationState = "installed" OR installationState = "error" OR installationState = "pending_restore")', callback);
|
||||
} else if (installationState === exports.ISTATE_PENDING_UPDATE || installationState === exports.ISTATE_PENDING_BACKUP) {
|
||||
updateWithConstraints(appId, values, 'AND installationState = "installed"', callback);
|
||||
} else if (installationState === exports.ISTATE_PENDING_CONFIGURE) {
|
||||
updateWithConstraints(appId, values, 'AND (installationState = "installed" OR installationState = "pending_configure" OR installationState = "error")', callback);
|
||||
} else {
|
||||
callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, 'invalid installationState'));
|
||||
}
|
||||
}
|
||||
|
||||
function setRunCommand(appId, runState, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof runState, 'string');
|
||||
assert.strictEqual(typeof values, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var values = { runState: runState };
|
||||
updateWithConstraints(appId, values, 'AND runState NOT LIKE "pending_%" AND installationState = "installed"', callback);
|
||||
if (!options.requireNullTaskId) return updateWithConstraints(appId, values, '', callback);
|
||||
|
||||
if (options.requiredState === null) {
|
||||
updateWithConstraints(appId, values, 'AND taskId IS NULL', callback);
|
||||
} else {
|
||||
updateWithConstraints(appId, values, `AND taskId IS NULL AND installationState = "${options.requiredState}"`, callback);
|
||||
}
|
||||
}
|
||||
|
||||
function getAppStoreIds(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT id, appStoreId FROM apps', function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
@@ -568,7 +488,7 @@ function setAddonConfig(appId, addonId, env, callback) {
|
||||
}
|
||||
|
||||
database.query(query + queryArgs.join(','), args, function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
@@ -581,7 +501,7 @@ function unsetAddonConfig(appId, addonId, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM appAddonConfigs WHERE appId = ? AND addonId = ?', [ appId, addonId ], function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -592,7 +512,7 @@ function unsetAddonConfigByAppId(appId, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM appAddonConfigs WHERE appId = ?', [ appId ], function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -604,7 +524,7 @@ function getAddonConfig(appId, addonId, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT name, value FROM appAddonConfigs WHERE appId = ? AND addonId = ?', [ appId, addonId ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
@@ -615,7 +535,7 @@ function getAddonConfigByAppId(appId, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT name, value FROM appAddonConfigs WHERE appId = ?', [ appId ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
@@ -628,51 +548,23 @@ function getAppIdByAddonConfigValue(addonId, namePattern, value, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT appId FROM appAddonConfigs WHERE addonId = ? AND name LIKE ? AND value = ?', [ addonId, namePattern, value ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
|
||||
|
||||
callback(null, results[0].appId);
|
||||
});
|
||||
}
|
||||
|
||||
function getAddonConfigByName(appId, addonId, name, callback) {
|
||||
function getAddonConfigByName(appId, addonId, namePattern, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof addonId, 'string');
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof namePattern, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT value FROM appAddonConfigs WHERE appId = ? AND addonId = ? AND name = ?', [ appId, addonId, name ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
database.query('SELECT value FROM appAddonConfigs WHERE appId = ? AND addonId = ? AND name LIKE ?', [ appId, addonId, namePattern ], function (error, results) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
|
||||
|
||||
callback(null, results[0].value);
|
||||
});
|
||||
}
|
||||
|
||||
function setOwner(appId, ownerId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof ownerId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('UPDATE apps SET ownerId=? WHERE appId=?', [ ownerId, appId ], function (error, results) {
|
||||
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such user'));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such app'));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function transferOwnership(oldOwnerId, newOwnerId, callback) {
|
||||
assert.strictEqual(typeof oldOwnerId, 'string');
|
||||
assert.strictEqual(typeof newOwnerId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('UPDATE apps SET ownerId=? WHERE ownerId=?', [ newOwnerId, oldOwnerId ], function (error) {
|
||||
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such user'));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
+23
-37
@@ -5,7 +5,7 @@ var appdb = require('./appdb.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
auditSource = require('./auditsource.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
debug = require('debug')('box:apphealthmonitor'),
|
||||
docker = require('./docker.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
@@ -14,7 +14,7 @@ var appdb = require('./appdb.js'),
|
||||
util = require('util');
|
||||
|
||||
exports = module.exports = {
|
||||
run: run
|
||||
run
|
||||
};
|
||||
|
||||
const HEALTHCHECK_INTERVAL = 10 * 1000; // every 10 seconds. this needs to be small since the UI makes only healthy apps clickable
|
||||
@@ -26,7 +26,7 @@ let gLastOomMailTime = Date.now() - (5 * 60 * 1000); // pretend we sent email 5
|
||||
function debugApp(app) {
|
||||
assert(typeof app === 'object');
|
||||
|
||||
debug(app.fqdn + ' ' + app.manifest.id + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)) + ' - ' + app.id);
|
||||
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)) + ' - ' + app.id);
|
||||
}
|
||||
|
||||
function setHealth(app, health, callback) {
|
||||
@@ -36,16 +36,16 @@ function setHealth(app, health, callback) {
|
||||
|
||||
let now = new Date(), healthTime = app.healthTime, curHealth = app.health;
|
||||
|
||||
if (health === appdb.HEALTH_HEALTHY) {
|
||||
if (health === apps.HEALTH_HEALTHY) {
|
||||
healthTime = now;
|
||||
if (curHealth && curHealth !== appdb.HEALTH_HEALTHY) { // app starts out with null health
|
||||
if (curHealth && curHealth !== apps.HEALTH_HEALTHY) { // app starts out with null health
|
||||
debugApp(app, 'app switched from %s to healthy', curHealth);
|
||||
|
||||
// do not send mails for dev apps
|
||||
if (!app.debugMode) eventlog.add(eventlog.ACTION_APP_UP, auditSource.HEALTH_MONITOR, { app: app });
|
||||
}
|
||||
} else if (Math.abs(now - healthTime) > UNHEALTHY_THRESHOLD) {
|
||||
if (curHealth === appdb.HEALTH_HEALTHY) {
|
||||
if (curHealth === apps.HEALTH_HEALTHY) {
|
||||
debugApp(app, 'marking as unhealthy since not seen for more than %s minutes', UNHEALTHY_THRESHOLD/(60 * 1000));
|
||||
|
||||
// do not send mails for dev apps
|
||||
@@ -57,7 +57,7 @@ function setHealth(app, health, callback) {
|
||||
}
|
||||
|
||||
appdb.setHealth(app.id, health, healthTime, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null); // app uninstalled?
|
||||
if (error && error.reason === BoxError.NOT_FOUND) return callback(null); // app uninstalled?
|
||||
if (error) return callback(error);
|
||||
|
||||
app.health = health;
|
||||
@@ -72,44 +72,33 @@ function checkAppHealth(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
|
||||
debugApp(app, 'skipped. istate:%s rstate:%s', app.installationState, app.runState);
|
||||
if (app.installationState !== apps.ISTATE_INSTALLED || app.runState !== apps.RSTATE_RUNNING) {
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
const manifest = app.manifest;
|
||||
|
||||
docker.inspect(app.containerId, function (error, data) {
|
||||
if (error || !data || !data.State) {
|
||||
debugApp(app, 'Error inspecting container');
|
||||
return setHealth(app, appdb.HEALTH_ERROR, callback);
|
||||
}
|
||||
|
||||
if (data.State.Running !== true) {
|
||||
debugApp(app, 'exited');
|
||||
return setHealth(app, appdb.HEALTH_DEAD, callback);
|
||||
}
|
||||
if (error || !data || !data.State) return setHealth(app, apps.HEALTH_ERROR, callback);
|
||||
if (data.State.Running !== true) return setHealth(app, apps.HEALTH_DEAD, callback);
|
||||
|
||||
// non-appstore apps may not have healthCheckPath
|
||||
if (!manifest.healthCheckPath) return setHealth(app, appdb.HEALTH_HEALTHY, callback);
|
||||
if (!manifest.healthCheckPath) return setHealth(app, apps.HEALTH_HEALTHY, callback);
|
||||
|
||||
// poll through docker network instead of nginx to bypass any potential oauth proxy
|
||||
var healthCheckUrl = 'http://127.0.0.1:' + app.httpPort + manifest.healthCheckPath;
|
||||
const healthCheckUrl = `http://${app.containerIp}:${manifest.httpPort}${manifest.healthCheckPath}`;
|
||||
superagent
|
||||
.get(healthCheckUrl)
|
||||
.set('Host', app.fqdn) // required for some apache configs with rewrite rules
|
||||
.set('User-Agent', 'Mozilla') // required for some apps (e.g. minio)
|
||||
.set('User-Agent', 'Mozilla (CloudronHealth)') // required for some apps (e.g. minio)
|
||||
.redirects(0)
|
||||
.timeout(HEALTHCHECK_INTERVAL)
|
||||
.end(function (error, res) {
|
||||
if (error && !error.response) {
|
||||
debugApp(app, 'not alive (network error): %s', error.message);
|
||||
setHealth(app, appdb.HEALTH_UNHEALTHY, callback);
|
||||
} else if (res.statusCode >= 400) { // 2xx and 3xx are ok
|
||||
debugApp(app, 'not alive : %s', error || res.status);
|
||||
setHealth(app, appdb.HEALTH_UNHEALTHY, callback);
|
||||
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
|
||||
} else if (res.statusCode >= 403) { // 2xx and 3xx are ok. even 401 and 403 are ok for now (for WP sites)
|
||||
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
|
||||
} else {
|
||||
setHealth(app, appdb.HEALTH_HEALTHY, callback);
|
||||
setHealth(app, apps.HEALTH_HEALTHY, callback);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -180,17 +169,14 @@ function processDockerEvents(intervalSecs, callback) {
|
||||
function processApp(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
apps.getAll(function (error, result) {
|
||||
apps.getAll(function (error, allApps) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.each(result, checkAppHealth, function (error) {
|
||||
if (error) console.error(error);
|
||||
async.each(allApps, checkAppHealth, function (error) {
|
||||
const alive = allApps
|
||||
.filter(function (a) { return a.installationState === apps.ISTATE_INSTALLED && a.runState === apps.RSTATE_RUNNING && a.health === apps.HEALTH_HEALTHY; });
|
||||
|
||||
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);
|
||||
debug(`app health: ${alive.length} alive / ${allApps.length - alive.length} dead.` + (error ? ` ${error.reason}` : ''));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -205,7 +191,7 @@ function run(intervalSecs, callback) {
|
||||
processApp, // this is first because docker.getEvents seems to get 'stuck' sometimes
|
||||
processDockerEvents.bind(null, intervalSecs)
|
||||
], function (error) {
|
||||
if (error) debug(error);
|
||||
if (error) debug(`run: could not check app health. ${error.message}`);
|
||||
|
||||
callback();
|
||||
});
|
||||
|
||||
+1378
-797
File diff suppressed because it is too large
Load Diff
+242
-228
@@ -1,83 +1,85 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
getFeatures: getFeatures,
|
||||
|
||||
getApps: getApps,
|
||||
getApp: getApp,
|
||||
getAppVersion: getAppVersion,
|
||||
|
||||
trackBeginSetup: trackBeginSetup,
|
||||
trackFinishedSetup: trackFinishedSetup,
|
||||
|
||||
registerWithLoginCredentials: registerWithLoginCredentials,
|
||||
registerWithLicense: registerWithLicense,
|
||||
|
||||
purchaseApp: purchaseApp,
|
||||
unpurchaseApp: unpurchaseApp,
|
||||
|
||||
getUserToken: getUserToken,
|
||||
getSubscription: getSubscription,
|
||||
isFreePlan: isFreePlan,
|
||||
|
||||
sendAliveStatus: sendAliveStatus,
|
||||
|
||||
getAppUpdate: getAppUpdate,
|
||||
getBoxUpdate: getBoxUpdate,
|
||||
|
||||
createTicket: createTicket,
|
||||
|
||||
AppstoreError: AppstoreError
|
||||
createTicket: createTicket
|
||||
};
|
||||
|
||||
var apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
custom = require('./custom.js'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:appstore'),
|
||||
domains = require('./domains.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
mail = require('./mail.js'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
settings = require('./settings.js'),
|
||||
superagent = require('superagent'),
|
||||
support = require('./support.js'),
|
||||
util = require('util');
|
||||
|
||||
function AppstoreError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
// These are the default options and will be adjusted once a subscription state is obtained
|
||||
// Keep in sync with appstore/routes/cloudrons.js
|
||||
let gFeatures = {
|
||||
userMaxCount: 5,
|
||||
domainMaxCount: 1,
|
||||
externalLdap: false,
|
||||
privateDockerRegistry: false,
|
||||
branding: false,
|
||||
support: false,
|
||||
directoryConfig: false,
|
||||
mailboxMaxCount: 5,
|
||||
emailPremium: false
|
||||
};
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
// attempt to load feature cache in case appstore would be down
|
||||
let tmp = safe.JSON.parse(safe.fs.readFileSync(paths.FEATURES_INFO_FILE, 'utf8'));
|
||||
if (tmp) gFeatures = tmp;
|
||||
|
||||
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;
|
||||
}
|
||||
function getFeatures() {
|
||||
return gFeatures;
|
||||
}
|
||||
util.inherits(AppstoreError, Error);
|
||||
AppstoreError.INTERNAL_ERROR = 'Internal Error';
|
||||
AppstoreError.EXTERNAL_ERROR = 'External Error';
|
||||
AppstoreError.ALREADY_EXISTS = 'Already Exists';
|
||||
AppstoreError.ACCESS_DENIED = 'Access Denied';
|
||||
AppstoreError.NOT_FOUND = 'Not Found';
|
||||
AppstoreError.PLAN_LIMIT = 'Plan limit reached'; // upstream 402 (subsciption_expired and subscription_required)
|
||||
AppstoreError.LICENSE_ERROR = 'License Error'; // upstream 422 (no license, invalid license)
|
||||
AppstoreError.INVALID_TOKEN = 'Invalid token'; // upstream 401 (invalid token)
|
||||
AppstoreError.NOT_REGISTERED = 'Not registered'; // upstream 412 (no token, not set yet)
|
||||
AppstoreError.ALREADY_REGISTERED = 'Already registered';
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
function isAppAllowed(appstoreId, listingConfig) {
|
||||
assert.strictEqual(typeof listingConfig, 'object');
|
||||
assert.strictEqual(typeof appstoreId, 'string');
|
||||
|
||||
if (listingConfig.blacklist && listingConfig.blacklist.includes(appstoreId)) return false;
|
||||
|
||||
if (listingConfig.whitelist) return listingConfig.whitelist.includes(appstoreId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getCloudronToken(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.getCloudronToken(function (error, token) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
if (!token) return callback(new AppstoreError(AppstoreError.NOT_REGISTERED));
|
||||
if (error) return callback(error);
|
||||
if (!token) return callback(new BoxError(BoxError.LICENSE_ERROR, 'Missing token'));
|
||||
|
||||
callback(null, token);
|
||||
});
|
||||
@@ -95,11 +97,11 @@ function login(email, password, totpToken, callback) {
|
||||
totpToken: totpToken
|
||||
};
|
||||
|
||||
const url = config.apiServerOrigin() + '/api/v1/login';
|
||||
const url = settings.apiServerOrigin() + '/api/v1/login';
|
||||
superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.ACCESS_DENIED));
|
||||
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, `login status code: ${result.statusCode}`));
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `login status code: ${result.statusCode}`));
|
||||
|
||||
callback(null, result.body); // { userId, accessToken }
|
||||
});
|
||||
@@ -115,31 +117,54 @@ function registerUser(email, password, callback) {
|
||||
password: password,
|
||||
};
|
||||
|
||||
const url = config.apiServerOrigin() + '/api/v1/register_user';
|
||||
const url = settings.apiServerOrigin() + '/api/v1/register_user';
|
||||
superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
if (result.statusCode === 409) return callback(new AppstoreError(AppstoreError.ALREADY_EXISTS));
|
||||
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, `register status code: ${result.statusCode}`));
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 409) return callback(new BoxError(BoxError.ALREADY_EXISTS, error.message));
|
||||
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `register status code: ${result.statusCode}`));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function getUserToken(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (settings.isDemo()) return callback(new BoxError(BoxError.BAD_FIELD, 'Not allowed in demo mode'));
|
||||
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const url = `${settings.apiServerOrigin()}/api/v1/user_token`;
|
||||
|
||||
superagent.post(url).send({}).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `getUserToken status code: ${result.status}`));
|
||||
|
||||
callback(null, result.body.accessToken);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getSubscription(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const url = config.apiServerOrigin() + '/api/v1/subscription';
|
||||
const url = settings.apiServerOrigin() + '/api/v1/subscription';
|
||||
superagent.get(url).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
|
||||
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR));
|
||||
if (result.statusCode === 502) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, `Stripe error: ${error.message}`));
|
||||
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, `Unknown error: ${error.message}`));
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR));
|
||||
if (result.statusCode === 502) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Stripe error: ${error.message}`));
|
||||
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Unknown error: ${error.message}`));
|
||||
|
||||
callback(null, result.body); // { email, subscription }
|
||||
// update the features cache
|
||||
gFeatures = result.body.features;
|
||||
safe.fs.writeFileSync(paths.FEATURES_INFO_FILE, JSON.stringify(gFeatures), 'utf8');
|
||||
|
||||
callback(null, result.body);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -158,16 +183,16 @@ function purchaseApp(data, callback) {
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const url = `${config.apiServerOrigin()}/api/v1/cloudronapps`;
|
||||
const url = `${settings.apiServerOrigin()}/api/v1/cloudronapps`;
|
||||
|
||||
superagent.post(url).send(data).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND)); // appstoreId does not exist
|
||||
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
|
||||
if (result.statusCode === 402) return callback(new AppstoreError(AppstoreError.PLAN_LIMIT, result.body.message));
|
||||
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR, result.body.message));
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND)); // appstoreId does not exist
|
||||
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (result.statusCode === 402) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
|
||||
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
|
||||
// 200 if already purchased, 201 is newly purchased
|
||||
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
|
||||
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -183,19 +208,19 @@ function unpurchaseApp(appId, data, callback) {
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const url = `${config.apiServerOrigin()}/api/v1/cloudronapps/${appId}`;
|
||||
const url = `${settings.apiServerOrigin()}/api/v1/cloudronapps/${appId}`;
|
||||
|
||||
superagent.get(url).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 404) return callback(null); // was never purchased
|
||||
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
|
||||
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR, result.body.message));
|
||||
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
|
||||
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
|
||||
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
|
||||
|
||||
superagent.del(url).send(data).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
|
||||
if (result.statusCode !== 204) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error));
|
||||
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (result.statusCode !== 204) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -203,143 +228,70 @@ function unpurchaseApp(appId, data, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function sendAliveStatus(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
var allSettings, allDomains, mailDomains, loginEvents;
|
||||
|
||||
async.series([
|
||||
function (callback) {
|
||||
settings.getAll(function (error, result) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
allSettings = result;
|
||||
callback();
|
||||
});
|
||||
},
|
||||
function (callback) {
|
||||
domains.getAll(function (error, result) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
allDomains = result;
|
||||
callback();
|
||||
});
|
||||
},
|
||||
function (callback) {
|
||||
mail.getDomains(function (error, result) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
mailDomains = result;
|
||||
callback();
|
||||
});
|
||||
},
|
||||
function (callback) {
|
||||
eventlog.getAllPaged([ eventlog.ACTION_USER_LOGIN ], null, 1, 1, function (error, result) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
loginEvents = result;
|
||||
callback();
|
||||
});
|
||||
}
|
||||
], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var backendSettings = {
|
||||
backupConfig: {
|
||||
provider: allSettings[settings.BACKUP_CONFIG_KEY].provider,
|
||||
hardlinks: !allSettings[settings.BACKUP_CONFIG_KEY].noHardlinks
|
||||
},
|
||||
domainConfig: {
|
||||
count: allDomains.length,
|
||||
domains: Array.from(new Set(allDomains.map(function (d) { return { domain: d.domain, provider: d.provider }; })))
|
||||
},
|
||||
mailConfig: {
|
||||
outboundCount: mailDomains.length,
|
||||
inboundCount: mailDomains.filter(function (d) { return d.enabled; }).length,
|
||||
catchAllCount: mailDomains.filter(function (d) { return d.catchAll.length !== 0; }).length,
|
||||
relayProviders: Array.from(new Set(mailDomains.map(function (d) { return d.relay.provider; })))
|
||||
},
|
||||
appAutoupdatePattern: allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY],
|
||||
boxAutoupdatePattern: allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY],
|
||||
timeZone: allSettings[settings.TIME_ZONE_KEY],
|
||||
};
|
||||
|
||||
var data = {
|
||||
version: config.version(),
|
||||
adminFqdn: config.adminFqdn(),
|
||||
provider: config.provider(),
|
||||
backendSettings: backendSettings,
|
||||
machine: {
|
||||
cpus: os.cpus(),
|
||||
totalmem: os.totalmem()
|
||||
},
|
||||
events: {
|
||||
lastLogin: loginEvents[0] ? (new Date(loginEvents[0].creationTime).getTime()) : 0
|
||||
}
|
||||
};
|
||||
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const url = `${config.apiServerOrigin()}/api/v1/alive`;
|
||||
superagent.post(url).send(data).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
|
||||
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
|
||||
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR, result.body.message));
|
||||
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Sending alive status failed. %s %j', result.status, result.body)));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getBoxUpdate(callback) {
|
||||
function getBoxUpdate(options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const url = `${config.apiServerOrigin()}/api/v1/boxupdate`;
|
||||
const url = `${settings.apiServerOrigin()}/api/v1/boxupdate`;
|
||||
|
||||
superagent.get(url).query({ accessToken: token, boxVersion: config.version() }).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
|
||||
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR, result.body.message));
|
||||
const query = {
|
||||
accessToken: token,
|
||||
boxVersion: constants.VERSION,
|
||||
automatic: options.automatic
|
||||
};
|
||||
|
||||
superagent.get(url).query(query).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
|
||||
if (result.statusCode === 204) return callback(null); // no update
|
||||
if (result.statusCode !== 200 || !result.body) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
if (result.statusCode !== 200 || !result.body) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
|
||||
var updateInfo = result.body;
|
||||
|
||||
if (!semver.valid(updateInfo.version) || semver.gt(config.version(), updateInfo.version)) {
|
||||
return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Invalid update version: %s %s', result.statusCode, result.text)));
|
||||
if (!semver.valid(updateInfo.version) || semver.gt(constants.VERSION, updateInfo.version)) {
|
||||
return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Invalid update version: %s %s', result.statusCode, result.text)));
|
||||
}
|
||||
|
||||
// updateInfo: { version, changelog, sourceTarballUrl, sourceTarballSigUrl, boxVersionsUrl, boxVersionsSigUrl }
|
||||
if (!updateInfo.version || typeof updateInfo.version !== 'string') return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response (bad version): %s %s', result.statusCode, result.text)));
|
||||
if (!updateInfo.changelog || !Array.isArray(updateInfo.changelog)) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response (bad version): %s %s', result.statusCode, result.text)));
|
||||
if (!updateInfo.sourceTarballUrl || typeof updateInfo.sourceTarballUrl !== 'string') return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response (bad sourceTarballUrl): %s %s', result.statusCode, result.text)));
|
||||
if (!updateInfo.sourceTarballSigUrl || typeof updateInfo.sourceTarballSigUrl !== 'string') return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response (bad sourceTarballSigUrl): %s %s', result.statusCode, result.text)));
|
||||
if (!updateInfo.boxVersionsUrl || typeof updateInfo.boxVersionsUrl !== 'string') return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response (bad boxVersionsUrl): %s %s', result.statusCode, result.text)));
|
||||
if (!updateInfo.boxVersionsSigUrl || typeof updateInfo.boxVersionsSigUrl !== 'string') return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response (bad boxVersionsSigUrl): %s %s', result.statusCode, result.text)));
|
||||
if (!updateInfo.version || typeof updateInfo.version !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad version): %s %s', result.statusCode, result.text)));
|
||||
if (!updateInfo.changelog || !Array.isArray(updateInfo.changelog)) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad version): %s %s', result.statusCode, result.text)));
|
||||
if (!updateInfo.sourceTarballUrl || typeof updateInfo.sourceTarballUrl !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad sourceTarballUrl): %s %s', result.statusCode, result.text)));
|
||||
if (!updateInfo.sourceTarballSigUrl || typeof updateInfo.sourceTarballSigUrl !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad sourceTarballSigUrl): %s %s', result.statusCode, result.text)));
|
||||
if (!updateInfo.boxVersionsUrl || typeof updateInfo.boxVersionsUrl !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad boxVersionsUrl): %s %s', result.statusCode, result.text)));
|
||||
if (!updateInfo.boxVersionsSigUrl || typeof updateInfo.boxVersionsSigUrl !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad boxVersionsSigUrl): %s %s', result.statusCode, result.text)));
|
||||
|
||||
callback(null, updateInfo);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getAppUpdate(app, callback) {
|
||||
function getAppUpdate(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const url = `${config.apiServerOrigin()}/api/v1/appupdate`;
|
||||
const url = `${settings.apiServerOrigin()}/api/v1/appupdate`;
|
||||
const query = {
|
||||
accessToken: token,
|
||||
boxVersion: constants.VERSION,
|
||||
appId: app.appStoreId,
|
||||
appVersion: app.manifest.version,
|
||||
automatic: options.automatic
|
||||
};
|
||||
|
||||
superagent.get(url).query({ accessToken: token, boxVersion: config.version(), appId: app.appStoreId, appVersion: app.manifest.version }).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
|
||||
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR, result.body.message));
|
||||
superagent.get(url).query(query).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error));
|
||||
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
|
||||
if (result.statusCode === 204) return callback(null); // no update
|
||||
if (result.statusCode !== 200 || !result.body) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
if (result.statusCode !== 200 || !result.body) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
|
||||
const updateInfo = result.body;
|
||||
|
||||
@@ -349,10 +301,12 @@ function getAppUpdate(app, callback) {
|
||||
// do some sanity checks
|
||||
if (!safe.query(updateInfo, 'manifest.version') || semver.gt(curAppVersion, safe.query(updateInfo, 'manifest.version'))) {
|
||||
debug('Skipping malformed update of app %s version: %s. got %j', app.id, curAppVersion, updateInfo);
|
||||
return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Malformed update: %s %s', result.statusCode, result.text)));
|
||||
return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Malformed update: %s %s', result.statusCode, result.text)));
|
||||
}
|
||||
|
||||
// { id, creationDate, manifest }
|
||||
updateInfo.unstable = !!updateInfo.unstable;
|
||||
|
||||
// { id, creationDate, manifest, unstable }
|
||||
callback(null, updateInfo);
|
||||
});
|
||||
});
|
||||
@@ -362,23 +316,23 @@ function registerCloudron(data, callback) {
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const url = `${config.apiServerOrigin()}/api/v1/register_cloudron`;
|
||||
const url = `${settings.apiServerOrigin()}/api/v1/register_cloudron`;
|
||||
|
||||
superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, `Unable to register cloudron: ${error.message}`));
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Unable to register cloudron: ${result.statusCode} ${error.message}`));
|
||||
|
||||
// cloudronId, token, licenseKey
|
||||
if (!result.body.cloudronId) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, 'Invalid response - no cloudron id'));
|
||||
if (!result.body.cloudronToken) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, 'Invalid response - no token'));
|
||||
if (!result.body.licenseKey) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, 'Invalid response - no license'));
|
||||
if (!result.body.cloudronId) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no cloudron id'));
|
||||
if (!result.body.cloudronToken) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no token'));
|
||||
if (!result.body.licenseKey) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no license'));
|
||||
|
||||
async.series([
|
||||
settings.setCloudronId.bind(null, result.body.cloudronId),
|
||||
settings.setCloudronToken.bind(null, result.body.cloudronToken),
|
||||
settings.setLicenseKey.bind(null, result.body.licenseKey),
|
||||
], function (error) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(error);
|
||||
|
||||
debug(`registerCloudron: Cloudron registered with id ${result.body.cloudronId}`);
|
||||
|
||||
@@ -387,15 +341,30 @@ function registerCloudron(data, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function registerWithLicense(license, domain, callback) {
|
||||
assert.strictEqual(typeof license, 'string');
|
||||
// This works without a Cloudron token as this Cloudron was not yet registered
|
||||
let gBeginSetupAlreadyTracked = false;
|
||||
function trackBeginSetup() {
|
||||
// avoid browser reload double tracking, not perfect since box might restart, but covers most cases and is simple
|
||||
if (gBeginSetupAlreadyTracked) return;
|
||||
gBeginSetupAlreadyTracked = true;
|
||||
|
||||
const url = `${settings.apiServerOrigin()}/api/v1/helper/setup_begin`;
|
||||
|
||||
superagent.post(url).send({}).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return debug(`trackBeginSetup: ${error.message}`);
|
||||
if (result.statusCode !== 200) return debug(`trackBeginSetup: ${result.statusCode} ${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
// This works without a Cloudron token as this Cloudron was not yet registered
|
||||
function trackFinishedSetup(domain) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getCloudronToken(function (error, token) {
|
||||
if (token) return callback(new AppstoreError(AppstoreError.ALREADY_REGISTERED));
|
||||
const url = `${settings.apiServerOrigin()}/api/v1/helper/setup_finished`;
|
||||
|
||||
registerCloudron({ license, domain }, callback);
|
||||
superagent.post(url).send({ domain }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return debug(`trackFinishedSetup: ${error.message}`);
|
||||
if (result.statusCode !== 200) return debug(`trackFinishedSetup: ${result.statusCode} ${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -410,7 +379,7 @@ function registerWithLoginCredentials(options, callback) {
|
||||
}
|
||||
|
||||
getCloudronToken(function (error, token) {
|
||||
if (token) return callback(new AppstoreError(AppstoreError.ALREADY_REGISTERED));
|
||||
if (token) return callback(new BoxError(BoxError.CONFLICT, 'Cloudron is already registered'));
|
||||
|
||||
maybeSignup(function (error) {
|
||||
if (error) return callback(error);
|
||||
@@ -418,19 +387,20 @@ function registerWithLoginCredentials(options, callback) {
|
||||
login(options.email, options.password, options.totpToken || '', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
registerCloudron({ domain: config.adminDomain(), accessToken: result.accessToken }, callback);
|
||||
registerCloudron({ domain: settings.adminDomain(), accessToken: result.accessToken, version: constants.VERSION, purpose: options.purpose || '' }, callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createTicket(info, callback) {
|
||||
function createTicket(info, auditSource, callback) {
|
||||
assert.strictEqual(typeof info, 'object');
|
||||
assert.strictEqual(typeof info.email, 'string');
|
||||
assert.strictEqual(typeof info.displayName, 'string');
|
||||
assert.strictEqual(typeof info.type, 'string');
|
||||
assert.strictEqual(typeof info.subject, 'string');
|
||||
assert.strictEqual(typeof info.description, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
function collectAppInfoIfNeeded(callback) {
|
||||
@@ -438,24 +408,55 @@ function createTicket(info, callback) {
|
||||
apps.get(info.appId, callback);
|
||||
}
|
||||
|
||||
function enableSshIfNeeded(callback) {
|
||||
if (!info.enableSshSupport) return callback();
|
||||
|
||||
support.enableRemoteSupport(true, auditSource, function (error) {
|
||||
// ensure we can at least get the ticket through
|
||||
if (error) debug('Unable to enable SSH support.', error);
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
collectAppInfoIfNeeded(function (error, result) {
|
||||
if (error) console.error('Unable to get app info', error);
|
||||
if (result) info.app = result;
|
||||
enableSshIfNeeded(function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
let url = config.apiServerOrigin() + '/api/v1/ticket';
|
||||
collectAppInfoIfNeeded(function (error, app) {
|
||||
if (error) return callback(error);
|
||||
if (app) info.app = app;
|
||||
|
||||
info.supportEmail = custom.spec().support.email; // destination address for tickets
|
||||
info.supportEmail = constants.SUPPORT_EMAIL; // destination address for tickets
|
||||
|
||||
superagent.post(url).query({ accessToken: token }).send(info).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
|
||||
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR, result.body.message));
|
||||
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
var req = superagent.post(`${settings.apiServerOrigin()}/api/v1/ticket`)
|
||||
.query({ accessToken: token })
|
||||
.timeout(30 * 1000);
|
||||
|
||||
callback(null);
|
||||
// either send as JSON through body or as multipart, depending on attachments
|
||||
if (info.app) {
|
||||
req.field('infoJSON', JSON.stringify(info));
|
||||
|
||||
apps.getLocalLogfilePaths(info.app).forEach(function (filePath) {
|
||||
var logs = safe.child_process.execSync(`tail --lines=1000 ${filePath}`);
|
||||
if (logs) req.attach(path.basename(filePath), logs, path.basename(filePath));
|
||||
});
|
||||
} else {
|
||||
req.send(info);
|
||||
}
|
||||
|
||||
req.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
|
||||
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
|
||||
eventlog.add(eventlog.ACTION_SUPPORT_TICKET, auditSource, info);
|
||||
|
||||
callback(null, { message: `An email was sent to ${constants.SUPPORT_EMAIL}. We will get back shortly!` });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -468,16 +469,23 @@ function getApps(callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
settings.getUnstableAppsConfig(function (error, unstable) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
const url = `${config.apiServerOrigin()}/api/v1/apps`;
|
||||
superagent.get(url).query({ accessToken: token, boxVersion: config.version(), unstable: unstable }).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
|
||||
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR, result.body.message));
|
||||
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App listing failed. %s %j', result.status, result.body)));
|
||||
if (!result.body.apps) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, result.body.apps);
|
||||
const url = `${settings.apiServerOrigin()}/api/v1/apps`;
|
||||
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION, unstable: unstable }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
|
||||
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App listing failed. %s %j', result.status, result.body)));
|
||||
if (!result.body.apps) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
|
||||
settings.getAppstoreListingConfig(function (error, listingConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const filteredApps = result.body.apps.filter(app => isAppAllowed(app.id, listingConfig));
|
||||
|
||||
callback(null, filteredApps);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -488,20 +496,26 @@ function getAppVersion(appId, version, callback) {
|
||||
assert.strictEqual(typeof version, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getCloudronToken(function (error, token) {
|
||||
settings.getAppstoreListingConfig(function (error, listingConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
let url = `${config.apiServerOrigin()}/api/v1/apps/${appId}`;
|
||||
if (version !== 'latest') url += `/versions/${version}`;
|
||||
if (!isAppAllowed(appId, listingConfig)) return callback(new BoxError(BoxError.FEATURE_DISABLED));
|
||||
|
||||
superagent.get(url).query({ accessToken: token }).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
|
||||
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
|
||||
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR, result.body.message));
|
||||
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App fetch failed. %s %j', result.status, result.body)));
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, result.body);
|
||||
let url = `${settings.apiServerOrigin()}/api/v1/apps/${appId}`;
|
||||
if (version !== 'latest') url += `/versions/${version}`;
|
||||
|
||||
superagent.get(url).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
|
||||
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App fetch failed. %s %j', result.status, result.body)));
|
||||
|
||||
callback(null, result.body);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+582
-477
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,96 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
scheduleTask
|
||||
};
|
||||
|
||||
let assert = require('assert'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
debug = require('debug')('box:apptaskmanager'),
|
||||
fs = require('fs'),
|
||||
locker = require('./locker.js'),
|
||||
safe = require('safetydance'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
scheduler = require('./scheduler.js'),
|
||||
sftp = require('./sftp.js'),
|
||||
tasks = require('./tasks.js');
|
||||
|
||||
let gActiveTasks = { }; // indexed by app id
|
||||
let gPendingTasks = [ ];
|
||||
let gInitialized = false;
|
||||
|
||||
const TASK_CONCURRENCY = 3;
|
||||
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
function waitText(lockOperation) {
|
||||
if (lockOperation === locker.OP_BOX_UPDATE) return 'Waiting for Cloudron to finish updating. See the Settings view';
|
||||
if (lockOperation === locker.OP_PLATFORM_START) return 'Waiting for Cloudron to initialize';
|
||||
if (lockOperation === locker.OP_FULL_BACKUP) return 'Wait for Cloudron to finish backup. See the Backups view';
|
||||
|
||||
return ''; // cannot happen
|
||||
}
|
||||
|
||||
function initializeSync() {
|
||||
gInitialized = true;
|
||||
locker.on('unlocked', startNextTask);
|
||||
}
|
||||
|
||||
// callback is called when task is finished
|
||||
function scheduleTask(appId, taskId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof taskId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!gInitialized) initializeSync();
|
||||
|
||||
if (appId in gActiveTasks) {
|
||||
return callback(new BoxError(BoxError.CONFLICT, `Task for %s is already active: ${appId}`));
|
||||
}
|
||||
|
||||
if (Object.keys(gActiveTasks).length >= TASK_CONCURRENCY) {
|
||||
debug(`Reached concurrency limit, queueing task id ${taskId}`);
|
||||
tasks.update(taskId, { percent: 1, message: 'Waiting for other app tasks to complete' }, NOOP_CALLBACK);
|
||||
gPendingTasks.push({ appId, taskId, callback });
|
||||
return;
|
||||
}
|
||||
|
||||
var lockError = locker.recursiveLock(locker.OP_APPTASK);
|
||||
|
||||
if (lockError) {
|
||||
debug(`Could not get lock. ${lockError.message}, queueing task id ${taskId}`);
|
||||
tasks.update(taskId, { percent: 1, message: waitText(lockError.operation) }, NOOP_CALLBACK);
|
||||
gPendingTasks.push({ appId, taskId, callback });
|
||||
return;
|
||||
}
|
||||
|
||||
gActiveTasks[appId] = {};
|
||||
|
||||
const logFile = path.join(paths.LOG_DIR, appId, 'apptask.log');
|
||||
|
||||
if (!fs.existsSync(path.dirname(logFile))) safe.fs.mkdirSync(path.dirname(logFile)); // ensure directory
|
||||
|
||||
scheduler.suspendJobs(appId);
|
||||
|
||||
// TODO: set memory limit for app backup task
|
||||
tasks.startTask(taskId, { logFile, timeout: 20 * 60 * 60 * 1000 /* 20 hours */, nice: 15 }, function (error, result) {
|
||||
callback(error, result);
|
||||
|
||||
delete gActiveTasks[appId];
|
||||
locker.unlock(locker.OP_APPTASK); // unlock event will trigger next task
|
||||
|
||||
// post app task hooks
|
||||
sftp.rebuild(error => { if (error) debug('Unable to rebuild sftp:', error); });
|
||||
scheduler.resumeJobs(appId);
|
||||
});
|
||||
}
|
||||
|
||||
function startNextTask() {
|
||||
if (gPendingTasks.length === 0) return;
|
||||
|
||||
assert(Object.keys(gActiveTasks).length < TASK_CONCURRENCY);
|
||||
|
||||
const t = gPendingTasks.shift();
|
||||
scheduleTask(t.appId, t.taskId, t.callback);
|
||||
}
|
||||
|
||||
+2
-2
@@ -3,9 +3,9 @@
|
||||
exports = module.exports = {
|
||||
CRON: { userId: null, username: 'cron' },
|
||||
HEALTH_MONITOR: { userId: null, username: 'healthmonitor' },
|
||||
SYSADMIN: { userId: null, username: 'sysadmin' },
|
||||
TASK_MANAGER: { userId: null, username: 'taskmanager' },
|
||||
APP_TASK: { userId: null, username: 'apptask' },
|
||||
EXTERNAL_LDAP_TASK: { userId: null, username: 'externalldap' },
|
||||
EXTERNAL_LDAP_AUTO_CREATE: { userId: null, username: 'externalldap' },
|
||||
|
||||
fromRequest: fromRequest
|
||||
};
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
get: get,
|
||||
add: add,
|
||||
del: del,
|
||||
delExpired: delExpired,
|
||||
|
||||
_clear: clear
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror');
|
||||
|
||||
var AUTHCODES_FIELDS = [ 'authCode', 'userId', 'clientId', 'expiresAt' ].join(',');
|
||||
|
||||
function get(authCode, callback) {
|
||||
assert.strictEqual(typeof authCode, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + AUTHCODES_FIELDS + ' FROM authcodes WHERE authCode = ? AND expiresAt > ?', [ authCode, Date.now() ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function add(authCode, clientId, userId, expiresAt, callback) {
|
||||
assert.strictEqual(typeof authCode, 'string');
|
||||
assert.strictEqual(typeof clientId, 'string');
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof expiresAt, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('INSERT INTO authcodes (authCode, clientId, userId, expiresAt) VALUES (?, ?, ?, ?)',
|
||||
[ authCode, clientId, userId, expiresAt ], function (error, result) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS));
|
||||
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function del(authCode, callback) {
|
||||
assert.strictEqual(typeof authCode, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM authcodes WHERE authCode = ?', [ authCode ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function delExpired(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM authcodes WHERE expiresAt <= ?', [ Date.now() ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
return callback(null, result.affectedRows);
|
||||
});
|
||||
}
|
||||
|
||||
function clear(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM authcodes', function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
+37
-42
@@ -1,32 +1,25 @@
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
safe = require('safetydance'),
|
||||
util = require('util');
|
||||
|
||||
var BACKUPS_FIELDS = [ 'id', 'creationTime', 'version', 'type', 'dependsOn', 'state', 'manifestJson', 'format', 'preserveSecs' ];
|
||||
var BACKUPS_FIELDS = [ 'id', 'identifier', 'creationTime', 'packageVersion', 'type', 'dependsOn', 'state', 'manifestJson', 'format', 'preserveSecs', 'encryptionVersion' ];
|
||||
|
||||
exports = module.exports = {
|
||||
add: add,
|
||||
add,
|
||||
|
||||
getByTypeAndStatePaged: getByTypeAndStatePaged,
|
||||
getByTypePaged: getByTypePaged,
|
||||
getByTypePaged,
|
||||
getByIdentifierPaged,
|
||||
getByIdentifierAndStatePaged,
|
||||
|
||||
get: get,
|
||||
del: del,
|
||||
update: update,
|
||||
getByAppIdPaged: getByAppIdPaged,
|
||||
get,
|
||||
del,
|
||||
update,
|
||||
|
||||
_clear: clear,
|
||||
|
||||
BACKUP_TYPE_APP: 'app',
|
||||
BACKUP_TYPE_BOX: 'box',
|
||||
|
||||
BACKUP_STATE_NORMAL: 'normal', // should rename to created to avoid listing in UI?
|
||||
BACKUP_STATE_CREATING: 'creating',
|
||||
BACKUP_STATE_ERROR: 'error'
|
||||
_clear: clear
|
||||
};
|
||||
|
||||
function postProcess(result) {
|
||||
@@ -38,16 +31,16 @@ function postProcess(result) {
|
||||
delete result.manifestJson;
|
||||
}
|
||||
|
||||
function getByTypeAndStatePaged(type, state, page, perPage, callback) {
|
||||
assert(type === exports.BACKUP_TYPE_APP || type === exports.BACKUP_TYPE_BOX);
|
||||
function getByIdentifierAndStatePaged(identifier, state, page, perPage, callback) {
|
||||
assert.strictEqual(typeof identifier, 'string');
|
||||
assert.strictEqual(typeof state, 'string');
|
||||
assert(typeof page === 'number' && page > 0);
|
||||
assert(typeof perPage === 'number' && perPage > 0);
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? AND state = ? ORDER BY creationTime DESC LIMIT ?,?',
|
||||
[ type, state, (page-1)*perPage, perPage ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE identifier = ? AND state = ? ORDER BY creationTime DESC LIMIT ?,?',
|
||||
[ identifier, state, (page-1)*perPage, perPage ], function (error, results) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
results.forEach(function (result) { postProcess(result); });
|
||||
|
||||
@@ -56,14 +49,14 @@ function getByTypeAndStatePaged(type, state, page, perPage, callback) {
|
||||
}
|
||||
|
||||
function getByTypePaged(type, page, perPage, callback) {
|
||||
assert(type === exports.BACKUP_TYPE_APP || type === exports.BACKUP_TYPE_BOX);
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(typeof page === 'number' && page > 0);
|
||||
assert(typeof perPage === 'number' && perPage > 0);
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? ORDER BY creationTime DESC LIMIT ?,?',
|
||||
[ type, (page-1)*perPage, perPage ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
results.forEach(function (result) { postProcess(result); });
|
||||
|
||||
@@ -71,16 +64,15 @@ function getByTypePaged(type, page, perPage, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getByAppIdPaged(page, perPage, appId, callback) {
|
||||
function getByIdentifierPaged(identifier, page, perPage, callback) {
|
||||
assert.strictEqual(typeof identifier, 'string');
|
||||
assert(typeof page === 'number' && page > 0);
|
||||
assert(typeof perPage === 'number' && perPage > 0);
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// box versions (0.93.x and below) used to use appbackup_ prefix
|
||||
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? AND state = ? AND id LIKE ? ORDER BY creationTime DESC LIMIT ?,?',
|
||||
[ exports.BACKUP_TYPE_APP, exports.BACKUP_STATE_NORMAL, '%app%\\_' + appId + '\\_%', (page-1)*perPage, perPage ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE identifier = ? ORDER BY creationTime DESC LIMIT ?,?',
|
||||
[ identifier, (page-1)*perPage, perPage ], function (error, results) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
results.forEach(function (result) { postProcess(result); });
|
||||
|
||||
@@ -94,8 +86,8 @@ function get(id, callback) {
|
||||
|
||||
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE id = ? ORDER BY creationTime DESC',
|
||||
[ 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));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Backup not found'));
|
||||
|
||||
postProcess(result[0]);
|
||||
|
||||
@@ -106,8 +98,11 @@ function get(id, callback) {
|
||||
function add(id, data, callback) {
|
||||
assert(data && typeof data === 'object');
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof data.version, 'string');
|
||||
assert(data.type === exports.BACKUP_TYPE_APP || data.type === exports.BACKUP_TYPE_BOX);
|
||||
assert(data.encryptionVersion === null || typeof data.encryptionVersion === 'number');
|
||||
assert.strictEqual(typeof data.packageVersion, 'string');
|
||||
assert.strictEqual(typeof data.type, 'string');
|
||||
assert.strictEqual(typeof data.identifier, 'string');
|
||||
assert.strictEqual(typeof data.state, 'string');
|
||||
assert(util.isArray(data.dependsOn));
|
||||
assert.strictEqual(typeof data.manifest, 'object');
|
||||
assert.strictEqual(typeof data.format, 'string');
|
||||
@@ -116,11 +111,11 @@ function add(id, data, callback) {
|
||||
var creationTime = data.creationTime || new Date(); // allow tests to set the time
|
||||
var manifestJson = JSON.stringify(data.manifest);
|
||||
|
||||
database.query('INSERT INTO backups (id, version, type, creationTime, state, dependsOn, manifestJson, format) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[ id, data.version, data.type, creationTime, exports.BACKUP_STATE_NORMAL, data.dependsOn.join(','), manifestJson, data.format ],
|
||||
database.query('INSERT INTO backups (id, identifier, encryptionVersion, packageVersion, type, creationTime, state, dependsOn, manifestJson, format) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[ id, data.identifier, data.encryptionVersion, data.packageVersion, data.type, creationTime, data.state, data.dependsOn.join(','), manifestJson, data.format ],
|
||||
function (error) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -139,8 +134,8 @@ function update(id, backup, callback) {
|
||||
values.push(id);
|
||||
|
||||
database.query('UPDATE backups SET ' + fields.join(', ') + ' WHERE id = ?', values, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.NOT_FOUND, 'Backup not found'));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -150,7 +145,7 @@ function clear(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('TRUNCATE TABLE backups', [], function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
@@ -160,7 +155,7 @@ function del(id, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM backups WHERE id=?', [ id ], function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
+661
-356
File diff suppressed because it is too large
Load Diff
+103
@@ -0,0 +1,103 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
exports = module.exports = BoxError;
|
||||
|
||||
function BoxError(reason, errorOrMessage, details) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
assert(typeof details === 'object' || typeof details === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
this.details = details || {};
|
||||
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else { // error object
|
||||
this.message = errorOrMessage.message;
|
||||
this.nestedError = errorOrMessage;
|
||||
_.extend(this.details, errorOrMessage); // copy enumerable properies
|
||||
}
|
||||
}
|
||||
util.inherits(BoxError, Error);
|
||||
BoxError.ACCESS_DENIED = 'Access Denied';
|
||||
BoxError.ADDONS_ERROR = 'Addons Error';
|
||||
BoxError.ALREADY_EXISTS = 'Already Exists';
|
||||
BoxError.BAD_FIELD = 'Bad Field';
|
||||
BoxError.BAD_STATE = 'Bad State';
|
||||
BoxError.BUSY = 'Busy';
|
||||
BoxError.COLLECTD_ERROR = 'Collectd Error';
|
||||
BoxError.CONFLICT = 'Conflict';
|
||||
BoxError.CRYPTO_ERROR = 'Crypto Error';
|
||||
BoxError.DATABASE_ERROR = 'Database Error';
|
||||
BoxError.DNS_ERROR = 'DNS Error';
|
||||
BoxError.DOCKER_ERROR = 'Docker Error';
|
||||
BoxError.EXTERNAL_ERROR = 'External Error'; // use this for external API errors
|
||||
BoxError.FEATURE_DISABLED = 'Feature Disabled';
|
||||
BoxError.FS_ERROR = 'FileSystem Error';
|
||||
BoxError.INACTIVE = 'Inactive';
|
||||
BoxError.INTERNAL_ERROR = 'Internal Error';
|
||||
BoxError.INVALID_CREDENTIALS = 'Invalid Credentials';
|
||||
BoxError.IPTABLES_ERROR = 'IPTables Error';
|
||||
BoxError.LICENSE_ERROR = 'License Error';
|
||||
BoxError.LOGROTATE_ERROR = 'Logrotate Error';
|
||||
BoxError.MAIL_ERROR = 'Mail Error';
|
||||
BoxError.NETWORK_ERROR = 'Network Error';
|
||||
BoxError.NGINX_ERROR = 'Nginx Error';
|
||||
BoxError.NOT_FOUND = 'Not found';
|
||||
BoxError.NOT_IMPLEMENTED = 'Not implemented';
|
||||
BoxError.NOT_SIGNED = 'Not Signed';
|
||||
BoxError.OPENSSL_ERROR = 'OpenSSL Error';
|
||||
BoxError.PLAN_LIMIT = 'Plan Limit';
|
||||
BoxError.SPAWN_ERROR = 'Spawn Error';
|
||||
BoxError.TASK_ERROR = 'Task Error';
|
||||
BoxError.TIMEOUT = 'Timeout';
|
||||
BoxError.TRY_AGAIN = 'Try Again';
|
||||
|
||||
BoxError.prototype.toPlainObject = function () {
|
||||
return _.extend({}, { message: this.message, reason: this.reason }, this.details);
|
||||
};
|
||||
|
||||
// this is a class method for now in case error is not a BoxError
|
||||
BoxError.toHttpError = function (error) {
|
||||
switch (error.reason) {
|
||||
case BoxError.BAD_FIELD:
|
||||
return new HttpError(400, error);
|
||||
case BoxError.LICENSE_ERROR:
|
||||
return new HttpError(402, error);
|
||||
case BoxError.NOT_FOUND:
|
||||
return new HttpError(404, error);
|
||||
case BoxError.FEATURE_DISABLED:
|
||||
return new HttpError(405, error);
|
||||
case BoxError.ALREADY_EXISTS:
|
||||
case BoxError.BAD_STATE:
|
||||
case BoxError.CONFLICT:
|
||||
return new HttpError(409, error);
|
||||
case BoxError.INVALID_CREDENTIALS:
|
||||
return new HttpError(412, error);
|
||||
case BoxError.EXTERNAL_ERROR:
|
||||
case BoxError.NETWORK_ERROR:
|
||||
case BoxError.FS_ERROR:
|
||||
case BoxError.MAIL_ERROR:
|
||||
case BoxError.DOCKER_ERROR:
|
||||
case BoxError.ADDONS_ERROR:
|
||||
case BoxError.IPTABLES_ERROR:
|
||||
return new HttpError(424, error);
|
||||
case BoxError.DATABASE_ERROR:
|
||||
case BoxError.INTERNAL_ERROR:
|
||||
default:
|
||||
return new HttpError(500, error);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
renderFooter
|
||||
};
|
||||
|
||||
const assert = require('assert'),
|
||||
constants = require('./constants.js');
|
||||
|
||||
function renderFooter(footer) {
|
||||
assert.strictEqual(typeof footer, 'string');
|
||||
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
return footer.replace(/%YEAR%/g, year)
|
||||
.replace(/%VERSION%/g, constants.VERSION);
|
||||
}
|
||||
|
||||
+96
-104
@@ -2,14 +2,15 @@
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
BoxError = require('../boxerror.js'),
|
||||
crypto = require('crypto'),
|
||||
debug = require('debug')('box:cert/acme2'),
|
||||
domains = require('../domains.js'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
paths = require('../paths.js'),
|
||||
request = require('request'),
|
||||
safe = require('safetydance'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
@@ -24,31 +25,6 @@ exports = module.exports = {
|
||||
_getChallengeSubdomain: getChallengeSubdomain
|
||||
};
|
||||
|
||||
function Acme2Error(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(Acme2Error, Error);
|
||||
Acme2Error.INTERNAL_ERROR = 'Internal Error';
|
||||
Acme2Error.EXTERNAL_ERROR = 'External Error';
|
||||
Acme2Error.ALREADY_EXISTS = 'Already Exists';
|
||||
Acme2Error.NOT_COMPLETED = 'Not Completed';
|
||||
Acme2Error.FORBIDDEN = 'Forbidden';
|
||||
|
||||
// http://jose.readthedocs.org/en/latest/
|
||||
// https://www.ietf.org/proceedings/92/slides/slides-92-acme-1.pdf
|
||||
// https://community.letsencrypt.org/t/list-of-client-implementations/2103
|
||||
@@ -65,15 +41,6 @@ function Acme2(options) {
|
||||
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, '');
|
||||
@@ -120,8 +87,12 @@ Acme2.prototype.sendSignedRequest = function (url, payload, callback) {
|
||||
|
||||
var payload64 = b64(payload);
|
||||
|
||||
this.getNonce(function (error, nonce) {
|
||||
if (error) return callback(error);
|
||||
request.get(this.directory.newNonce, { json: true, timeout: 30000 }, function (error, response) {
|
||||
if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error sending signed request: ${error.message}`));
|
||||
if (response.statusCode !== 204) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response code when fetching nonce : ' + response.statusCode));
|
||||
|
||||
const nonce = response.headers['Replay-Nonce'.toLowerCase()];
|
||||
if (!nonce) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'No nonce in response'));
|
||||
|
||||
debug('sendSignedRequest: using nonce %s for url %s', nonce, url);
|
||||
|
||||
@@ -137,14 +108,23 @@ Acme2.prototype.sendSignedRequest = function (url, payload, callback) {
|
||||
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
|
||||
request.post(url, { headers: { 'Content-Type': 'application/jose+json', 'User-Agent': 'acme-cloudron' }, body: JSON.stringify(data), timeout: 30000 }, function (error, response) {
|
||||
if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error sending signed request: ${error.message}`)); // network error
|
||||
|
||||
callback(null, res);
|
||||
// we don't set json: true in request because it ends up mangling the content-type
|
||||
// we don't set json: true in request because it ends up mangling the content-type
|
||||
if (response.headers['content-type'] === 'application/json') response.body = safe.JSON.parse(response.body);
|
||||
|
||||
callback(null, response);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// https://tools.ietf.org/html/rfc8555#section-6.3
|
||||
Acme2.prototype.postAsGet = function (url, callback) {
|
||||
this.sendSignedRequest(url, '', callback);
|
||||
};
|
||||
|
||||
Acme2.prototype.updateContact = function (registrationUri, callback) {
|
||||
assert.strictEqual(typeof registrationUri, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -158,8 +138,8 @@ Acme2.prototype.updateContact = function (registrationUri, callback) {
|
||||
|
||||
const that = this;
|
||||
this.sendSignedRequest(registrationUri, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
|
||||
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to update contact. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
if (error) return callback(error);
|
||||
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to update contact. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
debug(`updateContact: contact of user updated to ${that.email}`);
|
||||
|
||||
@@ -178,9 +158,9 @@ Acme2.prototype.registerUser = function (callback) {
|
||||
|
||||
var that = this;
|
||||
this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when registering new account: ' + error.message));
|
||||
if (error) return callback(error);
|
||||
// 200 if already exists. 201 for new accounts
|
||||
if (result.statusCode !== 200 && result.statusCode !== 201) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to register new account. Expecting 200 or 201, got %s %s', result.statusCode, result.text)));
|
||||
if (result.statusCode !== 200 && result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to register new account. Expecting 200 or 201, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
debug(`registerUser: user registered keyid: ${result.headers.location}`);
|
||||
|
||||
@@ -204,17 +184,17 @@ Acme2.prototype.newOrder = function (domain, callback) {
|
||||
debug('newOrder: %s', domain);
|
||||
|
||||
this.sendSignedRequest(this.directory.newOrder, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when registering domain: ' + error.message));
|
||||
if (result.statusCode === 403) return callback(new Acme2Error(Acme2Error.FORBIDDEN, result.body.detail));
|
||||
if (result.statusCode !== 201) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
if (error) return callback(error);
|
||||
if (result.statusCode === 403) return callback(new BoxError(BoxError.ACCESS_DENIED, `Forbidden sending new order: ${result.body.detail}`));
|
||||
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to send new order. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
debug('newOrder: created order %s %j', domain, result.body);
|
||||
|
||||
const order = result.body, orderUrl = result.headers.location;
|
||||
|
||||
if (!Array.isArray(order.authorizations)) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'invalid authorizations in order'));
|
||||
if (typeof order.finalize !== 'string') return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'invalid finalize in order'));
|
||||
if (typeof orderUrl !== 'string') return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'invalid order location in order header'));
|
||||
if (!Array.isArray(order.authorizations)) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'invalid authorizations in order'));
|
||||
if (typeof order.finalize !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'invalid finalize in order'));
|
||||
if (typeof orderUrl !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'invalid order location in order header'));
|
||||
|
||||
callback(null, order, orderUrl);
|
||||
});
|
||||
@@ -225,25 +205,26 @@ Acme2.prototype.waitForOrder = function (orderUrl, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`waitForOrder: ${orderUrl}`);
|
||||
const that = this;
|
||||
|
||||
async.retry({ times: 15, interval: 20000 }, function (retryCallback) {
|
||||
debug('waitForOrder: getting status');
|
||||
|
||||
superagent.get(orderUrl).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) {
|
||||
that.postAsGet(orderUrl, function (error, result) {
|
||||
if (error) {
|
||||
debug('waitForOrder: network error getting uri %s', orderUrl);
|
||||
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message)); // network error
|
||||
return retryCallback(error);
|
||||
}
|
||||
if (result.statusCode !== 200) {
|
||||
debug('waitForOrder: invalid response code getting uri %s', result.statusCode);
|
||||
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
|
||||
return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
|
||||
}
|
||||
|
||||
debug('waitForOrder: status is "%s %j', result.body.status, result.body);
|
||||
|
||||
if (result.body.status === 'pending' || result.body.status === 'processing') return retryCallback(new Acme2Error(Acme2Error.NOT_COMPLETED));
|
||||
if (result.body.status === 'pending' || result.body.status === 'processing') return retryCallback(new BoxError(BoxError.TRY_AGAIN, `Request is in ${result.body.status} state`));
|
||||
else if (result.body.status === 'valid' && result.body.certificate) return retryCallback(null, result.body.certificate);
|
||||
else return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Unexpected status or invalid response: ' + result.body));
|
||||
else return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, 'Unexpected status or invalid response: ' + result.body));
|
||||
});
|
||||
}, callback);
|
||||
};
|
||||
@@ -277,8 +258,8 @@ Acme2.prototype.notifyChallengeReady = function (challenge, callback) {
|
||||
};
|
||||
|
||||
this.sendSignedRequest(challenge.url, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when notifying challenge: ' + error.message));
|
||||
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
if (error) return callback(error);
|
||||
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
callback();
|
||||
});
|
||||
@@ -289,25 +270,26 @@ Acme2.prototype.waitForChallenge = function (challenge, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('waitingForChallenge: %j', challenge);
|
||||
const that = this;
|
||||
|
||||
async.retry({ times: 15, interval: 20000 }, function (retryCallback) {
|
||||
debug('waitingForChallenge: getting status');
|
||||
|
||||
superagent.get(challenge.url).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) {
|
||||
that.postAsGet(challenge.url, function (error, result) {
|
||||
if (error) {
|
||||
debug('waitForChallenge: network error getting uri %s', challenge.url);
|
||||
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message)); // network error
|
||||
return retryCallback(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));
|
||||
return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
|
||||
}
|
||||
|
||||
debug('waitForChallenge: status is "%s %j', result.body.status, result.body);
|
||||
debug('waitForChallenge: status is "%s" %j', result.body.status, result.body);
|
||||
|
||||
if (result.body.status === 'pending') return retryCallback(new Acme2Error(Acme2Error.NOT_COMPLETED));
|
||||
if (result.body.status === 'pending') return retryCallback(new BoxError(BoxError.TRY_AGAIN));
|
||||
else if (result.body.status === 'valid') return retryCallback();
|
||||
else return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Unexpected status: ' + result.body.status));
|
||||
else return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, 'Unexpected status: ' + result.body.status));
|
||||
});
|
||||
}, function retryFinished(error) {
|
||||
// async.retry will pass 'undefined' as second arg making it unusable with async.waterfall()
|
||||
@@ -329,9 +311,9 @@ Acme2.prototype.signCertificate = function (domain, finalizationUrl, csrDer, cal
|
||||
debug('signCertificate: sending sign request');
|
||||
|
||||
this.sendSignedRequest(finalizationUrl, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when signing certificate: ' + error.message));
|
||||
if (error) return callback(error);
|
||||
// 429 means we reached the cert limit for this domain
|
||||
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
@@ -350,16 +332,16 @@ Acme2.prototype.createKeyAndCsr = function (hostname, callback) {
|
||||
// in some old releases, csr file was corrupt. so always regenerate it
|
||||
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
|
||||
} else {
|
||||
var key = safe.child_process.execSync('openssl genrsa 4096');
|
||||
if (!key) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
|
||||
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
|
||||
var key = safe.child_process.execSync('openssl ecparam -genkey -name secp384r1'); // openssl ecparam -list_curves
|
||||
if (!key) return callback(new BoxError(BoxError.OPENSSL_ERROR, safe.error));
|
||||
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new BoxError(BoxError.FS_ERROR, safe.error));
|
||||
|
||||
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
|
||||
}
|
||||
|
||||
var csrDer = safe.child_process.execSync(`openssl req -new -key ${privateKeyFile} -outform DER -subj /CN=${hostname}`);
|
||||
if (!csrDer) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
|
||||
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error)); // bookkeeping
|
||||
if (!csrDer) return callback(new BoxError(BoxError.OPENSSL_ERROR, safe.error));
|
||||
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new BoxError(BoxError.FS_ERROR, safe.error)); // bookkeeping
|
||||
|
||||
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFile);
|
||||
|
||||
@@ -372,26 +354,27 @@ Acme2.prototype.downloadCertificate = function (hostname, certUrl, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var outdir = paths.APP_CERTS_DIR;
|
||||
const that = this;
|
||||
|
||||
superagent.get(certUrl).buffer().parse(function (res, done) {
|
||||
var data = [ ];
|
||||
res.on('data', function(chunk) { data.push(chunk); });
|
||||
res.on('end', function () { res.text = Buffer.concat(data); done(); });
|
||||
}).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when downloading certificate'));
|
||||
if (result.statusCode === 202) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, 'Retry not implemented yet'));
|
||||
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
|
||||
debug('downloadCertificate: downloading certificate');
|
||||
|
||||
const fullChainPem = result.text;
|
||||
that.postAsGet(certUrl, function (error, result) {
|
||||
if (error) return retryCallback(new BoxError(BoxError.NETWORK_ERROR, `Network error when downloading certificate: ${error.message}`));
|
||||
if (result.statusCode === 202) return retryCallback(new BoxError(BoxError.TRY_AGAIN, 'Retry downloading certificate'));
|
||||
if (result.statusCode !== 200) return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
const certName = hostname.replace('*.', '_.');
|
||||
var certificateFile = path.join(outdir, `${certName}.cert`);
|
||||
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
|
||||
const fullChainPem = result.body; // buffer
|
||||
|
||||
debug('downloadCertificate: cert file for %s saved at %s', hostname, certificateFile);
|
||||
const certName = hostname.replace('*.', '_.');
|
||||
var certificateFile = path.join(outdir, `${certName}.cert`);
|
||||
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return retryCallback(new BoxError(BoxError.FS_ERROR, safe.error));
|
||||
|
||||
callback();
|
||||
});
|
||||
debug('downloadCertificate: cert file for %s saved at %s', hostname, certificateFile);
|
||||
|
||||
retryCallback(null);
|
||||
});
|
||||
}, callback);
|
||||
};
|
||||
|
||||
Acme2.prototype.prepareHttpChallenge = function (hostname, domain, authorization, callback) {
|
||||
@@ -402,7 +385,7 @@ Acme2.prototype.prepareHttpChallenge = function (hostname, domain, authorization
|
||||
|
||||
debug('acmeFlow: challenges: %j', authorization);
|
||||
let httpChallenges = authorization.challenges.filter(function(x) { return x.type === 'http-01'; });
|
||||
if (httpChallenges.length === 0) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'no http challenges'));
|
||||
if (httpChallenges.length === 0) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'no http challenges'));
|
||||
let challenge = httpChallenges[0];
|
||||
|
||||
debug('prepareHttpChallenge: preparing for challenge %j', challenge);
|
||||
@@ -412,7 +395,7 @@ Acme2.prototype.prepareHttpChallenge = function (hostname, domain, authorization
|
||||
debug('prepareHttpChallenge: writing %s to %s', keyAuthorization, path.join(paths.ACME_CHALLENGES_DIR, challenge.token));
|
||||
|
||||
fs.writeFile(path.join(paths.ACME_CHALLENGES_DIR, challenge.token), keyAuthorization, function (error) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.FS_ERROR, error));
|
||||
|
||||
callback(null, challenge);
|
||||
});
|
||||
@@ -454,7 +437,7 @@ Acme2.prototype.prepareDnsChallenge = function (hostname, domain, authorization,
|
||||
|
||||
debug('acmeFlow: challenges: %j', authorization);
|
||||
let dnsChallenges = authorization.challenges.filter(function(x) { return x.type === 'dns-01'; });
|
||||
if (dnsChallenges.length === 0) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'no dns challenges'));
|
||||
if (dnsChallenges.length === 0) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'no dns challenges'));
|
||||
let challenge = dnsChallenges[0];
|
||||
|
||||
const keyAuthorization = this.getKeyAuthorization(challenge.token);
|
||||
@@ -467,10 +450,10 @@ Acme2.prototype.prepareDnsChallenge = function (hostname, domain, authorization,
|
||||
debug(`prepareDnsChallenge: update ${challengeSubdomain} with ${txtValue}`);
|
||||
|
||||
domains.upsertDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message));
|
||||
if (error) return callback(error);
|
||||
|
||||
domains.waitForDnsRecord(challengeSubdomain, domain, 'TXT', txtValue, { interval: 5000, times: 200 }, function (error) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message));
|
||||
domains.waitForDnsRecord(challengeSubdomain, domain, 'TXT', txtValue, { times: 200 }, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, challenge);
|
||||
});
|
||||
@@ -493,7 +476,7 @@ Acme2.prototype.cleanupDnsChallenge = function (hostname, domain, challenge, cal
|
||||
debug(`cleanupDnsChallenge: remove ${challengeSubdomain} with ${txtValue}`);
|
||||
|
||||
domains.removeDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error));
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -505,10 +488,12 @@ Acme2.prototype.prepareChallenge = function (hostname, domain, authorizationUrl,
|
||||
assert.strictEqual(typeof authorizationUrl, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`prepareChallenge: http: ${this.performHttpAuthorization}`);
|
||||
|
||||
const that = this;
|
||||
superagent.get(authorizationUrl).timeout(30 * 1000).end(function (error, response) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (response.statusCode !== 200) return callback(new Error('Invalid response code getting authorization : ' + response.statusCode));
|
||||
this.postAsGet(authorizationUrl, function (error, response) {
|
||||
if (error) return callback(error);
|
||||
if (response.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response code getting authorization : ' + response.statusCode));
|
||||
|
||||
const authorization = response.body;
|
||||
|
||||
@@ -526,6 +511,8 @@ Acme2.prototype.cleanupChallenge = function (hostname, domain, challenge, callba
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`cleanupChallenge: http: ${this.performHttpAuthorization}`);
|
||||
|
||||
if (this.performHttpAuthorization) {
|
||||
this.cleanupHttpChallenge(hostname, domain, challenge, callback);
|
||||
} else {
|
||||
@@ -541,7 +528,7 @@ Acme2.prototype.acmeFlow = function (hostname, domain, callback) {
|
||||
if (!fs.existsSync(paths.ACME_ACCOUNT_KEY_FILE)) {
|
||||
debug('getCertificate: generating acme account key on first run');
|
||||
this.accountKeyPem = safe.child_process.execSync('openssl genrsa 4096');
|
||||
if (!this.accountKeyPem) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
|
||||
if (!this.accountKeyPem) return callback(new BoxError(BoxError.OPENSSL_ERROR, safe.error));
|
||||
|
||||
safe.fs.writeFileSync(paths.ACME_ACCOUNT_KEY_FILE, this.accountKeyPem);
|
||||
} else {
|
||||
@@ -585,13 +572,13 @@ Acme2.prototype.acmeFlow = function (hostname, domain, 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));
|
||||
request.get(this.caDirectory, { json: true, timeout: 30000 }, function (error, response) {
|
||||
if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error getting directory: ${error.message}`));
|
||||
if (response.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response code when fetching directory : ' + response.statusCode));
|
||||
|
||||
if (typeof response.body.newNonce !== 'string' ||
|
||||
typeof response.body.newOrder !== 'string' ||
|
||||
typeof response.body.newAccount !== 'string') return callback(new Error(`Invalid response body : ${response.body}`));
|
||||
typeof response.body.newAccount !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Invalid response body : ${response.body}`));
|
||||
|
||||
that.directory = response.body;
|
||||
|
||||
@@ -631,6 +618,11 @@ function getCertificate(hostname, domain, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var acme = new Acme2(options || { });
|
||||
acme.getCertificate(hostname, domain, callback);
|
||||
let attempt = 1;
|
||||
async.retry({ times: 3, interval: 0 }, function (retryCallback) {
|
||||
debug(`getCertificate: attempt ${attempt++}`);
|
||||
|
||||
let acme = new Acme2(options || { });
|
||||
acme.getCertificate(hostname, domain, retryCallback);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
getCertificate: getCertificate,
|
||||
|
||||
// testing
|
||||
_name: 'caas'
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
debug = require('debug')('box:cert/caas.js');
|
||||
|
||||
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', hostname);
|
||||
|
||||
return callback(null, '', '');
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user